diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index b396b27da7a..b1ce539bc3d 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -436,24 +436,6 @@ class BuyingController(SubcontractingController): # validate rate with ref PR - def validate_rejected_warehouse(self): - for item in self.get("items"): - if flt(item.rejected_qty) and not item.rejected_warehouse: - if self.rejected_warehouse: - item.rejected_warehouse = self.rejected_warehouse - - if not item.rejected_warehouse: - frappe.throw( - _("Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1}").format( - item.idx, item.item_code - ) - ) - - if item.get("rejected_warehouse") and (item.get("rejected_warehouse") == item.get("warehouse")): - frappe.throw( - _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx) - ) - # validate accepted and rejected qty def validate_accepted_rejected_qty(self): for d in self.get("items"): diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 6633f4f6eba..d4270a76d4d 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -55,6 +55,23 @@ class SubcontractingController(StockController): else: super(SubcontractingController, self).validate() + def validate_rejected_warehouse(self): + for item in self.get("items"): + if flt(item.rejected_qty) and not item.rejected_warehouse: + if self.rejected_warehouse: + item.rejected_warehouse = self.rejected_warehouse + else: + frappe.throw( + _("Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1}").format( + item.idx, item.item_code + ) + ) + + if item.get("rejected_warehouse") and (item.get("rejected_warehouse") == item.get("warehouse")): + frappe.throw( + _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx) + ) + def remove_empty_rows(self): for key in ["service_items", "items", "supplied_items"]: if self.get(key): @@ -80,23 +97,27 @@ 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 not is_sub_contracted_item: - frappe.throw( - _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, 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 item.bom: - bom = frappe.get_doc("BOM", item.bom) - if not bom.is_active: - frappe.throw( - _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name) - ) - if bom.item != item.item_code: - frappe.throw( - _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name) - ) + if item.bom: + is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"]) + + if not is_active: + frappe.throw( + _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name) + ) + if bom_item != item.item_code: + frappe.throw( + _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name) + ) + else: + frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name)) else: - frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name)) + item.bom = None def __get_data_before_save(self): item_dict = {} @@ -874,19 +895,24 @@ class SubcontractingController(StockController): if self.total_additional_costs: if self.distribute_additional_costs_based_on == "Amount": - total_amt = sum(flt(item.amount) for item in self.get("items")) + total_amt = sum( + flt(item.amount) for item in self.get("items") if not item.get("is_scrap_item") + ) for item in self.items: - item.additional_cost_per_qty = ( - (item.amount * self.total_additional_costs) / total_amt - ) / item.qty + if not item.get("is_scrap_item"): + item.additional_cost_per_qty = ( + (item.amount * self.total_additional_costs) / total_amt + ) / item.qty else: - total_qty = sum(flt(item.qty) for item in self.get("items")) + total_qty = sum(flt(item.qty) for item in self.get("items") if not item.get("is_scrap_item")) additional_cost_per_qty = self.total_additional_costs / total_qty for item in self.items: - item.additional_cost_per_qty = additional_cost_per_qty + if not item.get("is_scrap_item"): + item.additional_cost_per_qty = additional_cost_per_qty else: for item in self.items: - item.additional_cost_per_qty = 0 + if not item.get("is_scrap_item"): + item.additional_cost_per_qty = 0 @frappe.whitelist() def get_current_stock(self): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 0b14d4d9f50..b7b344584cf 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -203,7 +203,10 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None): { "Subcontracting Order": { "doctype": "Subcontracting Receipt", - "field_map": {"supplier_warehouse": "supplier_warehouse"}, + "field_map": { + "supplier_warehouse": "supplier_warehouse", + "set_warehouse": "set_warehouse", + }, "validation": { "docstatus": ["=", 1], }, diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 6a2983faaaf..22fdc13cc1d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -591,6 +591,13 @@ def create_subcontracting_order(**args): for idx, val in enumerate(sco.items): val.warehouse = warehouses[idx] + warehouses = set() + for item in sco.items: + warehouses.add(item.warehouse) + + if len(warehouses) == 1: + sco.set_warehouse = list(warehouses)[0] + if not args.do_not_save: sco.insert() if not args.do_not_submit: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index e374077a784..acf95530526 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -22,7 +22,7 @@ frappe.ui.form.on('Subcontracting Receipt', { to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), company: frm.doc.company, show_cancelled_entries: frm.doc.docstatus === 2 - }; + } frappe.set_route('query-report', 'Stock Ledger'); }, __('View')); @@ -34,7 +34,7 @@ frappe.ui.form.on('Subcontracting Receipt', { company: frm.doc.company, group_by: 'Group by Voucher (Consolidated)', show_cancelled_entries: frm.doc.docstatus === 2 - }; + } frappe.set_route('query-report', 'General Ledger'); }, __('View')); } @@ -94,7 +94,7 @@ frappe.ui.form.on('Subcontracting Receipt', { company: frm.doc.company, is_group: 0 } - }; + } }); frm.set_query('rejected_warehouse', () => { @@ -103,7 +103,7 @@ frappe.ui.form.on('Subcontracting Receipt', { company: frm.doc.company, is_group: 0 } - }; + } }); frm.set_query('supplier_warehouse', () => { @@ -112,7 +112,7 @@ frappe.ui.form.on('Subcontracting Receipt', { company: frm.doc.company, is_group: 0 } - }; + } }); frm.set_query('warehouse', 'items', () => ({ @@ -129,10 +129,12 @@ frappe.ui.form.on('Subcontracting Receipt', { } })); - frm.set_query('expense_account', 'items', () => ({ + frm.set_query('expense_account', 'items', () => { + return { query: 'erpnext.controllers.queries.get_expense_account', filters: { 'company': frm.doc.company } - })); + } + }); frm.set_query('batch_no', 'items', (doc, cdt, cdn) => { var row = locals[cdt][cdn]; @@ -140,7 +142,7 @@ frappe.ui.form.on('Subcontracting Receipt', { filters: { item: row.item_code } - }; + } }); frm.set_query('batch_no', 'supplied_items', (doc, cdt, cdn) => { @@ -149,7 +151,7 @@ frappe.ui.form.on('Subcontracting Receipt', { filters: { item: row.rm_item_code } - }; + } }); frm.set_query('serial_and_batch_bundle', 'supplied_items', (doc, cdt, cdn) => { @@ -171,7 +173,7 @@ frappe.ui.form.on('Subcontracting Receipt', { 'item_code': row.doc.rm_item_code, 'voucher_type': frm.doc.doctype, } - }; + } } let batch_no_field = frm.get_docfield('items', 'batch_no'); @@ -180,7 +182,7 @@ frappe.ui.form.on('Subcontracting Receipt', { return { 'item': row.doc.item_code } - }; + } } }, @@ -190,15 +192,37 @@ frappe.ui.form.on('Subcontracting Receipt', { transaction_controller.setup_quality_inspection(); } }, + + get_scrap_items: (frm) => { + frappe.call({ + doc: frm.doc, + method: 'get_scrap_items', + args: { + recalculate_rate: true + }, + freeze: true, + freeze_message: __('Getting Scrap Items'), + callback: (r) => { + if (!r.exc) { + frm.refresh(); + } + } + }); + }, }); frappe.ui.form.on('Landed Cost Taxes and Charges', { amount: (frm, cdt, cdn) => { + set_missing_values(frm); frm.events.set_base_amount(frm, cdt, cdn); }, expense_account: (frm, cdt, cdn) => { frm.events.set_account_currency(frm, cdt, cdn); + }, + + additional_costs_remove: (frm) => { + set_missing_values(frm); } }); @@ -214,6 +238,16 @@ frappe.ui.form.on('Subcontracting Receipt Item', { rate(frm) { set_missing_values(frm); }, + + recalculate_rate(frm) { + if (frm.doc.recalculate_rate) { + set_missing_values(frm); + } + }, + + items_remove: (frm) => { + set_missing_values(frm); + }, }); frappe.ui.form.on('Subcontracting Receipt Supplied Item', { @@ -225,7 +259,7 @@ frappe.ui.form.on('Subcontracting Receipt Supplied Item', { let set_warehouse_in_children = (child_table, warehouse_field, warehouse) => { let transaction_controller = new erpnext.TransactionController(); transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse); -}; +} let set_missing_values = (frm) => { frappe.call({ @@ -235,4 +269,4 @@ let set_missing_values = (frm) => { if (!r.exc) frm.refresh(); }, }); -}; \ No newline at end of file +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index 4b3cc8365c5..8be1c1ba975 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -40,6 +40,7 @@ "col_break_warehouse", "supplier_warehouse", "items_section", + "get_scrap_items", "items", "section_break0", "total_qty", @@ -285,7 +286,7 @@ "reqd": 1 }, { - "depends_on": "supplied_items", + "depends_on": "eval: (!doc.__islocal && doc.docstatus == 0 && doc.supplied_items)", "fieldname": "get_current_stock", "fieldtype": "Button", "label": "Get Current Stock", @@ -626,12 +627,19 @@ "fieldtype": "Check", "label": "Edit Posting Date and Time", "print_hide": 1 + }, + { + "depends_on": "eval: (!doc.__islocal && doc.docstatus == 0)", + "fieldname": "get_scrap_items", + "fieldtype": "Button", + "label": "Get Scrap Items", + "options": "get_scrap_items" } ], "in_create": 1, "is_submittable": 1, "links": [], - "modified": "2023-07-06 18:43:16.171842", + "modified": "2023-08-26 10:52:04.050829", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index afe1b6068dc..8a12e3bcd03 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -8,6 +8,7 @@ from frappe.utils import cint, flt, getdate, nowdate import erpnext from erpnext.accounts.utils import get_account_currency from erpnext.controllers.subcontracting_controller import SubcontractingController +from erpnext.stock.stock_ledger import get_valuation_rate class SubcontractingReceipt(SubcontractingController): @@ -36,33 +37,6 @@ class SubcontractingReceipt(SubcontractingController): ), ) - def update_status_updater_args(self): - if cint(self.is_return): - self.status_updater.extend( - [ - { - "source_dt": "Subcontracting Receipt Item", - "target_dt": "Subcontracting Order Item", - "join_field": "subcontracting_order_item", - "target_field": "returned_qty", - "source_field": "-1 * qty", - "extra_cond": """ and exists (select name from `tabSubcontracting Receipt` - where name=`tabSubcontracting Receipt Item`.parent and is_return=1)""", - }, - { - "source_dt": "Subcontracting Receipt Item", - "target_dt": "Subcontracting Receipt Item", - "join_field": "subcontracting_receipt_item", - "target_field": "returned_qty", - "target_parent_dt": "Subcontracting Receipt", - "target_parent_field": "per_returned", - "target_ref_field": "received_qty", - "source_field": "-1 * received_qty", - "percent_join_field_parent": "return_against", - }, - ] - ) - def before_validate(self): super(SubcontractingReceipt, self).before_validate() self.validate_items_qty() @@ -71,15 +45,8 @@ class SubcontractingReceipt(SubcontractingController): self.set_items_expense_account() def validate(self): - if ( - frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") - == "BOM" - ): - self.supplied_items = [] - super(SubcontractingReceipt, self).validate() - self.set_missing_values() + self.reset_supplied_items() self.validate_posting_time() - self.validate_rejected_warehouse() if not self.get("is_return"): self.validate_inspection() @@ -87,15 +54,22 @@ class SubcontractingReceipt(SubcontractingController): if getdate(self.posting_date) > getdate(nowdate()): frappe.throw(_("Posting Date cannot be future date")) + super(SubcontractingReceipt, self).validate() + + if self.is_new() and self.get("_action") == "save" and not frappe.flags.in_test: + self.get_scrap_items() + + self.set_missing_values() + + if self.get("_action") == "submit": + self.validate_scrap_items() + self.validate_accepted_warehouse() + self.validate_rejected_warehouse() + self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.get_current_stock() - def on_update(self): - for table_field in ["items", "supplied_items"]: - if self.get(table_field): - self.set_serial_and_batch_bundle(table_field) - def on_submit(self): self.validate_available_qty_for_consumption() self.update_status_updater_args() @@ -107,6 +81,11 @@ class SubcontractingReceipt(SubcontractingController): self.repost_future_sle_and_gle() self.update_status() + def on_update(self): + for table_field in ["items", "supplied_items"]: + if self.get(table_field): + self.set_serial_and_batch_bundle(table_field) + def on_cancel(self): self.ignore_linked_doctypes = ( "GL Entry", @@ -124,108 +103,6 @@ class SubcontractingReceipt(SubcontractingController): self.set_subcontracting_order_status() self.update_status() - @frappe.whitelist() - def set_missing_values(self): - self.calculate_additional_costs() - self.calculate_supplied_items_qty_and_amount() - self.calculate_items_qty_and_amount() - - def set_available_qty_for_consumption(self): - supplied_items_details = {} - - sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item") - for item in self.get("items"): - supplied_items = ( - frappe.qb.from_(sco_supplied_item) - .select( - sco_supplied_item.rm_item_code, - sco_supplied_item.reference_name, - (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"), - ) - .where( - (sco_supplied_item.parent == item.subcontracting_order) - & (sco_supplied_item.main_item_code == item.item_code) - & (sco_supplied_item.reference_name == item.subcontracting_order_item) - ) - ).run(as_dict=True) - - if supplied_items: - supplied_items_details[item.name] = {} - - for supplied_item in supplied_items: - supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty - else: - for item in self.get("supplied_items"): - item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get( - item.rm_item_code, 0 - ) - - def calculate_supplied_items_qty_and_amount(self): - for item in self.get("supplied_items") or []: - item.amount = item.rate * item.consumed_qty - - self.set_available_qty_for_consumption() - - def calculate_items_qty_and_amount(self): - rm_supp_cost = {} - for item in self.get("supplied_items") or []: - if item.reference_name in rm_supp_cost: - rm_supp_cost[item.reference_name] += item.amount - else: - rm_supp_cost[item.reference_name] = item.amount - - total_qty = total_amount = 0 - for item in self.items: - if item.qty and item.name in rm_supp_cost: - item.rm_supp_cost = rm_supp_cost[item.name] - item.rm_cost_per_qty = item.rm_supp_cost / item.qty - rm_supp_cost.pop(item.name) - - if item.recalculate_rate: - item.rate = ( - flt(item.rm_cost_per_qty) + flt(item.service_cost_per_qty) + flt(item.additional_cost_per_qty) - ) - - item.received_qty = item.qty + flt(item.rejected_qty) - item.amount = item.qty * item.rate - total_qty += item.qty - total_amount += item.amount - else: - self.total_qty = total_qty - self.total = total_amount - - def validate_rejected_warehouse(self): - for item in self.items: - if flt(item.rejected_qty) and not item.rejected_warehouse: - if self.rejected_warehouse: - item.rejected_warehouse = self.rejected_warehouse - - if not item.rejected_warehouse: - frappe.throw( - _("Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1}").format( - item.idx, item.item_code - ) - ) - - if item.get("rejected_warehouse") and (item.get("rejected_warehouse") == item.get("warehouse")): - frappe.throw( - _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx) - ) - - def validate_available_qty_for_consumption(self): - for item in self.get("supplied_items"): - precision = item.precision("consumed_qty") - if ( - item.available_qty_for_consumption - and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0 - ): - msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)} - must be less than or equal to Available Qty For Consumption - {flt(item.available_qty_for_consumption, precision)} - in Consumed Items Table.""" - - frappe.throw(_(msg)) - def validate_items_qty(self): for item in self.items: if not (item.qty or item.rejected_qty): @@ -267,6 +144,236 @@ class SubcontractingReceipt(SubcontractingController): if not item.expense_account: item.expense_account = expense_account + def reset_supplied_items(self): + if ( + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") + == "BOM" + ): + self.supplied_items = [] + + @frappe.whitelist() + def get_scrap_items(self, recalculate_rate=False): + self.remove_scrap_items() + + for item in list(self.items): + if item.bom: + bom = frappe.get_doc("BOM", item.bom) + for scrap_item in bom.scrap_items: + qty = flt(item.qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity)) + rate = ( + get_valuation_rate( + scrap_item.item_code, + self.set_warehouse, + self.doctype, + self.name, + currency=erpnext.get_company_currency(self.company), + company=self.company, + ) + or scrap_item.rate + ) + self.append( + "items", + { + "is_scrap_item": 1, + "reference_name": item.name, + "item_code": scrap_item.item_code, + "item_name": scrap_item.item_name, + "qty": qty, + "stock_uom": scrap_item.stock_uom, + "recalculate_rate": 0, + "rate": rate, + "rm_cost_per_qty": 0, + "service_cost_per_qty": 0, + "additional_cost_per_qty": 0, + "scrap_cost_per_qty": 0, + "amount": qty * rate, + "warehouse": self.set_warehouse, + "rejected_warehouse": self.rejected_warehouse, + }, + ) + + if recalculate_rate: + self.calculate_additional_costs() + self.calculate_items_qty_and_amount() + + def remove_scrap_items(self, recalculate_rate=False): + for item in list(self.items): + if item.is_scrap_item: + self.remove(item) + else: + item.scrap_cost_per_qty = 0 + + if recalculate_rate: + self.calculate_items_qty_and_amount() + + @frappe.whitelist() + def set_missing_values(self): + self.set_available_qty_for_consumption() + self.calculate_additional_costs() + self.calculate_items_qty_and_amount() + + def set_available_qty_for_consumption(self): + supplied_items_details = {} + + sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item") + for item in self.get("items"): + supplied_items = ( + frappe.qb.from_(sco_supplied_item) + .select( + sco_supplied_item.rm_item_code, + sco_supplied_item.reference_name, + (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"), + ) + .where( + (sco_supplied_item.parent == item.subcontracting_order) + & (sco_supplied_item.main_item_code == item.item_code) + & (sco_supplied_item.reference_name == item.subcontracting_order_item) + ) + ).run(as_dict=True) + + if supplied_items: + supplied_items_details[item.name] = {} + + for supplied_item in supplied_items: + supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty + else: + for item in self.get("supplied_items"): + item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get( + item.rm_item_code, 0 + ) + + def calculate_items_qty_and_amount(self): + rm_cost_map = {} + for item in self.get("supplied_items") or []: + item.amount = flt(item.consumed_qty) * flt(item.rate) + + if item.reference_name in rm_cost_map: + rm_cost_map[item.reference_name] += item.amount + else: + rm_cost_map[item.reference_name] = item.amount + + scrap_cost_map = {} + for item in self.get("items") or []: + if item.is_scrap_item: + item.amount = flt(item.qty) * flt(item.rate) + + if item.reference_name in scrap_cost_map: + scrap_cost_map[item.reference_name] += item.amount + else: + scrap_cost_map[item.reference_name] = item.amount + + total_qty = total_amount = 0 + for item in self.get("items") or []: + if not item.is_scrap_item: + if item.qty: + if item.name in rm_cost_map: + item.rm_supp_cost = rm_cost_map[item.name] + item.rm_cost_per_qty = item.rm_supp_cost / item.qty + rm_cost_map.pop(item.name) + + if item.name in scrap_cost_map: + item.scrap_cost_per_qty = scrap_cost_map[item.name] / item.qty + scrap_cost_map.pop(item.name) + else: + item.scrap_cost_per_qty = 0 + + if item.recalculate_rate: + item.rate = ( + flt(item.rm_cost_per_qty) + + flt(item.service_cost_per_qty) + + flt(item.additional_cost_per_qty) + - flt(item.scrap_cost_per_qty) + ) + + item.received_qty = flt(item.qty) + flt(item.rejected_qty) + item.amount = flt(item.qty) * flt(item.rate) + + total_qty += flt(item.qty) + total_amount += item.amount + else: + self.total_qty = total_qty + self.total = total_amount + + def validate_scrap_items(self): + for item in self.items: + if item.is_scrap_item: + if not item.qty: + frappe.throw( + _("Row #{0}: Scrap Item Qty cannot be zero").format(item.idx), + ) + + if item.rejected_qty: + frappe.throw( + _("Row #{0}: Rejected Qty cannot be set for Scrap Item {1}.").format( + item.idx, frappe.bold(item.item_code) + ), + ) + + if not item.reference_name: + frappe.throw( + _("Row #{0}: Finished Good reference is mandatory for Scrap Item {1}.").format( + item.idx, frappe.bold(item.item_code) + ), + ) + + def validate_accepted_warehouse(self): + for item in self.get("items"): + if flt(item.qty) and not item.warehouse: + if self.set_warehouse: + item.warehouse = self.set_warehouse + else: + frappe.throw( + _("Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}").format( + item.idx, item.item_code + ) + ) + + if item.get("warehouse") and (item.get("warehouse") == item.get("rejected_warehouse")): + frappe.throw( + _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx) + ) + + def validate_available_qty_for_consumption(self): + for item in self.get("supplied_items"): + precision = item.precision("consumed_qty") + if ( + item.available_qty_for_consumption + and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0 + ): + msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)} + must be less than or equal to Available Qty For Consumption + {flt(item.available_qty_for_consumption, precision)} + in Consumed Items Table.""" + + frappe.throw(_(msg)) + + def update_status_updater_args(self): + if cint(self.is_return): + self.status_updater.extend( + [ + { + "source_dt": "Subcontracting Receipt Item", + "target_dt": "Subcontracting Order Item", + "join_field": "subcontracting_order_item", + "target_field": "returned_qty", + "source_field": "-1 * qty", + "extra_cond": """ and exists (select name from `tabSubcontracting Receipt` + where name=`tabSubcontracting Receipt Item`.parent and is_return=1)""", + }, + { + "source_dt": "Subcontracting Receipt Item", + "target_dt": "Subcontracting Receipt Item", + "join_field": "subcontracting_receipt_item", + "target_field": "returned_qty", + "target_parent_dt": "Subcontracting Receipt", + "target_parent_field": "per_returned", + "target_ref_field": "received_qty", + "source_field": "-1 * received_qty", + "percent_join_field_parent": "return_against", + }, + ] + ) + def update_status(self, status=None, update_modified=False): if not status: if self.docstatus == 0: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index a170527e2d0..1828f6960fa 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -23,6 +23,7 @@ from erpnext.controllers.tests.test_subcontracting_controller import ( make_subcontracted_items, set_backflush_based_on, ) +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -507,8 +508,6 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate) def test_subcontracting_receipt_raw_material_rate(self): - from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - # Step - 1: Set Backflush Based On as "BOM" set_backflush_based_on("BOM") @@ -625,6 +624,77 @@ class TestSubcontractingReceipt(FrappeTestCase): # ValidationError should not be raised as `Inspection Required before Purchase` is disabled scr2.submit() + def test_scrap_items_for_subcontracting_receipt(self): + set_backflush_based_on("BOM") + + fg_item = "Subcontracted Item SA1" + + # Create Raw Materials + raw_materials = [ + make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name, + make_item(properties={"is_stock_item": 1, "valuation_rate": 200}).name, + ] + + # Create Scrap Items + scrap_item_1 = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name + scrap_item_2 = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name + scrap_items = [scrap_item_1, scrap_item_2] + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 10, + }, + ] + + # Create BOM with Scrap Items + bom = make_bom( + item=fg_item, raw_materials=raw_materials, rate=100, currency="INR", do_not_submit=True + ) + for idx, item in enumerate(bom.items): + item.qty = 1 * (idx + 1) + for idx, item in enumerate(scrap_items): + bom.append( + "scrap_items", + { + "item_code": item, + "stock_qty": 1 * (idx + 1), + "rate": 10 * (idx + 1), + }, + ) + bom.save() + bom.submit() + + # Create PO and SCO + sco = get_subcontracting_order(service_items=service_items) + + # Inward Raw Materials + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + # Transfer RM's to Subcontractor + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + # Create Subcontracting Receipt + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.get_scrap_items() + + # Test - 1: Scrap Items should be fetched from BOM in items table with `is_scrap_item` = 1 + scr_scrap_items = set([item.item_code for item in scr.items if item.is_scrap_item]) + self.assertEqual(len(scr.items), 3) # 1 FG Item + 2 Scrap Items + self.assertEqual(scr_scrap_items, set(scrap_items)) + + scr.submit() + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index d72878061c1..c036390ba37 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -10,6 +10,7 @@ "item_code", "column_break_2", "item_name", + "is_scrap_item", "section_break_4", "description", "brand", @@ -24,8 +25,6 @@ "col_break2", "stock_uom", "conversion_factor", - "tracking_section", - "col_break_tracking_section", "rate_and_amount", "rate", "amount", @@ -34,18 +33,20 @@ "rm_cost_per_qty", "service_cost_per_qty", "additional_cost_per_qty", + "scrap_cost_per_qty", "rm_supp_cost", "warehouse_and_reference", "warehouse", - "rejected_warehouse", "subcontracting_order", - "column_break_40", - "schedule_date", - "quality_inspection", "subcontracting_order_item", "subcontracting_receipt_item", - "section_break_45", + "column_break_40", + "rejected_warehouse", "bom", + "quality_inspection", + "schedule_date", + "reference_name", + "section_break_45", "serial_and_batch_bundle", "serial_no", "col_break5", @@ -85,12 +86,13 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, "label": "Item Name", - "print_hide": 1, - "reqd": 1 + "print_hide": 1 }, { "collapsible": 1, @@ -99,11 +101,12 @@ "label": "Description" }, { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, "fieldname": "description", "fieldtype": "Text Editor", "label": "Description", "print_width": "300px", - "reqd": 1, "width": "300px" }, { @@ -157,6 +160,7 @@ "no_copy": 1, "print_hide": 1, "print_width": "100px", + "read_only_depends_on": "eval: doc.is_scrap_item", "width": "100px" }, { @@ -214,6 +218,8 @@ "fieldtype": "Column Break" }, { + "default": "0", + "depends_on": "eval: !doc.is_scrap_item", "fieldname": "rm_cost_per_qty", "fieldtype": "Currency", "label": "Raw Material Cost Per Qty", @@ -221,6 +227,8 @@ "read_only": 1 }, { + "default": "0", + "depends_on": "eval: !doc.is_scrap_item", "fieldname": "service_cost_per_qty", "fieldtype": "Currency", "label": "Service Cost Per Qty", @@ -229,6 +237,7 @@ }, { "default": "0", + "depends_on": "eval: !doc.is_scrap_item", "fieldname": "additional_cost_per_qty", "fieldtype": "Currency", "label": "Additional Cost Per Qty", @@ -260,6 +269,7 @@ "options": "Warehouse", "print_hide": 1, "print_width": "100px", + "read_only_depends_on": "eval: doc.is_scrap_item", "width": "100px" }, { @@ -295,7 +305,8 @@ }, { "fieldname": "section_break_45", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Serial and Batch Details" }, { "depends_on": "eval:!doc.is_fixed_asset", @@ -321,7 +332,8 @@ "fieldtype": "Small Text", "label": "Rejected Serial No", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "subcontracting_order_item", @@ -345,7 +357,8 @@ "label": "BOM", "no_copy": 1, "options": "BOM", - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval: doc.is_scrap_item" }, { "fetch_from": "item_code.brand", @@ -410,14 +423,6 @@ "fieldname": "image_column", "fieldtype": "Column Break" }, - { - "fieldname": "tracking_section", - "fieldtype": "Section Break" - }, - { - "fieldname": "col_break_tracking_section", - "fieldtype": "Column Break" - }, { "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", @@ -456,6 +461,7 @@ "print_hide": 1 }, { + "default": "0", "depends_on": "returned_qty", "fieldname": "returned_qty", "fieldtype": "Float", @@ -471,9 +477,11 @@ }, { "default": "1", + "depends_on": "eval: !doc.is_scrap_item", "fieldname": "recalculate_rate", "fieldtype": "Check", - "label": "Recalculate Rate" + "label": "Recalculate Rate", + "read_only_depends_on": "eval: doc.is_scrap_item" }, { "fieldname": "serial_and_batch_bundle", @@ -490,12 +498,40 @@ "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "default": "0", + "depends_on": "eval: !doc.bom", + "fieldname": "is_scrap_item", + "fieldtype": "Check", + "label": "Is Scrap Item", + "no_copy": 1, + "print_hide": 1, + "read_only_depends_on": "eval: doc.bom" + }, + { + "default": "0", + "depends_on": "eval: !doc.is_scrap_item", + "fieldname": "scrap_cost_per_qty", + "fieldtype": "Float", + "label": "Scrap Cost Per Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Data", + "hidden": 1, + "label": "Reference Name", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-06 18:43:45.599761", + "modified": "2023-08-25 20:09:03.069417", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item",