mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-22 18:59:58 +00:00
Merge pull request #56192 from mihir-kandoi/pg-purchase-receipt
refactor(stock): port Purchase Receipt + LCV raw SQL to qb/ORM + #39 GROUP-BY fixes (Postgres)
This commit is contained in:
@@ -9,7 +9,7 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
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
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -395,12 +395,12 @@ class LandedCostVoucher(Document):
|
|||||||
if not item.is_fixed_asset and item.serial_no:
|
if not item.is_fixed_asset and item.serial_no:
|
||||||
serial_nos = get_serial_nos(item.serial_no)
|
serial_nos = get_serial_nos(item.serial_no)
|
||||||
if serial_nos:
|
if serial_nos:
|
||||||
frappe.db.sql(
|
serial_no = frappe.qb.DocType("Serial No")
|
||||||
"update `tabSerial No` set purchase_rate=%s where name in ({})".format(
|
(
|
||||||
", ".join(["%s"] * len(serial_nos))
|
frappe.qb.update(serial_no)
|
||||||
),
|
.set(serial_no.purchase_rate, item.valuation_rate)
|
||||||
tuple([item.valuation_rate, *serial_nos]),
|
.where(serial_no.name.isin(serial_nos))
|
||||||
)
|
).run()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_vendor_invoice_amount(self, vendor_invoice: str):
|
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")
|
lcv_item = frappe.qb.DocType("Landed Cost Item")
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(lcv_item)
|
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))
|
.where((lcv_item.docstatus == 1) & (lcv_item.receipt_document == doc.name))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -21,18 +21,15 @@ from erpnext.stock.serial_batch_bundle import (
|
|||||||
|
|
||||||
def get_invoiced_qty_map(purchase_receipt: str) -> dict:
|
def get_invoiced_qty_map(purchase_receipt: str) -> dict:
|
||||||
"""returns a map: {pr_detail: invoiced_qty}"""
|
"""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(
|
return frappe._dict(query) if query else frappe._dict()
|
||||||
"""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
|
|
||||||
|
|
||||||
|
|
||||||
def get_returned_qty_map(purchase_receipt: str) -> dict:
|
def get_returned_qty_map(purchase_receipt: str) -> dict:
|
||||||
|
|||||||
@@ -339,14 +339,17 @@ class PurchaseReceipt(BuyingController):
|
|||||||
frappe.throw(_(msg))
|
frappe.throw(_(msg))
|
||||||
|
|
||||||
def get_already_received_qty(self, po, po_detail):
|
def get_already_received_qty(self, po, po_detail):
|
||||||
qty = frappe.db.sql(
|
qty = frappe.get_all(
|
||||||
"""select sum(qty) from `tabPurchase Receipt Item`
|
"Purchase Receipt Item",
|
||||||
where purchase_order_item = %s and docstatus = 1
|
filters={
|
||||||
and purchase_order=%s
|
"purchase_order_item": po_detail,
|
||||||
and parent != %s""",
|
"docstatus": 1,
|
||||||
(po_detail, po, self.name),
|
"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):
|
def get_po_qty_and_warehouse(self, po_detail):
|
||||||
po_qty, po_warehouse = frappe.db.get_value("Purchase Order Item", po_detail, ["qty", "warehouse"])
|
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):
|
def check_next_docstatus(self):
|
||||||
submit_rv = frappe.db.sql(
|
submit_rv = frappe.get_all(
|
||||||
"""select t1.name
|
"Purchase Invoice Item",
|
||||||
from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2
|
filters={"purchase_receipt": self.name, "docstatus": 1},
|
||||||
where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""",
|
fields=["parent"],
|
||||||
(self.name),
|
as_list=True,
|
||||||
|
limit=1,
|
||||||
)
|
)
|
||||||
if submit_rv:
|
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):
|
def on_cancel(self):
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
|
|
||||||
self.check_for_on_hold_or_closed_status("Purchase Order", "purchase_order")
|
self.check_for_on_hold_or_closed_status("Purchase Order", "purchase_order")
|
||||||
# Check if Purchase Invoice has been submitted against current Purchase Order
|
# Check if Purchase Invoice has been submitted against current Purchase Order
|
||||||
submitted = frappe.db.sql(
|
submitted = frappe.get_all(
|
||||||
"""select t1.name
|
"Purchase Invoice Item",
|
||||||
from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2
|
filters={"purchase_receipt": self.name, "docstatus": 1},
|
||||||
where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""",
|
fields=["parent"],
|
||||||
self.name,
|
as_list=True,
|
||||||
|
limit=1,
|
||||||
)
|
)
|
||||||
if submitted:
|
if submitted:
|
||||||
frappe.throw(_("Purchase Invoice {0} is already submitted").format(submitted[0][0]))
|
frappe.throw(_("Purchase Invoice {0} is already submitted").format(submitted[0][0]))
|
||||||
|
|||||||
@@ -6011,6 +6011,35 @@ class TestPurchaseReceipt(ERPNextTestSuite):
|
|||||||
srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account)
|
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)
|
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():
|
def create_asset_category_for_pr_test():
|
||||||
category_name = "Test Asset Category for PR"
|
category_name = "Test Asset Category for PR"
|
||||||
|
|||||||
@@ -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(
|
items_to_be_repost = frappe.db.get_all(
|
||||||
"Stock Ledger Entry",
|
"Stock Ledger Entry",
|
||||||
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
|
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",
|
order_by="creation asc",
|
||||||
group_by="item_code, warehouse",
|
group_by="item_code, warehouse",
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user