mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-29 22:58:25 +00:00
feat: subcontracting inward (#47728)
* feat: subcontracting inward * feat: stock reservation * feat: subcontracting delivery * feat: all remaining stuff * fix: linter errors * fix: patch * fix: modify stock entry type validation * fix: customer provided item cost field mandatory validation * fix: failing tests * fix: failing tests * fix: subcontracting controlller * refactor: semi final * refactor: final * chore: resolve conflicts * refactor: changes requested * fix: reservation transfer of extra qty * fix: consider add cost for customer provided rate field * test: create test data * test: subcontracted sales order (partial) * test: fin * fix: do not add self RM in DN created from SI * fix: failing test case * fix: conflicting function name * refactor: final changes * fix: more bugs * perf: various and major performance improvements * fix: consider warehouse as well in all queries * fix: same item code with diff warehouse in manufacture entry * refactor: readability * fix: frontend validations * perf: replace query inside loop with single query * fix: set additional item flag to true when extra customer provided item is received * fix: bugs found by coderabbit * fix: more coderabbit bugs * fix: add validation to disallow cancellation of manufacturing entry * perf: use cached values wherever it makes sense * test: fix redundant insert to child tables * fix: consider SI return of billed self RM * fix: bug found by coderabbit --------- Co-authored-by: Mihir Kandoi <mihirkandoi@Mihirs-MacBook-Air.local>
This commit is contained in:
@@ -35,6 +35,14 @@ class SubcontractingController(StockController):
|
||||
"order_supplied_items_field": "Purchase Order Item Supplied",
|
||||
}
|
||||
)
|
||||
elif self.doctype == "Subcontracting Inward Order":
|
||||
self.subcontract_data = frappe._dict(
|
||||
{
|
||||
"order_doctype": "Subcontracting Inward Order",
|
||||
"order_field": "subcontracting_inward_order",
|
||||
"rm_detail_field": "scio_detail",
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.subcontract_data = frappe._dict(
|
||||
{
|
||||
@@ -47,14 +55,22 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
|
||||
def before_validate(self):
|
||||
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
|
||||
if self.doctype in [
|
||||
"Subcontracting Order",
|
||||
"Subcontracting Inward Order",
|
||||
"Subcontracting Receipt",
|
||||
]:
|
||||
self.remove_empty_rows()
|
||||
self.set_items_conversion_factor()
|
||||
|
||||
def validate(self):
|
||||
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
|
||||
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt", "Subcontracting Inward Order"]:
|
||||
self.validate_items()
|
||||
self.create_raw_materials_supplied()
|
||||
self.create_raw_materials_supplied_or_received(
|
||||
raw_material_table="supplied_items"
|
||||
if self.doctype != "Subcontracting Inward Order"
|
||||
else "received_items"
|
||||
)
|
||||
self.set_valuation_rate_for_rm()
|
||||
else:
|
||||
super().validate()
|
||||
@@ -109,7 +125,7 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
|
||||
def remove_empty_rows(self):
|
||||
for key in ["service_items", "items", "supplied_items"]:
|
||||
for key in ["service_items", "items", "supplied_items", "received_items"]:
|
||||
if self.get(key):
|
||||
idx = 1
|
||||
for item in self.get(key)[:]:
|
||||
@@ -133,33 +149,47 @@ class SubcontractingController(StockController):
|
||||
if not is_stock_item:
|
||||
frappe.throw(_("Row {0}: Item {1} must be a stock item.").format(item.idx, item.item_name))
|
||||
|
||||
if (
|
||||
self.doctype == "Subcontracting Inward Order"
|
||||
and item.delivery_warehouse == self.customer_warehouse
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}."
|
||||
).format(item.idx, frappe.bold(item.item_name))
|
||||
)
|
||||
|
||||
if not item.get("is_scrap_item"):
|
||||
if not is_sub_contracted_item:
|
||||
frappe.throw(
|
||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
||||
)
|
||||
|
||||
if (
|
||||
self.doctype == "Subcontracting Order" and not item.subcontracting_conversion_factor
|
||||
): # this condition will only be true if user has recently updated from develop branch
|
||||
service_item_qty = frappe.get_value(
|
||||
"Subcontracting Order Service Item",
|
||||
filters={"purchase_order_item": item.purchase_order_item, "parent": self.name},
|
||||
fieldname=["qty"],
|
||||
if self.doctype != "Subcontracting Receipt" and item.qty > flt(
|
||||
get_pending_subcontracted_quantity(
|
||||
self.doctype,
|
||||
self.purchase_order if self.doctype == "Subcontracting Order" else self.sales_order,
|
||||
).get(
|
||||
item.purchase_order_item
|
||||
if self.doctype == "Subcontracting Order"
|
||||
else item.sales_order_item
|
||||
)
|
||||
item.subcontracting_conversion_factor = service_item_qty / item.qty
|
||||
|
||||
if self.doctype not in "Subcontracting Receipt" and item.qty > flt(
|
||||
get_pending_subcontracted_quantity(self.purchase_order).get(item.purchase_order_item)
|
||||
/ item.subcontracting_conversion_factor,
|
||||
frappe.get_precision("Purchase Order Item", "qty"),
|
||||
frappe.get_precision(
|
||||
"Purchase Order Item"
|
||||
if self.doctype == "Subcontracting Order"
|
||||
else "Sales Order Item",
|
||||
"qty",
|
||||
),
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Item {1}'s quantity cannot be higher than the available quantity."
|
||||
).format(item.idx, item.item_name)
|
||||
)
|
||||
item.amount = item.qty * item.rate
|
||||
|
||||
if self.doctype != "Subcontracting Inward Order":
|
||||
item.amount = item.qty * item.rate
|
||||
|
||||
if item.bom:
|
||||
is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"])
|
||||
@@ -198,7 +228,10 @@ class SubcontractingController(StockController):
|
||||
self.__changed_name = []
|
||||
self.__reference_name = []
|
||||
|
||||
if self.doctype in ["Purchase Order", "Subcontracting Order"] or self.is_new():
|
||||
if (
|
||||
self.doctype in ["Purchase Order", "Subcontracting Order", "Subcontracting Inward Order"]
|
||||
or self.is_new()
|
||||
):
|
||||
self.set(self.raw_material_table, [])
|
||||
return
|
||||
|
||||
@@ -220,8 +253,13 @@ class SubcontractingController(StockController):
|
||||
self.__changed_name.extend(item_dict.keys())
|
||||
|
||||
def __get_backflush_based_on(self):
|
||||
self.backflush_based_on = frappe.db.get_single_value(
|
||||
"Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
|
||||
self.backflush_based_on = (
|
||||
frappe.db.get_single_value(
|
||||
"Buying Settings",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
)
|
||||
if self.subcontract_data.order_doctype == "Subcontracting Order"
|
||||
else "Material Transferred for Subcontract"
|
||||
)
|
||||
|
||||
def initialized_fields(self):
|
||||
@@ -233,7 +271,7 @@ class SubcontractingController(StockController):
|
||||
def __get_subcontract_orders(self):
|
||||
self.subcontract_orders = []
|
||||
|
||||
if self.doctype in ["Purchase Order", "Subcontracting Order"]:
|
||||
if self.doctype in ["Purchase Order", "Subcontracting Order", "Subcontracting Inward Order"]:
|
||||
return
|
||||
|
||||
self.subcontract_orders = [
|
||||
@@ -543,8 +581,13 @@ class SubcontractingController(StockController):
|
||||
return frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
|
||||
|
||||
def __update_reserve_warehouse(self, row, item):
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
if (
|
||||
self.doctype == self.subcontract_data.order_doctype
|
||||
and self.doctype != "Subcontracting Inward Order"
|
||||
):
|
||||
row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
|
||||
elif frappe.get_cached_value("Item", row.rm_item_code, "is_customer_provided_item"):
|
||||
row.warehouse = self.customer_warehouse
|
||||
|
||||
def __set_alternative_item(self, bom_item):
|
||||
if self.alternative_item_details.get(bom_item.rm_item_code):
|
||||
@@ -619,7 +662,7 @@ class SubcontractingController(StockController):
|
||||
|
||||
return serial_nos
|
||||
|
||||
def __add_supplied_item(self, item_row, bom_item, qty):
|
||||
def __add_supplied_or_received_item(self, item_row, bom_item, qty):
|
||||
bom_item.conversion_factor = item_row.conversion_factor
|
||||
rm_obj = self.append(self.raw_material_table, bom_item)
|
||||
if rm_obj.get("qty"):
|
||||
@@ -632,7 +675,8 @@ class SubcontractingController(StockController):
|
||||
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
rm_obj.required_qty = flt(qty, rm_obj.precision("required_qty"))
|
||||
rm_obj.amount = flt(rm_obj.required_qty * rm_obj.rate, rm_obj.precision("amount"))
|
||||
if self.doctype != "Subcontracting Inward Order":
|
||||
rm_obj.amount = flt(rm_obj.required_qty * rm_obj.rate, rm_obj.precision("amount"))
|
||||
else:
|
||||
rm_obj.consumed_qty = flt(qty, rm_obj.precision("consumed_qty"))
|
||||
rm_obj.required_qty = flt(bom_item.required_qty or qty, rm_obj.precision("required_qty"))
|
||||
@@ -841,14 +885,14 @@ class SubcontractingController(StockController):
|
||||
|
||||
return qty
|
||||
|
||||
def __set_supplied_items(self):
|
||||
def __set_supplied_or_received_items(self):
|
||||
self.bom_items = {}
|
||||
|
||||
has_supplied_items = True if self.get(self.raw_material_table) else False
|
||||
has_items = True if self.get(self.raw_material_table) else False
|
||||
for row in self.items:
|
||||
if self.doctype != self.subcontract_data.order_doctype and (
|
||||
(self.__changed_name and row.name not in self.__changed_name)
|
||||
or (has_supplied_items and not self.__changed_name)
|
||||
or (has_items and not self.__changed_name)
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -862,7 +906,7 @@ class SubcontractingController(StockController):
|
||||
bom_item.main_item_code = row.item_code
|
||||
self.__update_reserve_warehouse(bom_item, row)
|
||||
self.__set_alternative_item(bom_item)
|
||||
self.__add_supplied_item(row, bom_item, qty)
|
||||
self.__add_supplied_or_received_item(row, bom_item, qty)
|
||||
|
||||
elif self.backflush_based_on != "BOM":
|
||||
for key, transfer_item in self.available_materials.items():
|
||||
@@ -872,7 +916,7 @@ class SubcontractingController(StockController):
|
||||
) and transfer_item.qty > 0:
|
||||
qty = flt(self.__get_qty_based_on_material_transfer(row, transfer_item))
|
||||
transfer_item.qty -= qty
|
||||
self.__add_supplied_item(row, transfer_item.get("item_details"), qty)
|
||||
self.__add_supplied_or_received_item(row, transfer_item.get("item_details"), qty)
|
||||
|
||||
if self.qty_to_be_received:
|
||||
self.qty_to_be_received[
|
||||
@@ -940,13 +984,13 @@ class SubcontractingController(StockController):
|
||||
):
|
||||
return row
|
||||
|
||||
def __prepare_supplied_items(self):
|
||||
def __prepare_supplied_or_received_items(self):
|
||||
self.initialized_fields()
|
||||
self.__get_subcontract_orders()
|
||||
self.__get_pending_qty_to_receive()
|
||||
self.get_available_materials()
|
||||
self.__remove_changed_rows()
|
||||
self.__set_supplied_items()
|
||||
self.__set_supplied_or_received_items()
|
||||
self.__modify_serial_and_batch_bundle()
|
||||
self.__set_rate_for_serial_and_batch_bundle()
|
||||
|
||||
@@ -973,7 +1017,7 @@ class SubcontractingController(StockController):
|
||||
msg = f"The Serial Nos {incorrect_sn} has not supplied against the {self.subcontract_data.order_doctype} {link}"
|
||||
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
|
||||
|
||||
def __validate_supplied_items(self):
|
||||
def __validate_supplied_or_received_items(self):
|
||||
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||
return
|
||||
|
||||
@@ -991,10 +1035,10 @@ class SubcontractingController(StockController):
|
||||
|
||||
self.raw_material_table = raw_material_table
|
||||
self.__identify_change_in_item_table()
|
||||
self.__prepare_supplied_items()
|
||||
self.__validate_supplied_items()
|
||||
self.__prepare_supplied_or_received_items()
|
||||
self.__validate_supplied_or_received_items()
|
||||
|
||||
def create_raw_materials_supplied(self, raw_material_table="supplied_items"):
|
||||
def create_raw_materials_supplied_or_received(self, raw_material_table="supplied_items"):
|
||||
self.set_materials_for_subcontracted_items(raw_material_table)
|
||||
|
||||
if self.doctype in ["Subcontracting Receipt", "Purchase Receipt", "Purchase Invoice"]:
|
||||
@@ -1247,14 +1291,16 @@ def get_item_details(items):
|
||||
return item_details
|
||||
|
||||
|
||||
def get_pending_subcontracted_quantity(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
def get_pending_subcontracted_quantity(doctype, name):
|
||||
table = frappe.qb.DocType(
|
||||
"Purchase Order Item" if doctype == "Subcontracting Order" else "Sales Order Item"
|
||||
)
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name, table.qty, table.subcontracted_quantity)
|
||||
.where(table.parent == po_name)
|
||||
.select(table.name, table.qty, table.subcontracted_qty)
|
||||
.where(table.parent == name)
|
||||
)
|
||||
return {item.name: item.qty - item.subcontracted_quantity for item in query.run(as_dict=True)}
|
||||
return {item.name: item.qty - item.subcontracted_qty for item in query.run(as_dict=True)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
Reference in New Issue
Block a user