From 1cd03aed34f49111a9cadd4aed0501f32719018a Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Mon, 31 Jul 2023 17:51:16 +0530 Subject: [PATCH] fix: get return balance serial number & batches --- backend/database/bespoke.ts | 186 +++++++++++++++++++++++++----- fyo/core/dbHandler.ts | 5 +- models/inventory/StockTransfer.ts | 138 +++++++++++++--------- models/inventory/types.ts | 20 ++++ 4 files changed, 261 insertions(+), 88 deletions(-) diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index 5479d889..4723f25f 100644 --- a/backend/database/bespoke.ts +++ b/backend/database/bespoke.ts @@ -8,8 +8,8 @@ import { import { ModelNameEnum } from '../../models/types'; import DatabaseCore from './core'; import { BespokeFunction } from './types'; +import { DocItem, ReturnDocItem } from 'models/inventory/types'; import { safeParseFloat } from 'utils/index'; -import { DocValueMap } from 'fyo/core/types'; export class BespokeQueries { [key: string]: BespokeFunction; @@ -187,13 +187,7 @@ export class BespokeQueries { db: DatabaseCore, schemaName: string, docName: string - ): Promise { - const docItems = (await db.knex!(`${schemaName}Item`) - .select('item') - .where('parent', docName) - .groupBy('item') - .sum({ quantity: 'quantity' })) as DocValueMap[]; - + ): Promise | undefined> { const returnDocNames = ( await db.knex!(schemaName) .select('name') @@ -206,41 +200,175 @@ export class BespokeQueries { return; } - const returnedItems = (await db.knex!(`${schemaName}Item`) - .select('item') + const returnedItems: DocItem[] = await db.knex!(`${schemaName}Item`) + .select('item', 'batch', 'serialNumber') .sum({ quantity: 'quantity' }) .whereIn('parent', returnDocNames) - .groupBy('item')) as DocValueMap[]; + .groupBy('item', 'batch', 'serialNumber'); if (!returnedItems.length) { return; } - const returnBalanceItemQty = []; + const docItems: DocItem[] = await db.knex!(`${schemaName}Item`) + .select('name', 'item', 'batch', 'serialNumber') + .where('parent', docName) + .groupBy('item', 'batch', 'serialNumber') + .sum({ quantity: 'quantity' }); - for (const item of returnedItems) { - const docItem = docItems.filter( - (invItem) => invItem.item === item.item - )[0]; + const docItemsMap = BespokeQueries.#getDocItemMap(docItems); + const returnedItemsMap = BespokeQueries.#getDocItemMap(returnedItems); - if (!docItem) { + const returnBalanceItems = BespokeQueries.#getReturnBalanceItemQtyMap( + docItemsMap, + returnedItemsMap + ); + + return returnBalanceItems; + } + + static #getDocItemMap(docItems: DocItem[]): Record { + const docItemsMap: Record = {}; + const batchesMap: + | Record< + string, + { quantity: number; serialNumbers?: string[] | undefined } + > + | undefined = {}; + + for (const item of docItems) { + if (!!docItemsMap[item.item]) { + if (item.batch) { + let serialNumbers: string[] | undefined; + + if (item.serialNumber) { + serialNumbers = item.serialNumber.split('\n'); + docItemsMap[item.item].batches![item.batch] = { + quantity: item.quantity, + serialNumbers, + }; + } + + docItemsMap[item.item].batches![item.batch] = { + quantity: item.quantity, + serialNumbers, + }; + } else { + docItemsMap[item.item].quantity += item.quantity; + } + + if (item.serialNumber) { + const serialNumbers: string[] = []; + + if (docItemsMap[item.item].serialNumbers) { + serialNumbers.push(...(docItemsMap[item.item].serialNumbers ?? [])); + } + + serialNumbers.push(...item.serialNumber.split('\n')); + docItemsMap[item.item].serialNumbers = serialNumbers; + } continue; } - let balanceQty = safeParseFloat( - (docItem.quantity as number) - Math.abs(item.quantity as number) + if (item.batch) { + let serialNumbers: string[] | undefined = undefined; + if (item.serialNumber) { + serialNumbers = item.serialNumber.split('\n'); + } + + batchesMap[item.batch] = { + serialNumbers, + quantity: item.quantity, + }; + } + + let serialNumbers: string[] | undefined = undefined; + + if (!item.batch && item.serialNumber) { + serialNumbers = item.serialNumber.split('\n'); + } + + docItemsMap[item.item] = { + serialNumbers, + batches: batchesMap, + quantity: item.quantity, + }; + } + return docItemsMap; + } + + static #getReturnBalanceItemQtyMap( + docItemsMap: Record, + returnedItemsMap: Record + ): Record { + const returnBalanceItems: Record | undefined = {}; + const balanceBatchQtyMap: + | Record< + string, + { quantity: number; serialNumbers: string[] | undefined } + > + | undefined = {}; + + for (const row in returnedItemsMap) { + const balanceSerialNumbersMap: string[] | undefined = []; + + if (!docItemsMap[row]) { + continue; + } + + const returnedItem = returnedItemsMap[row]; + const docItem = docItemsMap[row]; + let balanceQty = 0; + + const docItemHasBatch = !!Object.keys(docItem.batches ?? {}).length; + const returnedItemHasBatch = !!Object.keys(returnedItem.batches ?? {}) + .length; + + if (docItemHasBatch && returnedItemHasBatch && docItem.batches) { + for (const batch in returnedItem.batches) { + const returnedItemQty = Math.abs( + returnedItem.batches[batch].quantity + ); + const docBatchItemQty = docItem.batches[batch].quantity; + const balanceQty = returnedItemQty - docBatchItemQty; + const docItemSerialNumbers = docItem.batches[batch].serialNumbers; + const returnItemSerialNumbers = + returnedItem.batches[batch].serialNumbers; + + let balanceSerialNumbers: string[] | undefined; + + if (docItemSerialNumbers && returnItemSerialNumbers) { + balanceSerialNumbers = docItemSerialNumbers.filter( + (serialNumber: string) => + returnItemSerialNumbers.indexOf(serialNumber) == -1 + ); + } + + balanceBatchQtyMap[batch] = { + quantity: balanceQty, + serialNumbers: balanceSerialNumbers, + }; + } + } + + if (docItem.serialNumbers && returnedItem.serialNumbers) { + for (const serialNumber of docItem.serialNumbers) { + if (!returnedItem.serialNumbers.includes(serialNumber)) { + balanceSerialNumbersMap.push(serialNumber); + } + } + } + + balanceQty = safeParseFloat( + Math.abs(returnedItem.quantity) - docItemsMap[row].quantity ); - if (balanceQty === 0) { - continue; - } - - if (balanceQty > 0) { - balanceQty *= -1; - } - returnBalanceItemQty.push({ ...item, quantity: balanceQty }); + returnBalanceItems[row] = { + quantity: balanceQty, + batches: balanceBatchQtyMap, + serialNumbers: balanceSerialNumbersMap, + }; } - - return returnBalanceItemQty; + return returnBalanceItems; } } diff --git a/fyo/core/dbHandler.ts b/fyo/core/dbHandler.ts index 11ca7392..cb75cc9c 100644 --- a/fyo/core/dbHandler.ts +++ b/fyo/core/dbHandler.ts @@ -26,6 +26,7 @@ import { DocValueMap, RawValueMap, } from './types'; +import { ReturnDocItem } from 'models/inventory/types'; type FieldMap = Record>; @@ -333,12 +334,12 @@ export class DatabaseHandler extends DatabaseBase { async getReturnBalanceItemsQty( schemaName: string, docName: string - ): Promise { + ): Promise | undefined> { return (await this.#demux.callBespoke( 'getReturnBalanceItemsQty', schemaName, docName - )) as DocValueMap[] | undefined; + )) as Promise | undefined>; } /** diff --git a/models/inventory/StockTransfer.ts b/models/inventory/StockTransfer.ts index 4fd09753..1e187338 100644 --- a/models/inventory/StockTransfer.ts +++ b/models/inventory/StockTransfer.ts @@ -1,5 +1,5 @@ import { t } from 'fyo'; -import { Attachment } from 'fyo/core/types'; +import { Attachment, DocValueMap } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; import { ChangeArg, @@ -26,7 +26,7 @@ import { validateBatch, validateSerialNumber, } from './helpers'; -import { safeParseFloat } from 'utils/index'; +import { ReturnDocItem } from './types'; export abstract class StockTransfer extends Transfer { name?: string; @@ -37,14 +37,17 @@ export abstract class StockTransfer extends Transfer { grandTotal?: Money; backReference?: string; items?: StockTransferItem[]; - isReturn?: boolean; + isReturned?: boolean; returnAgainst?: string; - isItemsReturned?: boolean; get isSales() { return this.schemaName === ModelNameEnum.Shipment; } + get isReturn(): boolean { + return !!this.returnAgainst && this.returnAgainst.length > 1; + } + get invoiceSchemaName() { if (this.isSales) { return ModelNameEnum.SalesInvoice; @@ -65,8 +68,7 @@ export abstract class StockTransfer extends Transfer { terms: () => !(this.terms || !(this.isSubmitted || this.isCancelled)), attachment: () => !(this.attachment || !(this.isSubmitted || this.isCancelled)), - isReturn: () => !this.fyo.singles.AccountingSettings?.enableStockReturns, - returnAgainst: () => !this.isReturn, + returnAgainst: () => this.isSubmitted && !this.returnAgainst, }; static defaults: DefaultMap = { @@ -92,11 +94,6 @@ export abstract class StockTransfer extends Transfer { submitted: true, cancelled: false, }), - returnAgainst: () => ({ - isReturn: false, - submitted: true, - cancelled: false, - }), }; override _getTransferDetails() { @@ -116,9 +113,9 @@ export abstract class StockTransfer extends Transfer { quantity: row.quantity!, batch: row.batch!, serialNumber: row.serialNumber!, + isReturn: row.isReturn, fromLocation, toLocation, - isReturn: this.isReturn, }; }); } @@ -204,14 +201,14 @@ export abstract class StockTransfer extends Transfer { async afterSubmit() { await super.afterSubmit(); - await updateSerialNumbers(this, false); + await updateSerialNumbers(this, false, this.isReturn); await this._updateBackReference(); await this._updateItemsReturned(); } async afterCancel(): Promise { await super.afterCancel(); - await updateSerialNumbers(this, true); + await updateSerialNumbers(this, true, this.isReturn); await this._updateBackReference(); await this._updateItemsReturned(); } @@ -271,27 +268,22 @@ export abstract class StockTransfer extends Transfer { } async _updateItemsReturned() { - if (!this.returnAgainst) { + if (!this.isSubmitted || !this.returnAgainst) { return null; } - const returnDocs = await this.fyo.db.getAll(this.schema.name, { - filters: { - returnAgainst: this.returnAgainst, - submitted: true, - cancelled: false, - }, - }); - - const isItemsReturned = returnDocs.length; + const linkedReference = await this.loadAndGetLink('returnAgainst'); + if (!linkedReference) { + return; + } const referenceDoc = await this.fyo.doc.getDoc( - this.schema.name, - this.returnAgainst + this.schemaName, + linkedReference.name ); + const isReturned = !!referenceDoc; - await referenceDoc.setAndSync({ isItemsReturned }); - await referenceDoc.submit(); + await referenceDoc.setAndSync({ isReturned }); } _getTransferMap() { @@ -422,62 +414,88 @@ export abstract class StockTransfer extends Transfer { return invoice; } - async getReturnDoc(): Promise { - if (!this.items?.length) { - return null; + async getReturnDoc(): Promise { + if (!this.name) { + return; } const docData = this.getValidDict(true, true); - const docItems = docData.items as StockTransferItem[]; - const returnDocItems: StockTransferItem[] = []; + const docItems = docData.items as DocValueMap[]; + + if (!docItems) { + return; + } + + let returnDocItems: DocValueMap[] = []; const returnBalanceItemsQty = await this.fyo.db.getReturnBalanceItemsQty( - this.schema.name, - this.name! + this.schemaName, + this.name ); - for (const item of docItems) { - if (!item.quantity) { + if (!returnBalanceItemsQty) { + returnDocItems = docItems; + returnDocItems.map((row) => { + row.name = undefined; + (row.quantity as number) *= -1; + return row; + }); + break; + } + + const isItemExist = !!returnDocItems.filter( + (balanceItem) => balanceItem.item === item.item + ).length; + + if (isItemExist) { continue; } - let quantity = -1 * item.quantity; + const returnedItem: ReturnDocItem | undefined = + returnBalanceItemsQty[item.item as string]; - if (returnBalanceItemsQty) { - const balanceItemQty = returnBalanceItemsQty.filter( - (i) => i.item === item.item - )[0]; + let quantity = returnedItem.quantity; + let serialNumber: string | undefined = + returnedItem.serialNumbers?.join('\n'); - if (!balanceItemQty) { - continue; + if ( + item.batch && + returnedItem.batches && + returnedItem.batches[item.batch as string] + ) { + quantity = returnedItem.batches[item.batch as string].quantity; + + if (returnedItem.batches[item.batch as string].serialNumbers) { + serialNumber = + returnedItem.batches[item.batch as string].serialNumbers?.join( + '\n' + ); } - quantity = balanceItemQty.quantity as number; } - item.quantity = safeParseFloat(quantity); - delete item.name; - returnDocItems.push(item); + returnDocItems.push({ + ...item, + serialNumber, + name: undefined, + quantity: quantity, + }); } const returnDocData = { ...docData, - name: null, + name: undefined, date: new Date(), items: returnDocItems, - isReturn: true, returnAgainst: docData.name, - grandTotal: this.fyo.pesa(0), - }; + } as DocValueMap; - const rawReturnDoc = this.fyo.doc.getNewDoc( + const newReturnDoc = this.fyo.doc.getNewDoc( this.schema.name, returnDocData ) as StockTransfer; - rawReturnDoc.once('beforeSync', async () => { - await rawReturnDoc.runFormulas(); - }); - return rawReturnDoc; + await newReturnDoc.runFormulas(); + return newReturnDoc; } } @@ -502,6 +520,12 @@ async function validateSerialNumberStatus(doc: StockTransfer) { } const status = snDoc.status ?? 'Inactive'; + const isSubmitted = !!doc.isSubmitted; + const isReturn = !!doc.returnAgainst; + + if (isSubmitted || isReturn) { + return; + } if ( doc.schemaName === ModelNameEnum.PurchaseReceipt && diff --git a/models/inventory/types.ts b/models/inventory/types.ts index d6d16111..a663c326 100644 --- a/models/inventory/types.ts +++ b/models/inventory/types.ts @@ -40,4 +40,24 @@ export interface SMTransferDetails { isReturn?: boolean; } +export interface ReturnBalanceItemQty { + item?: string; + quantity: number; + batch?: string | undefined; + serialNumber?: string; +} + +export interface DocItem { + item: string; + quantity: number; + batch?: string | undefined; + serialNumber?: string; +} + +export interface ReturnDocItem { + quantity: number; + batches?: Record | undefined; + serialNumbers?: string[] | undefined; +} + export interface SMIDetails extends SMDetails, SMTransferDetails {}