Skip to content

Commit

Permalink
fix: Blanket Order (backport #34279) (#34548)
Browse files Browse the repository at this point in the history
* fix: hide `+` button based on `Blanket Order Type`

(cherry picked from commit abf9a28)

* feat: add field `Over Order Allowance (%)` in `Buying Settings`

(cherry picked from commit f5937f4)

# Conflicts:
#	erpnext/buying/doctype/buying_settings/buying_settings.json

* refactor: rewrite `blanket_order.py` queries in `QB`

(cherry picked from commit f399378)

* fix: don't map item row having `0` qty

(cherry picked from commit fc1088d)

* feat: consider `over_order_allowance` while validating order qty

(cherry picked from commit 8bcbc45)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.py

* feat: add field `Over Order Allowance (%)` in `Selling Settings`

(cherry picked from commit d7da892)

# Conflicts:
#	erpnext/selling/doctype/selling_settings/selling_settings.json

* feat: consider `over_order_allowance` while validating sales order qty

(cherry picked from commit 53701c3)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.py

* test: add test cases for `Over Order Allowance` against `Blanket Order`

(cherry picked from commit 66f6500)

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
  • Loading branch information
mergify[bot] and s-aga-r committed Mar 22, 2023
1 parent 00518eb commit 8ddbac5
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 132 deletions.
66 changes: 37 additions & 29 deletions erpnext/buying/doctype/buying_settings/buying_settings.json
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -82,48 +114,24 @@
"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",
"idx": 1,
"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",
Expand Down
4 changes: 4 additions & 0 deletions erpnext/buying/doctype/purchase_order/purchase_order.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down
6 changes: 6 additions & 0 deletions erpnext/manufacturing/doctype/blanket_order/blanket_order.js
Expand Up @@ -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");
},
Expand Down
67 changes: 53 additions & 14 deletions erpnext/manufacturing/doctype/blanket_order/blanket_order.py
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
)
)
27 changes: 27 additions & 0 deletions erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions erpnext/selling/doctype/sales_order/sales_order.py
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
)
Expand Down

0 comments on commit 8ddbac5

Please sign in to comment.