mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-12 22:21:50 +00:00
fix(stock): repair duplicated purchase receipt billing
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user