mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-15 07:28:39 +00:00
Merge pull request #58011 from mihir-kandoi/backport-58003-version-15-hotfix
fix(selling): bill re-delivered sales order quantities (backport #58003)
This commit is contained in:
@@ -1114,8 +1114,20 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
|
||||
return target_doc
|
||||
|
||||
|
||||
def get_qty_net_of_returns(so_item) -> float:
|
||||
"""Return the ordered quantity billable after returns and re-deliveries."""
|
||||
qty = flt(so_item.qty)
|
||||
|
||||
return min(qty, max(qty - flt(so_item.returned_qty), flt(so_item.delivered_qty)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
|
||||
def make_sales_invoice(
|
||||
source_name: str,
|
||||
target_doc: str | dict | Document | None = None,
|
||||
ignore_permissions: bool = False,
|
||||
args: str | dict | None = None,
|
||||
):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
@@ -1123,10 +1135,40 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
|
||||
|
||||
# 0 qty is accepted, as the qty is uncertain for some items
|
||||
has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items")
|
||||
billed_qty_by_item = None
|
||||
pending_qty_by_item = {}
|
||||
|
||||
def is_unit_price_row(source):
|
||||
return has_unit_price_items and source.qty == 0
|
||||
|
||||
def get_billed_qty_by_item():
|
||||
nonlocal billed_qty_by_item
|
||||
|
||||
if billed_qty_by_item is None:
|
||||
invoice_item = frappe.qb.DocType("Sales Invoice Item")
|
||||
sales_order_item = frappe.qb.DocType("Sales Order Item")
|
||||
rows = (
|
||||
frappe.qb.from_(invoice_item)
|
||||
.inner_join(sales_order_item)
|
||||
.on(invoice_item.so_detail == sales_order_item.name)
|
||||
.select(invoice_item.so_detail, Sum(invoice_item.qty).as_("qty"))
|
||||
.where((invoice_item.docstatus == 1) & (sales_order_item.parent == source_name))
|
||||
.groupby(invoice_item.so_detail)
|
||||
).run(as_dict=True)
|
||||
billed_qty_by_item = {row.so_detail: flt(row.qty) for row in rows}
|
||||
|
||||
return billed_qty_by_item
|
||||
|
||||
def get_pending_qty(source):
|
||||
if source.name not in pending_qty_by_item:
|
||||
billable_qty = get_qty_net_of_returns(source)
|
||||
if source.qty and source.billed_amt:
|
||||
billable_qty -= get_billed_qty_by_item().get(source.name, 0)
|
||||
|
||||
pending_qty_by_item[source.name] = max(flt(billable_qty, source.precision("qty")), 0)
|
||||
|
||||
return pending_qty_by_item[source.name]
|
||||
|
||||
def postprocess(source, target):
|
||||
set_missing_values(source, target)
|
||||
# Get the advance paid Journal Entries in Sales Invoice Advance
|
||||
@@ -1156,17 +1198,6 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
|
||||
target.debit_to = get_party_account("Customer", source.customer, source.company)
|
||||
|
||||
def update_item(source, target, source_parent):
|
||||
def get_billed_qty(so_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Sales Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.so_detail == so_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
if source_parent.has_unit_price_items:
|
||||
# 0 Amount rows (as seen in Unit Price Items) should be mapped as it is
|
||||
pending_amount = flt(source.amount) - flt(source.billed_amt)
|
||||
@@ -1175,11 +1206,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
|
||||
target.amount = flt(source.amount) - flt(source.billed_amt)
|
||||
|
||||
target.base_amount = target.amount * flt(source_parent.conversion_rate)
|
||||
target.qty = (
|
||||
source.qty - get_billed_qty(source.name)
|
||||
if (source.qty and source.billed_amt)
|
||||
else (source.qty if is_unit_price_row(source) else source.qty - source.returned_qty)
|
||||
)
|
||||
target.qty = source.qty if is_unit_price_row(source) else get_pending_qty(source)
|
||||
|
||||
if source_parent.project:
|
||||
target.cost_center = frappe.db.get_value("Project", source_parent.project, "cost_center")
|
||||
@@ -1215,12 +1242,16 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
|
||||
"parent": "sales_order",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (
|
||||
"condition": lambda doc: select_item(doc)
|
||||
and (
|
||||
True
|
||||
if is_unit_price_row(doc)
|
||||
else (doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)))
|
||||
)
|
||||
and select_item(doc),
|
||||
else (
|
||||
doc.qty
|
||||
and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))
|
||||
and get_pending_qty(doc) > 0
|
||||
)
|
||||
),
|
||||
},
|
||||
"Sales Taxes and Charges": {
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
|
||||
@@ -226,6 +226,95 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
si1 = make_sales_invoice(so.name)
|
||||
self.assertEqual(len(si1.get("items")), 0)
|
||||
|
||||
def test_make_sales_invoice_after_return_and_redelivery(self):
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||
|
||||
so = make_sales_order(qty=10, rate=100)
|
||||
dn = create_dn_against_so(so.name, 10)
|
||||
|
||||
dn_return = frappe.get_doc(make_sales_return(dn.name).as_dict())
|
||||
dn_return.insert()
|
||||
dn_return.submit()
|
||||
|
||||
self.assertEqual(len(make_sales_invoice(so.name).get("items")), 0)
|
||||
|
||||
create_dn_against_so(so.name, 10)
|
||||
|
||||
so.load_from_db()
|
||||
item = so.get("items")[0]
|
||||
self.assertEqual(item.delivered_qty, 10)
|
||||
self.assertEqual(item.returned_qty, 10)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
self.assertEqual(si.get("items")[0].qty, 10)
|
||||
|
||||
def test_make_sales_invoice_bills_ordered_qty_for_partial_delivery(self):
|
||||
so = make_sales_order(qty=10, rate=100)
|
||||
create_dn_against_so(so.name, 4)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
self.assertEqual(si.get("items")[0].qty, 10)
|
||||
|
||||
def test_make_sales_invoice_after_partial_billing_return_and_redelivery(self):
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||
|
||||
so = make_sales_order(qty=10, rate=100)
|
||||
dn = create_dn_against_so(so.name, 10)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
si.get("items")[0].qty = 4
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
dn_return = frappe.get_doc(make_sales_return(dn.name).as_dict())
|
||||
dn_return.insert()
|
||||
dn_return.submit()
|
||||
create_dn_against_so(so.name, 5)
|
||||
|
||||
so.load_from_db()
|
||||
item = so.get("items")[0]
|
||||
self.assertEqual(item.delivered_qty, 5)
|
||||
self.assertEqual(item.returned_qty, 10)
|
||||
self.assertEqual(item.billed_amt, 400)
|
||||
|
||||
pending_invoice = make_sales_invoice(so.name)
|
||||
self.assertEqual(pending_invoice.get("items")[0].qty, 1)
|
||||
pending_invoice.insert()
|
||||
pending_invoice.submit()
|
||||
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.get("items")[0].billed_amt, 500)
|
||||
|
||||
def test_make_sales_invoice_after_partial_billing_multiple_items(self):
|
||||
so = make_sales_order(
|
||||
item_list=[
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
},
|
||||
{
|
||||
"item_code": "_Test FG Item",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
si.get("items")[0].qty = 4
|
||||
si.get("items")[1].qty = 6
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
pending_invoice = make_sales_invoice(so.name)
|
||||
self.assertEqual(
|
||||
{item.so_detail: item.qty for item in pending_invoice.get("items")},
|
||||
{so.get("items")[0].name: 6, so.get("items")[1].name: 4},
|
||||
)
|
||||
|
||||
def test_so_billed_amount_against_return_entry(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user