2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 23:30:56 +00:00

feat: add stock balance report

This commit is contained in:
18alantom 2022-11-25 14:51:58 +05:30
parent a3f96a90e1
commit 1d61a870a5
6 changed files with 317 additions and 29 deletions

View File

@ -3,6 +3,7 @@ import { GeneralLedger } from './GeneralLedger/GeneralLedger';
import { GSTR1 } from './GoodsAndServiceTax/GSTR1';
import { GSTR2 } from './GoodsAndServiceTax/GSTR2';
import { StockLedger } from './inventory/StockLedger';
import { StockBalance } from './inventory/StockBalance';
import { ProfitAndLoss } from './ProfitAndLoss/ProfitAndLoss';
import { Report } from './Report';
import { TrialBalance } from './TrialBalance/TrialBalance';
@ -15,4 +16,5 @@ export const reports = {
GSTR1,
GSTR2,
StockLedger,
StockBalance,
} as Record<string, typeof Report>;

View File

@ -0,0 +1,143 @@
import { t } from 'fyo';
import { RawValueMap } from 'fyo/core/types';
import { Action } from 'fyo/model/types';
import getCommonExportActions from 'reports/commonExporter';
import { ColumnField, ReportData } from 'reports/types';
import { Field } from 'schemas/types';
import { getStockBalanceEntries } from './helpers';
import { StockLedger } from './StockLedger';
import { ReferenceType } from './types';
export class StockBalance extends StockLedger {
static title = t`Stock Balance`;
static reportName = 'stock-balance';
static isInventory = true;
override ascending: boolean = true;
override referenceType: ReferenceType = 'All';
override referenceName: string = '';
override async _getReportData(force?: boolean): Promise<ReportData> {
if (this.shouldRefresh || force || !this._rawData?.length) {
await this._setRawData();
}
const filters = {
item: this.item,
location: this.location,
fromDate: this.fromDate,
toDate: this.toDate,
};
const rawData = getStockBalanceEntries(this._rawData ?? [], filters);
return rawData.map((sbe, i) => {
const row = { ...sbe, name: i + 1 } as RawValueMap;
return this._convertRawDataRowToReportRow(row, {
incomingQuantity: 'green',
outgoingQuantity: 'red',
balanceQuantity: null,
});
});
}
getFilters(): Field[] {
return [
{
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',
},
] as Field[];
}
getColumns(): ColumnField[] {
return [
{
fieldname: 'name',
label: '#',
fieldtype: 'Int',
width: 0.5,
},
{
fieldname: 'item',
label: 'Item',
fieldtype: 'Link',
},
{
fieldname: 'location',
label: 'Location',
fieldtype: 'Link',
},
{
fieldname: 'balanceQuantity',
label: 'Balance Qty.',
fieldtype: 'Float',
},
{
fieldname: 'balanceValue',
label: 'Balance Value',
fieldtype: 'Float',
},
{
fieldname: 'openingQuantity',
label: 'Opening Qty.',
fieldtype: 'Float',
},
{
fieldname: 'openingValue',
label: 'Opening Value',
fieldtype: 'Float',
},
{
fieldname: 'incomingQuantity',
label: 'In Qty.',
fieldtype: 'Float',
},
{
fieldname: 'incomingValue',
label: 'In Value',
fieldtype: 'Currency',
},
{
fieldname: 'outgoingQuantity',
label: 'Out Qty.',
fieldtype: 'Float',
},
{
fieldname: 'outgoingValue',
label: 'Out Value',
fieldtype: 'Currency',
},
{
fieldname: 'valuationRate',
label: 'Valuation rate',
fieldtype: 'Currency',
},
];
}
getActions(): Action[] {
return getCommonExportActions(this);
}
}

View File

@ -1,4 +1,5 @@
import { Fyo, t } from 'fyo';
import { RawValueMap } from 'fyo/core/types';
import { Action } from 'fyo/model/types';
import { cloneDeep } from 'lodash';
import { DateTime } from 'luxon';
@ -7,16 +8,10 @@ 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 } from 'schemas/types';
import { Field, RawValue } from 'schemas/types';
import { isNumeric } from 'src/utils';
import { getRawStockLedgerEntries, getStockLedgerEntries } from './helpers';
import { ComputedStockLedgerEntry } from './types';
type ReferenceType =
| ModelNameEnum.StockMovement
| ModelNameEnum.Shipment
| ModelNameEnum.PurchaseReceipt
| 'All';
import { ComputedStockLedgerEntry, ReferenceType } from './types';
export class StockLedger extends Report {
static title = t`Stock Ledger`;
@ -73,7 +68,12 @@ export class StockLedger extends Report {
const filtered = this._getFilteredRawData(rawData);
const grouped = this._getGroupedRawData(filtered);
return grouped.map((row) => this._convertRawDataRowToReportRow(row));
return grouped.map((row) =>
this._convertRawDataRowToReportRow(row as RawValueMap, {
quantity: null,
valueChange: null,
})
);
}
async _setRawData() {
@ -92,8 +92,8 @@ export class StockLedger extends Report {
return [];
}
const fromDate = this.fromDate ? new Date(this.fromDate) : null;
const toDate = this.toDate ? new Date(this.toDate) : null;
const fromDate = this.fromDate ? Date.parse(this.fromDate) : null;
const toDate = this.toDate ? Date.parse(this.toDate) : null;
if (!this.ascending) {
rawData.reverse();
@ -110,11 +110,12 @@ export class StockLedger extends Report {
continue;
}
if (toDate && row.date > toDate) {
const date = row.date.valueOf();
if (toDate && date > toDate) {
continue;
}
if (fromDate && row.date < fromDate) {
if (fromDate && date < fromDate) {
continue;
}
@ -171,7 +172,8 @@ export class StockLedger extends Report {
}
_convertRawDataRowToReportRow(
row: ComputedStockLedgerEntry | { name: null }
row: RawValueMap,
colouredMap: Record<string, 'red' | 'green' | null>
): ReportRow {
const cells: ReportCell[] = [];
const columns = this.getColumns();
@ -191,16 +193,19 @@ export class StockLedger extends Report {
const fieldname = col.fieldname as keyof ComputedStockLedgerEntry;
const fieldtype = col.fieldtype;
const rawValue = row[fieldname];
const rawValue = row[fieldname] as RawValue;
const value = this.fyo.format(rawValue, fieldtype);
const align = isNumeric(fieldtype) ? 'right' : 'left';
const isColoured =
fieldname === 'quantity' || fieldname === 'valueChange';
const isColoured = fieldname in colouredMap;
const isNumber = typeof rawValue === 'number';
let color: 'red' | 'green' | undefined = undefined;
if (isColoured && rawValue > 0) {
if (isColoured && colouredMap[fieldname]) {
color = colouredMap[fieldname]!;
} else if (isColoured && isNumber && rawValue > 0) {
color = 'green';
} else if (isColoured && rawValue < 0) {
} else if (isColoured && isNumber && rawValue < 0) {
color = 'red';
}
@ -265,7 +270,7 @@ export class StockLedger extends Report {
},
{
fieldname: 'valuationRate',
label: 'Valuation rate',
label: 'Valuation Rate',
fieldtype: 'Currency',
},
{

View File

@ -3,7 +3,14 @@ import { StockQueue } from 'models/inventory/stockQueue';
import { ValuationMethod } from 'models/inventory/types';
import { ModelNameEnum } from 'models/types';
import { safeParseFloat, safeParseInt } from 'utils/index';
import { ComputedStockLedgerEntry, RawStockLedgerEntry } from './types';
import {
ComputedStockLedgerEntry,
RawStockLedgerEntry,
StockBalanceEntry,
} from './types';
type Item = string;
type Location = string;
export async function getRawStockLedgerEntries(fyo: Fyo) {
const fieldnames = [
@ -28,9 +35,6 @@ export function getStockLedgerEntries(
rawSLEs: RawStockLedgerEntry[],
valuationMethod: ValuationMethod
): ComputedStockLedgerEntry[] {
type Item = string;
type Location = string;
const computedSLEs: ComputedStockLedgerEntry[] = [];
const stockQueues: Record<Item, Record<Location, StockQueue>> = {};
@ -95,3 +99,101 @@ export function getStockLedgerEntries(
return computedSLEs;
}
export function getStockBalanceEntries(
computedSLEs: ComputedStockLedgerEntry[],
filters: {
item?: string;
location?: string;
fromDate?: string;
toDate?: string;
}
): StockBalanceEntry[] {
const sbeMap: Record<Item, Record<Location, StockBalanceEntry>> = {};
const fromDate = filters.fromDate ? Date.parse(filters.fromDate) : null;
const toDate = filters.toDate ? Date.parse(filters.toDate) : null;
for (const sle of computedSLEs) {
if (filters.item && sle.item !== filters.item) {
continue;
}
if (filters.location && sle.location !== filters.location) {
continue;
}
sbeMap[sle.item] ??= {};
sbeMap[sle.item][sle.location] ??= getSBE(sle.item, sle.location);
const date = sle.date.valueOf();
if (fromDate && date < fromDate) {
const sbe = sbeMap[sle.item][sle.location]!;
updateOpeningBalances(sbe, sle);
continue;
}
if (toDate && date > toDate) {
continue;
}
const sbe = sbeMap[sle.item][sle.location]!;
updateCurrentBalances(sbe, sle);
}
return Object.values(sbeMap)
.map((sbes) => Object.values(sbes))
.flat();
}
function getSBE(item: string, location: string): StockBalanceEntry {
return {
name: 0,
item,
location,
balanceQuantity: 0,
balanceValue: 0,
openingQuantity: 0,
openingValue: 0,
incomingQuantity: 0,
incomingValue: 0,
outgoingQuantity: 0,
outgoingValue: 0,
valuationRate: 0,
};
}
function updateOpeningBalances(
sbe: StockBalanceEntry,
sle: ComputedStockLedgerEntry
) {
sbe.openingQuantity += sle.quantity;
sbe.openingValue += sle.valueChange;
sbe.balanceQuantity += sle.quantity;
sbe.balanceValue += sle.valueChange;
}
function updateCurrentBalances(
sbe: StockBalanceEntry,
sle: ComputedStockLedgerEntry
) {
sbe.balanceQuantity += sle.quantity;
sbe.balanceValue += sle.valueChange;
if (sle.quantity > 0) {
sbe.incomingQuantity += sle.quantity;
sbe.incomingValue += sle.valueChange;
} else {
sbe.outgoingQuantity -= sle.quantity;
sbe.outgoingValue -= sle.valueChange;
}
sbe.valuationRate = sle.valuationRate;
}

View File

@ -1,3 +1,4 @@
import { ModelNameEnum } from "models/types";
export interface RawStockLedgerEntry {
name: string;
@ -31,3 +32,31 @@ export interface ComputedStockLedgerEntry{
referenceName: string;
referenceType: string;
}
export interface StockBalanceEntry{
name: number;
item: string;
location:string;
balanceQuantity: number;
balanceValue: number;
openingQuantity: number;
openingValue:number;
incomingQuantity:number;
incomingValue:number;
outgoingQuantity:number;
outgoingValue:number;
valuationRate:number;
}
export type ReferenceType =
| ModelNameEnum.StockMovement
| ModelNameEnum.Shipment
| ModelNameEnum.PurchaseReceipt
| 'All';

View File

@ -122,11 +122,18 @@ async function getReportSidebar() {
};
if (await getIsInventoryEnabled(fyo)) {
reports.items.push({
label: t`Stock Ledger`,
name: 'stock-ledger',
route: '/report/StockLedger',
});
reports.items.push(
{
label: t`Stock Ledger`,
name: 'stock-ledger',
route: '/report/StockLedger',
},
{
label: t`Stock Balance`,
name: 'stock-balance',
route: '/report/StockBalance',
}
);
}
return reports;