mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
incr: complete stock ledger
This commit is contained in:
parent
a277c748fb
commit
a9fd590512
@ -33,7 +33,7 @@ export function format(
|
||||
}
|
||||
|
||||
if (field.fieldtype === FieldTypeEnum.Datetime) {
|
||||
return formatDate(value, fyo);
|
||||
return formatDatetime(value, fyo);
|
||||
}
|
||||
|
||||
if (field.fieldtype === FieldTypeEnum.Check) {
|
||||
@ -47,18 +47,33 @@ export function format(
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function toDatetime(value: DocValue) {
|
||||
if (typeof value === 'string') {
|
||||
return DateTime.fromISO(value);
|
||||
} else if (value instanceof Date) {
|
||||
return DateTime.fromJSDate(value);
|
||||
} else {
|
||||
return DateTime.fromSeconds(value as number);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDatetime(value: DocValue, fyo: Fyo): string {
|
||||
const dateFormat =
|
||||
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;
|
||||
const formattedDatetime = toDatetime(value).toFormat(`${dateFormat} HH:mm:ss`);
|
||||
|
||||
if (value === 'Invalid DateTime') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return formattedDatetime;
|
||||
}
|
||||
|
||||
function formatDate(value: DocValue, fyo: Fyo): string {
|
||||
const dateFormat =
|
||||
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;
|
||||
|
||||
let dateValue: DateTime;
|
||||
if (typeof value === 'string') {
|
||||
dateValue = DateTime.fromISO(value);
|
||||
} else if (value instanceof Date) {
|
||||
dateValue = DateTime.fromJSDate(value);
|
||||
} else {
|
||||
dateValue = DateTime.fromSeconds(value as number);
|
||||
}
|
||||
const dateValue: DateTime = toDatetime(value);
|
||||
|
||||
const formattedDate = dateValue.toFormat(dateFormat);
|
||||
if (value === 'Invalid DateTime') {
|
||||
|
@ -1,72 +1,335 @@
|
||||
import { t } from 'fyo';
|
||||
import { RawValueMap } from 'fyo/core/types';
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { Action } from 'fyo/model/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { DateTime } from 'luxon';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import getCommonExportActions from 'reports/commonExporter';
|
||||
import { Report } from 'reports/Report';
|
||||
import { ColumnField, ReportCell, ReportData, ReportRow } from 'reports/types';
|
||||
import { Field, RawValue } from 'schemas/types';
|
||||
import { Field } from 'schemas/types';
|
||||
import { isNumeric } from 'src/utils';
|
||||
import { getRawStockLedgerEntries, getStockLedgerEntries } from './helpers';
|
||||
import { ComputedStockLedgerEntry } from './types';
|
||||
|
||||
export class StockLedger extends Report {
|
||||
static title = t`Stock Ledger`;
|
||||
static reportName = 'stock-ledger';
|
||||
usePagination: boolean = true;
|
||||
|
||||
_rawData?: ComputedStockLedgerEntry[];
|
||||
loading: boolean = false;
|
||||
shouldRefresh: boolean = false;
|
||||
|
||||
item?: string;
|
||||
location?: string;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
ascending?: boolean;
|
||||
groupBy: 'none' | 'item' | 'location' = 'none';
|
||||
|
||||
constructor(fyo: Fyo) {
|
||||
super(fyo);
|
||||
this._setObservers();
|
||||
}
|
||||
|
||||
async setDefaultFilters() {
|
||||
if (!this.toDate) {
|
||||
this.toDate = DateTime.now().plus({ days: 1 }).toISODate();
|
||||
this.fromDate = DateTime.now().minus({ years: 1 }).toISODate();
|
||||
}
|
||||
}
|
||||
|
||||
async setReportData(
|
||||
filter?: string | undefined,
|
||||
force?: boolean | undefined
|
||||
): Promise<void> {
|
||||
this.loading = true;
|
||||
this.reportData = await this._getReportData();
|
||||
this.reportData = await this._getReportData(force);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async _getReportData(): Promise<ReportData> {
|
||||
const columns = this.getColumns();
|
||||
const fieldnames = columns.map(({ fieldname }) => fieldname);
|
||||
const rawData = await this.fyo.db.getAllRaw(
|
||||
ModelNameEnum.StockLedgerEntry,
|
||||
{
|
||||
fields: fieldnames,
|
||||
}
|
||||
);
|
||||
|
||||
return this.convertRawDataToReportData(rawData, columns);
|
||||
}
|
||||
|
||||
convertRawDataToReportData(
|
||||
rawData: RawValueMap[],
|
||||
fields: Field[]
|
||||
): ReportData {
|
||||
const reportData: ReportData = [];
|
||||
for (const row of rawData) {
|
||||
reportData.push(this.convertRawDataRowToReportRow(row, fields));
|
||||
async _getReportData(force?: boolean): Promise<ReportData> {
|
||||
if (this.shouldRefresh || force || !this._rawData?.length) {
|
||||
await this._setRawData();
|
||||
}
|
||||
return reportData;
|
||||
|
||||
const rawData = cloneDeep(this._rawData);
|
||||
if (!rawData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filtered = this._getFilteredRawData(rawData);
|
||||
const grouped = this._getGroupedRawData(filtered);
|
||||
|
||||
return grouped.map((row) => this._convertRawDataRowToReportRow(row));
|
||||
}
|
||||
|
||||
convertRawDataRowToReportRow(row: RawValueMap, fields: Field[]): ReportRow {
|
||||
async _setRawData() {
|
||||
const rawSLEs = await getRawStockLedgerEntries(this.fyo);
|
||||
this._rawData = getStockLedgerEntries(rawSLEs);
|
||||
}
|
||||
|
||||
_getFilteredRawData(rawData: ComputedStockLedgerEntry[]) {
|
||||
const filteredRawData: ComputedStockLedgerEntry[] = [];
|
||||
if (!rawData.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fromDate = this.fromDate ? new Date(this.fromDate) : null;
|
||||
const toDate = this.toDate ? new Date(this.toDate) : null;
|
||||
|
||||
if (!this.ascending) {
|
||||
rawData.reverse();
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for (const idx in rawData) {
|
||||
const row = rawData[idx];
|
||||
if (this.item && row.item !== this.item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.location && row.location !== this.location) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (toDate && row.date > toDate) {
|
||||
console.log('here');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fromDate && row.date < fromDate) {
|
||||
console.log('here');
|
||||
continue;
|
||||
}
|
||||
|
||||
row.name = ++i;
|
||||
filteredRawData.push(row);
|
||||
}
|
||||
|
||||
return filteredRawData;
|
||||
}
|
||||
|
||||
_getGroupedRawData(rawData: ComputedStockLedgerEntry[]) {
|
||||
const groupBy = this.groupBy;
|
||||
if (groupBy === 'none') {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
const groups: Map<string, ComputedStockLedgerEntry[]> = new Map();
|
||||
for (const row of rawData) {
|
||||
const key = row[groupBy];
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, []);
|
||||
}
|
||||
|
||||
groups.get(key)?.push(row);
|
||||
}
|
||||
|
||||
const groupedRawData: (ComputedStockLedgerEntry | { name: null })[] = [];
|
||||
let i = 0;
|
||||
for (const key of groups.keys()) {
|
||||
for (const row of groups.get(key) ?? []) {
|
||||
row.name = ++i;
|
||||
groupedRawData.push(row);
|
||||
}
|
||||
|
||||
groupedRawData.push({ name: null });
|
||||
}
|
||||
|
||||
if (groupedRawData.at(-1)?.name === null) {
|
||||
groupedRawData.pop();
|
||||
}
|
||||
|
||||
return groupedRawData;
|
||||
}
|
||||
|
||||
_convertRawDataRowToReportRow(
|
||||
row: ComputedStockLedgerEntry | { name: null }
|
||||
): ReportRow {
|
||||
const cells: ReportCell[] = [];
|
||||
for (const { fieldname, fieldtype } of fields) {
|
||||
const rawValue = row[fieldname] as RawValue;
|
||||
const columns = this.getColumns();
|
||||
|
||||
if (row.name === null) {
|
||||
return {
|
||||
isEmpty: true,
|
||||
cells: columns.map((c) => ({
|
||||
rawValue: '',
|
||||
value: '',
|
||||
width: c.width ?? 1,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
for (const col of columns) {
|
||||
const fieldname = col.fieldname as keyof ComputedStockLedgerEntry;
|
||||
const fieldtype = col.fieldtype;
|
||||
|
||||
const rawValue = row[fieldname];
|
||||
const value = this.fyo.format(rawValue, fieldtype);
|
||||
const align = isNumeric(fieldtype) ? 'right' : 'left';
|
||||
const isColoured =
|
||||
fieldname === 'quantity' || fieldname === 'valueChange';
|
||||
|
||||
cells.push({ rawValue, value, align });
|
||||
let color: 'red' | 'green' | undefined = undefined;
|
||||
if (isColoured && rawValue > 0) {
|
||||
color = 'green';
|
||||
} else if (isColoured && rawValue < 0) {
|
||||
color = 'red';
|
||||
}
|
||||
|
||||
cells.push({ rawValue, value, align, color, width: col.width });
|
||||
}
|
||||
|
||||
return { cells };
|
||||
}
|
||||
|
||||
getColumns(): ColumnField[] {
|
||||
return (
|
||||
this.fyo.schemaMap[ModelNameEnum.StockLedgerEntry]?.fields ?? []
|
||||
).filter((f) => !f.meta);
|
||||
_setObservers() {
|
||||
const listener = () => (this.shouldRefresh = true);
|
||||
|
||||
this.fyo.doc.observer.on(
|
||||
`sync:${ModelNameEnum.StockLedgerEntry}`,
|
||||
listener
|
||||
);
|
||||
|
||||
this.fyo.doc.observer.on(
|
||||
`delete:${ModelNameEnum.StockLedgerEntry}`,
|
||||
listener
|
||||
);
|
||||
}
|
||||
|
||||
getFilters(): Field[] | Promise<Field[]> {
|
||||
return [];
|
||||
getColumns(): ColumnField[] {
|
||||
return [
|
||||
{
|
||||
fieldname: 'name',
|
||||
label: '#',
|
||||
fieldtype: 'Int',
|
||||
width: 0.5,
|
||||
},
|
||||
{
|
||||
fieldname: 'date',
|
||||
label: 'Date',
|
||||
fieldtype: 'Datetime',
|
||||
width: 1.25,
|
||||
},
|
||||
{
|
||||
fieldname: 'item',
|
||||
label: 'Item',
|
||||
fieldtype: 'Link',
|
||||
},
|
||||
{
|
||||
fieldname: 'location',
|
||||
label: 'Location',
|
||||
fieldtype: 'Link',
|
||||
},
|
||||
{
|
||||
fieldname: 'quantity',
|
||||
label: 'Quantity',
|
||||
fieldtype: 'Float',
|
||||
},
|
||||
{
|
||||
fieldname: 'balanceQuantity',
|
||||
label: 'Balance Qty.',
|
||||
fieldtype: 'Float',
|
||||
},
|
||||
{
|
||||
fieldname: 'incomingRate',
|
||||
label: 'Incoming rate',
|
||||
fieldtype: 'Currency',
|
||||
},
|
||||
{
|
||||
fieldname: 'valuationRate',
|
||||
label: 'Valuation rate',
|
||||
fieldtype: 'Currency',
|
||||
},
|
||||
{
|
||||
fieldname: 'balanceValue',
|
||||
label: 'Balance Value',
|
||||
fieldtype: 'Currency',
|
||||
},
|
||||
{
|
||||
fieldname: 'valueChange',
|
||||
label: 'Value Change',
|
||||
fieldtype: 'Currency',
|
||||
},
|
||||
{
|
||||
fieldname: 'referenceName',
|
||||
label: 'Ref. Name',
|
||||
fieldtype: 'DynamicLink',
|
||||
},
|
||||
{
|
||||
fieldname: 'referenceType',
|
||||
label: 'Ref. Type',
|
||||
fieldtype: 'Data',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getFilters(): Field[] {
|
||||
return [
|
||||
/*
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
options: [
|
||||
{ label: t`All`, value: 'All' },
|
||||
{ label: t`Stock Movements`, value: 'StockMovement' },
|
||||
],
|
||||
|
||||
label: t`Ref Type`,
|
||||
fieldname: 'referenceType',
|
||||
placeholder: t`Ref Type`,
|
||||
},
|
||||
{
|
||||
fieldtype: 'DynamicLink',
|
||||
label: t`Ref Name`,
|
||||
references: 'referenceType',
|
||||
placeholder: t`Ref Name`,
|
||||
emptyMessage: t`Change Ref Type`,
|
||||
fieldname: 'referenceName',
|
||||
},
|
||||
*/
|
||||
{
|
||||
fieldtype: 'Link',
|
||||
target: 'Item',
|
||||
placeholder: t`Item`,
|
||||
label: t`Item`,
|
||||
fieldname: 'item',
|
||||
},
|
||||
{
|
||||
fieldtype: 'Link',
|
||||
target: 'Location',
|
||||
placeholder: t`Location`,
|
||||
label: t`Location`,
|
||||
fieldname: 'location',
|
||||
},
|
||||
{
|
||||
fieldtype: 'Date',
|
||||
placeholder: t`From Date`,
|
||||
label: t`From Date`,
|
||||
fieldname: 'fromDate',
|
||||
},
|
||||
{
|
||||
fieldtype: 'Date',
|
||||
placeholder: t`To Date`,
|
||||
label: t`To Date`,
|
||||
fieldname: 'toDate',
|
||||
},
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
label: t`Group By`,
|
||||
fieldname: 'groupBy',
|
||||
options: [
|
||||
{ label: t`None`, value: 'none' },
|
||||
{ label: t`Item`, value: 'item' },
|
||||
{ label: t`Location`, value: 'location' },
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldtype: 'Check',
|
||||
label: t`Ascending Order`,
|
||||
fieldname: 'ascending',
|
||||
},
|
||||
] as Field[];
|
||||
}
|
||||
|
||||
getActions(): Action[] {
|
||||
|
91
reports/inventory/helpers.ts
Normal file
91
reports/inventory/helpers.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { StockQueue } from 'models/inventory/stockQueue';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { safeParseFloat, safeParseInt } from 'utils/index';
|
||||
import { ComputedStockLedgerEntry, RawStockLedgerEntry } from './types';
|
||||
|
||||
export async function getRawStockLedgerEntries(fyo: Fyo) {
|
||||
const fieldnames = [
|
||||
'name',
|
||||
'date',
|
||||
'item',
|
||||
'rate',
|
||||
'quantity',
|
||||
'location',
|
||||
'referenceName',
|
||||
'referenceType',
|
||||
];
|
||||
|
||||
return (await fyo.db.getAllRaw(ModelNameEnum.StockLedgerEntry, {
|
||||
fields: fieldnames,
|
||||
orderBy: 'date',
|
||||
order: 'asc',
|
||||
})) as RawStockLedgerEntry[];
|
||||
}
|
||||
|
||||
export function getStockLedgerEntries(
|
||||
rawSLEs: RawStockLedgerEntry[]
|
||||
): ComputedStockLedgerEntry[] {
|
||||
type Item = string;
|
||||
type Location = string;
|
||||
|
||||
const computedSLEs: ComputedStockLedgerEntry[] = [];
|
||||
const stockQueues: Record<Item, Record<Location, StockQueue>> = {};
|
||||
|
||||
for (const sle of rawSLEs) {
|
||||
const name = safeParseInt(sle.name);
|
||||
const date = new Date(sle.date);
|
||||
const rate = safeParseFloat(sle.rate);
|
||||
const { item, location, quantity, referenceName, referenceType } = sle;
|
||||
|
||||
if (quantity === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
stockQueues[item] ??= {};
|
||||
stockQueues[item][location] ??= new StockQueue();
|
||||
|
||||
const q = stockQueues[item][location];
|
||||
const initialValue = q.value;
|
||||
|
||||
let incomingRate: number | null;
|
||||
if (quantity > 0) {
|
||||
incomingRate = q.inward(rate, quantity);
|
||||
} else {
|
||||
incomingRate = q.outward(-quantity);
|
||||
}
|
||||
|
||||
if (incomingRate === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const balanceQuantity = q.quantity;
|
||||
const valuationRate = q.fifo;
|
||||
const balanceValue = q.value;
|
||||
const valueChange = balanceValue - initialValue;
|
||||
|
||||
const csle: ComputedStockLedgerEntry = {
|
||||
name,
|
||||
date,
|
||||
|
||||
item,
|
||||
location,
|
||||
|
||||
quantity,
|
||||
balanceQuantity,
|
||||
|
||||
incomingRate,
|
||||
valuationRate,
|
||||
|
||||
balanceValue,
|
||||
valueChange,
|
||||
|
||||
referenceName,
|
||||
referenceType,
|
||||
};
|
||||
|
||||
computedSLEs.push(csle);
|
||||
}
|
||||
|
||||
return computedSLEs;
|
||||
}
|
33
reports/inventory/types.ts
Normal file
33
reports/inventory/types.ts
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
export interface RawStockLedgerEntry {
|
||||
name: string;
|
||||
date: string;
|
||||
item: string;
|
||||
rate: string;
|
||||
quantity: number;
|
||||
location: string;
|
||||
referenceName: string;
|
||||
referenceType: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
export interface ComputedStockLedgerEntry{
|
||||
name: number;
|
||||
date: Date;
|
||||
|
||||
item: string;
|
||||
location:string;
|
||||
|
||||
quantity: number;
|
||||
balanceQuantity: number;
|
||||
|
||||
incomingRate: number;
|
||||
valuationRate:number;
|
||||
|
||||
balanceValue:number;
|
||||
valueChange:number;
|
||||
|
||||
referenceName: string;
|
||||
referenceType: string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user