fix(stock): repair duplicated purchase receipt billing

This commit is contained in:
Mihir Kandoi
2026-08-11 16:15:31 +05:30
parent 5e33a3c0bf
commit 8b7e04eae1
3 changed files with 133 additions and 0 deletions

View File

@@ -510,3 +510,4 @@ erpnext.patches.v16_0.merge_seeded_item_group_root
erpnext.patches.v16_0.set_stock_uom_in_job_card
erpnext.patches.v16_0.set_work_order_requested_and_picked_qty
erpnext.patches.v16_0.rename_italy_customer_name_fields
erpnext.patches.v16_0.recalculate_purchase_receipt_billing_status

View File

@@ -0,0 +1,90 @@
from collections import defaultdict
import frappe
from frappe.query_builder.functions import Count
from frappe.utils import flt
from erpnext.stock.doctype.purchase_receipt.services.billing_status import (
get_billed_amount_against_po,
get_billed_amount_against_pr,
get_purchase_receipts_against_po_details,
update_billed_amount_based_on_po,
update_billing_percentage,
)
def execute():
purchase_order_items = get_affected_purchase_order_items()
if not purchase_order_items:
return
updated_purchase_receipts = update_billed_amount_based_on_po(purchase_order_items)
for purchase_receipt in set(updated_purchase_receipts):
update_billing_percentage(frappe.get_doc("Purchase Receipt", purchase_receipt))
def get_affected_purchase_order_items() -> list[str]:
purchase_order_items = get_candidate_purchase_order_items()
if not purchase_order_items:
return []
purchase_receipt_items = get_purchase_receipts_against_po_details(purchase_order_items)
direct_billed_amounts = get_billed_amount_against_pr([item.name for item in purchase_receipt_items])
po_billed_amounts = get_billed_amount_against_po(purchase_order_items)
current_billed_amounts = defaultdict(float)
available_billed_amounts = defaultdict(float)
for purchase_order_item, billed_details in po_billed_amounts.items():
available_billed_amounts[purchase_order_item] = flt(billed_details["billed_amt"])
for item in purchase_receipt_items:
current_billed_amounts[item.purchase_order_item] += flt(item.billed_amt)
available_billed_amounts[item.purchase_order_item] += flt(direct_billed_amounts.get(item.name))
precision = frappe.get_precision("Purchase Receipt Item", "billed_amt") or 2
return [
purchase_order_item
for purchase_order_item in purchase_order_items
if flt(po_billed_amounts.get(purchase_order_item, {}).get("billed_amt")) > 0
and flt(po_billed_amounts.get(purchase_order_item, {}).get("billed_qty")) > 0
and flt(
current_billed_amounts[purchase_order_item] - available_billed_amounts[purchase_order_item],
precision,
)
> 0
]
def get_candidate_purchase_order_items() -> list[str]:
purchase_receipt = frappe.qb.DocType("Purchase Receipt")
purchase_receipt_item = frappe.qb.DocType("Purchase Receipt Item")
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
purchase_order_items_with_multiple_receipts = (
frappe.qb.from_(purchase_receipt_item)
.inner_join(purchase_receipt)
.on(purchase_receipt_item.parent == purchase_receipt.name)
.select(purchase_receipt_item.purchase_order_item)
.where(
(purchase_receipt.docstatus == 1)
& (purchase_receipt.is_return == 0)
& purchase_receipt_item.purchase_order_item.isnotnull()
)
.groupby(purchase_receipt_item.purchase_order_item)
.having(Count(purchase_receipt_item.name) > 1)
)
return (
frappe.qb.from_(purchase_invoice_item)
.inner_join(purchase_invoice)
.on(purchase_invoice_item.parent == purchase_invoice.name)
.select(purchase_invoice_item.po_detail)
.distinct()
.where(
(purchase_invoice.docstatus == 1)
& (purchase_invoice.update_stock == 0)
& purchase_invoice_item.pr_detail.isnull()
& purchase_invoice_item.po_detail.isin(purchase_order_items_with_multiple_receipts)
)
).run(pluck=True)

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from unittest.mock import patch
import frappe
from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowtime, today
from pypika import functions as fn
@@ -827,6 +829,46 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(flt(pr2.per_billed, 2), 66.67)
self.assertEqual(pr2.status, "Partly Billed")
from erpnext.patches.v16_0 import recalculate_purchase_receipt_billing_status
purchase_order_item = po.items[0].name
with patch.object(
recalculate_purchase_receipt_billing_status,
"get_candidate_purchase_order_items",
return_value=[purchase_order_item],
):
self.assertEqual(
recalculate_purchase_receipt_billing_status.get_affected_purchase_order_items(), []
)
frappe.db.set_value(
"Purchase Receipt Item",
pr2.items[0].name,
"billed_amt",
1500,
update_modified=False,
)
frappe.db.set_value(
"Purchase Receipt",
pr2.name,
{"per_billed": 100, "status": "Completed"},
update_modified=False,
)
self.assertEqual(
recalculate_purchase_receipt_billing_status.get_affected_purchase_order_items(),
[purchase_order_item],
)
recalculate_purchase_receipt_billing_status.execute()
self.assertEqual(
recalculate_purchase_receipt_billing_status.get_affected_purchase_order_items(), []
)
pr2.load_from_db()
self.assertEqual(pr2.get("items")[0].billed_amt, 1000)
self.assertEqual(flt(pr2.per_billed, 2), 66.67)
self.assertEqual(pr2.status, "Partly Billed")
pr2.cancel()
pr1.reload()
pr1.cancel()