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:
parent
a3f96a90e1
commit
1d61a870a5
@ -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>;
|
||||
|
143
reports/inventory/StockBalance.ts
Normal file
143
reports/inventory/StockBalance.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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',
|
||||
},
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user