From 559c95c8a8cb2b41b71c1a86372175fb9fd665d9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 13 Jun 2026 17:41:48 +0530 Subject: [PATCH 1/3] fix: pemission for whitelist functions (cherry picked from commit dd56e805126be2691df457c3fa53721d49995673) # Conflicts: # erpnext/accounts/doctype/bank_clearance/bank_clearance.py # erpnext/manufacturing/doctype/bom_creator/bom_creator.py # erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py --- .../doctype/bank_clearance/bank_clearance.py | 20 + .../repost_accounting_ledger.py | 3 +- erpnext/accounts/party.py | 3 +- erpnext/crm/doctype/lead/lead.py | 3 +- .../crm/doctype/opportunity/opportunity.py | 6 +- .../doctype/bom_creator/bom_creator.py | 51 ++ .../doctype/workstation/workstation.py | 4 +- .../transaction_deletion_record.py | 2 + .../doctype/delivery_trip/delivery_trip.py | 8 +- erpnext/stock/doctype/shipment/shipment.py | 4 +- .../incorrect_serial_and_batch_bundle.py | 5 +- .../subcontracting_inward_order.py | 559 ++++++++++++++++++ .../subcontracting_order.py | 4 +- erpnext/support/doctype/issue/issue.py | 8 +- 14 files changed, 664 insertions(+), 16 deletions(-) create mode 100644 erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 36cbb321518..93243ce07b1 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -90,7 +90,27 @@ class BankClearance(Document): @frappe.whitelist() def update_clearance_date(self): +<<<<<<< HEAD clearance_date_updated = False +======= + self.check_permission("write") + invalid_document = [] + invalid_cheque_date = [] + entries_to_update = [] + + def validate_entry(d): + is_valid = True + if not d.payment_document: + invalid_document.append(str(d.idx)) + is_valid = False + + if d.clearance_date and d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date): + invalid_cheque_date.append(str(d.idx)) + is_valid = False + + return is_valid + +>>>>>>> dd56e80512 (fix: pemission for whitelist functions) for d in self.get("payment_entries"): if d.clearance_date: if not d.payment_document: diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index a8e468415c1..f11aeda4383 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -154,12 +154,13 @@ class RepostAccountingLedger(Document): @frappe.whitelist() -def start_repost(account_repost_doc=str) -> None: +def start_repost(account_repost_doc: str | None = None) -> None: from erpnext.accounts.general_ledger import make_reverse_gl_entries frappe.flags.through_repost_accounting_ledger = True if account_repost_doc: repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc) + repost_doc.check_permission("write") if repost_doc.docstatus == 1: # Prevent repost on invoices with deferred accounting diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 5c0860b4fd3..14146a7872e 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -511,7 +511,8 @@ def get_party_advance_account(party_type, party, company): @frappe.whitelist() -def get_party_bank_account(party_type, party): +def get_party_bank_account(party_type: str, party: str): + frappe.has_permission("Bank Account", "read", throw=True) return frappe.db.get_value("Bank Account", {"party_type": party_type, "party": party, "is_default": 1}) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index f0f492191fb..0f901b5eff6 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -471,7 +471,7 @@ def get_lead_details(lead, posting_date=None, company=None, doctype=None): @frappe.whitelist() -def make_lead_from_communication(communication, ignore_communication_links=False): +def make_lead_from_communication(communication: str, ignore_communication_links: bool = False): """raise a issue from email""" doc = frappe.get_doc("Communication", communication) @@ -490,7 +490,6 @@ def make_lead_from_communication(communication, ignore_communication_links=False } ) lead.flags.ignore_mandatory = True - lead.flags.ignore_permissions = True lead.insert() lead_name = lead.name diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 91d60c924bf..b68ab28ee82 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -522,7 +522,9 @@ def auto_close_opportunity(): @frappe.whitelist() -def make_opportunity_from_communication(communication, company, ignore_communication_links=False): +def make_opportunity_from_communication( + communication: str, company: str, ignore_communication_links: bool = False +): from erpnext.crm.doctype.lead.lead import make_lead_from_communication doc = frappe.get_doc("Communication", communication) @@ -540,7 +542,7 @@ def make_opportunity_from_communication(communication, company, ignore_communica "opportunity_from": opportunity_from, "party_name": lead, } - ).insert(ignore_permissions=True) + ).insert() link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 71ba95cc8a6..3e31d972558 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -358,14 +358,53 @@ class BOMCreator(Document): production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name @frappe.whitelist() +<<<<<<< HEAD def get_default_bom(self, item_code: str) -> str: self.check_permission("read") +======= + def edit_bom_creator(self, docname: str, data: str | dict): + if not frappe.db.exists("BOM Creator Item", {"parent": self.name, "name": docname}): + frappe.throw(_("BOM Creator Item with name {0} does not exist").format(docname)) + + if isinstance(data, str): + data = frappe.parse_json(data) + + updated = False + for row in self.items: + if row.name == docname: + for key, value in data.items(): + if key in BOM_ITEM_FIELDS and row.get(key) != value: + row.set(key, value) + updated = True + break + + if updated: + self.set_rate_for_items() + self.save() + + frappe.msgprint(_("Updated successfully"), alert=True) + + return self + + def has_operations(self): + for row in self.items: + if row.operation: + return True + + return False + + @frappe.whitelist() + def get_default_bom(self, item_code) -> str: +>>>>>>> dd56e80512 (fix: pemission for whitelist functions) return frappe.get_cached_value("Item", item_code, "default_bom") @frappe.whitelist() def add_item(self, **kwargs): +<<<<<<< HEAD self.check_permission("write") +======= +>>>>>>> dd56e80512 (fix: pemission for whitelist functions) if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) @@ -400,8 +439,11 @@ class BOMCreator(Document): @frappe.whitelist() def add_sub_assembly(self, **kwargs): +<<<<<<< HEAD self.check_permission("write") +======= +>>>>>>> dd56e80512 (fix: pemission for whitelist functions) if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) @@ -437,6 +479,12 @@ class BOMCreator(Document): parent_row_no = item_row.idx name = "" else: +<<<<<<< HEAD +======= + if sbool(kwargs.phantom): + parent_row = next(item for item in self.items if item.name == kwargs.fg_reference_id) + parent_row.is_phantom_item = 1 +>>>>>>> dd56e80512 (fix: pemission for whitelist functions) parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id) for row in bom_item.get("items"): @@ -464,8 +512,11 @@ class BOMCreator(Document): @frappe.whitelist() def delete_node(self, **kwargs): +<<<<<<< HEAD self.check_permission("write") +======= +>>>>>>> dd56e80512 (fix: pemission for whitelist functions) if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 216194f68ff..c45fd7852f4 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -176,7 +176,7 @@ class Workstation(Document): doc.check_permission("write") doc.append("time_logs", {"from_time": from_time, "employee": employee}) - doc.save(ignore_permissions=True) + doc.save() return doc @@ -191,7 +191,7 @@ class Workstation(Document): row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60 row.completed_qty = qty - doc.save(ignore_permissions=True) + doc.save() doc.submit() return doc diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 9fa32c9d5ec..919dd29b040 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -209,6 +209,8 @@ class TransactionDeletionRecord(Document): @frappe.whitelist() def start_deletion_tasks(self): + self.check_permission("write") + # This method is the entry point for the chain of events that follow self.db_set("status", "Running") self.enqueue_task(task="Delete Bins") diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 58f393df5d0..9e5185dc0a0 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -335,7 +335,9 @@ def get_default_address(out, name): @frappe.whitelist() -def get_contact_display(contact): +def get_contact_display(contact: str): + frappe.has_permission("Contact", "read", doc=contact, throw=True) + contact_info = frappe.db.get_value( "Contact", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1 ) @@ -436,7 +438,9 @@ def get_attachments(delivery_stop): @frappe.whitelist() -def get_driver_email(driver): +def get_driver_email(driver: str): + frappe.has_permission("Driver", "read", doc=driver, throw=True) + employee = frappe.db.get_value("Driver", driver, "employee") email = frappe.db.get_value("Employee", employee, "prefered_email") return {"email": email} diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index 880f6b5e1c1..e1fb199c10b 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -123,7 +123,9 @@ def get_contact_name(ref_doctype, docname): @frappe.whitelist() -def get_company_contact(user): +def get_company_contact(user: str): + frappe.has_permission("User", "read", throw=True) + contact = frappe.db.get_value( "User", user, diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py index 0b27d697a4d..2a9640bab9e 100644 --- a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py @@ -134,12 +134,15 @@ def get_linked_cancelled_sabb(filters): @frappe.whitelist() -def fix_sabb_entries(selected_rows): +def fix_sabb_entries(selected_rows: str | list): + frappe.has_permission("Serial and Batch Bundle", "write", throw=True) + if isinstance(selected_rows, str): selected_rows = frappe.parse_json(selected_rows) for row in selected_rows: doc = frappe.get_doc("Serial and Batch Bundle", row.get("name")) + doc.check_permission("write") if doc.is_cancelled == 0 and not frappe.db.get_value( "Stock Ledger Entry", {"serial_and_batch_bundle": doc.name, "is_cancelled": 0}, diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py new file mode 100644 index 00000000000..79f2ed33ed2 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py @@ -0,0 +1,559 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe.utils import comma_and, flt, get_link_to_form + +from erpnext.buying.utils import check_on_hold_or_closed_status +from erpnext.controllers.subcontracting_controller import SubcontractingController + + +class SubcontractingInwardOrder(SubcontractingController): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.subcontracting.doctype.subcontracting_inward_order_item.subcontracting_inward_order_item import ( + SubcontractingInwardOrderItem, + ) + from erpnext.subcontracting.doctype.subcontracting_inward_order_received_item.subcontracting_inward_order_received_item import ( + SubcontractingInwardOrderReceivedItem, + ) + from erpnext.subcontracting.doctype.subcontracting_inward_order_secondary_item.subcontracting_inward_order_secondary_item import ( + SubcontractingInwardOrderSecondaryItem, + ) + from erpnext.subcontracting.doctype.subcontracting_inward_order_service_item.subcontracting_inward_order_service_item import ( + SubcontractingInwardOrderServiceItem, + ) + + amended_from: DF.Link | None + company: DF.Link + currency: DF.Link | None + customer: DF.Link + customer_name: DF.Data + customer_warehouse: DF.Link + items: DF.Table[SubcontractingInwardOrderItem] + naming_series: DF.Literal["SCI-ORD-.YYYY.-"] + per_delivered: DF.Percent + per_process_loss: DF.Percent + per_produced: DF.Percent + per_raw_material_received: DF.Percent + per_raw_material_returned: DF.Percent + per_returned: DF.Percent + received_items: DF.Table[SubcontractingInwardOrderReceivedItem] + sales_order: DF.Link + secondary_items: DF.Table[SubcontractingInwardOrderSecondaryItem] + service_items: DF.Table[SubcontractingInwardOrderServiceItem] + set_delivery_warehouse: DF.Link | None + status: DF.Literal[ + "Draft", "Open", "Ongoing", "Produced", "Delivered", "Returned", "Cancelled", "Closed" + ] + title: DF.Data | None + transaction_date: DF.Date + # end: auto-generated types + + pass + + def validate(self): + super().validate() + self.set_is_customer_provided_item() + self.validate_customer_provided_items() + self.validate_customer_warehouse() + self.validate_service_items() + self.set_missing_values() + + def on_submit(self): + self.update_status() + self.update_subcontracted_quantity_in_so() + + def on_cancel(self): + self.update_status() + self.update_subcontracted_quantity_in_so() + + def update_status(self, status=None, update_modified=True): + if self.status == "Closed" and self.status != status: + check_on_hold_or_closed_status("Sales Order", self.sales_order) + + total_to_be_received = total_received = total_rm_returned = 0 + for rm in self.get("received_items"): + if rm.get("is_customer_provided_item"): + total_to_be_received += flt(rm.required_qty) + total_received += flt(rm.received_qty) + total_rm_returned += flt(rm.returned_qty) + + total_to_be_produced = total_produced = total_process_loss = total_delivered = total_fg_returned = 0 + for item in self.get("items"): + total_to_be_produced += flt(item.qty) + total_produced += flt(item.produced_qty) + total_process_loss += flt(item.process_loss_qty) + total_delivered += flt(item.delivered_qty) + total_fg_returned += flt(item.returned_qty) + + per_raw_material_received = flt(total_received / total_to_be_received * 100, 2) + per_raw_material_returned = flt(total_rm_returned / total_received * 100, 2) if total_received else 0 + per_produced = flt(total_produced / total_to_be_produced * 100, 2) + per_process_loss = flt(total_process_loss / total_produced * 100, 2) if total_produced else 0 + per_delivered = flt(total_delivered / total_to_be_produced * 100, 2) + per_returned = flt(total_fg_returned / total_delivered * 100, 2) if total_delivered else 0 + + self.db_set("per_raw_material_received", per_raw_material_received, update_modified=update_modified) + self.db_set("per_raw_material_returned", per_raw_material_returned, update_modified=update_modified) + self.db_set("per_produced", per_produced, update_modified=update_modified) + self.db_set("per_process_loss", per_process_loss, update_modified=update_modified) + self.db_set("per_delivered", per_delivered, update_modified=update_modified) + self.db_set("per_returned", per_returned, update_modified=update_modified) + + if self.docstatus >= 1 and not status: + if self.docstatus == 1: + if self.status == "Draft": + status = "Open" + elif self.per_returned == 100: + status = "Returned" + elif self.per_delivered == 100: + status = "Delivered" + elif self.per_produced == 100: + status = "Produced" + elif self.per_raw_material_received > 0: + status = "Ongoing" + else: + status = "Open" + elif self.docstatus == 2: + status = "Cancelled" + + if status and self.status != status: + self.db_set("status", status, update_modified=update_modified) + + def update_subcontracted_quantity_in_so(self): + for service_item in self.service_items: + doc = frappe.get_doc("Sales Order Item", service_item.sales_order_item) + doc.subcontracted_qty = ( + (doc.subcontracted_qty + service_item.qty) + if self._action == "submit" + else (doc.subcontracted_qty - service_item.qty) + ) + doc.save() + + def validate_customer_warehouse(self): + if frappe.get_cached_value("Warehouse", self.customer_warehouse, "customer") != self.customer: + frappe.throw( + _("Customer Warehouse {0} does not belong to Customer {1}.").format( + frappe.bold(self.customer_warehouse), frappe.bold(self.customer) + ) + ) + + def validate_service_items(self): + sales_order_items = [item.sales_order_item for item in self.items] + self.service_items = [ + service_item + for service_item in self.service_items + if service_item.sales_order_item in sales_order_items + ] + + for service_item in self.service_items: + item = next(item for item in self.items if item.sales_order_item == service_item.sales_order_item) + service_item.qty = item.qty * item.subcontracting_conversion_factor + service_item.fg_item_qty = item.qty + service_item.amount = service_item.qty * service_item.rate + + def populate_items_table(self): + items = [] + + for si in self.service_items: + if si.fg_item: + item = frappe.get_doc("Item", si.fg_item) + + so_item = frappe.get_doc("Sales Order Item", si.sales_order_item) + available_qty = so_item.stock_qty - so_item.subcontracted_qty + + if available_qty == 0: + continue + + si.required_qty = available_qty + conversion_factor = so_item.stock_qty / so_item.fg_item_qty + si.fg_item_qty = flt( + available_qty / conversion_factor, frappe.get_precision("Sales Order Item", "qty") + ) + si.amount = available_qty * si.rate + + bom = ( + frappe.db.get_value( + "Subcontracting BOM", + {"finished_good": item.name, "is_active": 1}, + "finished_good_bom", + ) + or item.default_bom + ) + + items.append( + { + "item_code": item.name, + "item_name": item.item_name, + "expected_delivery_date": frappe.get_cached_value( + "Sales Order Item", si.sales_order_item, "delivery_date" + ), + "description": item.description, + "qty": si.fg_item_qty, + "subcontracting_conversion_factor": conversion_factor, + "stock_uom": item.stock_uom, + "bom": bom, + "sales_order_item": si.sales_order_item, + } + ) + else: + frappe.throw( + _("Please select Finished Good Item for Service Item {0}").format( + si.item_name or si.item_code + ) + ) + + if items: + for item in items: + self.append("items", item) + + def validate_customer_provided_items(self): + """Check if atleast one raw material is customer provided""" + for item in self.get("items"): + raw_materials = [rm for rm in self.get("received_items") if rm.main_item_code == item.item_code] + if not any([rm.is_customer_provided_item for rm in raw_materials]): + frappe.throw( + _( + "Atleast one raw material for Finished Good Item {0} should be customer provided." + ).format(frappe.bold(item.item_code)) + ) + + def set_is_customer_provided_item(self): + for item in self.get("received_items"): + item.is_customer_provided_item = frappe.get_cached_value( + "Item", item.rm_item_code, "is_customer_provided_item" + ) + + @frappe.whitelist() + def make_work_order(self): + """Create Work Order from Subcontracting Inward Order.""" + wo_list = [] + + for item in self.get_production_items(): + work_order = self.create_work_order(item) + if work_order: + wo_list.append(work_order) + + self.show_list_created_message("Work Order", wo_list) + + if not wo_list: + frappe.msgprint(_("No Work Orders were created")) + + return wo_list + + def get_production_items(self): + item_list = [] + + for d in self.items: + if d.produced_qty >= d.qty: + continue + + item_details = { + "production_item": d.item_code, + "use_multi_level_bom": d.include_exploded_items, + "subcontracting_inward_order": self.name, + "bom_no": d.bom, + "stock_uom": d.stock_uom, + "company": self.company, + "project": frappe.get_cached_value("Sales Order", self.sales_order, "project"), + "source_warehouse": self.customer_warehouse, + "subcontracting_inward_order_item": d.name, + "reserve_stock": 1, + "fg_warehouse": d.delivery_warehouse, + } + + qty = min( + [ + flt( + (item.received_qty - item.returned_qty - item.work_order_qty) + / flt(item.required_qty / d.qty, d.precision("qty")), + d.precision("qty"), + ) + for item in self.get("received_items") + if item.reference_name == d.name and item.is_customer_provided_item and item.required_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) + + return item_list + + def create_work_order(self, item): + from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError + + if flt(item.get("qty")) <= 0: + return + + wo = frappe.new_doc("Work Order") + wo.update(item) + + wo.set_work_order_operations() + wo.set_required_items() + + try: + wo.flags.ignore_mandatory = True + wo.flags.ignore_validate = True + wo.insert() + return wo.name + except OverProductionError: + pass + + def show_list_created_message(self, doctype, doc_list=None): + if not doc_list: + return + + frappe.flags.mute_messages = False + if doc_list: + doc_list = [get_link_to_form(doctype, p) for p in doc_list] + frappe.msgprint(_("{0} created").format(comma_and(doc_list))) + + @frappe.whitelist() + def make_rm_stock_entry_inward(self, target_doc=None): + def calculate_qty_as_per_bom(rm_item): + data = frappe.get_value( + "Subcontracting Inward Order Item", + {"name": rm_item.reference_name}, + ["process_loss_qty", "include_exploded_items"], + as_dict=True, + ) + stock_qty = frappe.get_value( + "BOM Explosion Item" if data.include_exploded_items else "BOM Item", + {"name": rm_item.bom_detail_no}, + "stock_qty", + ) + qty = flt( + stock_qty * data.process_loss_qty, + frappe.get_precision("Subcontracting Inward Order Received Item", "required_qty"), + ) + return rm_item.required_qty - rm_item.received_qty + rm_item.returned_qty + qty + + if target_doc and target_doc.get("items"): + target_doc.items = [] + + stock_entry = get_mapped_doc( + "Subcontracting Inward Order", + self.name, + { + "Subcontracting Inward Order": { + "doctype": "Stock Entry", + "validation": { + "docstatus": ["=", 1], + }, + }, + }, + target_doc, + ignore_child_tables=True, + ) + + stock_entry.purpose = "Receive from Customer" + stock_entry.subcontracting_inward_order = self.name + + stock_entry.set_stock_entry_type() + + for rm_item in self.received_items: + if not rm_item.required_qty or not rm_item.is_customer_provided_item: + continue + + items_dict = { + rm_item.get("rm_item_code"): { + "scio_detail": rm_item.get("name"), + "qty": calculate_qty_as_per_bom(rm_item), + "to_warehouse": rm_item.get("warehouse"), + "stock_uom": rm_item.get("stock_uom"), + } + } + + stock_entry.add_to_stock_entry_detail(items_dict) + + if target_doc: + return stock_entry + else: + return stock_entry.as_dict() + + @frappe.whitelist() + def make_rm_return(self, target_doc=None): + if target_doc and target_doc.get("items"): + target_doc.items = [] + + stock_entry = get_mapped_doc( + "Subcontracting Inward Order", + self.name, + { + "Subcontracting Inward Order": { + "doctype": "Stock Entry", + "validation": { + "docstatus": ["=", 1], + }, + }, + }, + target_doc, + ignore_child_tables=True, + ) + + stock_entry.purpose = "Return Raw Material to Customer" + stock_entry.set_stock_entry_type() + stock_entry.subcontracting_inward_order = self.name + + for rm_item in self.received_items: + items_dict = { + rm_item.get("rm_item_code"): { + "scio_detail": rm_item.get("name"), + "qty": rm_item.received_qty - rm_item.work_order_qty - rm_item.returned_qty, + "from_warehouse": rm_item.get("warehouse"), + "stock_uom": rm_item.get("stock_uom"), + } + } + + stock_entry.add_to_stock_entry_detail(items_dict) + + if target_doc: + return stock_entry + else: + return stock_entry.as_dict() + + @frappe.whitelist() + def make_subcontracting_delivery(self, target_doc=None): + if target_doc and target_doc.get("items"): + target_doc.items = [] + + stock_entry = get_mapped_doc( + "Subcontracting Inward Order", + self.name, + { + "Subcontracting Inward Order": { + "doctype": "Stock Entry", + "validation": { + "docstatus": ["=", 1], + }, + }, + }, + target_doc, + ignore_child_tables=True, + ) + + stock_entry.purpose = "Subcontracting Delivery" + stock_entry.set_stock_entry_type() + stock_entry.subcontracting_inward_order = self.name + scio_details = [] + + allow_over = frappe.get_single_value("Selling Settings", "allow_delivery_of_overproduced_qty") + for fg_item in self.items: + qty = ( + fg_item.produced_qty + if allow_over + else min(fg_item.qty, fg_item.produced_qty) - fg_item.delivered_qty + ) + if qty < 0: + continue + + scio_details.append(fg_item.name) + items_dict = { + fg_item.item_code: { + "qty": qty, + "from_warehouse": fg_item.delivery_warehouse, + "stock_uom": fg_item.stock_uom, + "scio_detail": fg_item.name, + "is_finished_item": 1, + } + } + + stock_entry.add_to_stock_entry_detail(items_dict) + + if ( + frappe.get_single_value("Selling Settings", "deliver_secondary_items") + and self.secondary_items + and scio_details + ): + secondary_items = [ + secondary_item + for secondary_item in self.secondary_items + if secondary_item.reference_name in scio_details + ] + for secondary_item in secondary_items: + qty = secondary_item.produced_qty - secondary_item.delivered_qty + if qty > 0: + items_dict = { + secondary_item.item_code: { + "qty": secondary_item.produced_qty - secondary_item.delivered_qty, + "from_warehouse": secondary_item.warehouse, + "stock_uom": secondary_item.stock_uom, + "scio_detail": secondary_item.name, + "type": secondary_item.type, + } + } + + stock_entry.add_to_stock_entry_detail(items_dict) + + if target_doc: + return stock_entry + else: + return stock_entry.as_dict() + + @frappe.whitelist() + def make_subcontracting_return(self, target_doc=None): + if target_doc and target_doc.get("items"): + target_doc.items = [] + + stock_entry = get_mapped_doc( + "Subcontracting Inward Order", + self.name, + { + "Subcontracting Inward Order": { + "doctype": "Stock Entry", + "validation": { + "docstatus": ["=", 1], + }, + "field_map": {"name": "subcontracting_inward_order"}, + }, + }, + target_doc, + ignore_child_tables=True, + ) + + stock_entry.purpose = "Subcontracting Return" + stock_entry.set_stock_entry_type() + + for fg_item in self.items: + qty = fg_item.delivered_qty - fg_item.returned_qty + if qty < 0: + continue + + items_dict = { + fg_item.item_code: { + "qty": qty, + "stock_uom": fg_item.stock_uom, + "scio_detail": fg_item.name, + "is_finished_item": 1, + } + } + + stock_entry.add_to_stock_entry_detail(items_dict) + + if target_doc: + return stock_entry + else: + return stock_entry.as_dict() + + +@frappe.whitelist() +def update_subcontracting_inward_order_status(scio: str | Document, status: str | None = None): + if isinstance(scio, str): + scio = frappe.get_doc("Subcontracting Inward Order", scio) + + scio.check_permission("write") + scio.update_status(status) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index aaf0ec87c23..7a8d4d62124 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -3,6 +3,7 @@ import frappe from frappe import _ +from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.utils import flt @@ -364,8 +365,9 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None): @frappe.whitelist() -def update_subcontracting_order_status(sco, status=None): +def update_subcontracting_order_status(sco: str | Document, status: str | None = None): if isinstance(sco, str): sco = frappe.get_doc("Subcontracting Order", sco) + sco.check_permission("write") sco.update_status(status) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index faa12bd5419..c35b76cf37d 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -118,7 +118,9 @@ class Issue(Document): communication.save() @frappe.whitelist() - def split_issue(self, subject, communication_id): + def split_issue(self, subject: str, communication_id: str): + self.check_permission("write") + # Bug: Pressing enter doesn't send subject from copy import deepcopy @@ -274,7 +276,7 @@ def make_task(source_name, target_doc=None): @frappe.whitelist() -def make_issue_from_communication(communication, ignore_communication_links=False): +def make_issue_from_communication(communication: str, ignore_communication_links: bool = False): """raise a issue from email""" doc = frappe.get_doc("Communication", communication) @@ -286,7 +288,7 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals "raised_by": doc.sender or "", "raised_by_phone": doc.phone_no or "", } - ).insert(ignore_permissions=True) + ).insert() link_communication_to_document(doc, "Issue", issue.name, ignore_communication_links) From 46b3e0c385658e1292f1c7c01bfd3fd356f923ad Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sat, 13 Jun 2026 18:43:12 +0530 Subject: [PATCH 2/3] fix: clearance date update with permission checks Refactor update_clearance_date to check permissions for payment documents before processing entries. --- .../doctype/bank_clearance/bank_clearance.py | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 93243ce07b1..1467400b598 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -90,27 +90,14 @@ class BankClearance(Document): @frappe.whitelist() def update_clearance_date(self): -<<<<<<< HEAD - clearance_date_updated = False -======= - self.check_permission("write") - invalid_document = [] - invalid_cheque_date = [] - entries_to_update = [] + payment_docs = [] + for d in self.get("payment_entries"): + if d.payment_document not in payment_docs: + payment_docs.append(d.payment_document) - def validate_entry(d): - is_valid = True - if not d.payment_document: - invalid_document.append(str(d.idx)) - is_valid = False + for doctype in payment_docs: + frappe.has_permission(doctype, "write", throw=True) - if d.clearance_date and d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date): - invalid_cheque_date.append(str(d.idx)) - is_valid = False - - return is_valid - ->>>>>>> dd56e80512 (fix: pemission for whitelist functions) for d in self.get("payment_entries"): if d.clearance_date: if not d.payment_document: From 08d9b8275daa1763dfc0bb05e56d4929a522b43c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sat, 13 Jun 2026 18:45:02 +0530 Subject: [PATCH 3/3] fix: BOM Creator by removing redundant permissions Removed unnecessary permission checks for whitelist functions and cleaned up the code. --- .../doctype/bom_creator/bom_creator.py | 59 -- .../subcontracting_inward_order.py | 559 ------------------ 2 files changed, 618 deletions(-) delete mode 100644 erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 3e31d972558..fd32d29fb50 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -288,8 +288,6 @@ class BOMCreator(Document): @frappe.whitelist() def edit_qty(self, docname: str, qty: float): - self.check_permission("write") - if not frappe.db.exists("BOM Creator Item", {"name": docname, "parent": self.name}): frappe.throw(_("BOM Creator Item {0} does not exist").format(docname)) @@ -358,53 +356,12 @@ class BOMCreator(Document): production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name @frappe.whitelist() -<<<<<<< HEAD def get_default_bom(self, item_code: str) -> str: self.check_permission("read") -======= - def edit_bom_creator(self, docname: str, data: str | dict): - if not frappe.db.exists("BOM Creator Item", {"parent": self.name, "name": docname}): - frappe.throw(_("BOM Creator Item with name {0} does not exist").format(docname)) - - if isinstance(data, str): - data = frappe.parse_json(data) - - updated = False - for row in self.items: - if row.name == docname: - for key, value in data.items(): - if key in BOM_ITEM_FIELDS and row.get(key) != value: - row.set(key, value) - updated = True - break - - if updated: - self.set_rate_for_items() - self.save() - - frappe.msgprint(_("Updated successfully"), alert=True) - - return self - - def has_operations(self): - for row in self.items: - if row.operation: - return True - - return False - - @frappe.whitelist() - def get_default_bom(self, item_code) -> str: ->>>>>>> dd56e80512 (fix: pemission for whitelist functions) return frappe.get_cached_value("Item", item_code, "default_bom") @frappe.whitelist() def add_item(self, **kwargs): -<<<<<<< HEAD - self.check_permission("write") - -======= ->>>>>>> dd56e80512 (fix: pemission for whitelist functions) if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) @@ -439,11 +396,6 @@ class BOMCreator(Document): @frappe.whitelist() def add_sub_assembly(self, **kwargs): -<<<<<<< HEAD - self.check_permission("write") - -======= ->>>>>>> dd56e80512 (fix: pemission for whitelist functions) if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) @@ -479,12 +431,6 @@ class BOMCreator(Document): parent_row_no = item_row.idx name = "" else: -<<<<<<< HEAD -======= - if sbool(kwargs.phantom): - parent_row = next(item for item in self.items if item.name == kwargs.fg_reference_id) - parent_row.is_phantom_item = 1 ->>>>>>> dd56e80512 (fix: pemission for whitelist functions) parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id) for row in bom_item.get("items"): @@ -512,11 +458,6 @@ class BOMCreator(Document): @frappe.whitelist() def delete_node(self, **kwargs): -<<<<<<< HEAD - self.check_permission("write") - -======= ->>>>>>> dd56e80512 (fix: pemission for whitelist functions) if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py deleted file mode 100644 index 79f2ed33ed2..00000000000 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py +++ /dev/null @@ -1,559 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.model.mapper import get_mapped_doc -from frappe.utils import comma_and, flt, get_link_to_form - -from erpnext.buying.utils import check_on_hold_or_closed_status -from erpnext.controllers.subcontracting_controller import SubcontractingController - - -class SubcontractingInwardOrder(SubcontractingController): - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.subcontracting.doctype.subcontracting_inward_order_item.subcontracting_inward_order_item import ( - SubcontractingInwardOrderItem, - ) - from erpnext.subcontracting.doctype.subcontracting_inward_order_received_item.subcontracting_inward_order_received_item import ( - SubcontractingInwardOrderReceivedItem, - ) - from erpnext.subcontracting.doctype.subcontracting_inward_order_secondary_item.subcontracting_inward_order_secondary_item import ( - SubcontractingInwardOrderSecondaryItem, - ) - from erpnext.subcontracting.doctype.subcontracting_inward_order_service_item.subcontracting_inward_order_service_item import ( - SubcontractingInwardOrderServiceItem, - ) - - amended_from: DF.Link | None - company: DF.Link - currency: DF.Link | None - customer: DF.Link - customer_name: DF.Data - customer_warehouse: DF.Link - items: DF.Table[SubcontractingInwardOrderItem] - naming_series: DF.Literal["SCI-ORD-.YYYY.-"] - per_delivered: DF.Percent - per_process_loss: DF.Percent - per_produced: DF.Percent - per_raw_material_received: DF.Percent - per_raw_material_returned: DF.Percent - per_returned: DF.Percent - received_items: DF.Table[SubcontractingInwardOrderReceivedItem] - sales_order: DF.Link - secondary_items: DF.Table[SubcontractingInwardOrderSecondaryItem] - service_items: DF.Table[SubcontractingInwardOrderServiceItem] - set_delivery_warehouse: DF.Link | None - status: DF.Literal[ - "Draft", "Open", "Ongoing", "Produced", "Delivered", "Returned", "Cancelled", "Closed" - ] - title: DF.Data | None - transaction_date: DF.Date - # end: auto-generated types - - pass - - def validate(self): - super().validate() - self.set_is_customer_provided_item() - self.validate_customer_provided_items() - self.validate_customer_warehouse() - self.validate_service_items() - self.set_missing_values() - - def on_submit(self): - self.update_status() - self.update_subcontracted_quantity_in_so() - - def on_cancel(self): - self.update_status() - self.update_subcontracted_quantity_in_so() - - def update_status(self, status=None, update_modified=True): - if self.status == "Closed" and self.status != status: - check_on_hold_or_closed_status("Sales Order", self.sales_order) - - total_to_be_received = total_received = total_rm_returned = 0 - for rm in self.get("received_items"): - if rm.get("is_customer_provided_item"): - total_to_be_received += flt(rm.required_qty) - total_received += flt(rm.received_qty) - total_rm_returned += flt(rm.returned_qty) - - total_to_be_produced = total_produced = total_process_loss = total_delivered = total_fg_returned = 0 - for item in self.get("items"): - total_to_be_produced += flt(item.qty) - total_produced += flt(item.produced_qty) - total_process_loss += flt(item.process_loss_qty) - total_delivered += flt(item.delivered_qty) - total_fg_returned += flt(item.returned_qty) - - per_raw_material_received = flt(total_received / total_to_be_received * 100, 2) - per_raw_material_returned = flt(total_rm_returned / total_received * 100, 2) if total_received else 0 - per_produced = flt(total_produced / total_to_be_produced * 100, 2) - per_process_loss = flt(total_process_loss / total_produced * 100, 2) if total_produced else 0 - per_delivered = flt(total_delivered / total_to_be_produced * 100, 2) - per_returned = flt(total_fg_returned / total_delivered * 100, 2) if total_delivered else 0 - - self.db_set("per_raw_material_received", per_raw_material_received, update_modified=update_modified) - self.db_set("per_raw_material_returned", per_raw_material_returned, update_modified=update_modified) - self.db_set("per_produced", per_produced, update_modified=update_modified) - self.db_set("per_process_loss", per_process_loss, update_modified=update_modified) - self.db_set("per_delivered", per_delivered, update_modified=update_modified) - self.db_set("per_returned", per_returned, update_modified=update_modified) - - if self.docstatus >= 1 and not status: - if self.docstatus == 1: - if self.status == "Draft": - status = "Open" - elif self.per_returned == 100: - status = "Returned" - elif self.per_delivered == 100: - status = "Delivered" - elif self.per_produced == 100: - status = "Produced" - elif self.per_raw_material_received > 0: - status = "Ongoing" - else: - status = "Open" - elif self.docstatus == 2: - status = "Cancelled" - - if status and self.status != status: - self.db_set("status", status, update_modified=update_modified) - - def update_subcontracted_quantity_in_so(self): - for service_item in self.service_items: - doc = frappe.get_doc("Sales Order Item", service_item.sales_order_item) - doc.subcontracted_qty = ( - (doc.subcontracted_qty + service_item.qty) - if self._action == "submit" - else (doc.subcontracted_qty - service_item.qty) - ) - doc.save() - - def validate_customer_warehouse(self): - if frappe.get_cached_value("Warehouse", self.customer_warehouse, "customer") != self.customer: - frappe.throw( - _("Customer Warehouse {0} does not belong to Customer {1}.").format( - frappe.bold(self.customer_warehouse), frappe.bold(self.customer) - ) - ) - - def validate_service_items(self): - sales_order_items = [item.sales_order_item for item in self.items] - self.service_items = [ - service_item - for service_item in self.service_items - if service_item.sales_order_item in sales_order_items - ] - - for service_item in self.service_items: - item = next(item for item in self.items if item.sales_order_item == service_item.sales_order_item) - service_item.qty = item.qty * item.subcontracting_conversion_factor - service_item.fg_item_qty = item.qty - service_item.amount = service_item.qty * service_item.rate - - def populate_items_table(self): - items = [] - - for si in self.service_items: - if si.fg_item: - item = frappe.get_doc("Item", si.fg_item) - - so_item = frappe.get_doc("Sales Order Item", si.sales_order_item) - available_qty = so_item.stock_qty - so_item.subcontracted_qty - - if available_qty == 0: - continue - - si.required_qty = available_qty - conversion_factor = so_item.stock_qty / so_item.fg_item_qty - si.fg_item_qty = flt( - available_qty / conversion_factor, frappe.get_precision("Sales Order Item", "qty") - ) - si.amount = available_qty * si.rate - - bom = ( - frappe.db.get_value( - "Subcontracting BOM", - {"finished_good": item.name, "is_active": 1}, - "finished_good_bom", - ) - or item.default_bom - ) - - items.append( - { - "item_code": item.name, - "item_name": item.item_name, - "expected_delivery_date": frappe.get_cached_value( - "Sales Order Item", si.sales_order_item, "delivery_date" - ), - "description": item.description, - "qty": si.fg_item_qty, - "subcontracting_conversion_factor": conversion_factor, - "stock_uom": item.stock_uom, - "bom": bom, - "sales_order_item": si.sales_order_item, - } - ) - else: - frappe.throw( - _("Please select Finished Good Item for Service Item {0}").format( - si.item_name or si.item_code - ) - ) - - if items: - for item in items: - self.append("items", item) - - def validate_customer_provided_items(self): - """Check if atleast one raw material is customer provided""" - for item in self.get("items"): - raw_materials = [rm for rm in self.get("received_items") if rm.main_item_code == item.item_code] - if not any([rm.is_customer_provided_item for rm in raw_materials]): - frappe.throw( - _( - "Atleast one raw material for Finished Good Item {0} should be customer provided." - ).format(frappe.bold(item.item_code)) - ) - - def set_is_customer_provided_item(self): - for item in self.get("received_items"): - item.is_customer_provided_item = frappe.get_cached_value( - "Item", item.rm_item_code, "is_customer_provided_item" - ) - - @frappe.whitelist() - def make_work_order(self): - """Create Work Order from Subcontracting Inward Order.""" - wo_list = [] - - for item in self.get_production_items(): - work_order = self.create_work_order(item) - if work_order: - wo_list.append(work_order) - - self.show_list_created_message("Work Order", wo_list) - - if not wo_list: - frappe.msgprint(_("No Work Orders were created")) - - return wo_list - - def get_production_items(self): - item_list = [] - - for d in self.items: - if d.produced_qty >= d.qty: - continue - - item_details = { - "production_item": d.item_code, - "use_multi_level_bom": d.include_exploded_items, - "subcontracting_inward_order": self.name, - "bom_no": d.bom, - "stock_uom": d.stock_uom, - "company": self.company, - "project": frappe.get_cached_value("Sales Order", self.sales_order, "project"), - "source_warehouse": self.customer_warehouse, - "subcontracting_inward_order_item": d.name, - "reserve_stock": 1, - "fg_warehouse": d.delivery_warehouse, - } - - qty = min( - [ - flt( - (item.received_qty - item.returned_qty - item.work_order_qty) - / flt(item.required_qty / d.qty, d.precision("qty")), - d.precision("qty"), - ) - for item in self.get("received_items") - if item.reference_name == d.name and item.is_customer_provided_item and item.required_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) - - return item_list - - def create_work_order(self, item): - from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError - - if flt(item.get("qty")) <= 0: - return - - wo = frappe.new_doc("Work Order") - wo.update(item) - - wo.set_work_order_operations() - wo.set_required_items() - - try: - wo.flags.ignore_mandatory = True - wo.flags.ignore_validate = True - wo.insert() - return wo.name - except OverProductionError: - pass - - def show_list_created_message(self, doctype, doc_list=None): - if not doc_list: - return - - frappe.flags.mute_messages = False - if doc_list: - doc_list = [get_link_to_form(doctype, p) for p in doc_list] - frappe.msgprint(_("{0} created").format(comma_and(doc_list))) - - @frappe.whitelist() - def make_rm_stock_entry_inward(self, target_doc=None): - def calculate_qty_as_per_bom(rm_item): - data = frappe.get_value( - "Subcontracting Inward Order Item", - {"name": rm_item.reference_name}, - ["process_loss_qty", "include_exploded_items"], - as_dict=True, - ) - stock_qty = frappe.get_value( - "BOM Explosion Item" if data.include_exploded_items else "BOM Item", - {"name": rm_item.bom_detail_no}, - "stock_qty", - ) - qty = flt( - stock_qty * data.process_loss_qty, - frappe.get_precision("Subcontracting Inward Order Received Item", "required_qty"), - ) - return rm_item.required_qty - rm_item.received_qty + rm_item.returned_qty + qty - - if target_doc and target_doc.get("items"): - target_doc.items = [] - - stock_entry = get_mapped_doc( - "Subcontracting Inward Order", - self.name, - { - "Subcontracting Inward Order": { - "doctype": "Stock Entry", - "validation": { - "docstatus": ["=", 1], - }, - }, - }, - target_doc, - ignore_child_tables=True, - ) - - stock_entry.purpose = "Receive from Customer" - stock_entry.subcontracting_inward_order = self.name - - stock_entry.set_stock_entry_type() - - for rm_item in self.received_items: - if not rm_item.required_qty or not rm_item.is_customer_provided_item: - continue - - items_dict = { - rm_item.get("rm_item_code"): { - "scio_detail": rm_item.get("name"), - "qty": calculate_qty_as_per_bom(rm_item), - "to_warehouse": rm_item.get("warehouse"), - "stock_uom": rm_item.get("stock_uom"), - } - } - - stock_entry.add_to_stock_entry_detail(items_dict) - - if target_doc: - return stock_entry - else: - return stock_entry.as_dict() - - @frappe.whitelist() - def make_rm_return(self, target_doc=None): - if target_doc and target_doc.get("items"): - target_doc.items = [] - - stock_entry = get_mapped_doc( - "Subcontracting Inward Order", - self.name, - { - "Subcontracting Inward Order": { - "doctype": "Stock Entry", - "validation": { - "docstatus": ["=", 1], - }, - }, - }, - target_doc, - ignore_child_tables=True, - ) - - stock_entry.purpose = "Return Raw Material to Customer" - stock_entry.set_stock_entry_type() - stock_entry.subcontracting_inward_order = self.name - - for rm_item in self.received_items: - items_dict = { - rm_item.get("rm_item_code"): { - "scio_detail": rm_item.get("name"), - "qty": rm_item.received_qty - rm_item.work_order_qty - rm_item.returned_qty, - "from_warehouse": rm_item.get("warehouse"), - "stock_uom": rm_item.get("stock_uom"), - } - } - - stock_entry.add_to_stock_entry_detail(items_dict) - - if target_doc: - return stock_entry - else: - return stock_entry.as_dict() - - @frappe.whitelist() - def make_subcontracting_delivery(self, target_doc=None): - if target_doc and target_doc.get("items"): - target_doc.items = [] - - stock_entry = get_mapped_doc( - "Subcontracting Inward Order", - self.name, - { - "Subcontracting Inward Order": { - "doctype": "Stock Entry", - "validation": { - "docstatus": ["=", 1], - }, - }, - }, - target_doc, - ignore_child_tables=True, - ) - - stock_entry.purpose = "Subcontracting Delivery" - stock_entry.set_stock_entry_type() - stock_entry.subcontracting_inward_order = self.name - scio_details = [] - - allow_over = frappe.get_single_value("Selling Settings", "allow_delivery_of_overproduced_qty") - for fg_item in self.items: - qty = ( - fg_item.produced_qty - if allow_over - else min(fg_item.qty, fg_item.produced_qty) - fg_item.delivered_qty - ) - if qty < 0: - continue - - scio_details.append(fg_item.name) - items_dict = { - fg_item.item_code: { - "qty": qty, - "from_warehouse": fg_item.delivery_warehouse, - "stock_uom": fg_item.stock_uom, - "scio_detail": fg_item.name, - "is_finished_item": 1, - } - } - - stock_entry.add_to_stock_entry_detail(items_dict) - - if ( - frappe.get_single_value("Selling Settings", "deliver_secondary_items") - and self.secondary_items - and scio_details - ): - secondary_items = [ - secondary_item - for secondary_item in self.secondary_items - if secondary_item.reference_name in scio_details - ] - for secondary_item in secondary_items: - qty = secondary_item.produced_qty - secondary_item.delivered_qty - if qty > 0: - items_dict = { - secondary_item.item_code: { - "qty": secondary_item.produced_qty - secondary_item.delivered_qty, - "from_warehouse": secondary_item.warehouse, - "stock_uom": secondary_item.stock_uom, - "scio_detail": secondary_item.name, - "type": secondary_item.type, - } - } - - stock_entry.add_to_stock_entry_detail(items_dict) - - if target_doc: - return stock_entry - else: - return stock_entry.as_dict() - - @frappe.whitelist() - def make_subcontracting_return(self, target_doc=None): - if target_doc and target_doc.get("items"): - target_doc.items = [] - - stock_entry = get_mapped_doc( - "Subcontracting Inward Order", - self.name, - { - "Subcontracting Inward Order": { - "doctype": "Stock Entry", - "validation": { - "docstatus": ["=", 1], - }, - "field_map": {"name": "subcontracting_inward_order"}, - }, - }, - target_doc, - ignore_child_tables=True, - ) - - stock_entry.purpose = "Subcontracting Return" - stock_entry.set_stock_entry_type() - - for fg_item in self.items: - qty = fg_item.delivered_qty - fg_item.returned_qty - if qty < 0: - continue - - items_dict = { - fg_item.item_code: { - "qty": qty, - "stock_uom": fg_item.stock_uom, - "scio_detail": fg_item.name, - "is_finished_item": 1, - } - } - - stock_entry.add_to_stock_entry_detail(items_dict) - - if target_doc: - return stock_entry - else: - return stock_entry.as_dict() - - -@frappe.whitelist() -def update_subcontracting_inward_order_status(scio: str | Document, status: str | None = None): - if isinstance(scio, str): - scio = frappe.get_doc("Subcontracting Inward Order", scio) - - scio.check_permission("write") - scio.update_status(status)