diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 6576380e862..31b3e057570 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -9,7 +9,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.meta import get_field_precision from frappe.query_builder.custom import ConstantColumn -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Max, Sum from frappe.utils import cint, flt import erpnext @@ -395,12 +395,12 @@ class LandedCostVoucher(Document): if not item.is_fixed_asset and item.serial_no: serial_nos = get_serial_nos(item.serial_no) if serial_nos: - frappe.db.sql( - "update `tabSerial No` set purchase_rate=%s where name in ({})".format( - ", ".join(["%s"] * len(serial_nos)) - ), - tuple([item.valuation_rate, *serial_nos]), - ) + serial_no = frappe.qb.DocType("Serial No") + ( + frappe.qb.update(serial_no) + .set(serial_no.purchase_rate, item.valuation_rate) + .where(serial_no.name.isin(serial_nos)) + ).run() @frappe.whitelist() def get_vendor_invoice_amount(self, vendor_invoice: str): @@ -532,7 +532,7 @@ def set_landed_cost_voucher_amount(doc): lcv_item = frappe.qb.DocType("Landed Cost Item") query = ( frappe.qb.from_(lcv_item) - .select(Sum(lcv_item.applicable_charges), lcv_item.cost_center) + .select(Sum(lcv_item.applicable_charges), Max(lcv_item.cost_center)) .where((lcv_item.docstatus == 1) & (lcv_item.receipt_document == doc.name)) ) diff --git a/erpnext/stock/doctype/purchase_receipt/mapper.py b/erpnext/stock/doctype/purchase_receipt/mapper.py index 9ea7371554a..273afcec7a9 100644 --- a/erpnext/stock/doctype/purchase_receipt/mapper.py +++ b/erpnext/stock/doctype/purchase_receipt/mapper.py @@ -21,18 +21,15 @@ from erpnext.stock.serial_batch_bundle import ( def get_invoiced_qty_map(purchase_receipt: str) -> dict: """returns a map: {pr_detail: invoiced_qty}""" - invoiced_qty_map = {} + pi_item = frappe.qb.DocType("Purchase Invoice Item") + query = ( + frappe.qb.from_(pi_item) + .select(pi_item.pr_detail, Sum(pi_item.qty).as_("qty")) + .where((pi_item.purchase_receipt == purchase_receipt) & (pi_item.docstatus == 1)) + .groupby(pi_item.pr_detail) + ).run(as_list=1) - for pr_detail, qty in frappe.db.sql( - """select pr_detail, qty from `tabPurchase Invoice Item` - where purchase_receipt=%s and docstatus=1""", - purchase_receipt, - ): - if not invoiced_qty_map.get(pr_detail): - invoiced_qty_map[pr_detail] = 0 - invoiced_qty_map[pr_detail] += qty - - return invoiced_qty_map + return frappe._dict(query) if query else frappe._dict() def get_returned_qty_map(purchase_receipt: str) -> dict: diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 0905b5b3b06..a99c536dd5b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -339,14 +339,17 @@ class PurchaseReceipt(BuyingController): frappe.throw(_(msg)) def get_already_received_qty(self, po, po_detail): - qty = frappe.db.sql( - """select sum(qty) from `tabPurchase Receipt Item` - where purchase_order_item = %s and docstatus = 1 - and purchase_order=%s - and parent != %s""", - (po_detail, po, self.name), + qty = frappe.get_all( + "Purchase Receipt Item", + filters={ + "purchase_order_item": po_detail, + "docstatus": 1, + "purchase_order": po, + "parent": ["!=", self.name], + }, + fields=[{"SUM": "qty", "as": "qty"}], ) - return qty and flt(qty[0][0]) or 0.0 + return flt(qty[0].qty) if qty and qty[0].qty else 0.0 def get_po_qty_and_warehouse(self, po_detail): po_qty, po_warehouse = frappe.db.get_value("Purchase Order Item", po_detail, ["qty", "warehouse"]) @@ -415,25 +418,27 @@ class PurchaseReceipt(BuyingController): ) def check_next_docstatus(self): - submit_rv = frappe.db.sql( - """select t1.name - from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2 - where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""", - (self.name), + submit_rv = frappe.get_all( + "Purchase Invoice Item", + filters={"purchase_receipt": self.name, "docstatus": 1}, + fields=["parent"], + as_list=True, + limit=1, ) if submit_rv: - frappe.throw(_("Purchase Invoice {0} is already submitted").format(self.submit_rv[0][0])) + frappe.throw(_("Purchase Invoice {0} is already submitted").format(submit_rv[0][0])) def on_cancel(self): super().on_cancel() self.check_for_on_hold_or_closed_status("Purchase Order", "purchase_order") # Check if Purchase Invoice has been submitted against current Purchase Order - submitted = frappe.db.sql( - """select t1.name - from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2 - where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""", - self.name, + submitted = frappe.get_all( + "Purchase Invoice Item", + filters={"purchase_receipt": self.name, "docstatus": 1}, + fields=["parent"], + as_list=True, + limit=1, ) if submitted: frappe.throw(_("Purchase Invoice {0} is already submitted").format(submitted[0][0])) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 0393e7ae037..68c360f9396 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -6011,6 +6011,35 @@ class TestPurchaseReceipt(ERPNextTestSuite): srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account) self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2) + def test_get_already_received_qty(self): + """get_already_received_qty sums prior submitted PR Item qty against the same PO line, + excluding the current PR — covers the converted SUM with `parent != self.name`.""" + from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_purchase_order, + make_pr_against_po, + ) + + po = create_purchase_order(qty=10) + po_detail = po.items[0].name + + make_pr_against_po(po.name, 4) # PR1 receives 4 + pr2 = make_pr_against_po(po.name, 2) # PR2 receives 2 + + # already received against this PO line, excluding pr2 itself, is pr1's 4 + self.assertEqual(pr2.get_already_received_qty(po.name, po_detail), 4.0) + + def test_check_next_docstatus_blocks_with_submitted_invoice(self): + """check_next_docstatus must flag a submitted Purchase Invoice drawn from the receipt — + covers the converted child-table get_all (Purchase Invoice Item, docstatus=1).""" + pr = make_purchase_receipt() + pi = make_purchase_invoice(pr.name) + pi.insert() + pi.submit() + + with self.assertRaises(frappe.ValidationError) as cm: + pr.check_next_docstatus() + self.assertIn("is already submitted", str(cm.exception)) + def create_asset_category_for_pr_test(): category_name = "Test Asset Category for PR" diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 91c9a36ee77..99dd4ae675f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -381,7 +381,16 @@ def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposti items_to_be_repost = frappe.db.get_all( "Stock Ledger Entry", filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, - fields=["item_code", "warehouse", "posting_date", "posting_time", "creation", "posting_datetime"], + fields=[ + "item_code", + "warehouse", + # aggregate the non-grouped columns (earliest row per item+warehouse) so the GROUP BY + # is valid on Postgres; a single voucher's entries share posting_date/time per group + {"MIN": "posting_date", "as": "posting_date"}, + {"MIN": "posting_time", "as": "posting_time"}, + {"MIN": "creation", "as": "creation"}, + {"MIN": "posting_datetime", "as": "posting_datetime"}, + ], order_by="creation asc", group_by="item_code, warehouse", )