From 2301e201c876864cdc6184af6a6ab34efe655fef Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Mon, 12 Jan 2026 10:20:36 +0530 Subject: [PATCH] feat: add auto-generation for serial numbers --- fyo/models/SerialNumberSeries.ts | 74 +++++++++ fyo/models/index.ts | 2 + models/baseModels/Item/Item.ts | 49 ++++++ models/helpers.ts | 11 ++ models/inventory/StockMovementItem.ts | 48 ++++++ models/inventory/StockTransfer.ts | 27 ++++ models/inventory/StockTransferItem.ts | 78 ++++++++++ models/inventory/helpers.ts | 208 +++++++++++++++++++++++++- models/types.ts | 1 + reports/inventory/StockBalance.ts | 60 +++++++- reports/inventory/helpers.ts | 46 ++++-- reports/inventory/types.ts | 10 +- schemas/app/Item.json | 6 + schemas/app/SerialNumberSeries.json | 38 +++++ schemas/schemas.ts | 2 + 15 files changed, 639 insertions(+), 21 deletions(-) create mode 100644 fyo/models/SerialNumberSeries.ts create mode 100644 schemas/app/SerialNumberSeries.json diff --git a/fyo/models/SerialNumberSeries.ts b/fyo/models/SerialNumberSeries.ts new file mode 100644 index 00000000..47304692 --- /dev/null +++ b/fyo/models/SerialNumberSeries.ts @@ -0,0 +1,74 @@ +import { Doc } from 'fyo/model/doc'; +import { ReadOnlyMap, ValidationMap } from 'fyo/model/types'; +import { ValidationError } from 'fyo/utils/errors'; + +const invalidNumberSeries = /[/\=\?\&\%]/; + +function getPaddedName(prefix: string, next: number, padZeros: number): string { + return prefix + next.toString().padStart(padZeros ?? 4, '0'); +} + +export default class SerialNumberSeries extends Doc { + validations: ValidationMap = { + name: (value) => { + if (typeof value !== 'string') { + return; + } + + if (invalidNumberSeries.test(value)) { + throw new ValidationError( + this.fyo + .t`The following characters cannot be used ${'/, ?, &, =, %'} in a Number Series name.` + ); + } + }, + }; + + setCurrent() { + let current = this.get('current') as number | null; + + /** + * Increment current if it isn't the first entry. This + * is to prevent reassignment of NumberSeries document ids. + */ + + if (!current) { + current = this.get('start') as number; + } else { + current = current + 1; + } + + this.current = current; + } + + async next(schemaName: string) { + this.setCurrent(); + const exists = await this.checkIfCurrentExists(schemaName); + + if (exists) { + this.current = (this.current as number) + 1; + } + + await this.sync(); + return this.getPaddedName(this.current as number); + } + + async checkIfCurrentExists(schemaName: string) { + if (!schemaName) { + return true; + } + + const name = this.getPaddedName(this.current as number); + return await this.fyo.db.exists(schemaName, name); + } + + getPaddedName(next: number): string { + return getPaddedName(this.name as string, next, this.padZeros as number); + } + + readOnly: ReadOnlyMap = { + referenceType: () => this.inserted, + padZeros: () => this.inserted, + start: () => this.inserted, + }; +} diff --git a/fyo/models/index.ts b/fyo/models/index.ts index a596c58a..79108b64 100644 --- a/fyo/models/index.ts +++ b/fyo/models/index.ts @@ -1,11 +1,13 @@ import { ModelMap } from 'fyo/model/types'; import NumberSeries from './NumberSeries'; +import SerialNumberSeries from './SerialNumberSeries'; import SystemSettings from './SystemSettings'; import { CustomField } from './CustomField'; import { CustomForm } from './CustomForm'; export const coreModels = { NumberSeries, + SerialNumberSeries, SystemSettings, CustomForm, CustomField, diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 419c394f..dbf58b21 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -29,6 +29,7 @@ export class Item extends Doc { itemGroup?: string; hsnCode?: number; hasSerialNumber?: boolean; + serialNumberSeries?: string; uomConversions: UOMConversionItem[] = []; formulas: FormulaMap = { @@ -92,6 +93,38 @@ export class Item extends Doc { }); this.uomConversions = Array.from(latestByUom.values()); + + if (this.serialNumberSeries && this.hasSerialNumber) { + const series = this.serialNumberSeries.trim(); + if (series && !series.endsWith('-')) { + this.serialNumberSeries = series + '-'; + } + } + } + + async afterSync(): Promise { + await super.afterSync(); + + if (this.hasSerialNumber && this.serialNumberSeries) { + const seriesName = this.serialNumberSeries?.trim(); + + if (!seriesName) { + return; + } + + const exists = await this.fyo.db.exists('SerialNumberSeries', seriesName); + + if (!exists) { + await this.fyo.doc + .getNewDoc('SerialNumberSeries', { + name: seriesName, + start: 1001, + padZeros: 4, + current: 1000, + }) + .sync(); + } + } } static filters: FiltersMap = { @@ -125,6 +158,21 @@ export class Item extends Doc { throw new ValidationError(this.fyo.t`Invalid HSN Code.`); } }, + serialNumberSeries: (value: DocValue) => { + if (!value) { + return; + } + + const series = (value as string).trim(); + const invalidChars = /[/\=\?\&\%]/; + + if (invalidChars.test(series)) { + throw new ValidationError( + this.fyo + .t`Serial Number Series cannot contain the following characters: /, ?, &, =, %` + ); + } + }, }; static getActions(fyo: Fyo): Action[] { @@ -177,6 +225,7 @@ export class Item extends Doc { !( this.fyo.singles.InventorySettings?.enableSerialNumber && this.trackItem ), + serialNumberSeries: () => !this.hasSerialNumber, uomConversions: () => !this.fyo.singles.InventorySettings?.enableUomConversions, itemGroup: () => !this.fyo.singles.AccountingSettings?.enableitemGroup, diff --git a/models/helpers.ts b/models/helpers.ts index 883b52bc..07e13435 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -48,6 +48,7 @@ import { getStockLedgerEntries, } from 'reports/inventory/helpers'; import { LoyaltyPointEntry } from './baseModels/LoyaltyPointEntry/LoyaltyPointEntry'; +import { generateSerialNumbersForItem } from './inventory/helpers'; export function getQuoteActions( fyo: Fyo, @@ -759,6 +760,16 @@ export async function addItem(name: string, doc: M) { } await item.set('item', name); + + if ( + doc instanceof StockTransfer && + doc.schemaName === ModelNameEnum.PurchaseReceipt + ) { + const serialNumbers = await generateSerialNumbersForItem(doc.fyo, name, 1); + if (serialNumbers) { + await item.set('serialNumber', serialNumbers); + } + } } export async function getReturnLoyaltyPoints(doc: Invoice) { diff --git a/models/inventory/StockMovementItem.ts b/models/inventory/StockMovementItem.ts index 6a501f8e..74dc5d7f 100644 --- a/models/inventory/StockMovementItem.ts +++ b/models/inventory/StockMovementItem.ts @@ -1,6 +1,7 @@ import { t } from 'fyo'; import { DocValue } from 'fyo/core/types'; import { + ChangeArg, FiltersMap, FormulaMap, HiddenMap, @@ -12,6 +13,7 @@ import { ValidationError } from 'fyo/utils/errors'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; import { safeParseFloat } from 'utils/index'; +import { generateSerialNumbersForItem } from './helpers'; import { StockMovement } from './StockMovement'; import { TransferItem } from './TransferItem'; import { MovementTypeEnum } from './types'; @@ -323,4 +325,50 @@ export class StockMovementItem extends TransferItem { static createFilters: FiltersMap = { item: () => ({ trackItem: true, itemType: 'Product' }), }; + + override async change(ch: ChangeArg): Promise { + await super.change(ch); + + const shouldGenerateSerialNumbers = + this.parentdoc?.movementType === MovementTypeEnum.MaterialReceipt && + this.item && + this.quantity && + this.quantity > 0; + + if (ch.changed === 'item') { + await this.set('serialNumber', ''); + + if (shouldGenerateSerialNumbers) { + await this.generateAndSetSerialNumbers(); + } + } + + if (ch.changed === 'quantity') { + if (!this.quantity || this.quantity <= 0) { + await this.set('serialNumber', ''); + } else if (shouldGenerateSerialNumbers) { + await this.generateAndSetSerialNumbers(); + } + } + } + + private async generateAndSetSerialNumbers(): Promise { + if (!this.item || !this.quantity) { + return; + } + + try { + const serialNumbers = await generateSerialNumbersForItem( + this.fyo, + this.item, + Math.abs(this.quantity) + ); + + if (serialNumbers) { + await this.set('serialNumber', serialNumbers); + } + } catch (error) { + // Don't throw - allow the user to manually enter serial numbers if auto-generation fails + } + } } diff --git a/models/inventory/StockTransfer.ts b/models/inventory/StockTransfer.ts index 6460b8df..34ec913b 100644 --- a/models/inventory/StockTransfer.ts +++ b/models/inventory/StockTransfer.ts @@ -25,6 +25,7 @@ import { updateSerialNumbers, validateBatch, validateSerialNumber, + generateSerialNumbersForItem, } from './helpers'; import { ReturnDocItem } from './types'; import { getShipmentCOGSAmountFromSLEs } from 'reports/inventory/helpers'; @@ -468,6 +469,32 @@ export abstract class StockTransfer extends Transfer { await this.set('terms', stDoc.terms); await this.set('date', stDoc.date); await this.set('items', stDoc.items); + + if (this.items) { + for (const item of this.items) { + if (!item.item || !item.quantity) { + continue; + } + + const hasSerialNumber = await this.fyo.getValue( + ModelNameEnum.Item, + item.item, + 'hasSerialNumber' + ); + + if (hasSerialNumber) { + const serialNumbers = await generateSerialNumbersForItem( + this.fyo, + item.item, + Math.abs(item.quantity) + ); + + if (serialNumbers) { + await item.set('serialNumber', serialNumbers); + } + } + } + } } async getInvoice(): Promise { diff --git a/models/inventory/StockTransferItem.ts b/models/inventory/StockTransferItem.ts index 09068e85..6a25767c 100644 --- a/models/inventory/StockTransferItem.ts +++ b/models/inventory/StockTransferItem.ts @@ -14,6 +14,10 @@ import { StockTransfer } from './StockTransfer'; import { TransferItem } from './TransferItem'; import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice'; import { PurchaseInvoice } from 'models/baseModels/PurchaseInvoice/PurchaseInvoice'; +import { + generateSerialNumbersForItem, + getExistingActiveSerialNumbersForItem, +} from './helpers'; export class StockTransferItem extends TransferItem { item?: string; @@ -243,6 +247,80 @@ export class StockTransferItem extends TransferItem { } }, }, + serialNumber: { + formula: async () => { + if (this.serialNumber) { + return this.serialNumber; + } + + if (!this.item || !this.parentdoc?.backReference) { + return undefined; + } + + const hasSerialNumber = await this.fyo.getValue( + ModelNameEnum.Item, + this.item, + 'hasSerialNumber' + ); + + if (!hasSerialNumber) { + return undefined; + } + + const quantity = Math.abs(this.quantity ?? 0); + if (quantity <= 0) { + return undefined; + } + + try { + if ( + !this.isSales && + this.parentdoc?.schemaName === ModelNameEnum.PurchaseReceipt + ) { + const serialNumbers = await generateSerialNumbersForItem( + this.fyo, + this.item, + quantity + ); + + if (serialNumbers) { + return serialNumbers; + } + } + + if ( + this.isSales && + this.parentdoc?.schemaName === ModelNameEnum.Shipment + ) { + const salesInvoice = (await this.fyo.doc.getDoc( + ModelNameEnum.SalesInvoice, + this.parentdoc.backReference + )) as SalesInvoice; + + const invoiceItem = salesInvoice?.items?.find( + (val) => val.item === this.item + ); + + if (invoiceItem?.serialNumber) { + return invoiceItem.serialNumber as string; + } + + const serialNumbers = await getExistingActiveSerialNumbersForItem( + this.fyo, + this.item, + quantity + ); + + if (serialNumbers) { + return serialNumbers; + } + } + } catch (error) {} + + return undefined; + }, + dependsOn: ['item', 'quantity'], + }, }; validations: ValidationMap = { diff --git a/models/inventory/helpers.ts b/models/inventory/helpers.ts index d5d200db..fc272526 100644 --- a/models/inventory/helpers.ts +++ b/models/inventory/helpers.ts @@ -6,11 +6,12 @@ import { ModelNameEnum } from 'models/types'; import { SerialNumber } from './SerialNumber'; import type { StockMovement } from './StockMovement'; import type { StockMovementItem } from './StockMovementItem'; -import type { StockTransfer } from './StockTransfer'; +import { StockTransfer } from './StockTransfer'; import type { StockTransferItem } from './StockTransferItem'; import { Transfer } from './Transfer'; import { TransferItem } from './TransferItem'; import type { SerialNumberStatus } from './types'; +import SerialNumberSeries from 'fyo/models/SerialNumberSeries'; export async function validateBatch( doc: StockMovement | StockTransfer | Invoice @@ -340,3 +341,208 @@ function getSerialNumberStatusForStockMovement( return isCancel ? 'Inactive' : 'Active'; } + +export async function generateSerialNumbersForItem( + fyo: Fyo, + item: string, + quantity: number +): Promise { + if (!quantity || quantity <= 0) { + return ''; + } + + const hasSerialNumber = await fyo.getValue( + ModelNameEnum.Item, + item, + 'hasSerialNumber' + ); + + if (!hasSerialNumber) { + return ''; + } + + const serialNumberSeries = await fyo.getValue( + ModelNameEnum.Item, + item, + 'serialNumberSeries' + ); + + if (!serialNumberSeries || typeof serialNumberSeries !== 'string') { + return ''; + } + + const seriesName = serialNumberSeries.trim(); + if (!seriesName) { + return ''; + } + + const exists = await fyo.db.exists( + ModelNameEnum.SerialNumberSeries, + seriesName + ); + + if (!exists) { + return ''; + } + + const seriesDoc = (await fyo.doc.getDoc( + ModelNameEnum.SerialNumberSeries, + seriesName + )) as SerialNumberSeries; + + const serialNumbers: string[] = []; + const padZeros = seriesDoc.padZeros as number; + + let currentValue = await getHighestSerialNumberForItem(fyo, item, seriesName); + + if (currentValue === null) { + currentValue = + ((seriesDoc.current as number) || (seriesDoc.start as number)) - 1; + } + + let generatedCount = 0; + let attempts = 0; + const maxAttempts = quantity * 10; + + while (generatedCount < quantity && attempts < maxAttempts) { + attempts++; + currentValue++; + + const serialNumber = getPaddedName(seriesName, currentValue, padZeros); + + const snExists = await fyo.db.exists( + ModelNameEnum.SerialNumber, + serialNumber + ); + + if (!snExists) { + serialNumbers.push(serialNumber); + generatedCount++; + } + } + + if (generatedCount < quantity) { + } + + if (serialNumbers.length > 0) { + await seriesDoc.set('current', currentValue); + await seriesDoc.sync(); + } + + const result = serialNumbers.join('\n'); + return result; +} + +async function getHighestSerialNumberForItem( + fyo: Fyo, + item: string, + seriesName: string +): Promise { + const serialNumbers = await fyo.db.getAllRaw(ModelNameEnum.SerialNumber, { + filters: { item: item }, + fields: ['name'], + }); + + if (!serialNumbers || serialNumbers.length === 0) { + return null; + } + + let highestValue = -1; + + for (const sn of serialNumbers) { + const name = sn.name as string; + + if (name.startsWith(seriesName)) { + const numericPart = name.substring(seriesName.length); + const value = parseInt(numericPart, 10); + + if (!isNaN(value) && value > highestValue) { + highestValue = value; + } + } + } + + return highestValue >= 0 ? highestValue : null; +} + +function getPaddedName(prefix: string, next: number, padZeros: number): string { + return prefix + next.toString().padStart(padZeros ?? 4, '0'); +} + +export async function getExistingActiveSerialNumbersForItem( + fyo: Fyo, + item: string, + quantity: number +): Promise { + if (!quantity || quantity <= 0) { + return ''; + } + + const hasSerialNumber = await fyo.getValue( + ModelNameEnum.Item, + item, + 'hasSerialNumber' + ); + + if (!hasSerialNumber) { + return ''; + } + + const activeSerialNumbers = await fyo.db.getAll(ModelNameEnum.SerialNumber, { + fields: ['name'], + filters: { item: item, status: 'Active' }, + orderBy: 'name', + order: 'asc', + }); + + if (!activeSerialNumbers || activeSerialNumbers.length === 0) { + return ''; + } + + const usedInShipments = await fyo.db.getAll(ModelNameEnum.ShipmentItem, { + fields: ['serialNumber'], + filters: { + item: item, + }, + }); + + const usedInPurchaseReceipts = await fyo.db.getAll( + ModelNameEnum.PurchaseReceiptItem, + { + fields: ['serialNumber'], + filters: { + item: item, + }, + } + ); + + const usedSerialNumbers = new Set(); + + usedInShipments.forEach((shipmentItem) => { + if (shipmentItem.serialNumber) { + const serialNumArray = getSerialNumbers( + String(shipmentItem.serialNumber) + ); + serialNumArray.forEach((sn) => usedSerialNumbers.add(sn)); + } + }); + + usedInPurchaseReceipts.forEach((receiptItem) => { + if (receiptItem.serialNumber) { + const serialNumArray = getSerialNumbers(String(receiptItem.serialNumber)); + serialNumArray.forEach((sn) => usedSerialNumbers.add(sn)); + } + }); + + const availableSerialNumbers = activeSerialNumbers + .map((sn) => String(sn.name)) + .filter((snName) => !usedSerialNumbers.has(snName)); + + if (availableSerialNumbers.length === 0) { + return ''; + } + + const selectedSerialNumbers = availableSerialNumbers.slice(0, quantity); + + return selectedSerialNumbers.join('\n'); +} diff --git a/models/types.ts b/models/types.ts index 444cda89..710293c7 100644 --- a/models/types.ts +++ b/models/types.ts @@ -28,6 +28,7 @@ export enum ModelNameEnum { JournalEntryAccount = 'JournalEntryAccount', Misc = 'Misc', NumberSeries = 'NumberSeries', + SerialNumberSeries = 'SerialNumberSeries', Lead = 'Lead', Party = 'Party', LoyaltyProgram = 'LoyaltyProgram', diff --git a/reports/inventory/StockBalance.ts b/reports/inventory/StockBalance.ts index 49096868..d45eb838 100644 --- a/reports/inventory/StockBalance.ts +++ b/reports/inventory/StockBalance.ts @@ -6,7 +6,7 @@ import { ColumnField, ReportData } from 'reports/types'; import { Field } from 'schemas/types'; import { getStockBalanceEntries } from './helpers'; import { StockLedger } from './StockLedger'; -import { ReferenceType } from './types'; +import { ReferenceType, SerialNumberStatus } from './types'; export class StockBalance extends StockLedger { static title = t`Stock Balance`; @@ -17,6 +17,9 @@ export class StockBalance extends StockLedger { override referenceType: ReferenceType = 'All'; override referenceName = ''; + showSerialNumbers = false; + serialNumberFilter: SerialNumberStatus = 'All'; + override async _getReportData(force?: boolean): Promise { if (this.shouldRefresh || force || !this._rawData?.length) { await this._setRawData(); @@ -30,7 +33,12 @@ export class StockBalance extends StockLedger { toDate: this.toDate, }; - const rawData = getStockBalanceEntries(this._rawData ?? [], filters); + const rawData = getStockBalanceEntries( + this._rawData ?? [], + filters, + this.showSerialNumbers, + this.serialNumberFilter + ); return rawData.map((sbe, i) => { const row = { ...sbe, name: i + 1 } as RawValueMap; @@ -81,12 +89,53 @@ export class StockBalance extends StockLedger { label: t`To Date`, fieldname: 'toDate', }, + { + fieldtype: 'Check', + placeholder: t`Serial Number`, + label: t`Serial Number`, + fieldname: 'showSerialNumbers', + }, + ...(this.showSerialNumbers + ? ([ + { + fieldtype: 'Select', + options: [ + { label: t`All`, value: 'All' }, + { label: t`In stock`, value: 'In stock' }, + { label: t`Out stock`, value: 'Out stock' }, + ], + placeholder: t`Serial Number Status`, + label: t`Serial Number Status`, + fieldname: 'serialNumberFilter', + default: 'All', + }, + ] as Field[]) + : []), ] as Field[]; return filters; } getColumns(): ColumnField[] { + const batch: ColumnField[] = []; + const serialNumber: ColumnField[] = []; + + if (this.hasBatches) { + batch.push({ + fieldname: 'batch', + label: 'Batch', + fieldtype: 'Link', + }); + } + + if (this.showSerialNumbers && this.hasSerialNumbers) { + serialNumber.push({ + fieldname: 'serialNumber', + label: 'Serial Number', + fieldtype: 'Data', + }); + } + return [ { fieldname: 'name', @@ -104,11 +153,8 @@ export class StockBalance extends StockLedger { label: 'Location', fieldtype: 'Link', }, - ...(this.hasBatches - ? ([ - { fieldname: 'batch', label: 'Batch', fieldtype: 'Link' }, - ] as ColumnField[]) - : []), + ...batch, + ...serialNumber, { fieldname: 'balanceQuantity', label: 'Balance Qty.', diff --git a/reports/inventory/helpers.ts b/reports/inventory/helpers.ts index 9ff9ce7a..2de59538 100644 --- a/reports/inventory/helpers.ts +++ b/reports/inventory/helpers.ts @@ -6,6 +6,7 @@ import { safeParseFloat, safeParseInt } from 'utils/index'; import type { ComputedStockLedgerEntry, RawStockLedgerEntry, + SerialNumberStatus, StockBalanceEntry, } from './types'; import type { QueryFilter } from 'utils/db/types'; @@ -192,11 +193,13 @@ export function getStockBalanceEntries( fromDate?: string; toDate?: string; batch?: string; - } + }, + showSerialNumbers = false, + serialNumberFilter: SerialNumberStatus = 'All' ): StockBalanceEntry[] { const sbeMap: Record< Item, - Record> + Record>> > = {}; const fromDate = filters.fromDate ? Date.parse(filters.fromDate) : null; @@ -215,19 +218,29 @@ export function getStockBalanceEntries( continue; } + if ( + showSerialNumbers && + (!sle.serialNumber || sle.serialNumber.trim() === '') + ) { + continue; + } + const batch = sle.batch || ''; + const serialNumber = showSerialNumbers ? sle.serialNumber : ''; sbeMap[sle.item] ??= {}; sbeMap[sle.item][sle.location] ??= {}; - sbeMap[sle.item][sle.location][batch] ??= getSBE( + sbeMap[sle.item][sle.location][batch] ??= {}; + sbeMap[sle.item][sle.location][batch][serialNumber] ??= getSBE( sle.item, sle.location, - batch + batch, + serialNumber ); const date = sle.date.valueOf(); if (fromDate && date < fromDate) { - const sbe = sbeMap[sle.item][sle.location][batch]; + const sbe = sbeMap[sle.item][sle.location][batch][serialNumber]; updateOpeningBalances(sbe, sle); continue; } @@ -236,21 +249,33 @@ export function getStockBalanceEntries( continue; } - const sbe = sbeMap[sle.item][sle.location][batch]; + const sbe = sbeMap[sle.item][sle.location][batch][serialNumber]; updateCurrentBalances(sbe, sle); } - return Object.values(sbeMap) + let entries = Object.values(sbeMap) .map((sbeBatched) => - Object.values(sbeBatched).map((sbes) => Object.values(sbes)) + Object.values(sbeBatched).map((sbes) => + Object.values(sbes).map((sbeWithSN) => Object.values(sbeWithSN)) + ) ) - .flat(2); + .flat(3); + + // Filter by serial number status + if (serialNumberFilter === 'In stock') { + entries = entries.filter((entry) => entry.balanceQuantity > 0); + } else if (serialNumberFilter === 'Out stock') { + entries = entries.filter((entry) => entry.balanceQuantity <= 0); + } + + return entries; } function getSBE( item: string, location: string, - batch: string + batch: string, + serialNumber: string ): StockBalanceEntry { return { name: 0, @@ -258,6 +283,7 @@ function getSBE( item, location, batch, + serialNumber, balanceQuantity: 0, balanceValue: 0, diff --git a/reports/inventory/types.ts b/reports/inventory/types.ts index fce7c705..43a54cbf 100644 --- a/reports/inventory/types.ts +++ b/reports/inventory/types.ts @@ -44,16 +44,17 @@ export interface StockBalanceEntry{ item: string; location:string; batch: string; + serialNumber: string; balanceQuantity: number; balanceValue: number; - + openingQuantity: number; openingValue:number; - + incomingQuantity:number; incomingValue:number; - + outgoingQuantity:number; outgoingValue:number; @@ -65,3 +66,6 @@ export type ReferenceType = | ModelNameEnum.Shipment | ModelNameEnum.PurchaseReceipt | 'All'; + +export type SerialNumberStatus = 'All' | 'In stock' | 'Out stock'; + diff --git a/schemas/app/Item.json b/schemas/app/Item.json index 79545a94..02a483fa 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -160,6 +160,12 @@ "default": false, "section": "Inventory" }, + { + "fieldname": "serialNumberSeries", + "label": "Serial Number Series", + "fieldtype": "Data", + "section": "Inventory" + }, { "fieldname": "uomConversions", "label": "UOM Conversions", diff --git a/schemas/app/SerialNumberSeries.json b/schemas/app/SerialNumberSeries.json new file mode 100644 index 00000000..3adda203 --- /dev/null +++ b/schemas/app/SerialNumberSeries.json @@ -0,0 +1,38 @@ +{ + "name": "SerialNumberSeries", + "label": "Serial Number Series", + "naming": "manual", + "isSingle": false, + "isChild": false, + "fields": [ + { + "fieldname": "name", + "label": "Prefix", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "start", + "label": "Start", + "fieldtype": "Int", + "default": 1001, + "required": true, + "minvalue": 0 + }, + { + "fieldname": "padZeros", + "label": "Pad Zeros", + "fieldtype": "Int", + "default": 4, + "required": true + }, + { + "fieldname": "current", + "label": "Current", + "fieldtype": "Int", + "required": true, + "readOnly": true + } + ], + "quickEditFields": ["start", "padZeros"] +} diff --git a/schemas/schemas.ts b/schemas/schemas.ts index 364c3572..2b7b25e0 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -14,6 +14,7 @@ import JournalEntry from './app/JournalEntry.json'; import JournalEntryAccount from './app/JournalEntryAccount.json'; import Misc from './app/Misc.json'; import NumberSeries from './app/NumberSeries.json'; +import SerialNumberSeries from './app/SerialNumberSeries.json'; import Party from './app/Party.json'; import Lead from './app/Lead.json'; import LoyaltyProgram from './app/LoyaltyProgram.json'; @@ -106,6 +107,7 @@ export const appSchemas: Schema[] | SchemaStub[] = [ Currency as Schema, Defaults as Schema, NumberSeries as Schema, + SerialNumberSeries as Schema, PrintSettings as Schema,