Merge pull request #1314 from Gadha2311/batchwise-returninvoice

fix: resolve batch-wise return issue for loyalty points
This commit is contained in:
Able k Saju
2025-08-06 15:13:32 +05:30
committed by GitHub
6 changed files with 96 additions and 30 deletions
+64 -17
View File
@@ -26,6 +26,7 @@ import {
roundFreeItemQty,
getReturnQtyTotal,
getReturnLoyaltyPoints,
getItemQtyMap,
} from 'models/helpers';
import { StockTransfer } from 'models/inventory/StockTransfer';
import { validateBatch } from 'models/inventory/helpers';
@@ -221,9 +222,7 @@ export abstract class Invoice extends Transactional {
await this._removeLoyaltyPointEntry();
await this._updateIsItemsReturned();
this.reduceUsedCountOfCoupons();
if (this.schemaName === ModelNameEnum.SalesInvoice)
[await this.updateIsItemsFullyReturned(this)];
return;
await this.updateIsItemsFullyReturned(this);
}
if (this.isQuote) {
@@ -264,7 +263,9 @@ export abstract class Invoice extends Transactional {
}
await this._updateIsItemsReturned();
await this._createLoyaltyPointEntry();
if (!this.isReturn) {
await this._createLoyaltyPointEntry();
}
if (this.schemaName === ModelNameEnum.SalesInvoice) {
this.updateUsedCountOfCoupons();
@@ -692,6 +693,17 @@ export abstract class Invoice extends Transactional {
for (const item of docItems) {
if (totalQtyOfReturnedItems) {
if (item.isFreeItem) {
returnDocItems.push({
...item,
name: undefined,
quantity: -(item.quantity as number),
transferQuantity: -(
(item.quantity as number) / (item.unitConversionFactor as number)
),
});
continue;
}
if (item.batch) {
const returnData = totalQtyOfReturnedItems[item.item as string];
if (typeof returnData === 'object' && returnData?.batches) {
@@ -822,19 +834,21 @@ export abstract class Invoice extends Transactional {
}
async updateIsItemsFullyReturned(doc?: Invoice) {
let sinvDoc;
if (doc?.returnAgainst) {
sinvDoc = await this.fyo.doc.getDoc(
ModelNameEnum.SalesInvoice,
doc.returnAgainst
);
if (!doc?.returnAgainst || doc.schemaName !== ModelNameEnum.SalesInvoice) {
return;
}
const sinvDoc = await this.fyo.doc.getDoc(
ModelNameEnum.SalesInvoice,
doc.returnAgainst
);
const totalQtyOfReturnedItems = await getReturnQtyTotal(
(sinvDoc as Invoice) ?? this
);
const isFullyReturned = Object.values(totalQtyOfReturnedItems).every(
(quantity) => quantity === 0
(value) =>
typeof value === 'number' ? value === 0 : value?.quantity === 0
);
if (!isFullyReturned) {
return;
@@ -1360,6 +1374,21 @@ export abstract class Invoice extends Transactional {
continue;
}
const isFreeItem = row.isFreeItem ?? false;
if (isFreeItem) {
await transfer.append('items', {
item: row.item,
quantity: row.quantity,
location,
rate: this.fyo.pesa(0),
batch: row.batch || null,
description: row.description,
hsnCode: row.hsnCode,
isFreeItem,
});
continue;
}
let quantity;
if (itemDoc.trackItem) {
quantity = row.stockNotTransferred;
@@ -1567,7 +1596,7 @@ export abstract class Invoice extends Transactional {
}
clearFreeItems() {
if (this.pricingRuleDetail?.length || !this.items) {
if (this.pricingRuleDetail?.length || !this.items || this.isReturn) {
return;
}
@@ -1585,7 +1614,9 @@ export abstract class Invoice extends Transactional {
return;
}
this.items = this.items.filter((item) => !item.isFreeItem);
if (!this.isReturn) {
this.items = this.items.filter((item) => !item.isFreeItem);
}
for (const item of this.items) {
const pricingRuleDetailForItem = this.pricingRuleDetail?.filter(
@@ -1629,18 +1660,34 @@ export abstract class Invoice extends Transactional {
}
if (pricingRuleDoc.roundFreeItemQty) {
roundFreeItemQty(
roundFreeItemQuantity = roundFreeItemQty(
roundFreeItemQuantity,
pricingRuleDoc.roundingMethod as 'round' | 'floor' | 'ceil'
'floor'
);
}
if (roundFreeItemQuantity <= 0) {
throw new ValidationError(
t`Free item "${
pricingRuleDoc.freeItem as string
}" was not added due to zero
quantity`
);
}
const freeItem = pricingRuleDoc.freeItem as string;
const itemQtyMap = await getItemQtyMap(this as SalesInvoice);
const availableQty = itemQtyMap[freeItem]?.availableQty ?? 0;
if (availableQty < roundFreeItemQuantity) {
continue;
}
await this.append('items', {
item: pricingRuleDoc.freeItem as string,
item: freeItem,
quantity: roundFreeItemQuantity,
isFreeItem: true,
pricingRule: pricingRuleDoc.title,
rate: pricingRuleDoc.freeItemRate,
unit: pricingRuleDoc.freeItemUnit,
});
}
@@ -3,9 +3,11 @@ import { ListViewSettings } from 'fyo/model/types';
export class LoyaltyPointEntry extends Doc {
loyaltyProgram?: string;
loyaltyProgramTier?: string;
customer?: string;
invoice?: string;
purchaseAmount?: number;
postingDate?: Date;
expiryDate?: Date;
static override getListViewSettings(): ListViewSettings {
@@ -30,7 +30,6 @@ export class PricingRule extends Doc {
freeItem?: string;
freeItemQuantity?: number;
freeItemUnit?: string;
freeItemRate?: Money;
roundFreeItemQty?: number;
roundingMethod?: string;
@@ -158,7 +157,6 @@ export class PricingRule extends Doc {
freeItem: () => this.isDiscountTypeIsPriceDiscount,
freeItemQuantity: () => this.isDiscountTypeIsPriceDiscount,
freeItemUnit: () => this.isDiscountTypeIsPriceDiscount,
freeItemRate: () => this.isDiscountTypeIsPriceDiscount,
roundFreeItemQty: () => this.isDiscountTypeIsPriceDiscount,
roundingMethod: () =>
this.isDiscountTypeIsPriceDiscount || !this.roundFreeItemQty,
+29 -5
View File
@@ -47,6 +47,7 @@ import {
getStockBalanceEntries,
getStockLedgerEntries,
} from 'reports/inventory/helpers';
import { LoyaltyPointEntry } from './baseModels/LoyaltyPointEntry/LoyaltyPointEntry';
export function getQuoteActions(
fyo: Fyo,
@@ -748,14 +749,16 @@ export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
export async function getReturnLoyaltyPoints(doc: Invoice) {
const returnDocs = await doc.fyo.db.getAll(doc.schemaName, {
fields: ['*'],
fields: ['name', 'loyaltyPoints'],
filters: {
returnAgainst: doc.returnAgainst as string,
submitted: true,
},
});
const totalLoyaltyPoints = returnDocs.reduce(
const sunvDocs = returnDocs.filter((sinvDoc) => sinvDoc.name !== doc.name);
const totalLoyaltyPoints = sunvDocs.reduce(
(sum, doc) => sum + Math.abs(doc.loyaltyPoints as number),
0
);
@@ -960,17 +963,38 @@ export async function removeLoyaltyPoint(doc: Doc) {
return;
}
const loyalityPointEntryDoc = await doc.fyo.doc.getDoc(
const lPEntryDoc = (await doc.fyo.doc.getDoc(
ModelNameEnum.LoyaltyPointEntry,
data[0].name
);
)) as LoyaltyPointEntry;
const newLoyaltyPoint =
(lPEntryDoc?.loyaltyPoints as number) +
Math.abs(doc.loyaltyPoints as number);
if (newLoyaltyPoint !== 0) {
const newLoyaltyPointEntry = doc.fyo.doc.getNewDoc(
ModelNameEnum.LoyaltyPointEntry,
{
loyaltyProgram: lPEntryDoc.loyaltyProgram,
customer: lPEntryDoc.customer,
invoice: lPEntryDoc.invoice,
postingDate: lPEntryDoc.date as Date,
purchaseAmount: lPEntryDoc.purchaseAmount,
expiryDate: lPEntryDoc.expiryDate,
loyaltyProgramTier: lPEntryDoc.loyaltyProgramTier,
loyaltyPoints: newLoyaltyPoint,
}
);
await newLoyaltyPointEntry.sync();
}
const party = (await doc.fyo.doc.getDoc(
ModelNameEnum.Party,
doc.party as string
)) as Party;
await loyalityPointEntryDoc.delete();
await lPEntryDoc.delete();
await party.updateLoyaltyPoints();
}
-6
View File
@@ -147,12 +147,6 @@
"target": "UOM",
"section": "Product Discount Scheme"
},
{
"fieldname": "freeItemRate",
"label": "Rate",
"fieldtype": "Currency",
"section": "Product Discount Scheme"
},
{
"fieldname": "roundFreeItemQty",
"label": "Round Free Item Quantity",
+1
View File
@@ -837,6 +837,7 @@ export default defineComponent({
await this.paymentDoc.set('paymentMethod', paymentMethod);
await this.paymentDoc.set('amount', this.fyo.pesa(this.paidAmount.float));
await this.paymentDoc.set('referenceType', ModelNameEnum.SalesInvoice);
const paymentMethodDoc = await this.paymentDoc.loadAndGetLink(
'paymentMethod'