From 941a965af4a62881bfe78758cc995377201afb54 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 4 Nov 2014 15:33:12 +0530 Subject: [PATCH] Ordered qty calculation logic --- .../purchase_common/purchase_common.py | 25 +----- .../doctype/purchase_order/purchase_order.py | 87 +++++++++---------- .../purchase_order/test_purchase_order.py | 26 ++++-- .../purchase_receipt/purchase_receipt.py | 35 +++----- 4 files changed, 75 insertions(+), 98 deletions(-) diff --git a/erpnext/buying/doctype/purchase_common/purchase_common.py b/erpnext/buying/doctype/purchase_common/purchase_common.py index 68a6e93e68..2cf8673dc5 100644 --- a/erpnext/buying/doctype/purchase_common/purchase_common.py +++ b/erpnext/buying/doctype/purchase_common/purchase_common.py @@ -3,15 +3,13 @@ from __future__ import unicode_literals import frappe - -from frappe.utils import cstr, flt +from frappe.utils import flt from frappe import _ from erpnext.stock.doctype.item.item import get_last_purchase_details from erpnext.controllers.buying_controller import BuyingController class PurchaseCommon(BuyingController): - def update_last_purchase_rate(self, obj, is_submit): """updates last_purchase_rate in item table for each item""" @@ -123,27 +121,6 @@ class PurchaseCommon(BuyingController): else: chk_dupl_itm.append(f) - def get_qty(self, curr_doctype, ref_tab_fname, ref_tab_dn, ref_doc_tname, transaction, curr_parent_name): - # Get total Quantities of current doctype (eg. PR) except for qty of this transaction - #------------------------------ - # please check as UOM changes from Material Request - Purchase Order ,so doing following else uom should be same . - # i.e. in PO uom is NOS then in PR uom should be NOS - # but if in Material Request uom KG it can change in PO - - get_qty = (transaction == 'Material Request - Purchase Order') and 'qty * conversion_factor' or 'qty' - qty = frappe.db.sql("""select sum(%s) from `tab%s` where %s = %s and - docstatus = 1 and parent != %s""" % (get_qty, curr_doctype, ref_tab_fname, '%s', '%s'), - (ref_tab_dn, curr_parent_name)) - qty = qty and flt(qty[0][0]) or 0 - - # get total qty of ref doctype - #-------------------- - max_qty = frappe.db.sql("""select qty from `tab%s` where name = %s - and docstatus = 1""" % (ref_doc_tname, '%s'), ref_tab_dn) - max_qty = max_qty and flt(max_qty[0][0]) or 0 - - return cstr(qty)+'~~~'+cstr(max_qty) - def check_for_stopped_status(self, doctype, docname): stopped = frappe.db.sql("""select name from `tab%s` where name = %s and status = 'Stopped'""" % (doctype, '%s'), docname) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 0bfd3e558b..9b473c19bd 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -96,50 +96,45 @@ class PurchaseOrder(BuyingController): check_list.append(d.prevdoc_docname) pc_obj.check_for_stopped_status( d.prevdoc_doctype, d.prevdoc_docname) + def update_requested_qty(self): + material_request_map = {} + for d in self.get("po_details"): + if d.prevdoc_doctype and d.prevdoc_doctype == "Material Request" and d.prevdoc_detail_docname: + material_request_map.setdefault(d.prevdoc_docname, []).append(d.prevdoc_detail_docname) - def update_bin(self, is_submit, is_stopped = 0): - from erpnext.stock.utils import update_bin - pc_obj = frappe.get_doc('Purchase Common') - for d in self.get('po_details'): - #1. Check if is_stock_item == 'Yes' - if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes": - # this happens when item is changed from non-stock to stock item - if not d.warehouse: - continue + for mr, mr_item_rows in material_request_map.items(): + if mr and mr_item_rows: + mr_obj = frappe.get_doc("Material Request", mr) - ind_qty, po_qty = 0, flt(d.qty) * flt(d.conversion_factor) - if is_stopped: - po_qty = flt(d.qty) > flt(d.received_qty) and \ - flt( flt(flt(d.qty) - flt(d.received_qty))*flt(d.conversion_factor)) or 0 + if mr_obj.status in ["Stopped", "Cancelled"]: + frappe.throw(_("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError) - # No updates in Material Request on Stop / Unstop - if cstr(d.prevdoc_doctype) == 'Material Request' and not is_stopped: - # get qty and pending_qty of prevdoc - curr_ref_qty = pc_obj.get_qty(d.doctype, 'prevdoc_detail_docname', - d.prevdoc_detail_docname, 'Material Request Item', - 'Material Request - Purchase Order', self.name) - max_qty, qty, curr_qty = flt(curr_ref_qty.split('~~~')[1]), \ - flt(curr_ref_qty.split('~~~')[0]), 0 + mr_obj.update_requested_qty(mr_item_rows) - if flt(qty) + flt(po_qty) > flt(max_qty): - curr_qty = flt(max_qty) - flt(qty) - # special case as there is no restriction - # for Material Request - Purchase Order - curr_qty = curr_qty > 0 and curr_qty or 0 - else: - curr_qty = flt(po_qty) + def update_ordered_qty(self, po_item_rows=None): + """update requested qty (before ordered_qty is updated)""" + from erpnext.stock.utils import get_bin - ind_qty = -flt(curr_qty) + def _update_ordered_qty(item_code, warehouse): + ordered_qty = frappe.db.sql(""" + select sum((po_item.qty - ifnull(po_item.received_qty, 0))*po_item.conversion_factor) + from `tabPurchase Order Item` po_item, `tabPurchase Order` po + where po_item.item_code=%s and po_item.warehouse=%s + and po_item.qty > ifnull(po_item.received_qty, 0) and po_item.parent=po.name + and po.status!='Stopped' and po.docstatus=1""", (item_code, warehouse)) - # Update ordered_qty and indented_qty in bin - args = { - "item_code": d.item_code, - "warehouse": d.warehouse, - "ordered_qty": (is_submit and 1 or -1) * flt(po_qty), - "indented_qty": (is_submit and 1 or -1) * flt(ind_qty), - "posting_date": self.transaction_date - } - update_bin(args) + bin_doc = get_bin(item_code, warehouse) + bin_doc.ordered_qty = flt(ordered_qty[0][0]) if ordered_qty else 0 + bin_doc.save() + + item_wh_list = [] + for d in self.get("po_details"): + if (not po_item_rows or d.name in po_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \ + and frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" and d.warehouse: + item_wh_list.append([d.item_code, d.warehouse]) + + for item_code, warehouse in item_wh_list: + _update_ordered_qty(item_code, warehouse) def check_modified_date(self): mod_db = frappe.db.sql("select modified from `tabPurchase Order` where name = %s", @@ -152,13 +147,11 @@ class PurchaseOrder(BuyingController): def update_status(self, status): self.check_modified_date() - # step 1:=> Set Status frappe.db.set(self,'status',cstr(status)) - # step 2:=> Update Bin - self.update_bin(is_submit = (status == 'Submitted') and 1 or 0, is_stopped = 1) + self.update_requested_qty() + self.update_ordered_qty() - # step 3:=> Acknowledge user msgprint(_("Status of {0} {1} is now {2}").format(self.doctype, self.name, status)) def on_submit(self): @@ -167,7 +160,8 @@ class PurchaseOrder(BuyingController): purchase_controller = frappe.get_doc("Purchase Common") self.update_prevdoc_status() - self.update_bin(is_submit = 1, is_stopped = 0) + self.update_requested_qty() + self.update_ordered_qty() frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.grand_total) @@ -192,8 +186,13 @@ class PurchaseOrder(BuyingController): throw(_("Purchase Invoice {0} is already submitted").format(", ".join(submitted))) frappe.db.set(self,'status','Cancelled') + self.update_prevdoc_status() - self.update_bin( is_submit = 0, is_stopped = 0) + + # Must be called after updating ordered qty in Material Request + self.update_requested_qty() + self.update_ordered_qty() + pc_obj.update_last_purchase_rate(self, is_submit = 0) def on_update(self): diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 842409f715..fc31a9a4b0 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -29,8 +29,7 @@ class TestPurchaseOrder(unittest.TestCase): frappe.get_doc(pr).insert() def test_ordered_qty(self): - frappe.db.sql("delete from tabBin") - + existing_ordered_qty = self._get_ordered_qty("_Test Item", "_Test Warehouse - _TC") from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt po = frappe.copy_doc(test_records[0]).insert() @@ -43,8 +42,7 @@ class TestPurchaseOrder(unittest.TestCase): po.get("po_details")[0].item_code = "_Test Item" po.submit() - self.assertEquals(frappe.db.get_value("Bin", {"item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC"}, "ordered_qty"), 10) + self.assertEquals(self._get_ordered_qty("_Test Item", "_Test Warehouse - _TC"), existing_ordered_qty + 10) pr = make_purchase_receipt(po.name) @@ -56,8 +54,9 @@ class TestPurchaseOrder(unittest.TestCase): pr.insert() pr.submit() - self.assertEquals(flt(frappe.db.get_value("Bin", {"item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC"}, "ordered_qty")), 6.0) + po.load_from_db() + self.assertEquals(po.get("po_details")[0].received_qty, 4) + self.assertEquals(self._get_ordered_qty("_Test Item", "_Test Warehouse - _TC"), existing_ordered_qty + 6) frappe.db.set_value('Item', '_Test Item', 'tolerance', 50) @@ -68,8 +67,16 @@ class TestPurchaseOrder(unittest.TestCase): pr1.insert() pr1.submit() - self.assertEquals(flt(frappe.db.get_value("Bin", {"item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC"}, "ordered_qty")), 0.0) + po.load_from_db() + self.assertEquals(po.get("po_details")[0].received_qty, 12) + self.assertEquals(self._get_ordered_qty("_Test Item", "_Test Warehouse - _TC"), existing_ordered_qty) + + pr1.load_from_db() + pr1.cancel() + + po.load_from_db() + self.assertEquals(po.get("po_details")[0].received_qty, 4) + self.assertEquals(self._get_ordered_qty("_Test Item", "_Test Warehouse - _TC"), existing_ordered_qty + 6) def test_make_purchase_invoice(self): from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice @@ -111,6 +118,9 @@ class TestPurchaseOrder(unittest.TestCase): from erpnext.controllers.tests.test_recurring_document import test_recurring_document test_recurring_document(self, test_records) + def _get_ordered_qty(self, item_code, warehouse): + return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "ordered_qty")) + test_dependencies = ["BOM", "Item Price"] diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 13495a0377..f38ee5d5de 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -8,7 +8,6 @@ from frappe.utils import cstr, flt, cint from frappe import _ import frappe.defaults -from erpnext.stock.utils import update_bin from erpnext.controllers.buying_controller import BuyingController @@ -157,29 +156,19 @@ class PurchaseReceipt(BuyingController): self.make_sl_entries(sl_entries) def update_ordered_qty(self): - stock_items = self.get_stock_items() + po_map = {} for d in self.get("purchase_receipt_details"): - if d.item_code in stock_items and d.warehouse \ - and cstr(d.prevdoc_doctype) == 'Purchase Order': + if d.prevdoc_doctype and d.prevdoc_doctype == "Purchase Order" and d.prevdoc_detail_docname: + po_map.setdefault(d.prevdoc_docname, []).append(d.prevdoc_detail_docname) - already_received_qty = self.get_already_received_qty(d.prevdoc_docname, - d.prevdoc_detail_docname) - po_qty, ordered_warehouse = self.get_po_qty_and_warehouse(d.prevdoc_detail_docname) + for po, po_item_rows in po_map.items(): + if po and po_item_rows: + po_obj = frappe.get_doc("Purchase Order", po) - if not ordered_warehouse: - frappe.throw(_("Warehouse is missing in Purchase Order")) + if po_obj.status in ["Stopped", "Cancelled"]: + frappe.throw(_("Material Request {0} is cancelled or stopped").format(po), frappe.InvalidStatusError) - if already_received_qty + d.qty > po_qty: - ordered_qty = - (po_qty - already_received_qty) * flt(d.conversion_factor) - else: - ordered_qty = - flt(d.qty) * flt(d.conversion_factor) - - update_bin({ - "item_code": d.item_code, - "warehouse": ordered_warehouse, - "posting_date": self.posting_date, - "ordered_qty": flt(ordered_qty) if self.docstatus==1 else -flt(ordered_qty) - }) + po_obj.update_ordered_qty(po_item_rows) def get_already_received_qty(self, po, po_detail): qty = frappe.db.sql("""select sum(qty) from `tabPurchase Receipt Item` @@ -265,11 +254,13 @@ class PurchaseReceipt(BuyingController): frappe.db.set(self,'status','Cancelled') - self.update_ordered_qty() - self.update_stock_ledger() self.update_prevdoc_status() + + # Must be called after updating received qty in PO + self.update_ordered_qty() + pc_obj.update_last_purchase_rate(self, 0) self.make_gl_entries_on_cancel()