Merge pull request #50189 from mihir-kandoi/inward-fix

fix: feat: multiple changes related to subcontracting inward
This commit is contained in:
Mihir Kandoi
2025-10-22 00:22:34 +05:30
committed by GitHub
16 changed files with 525 additions and 168 deletions

View File

@@ -1297,7 +1297,7 @@ class SalesInvoice(SellingController):
item.idx,
item.stock_qty,
item.stock_uom,
frappe.bold(item.item_code),
get_link_to_form("Item", item.item_code),
frappe.bold(max_qty),
)
)

View File

@@ -3939,8 +3939,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
if parent.is_subcontracted and not parent.can_update_items():
frappe.throw(
_(
"Items cannot be updated as Subcontracting Inward Order is created against the Sales Order {0}."
).format(frappe.bold(parent.name))
"Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order."
)
)
parent.validate_selling_price()
parent.validate_for_duplicate_items()

View File

@@ -156,7 +156,7 @@ class SubcontractingController(StockController):
frappe.throw(
_(
"Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}."
).format(item.idx, frappe.bold(item.item_name))
).format(item.idx, get_link_to_form("Item", item.item_code))
)
if not item.get("is_scrap_item"):
@@ -664,6 +664,8 @@ class SubcontractingController(StockController):
def __add_supplied_or_received_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor
if self.subcontract_data.order_doctype == "Subcontracting Inward Order":
bom_item.pop("rate")
rm_obj = self.append(self.raw_material_table, bom_item)
if rm_obj.get("qty"):
# Qty field not exists

View File

@@ -9,7 +9,7 @@ from erpnext.stock.serial_batch_bundle import get_serial_batch_list_from_item
class SubcontractingInwardController:
def validate_subcontracting_inward(self):
self.validate_inward_order()
self.validate_customer_provided_item_for_inward()
self.set_allow_zero_valuation_rate()
self.validate_warehouse_()
self.validate_serial_batch_for_return_or_delivery()
self.validate_delivery()
@@ -50,11 +50,22 @@ class SubcontractingInwardController:
self.validate_material_receipt()
case purpose if purpose in ["Return Raw Material to Customer", "Subcontracting Return"]:
self.validate_returns()
case "Material Transfer for Manufacture":
self.validate_material_transfer()
case "Manufacture":
self.validate_manufacture()
def validate_material_receipt(self):
rm_item_fg_combo = []
for item in self.items:
if not frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item"):
frappe.throw(
_("Row #{0}: Item {1} is not a Customer Provided Item.").format(
item.idx,
get_link_to_form("Item", item.item_code),
)
)
if (
item.scio_detail
and frappe.get_cached_value(
@@ -65,17 +76,44 @@ class SubcontractingInwardController:
frappe.throw(
_(
"Row #{0}: Item {1} mismatch. Changing of item code is not permitted, add another row instead."
).format(item.idx, bold(item.item_code))
).format(item.idx, get_link_to_form("Item", item.item_code))
)
if not item.scio_detail: # item is additional
if item.against_fg:
if (item.item_code, item.against_fg) not in rm_item_fg_combo:
rm_item_fg_combo.append((item.item_code, item.against_fg))
else:
frappe.throw(
_(
"Row #{0}: Customer Provided Item {1} against Subcontracting Inward Order Item {2} ({3}) cannot be added multiple times."
).format(
item.idx,
get_link_to_form("Item", item.item_code),
bold(item.against_fg),
get_link_to_form(
"Item",
frappe.get_cached_value(
"Subcontracting Inward Order Item", item.against_fg, "item_code"
),
),
)
)
else:
frappe.throw(
_(
"Row #{0}: Please select the Finished Good Item against which this Customer Provided Item will be used."
).format(item.idx)
)
def validate_returns(self):
for item in self.items:
if not item.scio_detail:
frappe.throw(
_("Row #{0}: Item {1} is not a part of Subcontracting Inward Order {2}").format(
item.idx,
bold(item.item_code),
bold(self.subcontracting_inward_order),
get_link_to_form("Item", item.item_code),
get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order),
)
)
elif item.item_code != (
@@ -86,7 +124,7 @@ class SubcontractingInwardController:
):
frappe.throw(
_("Row #{0}: Item {1} mismatch. Changing of item code is not permitted.").format(
item.idx, bold(item.item_code)
item.idx, get_link_to_form("Item", item.item_code)
)
)
@@ -101,7 +139,7 @@ class SubcontractingInwardController:
frappe.throw(
_(
"Row #{0}: Returned quantity cannot be greater than available quantity for Item {1}"
).format(item.idx, bold(item.item_code))
).format(item.idx, get_link_to_form("Item", item.item_code))
)
else:
data = frappe.get_value(
@@ -114,16 +152,88 @@ class SubcontractingInwardController:
frappe.throw(
_(
"Row #{0}: Returned quantity cannot be greater than available quantity to return for Item {1}"
).format(item.idx, bold(item.item_code))
).format(item.idx, get_link_to_form("Item", item.item_code))
)
def validate_material_transfer(self):
customer_warehouse = frappe.get_cached_value(
"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
)
item_codes = []
for item in self.items:
if not frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item"):
continue
elif item.s_warehouse != customer_warehouse:
frappe.throw(
_("Row #{0}: For Customer Provided Item {1}, Source Warehouse must be {2}").format(
item.idx,
get_link_to_form("Item", item.item_code),
get_link_to_form("Warehouse", customer_warehouse),
)
)
elif item.item_code in item_codes:
frappe.throw(
_(
"Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process."
).format(
item.idx,
get_link_to_form("Item", item.item_code),
)
)
else:
work_order_items = frappe.get_all(
"Work Order Item",
{"parent": self.work_order, "docstatus": 1, "is_customer_provided_item": 1},
["item_code", "transferred_qty", "required_qty", "stock_reserved_qty"],
)
wo_item_dict = frappe._dict(
{
wo_item.item_code: frappe._dict(
{
"transferred_qty": wo_item.transferred_qty,
"required_qty": wo_item.required_qty,
"stock_reserved_qty": wo_item.stock_reserved_qty,
}
)
for wo_item in work_order_items
}
)
if wo_item := wo_item_dict.get(item.item_code):
if wo_item.transferred_qty + item.transfer_qty > max(
wo_item.required_qty, wo_item.stock_reserved_qty
):
frappe.throw(
_(
"Row #{0}: Overconsumption of Customer Provided Item {1} against Work Order {2} is not allowed in the Subcontracting Inward process."
).format(
item.idx,
get_link_to_form("Item", item.item_code),
get_link_to_form("Work Order", self.work_order),
)
)
else:
item_codes.append(item.item_code)
else:
frappe.throw(
_("Row #{0}: Customer Provided Item {1} is not a part of Work Order {2}").format(
item.idx,
get_link_to_form("Item", item.item_code),
get_link_to_form("Work Order", self.work_order),
)
)
def validate_manufacture(self):
skip_transfer, customer_warehouse, wip_warehouse = frappe.get_cached_value(
"Work Order",
self.work_order,
["skip_transfer", "source_warehouse", "wip_warehouse"],
)
warehouse = customer_warehouse if skip_transfer else wip_warehouse
if next(item for item in self.items if item.is_finished_item).t_warehouse != (
fg_warehouse := frappe.get_cached_value("Work Order", self.work_order, "fg_warehouse")
):
frappe.throw(
_(
"Target Warehouse for Finished Good must be same as Finished Good Warehouse {1} in Work Order {2} linked to the Subcontracting Inward Order."
).format(
get_link_to_form("Warehouse", fg_warehouse),
get_link_to_form("Work Order", self.work_order),
)
)
items = [
item
@@ -133,74 +243,133 @@ class SubcontractingInwardController:
and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
]
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
query = (
frappe.qb.from_(table)
.select(
table.rm_item_code,
(table.received_qty - table.returned_qty).as_("total_qty"),
table.consumed_qty,
table.name,
)
.where(
(table.docstatus == 1)
& (table.parent == self.subcontracting_inward_order)
& (table.main_item_code == frappe.get_cached_value("BOM", self.bom_no, "item"))
& (table.warehouse == warehouse)
& (table.rm_item_code.isin([item.item_code for item in items]))
)
customer_warehouse = frappe.get_cached_value(
"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
)
rm_item_dict = frappe._dict(
{
d.rm_item_code: frappe._dict(
{"name": d.name, "total_qty": d.total_qty, "qty": d.consumed_qty}
if frappe.get_cached_value("Work Order", self.work_order, "skip_transfer"):
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
query = (
frappe.qb.from_(table)
.select(
table.rm_item_code,
(table.received_qty - table.returned_qty).as_("total_qty"),
table.consumed_qty,
table.name,
)
for d in query.run(as_dict=True)
}
)
for item in items:
if rm := rm_item_dict.get(item.item_code):
if rm.qty + item.transfer_qty > rm.total_qty:
frappe.throw(
_(
"Row #{0}: Customer Provided Item {1} exceeds quantity available through Subcontracting Inward Order"
).format(item.idx, bold(item.item_code), item.transfer_qty)
)
elif item.s_warehouse != warehouse:
frappe.throw(
_("Row #{0}: For Customer Provided Item {1}, Source Warehouse must be {2}").format(
item.idx,
bold(item.item_code),
bold(warehouse),
.where(
(table.docstatus == 1)
& (table.parent == self.subcontracting_inward_order)
& (
table.reference_name
== frappe.get_cached_value(
"Work Order", self.work_order, "subcontracting_inward_order_item"
)
)
else:
frappe.throw(
_(
"Row #{0}: Customer Provided Item {1} is not a part of Subcontracting Inward Order {2}"
).format(
item.idx,
bold(item.item_code),
bold(self.subcontracting_inward_order),
)
& (table.rm_item_code.isin([item.item_code for item in items]))
)
)
rm_item_dict = frappe._dict(
{
d.rm_item_code: frappe._dict(
{"name": d.name, "total_qty": d.total_qty, "qty": d.consumed_qty}
)
for d in query.run(as_dict=True)
}
)
def validate_customer_provided_item_for_inward(self):
if self.subcontracting_inward_order:
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return"]:
for item in self.items:
if (item.is_finished_item or item.is_scrap_item) and item.valuation_rate == 0:
item.allow_zero_valuation_rate = 1
elif self.purpose == "Receive from Customer":
for item in self.items:
if not frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item"):
item_codes = []
for item in items:
if rm := rm_item_dict.get(item.item_code):
if rm.qty + item.transfer_qty > rm.total_qty:
frappe.throw(
_("Row #{0}: Item {1} is not a customer provided item.").format(
_(
"Row #{0}: Customer Provided Item {1} exceeds quantity available through Subcontracting Inward Order"
).format(item.idx, get_link_to_form("Item", item.item_code), item.transfer_qty)
)
elif item.s_warehouse != customer_warehouse:
frappe.throw(
_(
"Row #{0}: For Customer Provided Item {1}, Source Warehouse must be {2}"
).format(
item.idx,
get_link_to_form("Item", item.item_code),
get_link_to_form("Warehouse", customer_warehouse),
)
)
elif item.item_code in item_codes:
frappe.throw(
_(
"Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process."
).format(
item.idx,
get_link_to_form("Item", item.item_code),
)
)
else:
item_codes.append(item.item_code)
else:
frappe.throw(
_(
"Row #{0}: Customer Provided Item {1} is not a part of Subcontracting Inward Order {2}"
).format(
item.idx,
get_link_to_form("Item", item.item_code),
get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order),
)
)
else:
work_order_items = frappe.get_all(
"Work Order Item",
{"parent": self.work_order, "docstatus": 1, "is_customer_provided_item": 1},
["item_code", "transferred_qty", "consumed_qty"],
)
wo_item_dict = frappe._dict(
{
wo_item.item_code: frappe._dict(
{"transferred_qty": wo_item.transferred_qty, "consumed_qty": wo_item.consumed_qty}
)
for wo_item in work_order_items
}
)
item_codes = []
for item in items:
if wo_item := wo_item_dict.get(item.item_code):
if wo_item.consumed_qty + item.transfer_qty > wo_item.transferred_qty:
frappe.throw(
_(
"Row #{0}: Overconsumption of Customer Provided Item {1} against Work Order {2} is not allowed in the Subcontracting Inward process."
).format(
item.idx,
get_link_to_form("Item", item.item_code),
get_link_to_form("Work Order", self.work_order),
)
)
elif item.item_code in item_codes:
frappe.throw(
_(
"Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process."
).format(
item.idx,
get_link_to_form("Item", item.item_code),
)
)
else:
item_codes.append(item.item_code)
else:
frappe.throw(
_("Row #{0}: Customer Provided Item {1} is not a part of Work Order {2}").format(
item.idx,
get_link_to_form("Item", item.item_code),
get_link_to_form("Work Order", self.work_order),
)
)
def set_allow_zero_valuation_rate(self):
if self.subcontracting_inward_order:
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
for item in self.items:
if (item.is_finished_item or item.is_scrap_item) and item.valuation_rate == 0:
item.allow_zero_valuation_rate = 1
def validate_warehouse_(self):
if self.subcontracting_inward_order and self.purpose in [
@@ -222,13 +391,13 @@ class SubcontractingInwardController:
frappe.throw(
_(
"Row #{0}: Target Warehouse must be same as Customer Warehouse {1} from the linked Subcontracting Inward Order"
).format(item.idx, bold(customer_warehouse))
).format(item.idx, get_link_to_form("Warehouse", customer_warehouse))
)
else:
frappe.throw(
_(
"Row #{0}: Source Warehouse must be same as Customer Warehouse {1} from the linked Subcontracting Inward Order"
).format(item.idx, bold(customer_warehouse))
).format(item.idx, get_link_to_form("Warehouse", customer_warehouse))
)
def validate_serial_batch_for_return_or_delivery(self):
@@ -249,7 +418,10 @@ class SubcontractingInwardController:
frappe.throw(
_(
"Row #{0}: Serial No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Serial No(s)."
).format(item.idx, ", ".join([bold(sn) for sn in incorrect_serial_nos]))
).format(
item.idx,
", ".join([get_link_to_form("Serial No", sn) for sn in incorrect_serial_nos]),
)
)
if batch_list and (
incorrect_batch_nos := [bn for bn in batch_list if bn not in list(batch_nos.keys())]
@@ -257,7 +429,10 @@ class SubcontractingInwardController:
frappe.throw(
_(
"Row #{0}: Batch No(s) {1} is not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)."
).format(item.idx, ", ".join([bold(bn) for bn in incorrect_batch_nos]))
).format(
item.idx,
", ".join([get_link_to_form("Batch No", bn) for bn in incorrect_batch_nos]),
)
)
def get_serial_nos_and_batches_from_sres(self, scio_detail, only_pending=True):
@@ -302,7 +477,7 @@ class SubcontractingInwardController:
frappe.throw(
_(
"Row #{0}: Cannot cancel this Stock Entry as returned quantity cannot be greater than delivered quantity for Item {1} in the linked Subcontracting Inward Order"
).format(item.idx, bold(item.item_code))
).format(item.idx, get_link_to_form("Item", item.item_code))
)
def validate_delivery_on_save(self):
@@ -315,8 +490,8 @@ class SubcontractingInwardController:
frappe.throw(
_("Row #{0}: Item {1} is not a part of Subcontracting Inward Order {2}").format(
item.idx,
bold(item.item_code),
bold(self.subcontracting_inward_order),
get_link_to_form("Item", item.item_code),
get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order),
)
)
@@ -359,7 +534,7 @@ class SubcontractingInwardController:
"Row #{0}: Quantity of Item {1} cannot be more than {2} {3} against Subcontracting Inward Order {4}"
).format(
item.idx,
bold(item.item_code),
get_link_to_form("Item", item.item_code),
bold(max_allowed_qty),
bold(
frappe.get_cached_value(
@@ -370,7 +545,7 @@ class SubcontractingInwardController:
"stock_uom",
)
),
bold(self.subcontracting_inward_order),
get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order),
)
)
@@ -400,7 +575,6 @@ class SubcontractingInwardController:
& (table.voucher_type == "Subcontracting Inward Order")
& (table.voucher_no == self.subcontracting_inward_order)
& (table.voucher_detail_no == item.scio_detail)
& (table.warehouse == item.s_warehouse)
)
.orderby(table.creation)
)
@@ -522,7 +696,7 @@ class SubcontractingInwardController:
) < scio_rm_item.work_order_qty:
frappe.throw(
_("Row #{0}: Work Order exists against full or partial quantity of Item {1}").format(
item.idx, bold(item.item_code)
item.idx, get_link_to_form("Item", item.item_code)
)
)
@@ -561,7 +735,7 @@ class SubcontractingInwardController:
frappe.throw(
_(
"Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Scrap Item {1} produced cannot be less than quantity delivered."
).format(item.idx, bold(item.item_code))
).format(item.idx, get_link_to_form("Item", item.item_code))
)
else:
scio_rm_item = frappe.get_value(
@@ -570,7 +744,7 @@ class SubcontractingInwardController:
"docstatus": 1,
"rm_item_code": item.item_code,
"warehouse": item.s_warehouse,
"reference_name": fg_item_name, # if this field is set then the additional item is NOT customer provided
"is_customer_provided_item": 0,
"is_additional_item": 1,
},
["consumed_qty", "billed_qty", "returned_qty"],
@@ -582,7 +756,7 @@ class SubcontractingInwardController:
frappe.throw(
_(
"Row #{0}: Cannot cancel this Manufacturing Stock Entry as billed quantity of Item {1} cannot be greater than consumed quantity."
).format(item.idx, bold(item.item_code))
).format(item.idx, get_link_to_form("Item", item.item_code))
)
def update_inward_order_item(self):
@@ -638,7 +812,9 @@ class SubcontractingInwardController:
data = frappe._dict()
for item in self.items:
if item.scio_detail:
data[item.scio_detail] = item.transfer_qty if self._action == "submit" else -item.transfer_qty
data[item.scio_detail] = frappe._dict(
{"transfer_qty": item.transfer_qty, "rate": item.customer_provided_item_cost}
)
else:
scio_rm = frappe.new_doc(
"Subcontracting Inward Order Received Item",
@@ -657,9 +833,16 @@ class SubcontractingInwardController:
consumed_qty=0,
work_order_qty=0,
returned_qty=0,
rate=item.customer_provided_item_cost,
is_customer_provided_item=True,
is_additional_item=True,
reference_name=item.against_fg,
main_item_code=frappe.get_cached_value(
"Subcontracting Inward Order Item", item.against_fg, "item_code"
),
)
scio_rm.insert()
scio_rm.submit()
item.db_set("scio_detail", scio_rm.name)
if data:
@@ -670,39 +853,47 @@ class SubcontractingInwardController:
"name": ["in", list(data.keys())],
"docstatus": 1,
},
fields=["name", "required_qty", "received_qty"],
fields=["rate", "name", "required_qty", "received_qty"],
)
deleted_docs = []
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
case_expr = Case()
case_expr_qty, case_expr_rate = Case(), Case()
for d in result:
d.received_qty += data[d.name]
d.received_qty += (
data[d.name].transfer_qty if self._action == "submit" else -data[d.name].transfer_qty
)
d.rate += data[d.name].rate if self._action == "submit" else -data[d.name].rate
if not d.required_qty and not d.received_qty:
deleted_docs.append(d.name)
frappe.delete_doc("Subcontracting Inward Order Received Item", d.name)
else:
case_expr = case_expr.when(table.name == d.name, d.received_qty)
case_expr_qty = case_expr_qty.when(table.name == d.name, d.received_qty)
case_expr_rate = case_expr_rate.when(table.name == d.name, d.rate)
if len(list(set(data.keys()) - set(deleted_docs))) > 0:
frappe.qb.update(table).set(table.received_qty, case_expr).where(
(table.name.isin(list(set(data.keys()) - set(deleted_docs)))) & (table.docstatus == 1)
).run()
if final_list := list(set(data.keys()) - set(deleted_docs)):
frappe.qb.update(table).set(table.received_qty, case_expr_qty).set(
table.rate, case_expr_rate
).where((table.name.isin(final_list)) & (table.docstatus == 1)).run()
def update_inward_order_received_items_for_manufacture(self):
customer_warehouse = frappe.get_cached_value(
"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
)
items = [item for item in self.items if not item.is_finished_item and not item.is_scrap_item]
item_code_wh = frappe._dict(
{
(item.item_code, item.s_warehouse): item.transfer_qty
if self._action == "submit"
else -item.transfer_qty
(
item.item_code,
customer_warehouse
if frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
else item.s_warehouse,
): item.transfer_qty if self._action == "submit" else -item.transfer_qty
for item in items
}
)
item_codes, warehouses = zip(*list(item_code_wh.keys()), strict=True)
item_codes = list(item_codes)
warehouses = list(warehouses)
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
data = (
@@ -712,22 +903,21 @@ class SubcontractingInwardController:
table.rm_item_code,
table.is_customer_provided_item,
table.consumed_qty,
table.required_qty,
table.warehouse,
table.is_additional_item,
)
.where(
(table.docstatus == 1)
& (table.rm_item_code.isin(item_codes))
& ((table.warehouse.isin(warehouses)) | (table.warehouse.isnull()))
& (table.rm_item_code.isin(list(set(item_codes))))
& (
(table.warehouse.isin(list(set(warehouses)))) | (table.warehouse.isnull())
) # warehouse will always be null for non additional self procured raw materials
& (table.parent == self.subcontracting_inward_order)
& (
(
table.reference_name
== frappe.get_cached_value(
"Work Order", self.work_order, "subcontracting_inward_order_item"
)
table.reference_name
== frappe.get_cached_value(
"Work Order", self.work_order, "subcontracting_inward_order_item"
)
| (table.reference_name.isnull())
)
)
)
@@ -745,25 +935,26 @@ class SubcontractingInwardController:
used_item_wh.append((d.rm_item_code, d.warehouse))
qty = d.consumed_qty + item_code_wh[(d.rm_item_code, d.warehouse)]
if qty or d.is_customer_provided_item:
if qty or d.is_customer_provided_item or not d.is_additional_item:
case_expr = case_expr.when((table.name == d.name), qty)
else:
deleted_docs.append(d.name)
frappe.delete_doc("Subcontracting Inward Order Received Item", d.name)
final_name_list = list(set([d.name for d in data]) - set(deleted_docs))
if len(final_name_list) > 0:
if final_list := list(set([d.name for d in data]) - set(deleted_docs)):
frappe.qb.update(table).set(table.consumed_qty, case_expr).where(
(table.name.isin(final_name_list)) & (table.docstatus == 1)
(table.name.isin(final_list)) & (table.docstatus == 1)
).run()
main_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
for extra_item in [
item
for item in items
if (item.item_code, item.s_warehouse) not in [(d.rm_item_code, d.warehouse) for d in data]
if not frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
and (item.item_code, item.s_warehouse)
not in [(d.rm_item_code, d.warehouse) for d in data if not d.is_customer_provided_item]
]:
frappe.new_doc(
doc = frappe.new_doc(
"Subcontracting Inward Order Received Item",
parent=self.subcontracting_inward_order,
parenttype="Subcontracting Inward Order",
@@ -783,7 +974,9 @@ class SubcontractingInwardController:
consumed_qty=extra_item.transfer_qty,
warehouse=extra_item.s_warehouse,
is_additional_item=True,
).insert()
)
doc.insert()
doc.submit()
def update_inward_order_scrap_items(self):
if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
@@ -835,8 +1028,9 @@ class SubcontractingInwardController:
table.name == value.name, value.produced_qty + scrap_items.get(key)
)
final_list = list(set([v.name for v in scrap_item_dict.values()]) - set(deleted_docs))
if len(final_list) > 0:
if final_list := list(
set([v.name for v in scrap_item_dict.values()]) - set(deleted_docs)
):
frappe.qb.update(table).set(table.produced_qty, case_expr).where(
(table.name.isin(final_list)) & (table.docstatus == 1)
).run()
@@ -847,7 +1041,7 @@ class SubcontractingInwardController:
for item in scrap_items_list
if (item.item_code, item.t_warehouse) not in [(d.item_code, d.warehouse) for d in result]
]:
frappe.new_doc(
doc = frappe.new_doc(
"Subcontracting Inward Order Scrap Item",
parent=scio,
parenttype="Subcontracting Inward Order",
@@ -862,7 +1056,9 @@ class SubcontractingInwardController:
reference_name=frappe.get_value(
"Work Order", self.work_order, "subcontracting_inward_order_item"
),
).insert()
)
doc.insert()
doc.submit()
def cancel_stock_reservation_entries_for_inward(self):
if self.purpose == "Receive from Customer":
@@ -1022,3 +1218,17 @@ class SubcontractingInwardController:
)
update_subcontracting_inward_order_status(self.subcontracting_inward_order)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_fg_reference_names(doctype, txt, searchfield, start, page_len, filters):
return frappe.get_all(
"Subcontracting Inward Order Item",
limit_start=start,
limit_page_length=page_len,
filters={"parent": filters.get("parent"), "item_code": ("like", "%%%s%%" % txt), "docstatus": 1},
fields=["name", "item_code", "delivery_warehouse"],
as_list=True,
order_by="idx",
)

View File

@@ -652,7 +652,11 @@ frappe.ui.form.on("Work Order Item", {
required_qty: row.required_qty || 1,
item_name: r.message.item_name,
description: r.message.description,
source_warehouse: r.message.default_warehouse,
source_warehouse:
r.message.is_customer_provided_item &&
frm.doc.subcontracting_inward_order_item
? frm.doc.source_warehouse
: r.message.default_warehouse,
allow_alternative_item: r.message.allow_alternative_item,
include_item_in_manufacturing: r.message.include_item_in_manufacturing,
});

View File

@@ -185,8 +185,6 @@ class WorkOrder(Document):
if not self.subcontracting_inward_order:
self.validate_sales_order()
else:
self.validate_self_rm_warehouse()
self.set_default_warehouse()
self.validate_warehouse_belongs_to_company()
@@ -276,13 +274,100 @@ class WorkOrder(Document):
):
frappe.throw(
_(
"Source Warehouse {0} must be same as Customer Warehouse {1} in the Subcontracting Inward Order"
"Source Warehouse {0} must be same as Customer Warehouse {1} in the Subcontracting Inward Order."
).format(
frappe.bold(self.source_warehouse),
frappe.bold(rm_receipt_warehouse),
get_link_to_form("Warehouse", self.source_warehouse),
get_link_to_form("Warehouse", rm_receipt_warehouse),
)
)
if self.fg_warehouse != (
delivery_warehouse := frappe.get_cached_value(
"Subcontracting Inward Order Item",
self.subcontracting_inward_order_item,
"delivery_warehouse",
)
):
frappe.throw(
_(
"Target Warehouse {0} must be same as Delivery Warehouse {1} in the Subcontracting Inward Order Item."
).format(
get_link_to_form("Warehouse", self.fg_warehouse),
get_link_to_form(
"Warehouse",
delivery_warehouse,
),
)
)
possible_customer_provided_items = frappe.get_all(
"Subcontracting Inward Order Received Item",
{
"reference_name": self.subcontracting_inward_order_item,
"is_customer_provided_item": 1,
"docstatus": 1,
},
["rm_item_code", "received_qty", "returned_qty", "work_order_qty"],
)
item_codes = []
for item in self.required_items:
if item.is_customer_provided_item:
if item.source_warehouse != self.source_warehouse:
frappe.throw(
_(
"Row #{0}: Source Warehouse {1} for item {2} must be same as Source Warehouse {3} in the Work Order."
).format(
item.idx,
get_link_to_form("Warehouse", item.source_warehouse),
get_link_to_form("Item", item.item_code),
get_link_to_form("Warehouse", self.source_warehouse),
)
)
elif item.item_code in item_codes:
frappe.throw(
_("Row #{0}: Customer Provided Item {1} cannot be added multiple times.").format(
item.idx,
get_link_to_form("Item", item.item_code),
)
)
else:
row = next(
(i for i in possible_customer_provided_items if i.rm_item_code == item.item_code),
None,
)
if row:
if item.required_qty > row.received_qty - row.returned_qty - row.work_order_qty:
frappe.throw(
_(
"Row #{0}: Customer Provided Item {1} has insufficient quantity in the Subcontracting Inward Order. Available quantity is {2}."
).format(
item.idx,
get_link_to_form("Item", item.item_code),
frappe.bold(row.received_qty - row.returned_qty - row.work_order_qty),
)
)
else:
item_codes.append(item.item_code)
else:
frappe.throw(
_(
"Row #{0}: Customer Provided Item {1} does not exist in the Required Items table linked to the Subcontracting Inward Order."
).format(
item.idx,
get_link_to_form("Item", item.item_code),
)
)
elif frappe.get_cached_value("Warehouse", item.source_warehouse, "customer"):
frappe.throw(
_(
"Row #{0}: Source Warehouse {1} for item {2} cannot be a customer warehouse."
).format(
item.idx,
get_link_to_form("Warehouse", item.source_warehouse),
get_link_to_form("Item", item.item_code),
)
)
def set_warehouses(self):
for row in self.required_items:
if not row.source_warehouse:
@@ -356,15 +441,6 @@ class WorkOrder(Document):
else:
frappe.throw(_("Sales Order {0} is not valid").format(self.sales_order))
def validate_self_rm_warehouse(self):
for item in [item for item in self.required_items if not item.is_customer_provided_item]:
if frappe.get_cached_value("Warehouse", item.source_warehouse, "customer"):
frappe.throw(
_("Row #{0}: Source Warehouse {1} for item {2} cannot be a customer warehouse.").format(
item.idx, frappe.bold(item.source_warehouse), frappe.bold(item.item_code)
)
)
def check_sales_order_on_hold_or_close(self):
status = frappe.db.get_value("Sales Order", self.sales_order, "status")
if status in ("Closed", "On Hold"):
@@ -703,33 +779,40 @@ class WorkOrder(Document):
def set_qty_change(self):
if scio_item_name := self.get("subcontracting_inward_order_item"):
scio_rm_item_names = frappe.db.get_all(
"Subcontracting Inward Order Received Item",
filters={"reference_name": scio_item_name, "docstatus": 1, "is_customer_provided_item": 1},
pluck="name",
)
self.qty_change = frappe._dict()
data = frappe.get_all(
"Subcontracting Inward Order Received Item",
{"name": ["in", scio_rm_item_names]},
{"reference_name": scio_item_name, "docstatus": 1, "is_customer_provided_item": 1},
["rm_item_code", "required_qty as bom_qty", "work_order_qty", "received_qty"],
)
for d in data:
wo_item = next(
wo_item for wo_item in self.get("required_items") if wo_item.item_code == d.rm_item_code
(
wo_item
for wo_item in self.get("required_items")
if wo_item.item_code == d.rm_item_code
),
None,
)
if (
d.work_order_qty + (wo_item.required_qty if self._action == "submit" else 0)
) == d.bom_qty and d.received_qty > d.bom_qty:
wo_item
and (d.work_order_qty + (wo_item.required_qty if self._action == "submit" else 0))
== d.bom_qty
and d.received_qty > d.bom_qty
):
self.qty_change[wo_item.name] = d.received_qty - d.bom_qty
def update_subcontracting_inward_order_received_items(self):
if scio_item_name := self.get("subcontracting_inward_order_item"):
scio_rm_data = frappe.get_all(
"Subcontracting Inward Order Received Item",
filters={"reference_name": scio_item_name, "docstatus": 1},
filters={
"reference_name": scio_item_name,
"docstatus": 1,
"rm_item_code": ["in", [d.item_code for d in self.get("required_items")]],
},
fields=["name", "rm_item_code"],
)
@@ -1328,7 +1411,7 @@ class WorkOrder(Document):
frappe.msgprint(
_(
"Warning: Quantity exceeds maximum producible quantity based on quantity of raw materials received through the Subcontracting Inward Order {0}."
).format(frappe.bold(self.subcontracting_inward_order)),
).format(get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order)),
alert=True,
indicator="orange",
)
@@ -2179,14 +2262,13 @@ def make_stock_entry(
stock_entry.from_bom = 1
stock_entry.bom_no = work_order.bom_no
stock_entry.use_multi_level_bom = work_order.use_multi_level_bom
if purpose in ["Material Transfer for Manufacture", "Manufacture"]:
stock_entry.subcontracting_inward_order = work_order.subcontracting_inward_order
# accept 0 qty as well
stock_entry.fg_completed_qty = (
qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty))
)
if purpose == "Manufacture" and work_order.subcontracting_inward_order:
stock_entry.subcontracting_inward_order = work_order.subcontracting_inward_order
if work_order.bom_no:
stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required")

View File

@@ -151,6 +151,17 @@ frappe.ui.form.on("Stock Entry", {
if (!check_should_not_attach_bom_items(frm.doc.bom_no)) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
if (frm.doc.purpose == "Receive from Customer") {
frm.set_query("against_fg", "items", function () {
return {
query: "erpnext.controllers.subcontracting_inward_controller.get_fg_reference_names",
filters: {
parent: frm.doc.subcontracting_inward_order,
},
};
});
}
},
setup_quality_inspection: function (frm) {
@@ -854,6 +865,10 @@ frappe.ui.form.on("Stock Entry Detail", {
if (item.is_finished_item) {
frm.events.set_fg_completed_qty(frm);
}
if (frm.doc.purpose === "Receive from Customer") {
item.t_warehouse = frm.doc.items.find((item) => item.scio_detail).t_warehouse;
}
},
set_basic_rate_manually(frm, cdt, cdn) {
let row = locals[cdt][cdn];

View File

@@ -904,10 +904,9 @@ class StockEntry(StockController, SubcontractingInwardController):
if d.s_warehouse or d.set_basic_rate_manually:
continue
if d.allow_zero_valuation_rate and self.purpose != "Receive from Customer":
if d.allow_zero_valuation_rate and d.basic_rate and self.purpose != "Receive from Customer":
d.basic_rate = 0.0
items.append(d.item_code)
elif d.is_finished_item:
if self.purpose == "Manufacture":
d.basic_rate = self.get_basic_rate_for_manufactured_item(

View File

@@ -21,6 +21,7 @@
"is_scrap_item",
"quality_inspection",
"subcontracted_item",
"against_fg",
"section_break_8",
"description",
"column_break_10",
@@ -113,7 +114,8 @@
"label": "Target Warehouse",
"oldfieldname": "t_warehouse",
"oldfieldtype": "Link",
"options": "Warehouse"
"options": "Warehouse",
"read_only_depends_on": "eval:parent.purpose === \"Receive from Customer\""
},
{
"fieldname": "sec_break1",
@@ -641,6 +643,16 @@
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"depends_on": "eval:parent.purpose === \"Receive from Customer\" && !doc.scio_detail",
"fieldname": "against_fg",
"fieldtype": "Link",
"label": "Against Finished Good",
"mandatory_depends_on": "eval:parent.purpose === \"Receive from Customer\" && !doc.scio_detail",
"no_copy": 1,
"options": "Subcontracting Inward Order Item",
"set_only_once": 1
}
],
"grid_page_length": 50,
@@ -648,7 +660,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-10-14 14:10:38.373099",
"modified": "2025-10-16 11:50:50.573443",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@@ -16,6 +16,7 @@ class StockEntryDetail(Document):
actual_qty: DF.Float
additional_cost: DF.Currency
against_fg: DF.Link | None
against_stock_entry: DF.Link | None
allow_alternative_item: DF.Check
allow_zero_valuation_rate: DF.Check

View File

@@ -60,6 +60,14 @@ frappe.ui.form.on("Subcontracting Inward Order", {
};
});
frm.set_query("bom", "items", () => {
return {
filters: {
is_active: 1,
},
};
});
frm.set_query("set_delivery_warehouse", () => {
return {
filters: {

View File

@@ -275,10 +275,13 @@ class SubcontractingInwardOrder(SubcontractingController):
d.precision("qty"),
)
for item in self.get("received_items")
if item.reference_name == d.name and item.is_customer_provided_item
if item.reference_name == d.name and item.is_customer_provided_item and item.required_qty
]
)
qty = int(qty) if frappe.get_cached_value("UOM", d.stock_uom, "must_be_whole_number") else qty
qty = min(
int(qty) if frappe.get_cached_value("UOM", d.stock_uom, "must_be_whole_number") else qty,
d.qty - d.produced_qty,
)
item_details.update({"qty": qty, "max_producible_qty": qty})
item_list.append(item_details)

View File

@@ -66,6 +66,7 @@ class IntegrationTestSubcontractingInwardOrder(IntegrationTestCase):
"transfer_qty": 5,
"uom": "Nos",
"conversion_factor": 1,
"against_fg": scio.items[0].name,
},
)
rm_in.submit()

View File

@@ -46,6 +46,7 @@
"in_global_search": 1,
"label": "Item Name",
"print_hide": 1,
"read_only": 1,
"reqd": 1
},
{
@@ -185,7 +186,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-10-14 10:29:29.256455",
"modified": "2025-10-18 18:04:04.204651",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Inward Order Item",

View File

@@ -22,7 +22,9 @@
"column_break_16",
"consumed_qty",
"work_order_qty",
"returned_qty"
"returned_qty",
"section_break_yhve",
"rate"
],
"fields": [
{
@@ -32,7 +34,8 @@
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"read_only": 1
"read_only": 1,
"reqd": 1
},
{
"columns": 2,
@@ -70,7 +73,8 @@
"fieldname": "reference_name",
"fieldtype": "Data",
"label": "Reference Name",
"read_only": 1
"read_only": 1,
"reqd": 1
},
{
"fieldname": "section_break_13",
@@ -116,7 +120,6 @@
},
{
"default": "0",
"depends_on": "eval:doc.returned_qty",
"fieldname": "returned_qty",
"fieldtype": "Float",
"label": "Returned Qty",
@@ -128,7 +131,7 @@
{
"allow_on_submit": 1,
"default": "0",
"depends_on": "eval:doc.work_order_qty",
"depends_on": "eval:!(!doc.is_customer_provided_item && doc.is_additional_item)",
"fieldname": "work_order_qty",
"fieldtype": "Float",
"label": "Work Order Qty",
@@ -146,7 +149,6 @@
"reqd": 1
},
{
"depends_on": "eval:!doc.is_customer_provided_item",
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
@@ -166,11 +168,27 @@
},
{
"default": "0",
"depends_on": "eval:!doc.bom_detail_no",
"fieldname": "is_additional_item",
"fieldtype": "Check",
"label": "Is Additional Item",
"read_only": 1
},
{
"depends_on": "eval:doc.is_customer_provided_item",
"fieldname": "section_break_yhve",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "rate",
"fieldtype": "Currency",
"label": "Rate",
"mandatory_depends_on": "eval:doc.is_customer_provided_item",
"no_copy": 1,
"non_negative": 1,
"options": "Company:company:default_currency",
"read_only": 1,
"read_only_depends_on": "eval:doc.is_customer_provided_item"
}
],
"grid_page_length": 50,
@@ -178,7 +196,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-10-14 10:18:58.905093",
"modified": "2025-10-21 23:44:18.302327",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Inward Order Received Item",

View File

@@ -19,12 +19,13 @@ class SubcontractingInwardOrderReceivedItem(Document):
consumed_qty: DF.Float
is_additional_item: DF.Check
is_customer_provided_item: DF.Check
main_item_code: DF.Link | None
main_item_code: DF.Link
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
rate: DF.Currency
received_qty: DF.Float
reference_name: DF.Data | None
reference_name: DF.Data
required_qty: DF.Float
returned_qty: DF.Float
rm_item_code: DF.Link