2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 14:50:56 +00:00

incr: complete stock ledger

This commit is contained in:
18alantom 2022-11-10 00:12:00 +05:30
parent a277c748fb
commit a9fd590512
4 changed files with 446 additions and 44 deletions

View File

@ -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') {

View File

@ -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);
async _getReportData(force?: boolean): Promise<ReportData> {
if (this.shouldRefresh || force || !this._rawData?.length) {
await this._setRawData();
}
convertRawDataToReportData(
rawData: RawValueMap[],
fields: Field[]
): ReportData {
const reportData: 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));
}
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) {
reportData.push(this.convertRawDataRowToReportRow(row, fields));
}
return reportData;
const key = row[groupBy];
if (!groups.has(key)) {
groups.set(key, []);
}
convertRawDataRowToReportRow(row: RawValueMap, fields: Field[]): ReportRow {
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[] {

View 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;
}

View 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;
}