diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index ee830635..87c3ab7b 100644 --- a/backend/database/bespoke.ts +++ b/backend/database/bespoke.ts @@ -137,7 +137,8 @@ export class BespokeQueries { item: string, location?: string, fromDate?: string, - toDate?: string + toDate?: string, + batchNumber?: string ): Promise { const query = db.knex!(ModelNameEnum.StockLedgerEntry) .sum('quantity') @@ -147,6 +148,10 @@ export class BespokeQueries { query.andWhere('location', location); } + if (batchNumber) { + query.andWhere('batchNumber', batchNumber); + } + if (fromDate) { query.andWhereRaw('datetime(date) > datetime(?)', [fromDate]); } diff --git a/fyo/core/dbHandler.ts b/fyo/core/dbHandler.ts index c02d826e..0193aea4 100644 --- a/fyo/core/dbHandler.ts +++ b/fyo/core/dbHandler.ts @@ -10,7 +10,7 @@ import { DatabaseBase, DatabaseDemuxBase, GetAllOptions, - QueryFilter + QueryFilter, } from 'utils/db/types'; import { schemaTranslateables } from 'utils/translationHelpers'; import { LanguageMap } from 'utils/types'; @@ -19,7 +19,7 @@ import { DatabaseDemuxConstructor, DocValue, DocValueMap, - RawValueMap + RawValueMap, } from './types'; // Return types of Bespoke Queries @@ -312,14 +312,16 @@ export class DatabaseHandler extends DatabaseBase { item: string, location?: string, fromDate?: string, - toDate?: string + toDate?: string, + batchNumber?: string ): Promise { return (await this.#demux.callBespoke( 'getStockQuantity', item, location, fromDate, - toDate + toDate, + batchNumber )) as number | null; } diff --git a/models/baseModels/BatchNumber/BatchNumber.ts b/models/baseModels/BatchNumber/BatchNumber.ts new file mode 100644 index 00000000..0df86c78 --- /dev/null +++ b/models/baseModels/BatchNumber/BatchNumber.ts @@ -0,0 +1,13 @@ +import { Doc } from 'fyo/model/doc'; +import { + ListViewSettings, +} from 'fyo/model/types'; + +export class BatchNumber extends Doc { + static getListViewSettings(): ListViewSettings { + return { + columns: ["name", "expiryDate", "manufactureDate"], + }; + } + +} diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 7a47d9f2..c7a6b8e1 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -11,6 +11,7 @@ import { ValidationMap, } from 'fyo/model/types'; import { ValidationError } from 'fyo/utils/errors'; +import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; import { AccountRootTypeEnum, AccountTypeEnum } from '../Account/types'; @@ -18,6 +19,7 @@ export class Item extends Doc { trackItem?: boolean; itemType?: 'Product' | 'Service'; for?: 'Purchases' | 'Sales' | 'Both'; + hasBatchNumber?: boolean; formulas: FormulaMap = { incomeAccount: { @@ -74,6 +76,29 @@ export class Item extends Doc { throw new ValidationError(this.fyo.t`Rate can't be negative.`); } }, + hasBatchNumber: async (value: DocValue) => { + const { hasBatchNumber } = await this.fyo.db.get( + 'Item', + this.name!, + 'hasBatchNumber' + ); + + if (hasBatchNumber && value !== hasBatchNumber) { + const itemEntriesInSLE = await this.fyo.db.count( + ModelNameEnum.StockLedgerEntry, + { + filters: { item: this.name! }, + } + ); + + if (itemEntriesInSLE > 0) { + throw new ValidationError( + this.fyo.t`Cannot change value of Has Batch No as Item ${this + .name!} already has transactions against it. ` + ); + } + } + }, }; static getActions(fyo: Fyo): Action[] { @@ -121,6 +146,7 @@ export class Item extends Doc { !this.fyo.singles.AccountingSettings?.enableInventory || this.itemType !== 'Product' || (this.inserted && !this.trackItem), + hasBatchNumber: () => !this.trackItem, barcode: () => !this.fyo.singles.InventorySettings?.enableBarcodes, uomConversions: () => !this.fyo.singles.AccountingSettings?.enableInventory, }; @@ -129,5 +155,6 @@ export class Item extends Doc { unit: () => this.inserted, itemType: () => this.inserted, trackItem: () => this.inserted, + hasBatchNumber: () => this.inserted, }; } diff --git a/models/index.ts b/models/index.ts index 1396de44..c12eb802 100644 --- a/models/index.ts +++ b/models/index.ts @@ -26,12 +26,14 @@ import { ShipmentItem } from './inventory/ShipmentItem'; import { StockLedgerEntry } from './inventory/StockLedgerEntry'; import { StockMovement } from './inventory/StockMovement'; import { StockMovementItem } from './inventory/StockMovementItem'; +import { BatchNumber } from './baseModels/BatchNumber/BatchNumber'; export const models = { Account, AccountingLedgerEntry, AccountingSettings, Address, + BatchNumber, Defaults, Item, JournalEntry, diff --git a/models/inventory/StockLedgerEntry.ts b/models/inventory/StockLedgerEntry.ts index d137bfea..49e3ec38 100644 --- a/models/inventory/StockLedgerEntry.ts +++ b/models/inventory/StockLedgerEntry.ts @@ -10,4 +10,5 @@ export class StockLedgerEntry extends Doc { location?: string; referenceName?: string; referenceType?: string; + batchNumber?: string; } diff --git a/models/inventory/StockManager.ts b/models/inventory/StockManager.ts index 20d86813..f5d0adc5 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -132,16 +132,54 @@ export class StockManager { } const date = details.date.toISOString(); + const formattedDate = this.fyo.format(details.date, 'Datetime'); + const isItemHasBatchNumber = await this.fyo.getValue( + 'Item', + details.item, + 'hasBatchNumber' + ); + + if (isItemHasBatchNumber && !this.isCancelled) { + if (!details.batchNumber) { + throw new ValidationError( + t`Please enter Batch Number for ${details.item}` + ); + } + + const itemsInBatch = + (await this.fyo.db.getStockQuantity( + details.item, + details.fromLocation, + undefined, + date, + details.batchNumber + )) ?? 0; + + if (details.quantity <= itemsInBatch) return; + + throw new ValidationError( + [ + t`Insufficient Quantity`, + t`Additional quantity (${ + details.quantity - itemsInBatch + }) is required in batch ${ + details.batchNumber + } to make the outward transfer of item ${details.item} from ${ + details.fromLocation + } on ${formattedDate}`, + ].join('\n') + ); + } + let quantityBefore = (await this.fyo.db.getStockQuantity( details.item, details.fromLocation, undefined, - date + date, + undefined )) ?? 0; - const formattedDate = this.fyo.format(details.date, 'Datetime'); - if (this.isCancelled) { quantityBefore += details.quantity; } @@ -162,7 +200,9 @@ export class StockManager { const quantityAfter = await this.fyo.db.getStockQuantity( details.item, details.fromLocation, - details.date.toISOString() + details.date.toISOString(), + undefined, + undefined ); if (quantityAfter === null) { // No future transactions @@ -204,6 +244,7 @@ class StockManagerItem { referenceType: string; fromLocation?: string; toLocation?: string; + batchNumber?: string; stockLedgerEntries?: StockLedgerEntry[]; @@ -218,6 +259,7 @@ class StockManagerItem { this.toLocation = details.toLocation; this.referenceName = details.referenceName; this.referenceType = details.referenceType; + this.batchNumber = details.batchNumber; this.fyo = fyo; } @@ -270,6 +312,7 @@ class StockManagerItem { date: this.date, item: this.item, rate: this.rate, + batchNumber: this.batchNumber, quantity, location, referenceName: this.referenceName, diff --git a/models/inventory/StockMovement.ts b/models/inventory/StockMovement.ts index b6d19a65..bc8d13c4 100644 --- a/models/inventory/StockMovement.ts +++ b/models/inventory/StockMovement.ts @@ -109,6 +109,7 @@ export class StockMovement extends Transfer { item: row.item!, rate: row.rate!, quantity: row.quantity!, + batchNumber: row.batchNumber!, fromLocation: row.fromLocation, toLocation: row.toLocation, })); diff --git a/models/inventory/StockMovementItem.ts b/models/inventory/StockMovementItem.ts index 107fa509..f1b385fb 100644 --- a/models/inventory/StockMovementItem.ts +++ b/models/inventory/StockMovementItem.ts @@ -30,6 +30,7 @@ export class StockMovementItem extends Doc { rate?: Money; amount?: Money; parentdoc?: StockMovement; + batchNumber?: string; get isIssue() { return this.parentdoc?.movementType === MovementType.MaterialIssue; diff --git a/models/inventory/StockTransfer.ts b/models/inventory/StockTransfer.ts index fce4f61e..fcbceaf7 100644 --- a/models/inventory/StockTransfer.ts +++ b/models/inventory/StockTransfer.ts @@ -82,6 +82,7 @@ export abstract class StockTransfer extends Transfer { item: row.item!, rate: row.rate!, quantity: row.quantity!, + batchNumber: row.batchNumber!, fromLocation, toLocation, }; diff --git a/models/inventory/StockTransferItem.ts b/models/inventory/StockTransferItem.ts index 86883161..96f63547 100644 --- a/models/inventory/StockTransferItem.ts +++ b/models/inventory/StockTransferItem.ts @@ -21,6 +21,7 @@ export class StockTransferItem extends Doc { amount?: Money; description?: string; hsnCode?: number; + batchNumber?: string formulas: FormulaMap = { description: { diff --git a/models/inventory/tests/helpers.ts b/models/inventory/tests/helpers.ts index f37d8ffd..607dbe8b 100644 --- a/models/inventory/tests/helpers.ts +++ b/models/inventory/tests/helpers.ts @@ -1,4 +1,5 @@ import { Fyo } from 'fyo'; +import { BatchNumber } from 'models/baseModels/BatchNumber/BatchNumber'; import { ModelNameEnum } from 'models/types'; import { StockMovement } from '../StockMovement'; import { StockTransfer } from '../StockTransfer'; @@ -26,6 +27,7 @@ type Transfer = { item: string; from?: string; to?: string; + batchNumber?: string; quantity: number; rate: number; }; @@ -34,8 +36,23 @@ interface TransferTwo extends Omit { location: string; } -export function getItem(name: string, rate: number) { - return { name, rate, trackItem: true }; +export function getItem(name: string, rate: number, hasBatchNumber?: boolean) { + return { name, rate, trackItem: true, hasBatchNumber }; +} + +export async function getBatchNumber( + schemaName: ModelNameEnum.BatchNumber, + batchNumber: string, + expiryDate: Date, + manufactureDate: Date, + fyo: Fyo +): Promise { + const doc = fyo.doc.getNewDoc(schemaName, { + batchNumber, + expiryDate, + manufactureDate, + }) as BatchNumber; + return doc; } export async function getStockTransfer( @@ -62,11 +79,11 @@ export async function getStockMovement( movementType, date, }) as StockMovement; - for (const { item, from: fromLocation, to: toLocation, + batchNumber: batchNumber, quantity, rate, } of transfers) { @@ -74,6 +91,7 @@ export async function getStockMovement( item, fromLocation, toLocation, + batchNumber, rate, quantity, }); diff --git a/models/inventory/tests/testInventory.spec.ts b/models/inventory/tests/testInventory.spec.ts index e8a8cb1e..008c8369 100644 --- a/models/inventory/tests/testInventory.spec.ts +++ b/models/inventory/tests/testInventory.spec.ts @@ -1,6 +1,6 @@ import { assertDoesNotThrow, - assertThrows + assertThrows, } from 'backend/database/tests/helpers'; import { ModelNameEnum } from 'models/types'; import { default as tape, default as test } from 'tape'; @@ -17,10 +17,12 @@ const itemMap = { Pen: { name: 'Pen', rate: 700, + hasBatchNumber: true, }, Ink: { name: 'Ink', rate: 50, + hasBatchNumber: false, }, }; @@ -29,14 +31,22 @@ const locationMap = { LocationTwo: 'LocationTwo', }; +const batchNumberMap = { + batchNumberOne: { + name: 'IK-AB001', + manufactureDate: '2022-11-03T09:57:04.528', + expiryDate: '2023-11-03T09:57:04.528', + }, +}; + /** * Section 1: Test Creation of Items and Locations */ test('create dummy items & locations', async (t) => { // Create Items - for (const { name, rate } of Object.values(itemMap)) { - const item = getItem(name, rate); + for (const { name, rate, hasBatchNumber } of Object.values(itemMap)) { + const item = getItem(name, rate, hasBatchNumber); await fyo.doc.getNewDoc(ModelNameEnum.Item, item).sync(); t.ok(await fyo.db.exists(ModelNameEnum.Item, name), `${name} exists`); } @@ -48,6 +58,25 @@ test('create dummy items & locations', async (t) => { } }); +test('create dummy batch numbers', async (t) => { + // create batchNumber + for (const { name, manufactureDate, expiryDate } of Object.values( + batchNumberMap + )) { + await fyo.doc + .getNewDoc(ModelNameEnum.BatchNumber, { + name, + expiryDate, + manufactureDate, + }) + .sync(); + t.ok( + await fyo.db.exists(ModelNameEnum.BatchNumber, name), + `${name} exists` + ); + } +}); + /** * Section 2: Test Creation of Stock Movements */ @@ -90,6 +119,55 @@ test('create stock movement, material receipt', async (t) => { t.equal(await fyo.db.getStockQuantity(itemMap.Ink.name), quantity); }); +test('Batch Enabled : create stock movement, material receipt', async (t) => { + const { rate } = itemMap.Pen; + const quantity = 2; + const batchNumber = batchNumberMap.batchNumberOne.name; + const amount = rate * quantity; + const stockMovement = await getStockMovement( + MovementType.MaterialReceipt, + new Date('2022-11-03T09:57:04.528'), + [ + { + item: itemMap.Pen.name, + to: locationMap.LocationOne, + quantity, + batchNumber, + rate, + }, + ], + fyo + ); + + await (await stockMovement.sync()).submit(); + t.ok(stockMovement.name?.startsWith('SMOV-')); + t.equal(stockMovement.amount?.float, amount); + t.equal(stockMovement.items?.[0].amount?.float, amount); + + const name = stockMovement.name!; + + const sles = await getSLEs(name, ModelNameEnum.StockMovement, fyo); + t.equal(sles.length, 1); + + const sle = sles[0]; + t.notEqual(new Date(sle.date).toString(), 'Invalid Date'); + t.equal(parseInt(sle.name), 2); + t.equal(sle.item, itemMap.Pen.name); + t.equal(parseFloat(sle.rate), rate); + t.equal(sle.quantity, quantity); + t.equal(sle.location, locationMap.LocationOne); + t.equal( + await fyo.db.getStockQuantity( + itemMap.Pen.name, + locationMap.LocationOne, + undefined, + undefined, + batchNumber + ), + quantity + ); +}); + test('create stock movement, material transfer', async (t) => { const { rate } = itemMap.Ink; const quantity = 2; @@ -136,6 +214,69 @@ test('create stock movement, material transfer', async (t) => { t.equal(await fyo.db.getStockQuantity(itemMap.Ink.name), quantity); }); +test('Batch Enabled create stock movement, material transfer', async (t) => { + const { rate } = itemMap.Pen; + const quantity = 2; + const batchNumber = batchNumberMap.batchNumberOne.name; + + const stockMovement = await getStockMovement( + MovementType.MaterialTransfer, + new Date('2022-11-03T09:58:04.528'), + [ + { + item: itemMap.Pen.name, + from: locationMap.LocationOne, + to: locationMap.LocationTwo, + batchNumber, + quantity, + rate, + }, + ], + fyo + ); + + await (await stockMovement.sync()).submit(); + const name = stockMovement.name!; + + const sles = await getSLEs(name, ModelNameEnum.StockMovement, fyo); + t.equal(sles.length, 2); + + for (const sle of sles) { + t.notEqual(new Date(sle.date).toString(), 'Invalid Date'); + t.equal(sle.item, itemMap.Pen.name); + t.equal(parseFloat(sle.rate), rate); + + if (sle.location === locationMap.LocationOne) { + t.equal(sle.quantity, -quantity); + } else if (sle.location === locationMap.LocationTwo) { + t.equal(sle.quantity, quantity); + } else { + t.ok(false, 'no-op'); + } + } + + t.equal( + await fyo.db.getStockQuantity( + itemMap.Pen.name, + locationMap.LocationOne, + undefined, + undefined, + batchNumber + ), + 0 + ); + t.equal( + await fyo.db.getStockQuantity( + itemMap.Pen.name, + locationMap.LocationTwo, + undefined, + undefined, + batchNumber + ), + quantity + ); +}); + test('create stock movement, material issue', async (t) => { const { rate } = itemMap.Ink; const quantity = 2; @@ -169,6 +310,50 @@ test('create stock movement, material issue', async (t) => { t.equal(await fyo.db.getStockQuantity(itemMap.Ink.name), 0); }); +test('Batch Enabled create stock movement, material issue', async (t) => { + const { rate } = itemMap.Pen; + const quantity = 2; + const batchNumber = batchNumberMap.batchNumberOne.name; + + const stockMovement = await getStockMovement( + MovementType.MaterialIssue, + new Date('2022-11-03T09:59:04.528'), + [ + { + item: itemMap.Pen.name, + from: locationMap.LocationTwo, + batchNumber, + quantity, + rate, + }, + ], + fyo + ); + + await (await stockMovement.sync()).submit(); + const name = stockMovement.name!; + + const sles = await getSLEs(name, ModelNameEnum.StockMovement, fyo); + t.equal(sles.length, 1); + + const sle = sles[0]; + t.notEqual(new Date(sle.date).toString(), 'Invalid Date'); + t.equal(sle.item, itemMap.Pen.name); + t.equal(parseFloat(sle.rate), rate); + t.equal(sle.quantity, -quantity); + t.equal(sle.location, locationMap.LocationTwo); + t.equal( + await fyo.db.getStockQuantity( + itemMap.Pen.name, + undefined, + undefined, + undefined, + batchNumber + ), + 0 + ); +}); + /** * Section 3: Test Cancellation of Stock Movements */ @@ -214,6 +399,7 @@ async function runEntries( item: string; to?: string; from?: string; + batchNumber?: string; quantity: number; rate: number; }[]; @@ -235,7 +421,7 @@ async function runEntries( } test('create stock movements, invalid entries, in sequence', async (t) => { - const { name: item, rate } = itemMap.Ink; + const { name: item, rate } = itemMap.Pen; const quantity = 10; await runEntries( item, @@ -249,6 +435,7 @@ test('create stock movements, invalid entries, in sequence', async (t) => { { item, to: locationMap.LocationOne, + batchNumber: batchNumberMap.batchNumberOne.name, quantity, rate, }, @@ -264,6 +451,7 @@ test('create stock movements, invalid entries, in sequence', async (t) => { item, from: locationMap.LocationOne, to: locationMap.LocationTwo, + batchNumber: batchNumberMap.batchNumberOne.name, quantity: quantity + 1, rate, }, @@ -278,6 +466,7 @@ test('create stock movements, invalid entries, in sequence', async (t) => { { item, from: locationMap.LocationOne, + batchNumber: batchNumberMap.batchNumberOne.name, quantity: quantity + 1, rate, }, @@ -292,6 +481,7 @@ test('create stock movements, invalid entries, in sequence', async (t) => { { item, from: locationMap.LocationOne, + batchNumber: batchNumberMap.batchNumberOne.name, to: locationMap.LocationTwo, quantity, rate, @@ -307,6 +497,7 @@ test('create stock movements, invalid entries, in sequence', async (t) => { { item, from: locationMap.LocationTwo, + batchNumber: batchNumberMap.batchNumberOne.name, quantity, rate, }, @@ -371,4 +562,28 @@ test('create stock movements, invalid entries, out of sequence', async (t) => { ); }); +test('create stock movements, material issue, insufficient quantity', async (t) => { + const { name, rate } = itemMap.Pen; + const quantity = 2; + const batchNumber = batchNumberMap.batchNumberOne.name; + + const stockMovement = await getStockMovement( + MovementType.MaterialIssue, + new Date('2022-11-03T09:59:04.528'), + [ + { + item: itemMap.Pen.name, + from: locationMap.LocationTwo, + batchNumber, + quantity, + rate, + }, + ], + fyo + ); + + await assertThrows(async () => (await stockMovement.sync()).submit()); + t.equal(await fyo.db.getStockQuantity(name), 0); +}); + closeTestFyo(fyo, __filename); diff --git a/models/inventory/types.ts b/models/inventory/types.ts index 80ecb81d..c07bc3e4 100644 --- a/models/inventory/types.ts +++ b/models/inventory/types.ts @@ -22,6 +22,7 @@ export interface SMTransferDetails { item: string; rate: Money; quantity: number; + batchNumber?: string; fromLocation?: string; toLocation?: string; } diff --git a/models/types.ts b/models/types.ts index b0db6d93..61cbaf02 100644 --- a/models/types.ts +++ b/models/types.ts @@ -4,6 +4,7 @@ export enum ModelNameEnum { AccountingLedgerEntry = 'AccountingLedgerEntry', AccountingSettings = 'AccountingSettings', Address = 'Address', + BatchNumber= 'BatchNumber', Color = 'Color', CompanySettings = 'CompanySettings', Currency = 'Currency', diff --git a/reports/inventory/StockLedger.ts b/reports/inventory/StockLedger.ts index 6bd81ecf..2150ed7a 100644 --- a/reports/inventory/StockLedger.ts +++ b/reports/inventory/StockLedger.ts @@ -260,6 +260,11 @@ export class StockLedger extends Report { label: 'Location', fieldtype: 'Link', }, + { + fieldname: 'batchNumber', + label: 'Batch No.', + fieldtype: 'Link', + }, { fieldname: 'quantity', label: 'Quantity', diff --git a/reports/inventory/helpers.ts b/reports/inventory/helpers.ts index 2cd093fb..c1bf70de 100644 --- a/reports/inventory/helpers.ts +++ b/reports/inventory/helpers.ts @@ -17,6 +17,7 @@ export async function getRawStockLedgerEntries(fyo: Fyo) { 'name', 'date', 'item', + 'batchNumber', 'rate', 'quantity', 'location', @@ -42,7 +43,14 @@ export function getStockLedgerEntries( const name = safeParseInt(sle.name); const date = new Date(sle.date); const rate = safeParseFloat(sle.rate); - const { item, location, quantity, referenceName, referenceType } = sle; + const { + item, + location, + batchNumber, + quantity, + referenceName, + referenceType, + } = sle; if (quantity === 0) { continue; @@ -80,6 +88,7 @@ export function getStockLedgerEntries( item, location, + batchNumber, quantity, balanceQuantity, @@ -124,7 +133,11 @@ export function getStockBalanceEntries( } sbeMap[sle.item] ??= {}; - sbeMap[sle.item][sle.location] ??= getSBE(sle.item, sle.location); + sbeMap[sle.item][sle.location] ??= getSBE( + sle.item, + sle.location, + sle.batchNumber + ); const date = sle.date.valueOf(); if (fromDate && date < fromDate) { @@ -146,12 +159,17 @@ export function getStockBalanceEntries( .flat(); } -function getSBE(item: string, location: string): StockBalanceEntry { +function getSBE( + item: string, + location: string, + batchNumber: string +): StockBalanceEntry { return { name: 0, item, location, + batchNumber, balanceQuantity: 0, balanceValue: 0, diff --git a/reports/inventory/types.ts b/reports/inventory/types.ts index ea7f69c5..ca433753 100644 --- a/reports/inventory/types.ts +++ b/reports/inventory/types.ts @@ -5,6 +5,7 @@ export interface RawStockLedgerEntry { date: string; item: string; rate: string; + batchNumber: string; quantity: number; location: string; referenceName: string; @@ -19,6 +20,7 @@ export interface ComputedStockLedgerEntry{ item: string; location:string; + batchNumber: string; quantity: number; balanceQuantity: number; @@ -39,6 +41,7 @@ export interface StockBalanceEntry{ item: string; location:string; + batchNumber: string; balanceQuantity: number; balanceValue: number; diff --git a/schemas/app/BatchNumber.json b/schemas/app/BatchNumber.json new file mode 100644 index 00000000..6596d02f --- /dev/null +++ b/schemas/app/BatchNumber.json @@ -0,0 +1,27 @@ +{ + "name": "BatchNumber", + "label": "Batch Number", + "naming": "manual", + "fields": [ + { + "fieldname": "name", + "fieldtype": "Data", + "label": "Batch Number", + "required": true + }, + { + "fieldname": "expiryDate", + "fieldtype": "Date", + "label": "Expiry Date", + "required": false + }, + { + "fieldname": "manufactureDate", + "fieldtype": "Date", + "label": "Manufacture Date", + "required": false + } + ], + "quickEditFields": ["expiryDate", "manufactureDate"], + "keywordFields": ["name"] +} diff --git a/schemas/app/InvoiceItem.json b/schemas/app/InvoiceItem.json index 436d7170..b6bbb522 100644 --- a/schemas/app/InvoiceItem.json +++ b/schemas/app/InvoiceItem.json @@ -47,6 +47,14 @@ "placeholder": "Unit", "readOnly": true }, + { + "fieldname": "batchNumber", + "label": "Batch No", + "fieldtype": "Link", + "create": true, + "target": "BatchNumber", + "placeholder": "Batch No" + }, { "fieldname": "quantity", "label": "Quantity", @@ -126,7 +134,7 @@ "readOnly": true } ], - "tableFields": ["item", "tax", "quantity", "rate", "amount"], + "tableFields": ["item", "tax", "batchNumber", "quantity", "rate", "amount"], "keywordFields": ["item", "tax"], "quickEditFields": [ "item", @@ -138,6 +146,7 @@ "transferQuantity", "transferUnit", + "batchNumber", "quantity", "unit", "unitConversionFactor", diff --git a/schemas/app/Item.json b/schemas/app/Item.json index b682ab0d..47d0901c 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -131,6 +131,13 @@ "section": "Inventory", "default": false }, + { + "fieldname": "hasBatchNumber", + "label": "Has Batch No", + "fieldtype": "Check", + "default": false, + "section": "Inventory" + }, { "fieldname": "uomConversions", "label": "UOM Conversions", @@ -151,6 +158,7 @@ "barcode", "hsnCode", "trackItem", + "hasBatchNumber", "uom" ], "keywordFields": ["name", "itemType", "for"] diff --git a/schemas/app/PrintSettings.json b/schemas/app/PrintSettings.json index 1499093e..25fa62df 100644 --- a/schemas/app/PrintSettings.json +++ b/schemas/app/PrintSettings.json @@ -29,6 +29,11 @@ "label": "Display Tax Invoice", "fieldtype": "Check" }, + { + "fieldname": "displayBatchNumber", + "label": "Display Batch Number", + "fieldtype": "Check" + }, { "fieldname": "phone", "label": "Phone", @@ -138,6 +143,7 @@ "logo", "displayLogo", "displayTaxInvoice", + "displayBatchNumber", "template", "color", "font", diff --git a/schemas/app/inventory/StockLedgerEntry.json b/schemas/app/inventory/StockLedgerEntry.json index f4955c1c..9f6dbda7 100644 --- a/schemas/app/inventory/StockLedgerEntry.json +++ b/schemas/app/inventory/StockLedgerEntry.json @@ -49,6 +49,13 @@ "label": "Ref. Type", "fieldtype": "Data", "readOnly": true + }, + { + "fieldname": "batchNumber", + "label": "Batch No", + "fieldtype": "Link", + "target": "BatchNumber", + "readOnly": true } ] } diff --git a/schemas/app/inventory/StockMovementItem.json b/schemas/app/inventory/StockMovementItem.json index 22a43cff..cffb017c 100644 --- a/schemas/app/inventory/StockMovementItem.json +++ b/schemas/app/inventory/StockMovementItem.json @@ -50,6 +50,13 @@ "placeholder": "Unit", "readOnly": true }, + { + "fieldname": "batchNumber", + "label": "Batch No", + "fieldtype": "Link", + "target": "BatchNumber", + "create": true + }, { "fieldname": "quantity", "label": "Quantity", @@ -77,7 +84,7 @@ "readOnly": true } ], - "tableFields": ["item", "fromLocation", "toLocation", "quantity", "rate"], + "tableFields": ["item", "fromLocation", "toLocation", "batchNumber", "quantity", "rate"], "keywordFields": ["item"], "quickEditFields": [ "item", @@ -86,6 +93,7 @@ "transferQuantity", "transferUnit", + "batchNumber", "quantity", "unit", "unitConversionFactor", diff --git a/schemas/app/inventory/StockTransferItem.json b/schemas/app/inventory/StockTransferItem.json index 96314ab3..5c6eb207 100644 --- a/schemas/app/inventory/StockTransferItem.json +++ b/schemas/app/inventory/StockTransferItem.json @@ -41,6 +41,12 @@ "placeholder": "Unit", "readOnly": true }, + { + "fieldname": "batchNumber", + "label": "Batch No", + "fieldtype": "Link", + "target": "BatchNumber" + }, { "fieldname": "quantity", "label": "Quantity", @@ -80,12 +86,13 @@ "placeholder": "HSN/SAC Code" } ], - "tableFields": ["item", "location", "quantity", "rate", "amount"], + "tableFields": ["item", "location", "batchNumber", "quantity", "rate", "amount"], "quickEditFields": [ "item", "transferQuantity", "transferUnit", + "batchNumber", "quantity", "unit", "unitConversionFactor", diff --git a/schemas/schemas.ts b/schemas/schemas.ts index f19307fa..25ca0c26 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -47,6 +47,7 @@ import submittable from './meta/submittable.json'; import tree from './meta/tree.json'; import { Schema, SchemaStub } from './types'; import InventorySettings from './app/inventory/InventorySettings.json'; +import BatchNumber from './app/BatchNumber.json' export const coreSchemas: Schema[] = [ PatchRun as Schema, @@ -114,4 +115,6 @@ export const appSchemas: Schema[] | SchemaStub[] = [ ShipmentItem as Schema, PurchaseReceipt as Schema, PurchaseReceiptItem as Schema, + + BatchNumber as Schema ]; diff --git a/src/components/SalesInvoice/Templates/BaseTemplate.vue b/src/components/SalesInvoice/Templates/BaseTemplate.vue index ac60ded5..0d77e16f 100644 --- a/src/components/SalesInvoice/Templates/BaseTemplate.vue +++ b/src/components/SalesInvoice/Templates/BaseTemplate.vue @@ -83,6 +83,7 @@ export default { showHSN: this.showHSN, displayLogo: this.printSettings.displayLogo, displayTaxInvoice: this.printSettings.displayTaxInvoice, + displayBatchNumber: this.printSettings.displayBatchNumber, discountAfterTax: this.doc.discountAfterTax, logo: this.printSettings.logo, companyName: this.fyo.singles.AccountingSettings.companyName, diff --git a/src/components/SalesInvoice/Templates/Basic.vue b/src/components/SalesInvoice/Templates/Basic.vue index e78940b2..d83520b3 100644 --- a/src/components/SalesInvoice/Templates/Basic.vue +++ b/src/components/SalesInvoice/Templates/Basic.vue @@ -71,7 +71,13 @@
HSN/SAC
-
Quantity
+
Quantity
+
+ Batch No +
Rate
Amount
@@ -84,7 +90,13 @@
{{ row.hsnCode }}
-
{{ row.quantity }}
+
{{ row.quantity }}
+
+ {{ row.batchNumber }} +
{{ row.rate }}
{{ row.amount }}
diff --git a/src/components/SalesInvoice/Templates/Business.vue b/src/components/SalesInvoice/Templates/Business.vue index 2a9be631..93162eee 100644 --- a/src/components/SalesInvoice/Templates/Business.vue +++ b/src/components/SalesInvoice/Templates/Business.vue @@ -68,6 +68,9 @@
Item
HSN/SAC
Quantity
+
+ Batch No +
Rate
Amount
@@ -81,6 +84,9 @@ {{ row.hsnCode }}
{{ row.quantity }}
+
+ {{ row.batchNumber }} +
{{ row.rate }}
{{ row.amount }}
diff --git a/src/components/SalesInvoice/Templates/Minimal.vue b/src/components/SalesInvoice/Templates/Minimal.vue index 1acf593a..30e38cc3 100644 --- a/src/components/SalesInvoice/Templates/Minimal.vue +++ b/src/components/SalesInvoice/Templates/Minimal.vue @@ -112,6 +112,9 @@
Item
HSN/SAC
Quantity
+
+ Batch No +
Rate
Amount
@@ -125,6 +128,9 @@ {{ row.hsnCode }}
{{ row.quantity }}
+
+ {{ row.batchNumber }} +
{{ row.rate }}
{{ row.amount }}
diff --git a/src/utils/sidebarConfig.ts b/src/utils/sidebarConfig.ts index d57d4475..a60e603c 100644 --- a/src/utils/sidebarConfig.ts +++ b/src/utils/sidebarConfig.ts @@ -95,7 +95,7 @@ async function getInventorySidebar(): Promise { label: t`Stock Balance`, name: 'stock-balance', route: '/report/StockBalance', - }, + } ], }, ];