mirror of
https://github.com/frappe/books.git
synced 2026-08-24 02:24:17 -05:00
Merge pull request #1255 from suhailanzar/partial-stock-return
fix: implemented complete partial item return handling
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
getPricingRulesConflicts,
|
||||
removeLoyaltyPoint,
|
||||
roundFreeItemQty,
|
||||
getReturnQtyTotal,
|
||||
} from 'models/helpers';
|
||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||
import { validateBatch } from 'models/inventory/helpers';
|
||||
@@ -94,6 +95,7 @@ export abstract class Invoice extends Transactional {
|
||||
|
||||
isReturned?: boolean;
|
||||
returnAgainst?: string;
|
||||
isFullyReturned?: boolean;
|
||||
|
||||
pricingRuleDetail?: PricingRuleDetail[];
|
||||
|
||||
@@ -196,7 +198,8 @@ export abstract class Invoice extends Transactional {
|
||||
await this._removeLoyaltyPointEntry();
|
||||
await this._updateIsItemsReturned();
|
||||
this.reduceUsedCountOfCoupons();
|
||||
|
||||
if (this.schemaName === ModelNameEnum.SalesInvoice)
|
||||
[await this.updateIsItemsFullyReturned(this)];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -456,6 +459,9 @@ export abstract class Invoice extends Transactional {
|
||||
}, (this.netTotal as Money).abs())
|
||||
.sub(totalDiscount);
|
||||
|
||||
if (this.redeemLoyaltyPoints) {
|
||||
return this.getLPAddedBaseGrandTotal();
|
||||
}
|
||||
return grandTotal;
|
||||
}
|
||||
|
||||
@@ -648,6 +654,8 @@ export abstract class Invoice extends Transactional {
|
||||
|
||||
let returnDocItems: DocValueMap[] = [];
|
||||
|
||||
const totalQtyOfReturnedItems = await getReturnQtyTotal(this);
|
||||
|
||||
const returnBalanceItemsQty = await this.fyo.db.getReturnBalanceItemsQty(
|
||||
this.schemaName,
|
||||
this.name
|
||||
@@ -655,13 +663,16 @@ export abstract class Invoice extends Transactional {
|
||||
|
||||
for (const item of docItems) {
|
||||
if (!returnBalanceItemsQty) {
|
||||
returnDocItems = docItems;
|
||||
returnDocItems = docItems.map((docItem) => ({
|
||||
...docItem,
|
||||
name: undefined,
|
||||
quantity: -(totalQtyOfReturnedItems[docItem.item as string] || 0),
|
||||
}));
|
||||
|
||||
for (const row of returnDocItems) {
|
||||
row.name = undefined;
|
||||
row.itemDiscountedTotal = await this.getItemsDiscountedTotal(
|
||||
row as InvoiceItem
|
||||
);
|
||||
(row.quantity as number) *= -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -677,6 +688,10 @@ export abstract class Invoice extends Transactional {
|
||||
const returnedItem: ReturnDocItem | undefined =
|
||||
returnBalanceItemsQty[item.item as string];
|
||||
|
||||
if (!returnedItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let quantity = returnedItem.quantity;
|
||||
let serialNumber: string | undefined =
|
||||
returnedItem.serialNumbers?.join('\n');
|
||||
@@ -703,6 +718,22 @@ export abstract class Invoice extends Transactional {
|
||||
quantity: quantity,
|
||||
});
|
||||
}
|
||||
|
||||
returnDocItems.forEach((docItems) => {
|
||||
const itemName = docItems.item as string;
|
||||
if (itemName in totalQtyOfReturnedItems) {
|
||||
docItems.quantity = totalQtyOfReturnedItems[itemName];
|
||||
}
|
||||
});
|
||||
|
||||
returnDocItems = returnDocItems.filter(
|
||||
(docItems) => (docItems.quantity as number) > 0
|
||||
);
|
||||
|
||||
returnDocItems.forEach((docItems) => {
|
||||
docItems.quantity = -(docItems.quantity as number);
|
||||
});
|
||||
|
||||
const returnDocData = {
|
||||
...docData,
|
||||
name: undefined,
|
||||
@@ -745,6 +776,32 @@ 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
|
||||
);
|
||||
}
|
||||
|
||||
const totalQtyOfReturnedItems = await getReturnQtyTotal(
|
||||
(sinvDoc as Invoice) ?? this
|
||||
);
|
||||
const isFullyReturned = Object.values(totalQtyOfReturnedItems).every(
|
||||
(quantity) => quantity === 0
|
||||
);
|
||||
if (!isFullyReturned) {
|
||||
return;
|
||||
}
|
||||
const invoiceDoc = await this.fyo.doc.getDoc(
|
||||
this.schemaName,
|
||||
this.returnAgainst
|
||||
);
|
||||
await invoiceDoc.setAndSync({ isFullyReturned });
|
||||
await invoiceDoc.submit();
|
||||
}
|
||||
|
||||
async _updateIsItemsReturned() {
|
||||
if (!this.isReturn || !this.returnAgainst || this.isQuote) {
|
||||
return;
|
||||
@@ -818,6 +875,10 @@ export abstract class Invoice extends Transactional {
|
||||
this.loyaltyPoints as number
|
||||
);
|
||||
|
||||
if (this.redeemLoyaltyPoints && (this.loyaltyPoints as number) > 0) {
|
||||
this.grandTotal?.add(totalLotaltyAmount);
|
||||
}
|
||||
|
||||
return this.grandTotal?.sub(totalLotaltyAmount);
|
||||
}
|
||||
|
||||
@@ -910,10 +971,6 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.redeemLoyaltyPoints) {
|
||||
return await this.getLPAddedBaseGrandTotal();
|
||||
}
|
||||
|
||||
return this.baseGrandTotal;
|
||||
},
|
||||
dependsOn: ['discountAmount', 'discountPercent'],
|
||||
|
||||
@@ -401,16 +401,7 @@ export abstract class InvoiceItem extends Doc {
|
||||
|
||||
return getTaxedTotalBeforeDiscounting(totalTaxRate, rate, quantity);
|
||||
},
|
||||
dependsOn: [
|
||||
'itemDiscountAmount',
|
||||
'itemDiscountPercent',
|
||||
'itemDiscountedTotal',
|
||||
'setItemDiscountAmount',
|
||||
'tax',
|
||||
'rate',
|
||||
'quantity',
|
||||
'item',
|
||||
],
|
||||
dependsOn: ['rate', 'quantity', 'item'],
|
||||
},
|
||||
stockNotTransferred: {
|
||||
formula: async () => {
|
||||
@@ -735,12 +726,12 @@ function getDiscountedTotalBeforeTaxation(
|
||||
* - if percent: Quantity * Rate (1 - DiscountPercent / 100)
|
||||
*/
|
||||
|
||||
const amount = rate.mul(quantity);
|
||||
if (setDiscountAmount) {
|
||||
return amount.sub(itemDiscountAmount);
|
||||
return rate.sub(itemDiscountAmount).mul(quantity);
|
||||
} else if (itemDiscountPercent > 0) {
|
||||
return rate.mul(quantity).percent(itemDiscountPercent);
|
||||
}
|
||||
|
||||
return amount.mul(1 - itemDiscountPercent / 100);
|
||||
return rate.mul(quantity);
|
||||
}
|
||||
|
||||
function getTaxedTotalAfterDiscounting(
|
||||
|
||||
@@ -470,11 +470,9 @@ export class Payment extends Transactional {
|
||||
|
||||
for (const row of this.for ?? []) {
|
||||
if (!this.fyo.singles.AccountingSettings?.enablePartialPayment) {
|
||||
const amount = !(this.amountPaid as Money).isZero()
|
||||
? (this.amountPaid as Money)
|
||||
: (this.amount as Money);
|
||||
const initialAmount = this.initialAmount as Money;
|
||||
if (amount.lt(initialAmount) && !amount.eq(initialAmount)) {
|
||||
const amount = this.amount as Money;
|
||||
const totalAmount = this.totalAmount as Money;
|
||||
if (amount.lt(totalAmount)) {
|
||||
if (this.writeoff?.isZero()) {
|
||||
this.amount = this.initialAmount;
|
||||
row.amountPaid = this.fyo.pesa(0);
|
||||
@@ -705,7 +703,7 @@ export class Payment extends Transactional {
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
amount: (value: DocValue) => {
|
||||
amount: async (value: DocValue) => {
|
||||
if ((value as Money).isNegative()) {
|
||||
throw new ValidationError(
|
||||
this.fyo.t`Payment amount cannot be less than zero.`
|
||||
@@ -716,13 +714,22 @@ export class Payment extends Transactional {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.initialAmount) {
|
||||
this.initialAmount = this.amount as Money;
|
||||
if (!this.totalAmount) {
|
||||
for (const row of this.for ?? []) {
|
||||
const referenceDoc = (await this.fyo.doc.getDoc(
|
||||
row.referenceType as string,
|
||||
row.referenceName as string
|
||||
)) as Invoice;
|
||||
|
||||
this.totalAmount = referenceDoc.outstandingAmount?.abs();
|
||||
}
|
||||
}
|
||||
if ((value as Money).gt(this.initialAmount)) {
|
||||
|
||||
if ((value as Money).gt(this.totalAmount as Money)) {
|
||||
this.amount = this.initialAmount;
|
||||
throw new ValidationError(
|
||||
this.fyo.t`Payment amount cannot exceed ${this.fyo.format(
|
||||
this.initialAmount,
|
||||
this.totalAmount,
|
||||
'Currency'
|
||||
)}.`
|
||||
);
|
||||
|
||||
+44
-1
@@ -299,7 +299,6 @@ export function getMakeReturnDocAction(fyo: Fyo): Action {
|
||||
label: fyo.t`Return`,
|
||||
group: fyo.t`Create`,
|
||||
condition: (doc: Doc) =>
|
||||
!doc.isReturn &&
|
||||
(!!fyo.singles.AccountingSettings?.enableInvoiceReturns ||
|
||||
!!fyo.singles.InventorySettings?.enableStockReturns) &&
|
||||
doc.isSubmitted &&
|
||||
@@ -737,6 +736,50 @@ export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
|
||||
await item.set('item', name);
|
||||
}
|
||||
|
||||
export async function getReturnQtyTotal(
|
||||
doc: Invoice
|
||||
): Promise<Record<string, number>> {
|
||||
const returnDocs = await doc.fyo.db.getAll(doc.schemaName, {
|
||||
fields: ['*'],
|
||||
filters: {
|
||||
returnAgainst: doc.name as string,
|
||||
},
|
||||
});
|
||||
|
||||
const returnedDocs = await Promise.all(
|
||||
returnDocs.map((docss) =>
|
||||
doc.fyo.doc.getDoc(doc.schemaName, docss.name as string)
|
||||
)
|
||||
);
|
||||
|
||||
const quantitySum: { [key: string]: number } = {};
|
||||
|
||||
if ('items' in doc && Array.isArray(doc.items)) {
|
||||
doc.items.forEach((docItem) => {
|
||||
const itemName = docItem.item as string;
|
||||
if (itemName) {
|
||||
quantitySum[itemName] = (docItem.quantity as number) || 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!returnedDocs) {
|
||||
return quantitySum;
|
||||
}
|
||||
returnedDocs.forEach((returnedDoc) => {
|
||||
if (returnedDoc && returnedDoc.items) {
|
||||
(returnedDoc.items as InvoiceItem[]).forEach((item) => {
|
||||
const itemName = item.item;
|
||||
if (itemName && quantitySum.hasOwnProperty(itemName)) {
|
||||
quantitySum[itemName] =
|
||||
quantitySum[itemName] - Math.abs(item.quantity as number);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return quantitySum;
|
||||
}
|
||||
|
||||
export async function createLoyaltyPointEntry(doc: Invoice) {
|
||||
const loyaltyProgramDoc = (await doc.fyo.doc.getDoc(
|
||||
ModelNameEnum.LoyaltyProgram,
|
||||
|
||||
@@ -187,6 +187,12 @@
|
||||
"hidden": true,
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "isFullyReturned",
|
||||
"fieldtype": "Check",
|
||||
"hidden": true,
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "isSyncedWithErp",
|
||||
"fieldtype": "Check",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Modal class="h-96 w-96" :set-close-listener="false">
|
||||
<p class="text-center py-4">Redeem Loyalty Points</p>
|
||||
<p class="text-center py-4 dark:text-gray-100">Redeem Loyalty Points</p>
|
||||
|
||||
<hr class="dark:border-gray-800" />
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<p>{{ loyaltyPoints }}</p>
|
||||
<p class="dark:text-gray-100">{{ loyaltyPoints }}</p>
|
||||
</div>
|
||||
|
||||
<Int
|
||||
|
||||
@@ -610,7 +610,7 @@ export default defineComponent({
|
||||
},
|
||||
async setLoyaltyPoints(value: number) {
|
||||
this.appliedLoyaltyPoints = value;
|
||||
this.sinvDoc.redeemLoyaltyPoints = true;
|
||||
await this.sinvDoc.set('redeemLoyaltyPoints', true);
|
||||
|
||||
const totalLotaltyAmount = await getAddedLPWithGrandTotal(
|
||||
this.fyo,
|
||||
@@ -623,6 +623,7 @@ export default defineComponent({
|
||||
.abs();
|
||||
|
||||
this.sinvDoc.grandTotal = total;
|
||||
this.sinvDoc.outstandingAmount = total;
|
||||
},
|
||||
async selectedInvoiceName(doc: SalesInvoice) {
|
||||
const salesInvoiceDoc = (await this.fyo.doc.getDoc(
|
||||
|
||||
@@ -212,11 +212,11 @@ export default defineComponent({
|
||||
|
||||
const returnedInvoiceNames = allInvoices
|
||||
.filter((inv) => {
|
||||
if (inv.isReturned) {
|
||||
if (inv.isFullyReturned || inv.returnAgainst) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (inv.isReturned && !(inv.outstandingAmount as Money).isZero()) {
|
||||
if (inv.isReturned && !inv.isFullyReturned) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user