From 08e46592a9552f0fffcfb3fe015ab1d05223d0b3 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 13 Jan 2023 18:46:52 +0530 Subject: [PATCH 01/27] feat: batch transactions --- models/baseModels/Item/Item.ts | 2 ++ models/inventory/PurchaseReceipt.ts | 1 + models/inventory/StockLedgerEntry.ts | 1 + models/inventory/StockManager.ts | 3 +++ models/inventory/StockMovement.ts | 1 + models/inventory/StockMovementItem.ts | 1 + models/inventory/StockTransfer.ts | 1 + models/inventory/StockTransferItem.ts | 1 + models/inventory/types.ts | 1 + reports/inventory/helpers.ts | 4 +++- reports/inventory/types.ts | 2 ++ schemas/app/InvoiceItem.json | 12 ++++++++++-- schemas/app/Item.json | 7 +++++++ schemas/app/inventory/StockLedgerEntry.json | 7 +++++++ schemas/app/inventory/StockTransferItem.json | 5 +++++ 15 files changed, 46 insertions(+), 3 deletions(-) diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 0eac8acd..fe82fc4e 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -18,6 +18,8 @@ export class Item extends Doc { trackItem?: boolean; itemType?: 'Product' | 'Service'; for?: 'Purchases' | 'Sales' | 'Both'; + hasBatchNumber?: boolean; + batchNumber?:string; formulas: FormulaMap = { incomeAccount: { diff --git a/models/inventory/PurchaseReceipt.ts b/models/inventory/PurchaseReceipt.ts index 019c75b4..41db3e23 100644 --- a/models/inventory/PurchaseReceipt.ts +++ b/models/inventory/PurchaseReceipt.ts @@ -11,6 +11,7 @@ export class PurchaseReceipt extends StockTransfer { formRoute: ({ name }) => `/edit/PurchaseReceipt/${name}`, columns: [ 'name', + 'batchNumber', getTransactionStatusColumn(), 'party', 'date', 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..503ba735 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -204,6 +204,7 @@ class StockManagerItem { referenceType: string; fromLocation?: string; toLocation?: string; + batchNumber?: string; stockLedgerEntries?: StockLedgerEntry[]; @@ -218,6 +219,7 @@ class StockManagerItem { this.toLocation = details.toLocation; this.referenceName = details.referenceName; this.referenceType = details.referenceType; + this.batchNumber = details.batchNumber; this.fyo = fyo; } @@ -270,6 +272,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 20811237..6827e748 100644 --- a/models/inventory/StockMovement.ts +++ b/models/inventory/StockMovement.ts @@ -84,6 +84,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 665852a1..b8ae74cf 100644 --- a/models/inventory/StockMovementItem.ts +++ b/models/inventory/StockMovementItem.ts @@ -19,6 +19,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 5aae9c1f..7b82dff4 100644 --- a/models/inventory/StockTransfer.ts +++ b/models/inventory/StockTransfer.ts @@ -68,6 +68,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 9b12d208..d5dadc00 100644 --- a/models/inventory/StockTransferItem.ts +++ b/models/inventory/StockTransferItem.ts @@ -12,6 +12,7 @@ export class StockTransferItem extends Doc { unit?: string; description?: string; hsnCode?: number; + batchNumber?: string formulas: FormulaMap = { description: { diff --git a/models/inventory/types.ts b/models/inventory/types.ts index 6a846b1a..0809a0e4 100644 --- a/models/inventory/types.ts +++ b/models/inventory/types.ts @@ -21,6 +21,7 @@ export interface SMTransferDetails { item: string; rate: Money; quantity: number; + batchNumber?: string; fromLocation?: string; toLocation?: string; } diff --git a/reports/inventory/helpers.ts b/reports/inventory/helpers.ts index 2cd093fb..6da6ca65 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,7 @@ 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; @@ -81,6 +82,7 @@ export function getStockLedgerEntries( item, location, + batchNumber, quantity, balanceQuantity, diff --git a/reports/inventory/types.ts b/reports/inventory/types.ts index ea7f69c5..b83244b1 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; @@ -20,6 +21,7 @@ export interface ComputedStockLedgerEntry{ item: string; location:string; + batchNumber: string; quantity: number; balanceQuantity: number; diff --git a/schemas/app/InvoiceItem.json b/schemas/app/InvoiceItem.json index 2d95b90f..68d0abfb 100644 --- a/schemas/app/InvoiceItem.json +++ b/schemas/app/InvoiceItem.json @@ -93,9 +93,16 @@ "label": "Stock Not Transferred", "fieldtype": "Float", "readOnly": true + }, + { + "fieldname": "batchNumber", + "label": "Batch No", + "fieldtype": "Data", + "create": true, + "placeholder": "Batch No" } ], - "tableFields": ["item", "tax", "quantity", "rate", "amount"], + "tableFields": ["item", "tax", "quantity", "rate", "amount", "batchNumber"], "keywordFields": ["item", "tax"], "quickEditFields": [ "item", @@ -110,6 +117,7 @@ "itemDiscountAmount", "itemDiscountPercent", "itemDiscountedTotal", - "itemTaxedTotal" + "itemTaxedTotal", + "batchNumber" ] } diff --git a/schemas/app/Item.json b/schemas/app/Item.json index 23a30650..f3bd5eb2 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -31,6 +31,12 @@ "default": "Unit", "placeholder": "Unit Type" }, + { + "fieldname": "hasBatchNumber", + "label": "Has Batch No", + "fieldtype": "Check", + "default": false + }, { "fieldname": "itemType", "label": "Type", @@ -116,6 +122,7 @@ "quickEditFields": [ "rate", "unit", + "hasBatchNumber", "itemType", "for", "tax", diff --git a/schemas/app/inventory/StockLedgerEntry.json b/schemas/app/inventory/StockLedgerEntry.json index f4955c1c..88a07c35 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": "Data", + "create": true, + "placeholder": "Batch No" } ] } diff --git a/schemas/app/inventory/StockTransferItem.json b/schemas/app/inventory/StockTransferItem.json index 464cdd8e..2390257a 100644 --- a/schemas/app/inventory/StockTransferItem.json +++ b/schemas/app/inventory/StockTransferItem.json @@ -17,6 +17,11 @@ "target": "Location", "required": true }, + { + "fieldname": "batchNumber", + "label": "Batch No", + "fieldtype": "Data" + }, { "fieldname": "quantity", "label": "Quantity", From 8a4e1e4f41a0f482e755f128df899ac00733728f Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Sat, 14 Jan 2023 16:58:16 +0530 Subject: [PATCH 02/27] feat: BatchNumber schema & batch-wise validation --- backend/database/bespoke.ts | 9 +++++- fyo/core/dbHandler.ts | 6 ++-- models/baseModels/BatchNumber/BatchNumber.ts | 13 ++++++++ models/baseModels/Item/Item.ts | 2 +- models/index.ts | 2 ++ models/inventory/StockManager.ts | 31 +++++++++++--------- models/inventory/StockMovementItem.ts | 2 +- schemas/app/BatchNumber.json | 29 ++++++++++++++++++ schemas/app/InvoiceItem.json | 3 +- schemas/app/inventory/StockMovementItem.json | 12 ++++++-- schemas/schemas.ts | 3 ++ src/utils/sidebarConfig.ts | 5 ++++ 12 files changed, 95 insertions(+), 22 deletions(-) create mode 100644 models/baseModels/BatchNumber/BatchNumber.ts create mode 100644 schemas/app/BatchNumber.json diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index ee830635..82f6bcd9 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]); } @@ -163,3 +168,5 @@ export class BespokeQueries { return value[0][Object.keys(value[0])[0]]; } } + +// \ No newline at end of file diff --git a/fyo/core/dbHandler.ts b/fyo/core/dbHandler.ts index c02d826e..83a74d90 100644 --- a/fyo/core/dbHandler.ts +++ b/fyo/core/dbHandler.ts @@ -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 fe82fc4e..c053c1aa 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -19,7 +19,6 @@ export class Item extends Doc { itemType?: 'Product' | 'Service'; for?: 'Purchases' | 'Sales' | 'Both'; hasBatchNumber?: boolean; - batchNumber?:string; formulas: FormulaMap = { incomeAccount: { @@ -122,6 +121,7 @@ export class Item extends Doc { !this.fyo.singles.AccountingSettings?.enableInventory || this.itemType !== 'Product' || (this.inserted && !this.trackItem), + batchNumber: () => (!this.hasBatchNumber), }; readOnly: ReadOnlyMap = { 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/StockManager.ts b/models/inventory/StockManager.ts index 503ba735..f0279888 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -82,7 +82,7 @@ export class StockManager { #getSMIDetails(transferDetails: SMTransferDetails): SMIDetails { return Object.assign({}, this.details, transferDetails); } - + // flag async #validate(details: SMIDetails) { this.#validateRate(details); this.#validateQuantity(details); @@ -125,7 +125,7 @@ export class StockManager { throw new ValidationError(t`Both From and To Location cannot be undefined`); } - + // flag async #validateStockAvailability(details: SMIDetails) { if (!details.fromLocation) { return; @@ -137,8 +137,11 @@ export class StockManager { details.item, details.fromLocation, undefined, - date + date, + details.batchNumber )) ?? 0; + console.log(quantityBefore); + const formattedDate = this.fyo.format(details.date, 'Datetime'); @@ -150,11 +153,9 @@ export class StockManager { throw new ValidationError( [ t`Insufficient Quantity.`, - t`Additional quantity (${ - details.quantity - quantityBefore - }) required to make outward transfer of item ${details.item} from ${ - details.fromLocation - } on ${formattedDate}`, + t`Additional quantity (${details.quantity - quantityBefore + }) required to make outward transfer of item ${details.item} from ${details.fromLocation + } on ${formattedDate}`, ].join('\n') ); } @@ -162,7 +163,9 @@ export class StockManager { const quantityAfter = await this.fyo.db.getStockQuantity( details.item, details.fromLocation, - details.date.toISOString() + details.date.toISOString(), + undefined, + details.batchNumber ); if (quantityAfter === null) { // No future transactions @@ -175,17 +178,17 @@ export class StockManager { [ t`Insufficient Quantity.`, t`Transfer will cause future entries to have negative stock.`, - t`Additional quantity (${ - quantityAfter - quantityRemaining - }) required to make outward transfer of item ${details.item} from ${ - details.fromLocation - } on ${formattedDate}`, + t`Additional quantity (${quantityAfter - quantityRemaining + }) required to make outward transfer of item ${details.item} from ${details.fromLocation + } on ${formattedDate}`, ].join('\n') ); } } } +// + class StockManagerItem { /** * The Stock Manager Item is used to move stock to and from a location. It diff --git a/models/inventory/StockMovementItem.ts b/models/inventory/StockMovementItem.ts index b8ae74cf..7ce8742e 100644 --- a/models/inventory/StockMovementItem.ts +++ b/models/inventory/StockMovementItem.ts @@ -19,7 +19,7 @@ export class StockMovementItem extends Doc { rate?: Money; amount?: Money; parentdoc?: StockMovement; - batchNumber? : string; + batchNumber?: string; get isIssue() { return this.parentdoc?.movementType === MovementType.MaterialIssue; diff --git a/schemas/app/BatchNumber.json b/schemas/app/BatchNumber.json new file mode 100644 index 00000000..247547f8 --- /dev/null +++ b/schemas/app/BatchNumber.json @@ -0,0 +1,29 @@ +{ + "name": "BatchNumber", + "label": "Batch Number", + "naming": "manual", + "fields": [ + { + "fieldname": "name", + "fieldtype": "Data", + "create": true, + "label": "Batch Number", + "required": true + }, + { + "fieldname": "expiryDate", + "fieldtype": "Date", + "label": "Expiry Date", + "create": true, + "required": false + }, + { + "fieldname": "manufactureDate", + "fieldtype": "Date", + "label": "Manufacture Date", + "create": true, + "required": false + } + ], + "quickEditFields": ["batchNumber", "expiryDate", "manufactureDate"] +} \ No newline at end of file diff --git a/schemas/app/InvoiceItem.json b/schemas/app/InvoiceItem.json index 68d0abfb..082ac3e0 100644 --- a/schemas/app/InvoiceItem.json +++ b/schemas/app/InvoiceItem.json @@ -97,8 +97,9 @@ { "fieldname": "batchNumber", "label": "Batch No", - "fieldtype": "Data", + "fieldtype": "Link", "create": true, + "target": "BatchNumber", "placeholder": "Batch No" } ], diff --git a/schemas/app/inventory/StockMovementItem.json b/schemas/app/inventory/StockMovementItem.json index 2ba7a6a3..46545c08 100644 --- a/schemas/app/inventory/StockMovementItem.json +++ b/schemas/app/inventory/StockMovementItem.json @@ -26,6 +26,13 @@ "target": "Location", "create": true }, + { + "fieldname": "batchNumber", + "label": "Batch No", + "fieldtype": "Link", + "target": "BatchNumber", + "create": true + }, { "fieldname": "quantity", "label": "Quantity", @@ -46,12 +53,13 @@ "readOnly": true } ], - "tableFields": ["item", "fromLocation", "toLocation", "quantity", "rate"], - "keywordFields": ["item"], + "tableFields": ["item", "fromLocation", "toLocation", "batchNumber", "quantity", "rate"], + "keywordFields": ["item", "batchNumber"], "quickEditFields": [ "item", "fromLocation", "toLocation", + "batchNumber", "quantity", "rate", "amount" diff --git a/schemas/schemas.ts b/schemas/schemas.ts index afde4c48..7760145f 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -46,6 +46,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, @@ -112,4 +113,6 @@ export const appSchemas: Schema[] | SchemaStub[] = [ ShipmentItem as Schema, PurchaseReceipt as Schema, PurchaseReceiptItem as Schema, + + BatchNumber as Schema ]; diff --git a/src/utils/sidebarConfig.ts b/src/utils/sidebarConfig.ts index 0d5105a0..c4427d9f 100644 --- a/src/utils/sidebarConfig.ts +++ b/src/utils/sidebarConfig.ts @@ -96,6 +96,11 @@ async function getInventorySidebar(): Promise { name: 'stock-balance', route: '/report/StockBalance', }, + { + label: t`Batch`, + name: 'batch-number', + route: '/list/BatchNumber', + } ], }, ]; From fef8a0af653b6adbd3c47bf4f3edbbbd3c9b6fd8 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Mon, 16 Jan 2023 14:57:40 +0530 Subject: [PATCH 03/27] feat: batch-wise stock validation --- models/inventory/StockManager.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/models/inventory/StockManager.ts b/models/inventory/StockManager.ts index f0279888..feaa7add 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -138,9 +138,8 @@ export class StockManager { details.fromLocation, undefined, date, - details.batchNumber + undefined )) ?? 0; - console.log(quantityBefore); const formattedDate = this.fyo.format(details.date, 'Datetime'); @@ -149,6 +148,30 @@ export class StockManager { quantityBefore += details.quantity; } + // batch-wise stock validation + let itemsInBatch = + (await this.fyo.db.getStockQuantity( + details.item, + details.fromLocation, + undefined, + date, + details.batchNumber + )) ?? 0; + + + let ifItemHasBatchNumber= await this.fyo.getValue('Item', details.item, 'hasBatchNumber'); + + if ((ifItemHasBatchNumber && details.batchNumber) && details.quantity > itemsInBatch) { + throw new ValidationError( + [ + t`Insufficient Quantity in Batch ${details.batchNumber}`, + 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') + ); + } + if (quantityBefore < details.quantity) { throw new ValidationError( [ @@ -165,7 +188,7 @@ export class StockManager { details.fromLocation, details.date.toISOString(), undefined, - details.batchNumber + undefined ); if (quantityAfter === null) { // No future transactions From 454e62793e5cdfb7fceee7747c716673f0be23f1 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Tue, 17 Jan 2023 14:04:12 +0530 Subject: [PATCH 04/27] rem: batchNumber hiding rule --- models/baseModels/Item/Item.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index c053c1aa..dfe87017 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -121,7 +121,6 @@ export class Item extends Doc { !this.fyo.singles.AccountingSettings?.enableInventory || this.itemType !== 'Product' || (this.inserted && !this.trackItem), - batchNumber: () => (!this.hasBatchNumber), }; readOnly: ReadOnlyMap = { From fc75a8b5361c6302cdc5d4634c3ae65b77eefe8c Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Tue, 17 Jan 2023 14:08:26 +0530 Subject: [PATCH 05/27] rem: batchNumber from listView --- models/inventory/PurchaseReceipt.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/models/inventory/PurchaseReceipt.ts b/models/inventory/PurchaseReceipt.ts index 41db3e23..019c75b4 100644 --- a/models/inventory/PurchaseReceipt.ts +++ b/models/inventory/PurchaseReceipt.ts @@ -11,7 +11,6 @@ export class PurchaseReceipt extends StockTransfer { formRoute: ({ name }) => `/edit/PurchaseReceipt/${name}`, columns: [ 'name', - 'batchNumber', getTransactionStatusColumn(), 'party', 'date', From 81d84f4220318098968b98ab2c148fa932fb8702 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Wed, 18 Jan 2023 17:32:26 +0530 Subject: [PATCH 06/27] fix: item-batch validation --- backend/database/bespoke.ts | 24 +++++++++++++++++++++--- fyo/core/dbHandler.ts | 6 ++++++ models/baseModels/Item/Item.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index 82f6bcd9..14dee427 100644 --- a/backend/database/bespoke.ts +++ b/backend/database/bespoke.ts @@ -132,6 +132,26 @@ export class BespokeQueries { `); } + static async itemHasTransactions( + db: DatabaseCore, + item: string + ): Promise { + /* + * to check if an item of given name is present in the SLE + * taking the count of items from SLE which has the given item name + * if the count is greater than 0 considers item exists in SLE, returns true + * if the count is lesser than 0 considers item does not exist in SLE, returns false + */ + const query = db.knex!(ModelNameEnum.StockLedgerEntry) + .select('item') + .where('item', item); + + const value = (await query) as Record[]; + + if (!value) return false; + return true; + } + static async getStockQuantity( db: DatabaseCore, item: string, @@ -167,6 +187,4 @@ export class BespokeQueries { return value[0][Object.keys(value[0])[0]]; } -} - -// \ No newline at end of file +} \ No newline at end of file diff --git a/fyo/core/dbHandler.ts b/fyo/core/dbHandler.ts index 83a74d90..b84c61df 100644 --- a/fyo/core/dbHandler.ts +++ b/fyo/core/dbHandler.ts @@ -325,6 +325,12 @@ export class DatabaseHandler extends DatabaseBase { )) as number | null; } + async itemHasTransactions(item: string): Promise { + return (await this.#demux.callBespoke('itemHasTransactions', item)) as + | boolean + | null; + } + /** * Internal methods */ diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index dfe87017..792259d9 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -20,6 +20,31 @@ export class Item extends Doc { for?: 'Purchases' | 'Sales' | 'Both'; hasBatchNumber?: boolean; + async beforeSync() { + /* + * This code block is to prevent users from changing the value of Has Batch No Checkbox + of the items which already did transactions + * allowing users to change the value of Has Batch No of the items which already did + transactions will result in incorect SLEs + */ + const ifItemHasBatchNumber = await Boolean( + this.fyo.db.get('Item', this.name! || '', 'hasBatchNumber') + ); + + if (this.hasBatchNumber == ifItemHasBatchNumber) { + return; + } + + const isItemExistsInSLE = await this.fyo.db.itemHasTransactions(this.name!); + + if (ifItemHasBatchNumber && isItemExistsInSLE) { + throw new ValidationError( + this.fyo.t`Cannot change value of Has Batch No as ${this + .name!} already has transactions against it. ` + ); + } + } + formulas: FormulaMap = { incomeAccount: { formula: async () => { @@ -121,6 +146,7 @@ export class Item extends Doc { !this.fyo.singles.AccountingSettings?.enableInventory || this.itemType !== 'Product' || (this.inserted && !this.trackItem), + hasBatchNumber: () => !this.trackItem || false, }; readOnly: ReadOnlyMap = { From 20279c5a12b6642a58ba520f1f0501384495d0b9 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Wed, 18 Jan 2023 17:33:51 +0530 Subject: [PATCH 07/27] fix: schema changes --- schemas/app/BatchNumber.json | 3 ++- schemas/app/InvoiceItem.json | 6 +++--- schemas/app/Item.json | 4 ++-- schemas/app/inventory/StockTransferItem.json | 6 ++++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/schemas/app/BatchNumber.json b/schemas/app/BatchNumber.json index 247547f8..3f2bdb7f 100644 --- a/schemas/app/BatchNumber.json +++ b/schemas/app/BatchNumber.json @@ -25,5 +25,6 @@ "required": false } ], - "quickEditFields": ["batchNumber", "expiryDate", "manufactureDate"] + "quickEditFields": ["batchNumber", "expiryDate", "manufactureDate"], + "keywordFields": ["name"] } \ No newline at end of file diff --git a/schemas/app/InvoiceItem.json b/schemas/app/InvoiceItem.json index 082ac3e0..b42cf8e9 100644 --- a/schemas/app/InvoiceItem.json +++ b/schemas/app/InvoiceItem.json @@ -103,7 +103,7 @@ "placeholder": "Batch No" } ], - "tableFields": ["item", "tax", "quantity", "rate", "amount", "batchNumber"], + "tableFields": ["item", "tax", "batchNumber", "quantity", "rate", "amount"], "keywordFields": ["item", "tax"], "quickEditFields": [ "item", @@ -111,6 +111,7 @@ "description", "hsnCode", "tax", + "batchNumber", "quantity", "rate", "amount", @@ -118,7 +119,6 @@ "itemDiscountAmount", "itemDiscountPercent", "itemDiscountedTotal", - "itemTaxedTotal", - "batchNumber" + "itemTaxedTotal" ] } diff --git a/schemas/app/Item.json b/schemas/app/Item.json index f3bd5eb2..0d1b1adb 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -122,7 +122,6 @@ "quickEditFields": [ "rate", "unit", - "hasBatchNumber", "itemType", "for", "tax", @@ -130,7 +129,8 @@ "incomeAccount", "expenseAccount", "hsnCode", - "trackItem" + "trackItem", + "hasBatchNumber" ], "keywordFields": ["name", "itemType", "for"] } diff --git a/schemas/app/inventory/StockTransferItem.json b/schemas/app/inventory/StockTransferItem.json index 2390257a..240eca17 100644 --- a/schemas/app/inventory/StockTransferItem.json +++ b/schemas/app/inventory/StockTransferItem.json @@ -20,7 +20,8 @@ { "fieldname": "batchNumber", "label": "Batch No", - "fieldtype": "Data" + "fieldtype": "Link", + "target": "BatchNumber" }, { "fieldname": "quantity", @@ -62,13 +63,14 @@ "placeholder": "HSN/SAC Code" } ], - "tableFields": ["item", "location", "quantity", "rate", "amount"], + "tableFields": ["item", "location", "batchNumber", "quantity", "rate", "amount"], "quickEditFields": [ "item", "unit", "description", "hsnCode", "location", + "batchNumber", "quantity", "rate", "amount" From a8d5d9d7ef8fabce0a4954d40b81cd177c8babe9 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Wed, 18 Jan 2023 17:37:31 +0530 Subject: [PATCH 08/27] fix: batch-wise stock validations --- models/inventory/StockManager.ts | 100 ++++++++++++++++++++----------- reports/inventory/StockLedger.ts | 5 ++ reports/inventory/helpers.ts | 24 ++++++-- 3 files changed, 90 insertions(+), 39 deletions(-) diff --git a/models/inventory/StockManager.ts b/models/inventory/StockManager.ts index feaa7add..a9bec91d 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -82,12 +82,13 @@ export class StockManager { #getSMIDetails(transferDetails: SMTransferDetails): SMIDetails { return Object.assign({}, this.details, transferDetails); } - // flag + async #validate(details: SMIDetails) { this.#validateRate(details); this.#validateQuantity(details); this.#validateLocation(details); await this.#validateStockAvailability(details); + await this.#validateBatchWiseStockAvailability(details); } #validateQuantity(details: SMIDetails) { @@ -125,7 +126,59 @@ export class StockManager { throw new ValidationError(t`Both From and To Location cannot be undefined`); } - // flag + + async #validateBatchWiseStockAvailability(details: SMIDetails) { + /* + * Checks if hasBatchNumber is enabled in the item + * If user has not entered batchNumber raises a ValidationError + * If entered quantity is greater than the available quantity in the batch, raises a ValidationError + */ + + if (!details.fromLocation) { + return; + } + + const ifItemHasBatchNumber = await this.fyo.getValue( + 'Item', + details.item, + 'hasBatchNumber' + ); + const date = details.date.toISOString(); + const formattedDate = this.fyo.format(details.date, 'Datetime'); + + if (ifItemHasBatchNumber) { + 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) { + throw new ValidationError( + [ + t`Insufficient Quantity in Batch ${details.batchNumber}`, + 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') + ); + } + } + } + async #validateStockAvailability(details: SMIDetails) { if (!details.fromLocation) { return; @@ -141,44 +194,21 @@ export class StockManager { undefined )) ?? 0; - const formattedDate = this.fyo.format(details.date, 'Datetime'); if (this.isCancelled) { quantityBefore += details.quantity; } - // batch-wise stock validation - let itemsInBatch = - (await this.fyo.db.getStockQuantity( - details.item, - details.fromLocation, - undefined, - date, - details.batchNumber - )) ?? 0; - - - let ifItemHasBatchNumber= await this.fyo.getValue('Item', details.item, 'hasBatchNumber'); - - if ((ifItemHasBatchNumber && details.batchNumber) && details.quantity > itemsInBatch) { - throw new ValidationError( - [ - t`Insufficient Quantity in Batch ${details.batchNumber}`, - 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') - ); - } - if (quantityBefore < details.quantity) { throw new ValidationError( [ t`Insufficient Quantity.`, - t`Additional quantity (${details.quantity - quantityBefore - }) required to make outward transfer of item ${details.item} from ${details.fromLocation - } on ${formattedDate}`, + t`Additional quantity (${ + details.quantity - quantityBefore + }) required to make outward transfer of item ${details.item} from ${ + details.fromLocation + } on ${formattedDate}`, ].join('\n') ); } @@ -201,17 +231,17 @@ export class StockManager { [ t`Insufficient Quantity.`, t`Transfer will cause future entries to have negative stock.`, - t`Additional quantity (${quantityAfter - quantityRemaining - }) required to make outward transfer of item ${details.item} from ${details.fromLocation - } on ${formattedDate}`, + t`Additional quantity (${ + quantityAfter - quantityRemaining + }) required to make outward transfer of item ${details.item} from ${ + details.fromLocation + } on ${formattedDate}`, ].join('\n') ); } } } -// - class StockManagerItem { /** * The Stock Manager Item is used to move stock to and from a location. It 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 6da6ca65..3f30f360 100644 --- a/reports/inventory/helpers.ts +++ b/reports/inventory/helpers.ts @@ -17,7 +17,7 @@ export async function getRawStockLedgerEntries(fyo: Fyo) { 'name', 'date', 'item', - "batchNumber", + 'batchNumber', 'rate', 'quantity', 'location', @@ -43,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, batchNumber, quantity, referenceName, referenceType } = sle; + const { + item, + location, + batchNumber, + quantity, + referenceName, + referenceType, + } = sle; if (quantity === 0) { continue; @@ -126,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) { @@ -148,13 +159,18 @@ 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, From 7ab6014c6086a75b268d2d81c342df7df52b7799 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Thu, 19 Jan 2023 10:34:34 +0530 Subject: [PATCH 09/27] fix: removed batchNumber from SBE --- reports/inventory/helpers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/reports/inventory/helpers.ts b/reports/inventory/helpers.ts index 3f30f360..6ed033ba 100644 --- a/reports/inventory/helpers.ts +++ b/reports/inventory/helpers.ts @@ -170,7 +170,6 @@ function getSBE( item, location, - batchNumber, balanceQuantity: 0, balanceValue: 0, From 9b1cd31ba33a06ceb6d30a5b3eeeb8c59d013907 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Tue, 24 Jan 2023 16:24:08 +0530 Subject: [PATCH 10/27] fix: code comments --- backend/database/bespoke.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index 14dee427..aa577a8d 100644 --- a/backend/database/bespoke.ts +++ b/backend/database/bespoke.ts @@ -138,9 +138,6 @@ export class BespokeQueries { ): Promise { /* * to check if an item of given name is present in the SLE - * taking the count of items from SLE which has the given item name - * if the count is greater than 0 considers item exists in SLE, returns true - * if the count is lesser than 0 considers item does not exist in SLE, returns false */ const query = db.knex!(ModelNameEnum.StockLedgerEntry) .select('item') From 622a4cccd3c9f6dfae4b8e47a053c6a1bcabb8f1 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 27 Jan 2023 16:06:04 +0530 Subject: [PATCH 11/27] fix: hasBatchNumber checkbox validation --- backend/database/bespoke.ts | 2 +- models/baseModels/Item/Item.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index aa577a8d..842a137e 100644 --- a/backend/database/bespoke.ts +++ b/backend/database/bespoke.ts @@ -145,7 +145,7 @@ export class BespokeQueries { const value = (await query) as Record[]; - if (!value) return false; + if (!value || value.length == 0) return false; return true; } diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 49e252c5..7f50e5ac 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -27,17 +27,19 @@ export class Item extends Doc { * allowing users to change the value of Has Batch No of the items which already did transactions will result in incorect SLEs */ - const ifItemHasBatchNumber = await Boolean( - this.fyo.db.get('Item', this.name! || '', 'hasBatchNumber') + const ifItemHasBatchNumber = await this.fyo.db.get( + 'Item', + this.name!, + 'hasBatchNumber' ); - if (this.hasBatchNumber == ifItemHasBatchNumber) { + if (this.hasBatchNumber == ifItemHasBatchNumber.hasBatchNumber) { return; } const isItemExistsInSLE = await this.fyo.db.itemHasTransactions(this.name!); - if (ifItemHasBatchNumber && isItemExistsInSLE) { + if (isItemExistsInSLE) { throw new ValidationError( this.fyo.t`Cannot change value of Has Batch No as ${this .name!} already has transactions against it. ` From 6cd1f249464b236ccafa7b570d3a6827d08b2de4 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 10:12:28 +0530 Subject: [PATCH 12/27] fix: batchNumber fieldtype in SLE schema --- schemas/app/inventory/StockLedgerEntry.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/schemas/app/inventory/StockLedgerEntry.json b/schemas/app/inventory/StockLedgerEntry.json index 88a07c35..9f6dbda7 100644 --- a/schemas/app/inventory/StockLedgerEntry.json +++ b/schemas/app/inventory/StockLedgerEntry.json @@ -53,9 +53,9 @@ { "fieldname": "batchNumber", "label": "Batch No", - "fieldtype": "Data", - "create": true, - "placeholder": "Batch No" + "fieldtype": "Link", + "target": "BatchNumber", + "readOnly": true } ] } From 722d8dd0fcffbdb0ff0c542436b165acf77ff07c Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 10:23:24 +0530 Subject: [PATCH 13/27] fix: batchNumber schema --- schemas/app/BatchNumber.json | 55 +++++++++++++++++------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/schemas/app/BatchNumber.json b/schemas/app/BatchNumber.json index 3f2bdb7f..6596d02f 100644 --- a/schemas/app/BatchNumber.json +++ b/schemas/app/BatchNumber.json @@ -1,30 +1,27 @@ { - "name": "BatchNumber", - "label": "Batch Number", - "naming": "manual", - "fields": [ - { - "fieldname": "name", - "fieldtype": "Data", - "create": true, - "label": "Batch Number", - "required": true - }, - { - "fieldname": "expiryDate", - "fieldtype": "Date", - "label": "Expiry Date", - "create": true, - "required": false - }, - { - "fieldname": "manufactureDate", - "fieldtype": "Date", - "label": "Manufacture Date", - "create": true, - "required": false - } - ], - "quickEditFields": ["batchNumber", "expiryDate", "manufactureDate"], - "keywordFields": ["name"] -} \ No newline at end of file + "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"] +} From 12ae65600d1cea7e1fc867a02a4d2a9eed6c819d Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 10:32:33 +0530 Subject: [PATCH 14/27] fix: removed batch from sidebarConfig --- src/utils/sidebarConfig.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/utils/sidebarConfig.ts b/src/utils/sidebarConfig.ts index c4427d9f..7dfc6005 100644 --- a/src/utils/sidebarConfig.ts +++ b/src/utils/sidebarConfig.ts @@ -95,11 +95,6 @@ async function getInventorySidebar(): Promise { label: t`Stock Balance`, name: 'stock-balance', route: '/report/StockBalance', - }, - { - label: t`Batch`, - name: 'batch-number', - route: '/list/BatchNumber', } ], }, From c624024505d102a88a93959400bfa9f335bf8dd0 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 10:40:38 +0530 Subject: [PATCH 15/27] fix: stockMovementItem keywordFields --- schemas/app/inventory/StockMovementItem.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/app/inventory/StockMovementItem.json b/schemas/app/inventory/StockMovementItem.json index 46545c08..a8f1e308 100644 --- a/schemas/app/inventory/StockMovementItem.json +++ b/schemas/app/inventory/StockMovementItem.json @@ -54,7 +54,7 @@ } ], "tableFields": ["item", "fromLocation", "toLocation", "batchNumber", "quantity", "rate"], - "keywordFields": ["item", "batchNumber"], + "keywordFields": ["item"], "quickEditFields": [ "item", "fromLocation", From 32fdcf72a6cb8b8b0b7de375c3f27c43f503cc7c Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 10:53:16 +0530 Subject: [PATCH 16/27] chore: batchNumber position in csle --- reports/inventory/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reports/inventory/helpers.ts b/reports/inventory/helpers.ts index 6ed033ba..b4bedde9 100644 --- a/reports/inventory/helpers.ts +++ b/reports/inventory/helpers.ts @@ -88,8 +88,8 @@ export function getStockLedgerEntries( item, location, - batchNumber, + quantity, balanceQuantity, From f966af56336e7194ee738d490374d898942081b1 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 12:16:50 +0530 Subject: [PATCH 17/27] fix: batchNumber in getSBE --- reports/inventory/helpers.ts | 1 + reports/inventory/types.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/reports/inventory/helpers.ts b/reports/inventory/helpers.ts index b4bedde9..c1bf70de 100644 --- a/reports/inventory/helpers.ts +++ b/reports/inventory/helpers.ts @@ -169,6 +169,7 @@ function getSBE( item, location, + batchNumber, balanceQuantity: 0, balanceValue: 0, diff --git a/reports/inventory/types.ts b/reports/inventory/types.ts index b83244b1..ca433753 100644 --- a/reports/inventory/types.ts +++ b/reports/inventory/types.ts @@ -20,8 +20,8 @@ export interface ComputedStockLedgerEntry{ item: string; location:string; - batchNumber: string; + quantity: number; balanceQuantity: number; @@ -41,6 +41,7 @@ export interface StockBalanceEntry{ item: string; location:string; + batchNumber: string; balanceQuantity: number; balanceValue: number; From 261a3df48d403c70bac6f3c9d3d5a3f8126fb727 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 12:25:47 +0530 Subject: [PATCH 18/27] chore: remove code comments --- models/inventory/StockManager.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/models/inventory/StockManager.ts b/models/inventory/StockManager.ts index a9bec91d..19e2e165 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -128,12 +128,6 @@ export class StockManager { } async #validateBatchWiseStockAvailability(details: SMIDetails) { - /* - * Checks if hasBatchNumber is enabled in the item - * If user has not entered batchNumber raises a ValidationError - * If entered quantity is greater than the available quantity in the batch, raises a ValidationError - */ - if (!details.fromLocation) { return; } From f10399074f3385b9a88f02f889cec615227648f6 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 14:56:42 +0530 Subject: [PATCH 19/27] refactor: hasBatchNumber validation --- backend/database/bespoke.ts | 19 +------------ fyo/core/dbHandler.ts | 10 ++----- models/baseModels/Item/Item.ts | 51 ++++++++++++++++------------------ 3 files changed, 27 insertions(+), 53 deletions(-) diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index 842a137e..87c3ab7b 100644 --- a/backend/database/bespoke.ts +++ b/backend/database/bespoke.ts @@ -132,23 +132,6 @@ export class BespokeQueries { `); } - static async itemHasTransactions( - db: DatabaseCore, - item: string - ): Promise { - /* - * to check if an item of given name is present in the SLE - */ - const query = db.knex!(ModelNameEnum.StockLedgerEntry) - .select('item') - .where('item', item); - - const value = (await query) as Record[]; - - if (!value || value.length == 0) return false; - return true; - } - static async getStockQuantity( db: DatabaseCore, item: string, @@ -184,4 +167,4 @@ export class BespokeQueries { return value[0][Object.keys(value[0])[0]]; } -} \ No newline at end of file +} diff --git a/fyo/core/dbHandler.ts b/fyo/core/dbHandler.ts index b84c61df..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 @@ -325,12 +325,6 @@ export class DatabaseHandler extends DatabaseBase { )) as number | null; } - async itemHasTransactions(item: string): Promise { - return (await this.#demux.callBespoke('itemHasTransactions', item)) as - | boolean - | null; - } - /** * Internal methods */ diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 7f50e5ac..5f33e420 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'; @@ -20,33 +21,6 @@ export class Item extends Doc { for?: 'Purchases' | 'Sales' | 'Both'; hasBatchNumber?: boolean; - async beforeSync() { - /* - * This code block is to prevent users from changing the value of Has Batch No Checkbox - of the items which already did transactions - * allowing users to change the value of Has Batch No of the items which already did - transactions will result in incorect SLEs - */ - const ifItemHasBatchNumber = await this.fyo.db.get( - 'Item', - this.name!, - 'hasBatchNumber' - ); - - if (this.hasBatchNumber == ifItemHasBatchNumber.hasBatchNumber) { - return; - } - - const isItemExistsInSLE = await this.fyo.db.itemHasTransactions(this.name!); - - if (isItemExistsInSLE) { - throw new ValidationError( - this.fyo.t`Cannot change value of Has Batch No as ${this - .name!} already has transactions against it. ` - ); - } - } - formulas: FormulaMap = { incomeAccount: { formula: async () => { @@ -102,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[] { From 7cecf4e8dfcce593153f2eb7cf98c177858b6782 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 15:09:57 +0530 Subject: [PATCH 20/27] refactor: removed hasBatchNumber validation --- backend/database/bespoke.ts | 19 +------------------ fyo/core/dbHandler.ts | 10 ++-------- models/baseModels/Item/Item.ts | 30 ++---------------------------- 3 files changed, 5 insertions(+), 54 deletions(-) diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index 842a137e..87c3ab7b 100644 --- a/backend/database/bespoke.ts +++ b/backend/database/bespoke.ts @@ -132,23 +132,6 @@ export class BespokeQueries { `); } - static async itemHasTransactions( - db: DatabaseCore, - item: string - ): Promise { - /* - * to check if an item of given name is present in the SLE - */ - const query = db.knex!(ModelNameEnum.StockLedgerEntry) - .select('item') - .where('item', item); - - const value = (await query) as Record[]; - - if (!value || value.length == 0) return false; - return true; - } - static async getStockQuantity( db: DatabaseCore, item: string, @@ -184,4 +167,4 @@ export class BespokeQueries { return value[0][Object.keys(value[0])[0]]; } -} \ No newline at end of file +} diff --git a/fyo/core/dbHandler.ts b/fyo/core/dbHandler.ts index b84c61df..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 @@ -325,12 +325,6 @@ export class DatabaseHandler extends DatabaseBase { )) as number | null; } - async itemHasTransactions(item: string): Promise { - return (await this.#demux.callBespoke('itemHasTransactions', item)) as - | boolean - | null; - } - /** * Internal methods */ diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 7f50e5ac..ce609582 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -20,33 +20,6 @@ export class Item extends Doc { for?: 'Purchases' | 'Sales' | 'Both'; hasBatchNumber?: boolean; - async beforeSync() { - /* - * This code block is to prevent users from changing the value of Has Batch No Checkbox - of the items which already did transactions - * allowing users to change the value of Has Batch No of the items which already did - transactions will result in incorect SLEs - */ - const ifItemHasBatchNumber = await this.fyo.db.get( - 'Item', - this.name!, - 'hasBatchNumber' - ); - - if (this.hasBatchNumber == ifItemHasBatchNumber.hasBatchNumber) { - return; - } - - const isItemExistsInSLE = await this.fyo.db.itemHasTransactions(this.name!); - - if (isItemExistsInSLE) { - throw new ValidationError( - this.fyo.t`Cannot change value of Has Batch No as ${this - .name!} already has transactions against it. ` - ); - } - } - formulas: FormulaMap = { incomeAccount: { formula: async () => { @@ -148,7 +121,7 @@ export class Item extends Doc { !this.fyo.singles.AccountingSettings?.enableInventory || this.itemType !== 'Product' || (this.inserted && !this.trackItem), - hasBatchNumber: () => !this.trackItem || false, + hasBatchNumber: () => !this.trackItem, barcode: () => !this.fyo.singles.InventorySettings?.enableBarcodes, }; @@ -156,5 +129,6 @@ export class Item extends Doc { unit: () => this.inserted, itemType: () => this.inserted, trackItem: () => this.inserted, + hasBatchNumber: () => this.inserted, }; } From ec31a3ba6533b57f0e260f3f481b0c3b1bc4922e Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 15:30:44 +0530 Subject: [PATCH 21/27] refactor: batch-wise stock availability validation --- models/inventory/StockManager.ts | 59 ++++++++++++++++---------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/models/inventory/StockManager.ts b/models/inventory/StockManager.ts index 19e2e165..c52db859 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -132,44 +132,45 @@ export class StockManager { return; } - const ifItemHasBatchNumber = await this.fyo.getValue( + const isItemHasBatchNumber = await this.fyo.getValue( 'Item', details.item, 'hasBatchNumber' ); + + if (!isItemHasBatchNumber) return; + const date = details.date.toISOString(); const formattedDate = this.fyo.format(details.date, 'Datetime'); - if (ifItemHasBatchNumber) { - if (!details.batchNumber) { - throw new ValidationError( - t`Please enter Batch Number for ${details.item}` - ); - } + 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; + const itemsInBatch = + (await this.fyo.db.getStockQuantity( + details.item, + details.fromLocation, + undefined, + date, + details.batchNumber + )) ?? 0; - if (details.quantity > itemsInBatch) { - throw new ValidationError( - [ - t`Insufficient Quantity in Batch ${details.batchNumber}`, - 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') - ); - } + if (details.quantity > itemsInBatch) { + throw new ValidationError( + [ + t`Insufficient Quantity in Batch ${details.batchNumber}`, + 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') + ); } } From 4fc6c3cf1b82c2491e48a327f322b5f8c915dee7 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 15:45:36 +0530 Subject: [PATCH 22/27] refactor: validateBatchWiseStockAvailability --- models/inventory/StockManager.ts | 33 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/models/inventory/StockManager.ts b/models/inventory/StockManager.ts index c52db859..f44a59df 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -139,9 +139,6 @@ export class StockManager { ); if (!isItemHasBatchNumber) return; - - const date = details.date.toISOString(); - const formattedDate = this.fyo.format(details.date, 'Datetime'); if (!details.batchNumber) { throw new ValidationError( @@ -149,6 +146,7 @@ export class StockManager { ); } + const date = details.date.toISOString(); const itemsInBatch = (await this.fyo.db.getStockQuantity( details.item, @@ -158,20 +156,21 @@ export class StockManager { details.batchNumber )) ?? 0; - if (details.quantity > itemsInBatch) { - throw new ValidationError( - [ - t`Insufficient Quantity in Batch ${details.batchNumber}`, - 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') - ); - } + if (details.quantity < itemsInBatch) return; + + const formattedDate = this.fyo.format(details.date, 'Datetime'); + throw new ValidationError( + [ + t`Insufficient Quantity in Batch ${details.batchNumber}`, + 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') + ); } async #validateStockAvailability(details: SMIDetails) { From 09591703e4f1dd494ab9eebd5d13603f62967a24 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Fri, 17 Feb 2023 16:04:46 +0530 Subject: [PATCH 23/27] refactor: validateStockAvailability --- models/inventory/StockManager.ts | 86 +++++++++++++++----------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/models/inventory/StockManager.ts b/models/inventory/StockManager.ts index f44a59df..a78de105 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -88,7 +88,6 @@ export class StockManager { this.#validateQuantity(details); this.#validateLocation(details); await this.#validateStockAvailability(details); - await this.#validateBatchWiseStockAvailability(details); } #validateQuantity(details: SMIDetails) { @@ -127,58 +126,51 @@ export class StockManager { throw new ValidationError(t`Both From and To Location cannot be undefined`); } - async #validateBatchWiseStockAvailability(details: SMIDetails) { - if (!details.fromLocation) { - return; - } - - const isItemHasBatchNumber = await this.fyo.getValue( - 'Item', - details.item, - 'hasBatchNumber' - ); - - if (!isItemHasBatchNumber) return; - - if (!details.batchNumber) { - throw new ValidationError( - t`Please enter Batch Number for ${details.item}` - ); - } - - const date = details.date.toISOString(); - const itemsInBatch = - (await this.fyo.db.getStockQuantity( - details.item, - details.fromLocation, - undefined, - date, - details.batchNumber - )) ?? 0; - - if (details.quantity < itemsInBatch) return; - - const formattedDate = this.fyo.format(details.date, 'Datetime'); - throw new ValidationError( - [ - t`Insufficient Quantity in Batch ${details.batchNumber}`, - 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') - ); - } - async #validateStockAvailability(details: SMIDetails) { if (!details.fromLocation) { return; } const date = details.date.toISOString(); + const isItemHasBatchNumber = await this.fyo.getValue( + 'Item', + details.item, + 'hasBatchNumber' + ); + + if (isItemHasBatchNumber) { + 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; + + const formattedDate = this.fyo.format(details.date, 'Datetime'); + throw new ValidationError( + [ + t`Insufficient Quantity in Batch ${details.batchNumber}`, + 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, From 306372db1c419385b3bf3362cab5c9f672861f5d Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Tue, 21 Feb 2023 11:37:55 +0530 Subject: [PATCH 24/27] test: batchNumber transactions --- models/inventory/StockManager.ts | 10 +- models/inventory/tests/helpers.ts | 24 ++- models/inventory/tests/testInventory.spec.ts | 214 ++++++++++++++++++- models/types.ts | 1 + 4 files changed, 236 insertions(+), 13 deletions(-) diff --git a/models/inventory/StockManager.ts b/models/inventory/StockManager.ts index a78de105..f5d0adc5 100644 --- a/models/inventory/StockManager.ts +++ b/models/inventory/StockManager.ts @@ -132,13 +132,14 @@ 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) { + if (isItemHasBatchNumber && !this.isCancelled) { if (!details.batchNumber) { throw new ValidationError( t`Please enter Batch Number for ${details.item}` @@ -154,12 +155,11 @@ export class StockManager { details.batchNumber )) ?? 0; - if (details.quantity < itemsInBatch) return; + if (details.quantity <= itemsInBatch) return; - const formattedDate = this.fyo.format(details.date, 'Datetime'); throw new ValidationError( [ - t`Insufficient Quantity in Batch ${details.batchNumber}`, + t`Insufficient Quantity`, t`Additional quantity (${ details.quantity - itemsInBatch }) is required in batch ${ @@ -180,8 +180,6 @@ export class StockManager { undefined )) ?? 0; - const formattedDate = this.fyo.format(details.date, 'Datetime'); - if (this.isCancelled) { quantityBefore += details.quantity; } 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..eaa75bfd 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, + undefined, + undefined, + undefined, + batchNumber + ), + quantity + ); +}); + test('create stock movement, material transfer', async (t) => { const { rate } = itemMap.Ink; const quantity = 2; @@ -136,6 +214,60 @@ 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.Ink.name), quantity); +}); + test('create stock movement, material issue', async (t) => { const { rate } = itemMap.Ink; const quantity = 2; @@ -169,6 +301,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 +390,7 @@ async function runEntries( item: string; to?: string; from?: string; + batchNumber?: string; quantity: number; rate: number; }[]; @@ -235,7 +412,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 +426,7 @@ test('create stock movements, invalid entries, in sequence', async (t) => { { item, to: locationMap.LocationOne, + batchNumber: batchNumberMap.batchNumberOne.name, quantity, rate, }, @@ -264,6 +442,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 +457,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 +472,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 +488,7 @@ test('create stock movements, invalid entries, in sequence', async (t) => { { item, from: locationMap.LocationTwo, + batchNumber: batchNumberMap.batchNumberOne.name, quantity, rate, }, @@ -371,4 +553,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/types.ts b/models/types.ts index 330c768f..bf36f267 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', From a665313e808ccd2ea026a9b1f6b5b92ebdb0c84b Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Tue, 21 Feb 2023 11:55:23 +0530 Subject: [PATCH 25/27] test: batchNumber transactions --- models/inventory/tests/testInventory.spec.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/models/inventory/tests/testInventory.spec.ts b/models/inventory/tests/testInventory.spec.ts index eaa75bfd..008c8369 100644 --- a/models/inventory/tests/testInventory.spec.ts +++ b/models/inventory/tests/testInventory.spec.ts @@ -159,7 +159,7 @@ test('Batch Enabled : create stock movement, material receipt', async (t) => { t.equal( await fyo.db.getStockQuantity( itemMap.Pen.name, - undefined, + locationMap.LocationOne, undefined, undefined, batchNumber @@ -265,7 +265,16 @@ test('Batch Enabled create stock movement, material transfer', async (t) => { ), 0 ); - t.equal(await fyo.db.getStockQuantity(itemMap.Ink.name), quantity); + t.equal( + await fyo.db.getStockQuantity( + itemMap.Pen.name, + locationMap.LocationTwo, + undefined, + undefined, + batchNumber + ), + quantity + ); }); test('create stock movement, material issue', async (t) => { From 7d97e72b92c93bfe62d6323d5bdc2a657413841c Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Wed, 22 Feb 2023 12:35:18 +0530 Subject: [PATCH 26/27] feat: batchNumber in print template --- schemas/app/PrintSettings.json | 6 ++++++ .../SalesInvoice/Templates/BaseTemplate.vue | 1 + src/components/SalesInvoice/Templates/Basic.vue | 16 ++++++++++++++-- .../SalesInvoice/Templates/Business.vue | 6 ++++++ .../SalesInvoice/Templates/Minimal.vue | 6 ++++++ 5 files changed, 33 insertions(+), 2 deletions(-) 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/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 }}
From 707fb798e7198a43390d1a695ecb4646857acabe Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Wed, 22 Feb 2023 14:47:48 +0530 Subject: [PATCH 27/27] fix: move hasBatchNumber to Inventory Section --- schemas/app/Item.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/schemas/app/Item.json b/schemas/app/Item.json index 200e6d99..47d0901c 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -135,7 +135,8 @@ "fieldname": "hasBatchNumber", "label": "Has Batch No", "fieldtype": "Check", - "default": false + "default": false, + "section": "Inventory" }, { "fieldname": "uomConversions",