From abf9a28d6af8b3c9bfab1e892e56bf3adb18ee8e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 2 Mar 2023 16:40:37 +0530 Subject: [PATCH 1/8] fix: hide `+` button based on `Blanket Order Type` --- .../manufacturing/doctype/blanket_order/blanket_order.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js index d3bb33e86e..7b26a14a57 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js @@ -7,6 +7,12 @@ frappe.ui.form.on('Blanket Order', { }, setup: function(frm) { + frm.custom_make_buttons = { + 'Purchase Order': 'Purchase Order', + 'Sales Order': 'Sales Order', + 'Quotation': 'Quotation', + }; + frm.add_fetch("customer", "customer_name", "customer_name"); frm.add_fetch("supplier", "supplier_name", "supplier_name"); }, From f5937f46cb60f3521463f7a4c80c765f8a65e52b Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 2 Mar 2023 17:04:09 +0530 Subject: [PATCH 2/8] feat: add field `Over Order Allowance (%)` in `Buying Settings` --- .../doctype/buying_settings/buying_settings.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 95857e4604..8c73e56a99 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -16,6 +16,7 @@ "transaction_settings_section", "po_required", "pr_required", + "over_order_allowance", "column_break_12", "maintain_same_rate", "set_landed_cost_based_on_purchase_invoice_rate", @@ -156,6 +157,13 @@ "fieldname": "set_landed_cost_based_on_purchase_invoice_rate", "fieldtype": "Check", "label": "Set Landed Cost Based on Purchase Invoice Rate" + }, + { + "default": "0", + "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", + "fieldname": "over_order_allowance", + "fieldtype": "Float", + "label": "Over Order Allowance (%)" } ], "icon": "fa fa-cog", @@ -163,7 +171,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-02-28 15:41:32.686805", + "modified": "2023-03-02 17:02:14.404622", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", From f3993783a3fc431a2909b445e9d09d9f584ff73e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 2 Mar 2023 17:18:46 +0530 Subject: [PATCH 3/8] refactor: rewrite `blanket_order.py` queries in `QB` --- .../doctype/blanket_order/blanket_order.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index ff2140199d..3298f43ac3 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import Sum from frappe.utils import flt, getdate from erpnext.stock.doctype.item.item import get_item_defaults @@ -29,21 +30,23 @@ class BlanketOrder(Document): def update_ordered_qty(self): ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order" + + trans = frappe.qb.DocType(ref_doctype) + trans_item = frappe.qb.DocType(f"{ref_doctype} Item") + item_ordered_qty = frappe._dict( - frappe.db.sql( - """ - select trans_item.item_code, sum(trans_item.stock_qty) as qty - from `tab{0} Item` trans_item, `tab{0}` trans - where trans.name = trans_item.parent - and trans_item.blanket_order=%s - and trans.docstatus=1 - and trans.status not in ('Closed', 'Stopped') - group by trans_item.item_code - """.format( - ref_doctype - ), - self.name, - ) + ( + frappe.qb.from_(trans_item) + .from_(trans) + .select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty")) + .where( + (trans.name == trans_item.parent) + & (trans_item.blanket_order == self.name) + & (trans.docstatus == 1) + & (trans.status.notin(["Stopped", "Closed"])) + ) + .groupby(trans_item.item_code) + ).run() ) for d in self.items: From fc1088d9c4787b12bd9734597604492044eff4a0 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 3 Mar 2023 10:49:25 +0530 Subject: [PATCH 4/8] fix: don't map item row having `0` qty --- erpnext/manufacturing/doctype/blanket_order/blanket_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index 3298f43ac3..d03f019b08 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -82,6 +82,7 @@ def make_order(source_name): "doctype": doctype + " Item", "field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"}, "postprocess": update_item, + "condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0, }, }, ) From 8bcbc45add7767ac947fa7c9b3aaca99fc9dda9b Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 3 Mar 2023 11:11:33 +0530 Subject: [PATCH 5/8] feat: consider `over_order_allowance` while validating order qty --- .../doctype/purchase_order/purchase_order.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 2415aec8cb..d9ff981322 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -69,6 +69,7 @@ class PurchaseOrder(BuyingController): self.validate_with_previous_doc() self.validate_for_subcontracting() self.validate_minimum_order_qty() + self.validate_against_blanket_order() if self.is_old_subcontracting_flow: self.validate_bom_for_subcontracting_items() @@ -197,6 +198,33 @@ class PurchaseOrder(BuyingController): ).format(item_code, qty, itemwise_min_order_qty.get(item_code)) ) + def validate_against_blanket_order(self): + po_data = {} + for item in self.get("items"): + if item.against_blanket_order and item.blanket_order: + if item.blanket_order in po_data: + if item.item_code in po_data[item.blanket_order]: + po_data[item.blanket_order][item.item_code] += item.qty + else: + po_data[item.blanket_order][item.item_code] = item.qty + else: + po_data[item.blanket_order] = {item.item_code: item.qty} + + if po_data: + allowance = flt(frappe.db.get_single_value("Buying Settings", "over_order_allowance")) + for bo_name, item_data in po_data.items(): + bo_doc = frappe.get_doc("Blanket Order", bo_name) + for item in bo_doc.get("items"): + if item.item_code in item_data: + remaining_qty = item.qty - item.ordered_qty + allowed_qty = remaining_qty + (remaining_qty * (allowance / 100)) + if allowed_qty < item_data[item.item_code]: + frappe.throw( + _( + f"Item {item.item_code} cannot be ordered more than {allowed_qty} against Blanket Order {bo_name}." + ) + ) + def validate_bom_for_subcontracting_items(self): for item in self.items: if not item.bom: From d7da8928ac44df3a84f6099fc7bfbc9a9161be20 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 3 Mar 2023 11:19:28 +0530 Subject: [PATCH 6/8] feat: add field `Over Order Allowance (%)` in `Selling Settings` --- .../doctype/selling_settings/selling_settings.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 6ea66a0237..45ad7d95a1 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -24,6 +24,7 @@ "so_required", "dn_required", "sales_update_frequency", + "over_order_allowance", "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", @@ -179,6 +180,12 @@ "fieldname": "allow_sales_order_creation_for_expired_quotation", "fieldtype": "Check", "label": "Allow Sales Order Creation For Expired Quotation" + }, + { + "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", + "fieldname": "over_order_allowance", + "fieldtype": "Float", + "label": "Over Order Allowance (%)" } ], "icon": "fa fa-cog", @@ -186,7 +193,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-02-04 12:37:53.380857", + "modified": "2023-03-03 11:16:54.333615", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From 53701c37b18c7aecfaa00efabf4d3be768e59cb3 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 3 Mar 2023 22:03:54 +0530 Subject: [PATCH 7/8] feat: consider `over_order_allowance` while validating sales order qty --- .../doctype/purchase_order/purchase_order.py | 32 +++-------------- .../doctype/blanket_order/blanket_order.py | 35 +++++++++++++++++++ .../doctype/sales_order/sales_order.py | 4 +++ 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index d9ff981322..06b9d29e69 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category from erpnext.accounts.party import get_party_account, get_party_account_currency from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items from erpnext.controllers.buying_controller import BuyingController +from erpnext.manufacturing.doctype.blanket_order.blanket_order import ( + validate_against_blanket_order, +) from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty @@ -69,7 +72,7 @@ class PurchaseOrder(BuyingController): self.validate_with_previous_doc() self.validate_for_subcontracting() self.validate_minimum_order_qty() - self.validate_against_blanket_order() + validate_against_blanket_order(self) if self.is_old_subcontracting_flow: self.validate_bom_for_subcontracting_items() @@ -198,33 +201,6 @@ class PurchaseOrder(BuyingController): ).format(item_code, qty, itemwise_min_order_qty.get(item_code)) ) - def validate_against_blanket_order(self): - po_data = {} - for item in self.get("items"): - if item.against_blanket_order and item.blanket_order: - if item.blanket_order in po_data: - if item.item_code in po_data[item.blanket_order]: - po_data[item.blanket_order][item.item_code] += item.qty - else: - po_data[item.blanket_order][item.item_code] = item.qty - else: - po_data[item.blanket_order] = {item.item_code: item.qty} - - if po_data: - allowance = flt(frappe.db.get_single_value("Buying Settings", "over_order_allowance")) - for bo_name, item_data in po_data.items(): - bo_doc = frappe.get_doc("Blanket Order", bo_name) - for item in bo_doc.get("items"): - if item.item_code in item_data: - remaining_qty = item.qty - item.ordered_qty - allowed_qty = remaining_qty + (remaining_qty * (allowance / 100)) - if allowed_qty < item_data[item.item_code]: - frappe.throw( - _( - f"Item {item.item_code} cannot be ordered more than {allowed_qty} against Blanket Order {bo_name}." - ) - ) - def validate_bom_for_subcontracting_items(self): for item in self.items: if not item.bom: diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index d03f019b08..32f1c365ad 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -87,3 +87,38 @@ def make_order(source_name): }, ) return target_doc + + +def validate_against_blanket_order(order_doc): + if order_doc.doctype in ("Sales Order", "Purchase Order"): + order_data = {} + + for item in order_doc.get("items"): + if item.against_blanket_order and item.blanket_order: + if item.blanket_order in order_data: + if item.item_code in order_data[item.blanket_order]: + order_data[item.blanket_order][item.item_code] += item.qty + else: + order_data[item.blanket_order][item.item_code] = item.qty + else: + order_data[item.blanket_order] = {item.item_code: item.qty} + + if order_data: + allowance = flt( + frappe.db.get_single_value( + "Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings", + "over_order_allowance", + ) + ) + for bo_name, item_data in order_data.items(): + bo_doc = frappe.get_doc("Blanket Order", bo_name) + for item in bo_doc.get("items"): + if item.item_code in item_data: + remaining_qty = item.qty - item.ordered_qty + allowed_qty = remaining_qty + (remaining_qty * (allowance / 100)) + if allowed_qty < item_data[item.item_code]: + frappe.throw( + _("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format( + item.item_code, allowed_qty, bo_name + ) + ) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 385d0f3a58..ee9161bee4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( ) from erpnext.accounts.party import get_party_account from erpnext.controllers.selling_controller import SellingController +from erpnext.manufacturing.doctype.blanket_order.blanket_order import ( + validate_against_blanket_order, +) from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, ) @@ -52,6 +55,7 @@ class SalesOrder(SellingController): self.validate_warehouse() self.validate_drop_ship() self.validate_serial_no_based_delivery() + validate_against_blanket_order(self) validate_inter_company_party( self.doctype, self.customer, self.company, self.inter_company_order_reference ) From 66f650061dbae8c1093878f5b808e2a62f3a144a Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 13 Mar 2023 17:21:07 +0530 Subject: [PATCH 8/8] test: add test cases for `Over Order Allowance` against `Blanket Order` --- .../blanket_order/test_blanket_order.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index 2f1f3ae0f5..58f3c95059 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -63,6 +63,33 @@ class TestBlanketOrder(FrappeTestCase): po1.currency = get_company_currency(po1.company) self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) + def test_over_order_allowance(self): + # Sales Order + bo = make_blanket_order(blanket_order_type="Selling", quantity=100) + + frappe.flags.args.doctype = "Sales Order" + so = make_order(bo.name) + so.currency = get_company_currency(so.company) + so.delivery_date = today() + so.items[0].qty = 110 + self.assertRaises(frappe.ValidationError, so.submit) + + frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10) + so.submit() + + # Purchase Order + bo = make_blanket_order(blanket_order_type="Purchasing", quantity=100) + + frappe.flags.args.doctype = "Purchase Order" + po = make_order(bo.name) + po.currency = get_company_currency(po.company) + po.schedule_date = today() + po.items[0].qty = 110 + self.assertRaises(frappe.ValidationError, po.submit) + + frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10) + po.submit() + def make_blanket_order(**args): args = frappe._dict(args)