2
0
mirror of https://github.com/frappe/books.git synced 2024-09-19 19:19:02 +00:00

fix: get return balance serial number & batches

This commit is contained in:
akshayitzme 2023-07-31 17:51:16 +05:30
parent 9a3a14bd51
commit 1cd03aed34
4 changed files with 261 additions and 88 deletions

View File

@ -8,8 +8,8 @@ import {
import { ModelNameEnum } from '../../models/types'; import { ModelNameEnum } from '../../models/types';
import DatabaseCore from './core'; import DatabaseCore from './core';
import { BespokeFunction } from './types'; import { BespokeFunction } from './types';
import { DocItem, ReturnDocItem } from 'models/inventory/types';
import { safeParseFloat } from 'utils/index'; import { safeParseFloat } from 'utils/index';
import { DocValueMap } from 'fyo/core/types';
export class BespokeQueries { export class BespokeQueries {
[key: string]: BespokeFunction; [key: string]: BespokeFunction;
@ -187,13 +187,7 @@ export class BespokeQueries {
db: DatabaseCore, db: DatabaseCore,
schemaName: string, schemaName: string,
docName: string docName: string
): Promise<DocValueMap[] | undefined> { ): Promise<Record<string, ReturnDocItem> | undefined> {
const docItems = (await db.knex!(`${schemaName}Item`)
.select('item')
.where('parent', docName)
.groupBy('item')
.sum({ quantity: 'quantity' })) as DocValueMap[];
const returnDocNames = ( const returnDocNames = (
await db.knex!(schemaName) await db.knex!(schemaName)
.select('name') .select('name')
@ -206,41 +200,175 @@ export class BespokeQueries {
return; return;
} }
const returnedItems = (await db.knex!(`${schemaName}Item`) const returnedItems: DocItem[] = await db.knex!(`${schemaName}Item`)
.select('item') .select('item', 'batch', 'serialNumber')
.sum({ quantity: 'quantity' }) .sum({ quantity: 'quantity' })
.whereIn('parent', returnDocNames) .whereIn('parent', returnDocNames)
.groupBy('item')) as DocValueMap[]; .groupBy('item', 'batch', 'serialNumber');
if (!returnedItems.length) { if (!returnedItems.length) {
return; 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 docItemsMap = BespokeQueries.#getDocItemMap(docItems);
const docItem = docItems.filter( const returnedItemsMap = BespokeQueries.#getDocItemMap(returnedItems);
(invItem) => invItem.item === item.item
)[0];
if (!docItem) { const returnBalanceItems = BespokeQueries.#getReturnBalanceItemQtyMap(
continue; docItemsMap,
} returnedItemsMap
let balanceQty = safeParseFloat(
(docItem.quantity as number) - Math.abs(item.quantity as number)
); );
if (balanceQty === 0) { return returnBalanceItems;
}
static #getDocItemMap(docItems: DocItem[]): Record<string, ReturnDocItem> {
const docItemsMap: Record<string, ReturnDocItem> = {};
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; continue;
} }
if (balanceQty > 0) { if (item.batch) {
balanceQty *= -1; let serialNumbers: string[] | undefined = undefined;
} if (item.serialNumber) {
returnBalanceItemQty.push({ ...item, quantity: balanceQty }); serialNumbers = item.serialNumber.split('\n');
} }
return returnBalanceItemQty; 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<string, ReturnDocItem>,
returnedItemsMap: Record<string, ReturnDocItem>
): Record<string, ReturnDocItem> {
const returnBalanceItems: Record<string, ReturnDocItem> | 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
);
returnBalanceItems[row] = {
quantity: balanceQty,
batches: balanceBatchQtyMap,
serialNumbers: balanceSerialNumbersMap,
};
}
return returnBalanceItems;
} }
} }

View File

@ -26,6 +26,7 @@ import {
DocValueMap, DocValueMap,
RawValueMap, RawValueMap,
} from './types'; } from './types';
import { ReturnDocItem } from 'models/inventory/types';
type FieldMap = Record<string, Record<string, Field>>; type FieldMap = Record<string, Record<string, Field>>;
@ -333,12 +334,12 @@ export class DatabaseHandler extends DatabaseBase {
async getReturnBalanceItemsQty( async getReturnBalanceItemsQty(
schemaName: string, schemaName: string,
docName: string docName: string
): Promise<DocValueMap[] | undefined> { ): Promise<Record<string, ReturnDocItem> | undefined> {
return (await this.#demux.callBespoke( return (await this.#demux.callBespoke(
'getReturnBalanceItemsQty', 'getReturnBalanceItemsQty',
schemaName, schemaName,
docName docName
)) as DocValueMap[] | undefined; )) as Promise<Record<string, ReturnDocItem> | undefined>;
} }
/** /**

View File

@ -1,5 +1,5 @@
import { t } from 'fyo'; import { t } from 'fyo';
import { Attachment } from 'fyo/core/types'; import { Attachment, DocValueMap } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { import {
ChangeArg, ChangeArg,
@ -26,7 +26,7 @@ import {
validateBatch, validateBatch,
validateSerialNumber, validateSerialNumber,
} from './helpers'; } from './helpers';
import { safeParseFloat } from 'utils/index'; import { ReturnDocItem } from './types';
export abstract class StockTransfer extends Transfer { export abstract class StockTransfer extends Transfer {
name?: string; name?: string;
@ -37,14 +37,17 @@ export abstract class StockTransfer extends Transfer {
grandTotal?: Money; grandTotal?: Money;
backReference?: string; backReference?: string;
items?: StockTransferItem[]; items?: StockTransferItem[];
isReturn?: boolean; isReturned?: boolean;
returnAgainst?: string; returnAgainst?: string;
isItemsReturned?: boolean;
get isSales() { get isSales() {
return this.schemaName === ModelNameEnum.Shipment; return this.schemaName === ModelNameEnum.Shipment;
} }
get isReturn(): boolean {
return !!this.returnAgainst && this.returnAgainst.length > 1;
}
get invoiceSchemaName() { get invoiceSchemaName() {
if (this.isSales) { if (this.isSales) {
return ModelNameEnum.SalesInvoice; return ModelNameEnum.SalesInvoice;
@ -65,8 +68,7 @@ export abstract class StockTransfer extends Transfer {
terms: () => !(this.terms || !(this.isSubmitted || this.isCancelled)), terms: () => !(this.terms || !(this.isSubmitted || this.isCancelled)),
attachment: () => attachment: () =>
!(this.attachment || !(this.isSubmitted || this.isCancelled)), !(this.attachment || !(this.isSubmitted || this.isCancelled)),
isReturn: () => !this.fyo.singles.AccountingSettings?.enableStockReturns, returnAgainst: () => this.isSubmitted && !this.returnAgainst,
returnAgainst: () => !this.isReturn,
}; };
static defaults: DefaultMap = { static defaults: DefaultMap = {
@ -92,11 +94,6 @@ export abstract class StockTransfer extends Transfer {
submitted: true, submitted: true,
cancelled: false, cancelled: false,
}), }),
returnAgainst: () => ({
isReturn: false,
submitted: true,
cancelled: false,
}),
}; };
override _getTransferDetails() { override _getTransferDetails() {
@ -116,9 +113,9 @@ export abstract class StockTransfer extends Transfer {
quantity: row.quantity!, quantity: row.quantity!,
batch: row.batch!, batch: row.batch!,
serialNumber: row.serialNumber!, serialNumber: row.serialNumber!,
isReturn: row.isReturn,
fromLocation, fromLocation,
toLocation, toLocation,
isReturn: this.isReturn,
}; };
}); });
} }
@ -204,14 +201,14 @@ export abstract class StockTransfer extends Transfer {
async afterSubmit() { async afterSubmit() {
await super.afterSubmit(); await super.afterSubmit();
await updateSerialNumbers(this, false); await updateSerialNumbers(this, false, this.isReturn);
await this._updateBackReference(); await this._updateBackReference();
await this._updateItemsReturned(); await this._updateItemsReturned();
} }
async afterCancel(): Promise<void> { async afterCancel(): Promise<void> {
await super.afterCancel(); await super.afterCancel();
await updateSerialNumbers(this, true); await updateSerialNumbers(this, true, this.isReturn);
await this._updateBackReference(); await this._updateBackReference();
await this._updateItemsReturned(); await this._updateItemsReturned();
} }
@ -271,27 +268,22 @@ export abstract class StockTransfer extends Transfer {
} }
async _updateItemsReturned() { async _updateItemsReturned() {
if (!this.returnAgainst) { if (!this.isSubmitted || !this.returnAgainst) {
return null; return null;
} }
const returnDocs = await this.fyo.db.getAll(this.schema.name, { const linkedReference = await this.loadAndGetLink('returnAgainst');
filters: { if (!linkedReference) {
returnAgainst: this.returnAgainst, return;
submitted: true, }
cancelled: false,
},
});
const isItemsReturned = returnDocs.length;
const referenceDoc = await this.fyo.doc.getDoc( const referenceDoc = await this.fyo.doc.getDoc(
this.schema.name, this.schemaName,
this.returnAgainst linkedReference.name
); );
const isReturned = !!referenceDoc;
await referenceDoc.setAndSync({ isItemsReturned }); await referenceDoc.setAndSync({ isReturned });
await referenceDoc.submit();
} }
_getTransferMap() { _getTransferMap() {
@ -422,62 +414,88 @@ export abstract class StockTransfer extends Transfer {
return invoice; return invoice;
} }
async getReturnDoc(): Promise<StockTransfer | null> { async getReturnDoc(): Promise<StockTransfer | undefined> {
if (!this.items?.length) { if (!this.name) {
return null; return;
} }
const docData = this.getValidDict(true, true); const docData = this.getValidDict(true, true);
const docItems = docData.items as StockTransferItem[]; const docItems = docData.items as DocValueMap[];
const returnDocItems: StockTransferItem[] = [];
if (!docItems) {
return;
}
let returnDocItems: DocValueMap[] = [];
const returnBalanceItemsQty = await this.fyo.db.getReturnBalanceItemsQty( const returnBalanceItemsQty = await this.fyo.db.getReturnBalanceItemsQty(
this.schema.name, this.schemaName,
this.name! this.name
); );
for (const item of docItems) { 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; continue;
} }
let quantity = -1 * item.quantity; const returnedItem: ReturnDocItem | undefined =
returnBalanceItemsQty[item.item as string];
if (returnBalanceItemsQty) { let quantity = returnedItem.quantity;
const balanceItemQty = returnBalanceItemsQty.filter( let serialNumber: string | undefined =
(i) => i.item === item.item returnedItem.serialNumbers?.join('\n');
)[0];
if (!balanceItemQty) { if (
continue; 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); returnDocItems.push({
delete item.name; ...item,
returnDocItems.push(item); serialNumber,
name: undefined,
quantity: quantity,
});
} }
const returnDocData = { const returnDocData = {
...docData, ...docData,
name: null, name: undefined,
date: new Date(), date: new Date(),
items: returnDocItems, items: returnDocItems,
isReturn: true,
returnAgainst: docData.name, 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, this.schema.name,
returnDocData returnDocData
) as StockTransfer; ) as StockTransfer;
rawReturnDoc.once('beforeSync', async () => { await newReturnDoc.runFormulas();
await rawReturnDoc.runFormulas(); return newReturnDoc;
});
return rawReturnDoc;
} }
} }
@ -502,6 +520,12 @@ async function validateSerialNumberStatus(doc: StockTransfer) {
} }
const status = snDoc.status ?? 'Inactive'; const status = snDoc.status ?? 'Inactive';
const isSubmitted = !!doc.isSubmitted;
const isReturn = !!doc.returnAgainst;
if (isSubmitted || isReturn) {
return;
}
if ( if (
doc.schemaName === ModelNameEnum.PurchaseReceipt && doc.schemaName === ModelNameEnum.PurchaseReceipt &&

View File

@ -40,4 +40,24 @@ export interface SMTransferDetails {
isReturn?: boolean; 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<string, { quantity: number, serialNumbers?: string[] }> | undefined;
serialNumbers?: string[] | undefined;
}
export interface SMIDetails extends SMDetails, SMTransferDetails {} export interface SMIDetails extends SMDetails, SMTransferDetails {}