mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-15 09:53:09 +00:00
fix(selling): exclude fully billed orders from the invoice picker (#58966)
This commit is contained in:
@@ -10,7 +10,7 @@ import frappe.utils
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.query_builder.functions import Abs, IfNull, Round, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate
|
||||
from pypika import Order
|
||||
|
||||
@@ -952,8 +952,26 @@ def get_stock_reservation_status():
|
||||
return frappe.get_single_value("Stock Settings", "enable_stock_reservation")
|
||||
|
||||
|
||||
def get_pending_qty_criterion(sales_order_item):
|
||||
"""Mirror the mapper's pending quantity check."""
|
||||
invoice_item = qb.DocType("Sales Invoice Item")
|
||||
billed_qty = (
|
||||
qb.from_(invoice_item)
|
||||
.select(IfNull(Sum(invoice_item.qty), 0))
|
||||
.where((invoice_item.docstatus == 1) & (invoice_item.so_detail == sales_order_item.name))
|
||||
)
|
||||
|
||||
qty_precision = frappe.get_precision("Sales Order Item", "qty")
|
||||
has_unbilled_ordered_qty = Round(sales_order_item.qty - billed_qty, qty_precision) > 0
|
||||
has_unbilled_delivered_qty = (
|
||||
Round(sales_order_item.qty - sales_order_item.returned_qty - billed_qty, qty_precision) > 0
|
||||
) | (Round(sales_order_item.delivered_qty - billed_qty, qty_precision) > 0)
|
||||
|
||||
return has_unbilled_ordered_qty & has_unbilled_delivered_qty
|
||||
|
||||
|
||||
def get_potentially_billable_item_criterion(sales_order, sales_order_item, item):
|
||||
"""Return the amount check for UI candidates. The mapper checks pending quantity."""
|
||||
"""Return the row level checks the Sales Invoice mapper applies."""
|
||||
global_allowance = flt(frappe.get_cached_value("Accounts Settings", None, "over_billing_allowance"))
|
||||
allowance = (
|
||||
Case().when(item.over_billing_allowance != 0, item.over_billing_allowance).else_(global_allowance)
|
||||
@@ -963,11 +981,12 @@ def get_potentially_billable_item_criterion(sales_order, sales_order_item, item)
|
||||
Abs(sales_order_item.billed_amt) < Abs(sales_order_item.amount) * (1 + allowance / 100)
|
||||
)
|
||||
is_unit_price_row = (sales_order.has_unit_price_items == 1) & (sales_order_item.qty == 0)
|
||||
|
||||
return (sales_order_item.closed == 0) & (
|
||||
is_unit_price_row | ((sales_order_item.qty != 0) & has_amount_headroom)
|
||||
is_billable_row = (
|
||||
(sales_order_item.qty != 0) & has_amount_headroom & get_pending_qty_criterion(sales_order_item)
|
||||
)
|
||||
|
||||
return (sales_order_item.closed == 0) & (is_unit_price_row | is_billable_row)
|
||||
|
||||
|
||||
def has_potentially_billable_items(sales_order: str) -> bool:
|
||||
"""Return whether a Sales Order has an item with billing amount headroom."""
|
||||
|
||||
@@ -440,6 +440,48 @@ class TestSalesOrder(ERPNextTestSuite):
|
||||
|
||||
self.assertEqual(len(make_sales_invoice(so.name).items), 0)
|
||||
|
||||
def test_fully_billed_order_is_not_offered_within_billing_allowance(self):
|
||||
item = make_item(
|
||||
"_Test Fully Billed Allowance Item",
|
||||
{"is_stock_item": 1, "over_billing_allowance": 0},
|
||||
).name
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
so.load_from_db()
|
||||
self.assertEqual(flt(so.per_billed), 100)
|
||||
|
||||
filters = {"docstatus": 1, "company": so.company, "customer": so.customer}
|
||||
|
||||
with change_settings("Accounts Settings", {"over_billing_allowance": 100}):
|
||||
self.assertFalse(has_potentially_billable_items(so.name))
|
||||
|
||||
rows = get_potentially_billable_sales_orders("Sales Order", "", "name", 0, 50, filters)
|
||||
self.assertNotIn(so.name, [row.name for row in rows])
|
||||
|
||||
self.assertEqual(len(make_sales_invoice(so.name).get("items")), 0)
|
||||
|
||||
def test_order_with_sub_precision_pending_qty_is_not_offered(self):
|
||||
item = make_item("_Test Sub Precision Qty Item", {"is_stock_item": 1}).name
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
si.get("items")[0].rate = 90
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
qty_precision = frappe.get_precision("Sales Order Item", "qty")
|
||||
billed_qty = 10 - 10 ** -(qty_precision + 1)
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Item", si.get("items")[0].name, "qty", billed_qty, update_modified=False
|
||||
)
|
||||
|
||||
self.assertFalse(has_potentially_billable_items(so.name))
|
||||
self.assertEqual(len(make_sales_invoice(so.name).get("items")), 0)
|
||||
|
||||
def test_make_sales_invoice_after_return_and_redelivery(self):
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user