fix(stock): use net purchase values in landed cost vouchers (backport #59274) (#59278)

Co-authored-by: pandiyan <pandiyanpalani37@gmail.com>
This commit is contained in:
Mihir Kandoi
2026-09-22 17:20:24 +05:30
committed by GitHub
parent 8e9c0f6e41
commit 6af589208b
3 changed files with 151 additions and 22 deletions

View File

@@ -73,24 +73,42 @@ erpnext.stock.LandedCostVoucher = class LandedCostVoucher extends erpnext.stock.
}
set_applicable_charges_for_item() {
var me = this;
if (this.frm.doc.taxes.length) {
var total_item_cost = 0.0;
var based_on = this.frm.doc.distribute_charges_based_on.toLowerCase();
if (based_on != "distribute manually") {
$.each(this.frm.doc.items || [], function (i, d) {
total_item_cost += flt(d[based_on]);
var items = this.frm.doc.items || [];
items.forEach((item) => {
total_item_cost += flt(item[based_on]);
});
if (items.length) {
total_item_cost = flt(total_item_cost, precision(based_on, items[0]));
}
if (!total_item_cost) {
items.forEach((item) => {
item.applicable_charges = 0;
});
refresh_field("items");
if (items.length) {
frappe.show_alert({
message: __(
"Total {0} of all items is zero, charges cannot be distributed on it.",
[this.frm.doc.distribute_charges_based_on]
),
indicator: "red",
});
}
return;
}
var total_charges = 0.0;
$.each(this.frm.doc.items || [], function (i, item) {
item.applicable_charges =
(flt(item[based_on]) * flt(me.frm.doc.total_taxes_and_charges)) /
flt(total_item_cost);
items.forEach((item) => {
item.applicable_charges = flt(
item.applicable_charges,
(flt(item[based_on]) * flt(this.frm.doc.total_taxes_and_charges)) /
flt(total_item_cost),
precision("applicable_charges", item)
);
total_charges += item.applicable_charges;
@@ -98,7 +116,7 @@ erpnext.stock.LandedCostVoucher = class LandedCostVoucher extends erpnext.stock.
if (total_charges != this.frm.doc.total_taxes_and_charges) {
var diff = this.frm.doc.total_taxes_and_charges - flt(total_charges);
this.frm.doc.items.slice(-1)[0].applicable_charges += diff;
items.slice(-1)[0].applicable_charges += diff;
}
refresh_field("items");
}

View File

@@ -64,7 +64,7 @@ class LandedCostVoucher(Document):
item.item_code = d.item_code
item.description = d.description
item.qty = d.qty
item.rate = d.get("base_rate") or d.get("rate")
item.rate = d.base_rate
item.cost_center = d.cost_center or erpnext.get_default_cost_center(self.company)
item.amount = d.base_amount
item.receipt_document_type = pr.receipt_document_type
@@ -288,22 +288,24 @@ class LandedCostVoucher(Document):
def set_applicable_charges_on_item(self):
if self.get("taxes") and self.distribute_charges_based_on != "Distribute Manually":
total_item_cost = 0.0
items = self.get("items")
total_charges = 0.0
item_count = 0
based_on_field = frappe.scrub(self.distribute_charges_based_on)
for item in self.get("items"):
total_item_cost += item.get(based_on_field)
total_item_cost = sum(flt(item.get(based_on_field)) for item in items)
if items:
total_item_cost = flt(total_item_cost, items[0].precision(based_on_field))
for item in self.get("items"):
if not total_item_cost and not item.get(based_on_field):
frappe.throw(
_(
"It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'"
)
if not total_item_cost:
frappe.throw(
_("Total {0} of all items is zero. Set 'Distribute Charges Based On' to {1}.").format(
self.distribute_charges_based_on,
_("Qty") if based_on_field == "amount" else _("Amount"),
)
)
for item in self.get("items"):
item.applicable_charges = flt(
flt(item.get(based_on_field))
* (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
@@ -548,8 +550,8 @@ def get_pr_items(purchase_receipt):
query = query.where(pr_item.is_finished_item == 1)
else:
query = query.select(
pr_item.base_rate,
pr_item.base_amount,
pr_item.base_net_rate.as_("base_rate"),
pr_item.base_net_amount.as_("base_amount"),
pr_item.is_fixed_asset,
)

View File

@@ -27,6 +27,115 @@ class TestLandedCostVoucher(ERPNextTestSuite):
def setUp(self):
self.load_test_records("Currency Exchange")
def test_landed_cost_uses_discounted_purchase_values(self):
for make_purchase in (make_purchase_receipt, make_purchase_invoice):
for apply_discount_on in ("Net Total", "Grand Total"):
with self.subTest(purchase=make_purchase.__name__, apply_discount_on=apply_discount_on):
lcv = frappe.new_doc("Landed Cost Voucher")
lcv.company = "_Test Company"
lcv.distribute_charges_based_on = "Amount"
for discount in (40, 0, 100):
purchase = make_purchase(qty=2, rate=100, update_stock=1, do_not_save=True)
purchase.apply_discount_on = apply_discount_on
purchase.additional_discount_percentage = discount
purchase.items[0].allow_zero_valuation_rate = 1
purchase.insert()
purchase.submit()
lcv.append(
"purchase_receipts",
{
"receipt_document_type": purchase.doctype,
"receipt_document": purchase.name,
},
)
lcv.get_items_from_purchase_receipts()
self.assertEqual([item.amount for item in lcv.items], [120, 200, 0])
self.assertEqual([item.rate for item in lcv.items], [60, 100, 0])
lcv.append("taxes", {"amount": 80})
lcv.total_taxes_and_charges = 80
lcv.set_applicable_charges_on_item()
self.assertEqual([item.applicable_charges for item in lcv.items], [30, 50, 0])
def test_landed_cost_rejects_offsetting_purchase_and_return_amounts(self):
for make_purchase in (make_purchase_receipt, make_purchase_invoice):
with self.subTest(purchase=make_purchase.__name__):
purchase = make_purchase(qty=2, rate=100, update_stock=1, do_not_save=True)
purchase.apply_discount_on = "Grand Total"
purchase.additional_discount_percentage = 50
purchase.insert()
purchase.submit()
original = make_purchase(qty=1, rate=100, update_stock=1)
purchase_return = make_purchase(
qty=-1, rate=100, update_stock=1, is_return=1, return_against=original.name
)
lcv = make_landed_cost_voucher(
receipt_document_type=purchase.doctype,
receipt_document=purchase.name,
charges=80,
do_not_save=True,
)
lcv.append(
"purchase_receipts",
{
"receipt_document_type": purchase_return.doctype,
"receipt_document": purchase_return.name,
},
)
lcv.get_items_from_purchase_receipts()
self.assertEqual([item.amount for item in lcv.items], [100, -100])
with self.assertRaisesRegex(frappe.ValidationError, "of all items is zero"):
lcv.insert()
lcv.distribute_charges_based_on = "Qty"
lcv.insert()
self.assertEqual([item.applicable_charges for item in lcv.items], [160, -80])
def test_landed_cost_rejects_fully_discounted_purchase(self):
for make_purchase in (make_purchase_receipt, make_purchase_invoice):
with self.subTest(purchase=make_purchase.__name__):
purchase = make_purchase(qty=2, rate=100, update_stock=1, do_not_save=True)
purchase.apply_discount_on = "Net Total"
purchase.additional_discount_percentage = 100
purchase.items[0].allow_zero_valuation_rate = 1
purchase.insert()
purchase.submit()
lcv = make_landed_cost_voucher(
receipt_document_type=purchase.doctype,
receipt_document=purchase.name,
charges=80,
do_not_save=True,
)
with self.assertRaisesRegex(frappe.ValidationError, "of all items is zero"):
lcv.insert()
lcv.distribute_charges_based_on = "Qty"
lcv.insert()
self.assertEqual([item.applicable_charges for item in lcv.items], [80])
def test_landed_cost_rejects_amounts_that_cancel_to_float_residue(self):
lcv = frappe.new_doc("Landed Cost Voucher")
lcv.company = "_Test Company"
lcv.distribute_charges_based_on = "Amount"
for amount in (100.10, 200.20, -300.30):
lcv.append("items", {"item_code": "_Test Item", "qty": 1, "amount": amount})
lcv.append("taxes", {"amount": 80})
lcv.total_taxes_and_charges = 80
self.assertNotEqual(sum(item.amount for item in lcv.items), 0)
with self.assertRaisesRegex(frappe.ValidationError, "of all items is zero"):
lcv.set_applicable_charges_on_item()
def test_get_vendor_invoices_runs(self):
# get_vendor_invoice_query filters unclaimed vendor invoices; the threshold moved from a HAVING
# (which referenced a SELECT alias with no GROUP BY -- invalid on Postgres) to a WHERE.
from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import get_vendor_invoices
pi = make_purchase_invoice(item_code="_Test Non Stock Item", qty=1, rate=100)
rows = get_vendor_invoices(
"Purchase Invoice", "", "name", 0, 20, {"company": "_Test Company", "name": pi.name}
)
self.assertTrue(any(r[0] == pi.name for r in rows))
def test_landed_cost_voucher(self):
frappe.db.set_single_value("Buying Settings", "allow_multiple_items", 1)