diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index b828a43d3cf4..52465c1a962b 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -14,6 +14,7 @@ "column_break_3", "po_required", "pr_required", + "over_order_allowance", "maintain_same_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", @@ -42,6 +43,23 @@ "label": "Default Buying Price List", "options": "Price List" }, + { + "default": "Stop", + "depends_on": "maintain_same_rate", + "description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.", + "fieldname": "maintain_same_rate_action", + "fieldtype": "Select", + "label": "Action If Same Rate is Not Maintained", + "mandatory_depends_on": "maintain_same_rate", + "options": "Stop\nWarn" + }, + { + "depends_on": "eval:doc.maintain_same_rate_action == 'Stop'", + "fieldname": "role_to_override_stop_action", + "fieldtype": "Link", + "label": "Role Allowed to Override Stop Action", + "options": "Role" + }, { "fieldname": "column_break_3", "fieldtype": "Column Break" @@ -58,6 +76,13 @@ "label": "Is Purchase Receipt Required for Purchase Invoice Creation?", "options": "No\nYes" }, + { + "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 (%)" + }, { "default": "0", "fieldname": "maintain_same_rate", @@ -70,6 +95,13 @@ "fieldtype": "Check", "label": "Allow Item To Be Added Multiple Times in a Transaction" }, + { + "default": "1", + "description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.", + "fieldname": "bill_for_rejected_quantity_in_purchase_invoice", + "fieldtype": "Check", + "label": "Bill for Rejected Quantity in Purchase Invoice" + }, { "fieldname": "subcontract", "fieldtype": "Section Break", @@ -82,40 +114,16 @@ "label": "Backflush Raw Materials of Subcontract Based On", "options": "BOM\nMaterial Transferred for Subcontract" }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, { "depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"BOM\"", "description": "Percentage you are allowed to transfer more against the quantity ordered. For example: If you have ordered 100 units. and your Allowance is 10% then you are allowed to transfer 110 units.", "fieldname": "over_transfer_allowance", "fieldtype": "Float", "label": "Over Transfer Allowance (%)" - }, - { - "fieldname": "column_break_11", - "fieldtype": "Column Break" - }, - { - "default": "Stop", - "depends_on": "maintain_same_rate", - "description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.", - "fieldname": "maintain_same_rate_action", - "fieldtype": "Select", - "label": "Action If Same Rate is Not Maintained", - "mandatory_depends_on": "maintain_same_rate", - "options": "Stop\nWarn" - }, - { - "depends_on": "eval:doc.maintain_same_rate_action == 'Stop'", - "fieldname": "role_to_override_stop_action", - "fieldtype": "Link", - "label": "Role Allowed to Override Stop Action", - "options": "Role" - }, - { - "default": "1", - "description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.", - "fieldname": "bill_for_rejected_quantity_in_purchase_invoice", - "fieldtype": "Check", - "label": "Bill for Rejected Quantity in Purchase Invoice" } ], "icon": "fa fa-cog", @@ -123,7 +131,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-08 19:26:23.548837", + "modified": "2023-03-22 13:01:49.640869", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 3888622d563d..9a2495ed4845 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.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 @@ -72,6 +75,7 @@ def validate(self): self.validate_bom_for_subcontracting_items() self.create_raw_materials_supplied("supplied_items") self.set_received_qty_for_drop_ship_items() + validate_against_blanket_order(self) validate_inter_company_party( self.doctype, self.supplier, self.company, self.inter_company_order_reference ) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js index d3bb33e86e0b..7b26a14a57b2 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"); }, diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index ff2140199de9..32f1c365adef 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -6,6 +6,7 @@ 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 @@ def validate_duplicate_items(self): 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: @@ -79,7 +82,43 @@ def update_item(source, target, source_parent): "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, }, }, ) 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/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index 2f1f3ae0f52e..58f3c9505989 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 @@ def test_purchase_order_creation(self): 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) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 865b9585618c..2f2a06f6fb12 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -22,6 +22,9 @@ ) 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, ) @@ -53,6 +56,7 @@ def validate(self): 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 ) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index ce976547dcdd..a51993ff7ff0 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -30,12 +30,18 @@ "so_required", "dn_required", "sales_update_frequency", + "over_order_allowance", "allow_multiple_items", "allow_against_multiple_purchase_orders", "hide_tax_id", "allow_sales_order_creation_for_expired_quotation" ], "fields": [ + { + "fieldname": "customer_defaults_section", + "fieldtype": "Section Break", + "label": "Customer Defaults" + }, { "default": "Customer Name", "fieldname": "cust_master_name", @@ -44,13 +50,6 @@ "label": "Customer Naming By", "options": "Customer Name\nNaming Series\nAuto Name" }, - { - "fieldname": "campaign_naming_by", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Campaign Naming By", - "options": "Campaign Name\nNaming Series\nAuto Name" - }, { "fieldname": "customer_group", "fieldtype": "Link", @@ -58,6 +57,10 @@ "label": "Default Customer Group", "options": "Customer Group" }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { "fieldname": "territory", "fieldtype": "Link", @@ -66,80 +69,50 @@ "options": "Territory" }, { - "fieldname": "selling_price_list", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Default Price List", - "options": "Price List" - }, - { - "default": "15", - "description": "Auto close Opportunity after the no. of days mentioned above", - "fieldname": "close_opportunity_after_days", - "fieldtype": "Int", - "label": "Close Opportunity After Days" - }, - { - "fieldname": "default_valid_till", - "fieldtype": "Data", - "label": "Default Quotation Validity Days" - }, - { - "fieldname": "so_required", - "fieldtype": "Select", - "label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?", - "options": "No\nYes" + "fieldname": "crm_settings_section", + "fieldtype": "Section Break", + "label": "CRM Settings" }, { - "fieldname": "dn_required", + "fieldname": "campaign_naming_by", "fieldtype": "Select", - "label": "Is Delivery Note Required for Sales Invoice Creation?", - "options": "No\nYes" + "in_list_view": 1, + "label": "Campaign Naming By", + "options": "Campaign Name\nNaming Series\nAuto Name" }, { - "default": "Each Transaction", - "description": "How often should Project and Company be updated based on Sales Transactions?", - "fieldname": "sales_update_frequency", + "fieldname": "contract_naming_by", "fieldtype": "Select", - "label": "Sales Update Frequency", - "options": "Each Transaction\nDaily\nMonthly", - "reqd": 1 - }, - { - "default": "0", - "fieldname": "maintain_same_sales_rate", - "fieldtype": "Check", - "label": "Maintain Same Rate Throughout Sales Cycle" + "label": "Contract Naming By", + "options": "Party Name\nNaming Series" }, { - "default": "0", - "fieldname": "editable_price_list_rate", - "fieldtype": "Check", - "label": "Allow User to Edit Price List Rate in Transactions" + "fieldname": "default_valid_till", + "fieldtype": "Data", + "label": "Default Quotation Validity Days" }, { - "default": "0", - "fieldname": "allow_multiple_items", - "fieldtype": "Check", - "label": "Allow Item to Be Added Multiple Times in a Transaction" + "fieldname": "column_break_9", + "fieldtype": "Column Break" }, { - "default": "0", - "fieldname": "allow_against_multiple_purchase_orders", - "fieldtype": "Check", - "label": "Allow Multiple Sales Orders Against a Customer's Purchase Order" + "default": "15", + "description": "Auto close Opportunity after the no. of days mentioned above", + "fieldname": "close_opportunity_after_days", + "fieldtype": "Int", + "label": "Close Opportunity After Days" }, { - "default": "0", - "fieldname": "validate_selling_price", - "fieldtype": "Check", - "label": "Validate Selling Price for Item Against Purchase Rate or Valuation Rate" + "fieldname": "item_price_settings_section", + "fieldtype": "Section Break", + "label": "Item Price Settings" }, { - "default": "0", - "fieldname": "hide_tax_id", - "fieldtype": "Check", - "label": "Hide Customer's Tax ID from Sales Transactions" + "fieldname": "selling_price_list", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Price List", + "options": "Price List" }, { "default": "Stop", @@ -161,6 +134,24 @@ "fieldname": "column_break_15", "fieldtype": "Column Break" }, + { + "default": "0", + "fieldname": "maintain_same_sales_rate", + "fieldtype": "Check", + "label": "Maintain Same Rate Throughout Sales Cycle" + }, + { + "default": "0", + "fieldname": "editable_price_list_rate", + "fieldtype": "Check", + "label": "Allow User to Edit Price List Rate in Transactions" + }, + { + "default": "0", + "fieldname": "validate_selling_price", + "fieldtype": "Check", + "label": "Validate Selling Price for Item Against Purchase Rate or Valuation Rate" + }, { "default": "0", "fieldname": "editable_bundle_item_rates", @@ -168,44 +159,61 @@ "label": "Calculate Product Bundle Price based on Child Items' Rates" }, { - "fieldname": "customer_defaults_section", + "fieldname": "sales_transactions_settings_section", "fieldtype": "Section Break", - "label": "Customer Defaults" + "label": "Transaction Settings" }, { - "fieldname": "column_break_4", - "fieldtype": "Column Break" + "fieldname": "so_required", + "fieldtype": "Select", + "label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?", + "options": "No\nYes" }, { - "fieldname": "crm_settings_section", - "fieldtype": "Section Break", - "label": "CRM Settings" + "fieldname": "dn_required", + "fieldtype": "Select", + "label": "Is Delivery Note Required for Sales Invoice Creation?", + "options": "No\nYes" }, { - "fieldname": "column_break_9", - "fieldtype": "Column Break" + "default": "Each Transaction", + "description": "How often should Project and Company be updated based on Sales Transactions?", + "fieldname": "sales_update_frequency", + "fieldtype": "Select", + "label": "Sales Update Frequency", + "options": "Each Transaction\nDaily\nMonthly", + "reqd": 1 }, { - "fieldname": "item_price_settings_section", - "fieldtype": "Section Break", - "label": "Item Price Settings" + "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 (%)" }, { - "fieldname": "sales_transactions_settings_section", - "fieldtype": "Section Break", - "label": "Transaction Settings" + "default": "0", + "fieldname": "allow_multiple_items", + "fieldtype": "Check", + "label": "Allow Item to Be Added Multiple Times in a Transaction" }, { - "fieldname": "contract_naming_by", - "fieldtype": "Select", - "label": "Contract Naming By", - "options": "Party Name\nNaming Series" + "default": "0", + "fieldname": "allow_against_multiple_purchase_orders", + "fieldtype": "Check", + "label": "Allow Multiple Sales Orders Against a Customer's Purchase Order" + }, + { + "default": "0", + "fieldname": "hide_tax_id", + "fieldtype": "Check", + "label": "Hide Customer's Tax ID from Sales Transactions" }, { - "default": "0", - "fieldname": "allow_sales_order_creation_for_expired_quotation", - "fieldtype": "Check", - "label": "Allow Sales Order Creation For Expired Quotation" + "default": "0", + "fieldname": "allow_sales_order_creation_for_expired_quotation", + "fieldtype": "Check", + "label": "Allow Sales Order Creation For Expired Quotation" } ], "icon": "fa fa-cog", @@ -213,7 +221,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-02-04 12:37:53.380857", + "modified": "2023-03-22 13:09:38.513317", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings",