diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index bc28edbf396..be6206965e0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2464,6 +2464,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) +<<<<<<< HEAD def test_last_purchase_rate(self): item = create_item("_Test Item For Last Purchase Rate from PI", is_stock_item=1) pi1 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=100) @@ -2481,6 +2482,77 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): pi1.cancel() item.reload() self.assertEqual(item.last_purchase_rate, 0) +======= + def test_adjust_incoming_rate_from_pi_with_multi_currency_and_partial_billing(self): + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) + + pr = make_purchase_receipt( + qty=10, rate=10, currency="USD", do_not_save=1, supplier="_Test Supplier USD" + ) + pr.conversion_rate = 5300 + pr.save() + pr.submit() + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 53000) # Asserting to confirm if the default calculation is correct + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.qty = 1 + + pi.save() + pi.submit() + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "incoming_rate", + ) + # Test 1 : Incoming rate should not change as only the qty has changed and not the rate (this was not the case before) + self.assertEqual(incoming_rate, 53000) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.qty = 1 + row.rate = 9 + + pi.save() + pi.submit() + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "incoming_rate", + ) + # Test 2 : Rate in new PI is lower than PR, so incoming rate should also be lower + self.assertEqual(incoming_rate, 50350) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.qty = 1 + row.rate = 12 + + pi.save() + pi.submit() + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "incoming_rate", + ) + # Test 3 : Rate in new PI is higher than PR, so incoming rate should also be higher + self.assertEqual(incoming_rate, 54766.667) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0) + + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) +>>>>>>> 17d415b105 (fix: set landed cost based on purchase invoice rate) def test_opening_invoice_rounding_adjustment_validation(self): pi = make_purchase_invoice(do_not_save=1) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 9643110b76a..c2a36ac36d0 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -333,7 +333,7 @@ class BuyingController(SubcontractingController): net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount) - + flt(item.get("rate_difference_with_purchase_invoice")) + + flt(item.get("amount_difference_with_purchase_invoice")) ) / qty_in_stock_uom else: item.valuation_rate = 0.0 diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a20b2a67bff..34794556022 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -261,7 +261,11 @@ erpnext.patches.v14_0.show_loan_management_deprecation_warning erpnext.patches.v14_0.clear_reconciliation_values_from_singles execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True) erpnext.patches.v14_0.update_proprietorship_to_individual +<<<<<<< HEAD erpnext.patches.v15_0.rename_subcontracting_fields +======= +erpnext.stock.doctype.purchase_receipt_item.patches.rename_field_from_rate_difference_to_amount_difference +>>>>>>> 17d415b105 (fix: set landed cost based on purchase invoice rate) [post_model_sync] erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets @@ -393,8 +397,12 @@ erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect erpnext.patches.v15_0.sync_auto_reconcile_config execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment") erpnext.patches.v14_0.disable_add_row_in_gross_profit +<<<<<<< HEAD erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment erpnext.patches.v14_0.update_posting_datetime erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes erpnext.patches.v15_0.rename_sla_fields erpnext.patches.v15_0.update_query_report +======= +erpnext.stock.doctype.purchase_receipt_item.patches.recalculate_amount_difference_field +>>>>>>> 17d415b105 (fix: set landed cost based on purchase invoice rate) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 7aa23c8153b..7f2c04316e9 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -424,6 +424,14 @@ class PurchaseReceipt(BuyingController): self.delete_auto_created_batches() self.set_consumed_qty_in_subcontract_order() + def before_cancel(self): + super().before_cancel() + self.remove_amount_difference_with_purchase_invoice() + + def remove_amount_difference_with_purchase_invoice(self): + for item in self.items: + item.amount_difference_with_purchase_invoice = 0 + def get_gl_entries(self, warehouse_account=None, via_landed_cost_voucher=False): from erpnext.accounts.general_ledger import process_gl_map @@ -571,15 +579,15 @@ class PurchaseReceipt(BuyingController): item=item, ) - def make_rate_difference_entry(item): - if item.rate_difference_with_purchase_invoice and stock_asset_rbnb: + def make_amount_difference_entry(item): + if item.amount_difference_with_purchase_invoice and stock_asset_rbnb: account_currency = get_account_currency(stock_asset_rbnb) self.add_gl_entry( gl_entries=gl_entries, account=stock_asset_rbnb, cost_center=item.cost_center, debit=0.0, - credit=flt(item.rate_difference_with_purchase_invoice), + credit=flt(item.amount_difference_with_purchase_invoice), remarks=_("Adjustment based on Purchase Invoice rate"), against_account=stock_asset_account_name, account_currency=account_currency, @@ -612,7 +620,7 @@ class PurchaseReceipt(BuyingController): + flt(item.landed_cost_voucher_amount) + flt(item.rm_supp_cost) + flt(item.item_tax_amount) - + flt(item.rate_difference_with_purchase_invoice) + + flt(item.amount_difference_with_purchase_invoice) ) divisional_loss = flt( @@ -712,7 +720,7 @@ class PurchaseReceipt(BuyingController): make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name) outgoing_amount = make_stock_received_but_not_billed_entry(d) make_landed_cost_gl_entries(d) - make_rate_difference_entry(d) + make_amount_difference_entry(d) make_sub_contracting_gl_entries(d) make_divisional_loss_gl_entry(d, outgoing_amount) elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or ( @@ -1094,11 +1102,19 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate if adjust_incoming_rate: adjusted_amt = 0.0 - if item.billed_amt is not None and item.amount is not None: - adjusted_amt = flt(item.billed_amt) - flt(item.amount) + item_wise_billed_qty = get_billed_qty_against_purchase_receipt(pr_doc) - adjusted_amt = adjusted_amt * flt(pr_doc.conversion_rate) - item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False) + if ( + item.billed_amt is not None + and item.amount is not None + and item_wise_billed_qty.get(item.name) + ): + adjusted_amt = ( + flt(item.billed_amt / item_wise_billed_qty.get(item.name)) - flt(item.rate) + ) * item.qty + + adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) + item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) @@ -1111,6 +1127,21 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate adjust_incoming_rate_for_pr(pr_doc) +def get_billed_qty_against_purchase_receipt(pr_doc): + pr_names = [d.name for d in pr_doc.items] + table = frappe.qb.DocType("Purchase Invoice Item") + query = ( + frappe.qb.from_(table) + .select(table.pr_detail, fn.Sum(table.qty).as_("qty")) + .where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1)) + ) + invoice_data = query.run(as_list=1) + + if not invoice_data: + return frappe._dict() + return frappe._dict(invoice_data) + + def adjust_incoming_rate_for_pr(doc): doc.update_valuation_rate(reset_outgoing_rate=False) diff --git a/erpnext/stock/doctype/purchase_receipt_item/patches/__init__.py b/erpnext/stock/doctype/purchase_receipt_item/patches/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/purchase_receipt_item/patches/recalculate_amount_difference_field.py b/erpnext/stock/doctype/purchase_receipt_item/patches/recalculate_amount_difference_field.py new file mode 100644 index 00000000000..e2d8bedaa66 --- /dev/null +++ b/erpnext/stock/doctype/purchase_receipt_item/patches/recalculate_amount_difference_field.py @@ -0,0 +1,37 @@ +import frappe +from frappe.utils import flt + +from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + adjust_incoming_rate_for_pr, + get_billed_qty_against_purchase_receipt, +) + + +def execute(): + table = frappe.qb.DocType("Purchase Receipt Item") + query = ( + frappe.qb.from_(table) + .select(table.parent) + .distinct() + .where((table.amount_difference_with_purchase_invoice > 0) & (table.docstatus == 1)) + ) + pr_names = [item.parent for item in query.run(as_dict=True)] + + for pr_name in pr_names: + pr_doc = frappe.get_doc("Purchase Receipt", pr_name) + for item in pr_doc.items: + adjusted_amt = 0.0 + item_wise_billed_qty = get_billed_qty_against_purchase_receipt(pr_doc) + + if ( + item.billed_amt is not None + and item.amount is not None + and item_wise_billed_qty.get(item.name) + ): + adjusted_amt = ( + flt(item.billed_amt / item_wise_billed_qty.get(item.name)) - flt(item.rate) + ) * item.qty + + adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) + item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) + adjust_incoming_rate_for_pr(pr_doc) diff --git a/erpnext/stock/doctype/purchase_receipt_item/patches/rename_field_from_rate_difference_to_amount_difference.py b/erpnext/stock/doctype/purchase_receipt_item/patches/rename_field_from_rate_difference_to_amount_difference.py new file mode 100644 index 00000000000..44c8c49cba8 --- /dev/null +++ b/erpnext/stock/doctype/purchase_receipt_item/patches/rename_field_from_rate_difference_to_amount_difference.py @@ -0,0 +1,17 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + frappe.db.set_value( + "DocField", + {"parent": "Purchase Receipt Item", "fieldname": "rate_difference_with_purchase_invoice"}, + "label", + "Amount Difference with Purchase Invoice", + ) + rename_field( + "Purchase Receipt Item", + "rate_difference_with_purchase_invoice", + "amount_difference_with_purchase_invoice", + ) + frappe.clear_cache(doctype="Purchase Receipt Item") diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 610bceddf0f..2f6598c3fb2 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -71,7 +71,7 @@ "item_tax_amount", "rm_supp_cost", "landed_cost_voucher_amount", - "rate_difference_with_purchase_invoice", + "amount_difference_with_purchase_invoice", "billed_amt", "warehouse_and_reference", "warehouse", @@ -998,14 +998,6 @@ "label": "Has Item Scanned", "read_only": 1 }, - { - "fieldname": "rate_difference_with_purchase_invoice", - "fieldtype": "Currency", - "label": "Rate Difference with Purchase Invoice", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, { "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", @@ -1135,12 +1127,29 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1 +<<<<<<< HEAD +======= + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" + }, + { + "fieldname": "amount_difference_with_purchase_invoice", + "fieldtype": "Currency", + "label": "Amount Difference with Purchase Invoice", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 +>>>>>>> 17d415b105 (fix: set landed cost based on purchase invoice rate) } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-07-19 12:14:21.521466", + "modified": "2025-02-17 13:15:36.692202", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py index 2154007771d..0db866f52c1 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py @@ -16,6 +16,7 @@ class PurchaseReceiptItem(Document): allow_zero_valuation_rate: DF.Check amount: DF.Currency + amount_difference_with_purchase_invoice: DF.Currency apply_tds: DF.Check asset_category: DF.Link | None asset_location: DF.Link | None @@ -76,7 +77,6 @@ class PurchaseReceiptItem(Document): qty: DF.Float quality_inspection: DF.Link | None rate: DF.Currency - rate_difference_with_purchase_invoice: DF.Currency rate_with_margin: DF.Currency received_qty: DF.Float received_stock_qty: DF.Float