diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index 394c64bd..872e95f0 100644 --- a/backend/database/bespoke.ts +++ b/backend/database/bespoke.ts @@ -138,7 +138,8 @@ export class BespokeQueries { location?: string, fromDate?: string, toDate?: string, - batch?: string + batch?: string, + serialNo?: string ): Promise { const query = db.knex!(ModelNameEnum.StockLedgerEntry) .sum('quantity') @@ -152,6 +153,10 @@ export class BespokeQueries { query.andWhere('batch', batch); } + if (serialNo) { + query.andWhere('serialNo', serialNo); + } + if (fromDate) { query.andWhereRaw('datetime(date) > datetime(?)', [fromDate]); } diff --git a/fyo/core/dbHandler.ts b/fyo/core/dbHandler.ts index 070c6118..908c57a3 100644 --- a/fyo/core/dbHandler.ts +++ b/fyo/core/dbHandler.ts @@ -313,7 +313,8 @@ export class DatabaseHandler extends DatabaseBase { location?: string, fromDate?: string, toDate?: string, - batch?: string + batch?: string, + serialNo?: string ): Promise { return (await this.#demux.callBespoke( 'getStockQuantity', @@ -321,7 +322,8 @@ export class DatabaseHandler extends DatabaseBase { location, fromDate, toDate, - batch + batch, + serialNo )) as number | null; } diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 1b243fdf..b0c7f969 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -19,6 +19,7 @@ export class Item extends Doc { itemType?: 'Product' | 'Service'; for?: 'Purchases' | 'Sales' | 'Both'; hasBatch?: boolean; + hasSerialNo?: boolean; formulas: FormulaMap = { incomeAccount: { @@ -124,6 +125,8 @@ export class Item extends Doc { barcode: () => !this.fyo.singles.InventorySettings?.enableBarcodes, hasBatch: () => !(this.fyo.singles.InventorySettings?.enableBatches && this.trackItem), + hasSerialNo: () => + !(this.fyo.singles.InventorySettings?.enableSerialNo && this.trackItem), uomConversions: () => !this.fyo.singles.InventorySettings?.enableUomConversions, }; @@ -133,5 +136,6 @@ export class Item extends Doc { itemType: () => this.inserted, trackItem: () => this.inserted, hasBatch: () => this.inserted, + hasSerialNo: () => this.inserted, }; } diff --git a/models/helpers.ts b/models/helpers.ts index a992db42..f60e019c 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -16,7 +16,7 @@ import { import { Invoice } from './baseModels/Invoice/Invoice'; import { StockMovement } from './inventory/StockMovement'; import { StockTransfer } from './inventory/StockTransfer'; -import { InvoiceStatus, ModelNameEnum } from './types'; +import { InvoiceStatus, SerialNoStatus, ModelNameEnum } from './types'; export function getInvoiceActions( fyo: Fyo, @@ -248,6 +248,45 @@ export function getInvoiceStatus(doc: RenderData | Doc): InvoiceStatus { return 'Saved'; } +export function getSerialNoStatusColumn(): ColumnConfig { + return { + label: t`Status`, + fieldname: 'status', + fieldtype: 'Select', + render(doc) { + const status = doc.status as SerialNoStatus; + const color = serialNoStatusColor[status]; + const label = getSerialNoStatusText(status as string); + + return { + template: `${label}`, + }; + }, + }; +} + +export const serialNoStatusColor: Record = { + Inactive: 'gray', + Active: 'green', + Delivered: 'green', + Expired: 'red', +}; + +export function getSerialNoStatusText(status: string): string { + switch (status) { + case 'Inactive': + return t`Inactive`; + case 'Active': + return t`Active`; + case 'Delivered': + return t`Delivered`; + case 'Expired': + return t`Expired`; + default: + return t`Inactive`; + } +} + export async function getExchangeRate({ fromCurrency, toCurrency, diff --git a/models/index.ts b/models/index.ts index 14a7ea4d..66846cef 100644 --- a/models/index.ts +++ b/models/index.ts @@ -19,6 +19,7 @@ import { SetupWizard } from './baseModels/SetupWizard/SetupWizard'; import { Tax } from './baseModels/Tax/Tax'; import { TaxSummary } from './baseModels/TaxSummary/TaxSummary'; import { Batch } from './inventory/Batch'; +import { SerialNo } from './inventory/SerialNo'; import { InventorySettings } from './inventory/InventorySettings'; import { Location } from './inventory/Location'; import { PurchaseReceipt } from './inventory/PurchaseReceipt'; @@ -48,6 +49,7 @@ export const models = { PurchaseInvoiceItem, SalesInvoice, SalesInvoiceItem, + SerialNo, SetupWizard, PrintTemplate, Tax, diff --git a/models/inventory/InventorySettings.ts b/models/inventory/InventorySettings.ts index da79b82d..ebdf58dc 100644 --- a/models/inventory/InventorySettings.ts +++ b/models/inventory/InventorySettings.ts @@ -11,6 +11,7 @@ export class InventorySettings extends Doc { costOfGoodsSold?: string; enableBarcodes?: boolean; enableBatches?: boolean; + enableSerialNo?: boolean; enableUomConversions?: boolean; static filters: FiltersMap = { @@ -35,6 +36,9 @@ export class InventorySettings extends Doc { enableBatches: () => { return !!this.enableBatches; }, + enableSerialNo: () => { + return !!this.enableSerialNo; + }, enableUomConversions: () => { return !!this.enableUomConversions; }, diff --git a/models/inventory/PurchaseReceipt.ts b/models/inventory/PurchaseReceipt.ts index 6da16e02..8894667b 100644 --- a/models/inventory/PurchaseReceipt.ts +++ b/models/inventory/PurchaseReceipt.ts @@ -1,11 +1,17 @@ import { ListViewSettings } from 'fyo/model/types'; import { getTransactionStatusColumn } from 'models/helpers'; +import { updateSerialNoStatus } from './helpers'; import { PurchaseReceiptItem } from './PurchaseReceiptItem'; import { StockTransfer } from './StockTransfer'; export class PurchaseReceipt extends StockTransfer { items?: PurchaseReceiptItem[]; + async afterSubmit(): Promise { + await super.afterSubmit(); + await updateSerialNoStatus(this, this.items!, 'Active'); + } + static getListViewSettings(): ListViewSettings { return { columns: [ diff --git a/models/inventory/SerialNo.ts b/models/inventory/SerialNo.ts new file mode 100644 index 00000000..aec0fb4b --- /dev/null +++ b/models/inventory/SerialNo.ts @@ -0,0 +1,17 @@ +import { Doc } from 'fyo/model/doc'; +import { ListViewSettings } from 'fyo/model/types'; +import { getSerialNoStatusColumn } from 'models/helpers'; + +export class SerialNo extends Doc { + static getListViewSettings(): ListViewSettings { + return { + columns: [ + 'name', + getSerialNoStatusColumn(), + 'item', + 'description', + 'party', + ], + }; + } +} \ No newline at end of file diff --git a/models/inventory/Shipment.ts b/models/inventory/Shipment.ts index fa559d4e..23c6ed5f 100644 --- a/models/inventory/Shipment.ts +++ b/models/inventory/Shipment.ts @@ -1,11 +1,22 @@ import { ListViewSettings } from 'fyo/model/types'; import { getTransactionStatusColumn } from 'models/helpers'; +import { updateSerialNoStatus } from './helpers'; import { ShipmentItem } from './ShipmentItem'; import { StockTransfer } from './StockTransfer'; export class Shipment extends StockTransfer { items?: ShipmentItem[]; + async afterSubmit(): Promise { + await super.afterSubmit(); + await updateSerialNoStatus(this, this.items!, 'Delivered'); + } + + async afterCancel(): Promise { + await super.afterCancel(); + await updateSerialNoStatus(this, this.items!, 'Active'); + } + static getListViewSettings(): ListViewSettings { return { columns: [ diff --git a/models/inventory/StockLedgerEntry.ts b/models/inventory/StockLedgerEntry.ts index b3ede093..19a39c26 100644 --- a/models/inventory/StockLedgerEntry.ts +++ b/models/inventory/StockLedgerEntry.ts @@ -11,6 +11,7 @@ export class StockLedgerEntry extends Doc { referenceName?: string; referenceType?: string; batch?: string; + serialNo?: string; static override getListViewSettings(): ListViewSettings { return { diff --git a/models/inventory/StockManager.ts b/models/inventory/StockManager.ts index e000d4da..49f05cab 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -4,6 +4,7 @@ import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; import { StockLedgerEntry } from './StockLedgerEntry'; import { SMDetails, SMIDetails, SMTransferDetails } from './types'; +import { getSerialNumbers } from './helpers'; export class StockManager { /** @@ -133,15 +134,33 @@ export class StockManager { const date = details.date.toISOString(); const formattedDate = this.fyo.format(details.date, 'Datetime'); const batch = details.batch || undefined; + const serialNo = details.serialNo || undefined; + let quantityBefore = 0; - let quantityBefore = - (await this.fyo.db.getStockQuantity( - details.item, - details.fromLocation, - undefined, - date, - batch - )) ?? 0; + if (serialNo) { + const serialNos = getSerialNumbers(serialNo); + for (const serialNo of serialNos) { + quantityBefore += + (await this.fyo.db.getStockQuantity( + details.item, + details.fromLocation, + undefined, + date, + batch, + serialNo + )) ?? 0; + } + } else { + quantityBefore = + (await this.fyo.db.getStockQuantity( + details.item, + details.fromLocation, + undefined, + date, + batch, + serialNo + )) ?? 0; + } if (this.isCancelled) { quantityBefore += details.quantity; @@ -167,7 +186,8 @@ export class StockManager { details.fromLocation, details.date.toISOString(), undefined, - batch + batch, + serialNo ); if (quantityAfter === null) { @@ -211,6 +231,7 @@ class StockManagerItem { fromLocation?: string; toLocation?: string; batch?: string; + serialNo?: string; stockLedgerEntries?: StockLedgerEntry[]; @@ -226,6 +247,7 @@ class StockManagerItem { this.referenceName = details.referenceName; this.referenceType = details.referenceType; this.batch = details.batch; + this.serialNo = details.serialNo; this.fyo = fyo; } @@ -260,10 +282,30 @@ class StockManagerItem { #moveStockForSingleLocation(location: string, isOutward: boolean) { let quantity = this.quantity!; + const serialNo = this.serialNo; if (quantity === 0) { return; } + if (serialNo) { + const serialNos = getSerialNumbers(serialNo!); + if (isOutward) { + quantity = -1; + } else { + quantity = 1; + } + + for (const serialNo of serialNos) { + const stockLedgerEntry = this.#getStockLedgerEntry( + location, + quantity, + serialNo! + ); + this.stockLedgerEntries?.push(stockLedgerEntry); + } + return; + } + if (isOutward) { quantity = -quantity; } @@ -273,12 +315,13 @@ class StockManagerItem { this.stockLedgerEntries?.push(stockLedgerEntry); } - #getStockLedgerEntry(location: string, quantity: number) { + #getStockLedgerEntry(location: string, quantity: number, serialNo?: string) { return this.fyo.doc.getNewDoc(ModelNameEnum.StockLedgerEntry, { date: this.date, item: this.item, rate: this.rate, batch: this.batch || null, + serialNo: serialNo || null, quantity, location, referenceName: this.referenceName, diff --git a/models/inventory/StockMovement.ts b/models/inventory/StockMovement.ts index 9817c1a4..23f28eb5 100644 --- a/models/inventory/StockMovement.ts +++ b/models/inventory/StockMovement.ts @@ -15,7 +15,12 @@ import { import { LedgerPosting } from 'models/Transactional/LedgerPosting'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; -import { validateBatch } from './helpers'; +import { + getSerialNumbers, + updateSerialNoStatus, + validateBatch, + validateSerialNo, +} from './helpers'; import { StockMovementItem } from './StockMovementItem'; import { Transfer } from './Transfer'; import { MovementType } from './types'; @@ -52,6 +57,7 @@ export class StockMovement extends Transfer { await super.validate(); this.validateManufacture(); await validateBatch(this); + await validateSerialNo(this); } validateManufacture() { @@ -71,6 +77,16 @@ export class StockMovement extends Transfer { } } + async afterSubmit(): Promise { + await super.afterSubmit(); + await updateSerialNoStatus(this, this.items!, 'Active'); + } + + async afterCancel(): Promise { + await super.afterCancel(); + await updateSerialNoStatus(this, this.items!, 'Inactive'); + } + static filters: FiltersMap = { numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }), }; @@ -110,6 +126,7 @@ export class StockMovement extends Transfer { rate: row.rate!, quantity: row.quantity!, batch: row.batch!, + serialNo: row.serialNo!, fromLocation: row.fromLocation, toLocation: row.toLocation, })); diff --git a/models/inventory/StockMovementItem.ts b/models/inventory/StockMovementItem.ts index 36318b21..edaa9f5e 100644 --- a/models/inventory/StockMovementItem.ts +++ b/models/inventory/StockMovementItem.ts @@ -32,6 +32,7 @@ export class StockMovementItem extends Doc { amount?: Money; parentdoc?: StockMovement; batch?: string; + serialNo?: string; get isIssue() { return this.parentdoc?.movementType === MovementType.MaterialIssue; @@ -236,6 +237,7 @@ export class StockMovementItem extends Doc { override hidden: HiddenMap = { batch: () => !this.fyo.singles.InventorySettings?.enableBatches, + serialNo: () => !this.fyo.singles.InventorySettings?.enableSerialNo, transferUnit: () => !this.fyo.singles.InventorySettings?.enableUomConversions, transferQuantity: () => diff --git a/models/inventory/StockTransfer.ts b/models/inventory/StockTransfer.ts index aa1e4df8..967ea9ea 100644 --- a/models/inventory/StockTransfer.ts +++ b/models/inventory/StockTransfer.ts @@ -17,7 +17,7 @@ import { LedgerPosting } from 'models/Transactional/LedgerPosting'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; import { TargetField } from 'schemas/types'; -import { validateBatch } from './helpers'; +import { validateBatch, validateSerialNo } from './helpers'; import { StockTransferItem } from './StockTransferItem'; import { Transfer } from './Transfer'; @@ -91,6 +91,7 @@ export abstract class StockTransfer extends Transfer { rate: row.rate!, quantity: row.quantity!, batch: row.batch!, + serialNo: row.serialNo!, fromLocation, toLocation, }; @@ -162,6 +163,7 @@ export abstract class StockTransfer extends Transfer { override async validate(): Promise { await super.validate(); await validateBatch(this); + await validateSerialNo(this); } static getActions(fyo: Fyo): Action[] { diff --git a/models/inventory/StockTransferItem.ts b/models/inventory/StockTransferItem.ts index e74fd322..a07319d0 100644 --- a/models/inventory/StockTransferItem.ts +++ b/models/inventory/StockTransferItem.ts @@ -28,6 +28,7 @@ export class StockTransferItem extends Doc { description?: string; hsnCode?: number; batch?: string; + serialNo?: string; parentdoc?: StockTransfer; @@ -215,6 +216,7 @@ export class StockTransferItem extends Doc { override hidden: HiddenMap = { batch: () => !this.fyo.singles.InventorySettings?.enableBatches, + serialNo: () => !this.fyo.singles.InventorySettings?.enableSerialNo, transferUnit: () => !this.fyo.singles.InventorySettings?.enableUomConversions, transferQuantity: () => diff --git a/models/inventory/helpers.ts b/models/inventory/helpers.ts index ba1d22a6..cf523331 100644 --- a/models/inventory/helpers.ts +++ b/models/inventory/helpers.ts @@ -1,7 +1,11 @@ +import { t } from 'fyo'; +import { Doc } from 'fyo/model/doc'; +import { DocValueMap } from 'fyo/core/types'; import { ValidationError } from 'fyo/utils/errors'; import { Invoice } from 'models/baseModels/Invoice/Invoice'; import { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem'; import { ModelNameEnum } from 'models/types'; +import { ShipmentItem } from './ShipmentItem'; import { StockMovement } from './StockMovement'; import { StockMovementItem } from './StockMovementItem'; import { StockTransfer } from './StockTransfer'; @@ -25,11 +29,7 @@ async function validateItemRowBatch( return; } - const hasBatch = await doc.fyo.getValue( - ModelNameEnum.Item, - item, - 'hasBatch' - ); + const hasBatch = await doc.fyo.getValue(ModelNameEnum.Item, item, 'hasBatch'); if (!hasBatch && batch) { throw new ValidationError( @@ -49,3 +49,211 @@ async function validateItemRowBatch( ); } } + +export async function validateSerialNo( + doc: StockMovement | StockTransfer | Invoice +) { + for (const row of doc.items ?? []) { + await validateItemRowSerialNo(row, doc.movementType as string); + } +} + +async function validateItemRowSerialNo( + doc: StockMovementItem | StockTransferItem | InvoiceItem, + movementType: string +) { + const idx = doc.idx ?? 0 + 1; + const item = doc.item; + + if (!item) { + return; + } + + if (doc.parentdoc?.cancelled) { + return; + } + + const hasSerialNo = await doc.fyo.getValue( + ModelNameEnum.Item, + item, + 'hasSerialNo' + ); + + if (hasSerialNo && !doc.serialNo) { + throw new ValidationError( + [ + doc.fyo.t`Serial No not set for row ${idx}.`, + doc.fyo.t`Serial No is enabled for Item ${item}`, + ].join(' ') + ); + } + + if (!hasSerialNo && doc.serialNo) { + throw new ValidationError( + [ + doc.fyo.t`Serial No set for row ${idx}.`, + doc.fyo.t`Serial No is not enabled for Item ${item}`, + ].join(' ') + ); + } + + if (!hasSerialNo) return; + + const serialNos = getSerialNumbers(doc.serialNo as string); + + if (serialNos.length !== doc.quantity) { + throw new ValidationError( + t`${doc.quantity!} Serial Numbers required for ${doc.item!}. You have provided ${ + serialNos.length + }.` + ); + } + + for (const serialNo of serialNos) { + const { name, status, item } = await doc.fyo.db.get( + ModelNameEnum.SerialNo, + serialNo, + ['name', 'status', 'item'] + ); + + if (movementType == 'MaterialIssue') { + await validateSNMaterialIssue( + doc, + name as string, + item as string, + serialNo, + status as string + ); + } + + if (movementType == 'MaterialReceipt') { + await validateSNMaterialReceipt( + doc, + name as string, + serialNo, + status as string + ); + } + + if (movementType === 'Shipment') { + await validateSNShipment(doc, serialNo); + } + + if (doc.parentSchemaName === 'PurchaseReceipt') { + await validateSNPurchaseReceipt( + doc, + name as string, + serialNo, + status as string + ); + } + } +} + +export const getSerialNumbers = (serialNo: string): string[] => { + return serialNo ? serialNo.split('\n') : []; +}; + +export const updateSerialNoStatus = async ( + doc: Doc, + items: StockMovementItem[] | ShipmentItem[], + newStatus: string +) => { + for (const item of items) { + const serialNos = getSerialNumbers(item.serialNo!); + if (!serialNos.length) break; + + for (const serialNo of serialNos) { + await doc.fyo.db.update(ModelNameEnum.SerialNo, { + name: serialNo, + status: newStatus, + }); + } + } +}; + + +const validateSNMaterialReceipt = async ( + doc: Doc, + name: string, + serialNo: string, + status: string +) => { + if (name === undefined) { + const values = { + name: serialNo, + item: doc.item, + party: doc.parentdoc?.party as string, + }; + ( + await doc.fyo.doc + .getNewDoc(ModelNameEnum.SerialNo, values as DocValueMap) + .sync() + ).submit(); + } + + if (status && status !== 'Inactive') { + throw new ValidationError(t`SerialNo ${serialNo} status is not Inactive`); + } +}; + +const validateSNPurchaseReceipt = async ( + doc: Doc, + name: string, + serialNo: string, + status: string +) => { + if (name === undefined) { + const values = { + name: serialNo, + item: doc.item, + party: doc.parentdoc?.party as string, + status: 'Inactive', + }; + ( + await doc.fyo.doc + .getNewDoc(ModelNameEnum.SerialNo, values as DocValueMap) + .sync() + ).submit(); + } + + if (status && status !== 'Inactive') { + throw new ValidationError(t`SerialNo ${serialNo} status is not Inactive`); + } +}; + +const validateSNMaterialIssue = async ( + doc: Doc, + name: string, + item: string, + serialNo: string, + status: string +) => { + if (doc.isCancelled) return; + + if (!name) + throw new ValidationError(t`Serial Number ${serialNo} does not exist.`); + + if (status !== 'Active') + throw new ValidationError( + t`Serial Number ${serialNo} status is not Active` + ); + if (doc.item !== item) { + throw new ValidationError( + t`Serial Number ${serialNo} does not belong to the item ${ + doc.item! as string + }` + ); + } +}; + +const validateSNShipment = async (doc: Doc, serialNo: string) => { + const { status } = await doc.fyo.db.get( + ModelNameEnum.SerialNo, + serialNo, + 'status' + ); + + if (status !== 'Active') + throw new ValidationError(t`Serial No ${serialNo} status is not Active`); +}; diff --git a/models/inventory/tests/helpers.ts b/models/inventory/tests/helpers.ts index 245d859d..4d59d60e 100644 --- a/models/inventory/tests/helpers.ts +++ b/models/inventory/tests/helpers.ts @@ -28,6 +28,7 @@ type Transfer = { from?: string; to?: string; batch?: string; + serialNo?: string; quantity: number; rate: number; }; @@ -36,8 +37,8 @@ interface TransferTwo extends Omit { location: string; } -export function getItem(name: string, rate: number, hasBatch: boolean = false) { - return { name, rate, trackItem: true, hasBatch }; +export function getItem(name: string, rate: number, hasBatch: boolean = false, hasSerialNo: boolean = false) { + return { name, rate, trackItem: true, hasBatch, hasSerialNo }; } export async function getBatch( @@ -84,6 +85,7 @@ export async function getStockMovement( from: fromLocation, to: toLocation, batch, + serialNo, quantity, rate, } of transfers) { @@ -92,6 +94,7 @@ export async function getStockMovement( fromLocation, toLocation, batch, + serialNo, rate, quantity, }); diff --git a/models/inventory/types.ts b/models/inventory/types.ts index eab8769c..ab8f80ec 100644 --- a/models/inventory/types.ts +++ b/models/inventory/types.ts @@ -23,6 +23,7 @@ export interface SMTransferDetails { rate: Money; quantity: number; batch?: string; + serialNo?: string; fromLocation?: string; toLocation?: string; } diff --git a/models/types.ts b/models/types.ts index e68fea77..5686dd71 100644 --- a/models/types.ts +++ b/models/types.ts @@ -1,4 +1,5 @@ export type InvoiceStatus = 'Draft' | 'Saved' | 'Unpaid' | 'Cancelled' | 'Paid'; +export type SerialNoStatus = 'Inactive' | 'Active' | 'Delivered' | 'Expired'; export enum ModelNameEnum { Account = 'Account', AccountingLedgerEntry = 'AccountingLedgerEntry', @@ -25,6 +26,7 @@ export enum ModelNameEnum { PurchaseInvoiceItem = 'PurchaseInvoiceItem', SalesInvoice = 'SalesInvoice', SalesInvoiceItem = 'SalesInvoiceItem', + SerialNo = 'SerialNo', SetupWizard = 'SetupWizard', Tax = 'Tax', TaxDetail = 'TaxDetail', diff --git a/reports/inventory/StockLedger.ts b/reports/inventory/StockLedger.ts index 2544d8d9..f7e7c672 100644 --- a/reports/inventory/StockLedger.ts +++ b/reports/inventory/StockLedger.ts @@ -28,6 +28,7 @@ export class StockLedger extends Report { item?: string; location?: string; batch?: string; + serialNo?: string; fromDate?: string; toDate?: string; ascending?: boolean; @@ -41,6 +42,11 @@ export class StockLedger extends Report { .enableBatches; } + get hasSerialNos(): boolean { + return !!(this.fyo.singles.InventorySettings as InventorySettings) + .enableSerialNo; + } + constructor(fyo: Fyo) { super(fyo); this._setObservers(); @@ -276,6 +282,11 @@ export class StockLedger extends Report { { fieldname: 'batch', label: 'Batch', fieldtype: 'Link' }, ] as ColumnField[]) : []), + ...(this.hasSerialNos + ? ([ + { fieldname: 'serialNo', label: 'Serial No', fieldtype: 'Data' }, + ] as ColumnField[]) + : []), { fieldname: 'quantity', label: 'Quantity', diff --git a/reports/inventory/helpers.ts b/reports/inventory/helpers.ts index 2b2ad7ed..4aa77f6a 100644 --- a/reports/inventory/helpers.ts +++ b/reports/inventory/helpers.ts @@ -19,6 +19,7 @@ export async function getRawStockLedgerEntries(fyo: Fyo) { 'date', 'item', 'batch', + 'serialNo', 'rate', 'quantity', 'location', @@ -49,6 +50,7 @@ export function getStockLedgerEntries( const rate = safeParseFloat(sle.rate); const { item, location, quantity, referenceName, referenceType } = sle; const batch = sle.batch ?? ''; + const serialNo = sle.serialNo ?? ''; if (quantity === 0) { continue; @@ -88,6 +90,7 @@ export function getStockLedgerEntries( item, location, batch, + serialNo, quantity, balanceQuantity, diff --git a/reports/inventory/types.ts b/reports/inventory/types.ts index af877e81..fc9b2121 100644 --- a/reports/inventory/types.ts +++ b/reports/inventory/types.ts @@ -6,6 +6,7 @@ export interface RawStockLedgerEntry { item: string; rate: string; batch: string | null; + serialNo: string | null; quantity: number; location: string; referenceName: string; @@ -21,6 +22,7 @@ export interface ComputedStockLedgerEntry{ item: string; location:string; batch: string; + serialNo: string; quantity: number; balanceQuantity: number; diff --git a/schemas/schemas.ts b/schemas/schemas.ts index 811049e9..3526a66c 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -11,6 +11,7 @@ import InventorySettings from './app/inventory/InventorySettings.json'; import Location from './app/inventory/Location.json'; import PurchaseReceipt from './app/inventory/PurchaseReceipt.json'; import PurchaseReceiptItem from './app/inventory/PurchaseReceiptItem.json'; +import SerialNo from './app/inventory/SerialNo.json'; import Shipment from './app/inventory/Shipment.json'; import ShipmentItem from './app/inventory/ShipmentItem.json'; import StockLedgerEntry from './app/inventory/StockLedgerEntry.json'; @@ -117,4 +118,5 @@ export const appSchemas: Schema[] | SchemaStub[] = [ PurchaseReceiptItem as Schema, Batch as Schema, + SerialNo as Schema, ]; diff --git a/src/utils/sidebarConfig.ts b/src/utils/sidebarConfig.ts index 4eaba42b..7590ede0 100644 --- a/src/utils/sidebarConfig.ts +++ b/src/utils/sidebarConfig.ts @@ -96,6 +96,13 @@ async function getInventorySidebar(): Promise { name: 'stock-balance', route: '/report/StockBalance', }, + { + label: t`Serial No`, + name: 'serial-no', + route: `/list/SerialNo`, + schemaName: 'SerialNo', + hidden: () => !fyo.singles.InventorySettings?.enableSerialNo as boolean, + }, ], }, ];