diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index c227ffb7187..5495c557d1b 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -523,6 +523,9 @@ class PurchaseOrder(BuyingController): if self.is_against_so(): self.update_status_updater() + if self.is_against_pp(): + self.update_status_updater_if_from_pp() + if self.has_drop_ship_item(): self.update_delivered_qty_in_sales_order() @@ -1007,6 +1010,13 @@ def get_mapped_subcontracting_order(source_name, target_doc=None): "Job Card", item.job_card, "wip_warehouse" ) + production_plan = set([item.production_plan for item in source_doc.items if item.production_plan]) + if production_plan: + target_doc.production_plan = production_plan.pop() + target_doc.reserve_stock = frappe.get_single_value( + "Stock Settings", "auto_reserve_stock" + ) or frappe.get_value("Production Plan", target_doc.production_plan, "reserve_stock") + if target_doc and isinstance(target_doc, str): target_doc = json.loads(target_doc) for key in ["service_items", "items", "supplied_items"]: diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 719fac80bf4..3679e7337e8 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -830,6 +830,7 @@ "fieldname": "production_plan", "fieldtype": "Link", "label": "Production Plan", + "no_copy": 1, "options": "Production Plan", "print_hide": 1, "read_only": 1 @@ -948,7 +949,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-10-12 10:57:31.552812", + "modified": "2025-10-30 16:51:56.761673", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 4ad3d5a656e..262bff7d640 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -12,7 +12,6 @@ from frappe.utils import cint, flt, format_datetime, get_datetime import erpnext from erpnext.stock.serial_batch_bundle import get_batches_from_bundle -from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method @@ -145,7 +144,7 @@ def validate_returned_items(doc): ref.rate and flt(d.rate) > ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") - and get_valuation_method(ref.item_code) != "Moving Average" + and get_valuation_method(ref.item_code, doc.company) != "Moving Average" ): frappe.throw( _("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format( diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 8b0a0f19f9b..f6b293605c4 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -524,7 +524,7 @@ class SellingController(StockController): ) if not self.get("return_against") or ( - get_valuation_method(d.item_code) == "Moving Average" + get_valuation_method(d.item_code, self.company) == "Moving Average" and self.get("is_return") and not item_details.has_serial_no and not item_details.has_batch_no @@ -535,7 +535,10 @@ class SellingController(StockController): if ( not d.incoming_rate or self.is_internal_transfer() - or (get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")) + or ( + get_valuation_method(d.item_code, self.company) == "Moving Average" + and self.get("is_return") + ) ): d.incoming_rate = get_incoming_rate( { @@ -560,7 +563,7 @@ class SellingController(StockController): not d.incoming_rate and self.get("return_against") and self.get("is_return") - and get_valuation_method(d.item_code) == "Moving Average" + and get_valuation_method(d.item_code, self.company) == "Moving Average" ): d.incoming_rate = get_rate_for_return( self.doctype, self.name, d.item_code, self.return_against, item_row=d diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index ad2581935ba..af4e76495e9 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1645,6 +1645,128 @@ class StockController(AccountsController): gl_entries.append(self.get_gl_dict(gl_entry, item=item)) + def update_stock_reservation_entries(self): + def get_sre_list(): + table = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(table) + .select(table.name) + .where( + (table.docstatus == 1) + & (table.voucher_type == data_map[purpose or self.doctype]["voucher_type"]) + & ( + table.voucher_no + == data_map[purpose or self.doctype].get( + "voucher_no", item.get("subcontracting_order") + ) + ) + ) + .orderby(table.creation) + ) + if reference_field := data_map[purpose or self.doctype].get("voucher_detail_no_field"): + query = query.where(table.voucher_detail_no == item.get(reference_field)) + else: + query = query.where( + (table.item_code == item.rm_item_code) & (table.warehouse == self.supplier_warehouse) + ) + + return query.run(pluck="name") + + def get_data_map(): + return { + "Subcontracting Delivery": { + "table_name": "items", + "voucher_type": "Subcontracting Inward Order", + "voucher_no": self.get("subcontracting_inward_order"), + "voucher_detail_no_field": "scio_detail", + "field": "delivered_qty", + }, + "Send to Subcontractor": { + "table_name": "items", + "voucher_type": "Subcontracting Order", + "voucher_no": self.get("subcontracting_order"), + "voucher_detail_no_field": "sco_rm_detail", + "field": "transferred_qty", + }, + "Subcontracting Receipt": { + "table_name": "supplied_items", + "voucher_type": "Subcontracting Order", + "field": "consumed_qty", + }, + } + + purpose = self.get("purpose") + if ( + purpose == "Subcontracting Delivery" + or ( + purpose == "Send to Subcontractor" + and frappe.get_value("Subcontracting Order", self.subcontracting_order, "reserve_stock") + ) + or (self.doctype == "Subcontracting Receipt" and self.has_reserved_stock() and not self.is_return) + ): + data_map = get_data_map() + + field = data_map[purpose or self.doctype]["field"] + for item in self.get(data_map[purpose or self.doctype]["table_name"]): + sre_list = get_sre_list() + + if not sre_list: + continue + + qty = item.get("transfer_qty", item.get("consumed_qty")) + for sre in sre_list: + if qty <= 0: + break + + sre_doc = frappe.get_doc("Stock Reservation Entry", sre) + + working_qty = 0 + if sre_doc.reservation_based_on == "Serial and Batch": + sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) + if sre_doc.has_serial_no: + serial_nos = [d.serial_no for d in sbb.entries] + for entry in sre_doc.sb_entries: + if entry.serial_no in serial_nos: + entry.delivered_qty = 1 if self._action == "submit" else 0 + entry.db_update() + working_qty += 1 + serial_nos.remove(entry.serial_no) + else: + batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries} + for entry in sre_doc.sb_entries: + if entry.batch_no in batch_qty: + delivered_qty = min( + (entry.qty - entry.delivered_qty) + if self._action == "submit" + else entry.delivered_qty, + batch_qty[entry.batch_no], + ) + entry.delivered_qty += ( + delivered_qty if self._action == "submit" else (-1 * delivered_qty) + ) + entry.db_update() + working_qty += delivered_qty + batch_qty[entry.batch_no] -= delivered_qty + else: + working_qty = min( + (sre_doc.reserved_qty - sre_doc.get(field)) + if self._action == "submit" + else sre_doc.get(field), + qty, + ) + + sre_doc.set( + field, + sre_doc.get(field) + + (working_qty if self._action == "submit" else (-1 * working_qty)), + ) + sre_doc.db_update() + sre_doc.update_reserved_qty_in_voucher() + sre_doc.update_status() + sre_doc.update_reserved_stock_in_bin() + + qty -= working_qty + @frappe.whitelist() def show_accounting_ledger_preview(company, doctype, docname): diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index adc5f6ae36b..6848a345d9b 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -497,11 +497,10 @@ class SubcontractingController(StockController): if row.serial_no: details.serial_no.extend(get_serial_nos(row.serial_no)) - - elif row.batch_no: + if row.batch_no: details.batch_no[row.batch_no] += row.qty - elif voucher_bundle_data: + if not row.serial_no and not row.batch_no and voucher_bundle_data: bundle_key = (row.rm_item_code, row.main_item_code, row.t_warehouse, row.voucher_no) bundle_data = voucher_bundle_data.get(bundle_key, frappe._dict()) @@ -551,6 +550,8 @@ class SubcontractingController(StockController): frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True) def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): + data = [] + doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] @@ -559,7 +560,7 @@ class SubcontractingController(StockController): "name": "bom_detail_no", "source_warehouse": "reserve_warehouse", } - for field in [ + fields_list = [ "item_code", "name", "rate", @@ -568,7 +569,12 @@ class SubcontractingController(StockController): "description", "item_name", "stock_uom", - ]: + ] + + if doctype == "BOM Item": + fields_list.extend(["is_phantom_item", "bom_no"]) + + for field in fields_list: fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}") filters = [ @@ -578,7 +584,19 @@ class SubcontractingController(StockController): [doctype, "sourced_by_supplier", "=", 0], ] - return frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or [] + data = frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or [] + to_remove = [] + for item in data: + if item.is_phantom_item: + data += self.__get_materials_from_bom( + item.rm_item_code, item.bom_no, exploded_item=exploded_item + ) + to_remove.append(item) + + for item in to_remove: + data.remove(item) + + return data def __update_reserve_warehouse(self, row, item): if ( diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index eaa30a97cd4..056bfcdec9d 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -556,131 +556,6 @@ class SubcontractingInwardController: item.basic_rate + (item.additional_cost / item.transfer_qty), item.precision("basic_rate") ) - def update_sre_for_subcontracting_delivery(self) -> None: - if self.purpose == "Subcontracting Delivery": - if self._action == "submit": - self.update_sre_for_subcontracting_delivery_submit() - elif self._action == "cancel": - self.update_sre_for_subcontracting_delivery_cancel() - - def update_sre_for_subcontracting_delivery_submit(self): - for item in self.get("items"): - table = frappe.qb.DocType("Stock Reservation Entry") - query = ( - frappe.qb.from_(table) - .select(table.name) - .where( - (table.docstatus == 1) - & (table.voucher_type == "Subcontracting Inward Order") - & (table.voucher_no == self.subcontracting_inward_order) - & (table.voucher_detail_no == item.scio_detail) - ) - .orderby(table.creation) - ) - sre_list = query.run(pluck="name") - - if not sre_list: - continue - - qty_to_deliver = item.transfer_qty - for sre in sre_list: - if qty_to_deliver <= 0: - break - - sre_doc = frappe.get_doc("Stock Reservation Entry", sre) - - qty_can_be_deliver = 0 - if sre_doc.reservation_based_on == "Serial and Batch": - sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) - if sre_doc.has_serial_no: - delivered_serial_nos = [d.serial_no for d in sbb.entries] - for entry in sre_doc.sb_entries: - if entry.serial_no in delivered_serial_nos: - entry.delivered_qty = 1 - entry.db_update() - qty_can_be_deliver += 1 - delivered_serial_nos.remove(entry.serial_no) - else: - delivered_batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries} - for entry in sre_doc.sb_entries: - if entry.batch_no in delivered_batch_qty: - delivered_qty = min( - (entry.qty - entry.delivered_qty), - delivered_batch_qty[entry.batch_no], - ) - entry.delivered_qty += delivered_qty - entry.db_update() - qty_can_be_deliver += delivered_qty - delivered_batch_qty[entry.batch_no] -= delivered_qty - else: - qty_can_be_deliver = min((sre_doc.reserved_qty - sre_doc.delivered_qty), qty_to_deliver) - - sre_doc.delivered_qty += qty_can_be_deliver - sre_doc.db_update() - sre_doc.update_status() - sre_doc.update_reserved_stock_in_bin() - - qty_to_deliver -= qty_can_be_deliver - - def update_sre_for_subcontracting_delivery_cancel(self): - for item in self.get("items"): - table = frappe.qb.DocType("Stock Reservation Entry") - query = ( - frappe.qb.from_(table) - .select(table.name) - .where( - (table.docstatus == 1) - & (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) - ) - sre_list = query.run(pluck="name") - - if not sre_list: - continue - - qty_to_undelivered = item.transfer_qty - for sre in sre_list: - if qty_to_undelivered <= 0: - break - - sre_doc = frappe.get_doc("Stock Reservation Entry", sre) - - qty_can_be_undelivered = 0 - if sre_doc.reservation_based_on == "Serial and Batch": - sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) - if sre_doc.has_serial_no: - serial_nos_to_undelivered = [d.serial_no for d in sbb.entries] - for entry in sre_doc.sb_entries: - if entry.serial_no in serial_nos_to_undelivered: - entry.delivered_qty = 0 - entry.db_update() - qty_can_be_undelivered += 1 - serial_nos_to_undelivered.remove(entry.serial_no) - else: - batch_qty_to_undelivered = {d.batch_no: -1 * d.qty for d in sbb.entries} - for entry in sre_doc.sb_entries: - if entry.batch_no in batch_qty_to_undelivered: - undelivered_qty = min( - entry.delivered_qty, batch_qty_to_undelivered[entry.batch_no] - ) - entry.delivered_qty -= undelivered_qty - entry.db_update() - qty_can_be_undelivered += undelivered_qty - batch_qty_to_undelivered[entry.batch_no] -= undelivered_qty - else: - qty_can_be_undelivered = min(sre_doc.delivered_qty, qty_to_undelivered) - - sre_doc.delivered_qty -= qty_can_be_undelivered - sre_doc.db_update() - sre_doc.update_status() - sre_doc.update_reserved_stock_in_bin() - - qty_to_undelivered -= qty_can_be_undelivered - def validate_receive_from_customer_cancel(self): if self.purpose == "Receive from Customer": for item in self.items: diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index b3e06d4db6c..bd6afdf56a7 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -1141,6 +1141,28 @@ class TestSubcontractingController(IntegrationTestCase): itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6], ) + def test_phantom_bom_explosion(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_tree_for_phantom_bom_tests + + expected = create_tree_for_phantom_bom_tests() + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 11", + "qty": 5, + "rate": 100, + "fg_item": "Top Level Parent", + "fg_item_qty": 5, + }, + ] + sco = get_subcontracting_order(service_items=service_items, do_not_submit=True) + sco.items[0].include_exploded_items = 0 + sco.save() + sco.submit() + sco.reload() + + self.assertEqual([item.rm_item_code for item in sco.supplied_items], expected) + def add_second_row_in_scr(scr): item_dict = {} @@ -1308,7 +1330,12 @@ def make_subcontracted_items(): "Subcontracted Item SA7": {}, "Subcontracted Item SA8": {}, "Subcontracted Item SA9": {"stock_uom": "Litre"}, - "Subcontracted Item SA10": {}, + "Subcontracted Item SA10": { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SBAT.####", + }, + "Top Level Parent": {}, } for item, properties in sub_contracted_items.items(): @@ -1360,6 +1387,7 @@ def make_service_items(): "Subcontracted Service Item 8": {}, "Subcontracted Service Item 9": {}, "Subcontracted Service Item 10": {}, + "Subcontracted Service Item 11": {}, } for item, properties in service_items.items(): @@ -1385,6 +1413,7 @@ def make_bom_for_subcontracted_items(): "Subcontracted Item SA7": ["Subcontracted SRM Item 1"], "Subcontracted Item SA8": ["Subcontracted SRM Item 8"], "Subcontracted Item SA10": ["Subcontracted SRM Item 10"], + "Subcontracted Service Item 11": ["Top Level Parent"], } for item_code, raw_materials in boms.items(): diff --git a/erpnext/locale/ar.po b/erpnext/locale/ar.po index a16fd8d3355..fecef1adb47 100644 --- a/erpnext/locale/ar.po +++ b/erpnext/locale/ar.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-11-10 12:11+0000\n" -"PO-Revision-Date: 2025-11-10 21:17\n" +"PO-Revision-Date: 2025-11-15 21:35\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Arabic\n" "MIME-Version: 1.0\n" @@ -1050,7 +1050,7 @@ msgstr "" #. Label of the abbr (Data) field in DocType 'Company' #: erpnext/setup/doctype/company/company.json msgid "Abbr" -msgstr "اسم مختصر" +msgstr "" #. Label of the abbr (Data) field in DocType 'Item Attribute Value' #: erpnext/stock/doctype/item_attribute_value/item_attribute_value.json @@ -1231,7 +1231,7 @@ msgstr "تفاصيل الحساب" #: erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json #: erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json msgid "Account Head" -msgstr "رئيس حساب" +msgstr "" #. Label of the account_manager (Link) field in DocType 'Customer' #: erpnext/selling/doctype/customer/customer.json @@ -1545,7 +1545,7 @@ msgstr "المحاسبة" #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json msgid "Accounting Details" -msgstr "تفاصيل المحاسبة" +msgstr "" #. Name of a DocType #. Label of the accounting_dimension (Select) field in DocType 'Accounting @@ -2150,7 +2150,7 @@ msgstr "" #: erpnext/crm/doctype/opportunity/opportunity.json #: erpnext/crm/doctype/prospect/prospect.json msgid "Activities" -msgstr "أنشطة" +msgstr "" #. Name of a DocType #. Label of a Link in the Projects Workspace @@ -3009,7 +3009,7 @@ msgstr "" #. Label of the advance_account (Link) field in DocType 'Party Account' #: erpnext/accounts/doctype/party_account/party_account.json msgid "Advance Account" -msgstr "حساب مقدم" +msgstr "" #: erpnext/utilities/transaction_base.py:215 msgid "Advance Account: {0} must be in either customer billing currency: {1} or Company default currency: {2}" @@ -3027,7 +3027,7 @@ msgstr "المبلغ مقدما" #: erpnext/buying/doctype/purchase_order/purchase_order.json #: erpnext/selling/doctype/sales_order/sales_order.json msgid "Advance Paid" -msgstr "مسبقا المدفوعة" +msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order_list.js:75 #: erpnext/selling/doctype/sales_order/sales_order_list.js:122 @@ -3130,7 +3130,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json msgid "Advances" -msgstr "الدفعات المقدمة" +msgstr "" #: erpnext/setup/setup_wizard/data/marketing_source.txt:3 msgid "Advertisement" @@ -6818,7 +6818,7 @@ msgstr "معلومات الحساب البنكي" #: erpnext/accounts/doctype/payment_entry/payment_entry.json #: erpnext/accounts/doctype/payment_request/payment_request.json msgid "Bank Account No" -msgstr "رقم الحساب البنكي" +msgstr "" #. Name of a DocType #: erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.json @@ -6920,7 +6920,7 @@ msgstr "نوع الضمان المصرفي" #: erpnext/accounts/doctype/cheque_print_template/cheque_print_template.json #: erpnext/setup/doctype/employee/employee.json msgid "Bank Name" -msgstr "اسم المصرف" +msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:98 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:142 @@ -7409,7 +7409,7 @@ msgstr "قبل المصالحة" #. Label of the start (Int) field in DocType 'Task' #: erpnext/projects/doctype/task/task.json msgid "Begin On (Days)" -msgstr "ابدأ (بالأيام)" +msgstr "" #. Option for the 'Generate Invoice At' (Select) field in DocType #. 'Subscription' @@ -7795,7 +7795,7 @@ msgstr "فصيلة الدم" #. Accounts' #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json msgid "Body" -msgstr "الجسم" +msgstr "" #. Label of the body_text (Text Editor) field in DocType 'Dunning' #. Label of the body_text (Text Editor) field in DocType 'Dunning Letter Text' @@ -10418,7 +10418,7 @@ msgstr "وصف الشركة" #. 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Company Details" -msgstr "تفاصيل الشركة" +msgstr "" #. Option for the 'Preferred Contact Email' (Select) field in DocType #. 'Employee' @@ -11010,7 +11010,7 @@ msgstr "اسم جهة الاتصال" #. Label of the contact_no (Data) field in DocType 'Sales Team' #: erpnext/selling/doctype/sales_team/sales_team.json msgid "Contact No." -msgstr "الاتصال رقم" +msgstr "" #. Label of the contact_person (Link) field in DocType 'Dunning' #. Label of the contact_person (Link) field in DocType 'POS Invoice' @@ -11343,7 +11343,7 @@ msgstr "" #. Label of the cost (Currency) field in DocType 'Subscription Plan' #: erpnext/accounts/doctype/subscription_plan/subscription_plan.json msgid "Cost" -msgstr "كلفة" +msgstr "" #. Label of the cost_center (Link) field in DocType 'Account Closing Balance' #. Label of the cost_center (Link) field in DocType 'Advance Taxes and Charges' @@ -11658,7 +11658,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json #: erpnext/projects/doctype/task/task.json msgid "Costing" -msgstr "تكلف" +msgstr "" #. Label of the costing_amount (Currency) field in DocType 'Timesheet Detail' #. Label of the base_costing_amount (Currency) field in DocType 'Timesheet @@ -12350,7 +12350,7 @@ msgstr "الدائنين" #. Label of the criteria (Table) field in DocType 'Supplier Scorecard Period' #: erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.json msgid "Criteria" -msgstr "المعايير" +msgstr "" #. Label of the formula (Small Text) field in DocType 'Supplier Scorecard #. Criteria' @@ -13499,7 +13499,7 @@ msgstr "" #. Label of the date_of_birth (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Date of Birth" -msgstr "تاريخ الميلاد" +msgstr "" #: erpnext/setup/doctype/employee/employee.py:147 msgid "Date of Birth cannot be greater than today." @@ -13532,7 +13532,7 @@ msgstr "تاريخ الإصدار" #. Label of the date_of_joining (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Date of Joining" -msgstr "تاريخ الالتحاق بالعمل" +msgstr "" #: erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py:273 msgid "Date of Transaction" @@ -13851,7 +13851,7 @@ msgstr "الخصومات أو الخسارة" #: erpnext/accounts/doctype/mode_of_payment_account/mode_of_payment_account.json #: erpnext/accounts/doctype/party_account/party_account.json msgid "Default Account" -msgstr "الافتراضي حساب" +msgstr "" #. Label of the default_accounts_section (Section Break) field in DocType #. 'Supplier' @@ -14359,7 +14359,7 @@ msgstr "المصروفات المؤجلة" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/stock/doctype/item_default/item_default.json msgid "Deferred Expense Account" -msgstr "حساب المصروفات المؤجلة" +msgstr "" #. Option for the 'Entry Type' (Select) field in DocType 'Journal Entry' #. Label of the deferred_revenue (Section Break) field in DocType 'POS Invoice @@ -15248,7 +15248,7 @@ msgstr "إيراد مباشر" #: erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json #: erpnext/stock/doctype/putaway_rule/putaway_rule.json msgid "Disable" -msgstr "تعطيل" +msgstr "" #. Label of the disable_capacity_planning (Check) field in DocType #. 'Manufacturing Settings' @@ -15292,7 +15292,7 @@ msgstr "" #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Disable Rounded Total" -msgstr "تعطيل الاجمالي المقرب" +msgstr "" #. Label of the disable_serial_no_and_batch_selector (Check) field in DocType #. 'Stock Settings' @@ -16207,7 +16207,7 @@ msgstr "مكرر {0} موجود في الجدول" #. Label of the duration (Int) field in DocType 'Task' #: erpnext/projects/doctype/task/task.json msgid "Duration (Days)" -msgstr "المدة (أيام)" +msgstr "" #: erpnext/crm/report/lead_conversion_time/lead_conversion_time.py:66 msgid "Duration in Days" @@ -16583,7 +16583,7 @@ msgstr "موظف" #. Account' #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json msgid "Employee Advance" -msgstr "تقدم الموظف" +msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:16 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:23 @@ -16646,7 +16646,7 @@ msgstr "اسم الموظف" #. Label of the employee_number (Data) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Employee Number" -msgstr "رقم الموظف" +msgstr "" #. Label of the employee_user_id (Link) field in DocType 'Call Log' #: erpnext/telephony/doctype/call_log/call_log.json @@ -16672,7 +16672,7 @@ msgstr "" #: erpnext/manufacturing/doctype/workstation/workstation.js:351 msgid "Employees" -msgstr "الموظفين" +msgstr "" #: erpnext/stock/doctype/batch/batch_list.js:16 msgid "Empty" @@ -16863,7 +16863,7 @@ msgstr "" #. Label of the encashment_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Encashment Date" -msgstr "تاريخ التحصيل" +msgstr "" #: erpnext/crm/doctype/contract/contract.py:70 msgid "End Date cannot be before Start Date." @@ -16880,7 +16880,7 @@ msgstr "لا يمكن أن يكون تاريخ الانتهاء قبل تاري #: erpnext/support/doctype/service_day/service_day.json #: erpnext/telephony/doctype/call_log/call_log.json msgid "End Time" -msgstr "وقت الانتهاء" +msgstr "" #: erpnext/stock/doctype/stock_entry/stock_entry.js:287 msgid "End Transit" @@ -17307,7 +17307,7 @@ msgstr "" #: erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Exchange Rate" -msgstr "سعر الصرف" +msgstr "" #. Name of a DocType #. Option for the 'Entry Type' (Select) field in DocType 'Journal Entry' @@ -17582,7 +17582,7 @@ msgstr "حساب المصاريف مفقود" #. Account' #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json msgid "Expense Claim" -msgstr "طلب النفقات" +msgstr "" #. Label of the expense_account (Link) field in DocType 'Purchase Invoice Item' #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -18973,7 +18973,7 @@ msgstr "" #. Label of the from_employee (Link) field in DocType 'Asset Movement Item' #: erpnext/assets/doctype/asset_movement_item/asset_movement_item.json msgid "From Employee" -msgstr "من الموظف" +msgstr "" #: erpnext/assets/doctype/asset_movement/asset_movement.py:85 msgid "From Employee is required while issuing Asset {0}" @@ -20526,7 +20526,7 @@ msgstr "اسم قائمة العطلات" #. Label of the holidays (Table) field in DocType 'Holiday List' #: erpnext/setup/doctype/holiday_list/holiday_list.json msgid "Holidays" -msgstr "العطلات" +msgstr "" #. Option for the 'Forecasting Method' (Select) field in DocType 'Sales #. Forecast' @@ -20556,7 +20556,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json msgid "Hour Rate" -msgstr "سعرالساعة" +msgstr "" #. Label of the hours (Float) field in DocType 'Workstation Working Hour' #: erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json @@ -21293,7 +21293,7 @@ msgstr "في المئة" #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "In Process" -msgstr "في عملية" +msgstr "" #: erpnext/stock/report/item_variant_details/item_variant_details.py:107 msgid "In Production" @@ -22156,7 +22156,7 @@ msgstr "إعدادات نقل المستودعات الداخلية" #. Label of the interest (Currency) field in DocType 'Overdue Payment' #: erpnext/accounts/doctype/overdue_payment/overdue_payment.json msgid "Interest" -msgstr "فائدة" +msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:3052 msgid "Interest and/or dunning fee" @@ -23001,7 +23001,7 @@ msgstr "هو المورد الداخلي" #. Label of the is_mandatory (Check) field in DocType 'Applicable On Account' #: erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json msgid "Is Mandatory" -msgstr "إلزامي" +msgstr "" #. Label of the is_milestone (Check) field in DocType 'Task' #: erpnext/projects/doctype/task/task.json @@ -23055,7 +23055,7 @@ msgstr "" #. Label of the is_paid (Check) field in DocType 'Purchase Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json msgid "Is Paid" -msgstr "مدفوع" +msgstr "" #. Label of the is_paused (Check) field in DocType 'Job Card' #: erpnext/manufacturing/doctype/job_card/job_card.json @@ -24992,7 +24992,7 @@ msgstr "بدأ العمل" #: erpnext/crm/doctype/lead/lead.json #: erpnext/crm/doctype/opportunity/opportunity.json msgid "Job Title" -msgstr "المسمى الوظيفي" +msgstr "" #. Label of the supplier (Link) field in DocType 'Subcontracting Order' #. Label of the supplier (Link) field in DocType 'Subcontracting Receipt' @@ -25782,7 +25782,7 @@ msgstr "رقم الرخصة" #. Label of the license_plate (Data) field in DocType 'Vehicle' #: erpnext/setup/doctype/vehicle/vehicle.json msgid "License Plate" -msgstr "لوحة الترخيص" +msgstr "" #: erpnext/controllers/status_updater.py:459 msgid "Limit Crossed" @@ -25911,7 +25911,7 @@ msgstr "" #. Account' #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json msgid "Loan" -msgstr "قرض" +msgstr "" #. Label of the loan_end_date (Date) field in DocType 'Invoice Discounting' #: erpnext/accounts/doctype/invoice_discounting/invoice_discounting.json @@ -28002,7 +28002,7 @@ msgstr "طريقة الدفع" #. Label of the model (Data) field in DocType 'Vehicle' #: erpnext/setup/doctype/vehicle/vehicle.json msgid "Model" -msgstr "الموديل" +msgstr "" #. Label of the section_break_11 (Section Break) field in DocType 'POS Closing #. Entry' @@ -29597,7 +29597,7 @@ msgstr "قراءة عداد المسافات (الأخيرة)" #. Label of the scheduled_confirmation_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Offer Date" -msgstr "تاريخ العرض" +msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:29 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:42 @@ -29639,7 +29639,7 @@ msgstr "" #: erpnext/setup/doctype/supplier_group/supplier_group.json #: erpnext/stock/doctype/warehouse/warehouse.json msgid "Old Parent" -msgstr "الحساب الأب السابق" +msgstr "" #. Option for the 'Reconciliation Takes Effect On' (Select) field in DocType #. 'Company' @@ -30606,7 +30606,7 @@ msgstr "البند الأصلي" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json msgid "Other Details" -msgstr "تفاصيل أخرى" +msgstr "" #. Label of the other_info_tab (Tab Break) field in DocType 'Asset' #. Label of the other_info_tab (Tab Break) field in DocType 'Stock Entry' @@ -30639,7 +30639,7 @@ msgstr "تقارير أخرى" #. 'Manufacturing Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json msgid "Other Settings" -msgstr "اعدادات اخرى" +msgstr "" #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json @@ -30928,7 +30928,7 @@ msgstr "زيادة الإنتاج للمبيعات وطلب العمل" #. Option for the 'Current Address Is' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Owned" -msgstr "مملوك" +msgstr "" #: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js:29 #: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py:23 @@ -32060,7 +32060,7 @@ msgstr "" #. Label of the passport_number (Data) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Passport Number" -msgstr "رقم جواز السفر" +msgstr "" #. Option for the 'Status' (Select) field in DocType 'Subscription' #: erpnext/accounts/doctype/subscription/subscription.json @@ -32177,7 +32177,7 @@ msgstr "دفع" #: erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json #: erpnext/accounts/doctype/payment_request/payment_request.json msgid "Payment Account" -msgstr "حساب الدفع" +msgstr "" #. Label of the payment_amount (Currency) field in DocType 'Overdue Payment' #. Label of the payment_amount (Currency) field in DocType 'Payment Schedule' @@ -32768,7 +32768,7 @@ msgstr "" #. Account' #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json msgid "Payroll Entry" -msgstr "دخول الرواتب" +msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:88 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:119 @@ -35502,7 +35502,7 @@ msgstr "" #. Request' #: erpnext/stock/doctype/material_request/material_request.json msgid "Printing Details" -msgstr "تفاصيل الطباعة" +msgstr "" #. Label of the printing_settings_section (Section Break) field in DocType #. 'Dunning' @@ -37971,7 +37971,7 @@ msgstr "رفع طلب المواد عندما يصل المخزون إلى مس #. Label of the complaint_raised_by (Data) field in DocType 'Warranty Claim' #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Raised By" -msgstr "التي أثارها" +msgstr "" #. Label of the raised_by (Data) field in DocType 'Issue' #: erpnext/support/doctype/issue/issue.json @@ -39282,7 +39282,7 @@ msgstr "يجب أن يكون تاريخ الإصدار في المستقبل" #. Label of the relieving_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Relieving Date" -msgstr "تاريخ المغادرة" +msgstr "" #: erpnext/public/js/bank_reconciliation_tool/dialog_manager.js:125 msgid "Remaining" @@ -40150,7 +40150,7 @@ msgstr "إعادة ضبط اتفاقية مستوى الخدمة." #. Label of the resignation_letter_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Resignation Letter Date" -msgstr "تاريخ رسالة الإستقالة" +msgstr "" #. Label of the sb_00 (Section Break) field in DocType 'Quality Action' #. Label of the resolution (Text Editor) field in DocType 'Quality Action @@ -40173,7 +40173,7 @@ msgstr "القرار بواسطة" #: erpnext/support/doctype/issue/issue.json #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Resolution Date" -msgstr "تاريخ القرار" +msgstr "" #. Label of the section_break_19 (Section Break) field in DocType 'Issue' #. Label of the resolution_details (Text Editor) field in DocType 'Issue' @@ -40181,7 +40181,7 @@ msgstr "تاريخ القرار" #: erpnext/support/doctype/issue/issue.json #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Resolution Details" -msgstr "قرار تفاصيل" +msgstr "" #. Option for the 'Service Level Agreement Status' (Select) field in DocType #. 'Issue' @@ -40222,7 +40222,7 @@ msgstr "تم الحل" #. Label of the resolved_by (Link) field in DocType 'Warranty Claim' #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Resolved By" -msgstr "حلها عن طريق" +msgstr "" #. Label of the response_by (Datetime) field in DocType 'Issue' #: erpnext/support/doctype/issue/issue.json @@ -40494,7 +40494,7 @@ msgstr "" #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:138 #: erpnext/stock/doctype/shipment/shipment.json msgid "Returned" -msgstr "تم إرجاعه" +msgstr "" #. Label of the returned_against (Data) field in DocType 'Serial and Batch #. Bundle' @@ -40816,7 +40816,7 @@ msgstr "تقريب إجمالي" #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Rounded Total (Company Currency)" -msgstr "المشاركات تقريب (العملة الشركة)" +msgstr "" #. Label of the rounding_adjustment (Currency) field in DocType 'POS Invoice' #. Label of the rounding_adjustment (Currency) field in DocType 'Purchase @@ -48069,7 +48069,7 @@ msgstr "" #: erpnext/setup/doctype/driver/driver.json #: erpnext/setup/doctype/employee/employee.json msgid "Suspended" -msgstr "معلق" +msgstr "" #: erpnext/selling/page/point_of_sale/pos_payment.js:442 msgid "Switch Between Payment Modes" @@ -48420,7 +48420,7 @@ msgstr "نوع المهمة" #. Option for the '% Complete Method' (Select) field in DocType 'Project' #: erpnext/projects/doctype/project/project.json msgid "Task Weight" -msgstr "وزن المهمة" +msgstr "" #: erpnext/projects/doctype/project_template/project_template.py:41 msgid "Task {0} depends on Task {1}. Please add Task {1} to the Tasks list." @@ -48959,7 +48959,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json #: erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.json msgid "Template Name" -msgstr "اسم القالب" +msgstr "" #. Label of the template_task (Data) field in DocType 'Task' #: erpnext/projects/doctype/task/task.json @@ -49037,7 +49037,7 @@ msgstr "تفاصيل الشروط" #: erpnext/stock/doctype/material_request/material_request.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Terms" -msgstr "الشروط" +msgstr "" #. Label of the terms_section_break (Section Break) field in DocType 'Purchase #. Order' @@ -50056,7 +50056,7 @@ msgstr "الوقت المطلوب (بالدقائق)" #. Label of the time_sheet (Link) field in DocType 'Sales Invoice Timesheet' #: erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json msgid "Time Sheet" -msgstr "ورقة الوقت" +msgstr "" #. Label of the time_sheet_list (Section Break) field in DocType 'POS Invoice' #. Label of the time_sheet_list (Section Break) field in DocType 'Sales @@ -50718,7 +50718,7 @@ msgstr "إجمالي مبلغ الفاتورة (عبر فواتير المبيع #. Label of the total_billed_hours (Float) field in DocType 'Timesheet' #: erpnext/projects/doctype/timesheet/timesheet.json msgid "Total Billed Hours" -msgstr "مجموع الساعات وصفت" +msgstr "" #. Label of the total_billing_amount (Currency) field in DocType 'POS Invoice' #. Label of the total_billing_amount (Currency) field in DocType 'Sales @@ -50870,7 +50870,7 @@ msgstr "إجمالي وقت الانتظار" #. Label of the total_holidays (Int) field in DocType 'Holiday List' #: erpnext/setup/doctype/holiday_list/holiday_list.json msgid "Total Holidays" -msgstr "مجموع العطلات" +msgstr "" #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:115 msgid "Total Income" @@ -51211,7 +51211,7 @@ msgstr "مجموع الضرائب" #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Total Taxes and Charges" -msgstr "مجموع الضرائب والرسوم" +msgstr "" #. Label of the base_total_taxes_and_charges (Currency) field in DocType #. 'Payment Entry' @@ -51323,7 +51323,7 @@ msgstr "" #: erpnext/manufacturing/doctype/workstation/workstation.json #: erpnext/projects/doctype/timesheet/timesheet.json msgid "Total Working Hours" -msgstr "مجموع ساعات العمل" +msgstr "" #. Label of the total_workstation_time (Int) field in DocType 'Item Lead Time' #: erpnext/stock/doctype/item_lead_time/item_lead_time.json @@ -53764,7 +53764,7 @@ msgstr "" #: erpnext/setup/setup_wizard/data/marketing_source.txt:10 msgid "Walk In" -msgstr "عميل غير مسجل" +msgstr "" #: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js:4 msgid "Warehouse Capacity Summary" diff --git a/erpnext/locale/bs.po b/erpnext/locale/bs.po index 04b9a36491e..39642d2b091 100644 --- a/erpnext/locale/bs.po +++ b/erpnext/locale/bs.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-11-10 12:11+0000\n" -"PO-Revision-Date: 2025-11-11 21:15\n" +"PO-Revision-Date: 2025-11-12 21:29\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Bosnian\n" "MIME-Version: 1.0\n" @@ -32241,7 +32241,7 @@ msgstr "Plati / Uplata od" #: erpnext/accounts/report/account_balance/account_balance.js:54 #: erpnext/setup/doctype/party_type/party_type.json msgid "Payable" -msgstr "Plaća se" +msgstr "Obaveze" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:39 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1160 @@ -32249,7 +32249,7 @@ msgstr "Plaća se" #: erpnext/accounts/report/purchase_register/purchase_register.py:194 #: erpnext/accounts/report/purchase_register/purchase_register.py:235 msgid "Payable Account" -msgstr "Račun Plaćanja" +msgstr "Račun Obaveza" #. Name of a Workspace #. Label of the payables (Check) field in DocType 'Email Digest' diff --git a/erpnext/locale/es.po b/erpnext/locale/es.po index badae208dc2..92e2b69bf36 100644 --- a/erpnext/locale/es.po +++ b/erpnext/locale/es.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-11-10 12:11+0000\n" -"PO-Revision-Date: 2025-11-10 21:18\n" +"PO-Revision-Date: 2025-11-15 21:36\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Spanish\n" "MIME-Version: 1.0\n" @@ -6922,7 +6922,7 @@ msgstr "Información de la Cuenta Bancaria" #: erpnext/accounts/doctype/payment_entry/payment_entry.json #: erpnext/accounts/doctype/payment_request/payment_request.json msgid "Bank Account No" -msgstr "Número de Cuenta Bancaria" +msgstr "" #. Name of a DocType #: erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.json @@ -7024,7 +7024,7 @@ msgstr "Tipo de Garantía Bancaria" #: erpnext/accounts/doctype/cheque_print_template/cheque_print_template.json #: erpnext/setup/doctype/employee/employee.json msgid "Bank Name" -msgstr "Nombre del banco" +msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:98 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:142 @@ -7513,7 +7513,7 @@ msgstr "Antes de Reconciliación" #. Label of the start (Int) field in DocType 'Task' #: erpnext/projects/doctype/task/task.json msgid "Begin On (Days)" -msgstr "Comience el (días)" +msgstr "" #. Option for the 'Generate Invoice At' (Select) field in DocType #. 'Subscription' @@ -11762,7 +11762,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json #: erpnext/projects/doctype/task/task.json msgid "Costing" -msgstr "Presupuesto" +msgstr "" #. Label of the costing_amount (Currency) field in DocType 'Timesheet Detail' #. Label of the base_costing_amount (Currency) field in DocType 'Timesheet @@ -12454,7 +12454,7 @@ msgstr "Acreedores" #. Label of the criteria (Table) field in DocType 'Supplier Scorecard Period' #: erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.json msgid "Criteria" -msgstr "Criterios" +msgstr "" #. Label of the formula (Small Text) field in DocType 'Supplier Scorecard #. Criteria' @@ -13579,7 +13579,7 @@ msgstr "Importación de datos y configuraciones" #. Label of the date (Date) field in DocType 'Bulk Transaction Log Detail' #: erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json msgid "Date " -msgstr "Fecha " +msgstr "" #: erpnext/assets/report/fixed_asset_register/fixed_asset_register.js:97 msgid "Date Based On" @@ -13603,7 +13603,7 @@ msgstr "La fecha debe estar entre {0} y {1}" #. Label of the date_of_birth (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Date of Birth" -msgstr "Fecha de nacimiento" +msgstr "" #: erpnext/setup/doctype/employee/employee.py:147 msgid "Date of Birth cannot be greater than today." @@ -13636,7 +13636,7 @@ msgstr "Fecha de Emisión." #. Label of the date_of_joining (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Date of Joining" -msgstr "Fecha de Ingreso" +msgstr "" #: erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py:273 msgid "Date of Transaction" @@ -13955,7 +13955,7 @@ msgstr "Deducciones o Pérdida" #: erpnext/accounts/doctype/mode_of_payment_account/mode_of_payment_account.json #: erpnext/accounts/doctype/party_account/party_account.json msgid "Default Account" -msgstr "Cuenta predeterminada" +msgstr "" #. Label of the default_accounts_section (Section Break) field in DocType #. 'Supplier' @@ -14463,7 +14463,7 @@ msgstr "Gasto Diferido" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/stock/doctype/item_default/item_default.json msgid "Deferred Expense Account" -msgstr "Cuenta de Gastos Diferidos" +msgstr "" #. Option for the 'Entry Type' (Select) field in DocType 'Journal Entry' #. Label of the deferred_revenue (Section Break) field in DocType 'POS Invoice @@ -15352,7 +15352,7 @@ msgstr "Ingreso directo" #: erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json #: erpnext/stock/doctype/putaway_rule/putaway_rule.json msgid "Disable" -msgstr "Desactivar" +msgstr "" #. Label of the disable_capacity_planning (Check) field in DocType #. 'Manufacturing Settings' @@ -15396,7 +15396,7 @@ msgstr "Desactivar última tasa de compra" #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Disable Rounded Total" -msgstr "Desactivar redondeo" +msgstr "" #. Label of the disable_serial_no_and_batch_selector (Check) field in DocType #. 'Stock Settings' @@ -16311,7 +16311,7 @@ msgstr "Duplicado {0} encontrado en la tabla" #. Label of the duration (Int) field in DocType 'Task' #: erpnext/projects/doctype/task/task.json msgid "Duration (Days)" -msgstr "Duración (Días)" +msgstr "" #: erpnext/crm/report/lead_conversion_time/lead_conversion_time.py:66 msgid "Duration in Days" @@ -16687,7 +16687,7 @@ msgstr "Empleado" #. Account' #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json msgid "Employee Advance" -msgstr "Avance del Empleado" +msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:16 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:23 @@ -16750,7 +16750,7 @@ msgstr "Nombre de empleado" #. Label of the employee_number (Data) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Employee Number" -msgstr "Número de empleado" +msgstr "" #. Label of the employee_user_id (Link) field in DocType 'Call Log' #: erpnext/telephony/doctype/call_log/call_log.json @@ -16776,7 +16776,7 @@ msgstr "" #: erpnext/manufacturing/doctype/workstation/workstation.js:351 msgid "Employees" -msgstr "Empleados" +msgstr "" #: erpnext/stock/doctype/batch/batch_list.js:16 msgid "Empty" @@ -16967,7 +16967,7 @@ msgstr "" #. Label of the encashment_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Encashment Date" -msgstr "Fecha de Cobro" +msgstr "" #: erpnext/crm/doctype/contract/contract.py:70 msgid "End Date cannot be before Start Date." @@ -17413,7 +17413,7 @@ msgstr "" #: erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Exchange Rate" -msgstr "Tipo de cambio" +msgstr "" #. Name of a DocType #. Option for the 'Entry Type' (Select) field in DocType 'Journal Entry' @@ -17688,7 +17688,7 @@ msgstr "Falta la cuenta de gastos" #. Account' #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json msgid "Expense Claim" -msgstr "Reembolso de gastos" +msgstr "" #. Label of the expense_account (Link) field in DocType 'Purchase Invoice Item' #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -19079,7 +19079,7 @@ msgstr "" #. Label of the from_employee (Link) field in DocType 'Asset Movement Item' #: erpnext/assets/doctype/asset_movement_item/asset_movement_item.json msgid "From Employee" -msgstr "Desde Empleado" +msgstr "" #: erpnext/assets/doctype/asset_movement/asset_movement.py:85 msgid "From Employee is required while issuing Asset {0}" @@ -20632,7 +20632,7 @@ msgstr "Nombre de festividad" #. Label of the holidays (Table) field in DocType 'Holiday List' #: erpnext/setup/doctype/holiday_list/holiday_list.json msgid "Holidays" -msgstr "Vacaciones" +msgstr "" #. Option for the 'Forecasting Method' (Select) field in DocType 'Sales #. Forecast' @@ -20662,7 +20662,7 @@ msgstr "Hora" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json msgid "Hour Rate" -msgstr "Salario por hora" +msgstr "" #. Label of the hours (Float) field in DocType 'Workstation Working Hour' #: erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json @@ -21399,7 +21399,7 @@ msgstr "En porcentaje" #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "In Process" -msgstr "En Proceso" +msgstr "" #: erpnext/stock/report/item_variant_details/item_variant_details.py:107 msgid "In Production" @@ -22262,7 +22262,7 @@ msgstr "Configuración de transferencia entre almacenes" #. Label of the interest (Currency) field in DocType 'Overdue Payment' #: erpnext/accounts/doctype/overdue_payment/overdue_payment.json msgid "Interest" -msgstr "Interesar" +msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:3052 msgid "Interest and/or dunning fee" @@ -23107,7 +23107,7 @@ msgstr "Es un Proveedor Interno" #. Label of the is_mandatory (Check) field in DocType 'Applicable On Account' #: erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json msgid "Is Mandatory" -msgstr "Es obligatorio" +msgstr "" #. Label of the is_milestone (Check) field in DocType 'Task' #: erpnext/projects/doctype/task/task.json @@ -23161,7 +23161,7 @@ msgstr "" #. Label of the is_paid (Check) field in DocType 'Purchase Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json msgid "Is Paid" -msgstr "Está pagado" +msgstr "" #. Label of the is_paused (Check) field in DocType 'Job Card' #: erpnext/manufacturing/doctype/job_card/job_card.json @@ -25098,7 +25098,7 @@ msgstr "Trabajo comenzó" #: erpnext/crm/doctype/lead/lead.json #: erpnext/crm/doctype/opportunity/opportunity.json msgid "Job Title" -msgstr "Título del trabajo" +msgstr "" #. Label of the supplier (Link) field in DocType 'Subcontracting Order' #. Label of the supplier (Link) field in DocType 'Subcontracting Receipt' @@ -25888,7 +25888,7 @@ msgstr "Número de Licencia" #. Label of the license_plate (Data) field in DocType 'Vehicle' #: erpnext/setup/doctype/vehicle/vehicle.json msgid "License Plate" -msgstr "Matrículas" +msgstr "" #: erpnext/controllers/status_updater.py:459 msgid "Limit Crossed" @@ -26017,7 +26017,7 @@ msgstr "" #. Account' #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json msgid "Loan" -msgstr "Préstamo" +msgstr "" #. Label of the loan_end_date (Date) field in DocType 'Invoice Discounting' #: erpnext/accounts/doctype/invoice_discounting/invoice_discounting.json @@ -28108,7 +28108,7 @@ msgstr "Modo de pago" #. Label of the model (Data) field in DocType 'Vehicle' #: erpnext/setup/doctype/vehicle/vehicle.json msgid "Model" -msgstr "Modelo" +msgstr "" #. Label of the section_break_11 (Section Break) field in DocType 'POS Closing #. Entry' @@ -29703,7 +29703,7 @@ msgstr "Valor del cuentakilómetros (Última)" #. Label of the scheduled_confirmation_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Offer Date" -msgstr "Fecha de oferta" +msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:29 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:42 @@ -29745,7 +29745,7 @@ msgstr "" #: erpnext/setup/doctype/supplier_group/supplier_group.json #: erpnext/stock/doctype/warehouse/warehouse.json msgid "Old Parent" -msgstr "Antiguo Padre" +msgstr "" #. Option for the 'Reconciliation Takes Effect On' (Select) field in DocType #. 'Company' @@ -30712,7 +30712,7 @@ msgstr "Artículo Original" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json msgid "Other Details" -msgstr "Otros detalles" +msgstr "" #. Label of the other_info_tab (Tab Break) field in DocType 'Asset' #. Label of the other_info_tab (Tab Break) field in DocType 'Stock Entry' @@ -30745,7 +30745,7 @@ msgstr "Otros Reportes" #. 'Manufacturing Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json msgid "Other Settings" -msgstr "Otros ajustes" +msgstr "" #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json @@ -31034,7 +31034,7 @@ msgstr "Sobreproducción para ventas y órdenes de trabajo" #. Option for the 'Current Address Is' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Owned" -msgstr "Propiedad" +msgstr "" #: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js:29 #: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py:23 @@ -32166,7 +32166,7 @@ msgstr "" #. Label of the passport_number (Data) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Passport Number" -msgstr "Número de pasaporte" +msgstr "" #. Option for the 'Status' (Select) field in DocType 'Subscription' #: erpnext/accounts/doctype/subscription/subscription.json @@ -32283,7 +32283,7 @@ msgstr "Pago" #: erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json #: erpnext/accounts/doctype/payment_request/payment_request.json msgid "Payment Account" -msgstr "Cuenta de pagos" +msgstr "" #. Label of the payment_amount (Currency) field in DocType 'Overdue Payment' #. Label of the payment_amount (Currency) field in DocType 'Payment Schedule' @@ -32874,7 +32874,7 @@ msgstr "" #. Account' #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json msgid "Payroll Entry" -msgstr "Entrada de Nómina" +msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:88 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:119 @@ -35608,7 +35608,7 @@ msgstr "" #. Request' #: erpnext/stock/doctype/material_request/material_request.json msgid "Printing Details" -msgstr "Detalles de impresión" +msgstr "" #. Label of the printing_settings_section (Section Break) field in DocType #. 'Dunning' @@ -38077,7 +38077,7 @@ msgstr "Aumente la solicitud de material cuando el stock alcance el nivel de ped #. Label of the complaint_raised_by (Data) field in DocType 'Warranty Claim' #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Raised By" -msgstr "Propuesto por" +msgstr "" #. Label of the raised_by (Data) field in DocType 'Issue' #: erpnext/support/doctype/issue/issue.json @@ -39388,7 +39388,7 @@ msgstr "La fecha de lanzamiento debe ser en el futuro" #. Label of the relieving_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Relieving Date" -msgstr "Fecha de relevo" +msgstr "" #: erpnext/public/js/bank_reconciliation_tool/dialog_manager.js:125 msgid "Remaining" @@ -40256,7 +40256,7 @@ msgstr "Restablecimiento del acuerdo de nivel de servicio." #. Label of the resignation_letter_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Resignation Letter Date" -msgstr "Fecha de carta de renuncia" +msgstr "" #. Label of the sb_00 (Section Break) field in DocType 'Quality Action' #. Label of the resolution (Text Editor) field in DocType 'Quality Action @@ -40279,7 +40279,7 @@ msgstr "Resolución por" #: erpnext/support/doctype/issue/issue.json #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Resolution Date" -msgstr "Fecha de resolución" +msgstr "" #. Label of the section_break_19 (Section Break) field in DocType 'Issue' #. Label of the resolution_details (Text Editor) field in DocType 'Issue' @@ -40287,7 +40287,7 @@ msgstr "Fecha de resolución" #: erpnext/support/doctype/issue/issue.json #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Resolution Details" -msgstr "Detalles de la resolución" +msgstr "" #. Option for the 'Service Level Agreement Status' (Select) field in DocType #. 'Issue' @@ -40328,7 +40328,7 @@ msgstr "Resuelto" #. Label of the resolved_by (Link) field in DocType 'Warranty Claim' #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Resolved By" -msgstr "Resuelto por" +msgstr "" #. Label of the response_by (Datetime) field in DocType 'Issue' #: erpnext/support/doctype/issue/issue.json @@ -40600,7 +40600,7 @@ msgstr "" #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:138 #: erpnext/stock/doctype/shipment/shipment.json msgid "Returned" -msgstr "Devuelto" +msgstr "" #. Label of the returned_against (Data) field in DocType 'Serial and Batch #. Bundle' @@ -48178,7 +48178,7 @@ msgstr "" #: erpnext/setup/doctype/driver/driver.json #: erpnext/setup/doctype/employee/employee.json msgid "Suspended" -msgstr "Suspendido" +msgstr "" #: erpnext/selling/page/point_of_sale/pos_payment.js:442 msgid "Switch Between Payment Modes" @@ -48529,7 +48529,7 @@ msgstr "Tipo de tarea" #. Option for the '% Complete Method' (Select) field in DocType 'Project' #: erpnext/projects/doctype/project/project.json msgid "Task Weight" -msgstr "Peso de la Tarea" +msgstr "" #: erpnext/projects/doctype/project_template/project_template.py:41 msgid "Task {0} depends on Task {1}. Please add Task {1} to the Tasks list." @@ -49068,7 +49068,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json #: erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.json msgid "Template Name" -msgstr "Nombre de Plantilla" +msgstr "" #. Label of the template_task (Data) field in DocType 'Task' #: erpnext/projects/doctype/task/task.json @@ -49146,7 +49146,7 @@ msgstr "Detalles de términos y condiciones" #: erpnext/stock/doctype/material_request/material_request.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Terms" -msgstr "Términos." +msgstr "" #. Label of the terms_section_break (Section Break) field in DocType 'Purchase #. Order' @@ -50165,7 +50165,7 @@ msgstr "Tiempo requerido (en minutos)" #. Label of the time_sheet (Link) field in DocType 'Sales Invoice Timesheet' #: erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json msgid "Time Sheet" -msgstr "Hoja de horario" +msgstr "" #. Label of the time_sheet_list (Section Break) field in DocType 'POS Invoice' #. Label of the time_sheet_list (Section Break) field in DocType 'Sales @@ -50827,7 +50827,7 @@ msgstr "Importe Total Facturado (a través de Facturas de Ventas)" #. Label of the total_billed_hours (Float) field in DocType 'Timesheet' #: erpnext/projects/doctype/timesheet/timesheet.json msgid "Total Billed Hours" -msgstr "Total de Horas Facturadas" +msgstr "" #. Label of the total_billing_amount (Currency) field in DocType 'POS Invoice' #. Label of the total_billing_amount (Currency) field in DocType 'Sales @@ -50979,7 +50979,7 @@ msgstr "Tiempo total de espera" #. Label of the total_holidays (Int) field in DocType 'Holiday List' #: erpnext/setup/doctype/holiday_list/holiday_list.json msgid "Total Holidays" -msgstr "Vacaciones Totales" +msgstr "" #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:115 msgid "Total Income" @@ -51320,7 +51320,7 @@ msgstr "Impuesto Total" #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Total Taxes and Charges" -msgstr "Total Impuestos y Cargos" +msgstr "" #. Label of the base_total_taxes_and_charges (Currency) field in DocType #. 'Payment Entry' @@ -51432,7 +51432,7 @@ msgstr "" #: erpnext/manufacturing/doctype/workstation/workstation.json #: erpnext/projects/doctype/timesheet/timesheet.json msgid "Total Working Hours" -msgstr "Horas de trabajo total" +msgstr "" #. Label of the total_workstation_time (Int) field in DocType 'Item Lead Time' #: erpnext/stock/doctype/item_lead_time/item_lead_time.json @@ -53873,7 +53873,7 @@ msgstr "Esperando Pago..." #: erpnext/setup/setup_wizard/data/marketing_source.txt:10 msgid "Walk In" -msgstr "Entrar" +msgstr "" #: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js:4 msgid "Warehouse Capacity Summary" diff --git a/erpnext/locale/fa.po b/erpnext/locale/fa.po index 0d8c2b8394f..32d1682d390 100644 --- a/erpnext/locale/fa.po +++ b/erpnext/locale/fa.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-11-10 12:11+0000\n" -"PO-Revision-Date: 2025-11-10 21:18\n" +"PO-Revision-Date: 2025-11-16 22:14\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Persian\n" "MIME-Version: 1.0\n" @@ -1435,7 +1435,7 @@ msgstr "حساب {0} متعلق به شرکت {1} نیست" #: erpnext/accounts/doctype/account/account.py:541 msgid "Account {0} exists in parent company {1}." -msgstr "حساب {0} در شرکت مادر {1} وجود دارد." +msgstr "حساب {0} در شرکت والد {1} وجود دارد." #: erpnext/accounts/doctype/budget/budget.py:114 msgid "Account {0} has been entered multiple times" @@ -3645,7 +3645,7 @@ msgstr "همه این آیتمها قبلاً صورتحساب/بازگردا #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:85 #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:92 msgid "Allocate" -msgstr "" +msgstr "تخصیص" #. Label of the allocate_advances_automatically (Check) field in DocType 'POS #. Invoice' @@ -7703,7 +7703,7 @@ msgstr "بیوتکنولوژی" #. Name of a DocType #: erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json msgid "Bisect Accounting Statements" -msgstr "" +msgstr "صورتهای حسابداری دوبخشی" #: erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js:9 msgid "Bisect Left" @@ -7812,7 +7812,7 @@ msgstr "گروه خونی" #. Accounts' #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json msgid "Body" -msgstr "بدنه" +msgstr "" #. Label of the body_text (Text Editor) field in DocType 'Dunning' #. Label of the body_text (Text Editor) field in DocType 'Dunning Letter Text' @@ -9378,7 +9378,7 @@ msgstr "اگر ثبت انتقال مواد مورد نیاز نیست علام #. Label of the warehouse_group (Link) field in DocType 'Item Reorder' #: erpnext/stock/doctype/item_reorder/item_reorder.json msgid "Check in (group)" -msgstr "اعلام حضور (گروهی)" +msgstr "بررسی در (گروه)" #. Description of the 'Must be Whole Number' (Check) field in DocType 'UOM' #: erpnext/setup/doctype/uom/uom.json @@ -12413,7 +12413,7 @@ msgstr "" #. Description of a DocType #: erpnext/setup/doctype/website_item_group/website_item_group.json msgid "Cross Listing of Item in multiple groups" -msgstr "" +msgstr "لیست کردن متقابل آیتمها در چندین گروه" #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json @@ -14376,7 +14376,7 @@ msgstr "هزینه معوق" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/stock/doctype/item_default/item_default.json msgid "Deferred Expense Account" -msgstr "حساب هزینه معوق" +msgstr "" #. Option for the 'Entry Type' (Select) field in DocType 'Journal Entry' #. Label of the deferred_revenue (Section Break) field in DocType 'POS Invoice @@ -18407,7 +18407,7 @@ msgstr "سال مالی {0} الزامی است" #: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js:28 msgid "Fix SABB Entry" -msgstr "" +msgstr "رفع مشکل ثبت SABB" #. Option for the 'Calculate Based On' (Select) field in DocType 'Shipping #. Rule' @@ -20573,7 +20573,7 @@ msgstr "ساعت" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json msgid "Hour Rate" -msgstr "نرخ ساعت" +msgstr "" #. Label of the hours (Float) field in DocType 'Workstation Working Hour' #: erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json @@ -21311,7 +21311,7 @@ msgstr "در درصد" #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "In Process" -msgstr "در جریان" +msgstr "" #: erpnext/stock/report/item_variant_details/item_variant_details.py:107 msgid "In Production" @@ -22361,7 +22361,7 @@ msgstr "ثبتهای دفتر نامعتبر" #: erpnext/assets/doctype/asset/asset.py:450 msgid "Invalid Net Purchase Amount" -msgstr "" +msgstr "مبلغ خالص خرید نامعتبر است" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:77 #: erpnext/accounts/general_ledger.py:796 @@ -22442,7 +22442,7 @@ msgstr "باندل سریال و دسته نامعتبر" #: erpnext/stock/doctype/stock_entry/stock_entry.py:891 #: erpnext/stock/doctype/stock_entry/stock_entry.py:913 msgid "Invalid Source and Target Warehouse" -msgstr "" +msgstr "انبار منبع و هدف نامعتبر" #: erpnext/controllers/item_variant.py:145 msgid "Invalid Value" @@ -23073,7 +23073,7 @@ msgstr "" #. Label of the is_paid (Check) field in DocType 'Purchase Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json msgid "Is Paid" -msgstr "پرداخت شده" +msgstr "" #. Label of the is_paused (Check) field in DocType 'Job Card' #: erpnext/manufacturing/doctype/job_card/job_card.json @@ -26776,7 +26776,7 @@ msgstr "شماره قطعه تولید کننده {0} نامعتبر اس #. Description of a DocType #: erpnext/stock/doctype/manufacturer/manufacturer.json msgid "Manufacturers used in Items" -msgstr "" +msgstr "تولیدکنندگان مورد استفاده در آیتمها" #. Label of the work_order_details_section (Section Break) field in DocType #. 'Production Plan Sub Assembly Item' @@ -28020,7 +28020,7 @@ msgstr "نحوه پرداختها" #. Label of the model (Data) field in DocType 'Vehicle' #: erpnext/setup/doctype/vehicle/vehicle.json msgid "Model" -msgstr "مدل" +msgstr "" #. Label of the section_break_11 (Section Break) field in DocType 'POS Closing #. Entry' @@ -28491,7 +28491,7 @@ msgstr "مبلغ خالص خرید" #: erpnext/assets/doctype/asset/asset.py:385 msgid "Net Purchase Amount is mandatory" -msgstr "" +msgstr "مبلغ خالص خرید الزامی است" #: erpnext/assets/doctype/asset/asset.py:445 msgid "Net Purchase Amount should be equal to purchase amount of one single Asset." @@ -29657,7 +29657,7 @@ msgstr "جبران برای بعد حسابداری" #: erpnext/setup/doctype/supplier_group/supplier_group.json #: erpnext/stock/doctype/warehouse/warehouse.json msgid "Old Parent" -msgstr "مرجع پیشین" +msgstr "" #. Option for the 'Reconciliation Takes Effect On' (Select) field in DocType #. 'Company' @@ -31522,7 +31522,7 @@ msgstr "دسته والد" #. Label of the parent_company (Link) field in DocType 'Company' #: erpnext/setup/doctype/company/company.json msgid "Parent Company" -msgstr "شرکت مادر" +msgstr "شرکت والد" #: erpnext/setup/doctype/company/company.py:555 msgid "Parent Company must be a group company" @@ -32196,7 +32196,7 @@ msgstr "پرداخت" #: erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json #: erpnext/accounts/doctype/payment_request/payment_request.json msgid "Payment Account" -msgstr "حساب پرداخت" +msgstr "" #. Label of the payment_amount (Currency) field in DocType 'Overdue Payment' #. Label of the payment_amount (Currency) field in DocType 'Payment Schedule' @@ -33687,7 +33687,7 @@ msgstr "لطفا بیش از 500 آیتم را همزمان ایجاد نکنی #: erpnext/accounts/doctype/budget/budget.py:133 msgid "Please enable Applicable on Booking Actual Expenses" -msgstr "لطفاً Applicable on Booking Actual Expenses را فعال کنید" +msgstr "" #: erpnext/accounts/doctype/budget/budget.py:129 msgid "Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses" @@ -33711,7 +33711,7 @@ msgstr "لطفاً {} را در {} فعال کنید تا یک مورد در چ #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:373 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." -msgstr "" +msgstr "لطفاً مطمئن شوید که حساب {0} یک حساب ترازنامه است. می توانید حساب مادر را به حساب ترازنامه تغییر دهید یا حساب دیگری را انتخاب کنید." #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:381 msgid "Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account." @@ -34190,7 +34190,7 @@ msgstr "" #: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js:33 msgid "Please select at least one row to fix" -msgstr "" +msgstr "لطفا حداقل یک ردیف را برای اصلاح انتخاب کنید" #: erpnext/selling/doctype/sales_order/sales_order.js:1274 msgid "Please select atleast one item to continue" @@ -35748,7 +35748,7 @@ msgstr "فرآیندها" #. Voucher Detail' #: erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json msgid "Processing Date" -msgstr "" +msgstr "تاریخ پردازش" #: erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py:52 msgid "Processing XML Files" @@ -37990,7 +37990,7 @@ msgstr "ایجاد درخواست مواد زمانی که موجودی به س #. Label of the complaint_raised_by (Data) field in DocType 'Warranty Claim' #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Raised By" -msgstr "مطرح شده توسط" +msgstr "" #. Label of the raised_by (Data) field in DocType 'Issue' #: erpnext/support/doctype/issue/issue.json @@ -39160,11 +39160,11 @@ msgstr "منابع" #: erpnext/stock/doctype/delivery_note/delivery_note.py:387 msgid "References to Sales Invoices are Incomplete" -msgstr "" +msgstr "ارجاعات به فاکتورهای فروش ناقص است" #: erpnext/stock/doctype/delivery_note/delivery_note.py:382 msgid "References to Sales Orders are Incomplete" -msgstr "" +msgstr "ارجاعات به سفارشهای فروش ناقص است" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:733 msgid "References {0} of type {1} had no outstanding amount left before submitting the Payment Entry. Now they have a negative outstanding amount." @@ -40169,7 +40169,7 @@ msgstr "بازنشانی قرارداد سطح سرویس." #. Label of the resignation_letter_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Resignation Letter Date" -msgstr "تاریخ استعفا نامه" +msgstr "" #. Label of the sb_00 (Section Break) field in DocType 'Quality Action' #. Label of the resolution (Text Editor) field in DocType 'Quality Action @@ -40192,7 +40192,7 @@ msgstr "حل و فصل توسط" #: erpnext/support/doctype/issue/issue.json #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Resolution Date" -msgstr "تاریخ حل و فصل" +msgstr "" #. Label of the section_break_19 (Section Break) field in DocType 'Issue' #. Label of the resolution_details (Text Editor) field in DocType 'Issue' @@ -40241,7 +40241,7 @@ msgstr "حل شد" #. Label of the resolved_by (Link) field in DocType 'Warranty Claim' #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Resolved By" -msgstr "حل شده توسط" +msgstr "" #. Label of the response_by (Datetime) field in DocType 'Issue' #: erpnext/support/doctype/issue/issue.json @@ -40513,7 +40513,7 @@ msgstr "" #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:138 #: erpnext/stock/doctype/shipment/shipment.json msgid "Returned" -msgstr "بازگشت" +msgstr "" #. Label of the returned_against (Data) field in DocType 'Serial and Batch #. Bundle' @@ -40835,7 +40835,7 @@ msgstr "مجموع گرد شده" #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Rounded Total (Company Currency)" -msgstr "کل گرد شده (ارز شرکت)" +msgstr "" #. Label of the rounding_adjustment (Currency) field in DocType 'POS Invoice' #. Label of the rounding_adjustment (Currency) field in DocType 'Purchase @@ -41976,7 +41976,7 @@ msgstr "ردیف {0}: {1} {2} با {3} مطابقت ندارد" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:102 msgid "Row {0}: {2} Item {1} does not exist in {2} {3}" -msgstr "" +msgstr "ردیف {0}: {2} آیتم {1} در {2} {3} وجود ندارد" #: erpnext/utilities/transaction_base.py:558 msgid "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." @@ -42158,7 +42158,7 @@ msgstr "حقوق" #. Label of the salary_currency (Link) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Salary Currency" -msgstr "ارز حقوق و دستمزد" +msgstr "" #. Label of the salary_mode (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json @@ -43579,7 +43579,7 @@ msgstr "حسابی را برای چاپ با ارز حساب انتخاب کنی #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:19 msgid "Select an invoice to load summary data" -msgstr "" +msgstr "برای بارگیری خلاصه دادهها، فاکتور را انتخاب کنید" #: erpnext/selling/doctype/quotation/quotation.js:340 msgid "Select an item from each set to be used in the Sales Order." @@ -44136,7 +44136,7 @@ msgstr "شماره های سریال در ورودی های رزرو موجود #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:333 msgid "Serial Nos {0} are already Delivered. You cannot use them again in Manufacture / Repack entry." -msgstr "" +msgstr "شماره سریالهای {0} قبلاً تحویل داده شدهاند. شما نمیتوانید دوباره از آنها در ثبت ساخت / بستهبندی مجدد استفاده کنید." #. Label of the serial_no_series (Data) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json @@ -44443,7 +44443,7 @@ msgstr "کل مبلغ هزینه خدمات" #. 'Asset Capitalization' #: erpnext/assets/doctype/asset_capitalization/asset_capitalization.json msgid "Service Expenses" -msgstr "هزینه های خدمات" +msgstr "" #. Label of the service_item (Link) field in DocType 'Subcontracting BOM' #: erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.json @@ -44679,7 +44679,7 @@ msgstr "تنظیم هزینه عملیاتی بر اساس مقدار BOM" #: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:88 msgid "Set Parent Row No in Items Table" -msgstr "" +msgstr "تنظیم شماره ردیف والد در جدول آیتمها" #. Label of the set_posting_date (Check) field in DocType 'POS Opening Entry' #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.json @@ -48089,7 +48089,7 @@ msgstr "" #: erpnext/setup/doctype/driver/driver.json #: erpnext/setup/doctype/employee/employee.json msgid "Suspended" -msgstr "معلق" +msgstr "" #: erpnext/selling/page/point_of_sale/pos_payment.js:442 msgid "Switch Between Payment Modes" @@ -48372,7 +48372,7 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order/work_order.py:741 msgid "Target Warehouse is required before Submit" -msgstr "" +msgstr "انبار هدف قبل از ارسال الزامی است" #: erpnext/controllers/selling_controller.py:840 msgid "Target Warehouse is set for some items but the customer is not an internal customer." @@ -50076,7 +50076,7 @@ msgstr "زمان مورد نیاز (بر حسب دقیقه)" #. Label of the time_sheet (Link) field in DocType 'Sales Invoice Timesheet' #: erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json msgid "Time Sheet" -msgstr "برگه زمان" +msgstr "" #. Label of the time_sheet_list (Section Break) field in DocType 'POS Invoice' #. Label of the time_sheet_list (Section Break) field in DocType 'Sales @@ -50738,7 +50738,7 @@ msgstr "کل مبلغ صورتحساب (از طریق فاکتور فروش)" #. Label of the total_billed_hours (Float) field in DocType 'Timesheet' #: erpnext/projects/doctype/timesheet/timesheet.json msgid "Total Billed Hours" -msgstr "مجموع ساعات صورتحساب" +msgstr "" #. Label of the total_billing_amount (Currency) field in DocType 'POS Invoice' #. Label of the total_billing_amount (Currency) field in DocType 'Sales @@ -50890,7 +50890,7 @@ msgstr "کل زمان نگهداری" #. Label of the total_holidays (Int) field in DocType 'Holiday List' #: erpnext/setup/doctype/holiday_list/holiday_list.json msgid "Total Holidays" -msgstr "کل تعطیلات" +msgstr "" #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:115 msgid "Total Income" @@ -51231,7 +51231,7 @@ msgstr "کل مالیات" #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Total Taxes and Charges" -msgstr "کل مالیات ها و هزینه ها" +msgstr "" #. Label of the base_total_taxes_and_charges (Currency) field in DocType #. 'Payment Entry' @@ -52174,7 +52174,7 @@ msgstr "واحد" #: erpnext/controllers/accounts_controller.py:3830 msgid "Unit Price" -msgstr "" +msgstr "قیمت واحد" #: erpnext/buying/report/procurement_tracker/procurement_tracker.py:68 msgid "Unit of Measure" diff --git a/erpnext/locale/fr.po b/erpnext/locale/fr.po index 3783a5050f7..6155825c93c 100644 --- a/erpnext/locale/fr.po +++ b/erpnext/locale/fr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-11-10 12:11+0000\n" -"PO-Revision-Date: 2025-11-10 21:17\n" +"PO-Revision-Date: 2025-11-15 21:35\n" "Last-Translator: hello@frappe.io\n" "Language-Team: French\n" "MIME-Version: 1.0\n" @@ -820,7 +820,7 @@ msgstr "" #. Header text in the Stock Workspace #: erpnext/stock/workspace/stock/stock.json msgid "Masters & Reports" -msgstr "Rapports & Fonctionnalités principales" +msgstr "" #. Header text in the Selling Workspace #. Header text in the Stock Workspace @@ -3056,7 +3056,7 @@ msgstr "Montant de l'Avance" #: erpnext/buying/doctype/purchase_order/purchase_order.json #: erpnext/selling/doctype/sales_order/sales_order.json msgid "Advance Paid" -msgstr "Avance Payée" +msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order_list.js:75 #: erpnext/selling/doctype/sales_order/sales_order_list.js:122 @@ -3159,7 +3159,7 @@ msgstr "Seul les paiements anticipés alloués aux commandes seront uniquement r #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json msgid "Advances" -msgstr "Avances" +msgstr "" #: erpnext/setup/setup_wizard/data/marketing_source.txt:3 msgid "Advertisement" @@ -7824,7 +7824,7 @@ msgstr "Groupe Sanguin" #. Accounts' #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json msgid "Body" -msgstr "Corps" +msgstr "" #. Label of the body_text (Text Editor) field in DocType 'Dunning' #. Label of the body_text (Text Editor) field in DocType 'Dunning Letter Text' @@ -13528,7 +13528,7 @@ msgstr "" #. Label of the date_of_birth (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Date of Birth" -msgstr "Date de naissance" +msgstr "" #: erpnext/setup/doctype/employee/employee.py:147 msgid "Date of Birth cannot be greater than today." @@ -13561,7 +13561,7 @@ msgstr "Date d'Émission" #. Label of the date_of_joining (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Date of Joining" -msgstr "Date d'Embauche" +msgstr "" #: erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py:273 msgid "Date of Transaction" @@ -14388,7 +14388,7 @@ msgstr "Frais différés" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/stock/doctype/item_default/item_default.json msgid "Deferred Expense Account" -msgstr "Compte de dépenses différées" +msgstr "" #. Option for the 'Entry Type' (Select) field in DocType 'Journal Entry' #. Label of the deferred_revenue (Section Break) field in DocType 'POS Invoice @@ -16632,7 +16632,7 @@ msgstr "Formation de l'Employé" #. Label of the exit (Tab Break) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Employee Exit" -msgstr "Sortie de l’employé" +msgstr "" #. Name of a DocType #: erpnext/setup/doctype/employee_external_work_history/employee_external_work_history.json @@ -16675,7 +16675,7 @@ msgstr "Nom de l'Employé" #. Label of the employee_number (Data) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Employee Number" -msgstr "Numéro d'Employé" +msgstr "" #. Label of the employee_user_id (Link) field in DocType 'Call Log' #: erpnext/telephony/doctype/call_log/call_log.json @@ -16892,7 +16892,7 @@ msgstr "" #. Label of the encashment_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Encashment Date" -msgstr "Date de l'Encaissement" +msgstr "" #: erpnext/crm/doctype/contract/contract.py:70 msgid "End Date cannot be before Start Date." @@ -20555,7 +20555,7 @@ msgstr "Nom de la Liste de Vacances" #. Label of the holidays (Table) field in DocType 'Holiday List' #: erpnext/setup/doctype/holiday_list/holiday_list.json msgid "Holidays" -msgstr "Jours Fériés" +msgstr "" #. Option for the 'Forecasting Method' (Select) field in DocType 'Sales #. Forecast' @@ -20585,7 +20585,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json msgid "Hour Rate" -msgstr "Tarif Horaire" +msgstr "" #. Label of the hours (Float) field in DocType 'Workstation Working Hour' #: erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json @@ -21322,7 +21322,7 @@ msgstr "En pourcentage" #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "In Process" -msgstr "En Cours" +msgstr "" #: erpnext/stock/report/item_variant_details/item_variant_details.py:107 msgid "In Production" @@ -22185,7 +22185,7 @@ msgstr "Paramètres de transfert entre entrepôts" #. Label of the interest (Currency) field in DocType 'Overdue Payment' #: erpnext/accounts/doctype/overdue_payment/overdue_payment.json msgid "Interest" -msgstr "Intérêt" +msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:3052 msgid "Interest and/or dunning fee" @@ -29626,7 +29626,7 @@ msgstr "Valeur Compteur Kilométrique (Dernier)" #. Label of the scheduled_confirmation_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Offer Date" -msgstr "Date de la Proposition" +msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:29 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:42 @@ -29668,7 +29668,7 @@ msgstr "" #: erpnext/setup/doctype/supplier_group/supplier_group.json #: erpnext/stock/doctype/warehouse/warehouse.json msgid "Old Parent" -msgstr "Grand Parent" +msgstr "" #. Option for the 'Reconciliation Takes Effect On' (Select) field in DocType #. 'Company' @@ -30957,7 +30957,7 @@ msgstr "Surproduction pour les ventes et les bons de travail" #. Option for the 'Current Address Is' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Owned" -msgstr "Détenu" +msgstr "" #: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js:29 #: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py:23 @@ -32206,7 +32206,7 @@ msgstr "Paiement" #: erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json #: erpnext/accounts/doctype/payment_request/payment_request.json msgid "Payment Account" -msgstr "Compte de Paiement" +msgstr "" #. Label of the payment_amount (Currency) field in DocType 'Overdue Payment' #. Label of the payment_amount (Currency) field in DocType 'Payment Schedule' @@ -32797,7 +32797,7 @@ msgstr "" #. Account' #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json msgid "Payroll Entry" -msgstr "Entrée de la paie" +msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:88 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:119 @@ -38000,7 +38000,7 @@ msgstr "Augmenter la demande d'article lorsque le stock atteint le niveau de com #. Label of the complaint_raised_by (Data) field in DocType 'Warranty Claim' #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Raised By" -msgstr "Créé par" +msgstr "" #. Label of the raised_by (Data) field in DocType 'Issue' #: erpnext/support/doctype/issue/issue.json @@ -39311,7 +39311,7 @@ msgstr "La date de sortie doit être dans le futur" #. Label of the relieving_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Relieving Date" -msgstr "Date de Relève" +msgstr "" #: erpnext/public/js/bank_reconciliation_tool/dialog_manager.js:125 msgid "Remaining" @@ -40179,7 +40179,7 @@ msgstr "Réinitialisation de l'accord de niveau de service." #. Label of the resignation_letter_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Resignation Letter Date" -msgstr "Date de la Lettre de Démission" +msgstr "" #. Label of the sb_00 (Section Break) field in DocType 'Quality Action' #. Label of the resolution (Text Editor) field in DocType 'Quality Action @@ -40523,7 +40523,7 @@ msgstr "" #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:138 #: erpnext/stock/doctype/shipment/shipment.json msgid "Returned" -msgstr "retourné" +msgstr "" #. Label of the returned_against (Data) field in DocType 'Serial and Batch #. Bundle' @@ -40845,7 +40845,7 @@ msgstr "Total arrondi" #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Rounded Total (Company Currency)" -msgstr "Total Arrondi (Devise Société)" +msgstr "" #. Label of the rounding_adjustment (Currency) field in DocType 'POS Invoice' #. Label of the rounding_adjustment (Currency) field in DocType 'Purchase @@ -48449,7 +48449,7 @@ msgstr "Type de tâche" #. Option for the '% Complete Method' (Select) field in DocType 'Project' #: erpnext/projects/doctype/project/project.json msgid "Task Weight" -msgstr "Poids de la Tâche" +msgstr "" #: erpnext/projects/doctype/project_template/project_template.py:41 msgid "Task {0} depends on Task {1}. Please add Task {1} to the Tasks list." @@ -48988,7 +48988,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json #: erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.json msgid "Template Name" -msgstr "Nom du Modèle" +msgstr "" #. Label of the template_task (Data) field in DocType 'Task' #: erpnext/projects/doctype/task/task.json @@ -49066,7 +49066,7 @@ msgstr "Détails du Terme" #: erpnext/stock/doctype/material_request/material_request.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Terms" -msgstr "Termes" +msgstr "" #. Label of the terms_section_break (Section Break) field in DocType 'Purchase #. Order' @@ -51537,7 +51537,7 @@ msgstr "" #: erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py:45 msgid "Transaction Name" -msgstr "Nom de la transaction" +msgstr "" #. Label of the transaction_settings_section (Tab Break) field in DocType #. 'Buying Settings' @@ -53793,7 +53793,7 @@ msgstr "" #: erpnext/setup/setup_wizard/data/marketing_source.txt:10 msgid "Walk In" -msgstr "Spontané" +msgstr "" #: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js:4 msgid "Warehouse Capacity Summary" diff --git a/erpnext/locale/hu.po b/erpnext/locale/hu.po index 709fe886a91..57650ded498 100644 --- a/erpnext/locale/hu.po +++ b/erpnext/locale/hu.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-11-10 12:11+0000\n" -"PO-Revision-Date: 2025-11-10 21:17\n" +"PO-Revision-Date: 2025-11-14 21:33\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Hungarian\n" "MIME-Version: 1.0\n" @@ -25,11 +25,11 @@ msgstr "" #: erpnext/selling/doctype/quotation/quotation.js:73 msgid " Address" -msgstr "" +msgstr " Cím" #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:677 msgid " Amount" -msgstr "" +msgstr " Összeg" #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:114 msgid " BOM" diff --git a/erpnext/locale/id.po b/erpnext/locale/id.po index 8ce82bdc142..1e0a75f7b7f 100644 --- a/erpnext/locale/id.po +++ b/erpnext/locale/id.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-11-10 12:11+0000\n" -"PO-Revision-Date: 2025-11-10 21:18\n" +"PO-Revision-Date: 2025-11-15 21:35\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Indonesian\n" "MIME-Version: 1.0\n" @@ -16771,7 +16771,7 @@ msgstr "" #: erpnext/manufacturing/doctype/workstation/workstation.js:351 msgid "Employees" -msgstr "Para karyawan" +msgstr "" #: erpnext/stock/doctype/batch/batch_list.js:16 msgid "Empty" @@ -19907,7 +19907,7 @@ msgstr "Sasaran dan Prosedur" #. Group in Quality Procedure's connections #: erpnext/quality_management/doctype/quality_procedure/quality_procedure.json msgid "Goals" -msgstr "tujuan" +msgstr "" #. Option for the 'Shipment Type' (Select) field in DocType 'Shipment' #: erpnext/stock/doctype/shipment/shipment.json diff --git a/erpnext/locale/pl.po b/erpnext/locale/pl.po index 934d76b53a8..c0e51ed27a1 100644 --- a/erpnext/locale/pl.po +++ b/erpnext/locale/pl.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-11-10 12:11+0000\n" -"PO-Revision-Date: 2025-11-10 21:17\n" +"PO-Revision-Date: 2025-11-15 21:35\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Polish\n" "MIME-Version: 1.0\n" @@ -16652,7 +16652,7 @@ msgstr "" #. Label of the exit (Tab Break) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Employee Exit" -msgstr "Odejście pracownika" +msgstr "" #. Name of a DocType #: erpnext/setup/doctype/employee_external_work_history/employee_external_work_history.json diff --git a/erpnext/locale/tr.po b/erpnext/locale/tr.po index 290405bfcfa..eded166b760 100644 --- a/erpnext/locale/tr.po +++ b/erpnext/locale/tr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-11-10 12:11+0000\n" -"PO-Revision-Date: 2025-11-10 21:18\n" +"PO-Revision-Date: 2025-11-15 21:35\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Turkish\n" "MIME-Version: 1.0\n" @@ -16709,7 +16709,7 @@ msgstr "Eğitim Hayatı" #. Label of the exit (Tab Break) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Employee Exit" -msgstr "Çalışan Çıkışı" +msgstr "" #. Name of a DocType #: erpnext/setup/doctype/employee_external_work_history/employee_external_work_history.json diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 5813593775d..c3740281f5a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -45,7 +45,7 @@ frappe.ui.form.on("BOM", { return { query: "erpnext.manufacturing.doctype.bom.bom.item_query", filters: { - is_stock_item: 1, + is_stock_item: !frm.doc.is_phantom_bom, }, }; }); @@ -183,7 +183,7 @@ frappe.ui.form.on("BOM", { ); } - if (frm.doc.docstatus == 1) { + if (frm.doc.docstatus == 1 && !frm.doc.is_phantom_bom) { frm.add_custom_button( __("Work Order"), function () { @@ -529,6 +529,14 @@ frappe.ui.form.on("BOM", { frm.set_value("process_loss_qty", qty); }, + + is_phantom_bom(frm) { + frm.doc.item = ""; + frm.doc.uom = ""; + frm.doc.quantity = 1; + frm.doc.items = undefined; + frm.refresh(); + }, }); frappe.ui.form.on("BOM Operation", { diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 9b89f08d214..86ecff52c11 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -16,6 +16,7 @@ "is_default", "allow_alternative_item", "set_rate_of_sub_assembly_item_based_on_bom", + "is_phantom_bom", "project", "image", "currency_detail", @@ -201,6 +202,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "currency_detail", "fieldtype": "Section Break", "label": "Cost Configuration" @@ -293,6 +295,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "scrap_section", "fieldtype": "Tab Break", "label": "Scrap & Process Loss" @@ -310,6 +313,7 @@ "oldfieldtype": "Section Break" }, { + "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "operating_cost", "fieldtype": "Currency", "label": "Operating Cost", @@ -324,6 +328,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "scrap_material_cost", "fieldtype": "Currency", "label": "Scrap Material Cost", @@ -336,6 +341,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "base_operating_cost", "fieldtype": "Currency", "label": "Operating Cost (Company Currency)", @@ -352,6 +358,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "base_scrap_material_cost", "fieldtype": "Currency", "label": "Scrap Material Cost(Company Currency)", @@ -380,6 +387,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "project", "fieldtype": "Link", "label": "Project", @@ -427,6 +435,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "website_section", "fieldtype": "Tab Break", "label": "Website" @@ -536,6 +545,7 @@ { "collapsible": 1, "collapsible_depends_on": "eval:doc.with_operations", + "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "operations_section_section", "fieldtype": "Section Break", "label": "Operations" @@ -570,6 +580,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "quality_inspection_section_break", "fieldtype": "Section Break", "label": "Quality Inspection" @@ -659,6 +670,12 @@ "fieldtype": "Link", "label": "Default Target Warehouse", "options": "Warehouse" + }, + { + "default": "0", + "fieldname": "is_phantom_bom", + "fieldtype": "Check", + "label": "Is Phantom BOM" } ], "icon": "fa fa-sitemap", @@ -666,7 +683,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2025-10-29 17:43:12.966753", + "modified": "2025-11-06 15:27:54.806116", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 39f0a0a4258..754a64e11bb 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -135,6 +135,7 @@ class BOM(WebsiteGenerator): inspection_required: DF.Check is_active: DF.Check is_default: DF.Check + is_phantom_bom: DF.Check item: DF.Link item_name: DF.Data | None items: DF.Table[BOMItem] @@ -447,6 +448,9 @@ class BOM(WebsiteGenerator): "uom": args["uom"] if args.get("uom") else item and args["stock_uom"] or "", "conversion_factor": args["conversion_factor"] if args.get("conversion_factor") else 1, "bom_no": args["bom_no"], + "is_phantom_item": frappe.get_value("BOM", args["bom_no"], "is_phantom_bom") + if args["bom_no"] + else 0, "rate": rate, "qty": args.get("qty") or args.get("stock_qty") or 1, "stock_qty": args.get("stock_qty") or args.get("qty") or 1, @@ -455,6 +459,9 @@ class BOM(WebsiteGenerator): "sourced_by_supplier": args.get("sourced_by_supplier", 0), } + if ret_item["is_phantom_item"]: + ret_item["do_not_explode"] = 0 + if args.get("do_not_explode"): ret_item["bom_no"] = "" @@ -481,7 +488,9 @@ class BOM(WebsiteGenerator): if not frappe.db.get_value("Item", arg["item_code"], "is_customer_provided_item") and not arg.get( "sourced_by_supplier" ): - if arg.get("bom_no") and self.set_rate_of_sub_assembly_item_based_on_bom: + if arg.get("bom_no") and ( + self.set_rate_of_sub_assembly_item_based_on_bom or arg.get("is_phantom_item") + ): rate = flt(self.get_bom_unitcost(arg["bom_no"])) * (arg.get("conversion_factor") or 1) else: rate = get_bom_item_rate(arg, self) @@ -888,7 +897,7 @@ class BOM(WebsiteGenerator): for d in self.get("items"): old_rate = d.rate - if not self.bom_creator and d.is_stock_item: + if not self.bom_creator and (d.is_stock_item or d.is_phantom_item): d.rate = self.get_rm_rate( { "company": self.company, @@ -899,6 +908,7 @@ class BOM(WebsiteGenerator): "stock_uom": d.stock_uom, "conversion_factor": d.conversion_factor, "sourced_by_supplier": d.sourced_by_supplier, + "is_phantom_item": d.is_phantom_item, } ) @@ -1277,16 +1287,16 @@ def get_bom_items_as_dict( where bom_item.docstatus < 2 and bom.name = %(bom)s - and item.is_stock_item in (1, {is_stock_item}) + and (item.is_stock_item in (1, {is_stock_item}) {where_conditions} {group_by_cond} order by idx""" - is_stock_item = 0 if include_non_stock_items else 1 + is_stock_item = cint(not include_non_stock_items) if cint(fetch_exploded): query = query.format( table="BOM Explosion Item", - where_conditions="", + where_conditions=")", is_stock_item=is_stock_item, qty_field="stock_qty", group_by_cond=group_by_cond, @@ -1301,7 +1311,7 @@ def get_bom_items_as_dict( elif fetch_scrap_items: query = query.format( table="BOM Scrap Item", - where_conditions="", + where_conditions=")", select_columns=", item.description", is_stock_item=is_stock_item, qty_field="stock_qty", @@ -1312,12 +1322,12 @@ def get_bom_items_as_dict( else: query = query.format( table="BOM Item", - where_conditions="", + where_conditions="or bom_item.is_phantom_item)", is_stock_item=is_stock_item, qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty", select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier, - bom_item.description, bom_item.base_rate as rate, bom_item.operation_row_id """, + bom_item.description, bom_item.base_rate as rate, bom_item.operation_row_id, bom_item.is_phantom_item , bom_item.bom_no """, group_by_cond=group_by_cond, ) items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True) @@ -1327,7 +1337,24 @@ def get_bom_items_as_dict( if item.operation_row_id: key = (item.item_code, item.operation_row_id) - if key in item_dict: + if item.get("is_phantom_item"): + data = get_bom_items_as_dict( + item.get("bom_no"), + company, + qty=item.get("qty"), + fetch_exploded=fetch_exploded, + fetch_scrap_items=fetch_scrap_items, + include_non_stock_items=include_non_stock_items, + fetch_qty_in_stock_uom=fetch_qty_in_stock_uom, + ) + + for k, v in data.items(): + if item_dict.get(k): + item_dict[k]["qty"] += flt(v.qty) + else: + item_dict[k] = v + + elif key in item_dict: item_dict[key]["qty"] += flt(item.qty) else: item_dict[key] = item @@ -1379,7 +1406,7 @@ def validate_bom_no(item, bom_no): @frappe.whitelist() -def get_children(parent=None, is_root=False, **filters): +def get_children(parent=None, return_all=True, fetch_phantom_items=False, is_root=False, **filters): if not parent or parent == "BOM": frappe.msgprint(_("Please select a BOM")) return @@ -1391,10 +1418,13 @@ def get_children(parent=None, is_root=False, **filters): bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent) frappe.has_permission("BOM", doc=bom_doc, throw=True) + filters = [["parent", "=", frappe.form_dict.parent]] + if not return_all: + filters.append(["is_phantom_item", "=", cint(fetch_phantom_items)]) bom_items = frappe.get_all( "BOM Item", - fields=["item_code", "bom_no as value", "stock_qty", "qty"], - filters=[["parent", "=", frappe.form_dict.parent]], + fields=["item_code", "bom_no as value", "stock_qty", "qty", "is_phantom_item", "bom_no"], + filters=filters, order_by="idx", ) diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html index 4cd06bbd024..06dd4365c67 100644 --- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html +++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html @@ -12,7 +12,10 @@ {{ __("Description") }}
{{ __("Phantom Item") }}
+ {% endif %} +{{ data.description }}
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index a185b2d0962..000b4723e59 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -794,7 +794,7 @@ def level_order_traversal(node):
return traversal
-def create_nested_bom(tree, prefix="_Test bom ", submit=True):
+def create_nested_bom(tree, prefix="_Test bom ", submit=True, phantom_items=None):
"""Helper function to create a simple nested bom from tree describing item names. (along with required items)"""
def create_items(bom_tree):
@@ -806,6 +806,9 @@ def create_nested_bom(tree, prefix="_Test bom ", submit=True):
).insert()
create_items(subtree)
+ if not phantom_items:
+ phantom_items = []
+
create_items(tree)
def dfs(tree, node):
@@ -824,7 +827,7 @@ def create_nested_bom(tree, prefix="_Test bom ", submit=True):
child_items = dfs(tree, item)
if child_items:
bom_item_code = prefix + item
- bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
+ bom = frappe.get_doc(doctype="BOM", item=bom_item_code, is_phantom_bom=item in phantom_items)
for child_item in child_items.keys():
bom.append("items", {"item_code": prefix + child_item})
bom.company = "_Test Company"
@@ -906,3 +909,15 @@ def create_process_loss_bom_item(item_tuple):
return make_item(item_code, {"stock_uom": stock_uom, "valuation_rate": 100})
else:
return frappe.get_doc("Item", item_code)
+
+
+def create_tree_for_phantom_bom_tests(): # returns expected explosion result
+ bom_tree_1 = {
+ "Top Level Parent": {
+ "Sub Assembly Level 1-1": {"Phantom Item Level 1-2": {"Item Level 1-3": {}}},
+ "Phantom Item Level 2-1": {"Phantom Item Level 2-2": {"Item Level 2-3": {}}},
+ }
+ }
+ phantom_list = ["Phantom Item Level 1-2", "Phantom Item Level 2-1", "Phantom Item Level 2-2"]
+ create_nested_bom(bom_tree_1, prefix="", phantom_items=phantom_list)
+ return ["Sub Assembly Level 1-1", "Item Level 2-3"]
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
index 69954c47ecb..d67f0c3536b 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -6,7 +6,7 @@ from collections import OrderedDict
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cint, flt
+from frappe.utils import cint, flt, sbool
from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate
@@ -29,6 +29,7 @@ BOM_ITEM_FIELDS = [
"conversion_factor",
"do_not_explode",
"operation",
+ "is_phantom_item",
]
@@ -305,6 +306,7 @@ class BOMCreator(Document):
"allow_alternative_item": 1,
"bom_creator": self.name,
"bom_creator_item": bom_creator_item,
+ "is_phantom_bom": row.get("is_phantom_item"),
}
)
@@ -332,7 +334,7 @@ class BOMCreator(Document):
{
"bom_no": bom_no,
"allow_alternative_item": 1,
- "allow_scrap_items": 1,
+ "allow_scrap_items": not item.get("is_phantom_item"),
"include_item_in_manufacturing": 1,
}
)
@@ -456,12 +458,16 @@ def add_sub_assembly(**kwargs):
"is_expandable": 1,
"stock_uom": item_info.stock_uom,
"operation": bom_item.operation,
+ "is_phantom_item": sbool(kwargs.phantom),
},
)
parent_row_no = item_row.idx
name = ""
else:
+ if sbool(kwargs.phantom):
+ parent_row = next(item for item in doc.items if item.name == kwargs.fg_reference_id)
+ parent_row.db_set("is_phantom_item", 1)
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
for row in bom_item.get("items"):
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
index baf31722838..c5b39d88735 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
@@ -15,6 +15,7 @@
"sourced_by_supplier",
"bom_created",
"is_subcontracted",
+ "is_phantom_item",
"operation_section",
"operation",
"column_break_cbnk",
@@ -159,8 +160,8 @@
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
- "read_only": 1,
- "options": "currency"
+ "options": "currency",
+ "read_only": 1
},
{
"fieldname": "column_break_yuca",
@@ -229,6 +230,7 @@
"print_hide": 1
},
{
+ "depends_on": "eval:!doc.is_phantom_item",
"fieldname": "operation_section",
"fieldtype": "Section Break",
"label": "Operation"
@@ -245,22 +247,31 @@
},
{
"default": "0",
+ "depends_on": "eval:!doc.is_phantom_item",
"fieldname": "is_subcontracted",
"fieldtype": "Check",
"label": "Is Subcontracted",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_phantom_item",
+ "fieldtype": "Check",
+ "label": "Is Phantom Item",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-11-25 18:13:34.542391",
+ "modified": "2025-11-05 21:15:55.187671",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator Item",
"owner": "Administrator",
"permissions": [],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
index 01f93719df4..d734cc0cda4 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
@@ -25,6 +25,7 @@ class BOMCreatorItem(Document):
fg_reference_id: DF.Data | None
instruction: DF.SmallText | None
is_expandable: DF.Check
+ is_phantom_item: DF.Check
is_subcontracted: DF.Check
item_code: DF.Link
item_group: DF.Link | None
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json
index 1861207fd66..52e7d4da609 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.json
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json
@@ -42,7 +42,8 @@
"original_item",
"column_break_33",
"sourced_by_supplier",
- "is_sub_assembly_item"
+ "is_sub_assembly_item",
+ "is_phantom_item"
],
"fields": [
{
@@ -81,6 +82,7 @@
"fieldtype": "Link",
"in_filter": 1,
"label": "BOM No",
+ "mandatory_depends_on": "eval:doc.is_phantom_item",
"oldfieldname": "bom_no",
"oldfieldtype": "Link",
"options": "BOM",
@@ -278,6 +280,7 @@
},
{
"default": "0",
+ "depends_on": "eval:!doc.is_phantom_item",
"fieldname": "sourced_by_supplier",
"fieldtype": "Check",
"label": "Sourced by Supplier"
@@ -286,7 +289,8 @@
"default": "0",
"fieldname": "do_not_explode",
"fieldtype": "Check",
- "label": "Do Not Explode"
+ "label": "Do Not Explode",
+ "read_only_depends_on": "eval:doc.is_phantom_item"
},
{
"default": "0",
@@ -304,18 +308,26 @@
},
{
"default": "0",
+ "depends_on": "eval:!doc.is_phantom_item",
"fieldname": "is_sub_assembly_item",
"fieldtype": "Check",
"label": "Is Sub Assembly Item",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_phantom_item",
+ "fieldtype": "Check",
+ "label": "Is Phantom Item",
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2025-08-12 20:01:59.532613",
+ "modified": "2025-11-05 19:00:38.646539",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Item",
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.py b/erpnext/manufacturing/doctype/bom_item/bom_item.py
index 91177bc72ef..6f58edb24b0 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.py
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.py
@@ -25,6 +25,7 @@ class BOMItem(Document):
has_variants: DF.Check
image: DF.Attach | None
include_item_in_manufacturing: DF.Check
+ is_phantom_item: DF.Check
is_stock_item: DF.Check
is_sub_assembly_item: DF.Check
item_code: DF.Link
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 683ce626bc1..1bd0cc4a58f 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -1085,7 +1085,7 @@ class JobCard(Document):
def set_wip_warehouse(self):
if not self.wip_warehouse:
- self.wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
+ self.wip_warehouse = frappe.get_cached_value("Company", self.company, "default_wip_warehouse")
def validate_operation_id(self):
if (
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js
index f54478a1c10..17c9c085b12 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js
@@ -18,18 +18,6 @@ frappe.tour["Manufacturing Settings"] = [
"The Stock Entry of type 'Manufacture' is known as backflush. Raw materials being consumed to manufacture finished goods is known as backflushing.
When creating Manufacture Entry, raw-material items are backflushed based on BOM of production item. If you want raw-material items to be backflushed based on Material Transfer entry made against that Work Order instead, then you can set it under this field."
),
},
- {
- fieldname: "default_wip_warehouse",
- title: __("Work In Progress Warehouse"),
- description: __(
- "This Warehouse will be auto-updated in the Work In Progress Warehouse field of Work Orders."
- ),
- },
- {
- fieldname: "default_fg_warehouse",
- title: __("Finished Goods Warehouse"),
- description: __("This Warehouse will be auto-updated in the Target Warehouse field of Work Order."),
- },
{
fieldname: "update_bom_costs_automatically",
title: __("Update BOM Cost Automatically"),
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
index 80e97f8fb46..a43804c0191 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
@@ -16,11 +16,6 @@
"update_bom_costs_automatically",
"column_break_lhyt",
"allow_editing_of_items_and_quantities_in_work_order",
- "section_break_6",
- "default_wip_warehouse",
- "default_fg_warehouse",
- "column_break_11",
- "default_scrap_warehouse",
"over_production_for_sales_and_work_order_section",
"overproduction_percentage_for_sales_order",
"column_break_16",
@@ -86,11 +81,6 @@
"fieldtype": "Int",
"label": "Time Between Operations (Mins)"
},
- {
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "label": "Default Warehouses for Production"
- },
{
"fieldname": "overproduction_percentage_for_sales_order",
"fieldtype": "Percent",
@@ -122,34 +112,12 @@
"fieldtype": "Check",
"label": "Update BOM Cost Automatically"
},
- {
- "fieldname": "column_break_11",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "default_wip_warehouse",
- "fieldtype": "Link",
- "label": "Default Work In Progress Warehouse",
- "options": "Warehouse"
- },
- {
- "fieldname": "default_fg_warehouse",
- "fieldtype": "Link",
- "label": "Default Finished Goods Warehouse",
- "options": "Warehouse"
- },
{
"default": "0",
"fieldname": "disable_capacity_planning",
"fieldtype": "Check",
"label": "Disable Capacity Planning"
},
- {
- "fieldname": "default_scrap_warehouse",
- "fieldtype": "Link",
- "label": "Default Scrap Warehouse",
- "options": "Warehouse"
- },
{
"fieldname": "over_production_for_sales_and_work_order_section",
"fieldtype": "Section Break",
@@ -275,7 +243,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2025-11-07 14:52:56.241459",
+ "modified": "2025-11-13 12:30:29.006822",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
index b3d7742929e..e60a9627a21 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
@@ -23,9 +23,6 @@ class ManufacturingSettings(Document):
allow_production_on_holidays: DF.Check
backflush_raw_materials_based_on: DF.Literal["BOM", "Material Transferred for Manufacture"]
capacity_planning_for_days: DF.Int
- default_fg_warehouse: DF.Link | None
- default_scrap_warehouse: DF.Link | None
- default_wip_warehouse: DF.Link | None
disable_capacity_planning: DF.Check
enforce_time_logs: DF.Check
get_rm_cost_from_consumption_entry: DF.Check
diff --git a/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.js b/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.js
index 2373ba2a2c7..7899183e7f9 100644
--- a/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.js
+++ b/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.js
@@ -6,6 +6,8 @@ frappe.ui.form.on("Master Production Schedule", {
frm.trigger("set_query_filters");
frm.set_df_property("items", "cannot_add_rows", true);
+ frm.set_df_property("material_requests", "cannot_add_rows", true);
+ frm.set_df_property("sales_orders", "cannot_add_rows", true);
frm.fields_dict.items.$wrapper.find("[data-action='duplicate_rows']").css("display", "none");
frm.trigger("set_custom_buttons");
@@ -36,6 +38,14 @@ frappe.ui.form.on("Master Production Schedule", {
},
};
});
+
+ frm.set_query("sales_forecast", (doc) => {
+ return {
+ filters: {
+ company: doc.company,
+ },
+ };
+ });
},
get_actual_demand(frm) {
diff --git a/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.json b/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.json
index 3110c44bcfc..39e4bbb16ad 100644
--- a/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.json
+++ b/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.json
@@ -26,8 +26,8 @@
"material_requests",
"section_break_xtby",
"column_break_yhkr",
- "column_break_vvys",
"get_actual_demand",
+ "column_break_vvys",
"section_break_cmgo",
"items",
"forecast_demand_section",
@@ -60,7 +60,6 @@
"fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
@@ -189,7 +188,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2025-09-02 19:33:28.244544",
+ "modified": "2025-11-13 19:15:36.090622",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Master Production Schedule",
diff --git a/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.py b/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.py
index a558d2bd19a..9698bd8eb49 100644
--- a/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.py
+++ b/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.py
@@ -4,7 +4,7 @@
import math
import frappe
-from frappe import _
+from frappe import _, bold
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
@@ -64,6 +64,22 @@ class MasterProductionSchedule(Document):
def validate(self):
self.set_to_date()
+ self.validate_company()
+
+ def validate_company(self):
+ if self.sales_forecast:
+ sales_forecast_company = frappe.db.get_value("Sales Forecast", self.sales_forecast, "company")
+ if sales_forecast_company != self.company:
+ frappe.throw(
+ _(
+ "The Company {0} of Sales Forecast {1} does not match with the Company {2} of Master Production Schedule {3}."
+ ).format(
+ bold(sales_forecast_company),
+ bold(self.sales_forecast),
+ bold(self.company),
+ bold(self.name),
+ )
+ )
def set_to_date(self):
self.to_date = None
diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
index f10db8d8a7c..8bc37d2e02d 100644
--- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
+++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
@@ -8,12 +8,16 @@
"item_code",
"from_warehouse",
"warehouse",
- "item_name",
"material_request_type",
"column_break_4",
+ "item_name",
"uom",
"conversion_factor",
"section_break_azee",
+ "from_bom",
+ "column_break_scnz",
+ "main_item_code",
+ "section_break_qnpt",
"required_bom_qty",
"projected_qty",
"column_break_wack",
@@ -25,6 +29,7 @@
"min_order_qty",
"section_break_8",
"sales_order",
+ "sub_assembly_item_reference",
"bin_qty_section",
"actual_qty",
"requested_qty",
@@ -220,12 +225,48 @@
"label": "Stock Reserved Qty",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "depends_on": "from_bom",
+ "fieldname": "from_bom",
+ "fieldtype": "Link",
+ "label": "From BOM",
+ "mandatory_depends_on": "eval:parent.reserve_stock",
+ "no_copy": 1,
+ "options": "BOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "sub_assembly_item_reference",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Sub Assembly Item Reference",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_qnpt",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "main_item_code",
+ "fieldname": "main_item_code",
+ "fieldtype": "Link",
+ "label": "Main Item Code",
+ "mandatory_depends_on": "eval:parent.reserve_stock",
+ "no_copy": 1,
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_scnz",
+ "fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"istable": 1,
"links": [],
- "modified": "2025-05-01 14:50:55.805442",
+ "modified": "2025-10-30 17:01:25.996352",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Material Request Plan Item",
diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py
index 2b6e0994f46..44c706c10ab 100644
--- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py
+++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py
@@ -17,9 +17,11 @@ class MaterialRequestPlanItem(Document):
actual_qty: DF.Float
conversion_factor: DF.Float
description: DF.TextEditor | None
+ from_bom: DF.Link | None
from_warehouse: DF.Link | None
item_code: DF.Link
item_name: DF.Data | None
+ main_item_code: DF.Link | None
material_request_type: DF.Literal[
"",
"Purchase",
@@ -43,6 +45,7 @@ class MaterialRequestPlanItem(Document):
sales_order: DF.Link | None
schedule_date: DF.Date | None
stock_reserved_qty: DF.Float
+ sub_assembly_item_reference: DF.Data | None
uom: DF.Link | None
warehouse: DF.Link
# end: auto-generated types
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 8a7133490c9..ff737dff630 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -568,6 +568,7 @@ class ProductionPlan(Document):
def on_submit(self):
self.update_bin_qty()
self.update_sales_order()
+ self.add_reference_to_raw_materials()
self.update_stock_reservation()
def on_cancel(self):
@@ -583,6 +584,24 @@ class ProductionPlan(Document):
make_stock_reservation_entries(self)
+ def add_reference_to_raw_materials(self):
+ for item in self.mr_items:
+ if reference := next(
+ (
+ sa_item.name
+ for sa_item in self.sub_assembly_items
+ if sa_item.production_item == item.main_item_code and sa_item.bom_no == item.from_bom
+ ),
+ None,
+ ):
+ item.db_set("sub_assembly_item_reference", reference)
+ elif self.reserve_stock and item.main_item_code and item.from_bom:
+ frappe.throw(
+ _(
+ "Sub assembly item references are missing. Please fetch the sub assemblies and raw materials again."
+ )
+ )
+
def update_sales_order(self):
sales_orders = [row.sales_order for row in self.po_items if row.sales_order]
if sales_orders:
@@ -737,7 +756,7 @@ class ProductionPlan(Document):
wo_list, po_list = [], []
subcontracted_po = {}
- default_warehouses = get_default_warehouse()
+ default_warehouses = get_default_warehouse(self.company)
self.make_work_order_for_finished_goods(wo_list, default_warehouses)
self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses)
@@ -1335,14 +1354,19 @@ def get_subitems(
item.purchase_uom,
item_uom.conversion_factor,
bom.item.as_("main_bom_item"),
+ bom_item.is_phantom_item,
)
.where(
(bom.name == bom_no)
& (bom_item.is_sub_assembly_item == 0)
& (bom_item.docstatus < 2)
- & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
+ & (
+ (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
+ | (bom_item.is_phantom_item == 1)
+ )
)
.groupby(bom_item.item_code)
+ .orderby(bom_item.idx)
).run(as_dict=True)
for d in items:
@@ -1355,10 +1379,12 @@ def get_subitems(
item_details[d.item_code] = d
- if data.get("include_exploded_items") and d.default_bom:
+ if d.is_phantom_item or (data.get("include_exploded_items") and d.default_bom):
if (
- d.default_material_request_type in ["Manufacture", "Purchase"] and not d.is_sub_contracted
- ) or (d.is_sub_contracted and include_subcontracted_items):
+ (d.default_material_request_type in ["Manufacture", "Purchase"] and not d.is_sub_contracted)
+ or (d.is_sub_contracted and include_subcontracted_items)
+ or d.is_phantom_item
+ ):
if d.qty > 0:
get_subitems(
doc,
@@ -1370,7 +1396,7 @@ def get_subitems(
include_subcontracted_items,
d.qty,
)
- return item_details
+ return {key: value for key, value in item_details.items() if not value.get("is_phantom_item")}
def get_material_request_items(
@@ -1382,14 +1408,14 @@ def get_material_request_items(
include_safety_stock,
warehouse,
bin_dict,
+ total_qty,
):
- total_qty = row["qty"]
-
required_qty = 0
if not ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
- required_qty = total_qty
- elif total_qty > bin_dict.get("projected_qty", 0):
- required_qty = total_qty - bin_dict.get("projected_qty", 0)
+ required_qty = total_qty[row.get("item_code")]
+ elif total_qty[row.get("item_code")] > bin_dict.get("projected_qty", 0):
+ required_qty = total_qty[row.get("item_code")] - bin_dict.get("projected_qty", 0)
+ total_qty[row.get("item_code")] -= required_qty
if doc.get("consider_minimum_order_qty") and required_qty > 0 and required_qty < row["min_order_qty"]:
required_qty = row["min_order_qty"]
@@ -1432,7 +1458,7 @@ def get_material_request_items(
"item_name": row.item_name,
"quantity": required_qty / conversion_factor,
"conversion_factor": conversion_factor,
- "required_bom_qty": total_qty,
+ "required_bom_qty": row.get("qty"),
"stock_uom": row.get("stock_uom"),
"warehouse": warehouse
or row.get("source_warehouse")
@@ -1448,7 +1474,8 @@ def get_material_request_items(
"sales_order": sales_order,
"description": row.get("description"),
"uom": row.get("purchase_uom") or row.get("stock_uom"),
- "main_bom_item": row.get("main_bom_item"),
+ "main_item_code": row.get("main_bom_item"),
+ "from_bom": row.get("main_bom"),
}
@@ -1629,7 +1656,27 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
sub_assembly_items = defaultdict(int)
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
for d in doc.get("sub_assembly_items"):
- sub_assembly_items[(d.get("production_item"), d.get("bom_no"))] += d.get("qty")
+ sub_assembly_items[
+ (d.get("production_item"), d.get("bom_no"), d.get("type_of_manufacturing"))
+ ] += d.get("qty")
+ sub_assembly_items = {k[:2]: v for k, v in sub_assembly_items.items()}
+
+ data = []
+ for row in doc.get("po_items"):
+ get_sub_assembly_items(
+ [],
+ frappe._dict(),
+ row.get("bom_no"),
+ data,
+ row.get("planned_qty"),
+ doc.get("company"),
+ warehouse=doc.get("sub_assembly_warehouse"),
+ skip_available_sub_assembly_item=doc.get("skip_available_sub_assembly_item"),
+ fetch_phantom_items=True,
+ )
+
+ for d in data:
+ sub_assembly_items[(d.get("production_item"), d.get("bom_no"))] += d.get("stock_qty")
for data in po_items:
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
@@ -1668,7 +1715,6 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
sub_assembly_items,
planned_qty=planned_qty,
)
-
elif data.get("include_exploded_items") and include_subcontracted_items:
# fetch exploded items from BOM
item_details = get_exploded_items(
@@ -1698,7 +1744,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
get_uom_conversion_factor(item_master.name, purchase_uom) if item_master.purchase_uom else 1.0
)
- item_details[item_master.name] = frappe._dict(
+ item_details[item_master.item_code] = frappe._dict(
{
"item_name": item_master.item_name,
"default_bom": doc.bom,
@@ -1707,7 +1753,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
"min_order_qty": item_master.min_order_qty,
"default_material_request_type": item_master.default_material_request_type,
"qty": planned_qty or 1,
- "is_sub_contracted": item_master.is_subcontracted_item,
+ "is_sub_contracted": item_master.is_sub_contracted_item,
"item_code": item_master.name,
"description": item_master.description,
"stock_uom": item_master.stock_uom,
@@ -1718,19 +1764,21 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
sales_order = data.get("sales_order")
- for item_code, details in item_details.items():
+ for key, details in item_details.items():
so_item_details.setdefault(sales_order, frappe._dict())
- if item_code in so_item_details.get(sales_order, {}):
- so_item_details[sales_order][item_code]["qty"] = so_item_details[sales_order][item_code].get(
+ if key in so_item_details.get(sales_order, {}):
+ so_item_details[sales_order][key]["qty"] = so_item_details[sales_order][key].get(
"qty", 0
) + flt(details.qty)
else:
- so_item_details[sales_order][item_code] = details
+ so_item_details[sales_order][key] = details
mr_items = []
for sales_order in so_item_details:
item_dict = so_item_details[sales_order]
+ total_qty = defaultdict(float)
for details in item_dict.values():
+ total_qty[details.item_code] += flt(details.qty)
bin_dict = get_bin_details(details, doc.company, warehouse)
bin_dict = bin_dict[0] if bin_dict else {}
@@ -1744,6 +1792,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
include_safety_stock,
warehouse,
bin_dict,
+ total_qty,
)
if items:
mr_items.append(items)
@@ -1847,8 +1896,9 @@ def get_sub_assembly_items(
warehouse=None,
indent=0,
skip_available_sub_assembly_item=False,
+ fetch_phantom_items=False,
):
- data = get_bom_children(parent=bom_no)
+ data = get_bom_children(parent=bom_no, return_all=False, fetch_phantom_items=fetch_phantom_items)
for d in data:
if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
@@ -1892,6 +1942,7 @@ def get_sub_assembly_items(
"projected_qty": bin_details[d.item_code][0].get("projected_qty", 0)
if bin_details.get(d.item_code)
else 0,
+ "main_bom": bom_no,
}
)
)
@@ -1907,6 +1958,7 @@ def get_sub_assembly_items(
warehouse,
indent=indent + 1,
skip_available_sub_assembly_item=skip_available_sub_assembly_item,
+ fetch_phantom_items=fetch_phantom_items,
)
@@ -1998,7 +2050,7 @@ def get_raw_materials_of_sub_assembly_items(
item_default = frappe.qb.DocType("Item Default")
item_uom = frappe.qb.DocType("UOM Conversion Detail")
- items = (
+ query = (
frappe.qb.from_(bei)
.join(bom)
.on(bom.name == bei.parent)
@@ -2014,6 +2066,7 @@ def get_raw_materials_of_sub_assembly_items(
item.name.as_("item_code"),
bei.description,
bei.stock_uom,
+ bei.is_phantom_item,
bei.bom_no,
item.min_order_qty,
bei.source_warehouse,
@@ -2024,19 +2077,28 @@ def get_raw_materials_of_sub_assembly_items(
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
+ bom.name.as_("main_bom"),
)
.where(
(bei.docstatus == 1)
& (bei.is_sub_assembly_item == 0)
& (bom.name == bom_no)
- & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
+ & (
+ (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
+ | (bei.is_phantom_item == 1)
+ )
)
.groupby(bei.item_code, bei.stock_uom)
- ).run(as_dict=True)
+ )
- for item in items:
+ for item in query.run(as_dict=True):
key = (item.item_code, item.bom_no)
- if (item.bom_no and key not in sub_assembly_items) or (item.item_code in existing_sub_assembly_items):
+ if item.is_phantom_item:
+ sub_assembly_items[key] += item.get("qty")
+
+ if (item.bom_no and key not in sub_assembly_items) or (
+ (item.item_code, item.bom_no or item.main_bom) in existing_sub_assembly_items
+ ):
continue
if item.bom_no:
@@ -2050,15 +2112,15 @@ def get_raw_materials_of_sub_assembly_items(
sub_assembly_items,
planned_qty=planned_qty,
)
- existing_sub_assembly_items.add(item.item_code)
+ existing_sub_assembly_items.add((item.item_code, item.bom_no or item.main_bom))
else:
if not item.conversion_factor and item.purchase_uom:
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
- if details := item_details.get(item.get("item_code")):
+ if details := item_details.get((item.get("item_code"), item.get("main_bom"))):
details.qty += item.get("qty")
else:
- item_details.setdefault(item.get("item_code"), item)
+ item_details.setdefault((item.get("item_code"), item.get("main_bom")), item)
return item_details
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index e7fe26c0ce5..841a1e42b22 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1944,11 +1944,17 @@ class TestProductionPlan(IntegrationTestCase):
mr_items = get_items_for_material_requests(plan.as_dict())
+ from collections import defaultdict
+
+ mr_items_dict = defaultdict(float)
+ for item in mr_items:
+ mr_items_dict[item.get("item_code")] += item.get("quantity")
+
# RM Item 1 (FG1 (100 + 100) + FG2 (50) + FG3 (10) - 90 in stock - 80 sub assembly stock)
- self.assertEqual(mr_items[0].get("quantity"), 90)
+ self.assertEqual(mr_items_dict["RM Item 1"], 90)
# RM Item 2 (FG1 (100) + FG2 (50) + FG4 (10) - 80 sub assembly stock)
- self.assertEqual(mr_items[1].get("quantity"), 80)
+ self.assertEqual(mr_items_dict["RM Item 2"], 80)
def test_stock_reservation_against_production_plan(self):
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
@@ -2362,11 +2368,7 @@ class TestProductionPlan(IntegrationTestCase):
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
def test_production_plan_for_partial_sub_assembly_items(self):
- from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
- from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import (
- create_subcontracting_bom,
- )
frappe.flags.test_print = False
@@ -2418,6 +2420,30 @@ class TestProductionPlan(IntegrationTestCase):
for row in plan.sub_assembly_items:
self.assertEqual(row.ordered_qty, 10.0)
+ def test_phantom_bom_explosion(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_tree_for_phantom_bom_tests
+
+ create_tree_for_phantom_bom_tests()
+
+ plan = create_production_plan(
+ item_code="Top Level Parent",
+ planned_qty=10,
+ use_multi_level_bom=0,
+ do_not_submit=True,
+ company="_Test Company",
+ skip_getting_mr_items=True,
+ )
+ plan.get_sub_assembly_items()
+ plan.submit()
+
+ plan.set("mr_items", [])
+ mr_items = get_items_for_material_requests(plan.as_dict())
+ for d in mr_items:
+ plan.append("mr_items", d)
+
+ self.assertEqual(plan.sub_assembly_items[0].production_item, "Sub Assembly Level 1-1")
+ self.assertEqual([item.item_code for item in plan.mr_items], ["Item Level 1-3", "Item Level 2-3"])
+
def create_production_plan(**args):
"""
@@ -2440,6 +2466,7 @@ def create_production_plan(**args):
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
"sub_assembly_warehouse": args.sub_assembly_warehouse,
"reserve_stock": args.reserve_stock or 0,
+ "for_warehouse": args.for_warehouse or None,
}
)
diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
index 0dfa29b8ddd..5fbb83ae579 100644
--- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
@@ -79,13 +79,14 @@
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty",
+ "no_copy": 1,
"read_only": 1
},
{
"fieldname": "bom_no",
"fieldtype": "Link",
"in_list_view": 1,
- "label": "Bom No",
+ "label": "BOM No",
"options": "BOM"
},
{
@@ -245,7 +246,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2025-06-10 13:36:24.759101",
+ "modified": "2025-11-03 14:33:50.677717",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",
diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js
index fe67fe3feb6..83b81690ec3 100644
--- a/erpnext/manufacturing/doctype/routing/routing.js
+++ b/erpnext/manufacturing/doctype/routing/routing.js
@@ -2,6 +2,16 @@
// For license information, please see license.txt
frappe.ui.form.on("Routing", {
+ setup: function (frm) {
+ frm.set_query("bom_no", "operations", function () {
+ return {
+ filters: {
+ is_phantom_bom: 0,
+ },
+ };
+ });
+ },
+
refresh: function (frm) {
frm.trigger("display_sequence_id_column");
},
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index ff2d094df3e..282da4775f6 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -3270,6 +3270,14 @@ class TestWorkOrder(IntegrationTestCase):
)
frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", original_auto_reserve)
+ def test_phantom_bom_explosion(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_tree_for_phantom_bom_tests
+
+ expected = create_tree_for_phantom_bom_tests()
+
+ wo = make_wo_order_test_record(item="Top Level Parent")
+ self.assertEqual([item.item_code for item in wo.required_items], expected)
+
def get_reserved_entries(voucher_no, warehouse=None):
doctype = frappe.qb.DocType("Stock Reservation Entry")
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 6c9c707fa22..82b26e28912 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -932,6 +932,9 @@ erpnext.work_order = {
if (!(frm.doc.wip_warehouse || frm.doc.fg_warehouse)) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.get_default_warehouse",
+ args: {
+ company: frm.doc.company,
+ },
callback: function (r) {
if (!r.exe) {
frm.set_value("wip_warehouse", r.message.wip_warehouse);
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 6bc0dde543d..b651b211e11 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -453,9 +453,9 @@ class WorkOrder(Document):
def set_default_warehouse(self):
if not self.wip_warehouse and not self.skip_transfer:
- self.wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
+ self.wip_warehouse = frappe.get_cached_value("Company", self.company, "default_wip_warehouse")
if not self.fg_warehouse:
- self.fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_fg_warehouse")
+ self.fg_warehouse = frappe.get_cached_value("Company", self.company, "default_fg_warehouse")
def check_wip_warehouse_skip(self):
if self.skip_transfer and not self.from_wip_warehouse:
@@ -2318,13 +2318,14 @@ def make_stock_entry(
@frappe.whitelist()
-def get_default_warehouse():
- doc = frappe.get_cached_doc("Manufacturing Settings")
-
+def get_default_warehouse(company):
+ wip, fg, scrap = frappe.get_cached_value(
+ "Company", company, ["default_wip_warehouse", "default_fg_warehouse", "default_scrap_warehouse"]
+ )
return {
- "wip_warehouse": doc.default_wip_warehouse,
- "fg_warehouse": doc.default_fg_warehouse,
- "scrap_warehouse": doc.default_scrap_warehouse,
+ "wip_warehouse": wip,
+ "fg_warehouse": fg,
+ "scrap_warehouse": scrap,
}
diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
index de6dec9ebb8..680cb83b312 100644
--- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
+++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
@@ -21,7 +21,17 @@ def get_exploded_items(bom, data, indent=0, qty=1):
exploded_items = frappe.get_all(
"BOM Item",
filters={"parent": bom},
- fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom", "idx"],
+ fields=[
+ "qty",
+ "bom_no",
+ "qty",
+ "item_code",
+ "item_name",
+ "description",
+ "uom",
+ "idx",
+ "is_phantom_item",
+ ],
order_by="idx ASC",
)
@@ -37,6 +47,7 @@ def get_exploded_items(bom, data, indent=0, qty=1):
"qty": item.qty * qty,
"uom": item.uom,
"description": item.description,
+ "is_phantom_item": item.is_phantom_item,
}
)
if item.bom_no:
@@ -54,6 +65,7 @@ def get_columns():
},
{"label": _("Item Name"), "fieldtype": "data", "fieldname": "item_name", "width": 100},
{"label": _("BOM"), "fieldtype": "Link", "fieldname": "bom", "width": 150, "options": "BOM"},
+ {"label": _("Is Phantom Item"), "fieldtype": "Check", "fieldname": "is_phantom_item"},
{"label": _("Qty"), "fieldtype": "data", "fieldname": "qty", "width": 100},
{"label": _("UOM"), "fieldtype": "data", "fieldname": "uom", "width": 100},
{"label": _("BOM Level"), "fieldtype": "Int", "fieldname": "bom_level", "width": 100},
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
index 6bc05a468f1..4b5df4df4b2 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
@@ -32,6 +32,7 @@ def get_report_data(last_purchase_rate, required_qty, row, manufacture_details):
return [
row.item_code,
row.description,
+ row.from_bom_no,
comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False),
comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False),
qty_per_unit,
@@ -57,6 +58,13 @@ def get_columns():
"fieldtype": "Data",
"width": 150,
},
+ {
+ "fieldname": "from_bom_no",
+ "label": _("From BOM No"),
+ "fieldtype": "Link",
+ "options": "BOM",
+ "width": 150,
+ },
{
"fieldname": "manufacturer",
"label": _("Manufacturer"),
@@ -103,10 +111,7 @@ def get_columns():
def get_bom_data(filters):
- if filters.get("show_exploded_view"):
- bom_item_table = "BOM Explosion Item"
- else:
- bom_item_table = "BOM Item"
+ bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item"
bom_item = frappe.qb.DocType(bom_item_table)
bin = frappe.qb.DocType("Bin")
@@ -118,11 +123,13 @@ def get_bom_data(filters):
.select(
bom_item.item_code,
bom_item.description,
+ bom_item.parent.as_("from_bom_no"),
bom_item.qty_consumed_per_unit.as_("qty_per_unit"),
IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"),
)
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
.groupby(bom_item.item_code)
+ .orderby(bom_item.idx)
)
if filters.get("warehouse"):
@@ -146,7 +153,36 @@ def get_bom_data(filters):
else:
query = query.where(bin.warehouse == filters.get("warehouse"))
- return query.run(as_dict=True)
+ if bom_item_table == "BOM Item":
+ query = query.select(bom_item.bom_no, bom_item.is_phantom_item)
+
+ data = query.run(as_dict=True)
+ return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data
+
+
+def explode_phantom_boms(data, filters):
+ original_bom = filters.get("bom")
+ replacements = []
+
+ for idx, item in enumerate(data):
+ if not item.is_phantom_item:
+ continue
+
+ filters["bom"] = item.bom_no
+ children = get_bom_data(filters)
+ filters["bom"] = original_bom
+
+ for child in children:
+ child.qty_per_unit = (child.qty_per_unit or 0) * (item.qty_per_unit or 0)
+
+ replacements.append((idx, children))
+
+ for idx, children in reversed(replacements):
+ data.pop(idx)
+ data[idx:idx] = children
+
+ filters["bom"] = original_bom
+ return data
def get_manufacturer_records():
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py
index 8f5f768a698..6bbbacbeb77 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py
@@ -102,6 +102,7 @@ def get_expected_data(bom, qty_to_make):
[
bom.items[idx].item_code,
bom.items[idx].item_code,
+ bom.name,
"",
"",
float(bom.items[idx].stock_qty / bom.quantity),
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index 5fe4d63ccbf..eeda32c64c7 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -22,8 +22,9 @@ def get_columns():
_("Item") + ":Link/Item:150",
_("Item Name") + "::240",
_("Description") + "::300",
+ _("From BOM No") + "::200",
_("BOM Qty") + ":Float:160",
- _("BOM UoM") + "::160",
+ _("BOM UOM") + "::160",
_("Required Qty") + ":Float:120",
_("In Stock Qty") + ":Float:120",
_("Enough Parts to Build") + ":Float:200",
@@ -72,6 +73,7 @@ def get_bom_stock(filters):
BOM_ITEM.item_code,
BOM_ITEM.item_name,
BOM_ITEM.description,
+ BOM.name,
Sum(BOM_ITEM.stock_qty),
BOM_ITEM.stock_uom,
(Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity,
@@ -80,6 +82,25 @@ def get_bom_stock(filters):
)
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
.groupby(BOM_ITEM.item_code)
+ .orderby(BOM_ITEM.idx)
)
- return QUERY.run()
+ if bom_item_table == "BOM Item":
+ QUERY = QUERY.select(BOM_ITEM.bom_no, BOM_ITEM.is_phantom_item)
+
+ data = QUERY.run(as_list=True)
+ return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data
+
+
+def explode_phantom_boms(data, filters):
+ expanded = []
+ for row in data:
+ if row[-1]: # last element is `is_phantom_item`
+ phantom_filters = filters.copy()
+ phantom_filters["qty_to_produce"] = row[-5]
+ phantom_filters["bom"] = row[-2]
+ expanded.extend(get_bom_stock(phantom_filters))
+ else:
+ expanded.append(row)
+
+ return expanded
diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
index 860ba3f57f7..2bcb43b409f 100644
--- a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
@@ -96,6 +96,7 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
item.item_code,
item.item_name,
item.description,
+ bom.name,
item.stock_qty,
item.stock_uom,
item.stock_qty * qty_to_produce / bom.quantity,
@@ -103,6 +104,8 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity))
if in_stock_qty
else None,
+ item.bom_no,
+ item.is_phantom_item,
]
)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index a1edea61b40..4cfc3d2a232 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -446,3 +446,5 @@ erpnext.patches.v16_0.add_new_stock_entry_types
erpnext.patches.v15_0.set_asset_status_if_not_already_set
erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing
erpnext.patches.v16_0.update_serial_batch_entries
+erpnext.patches.v16_0.set_company_wise_warehouses
+erpnext.patches.v16_0.set_valuation_method_on_companies
diff --git a/erpnext/patches/v16_0/set_company_wise_warehouses.py b/erpnext/patches/v16_0/set_company_wise_warehouses.py
new file mode 100644
index 00000000000..8d9e7320076
--- /dev/null
+++ b/erpnext/patches/v16_0/set_company_wise_warehouses.py
@@ -0,0 +1,14 @@
+import frappe
+
+
+def execute():
+ warehouses = frappe.get_single_value(
+ "Manufacturing Settings",
+ ["default_wip_warehouse", "default_fg_warehouse", "default_scrap_warehouse"],
+ as_dict=True,
+ )
+
+ for name, warehouse in warehouses.items():
+ if warehouse:
+ company = frappe.get_value("Warehouse", warehouse, "company")
+ frappe.db.set_value("Company", company, name, warehouse)
diff --git a/erpnext/patches/v16_0/set_valuation_method_on_companies.py b/erpnext/patches/v16_0/set_valuation_method_on_companies.py
new file mode 100644
index 00000000000..f1ea8064dcf
--- /dev/null
+++ b/erpnext/patches/v16_0/set_valuation_method_on_companies.py
@@ -0,0 +1,7 @@
+import frappe
+
+
+def execute():
+ valuation_method = frappe.get_single_value("Stock Settings", "valuation_method")
+ for company in frappe.get_all("Company", pluck="name"):
+ frappe.db.set_value("Company", company, "valuation_method", valuation_method)
diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
index facbdf15482..49eee62e14d 100644
--- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
+++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
@@ -140,6 +140,17 @@ class BOMConfigurator {
},
btnClass: "hidden-xs",
},
+ {
+ label: __(frappe.utils.icon("add", "sm") + " Phantom Item"),
+ click: function (node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.add_sub_assembly(node, view, true);
+ },
+ condition: function (node) {
+ return node.expandable;
+ },
+ btnClass: "hidden-xs",
+ },
{
label: __("Collapse All"),
click: function (node) {
@@ -170,6 +181,17 @@ class BOMConfigurator {
},
btnClass: "hidden-xs",
},
+ {
+ label: __(frappe.utils.icon("move", "sm") + " Phantom Item"),
+ click: function (node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.convert_to_sub_assembly(node, view, true);
+ },
+ condition: function (node) {
+ return !node.expandable;
+ },
+ btnClass: "hidden-xs",
+ },
{
label: __(frappe.utils.icon("delete", "sm") + " Item"),
click: function (node) {
@@ -253,10 +275,10 @@ class BOMConfigurator {
}
}
- add_sub_assembly(node, view) {
+ add_sub_assembly(node, view, phantom = false) {
let dialog = new frappe.ui.Dialog({
- fields: view.events.get_sub_assembly_modal_fields(view, node.is_root),
- title: __("Add Sub Assembly"),
+ fields: view.events.get_sub_assembly_modal_fields(view, node.is_root, false, phantom),
+ title: phantom ? __("Add Phantom Item") : __("Add Sub Assembly"),
});
view.events.set_query_for_workstation(dialog);
@@ -282,6 +304,7 @@ class BOMConfigurator {
operation: node.data.operation,
workstation_type: node.data.workstation_type,
operation_time: node.data.operation_time,
+ phantom: phantom,
},
callback: (r) => {
view.events.load_tree(r, node);
@@ -292,15 +315,18 @@ class BOMConfigurator {
});
}
- get_sub_assembly_modal_fields(view, is_root = false, read_only = false) {
+ get_sub_assembly_modal_fields(view, is_root = false, read_only = false, phantom = false) {
let fields = [
{
- label: __("Sub Assembly Item"),
+ label: phantom ? __("Phantom Item") : __("Sub Assembly Item"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
reqd: 1,
read_only: read_only,
+ filters: {
+ is_stock_item: !phantom,
+ },
},
{ fieldtype: "Column Break" },
{
@@ -320,7 +346,7 @@ class BOMConfigurator {
},
];
- if (is_root) {
+ if (is_root && !phantom) {
fields.push(
...[
{ fieldtype: "Section Break" },
@@ -384,10 +410,10 @@ class BOMConfigurator {
return fields;
}
- convert_to_sub_assembly(node, view) {
+ convert_to_sub_assembly(node, view, phantom = false) {
let dialog = new frappe.ui.Dialog({
- fields: view.events.get_sub_assembly_modal_fields(view, node.is_root, true),
- title: __("Add Sub Assembly"),
+ fields: view.events.get_sub_assembly_modal_fields(view, node.is_root, true, phantom),
+ title: phantom ? __("Add Phantom Item") : __("Add Sub Assembly"),
});
dialog.set_values({
@@ -400,7 +426,9 @@ class BOMConfigurator {
let bom_item = dialog.get_values();
if (!bom_item.item_code) {
- frappe.throw(__("Sub Assembly Item is mandatory"));
+ frappe.throw(
+ phantom ? __("Phantom Item is mandatory") : __("Sub Assembly Item is mandatory")
+ );
}
bom_item.items.forEach((d) => {
@@ -425,6 +453,7 @@ class BOMConfigurator {
workstation_type: node.data.workstation_type,
operation_time: node.data.operation_time,
workstation: node.data.workstation,
+ phantom: phantom,
},
callback: (r) => {
node.expandable = true;
diff --git a/erpnext/public/js/print.js b/erpnext/public/js/print.js
index 56bb25b48d7..53f03f83bda 100644
--- a/erpnext/public/js/print.js
+++ b/erpnext/public/js/print.js
@@ -1,6 +1,16 @@
let beforePrintHandled = false;
frappe.realtime.on("sales_invoice_before_print", (data) => {
+ let print_format = $('input[data-fieldname="print_format"]').val();
+ let letterhead = $('input[data-fieldname="letterhead"]').val();
+
+ let allowed_print_formats = ["Sales Invoice Standard", "Sales Invoice with Item Image"];
+ let allowed_letterheads = ["Company Letterhead", "Company Letterhead - Grey"];
+
+ if (!allowed_print_formats.includes(print_format) && !allowed_letterheads.includes(letterhead)) {
+ return;
+ }
+
const route = frappe.get_route();
if (!beforePrintHandled && route[0] === "print" && route[1] === "Sales Invoice") {
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 2d01539ca04..a75e6eca08f 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1796,7 +1796,7 @@ class TestSalesOrder(AccountsTestMixin, IntegrationTestCase):
mr.submit()
# WO from MR
- wo_name = raise_work_orders(mr.name)[0]
+ wo_name = raise_work_orders(mr.name, mr.company)[0]
wo = frappe.get_doc("Work Order", wo_name)
wo.wip_warehouse = "Work In Progress - _TC"
wo.skip_transfer = True
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index b5cbc3907d7..546f2af8dc8 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -117,6 +117,7 @@
"enable_item_wise_inventory_account",
"enable_provisional_accounting_for_non_stock_items",
"default_inventory_account",
+ "valuation_method",
"column_break_32",
"stock_adjustment_account",
"stock_received_but_not_billed",
@@ -124,6 +125,10 @@
"default_in_transit_warehouse",
"manufacturing_section",
"default_operating_cost_account",
+ "column_break_9prc",
+ "default_wip_warehouse",
+ "default_fg_warehouse",
+ "default_scrap_warehouse",
"dashboard_tab"
],
"fields": [
@@ -885,6 +890,39 @@
"fieldname": "enable_item_wise_inventory_account",
"fieldtype": "Check",
"label": "Enable Item-wise Inventory Account"
+ },
+ {
+ "default": "FIFO",
+ "fieldname": "valuation_method",
+ "fieldtype": "Select",
+ "label": "Default Stock Valuation Method",
+ "options": "FIFO\nMoving Average\nLIFO",
+ "reqd": 1
+ },
+ {
+ "fieldname": "default_wip_warehouse",
+ "fieldtype": "Link",
+ "label": " Default Work In Progress Warehouse ",
+ "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0]]",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "default_fg_warehouse",
+ "fieldtype": "Link",
+ "label": "Default Finished Goods Warehouse",
+ "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0]]",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "default_scrap_warehouse",
+ "fieldtype": "Link",
+ "label": "Default Scrap Warehouse",
+ "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0]]",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "column_break_9prc",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-building",
@@ -892,7 +930,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2025-10-23 13:15:52.411984",
+ "modified": "2025-11-16 16:50:27.624096",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 9eebf21b1d0..05b2044ac71 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -59,6 +59,7 @@ class Company(NestedSet):
default_deferred_revenue_account: DF.Link | None
default_discount_account: DF.Link | None
default_expense_account: DF.Link | None
+ default_fg_warehouse: DF.Link | None
default_finance_book: DF.Link | None
default_holiday_list: DF.Link | None
default_in_transit_warehouse: DF.Link | None
@@ -69,8 +70,10 @@ class Company(NestedSet):
default_payable_account: DF.Link | None
default_provisional_account: DF.Link | None
default_receivable_account: DF.Link | None
+ default_scrap_warehouse: DF.Link | None
default_selling_terms: DF.Link | None
default_warehouse_for_sales_return: DF.Link | None
+ default_wip_warehouse: DF.Link | None
depreciation_cost_center: DF.Link | None
depreciation_expense_account: DF.Link | None
disposal_account: DF.Link | None
@@ -113,6 +116,7 @@ class Company(NestedSet):
transactions_annual_history: DF.Code | None
unrealized_exchange_gain_loss_account: DF.Link | None
unrealized_profit_loss_account: DF.Link | None
+ valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"]
website: DF.Data | None
write_off_account: DF.Link | None
# end: auto-generated types
@@ -163,6 +167,32 @@ class Company(NestedSet):
self.validate_parent_company()
self.set_reporting_currency()
self.validate_inventory_account_settings()
+ self.cant_change_valuation_method()
+
+ def cant_change_valuation_method(self):
+ doc_before_save = self.get_doc_before_save()
+ if not doc_before_save:
+ return
+
+ previous_valuation_method = doc_before_save.get("valuation_method")
+
+ if previous_valuation_method and previous_valuation_method != self.valuation_method:
+ # check if there are any stock ledger entries against items
+ # which does not have it's own valuation method
+ sle = frappe.db.sql(
+ """select name from `tabStock Ledger Entry` sle
+ where exists(select name from tabItem
+ where name=sle.item_code and (valuation_method is null or valuation_method='')) and sle.company=%s limit 1
+ """,
+ self.name,
+ )
+
+ if sle:
+ frappe.throw(
+ _(
+ "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method"
+ )
+ )
def validate_inventory_account_settings(self):
doc_before_save = self.get_doc_before_save()
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index ed3e6da0e9a..7dc7932470e 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -5,12 +5,11 @@
import os
import frappe
-from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
-from frappe.utils import cint
from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
+from erpnext.setup.utils import identity as _
from .default_success_action import get_default_success_action
@@ -184,33 +183,27 @@ def add_company_to_session_defaults():
def add_standard_navbar_items():
navbar_settings = frappe.get_single("Navbar Settings")
-
- # Translatable strings for below navbar items
- __ = _("Documentation")
- __ = _("User Forum")
- __ = _("Report an Issue")
-
erpnext_navbar_items = [
{
- "item_label": "Documentation",
+ "item_label": _("Documentation"),
"item_type": "Route",
"route": "https://docs.erpnext.com/",
"is_standard": 1,
},
{
- "item_label": "User Forum",
+ "item_label": _("User Forum"),
"item_type": "Route",
"route": "https://discuss.frappe.io",
"is_standard": 1,
},
{
- "item_label": "Frappe School",
+ "item_label": _("Frappe School"),
"item_type": "Route",
"route": "https://frappe.io/school?utm_source=in_app",
"is_standard": 1,
},
{
- "item_label": "Report an Issue",
+ "item_label": _("Report an Issue"),
"item_type": "Route",
"route": "https://github.com/frappe/erpnext/issues",
"is_standard": 1,
@@ -319,26 +312,26 @@ def create_letter_head():
DEFAULT_ROLE_PROFILES = {
- "Inventory": [
+ _("Inventory"): [
"Stock User",
"Stock Manager",
"Item Manager",
],
- "Manufacturing": [
+ _("Manufacturing"): [
"Stock User",
"Manufacturing User",
"Manufacturing Manager",
],
- "Accounts": [
+ _("Accounts"): [
"Accounts User",
"Accounts Manager",
],
- "Sales": [
+ _("Sales"): [
"Sales User",
"Stock User",
"Sales Manager",
],
- "Purchase": [
+ _("Purchase"): [
"Item Manager",
"Stock User",
"Purchase User",
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 5677c07b87c..b1d64f8311b 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -15,14 +15,7 @@ from frappe.utils import cstr, getdate
from erpnext.accounts.doctype.account.account import RootNotEditable
from erpnext.regional.address_template.setup import set_up_address_templates
-
-
-def _(x, *args, **kwargs):
- """Redefine the translation function to return the string as is.
-
- We want to create english records but still mark the strings as translatable.
- The respective DocTypes have 'Translate Link Fields' enabled."""
- return x
+from erpnext.setup.utils import identity as _
def read_lines(filename: str) -> list[str]:
@@ -579,7 +572,7 @@ def create_bank_account(args, demo=False):
return doc
except RootNotEditable:
- frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account")))
+ frappe.throw(frappe._("Bank account cannot be named as {0}").format(args.get("bank_account")))
except frappe.DuplicateEntryError:
# bank account same as a CoA entry
pass
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 25a08438460..3cdd7833908 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -232,3 +232,15 @@ def welcome_email():
site_name = get_default_company() or "ERPNext"
title = _("Welcome to {0}").format(site_name)
return title
+
+
+def identity(x, *args, **kwargs):
+ """Used for redefining the translation function to return the string as is.
+
+ We want to create english records but still mark the strings as translatable.
+ E.g. when the respective DocTypes have 'Translate Link Fields' enabled or
+ we're creating custom fields.
+
+ Use like this: `from erpnext.setup.utils import identity as _`
+ """
+ return x
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index f322fd0c492..00f80fbbeca 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -495,6 +495,7 @@ frappe.ui.form.on("Material Request", {
method: "erpnext.stock.doctype.material_request.material_request.raise_work_orders",
args: {
material_request: frm.doc.name,
+ company: frm.doc.company,
},
freeze: true,
callback: function (r) {
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index b8817996bac..6f12fd05ea7 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -833,11 +833,11 @@ def make_stock_entry(source_name, target_doc=None):
@frappe.whitelist()
-def raise_work_orders(material_request):
+def raise_work_orders(material_request, company):
mr = frappe.get_doc("Material Request", material_request)
errors = []
work_orders = []
- default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
+ default_wip_warehouse = frappe.get_cached_value("Company", company, "default_wip_warehouse")
for d in mr.items:
if (d.stock_qty - d.ordered_qty) > 0:
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 0a6251f7794..4a888dea879 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -747,7 +747,7 @@ class TestMaterialRequest(IntegrationTestCase):
(mr.items[0].item_code, mr.items[0].warehouse),
)[0][0]
- prod_order = raise_work_orders(mr.name)
+ prod_order = raise_work_orders(mr.name, mr.company)
po = frappe.get_doc("Work Order", prod_order[0])
po.wip_warehouse = "_Test Warehouse 1 - _TC"
po.submit()
@@ -789,7 +789,7 @@ class TestMaterialRequest(IntegrationTestCase):
self.assertEqual(requested_qty, existing_requested_qty + 120)
- work_order = raise_work_orders(mr.name)
+ work_order = raise_work_orders(mr.name, mr.company)
wo = frappe.get_doc("Work Order", work_order[0])
wo.qty = 50
wo.wip_warehouse = "_Test Warehouse 1 - _TC"
@@ -924,7 +924,7 @@ class TestMaterialRequest(IntegrationTestCase):
item_code="_Test FG Item", material_request_type="Manufacture", do_not_submit=False
)
- work_order = raise_work_orders(mr.name)
+ work_order = raise_work_orders(mr.name, mr.company)
wo = frappe.get_doc("Work Order", work_order[0])
wo.wip_warehouse = "_Test Warehouse 1 - _TC"
wo.submit()
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index b91e8f5d619..69b3abdc5ee 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -514,7 +514,7 @@ class SerialandBatchBundle(Document):
if hasattr(sn_obj, "stock_queue") and sn_obj.stock_queue:
stock_queue = parse_json(sn_obj.stock_queue)
- val_method = get_valuation_method(self.item_code)
+ val_method = get_valuation_method(self.item_code, self.company)
for d in self.entries:
available_qty = 0
@@ -642,7 +642,7 @@ class SerialandBatchBundle(Document):
def set_incoming_rate_for_inward_transaction(self, row=None, save=False, prev_sle=None):
from erpnext.stock.utils import get_valuation_method
- valuation_method = get_valuation_method(self.item_code)
+ valuation_method = get_valuation_method(self.item_code, self.company)
valuation_field = "valuation_rate"
if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]:
@@ -2502,18 +2502,11 @@ def get_auto_batch_nos(kwargs):
def get_batch_nos_from_sre(kwargs):
- from frappe.query_builder.functions import Max, Min, Sum
+ from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Stock Reservation Entry")
child_table = frappe.qb.DocType("Serial and Batch Entry")
- if kwargs.based_on == "LIFO":
- creation_field = Max(child_table.creation).as_("sort_creation")
- order = frappe.query_builder.Order.desc
- else:
- creation_field = Min(child_table.creation).as_("sort_creation")
- order = frappe.query_builder.Order.asc
-
query = (
frappe.qb.from_(table)
.join(child_table)
@@ -2522,7 +2515,6 @@ def get_batch_nos_from_sre(kwargs):
child_table.batch_no,
child_table.warehouse,
Sum(child_table.qty - child_table.delivered_qty).as_("qty"),
- creation_field,
)
.where(
(table.docstatus == 1)
@@ -2530,7 +2522,6 @@ def get_batch_nos_from_sre(kwargs):
& (child_table.qty != child_table.delivered_qty)
)
.groupby(child_table.batch_no, child_table.warehouse)
- .orderby("sort_creation", order=order)
.orderby(child_table.batch_no, order=frappe.query_builder.Order.asc)
)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index bc4f6a3e1ca..604db8dd26a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -1382,8 +1382,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
this.frm.script_manager.copy_from_first_row("items", row, ["expense_account", "cost_center"]);
}
- if (!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse;
- if (!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
+ if (this.frm.doc.from_warehouse) row.s_warehouse = this.frm.doc.from_warehouse;
+ if (this.frm.doc.to_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
if (cint(frappe.user_defaults?.use_serial_batch_fields)) {
frappe.model.set_value(row.doctype, row.name, "use_serial_batch_fields", 1);
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 5906eb36c56..64236aa2a8d 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -202,6 +202,12 @@ class StockEntry(StockController, SubcontractingInwardController):
for item in self.get("items"):
item.update(get_bin_details(item.item_code, item.s_warehouse))
+ def before_insert(self):
+ if self.subcontracting_order and frappe.get_cached_value(
+ "Subcontracting Order", self.subcontracting_order, "reserve_stock"
+ ):
+ self.set_serial_batch_from_reserved_entry()
+
def before_validate(self):
from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule
@@ -274,9 +280,10 @@ class StockEntry(StockController, SubcontractingInwardController):
self.update_work_order()
self.update_disassembled_order()
self.adjust_stock_reservation_entries_for_return()
- self.update_sre_for_subcontracting_delivery()
+ self.update_stock_reservation_entries()
self.update_stock_ledger()
self.make_stock_reserve_for_wip_and_fg()
+ self.reserve_stock_for_subcontracting()
self.update_subcontract_order_supplied_items()
self.update_subcontracting_order_status()
@@ -324,7 +331,7 @@ class StockEntry(StockController, SubcontractingInwardController):
self.update_transferred_qty()
self.update_quality_inspection()
self.adjust_stock_reservation_entries_for_return()
- self.update_sre_for_subcontracting_delivery()
+ self.update_stock_reservation_entries()
self.delete_auto_created_batches()
self.delete_linked_stock_entry()
@@ -1889,6 +1896,30 @@ class StockEntry(StockController, SubcontractingInwardController):
pro_doc.set_reserved_qty_for_wip_and_fg(self)
+ def reserve_stock_for_subcontracting(self):
+ if self.purpose == "Send to Subcontractor" and frappe.get_value(
+ "Subcontracting Order", self.subcontracting_order, "reserve_stock"
+ ):
+ items = {}
+ for item in self.items:
+ if item.sco_rm_detail in items:
+ items[item.sco_rm_detail].qty_to_reserve += item.transfer_qty
+ items[item.sco_rm_detail].serial_and_batch_bundles.append(item.serial_and_batch_bundle)
+ else:
+ items[item.sco_rm_detail] = frappe._dict(
+ {
+ "name": item.sco_rm_detail,
+ "qty_to_reserve": item.transfer_qty,
+ "warehouse": item.t_warehouse,
+ "reference_voucher_detail_no": item.name,
+ "serial_and_batch_bundles": [item.serial_and_batch_bundle],
+ }
+ )
+
+ frappe.get_doc("Subcontracting Order", self.subcontracting_order).reserve_raw_materials(
+ items=items.values(), stock_entry=self.name
+ )
+
def cancel_stock_reserve_for_wip_and_fg(self):
if self.is_stock_reserve_for_work_order():
pro_doc = frappe.get_doc("Work Order", self.work_order)
@@ -2230,21 +2261,16 @@ class StockEntry(StockController, SubcontractingInwardController):
self.calculate_rate_and_amount(raise_error_if_no_rate=False)
def set_serial_batch_from_reserved_entry(self):
- if not self.work_order:
- return
+ if self.work_order and frappe.get_cached_value("Work Order", self.work_order, "reserve_stock"):
+ skip_transfer = frappe.get_cached_value("Work Order", self.work_order, "skip_transfer")
- if not frappe.get_cached_value("Work Order", self.work_order, "reserve_stock"):
- return
-
- skip_transfer = frappe.get_cached_value("Work Order", self.work_order, "skip_transfer")
-
- if (
- self.purpose not in ["Material Transfer for Manufacture"]
- and frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")
- != "BOM"
- and not skip_transfer
- ):
- return
+ if (
+ self.purpose not in ["Material Transfer for Manufacture"]
+ and frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")
+ != "BOM"
+ and not skip_transfer
+ ):
+ return
reservation_entries = self.get_available_reserved_materials()
if not reservation_entries:
@@ -2252,6 +2278,9 @@ class StockEntry(StockController, SubcontractingInwardController):
new_items_to_add = []
for d in self.items:
+ if d.serial_and_batch_bundle or d.serial_no or d.batch_no:
+ continue
+
key = (d.item_code, d.s_warehouse)
if details := reservation_entries.get(key):
original_qty = d.qty
@@ -2363,7 +2392,7 @@ class StockEntry(StockController, SubcontractingInwardController):
)
.where(
(doctype.docstatus == 1)
- & (doctype.voucher_no == self.work_order)
+ & (doctype.voucher_no == (self.work_order or self.subcontracting_order))
& (serial_batch_doc.delivered_qty < serial_batch_doc.qty)
)
.orderby(serial_batch_doc.idx)
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
index 5f81d391b59..795e7eebf35 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
@@ -84,7 +84,7 @@
"no_copy": 1,
"oldfieldname": "voucher_type",
"oldfieldtype": "Data",
- "options": "\nSales Order\nWork Order\nSubcontracting Inward Order\nProduction Plan",
+ "options": "\nSales Order\nWork Order\nSubcontracting Inward Order\nProduction Plan\nSubcontracting Order",
"print_width": "150px",
"read_only": 1,
"width": "150px"
@@ -315,8 +315,7 @@
},
{
"fieldname": "production_section",
- "fieldtype": "Section Break",
- "label": "Production"
+ "fieldtype": "Section Break"
},
{
"fieldname": "column_break_qdwj",
@@ -335,7 +334,7 @@
{
"fieldname": "transferred_qty",
"fieldtype": "Float",
- "label": "Qty in WIP Warehouse"
+ "label": "Transferred Qty"
}
],
"grid_page_length": 50,
@@ -344,7 +343,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2025-10-12 19:48:33.170835",
+ "modified": "2025-11-10 16:09:10.380024",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reservation Entry",
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
index d6ec32aeff8..4c87c40df15 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -64,7 +64,12 @@ class StockReservationEntry(Document):
voucher_no: DF.DynamicLink | None
voucher_qty: DF.Float
voucher_type: DF.Literal[
- "", "Sales Order", "Work Order", "Subcontracting Inward Order", "Production Plan"
+ "",
+ "Sales Order",
+ "Work Order",
+ "Subcontracting Inward Order",
+ "Production Plan",
+ "Subcontracting Order",
]
warehouse: DF.Link | None
# end: auto-generated types
@@ -338,7 +343,7 @@ class StockReservationEntry(Document):
def validate_reservation_based_on_serial_and_batch(self) -> None:
"""Validates `Reserved Qty`, `Serial and Batch Nos` when `Reservation Based On` is `Serial and Batch`."""
- if self.voucher_type == "Work Order":
+ if self.voucher_type in ["Work Order", "Subcontracting Order"]:
return
if self.reservation_based_on == "Serial and Batch":
@@ -460,13 +465,14 @@ class StockReservationEntry(Document):
"Sales Order": "Sales Order Item",
"Work Order": "Work Order Item",
"Production Plan": "Production Plan Sub Assembly Item",
+ "Subcontracting Order": "Subcontracting Order Supplied Item",
}.get(self.voucher_type, None)
if item_doctype:
sre = frappe.qb.DocType("Stock Reservation Entry")
reserved_qty = (
frappe.qb.from_(sre)
- .select(Sum(sre.reserved_qty))
+ .select(Sum(sre.reserved_qty - sre.delivered_qty - sre.transferred_qty - sre.consumed_qty))
.where(
(sre.docstatus == 1)
& (sre.voucher_type == self.voucher_type)
@@ -574,7 +580,7 @@ class StockReservationEntry(Document):
)
from_voucher_detail_no = None
- if self.from_voucher_type and self.from_voucher_type == "Stock Entry":
+ if self.from_voucher_type and self.from_voucher_type in ["Stock Entry", "Production Plan"]:
from_voucher_detail_no = self.from_voucher_detail_no
total_reserved_qty = get_sre_reserved_qty_for_voucher_detail_no(
@@ -1276,7 +1282,7 @@ class StockReservation:
if not reservation_entries:
return
- entries_to_reserve = frappe._dict({})
+ entries_to_reserve = frappe._dict()
for row in reservation_entries:
reserved_qty_field = "reserved_qty" if row.reservation_based_on == "Qty" else "sabb_qty"
delivered_qty_field = (
@@ -1293,7 +1299,7 @@ class StockReservation:
if available_qty <= 0:
continue
- key = (row.item_code, row.warehouse)
+ key = (row.item_code, row.warehouse, entry.voucher_detail_no)
if key not in entries_to_reserve:
entries_to_reserve.setdefault(
@@ -1303,7 +1309,7 @@ class StockReservation:
"qty_to_reserve": 0.0,
"item_code": row.item_code,
"warehouse": row.warehouse,
- "voucher_type": entry.voucher_type,
+ "voucher_type": entry.voucher_type or to_doctype,
"voucher_no": entry.voucher_no,
"voucher_detail_no": entry.voucher_detail_no,
"serial_nos": [],
@@ -1475,6 +1481,9 @@ class StockReservation:
.orderby(sabb_entry.idx)
)
+ if self.items and (data := [item.from_voucher_detail_no for item in self.items]):
+ query = query.where(sre.voucher_detail_no.isin(data))
+
if against_fg_item:
query = query.where(
sre.voucher_detail_no.isin(
@@ -1490,9 +1499,14 @@ class StockReservation:
def get_items_to_reserve(self, docnames, from_doctype, to_doctype):
field = frappe.scrub(from_doctype)
+ item_code_fieldname, child_table_suffix = (
+ ("rm_item_code", " Supplied Item")
+ if to_doctype == "Subcontracting Order"
+ else ("item_code", " Item")
+ )
doctype = frappe.qb.DocType(to_doctype)
- child_doctype = frappe.qb.DocType(to_doctype + " Item")
+ child_doctype = frappe.qb.DocType(to_doctype + child_table_suffix)
query = (
frappe.qb.from_(doctype)
@@ -1501,11 +1515,12 @@ class StockReservation:
.select(
doctype.name.as_("voucher_no"),
child_doctype.name.as_("voucher_detail_no"),
- child_doctype.item_code,
+ child_doctype[item_code_fieldname].as_("item_code"),
doctype.company,
child_doctype.stock_uom,
)
.where((doctype.docstatus == 1) & (doctype[field].isin(docnames)))
+ .groupby(child_doctype.name)
)
if to_doctype == "Work Order":
@@ -1523,6 +1538,15 @@ class StockReservation:
(doctype.qty > doctype.material_transferred_for_manufacturing)
& (doctype.status != "Completed")
)
+ elif to_doctype == "Subcontracting Order":
+ query = query.select(
+ child_doctype.stock_reserved_qty,
+ child_doctype.required_qty.as_("qty"),
+ child_doctype.reserve_warehouse.as_("source_warehouse"),
+ )
+
+ if self.items and (data := [item.voucher_detail_no for item in self.items]):
+ query = query.where(child_doctype.name.isin(data))
data = query.run(as_dict=True)
items = []
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index e2cce6e99da..0f37b3f0116 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -52,7 +52,7 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
range_values = get_range_age(filters, fifo_queue, to_date, item_dict)
check_and_replace_valuations_if_moving_average(
- range_values, details.valuation_method, details.valuation_rate
+ range_values, details.valuation_method, details.valuation_rate, filters.get("company")
)
row = [details.name, details.item_name, details.description, details.item_group, details.brand]
@@ -76,10 +76,12 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
return data
-def check_and_replace_valuations_if_moving_average(range_values, item_valuation_method, valuation_rate):
+def check_and_replace_valuations_if_moving_average(
+ range_values, item_valuation_method, valuation_rate, company
+):
if item_valuation_method == "Moving Average" or (
not item_valuation_method
- and frappe.db.get_single_value("Stock Settings", "valuation_method") == "Moving Average"
+ and frappe.get_cached_value("Company", company, "valuation_method") == "Moving Average"
):
for i in range(0, len(range_values), 2):
range_values[i + 1] = range_values[i] * valuation_rate
diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py
index 808afadd05a..97243d57001 100644
--- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py
+++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py
@@ -201,7 +201,7 @@ def get_columns():
def get_data(filters=None):
filters = frappe._dict(filters or {})
item_warehouse_map = get_item_warehouse_combinations(filters)
- valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method")
+ valuation_method = frappe.get_cached_value("Company", filters.get("company"), "valuation_method")
data = []
if item_warehouse_map:
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index e7cbc8bceed..1fb5d1391aa 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -861,9 +861,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
self.batchwise_valuation_batches = []
self.non_batchwise_valuation_batches = []
- if get_valuation_method(self.sle.item_code) == "Moving Average" and frappe.get_single_value(
- "Stock Settings", "do_not_use_batchwise_valuation"
- ):
+ if get_valuation_method(
+ self.sle.item_code, self.sle.company
+ ) == "Moving Average" and frappe.get_single_value("Stock Settings", "do_not_use_batchwise_valuation"):
self.non_batchwise_valuation_batches = self.batches
return
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 1801aeb2f9e..2c9e57afecb 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -563,7 +563,7 @@ class update_entries_after:
self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
self.set_precision()
- self.valuation_method = get_valuation_method(self.item_code)
+ self.valuation_method = get_valuation_method(self.item_code, self.company)
self.new_items_found = False
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
@@ -1087,7 +1087,7 @@ class update_entries_after:
avg_rate = 0.0
for d in sabb_data:
- incoming_rate = get_incoming_rate_for_serial_and_batch(self.item_code, d, sn_obj)
+ incoming_rate = get_incoming_rate_for_serial_and_batch(self.item_code, d, sn_obj, self.company)
amount = incoming_rate * flt(d.qty)
tot_amt += flt(amount)
total_qty += flt(d.qty)
@@ -2398,7 +2398,7 @@ def get_serial_from_sabb(serial_and_batch_bundle):
)
-def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj):
+def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj, company):
if row.serial_no:
return abs(sn_obj.serial_no_incoming_rate.get(row.serial_no, 0.0))
else:
@@ -2406,7 +2406,7 @@ def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj):
if hasattr(sn_obj, "stock_queue") and sn_obj.stock_queue:
stock_queue = parse_json(sn_obj.stock_queue)
- val_method = get_valuation_method(item_code)
+ val_method = get_valuation_method(item_code, company)
actual_qty = row.qty
if stock_queue and val_method == "FIFO" and row.batch_no in sn_obj.non_batchwise_valuation_batches:
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index bee190e13a1..b891e0744fc 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -302,7 +302,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
return batch_obj.get_incoming_rate()
else:
- valuation_method = get_valuation_method(args.get("item_code"))
+ valuation_method = get_valuation_method(args.get("item_code"), args.get("company"))
previous_sle = get_previous_sle(args)
if valuation_method in ("FIFO", "LIFO"):
if previous_sle:
@@ -374,11 +374,15 @@ def get_avg_purchase_rate(serial_nos):
@frappe.request_cache
-def get_valuation_method(item_code):
+def get_valuation_method(item_code, company=None):
"""get valuation method from item or default"""
val_method = frappe.get_cached_value("Item", item_code, "valuation_method")
if not val_method:
- val_method = frappe.get_cached_doc("Stock Settings").valuation_method or "FIFO"
+ val_method = (
+ frappe.get_cached_value("Company", company, "valuation_method")
+ if company
+ else frappe.get_single_value("Stock Settings", "valuation_method") or "FIFO"
+ )
return val_method
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
index 11d4dad94ee..f8661e59e4b 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
@@ -172,11 +172,279 @@ frappe.ui.form.on("Subcontracting Order", {
__("Status")
);
}
+
+ if (frm.doc.reserve_stock) {
+ if (frm.doc.status !== "Closed") {
+ if (frm.doc.__onload && frm.doc.__onload.has_unreserved_stock) {
+ frm.add_custom_button(
+ __("Reserve"),
+ () => frm.events.create_stock_reservation_entries(frm),
+ __("Stock Reservation")
+ );
+ }
+ }
+
+ if (
+ frm.doc.__onload &&
+ frm.doc.__onload.has_reserved_stock &&
+ frappe.model.can_cancel("Stock Reservation Entry")
+ ) {
+ frm.add_custom_button(
+ __("Unreserve"),
+ () => frm.events.cancel_stock_reservation_entries(frm),
+ __("Stock Reservation")
+ );
+ }
+
+ frm.doc.supplied_items.forEach((item) => {
+ if (
+ flt(item.stock_reserved_qty) > 0 &&
+ frappe.model.can_read("Stock Reservation Entry")
+ ) {
+ frm.add_custom_button(
+ __("Reserved Stock"),
+ () => frm.events.show_reserved_stock(frm),
+ __("Stock Reservation")
+ );
+ return;
+ }
+ });
+ }
}
frm.trigger("get_materials_from_supplier");
},
+ create_stock_reservation_entries(frm) {
+ const dialog = new frappe.ui.Dialog({
+ title: __("Stock Reservation"),
+ size: "extra-large",
+ fields: [
+ {
+ fieldname: "items",
+ fieldtype: "Table",
+ label: __("Items to Reserve"),
+ allow_bulk_edit: false,
+ cannot_add_rows: true,
+ cannot_delete_rows: true,
+ data: [],
+ fields: [
+ {
+ fieldname: "subcontracting_order_supplied_item",
+ fieldtype: "Link",
+ label: __("Subcontracting Order Supplied Item"),
+ options: "Subcontracting Order Supplied Item",
+ reqd: 1,
+ in_list_view: 1,
+ read_only: 1,
+ get_query: () => {
+ return {
+ query: "erpnext.controllers.queries.get_filtered_child_rows",
+ filters: {
+ parenttype: frm.doc.doctype,
+ parent: frm.doc.name,
+ },
+ };
+ },
+ },
+ {
+ fieldname: "rm_item_code",
+ fieldtype: "Link",
+ label: __("Item Code"),
+ options: "Item",
+ reqd: 1,
+ read_only: 1,
+ in_list_view: 1,
+ },
+ {
+ fieldname: "warehouse",
+ fieldtype: "Link",
+ label: __("Warehouse"),
+ options: "Warehouse",
+ reqd: 1,
+ in_list_view: 1,
+ read_only: 1,
+ },
+ {
+ fieldname: "qty_to_reserve",
+ fieldtype: "Float",
+ label: __("Qty"),
+ reqd: 1,
+ in_list_view: 1,
+ },
+ ],
+ },
+ ],
+ primary_action_label: __("Reserve Stock"),
+ primary_action: () => {
+ var data = { items: dialog.fields_dict.items.grid.get_selected_children() };
+
+ if (data.items && data.items.length > 0) {
+ frappe.call({
+ doc: frm.doc,
+ method: "reserve_raw_materials",
+ args: {
+ items: data.items.map((item) => ({
+ name: item.subcontracting_order_supplied_item,
+ qty_to_reserve: item.qty_to_reserve,
+ })),
+ },
+ freeze: true,
+ freeze_message: __("Reserving Stock..."),
+ callback: (_) => {
+ frm.reload_doc();
+ },
+ });
+
+ dialog.hide();
+ } else {
+ frappe.msgprint(__("Please select items to reserve."));
+ }
+ },
+ });
+
+ frm.doc.supplied_items.forEach((item) => {
+ let unreserved_qty =
+ flt(item.required_qty) - flt(item.supplied_qty) - flt(item.stock_reserved_qty);
+
+ if (unreserved_qty > 0) {
+ dialog.fields_dict.items.df.data.push({
+ __checked: 1,
+ subcontracting_order_supplied_item: item.name,
+ rm_item_code: item.rm_item_code,
+ warehouse: item.reserve_warehouse,
+ qty_to_reserve: unreserved_qty,
+ });
+ }
+ });
+
+ dialog.fields_dict.items.grid.refresh();
+ dialog.show();
+ },
+
+ cancel_stock_reservation_entries(frm) {
+ const dialog = new frappe.ui.Dialog({
+ title: __("Stock Unreservation"),
+ size: "extra-large",
+ fields: [
+ {
+ fieldname: "sr_entries",
+ fieldtype: "Table",
+ label: __("Reserved Stock"),
+ allow_bulk_edit: false,
+ cannot_add_rows: true,
+ cannot_delete_rows: true,
+ in_place_edit: true,
+ data: [],
+ fields: [
+ {
+ fieldname: "sre",
+ fieldtype: "Link",
+ label: __("Stock Reservation Entry"),
+ options: "Stock Reservation Entry",
+ reqd: 1,
+ read_only: 1,
+ in_list_view: 1,
+ },
+ {
+ fieldname: "item_code",
+ fieldtype: "Link",
+ label: __("Item Code"),
+ options: "Item",
+ reqd: 1,
+ read_only: 1,
+ in_list_view: 1,
+ },
+ {
+ fieldname: "warehouse",
+ fieldtype: "Link",
+ label: __("Warehouse"),
+ options: "Warehouse",
+ reqd: 1,
+ read_only: 1,
+ in_list_view: 1,
+ },
+ {
+ fieldname: "qty",
+ fieldtype: "Float",
+ label: __("Qty"),
+ reqd: 1,
+ read_only: 1,
+ in_list_view: 1,
+ },
+ ],
+ },
+ ],
+ primary_action_label: __("Unreserve Stock"),
+ primary_action: () => {
+ var data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() };
+
+ if (data.sr_entries && data.sr_entries.length > 0) {
+ frappe.call({
+ doc: frm.doc,
+ method: "cancel_stock_reservation_entries",
+ args: {
+ sre_list: data.sr_entries.map((item) => item.sre),
+ },
+ freeze: true,
+ freeze_message: __("Unreserving Stock..."),
+ callback: (_) => {
+ frm.doc.__onload.has_reserved_stock = false;
+ frm.reload_doc();
+ },
+ });
+
+ dialog.hide();
+ } else {
+ frappe.msgprint(__("Please select items to unreserve."));
+ }
+ },
+ });
+
+ frappe
+ .call({
+ method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.get_stock_reservation_entries_for_voucher",
+ args: {
+ voucher_type: frm.doctype,
+ voucher_no: frm.doc.name,
+ },
+ callback: (r) => {
+ if (!r.exc && r.message) {
+ r.message.forEach((sre) => {
+ if (flt(sre.reserved_qty) > flt(sre.delivered_qty)) {
+ dialog.fields_dict.sr_entries.df.data.push({
+ sre: sre.name,
+ item_code: sre.item_code,
+ warehouse: sre.warehouse,
+ qty: flt(sre.reserved_qty) - flt(sre.delivered_qty),
+ });
+ }
+ });
+ }
+ },
+ })
+ .then((r) => {
+ dialog.fields_dict.sr_entries.grid.refresh();
+ dialog.show();
+ });
+ },
+
+ show_reserved_stock(frm) {
+ // Get the latest modified date from the items table.
+ var to_date = moment(new Date(Math.max(...frm.doc.items.map((e) => new Date(e.modified))))).format(
+ "YYYY-MM-DD"
+ );
+
+ frappe.route_options = {
+ company: frm.doc.company,
+ from_date: frm.doc.transaction_date,
+ to_date: to_date,
+ voucher_type: frm.doc.doctype,
+ voucher_no: frm.doc.name,
+ };
+ frappe.set_route("query-report", "Reserved Stock");
+ },
+
update_subcontracting_order_status(frm, status) {
frappe.call({
method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.update_subcontracting_order_status",
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
index 9d2888058a5..d0223a3acd2 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
@@ -36,6 +36,7 @@
"service_items",
"raw_materials_supplied_section",
"set_reserve_warehouse",
+ "reserve_stock",
"supplied_items",
"tab_address_and_contact",
"supplier_address",
@@ -62,7 +63,8 @@
"select_print_heading",
"column_break_43",
"letter_head",
- "tab_connections"
+ "tab_connections",
+ "production_plan"
],
"fields": [
{
@@ -471,6 +473,22 @@
"no_copy": 1,
"options": "Currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "reserve_stock",
+ "fieldtype": "Check",
+ "label": "Reserve Stock",
+ "no_copy": 1,
+ "show_on_timeline": 1
+ },
+ {
+ "fieldname": "production_plan",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Production Plan",
+ "no_copy": 1,
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
index be7b6ec2247..dc5f11953c4 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -8,6 +8,10 @@ from frappe.utils import flt
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.subcontracting_controller import SubcontractingController
+from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+ StockReservation,
+ has_reserved_stock,
+)
from erpnext.stock.stock_balance import update_bin_qty
from erpnext.stock.utils import get_bin
@@ -50,8 +54,10 @@ class SubcontractingOrder(SubcontractingController):
letter_head: DF.Link | None
naming_series: DF.Literal["SC-ORD-.YYYY.-"]
per_received: DF.Percent
+ production_plan: DF.Data | None
project: DF.Link | None
purchase_order: DF.Link
+ reserve_stock: DF.Check
schedule_date: DF.Date | None
select_print_heading: DF.Link | None
service_items: DF.Table[SubcontractingOrderServiceItem]
@@ -105,6 +111,13 @@ class SubcontractingOrder(SubcontractingController):
frappe.db.get_single_value("Buying Settings", "over_transfer_allowance"),
)
+ if self.reserve_stock:
+ if self.has_unreserved_stock():
+ self.set_onload("has_unreserved_stock", True)
+
+ if has_reserved_stock(self.doctype, self.name):
+ self.set_onload("has_reserved_stock", True)
+
def before_validate(self):
super().before_validate()
@@ -121,6 +134,7 @@ class SubcontractingOrder(SubcontractingController):
self.update_prevdoc_status()
self.update_status()
self.update_subcontracted_quantity_in_po()
+ self.reserve_raw_materials()
def on_cancel(self):
self.update_prevdoc_status()
@@ -253,10 +267,10 @@ class SubcontractingOrder(SubcontractingController):
if si.fg_item:
item = frappe.get_doc("Item", si.fg_item)
- qty, subcontracted_qty, fg_item_qty = frappe.db.get_value(
+ qty, subcontracted_qty, fg_item_qty, production_plan_sub_assembly_item = frappe.db.get_value(
"Purchase Order Item",
si.purchase_order_item,
- ["qty", "subcontracted_qty", "fg_item_qty"],
+ ["qty", "subcontracted_qty", "fg_item_qty", "production_plan_sub_assembly_item"],
)
available_qty = flt(qty) - flt(subcontracted_qty)
@@ -292,6 +306,7 @@ class SubcontractingOrder(SubcontractingController):
"purchase_order_item": si.purchase_order_item,
"material_request": si.material_request,
"material_request_item": si.material_request_item,
+ "production_plan_sub_assembly_item": production_plan_sub_assembly_item,
}
)
else:
@@ -362,6 +377,90 @@ class SubcontractingOrder(SubcontractingController):
subcontracted_qty,
)
+ @frappe.whitelist()
+ def reserve_raw_materials(self, items=None, stock_entry=None):
+ if self.reserve_stock:
+ item_dict = {}
+
+ if items:
+ item_dict = {d["name"]: d for d in items}
+ items = [item for item in self.supplied_items if item.name in item_dict]
+
+ reservation_items = []
+ is_transfer = False
+ for item in items or self.supplied_items:
+ data = frappe._dict(
+ {
+ "voucher_no": self.name,
+ "voucher_type": self.doctype,
+ "voucher_detail_no": item.name,
+ "item_code": item.rm_item_code,
+ "warehouse": item_dict.get(item.name, {}).get("warehouse", item.reserve_warehouse),
+ "stock_qty": item_dict.get(item.name, {}).get("qty_to_reserve", item.required_qty),
+ }
+ )
+
+ if stock_entry:
+ data.update(
+ {
+ "from_voucher_no": stock_entry,
+ "from_voucher_type": "Stock Entry",
+ "from_voucher_detail_no": item_dict[item.name]["reference_voucher_detail_no"],
+ "serial_and_batch_bundles": item_dict[item.name]["serial_and_batch_bundles"],
+ }
+ )
+ elif self.production_plan:
+ fg_item = next(i for i in self.items if i.name == item.reference_name)
+ if production_plan_sub_assembly_item := fg_item.production_plan_sub_assembly_item:
+ from_voucher_detail_no, reserved_qty = frappe.get_value(
+ "Material Request Plan Item",
+ {
+ "parent": self.production_plan,
+ "item_code": item.rm_item_code,
+ "warehouse": item.reserve_warehouse,
+ "sub_assembly_item_reference": production_plan_sub_assembly_item,
+ "docstatus": 1,
+ },
+ ["name", "stock_reserved_qty"],
+ )
+ if flt(item.stock_reserved_qty) < reserved_qty:
+ is_transfer = True
+ data.update(
+ {
+ "from_voucher_no": self.production_plan,
+ "from_voucher_type": "Production Plan",
+ "from_voucher_detail_no": from_voucher_detail_no,
+ }
+ )
+
+ reservation_items.append(data)
+
+ sre = StockReservation(self, items=reservation_items, notify=True)
+ if is_transfer:
+ sre.transfer_reservation_entries_to(
+ self.production_plan, from_doctype="Production Plan", to_doctype="Subcontracting Order"
+ )
+ else:
+ if sre.make_stock_reservation_entries():
+ frappe.msgprint(_("Stock Reservation Entries created"), alert=True, indicator="blue")
+
+ def has_unreserved_stock(self) -> bool:
+ for item in self.get("supplied_items"):
+ if item.required_qty - flt(item.supplied_qty) - flt(item.stock_reserved_qty) > 0:
+ return True
+
+ return False
+
+ @frappe.whitelist()
+ def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None:
+ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+ cancel_stock_reservation_entries,
+ )
+
+ cancel_stock_reservation_entries(
+ voucher_type=self.doctype, voucher_no=self.name, sre_list=sre_list, notify=notify
+ )
+
@frappe.whitelist()
def make_subcontracting_receipt(source_name, target_doc=None):
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py
index f17d8cd961c..b0615bf4a9e 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py
@@ -4,5 +4,15 @@ from frappe import _
def get_data():
return {
"fieldname": "subcontracting_order",
- "transactions": [{"label": _("Reference"), "items": ["Subcontracting Receipt", "Stock Entry"]}],
+ "non_standard_fieldnames": {"Stock Reservation Entry": "voucher_no"},
+ "transactions": [
+ {
+ "label": _("Reference"),
+ "items": ["Subcontracting Receipt", "Stock Entry"],
+ },
+ {
+ "label": _("Stock Reservation"),
+ "items": ["Stock Reservation Entry"],
+ },
+ ],
}
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
index 913a15d40c8..363a9cdd565 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -700,6 +700,126 @@ class TestSubcontractingOrder(IntegrationTestCase):
self.assertEqual(sco.supplied_items[0].required_qty, 210.149)
+ def test_stock_reservation(self):
+ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+ get_sre_details_for_voucher,
+ )
+
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 4",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA4",
+ "fg_item_qty": 10,
+ }
+ ]
+
+ sco = get_subcontracting_order(service_items=service_items, do_not_submit=1)
+ sco.reserve_stock = 1
+
+ rm_items = get_rm_items(sco.supplied_items)
+ make_stock_in_entry(rm_items=rm_items)
+ sco.submit()
+
+ sre_list = get_sre_details_for_voucher("Subcontracting Order", sco.name)
+ self.assertTrue(len(sre_list) > 0)
+
+ se_dict = make_rm_stock_entry(sco.name)
+ se = frappe.get_doc(se_dict)
+ se.items[-1].use_serial_batch_fields = 1
+ se.save()
+ se.submit()
+ sco.reload()
+
+ for sre in sre_list:
+ self.assertEqual(frappe.get_value("Stock Reservation Entry", sre.name, "status"), "Closed")
+
+ make_subcontracting_receipt(sco.name).submit()
+ for status in frappe.get_all(
+ "Stock Reservation Entry", filters={"voucher_no": sco.name, "docstatus": 1}, pluck="status"
+ )[:3]:
+ self.assertEqual(status, "Delivered")
+
+ def test_stock_reservation_transfer(self):
+ from erpnext.manufacturing.doctype.production_plan.production_plan import (
+ get_items_for_material_requests,
+ )
+ from erpnext.manufacturing.doctype.production_plan.test_production_plan import create_production_plan
+ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+ get_serial_batch_entries_for_voucher,
+ get_sre_details_for_voucher,
+ )
+
+ parent_fg = make_item()
+ make_bom(
+ item=parent_fg.name, raw_materials=["Subcontracted Item SA10"], rate=100, rm_qty=1, currency="INR"
+ )
+
+ plan = create_production_plan(
+ item_code=parent_fg.name,
+ planned_qty=10,
+ do_not_submit=True,
+ reserve_stock=True,
+ skip_available_sub_assembly_item=True,
+ for_warehouse="_Test Warehouse - _TC",
+ sub_assembly_warehouse="_Test Warehouse - _TC",
+ skip_getting_mr_items=True,
+ )
+ plan.get_sub_assembly_items()
+ plan.sub_assembly_items[0].supplier = "_Test Supplier"
+ mr_items = get_items_for_material_requests(plan.as_dict())
+ for d in mr_items:
+ plan.append("mr_items", d)
+
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="Subcontracted SRM Item 1", qty=10, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="Subcontracted SRM Item 2", qty=10, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="Subcontracted SRM Item 3", qty=10, basic_rate=100
+ )
+ plan.submit()
+
+ sre_against_plan = get_sre_details_for_voucher("Production Plan", plan.name)
+ sbe_pp_list = []
+ for sre in sre_against_plan:
+ sbe_pp_list.append(
+ sorted(
+ get_serial_batch_entries_for_voucher(sre.name),
+ key=lambda x: x.get("serial_no") or x.get("batch_no") or "",
+ )
+ )
+
+ plan.make_work_order()
+ po = frappe.get_doc(
+ "Purchase Order",
+ frappe.get_value("Purchase Order Item", {"production_plan": plan.name}, "parent"),
+ )
+ po.items[0].item_code = "Subcontracted Service Item 4"
+ po.items[0].qty = 10
+ po.submit()
+ so = create_subcontracting_order(po_name=po.name, do_not_save=1)
+ so.supplier_warehouse = "_Test Warehouse 1 - _TC"
+ so.reserve_stock = True
+ so.submit()
+ so.reload()
+
+ sre_against_so = get_sre_details_for_voucher("Subcontracting Order", so.name)
+ sbe_so_list = []
+ for sre in sre_against_so:
+ sbe_so_list.append(
+ sorted(
+ get_serial_batch_entries_for_voucher(sre.name),
+ key=lambda x: x.get("serial_no") or x.get("batch_no") or "",
+ )
+ )
+
+ self.assertEqual(sbe_pp_list, sbe_so_list)
+
def create_subcontracting_order(**args):
args = frappe._dict(args)
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json
index 98154b9f7f7..689b64492f5 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json
@@ -55,7 +55,8 @@
"section_break_34",
"purchase_order_item",
"page_break",
- "subcontracting_conversion_factor"
+ "subcontracting_conversion_factor",
+ "production_plan_sub_assembly_item"
],
"fields": [
{
@@ -407,6 +408,16 @@
"hidden": 1,
"label": "Subcontracting Conversion Factor",
"read_only": 1
+ },
+ {
+ "fieldname": "production_plan_sub_assembly_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Production Plan Sub Assembly Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1,
+ "report_hide": 1
}
],
"grid_page_length": 50,
@@ -414,7 +425,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2025-08-10 22:37:39.863628",
+ "modified": "2025-11-03 12:29:45.156101",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Item",
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py
index af741b6637c..bb390717171 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py
@@ -35,6 +35,7 @@ class SubcontractingOrderItem(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
+ production_plan_sub_assembly_item: DF.Data | None
project: DF.Link | None
purchase_order_item: DF.Data | None
qty: DF.Float
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json
index f4f8d540a85..acd6aae6220 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json
@@ -21,6 +21,7 @@
"section_break_13",
"required_qty",
"supplied_qty",
+ "stock_reserved_qty",
"column_break_16",
"consumed_qty",
"returned_qty",
@@ -52,7 +53,7 @@
{
"fieldname": "stock_uom",
"fieldtype": "Link",
- "label": "Stock Uom",
+ "label": "Stock UOM",
"options": "UOM",
"read_only": 1
},
@@ -160,18 +161,29 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:parent.reserve_stock",
+ "fieldname": "stock_reserved_qty",
+ "fieldtype": "Float",
+ "label": "Reserved Qty",
+ "no_copy": 1,
+ "non_negative": 1,
+ "read_only": 1
}
],
"hide_toolbar": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:10:46.680164",
+ "modified": "2025-10-30 16:00:43.379828",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Supplied Item",
"owner": "Administrator",
"permissions": [],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py
index 4892601d082..cace603faff 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py
@@ -28,6 +28,7 @@ class SubcontractingOrderSuppliedItem(Document):
reserve_warehouse: DF.Link | None
returned_qty: DF.Float
rm_item_code: DF.Link | None
+ stock_reserved_qty: DF.Float
stock_uom: DF.Link | None
supplied_qty: DF.Float
total_supplied_qty: DF.Float
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 51520d401ba..a622b1fb9c4 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -164,6 +164,8 @@ class SubcontractingReceipt(SubcontractingController):
for table_name in ["items", "supplied_items"]:
self.make_bundle_using_old_serial_batch_fields(table_name)
+
+ self.update_stock_reservation_entries()
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
@@ -189,6 +191,7 @@ class SubcontractingReceipt(SubcontractingController):
self.set_consumed_qty_in_subcontract_order()
self.set_subcontracting_order_status(update_bin=False)
self.update_stock_ledger()
+ self.update_stock_reservation_entries()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
self.update_status()
@@ -199,7 +202,7 @@ class SubcontractingReceipt(SubcontractingController):
def reset_raw_materials(self):
self.supplied_items = []
self.flags.reset_raw_materials = True
- self.create_raw_materials_supplied()
+ self.create_raw_materials_supplied_or_received()
def validate_closed_subcontracting_order(self):
for item in self.items:
@@ -853,6 +856,17 @@ class SubcontractingReceipt(SubcontractingController):
if frappe.db.get_single_value("Buying Settings", "auto_create_purchase_receipt"):
make_purchase_receipt(self, save=True, notify=True)
+ def has_reserved_stock(self):
+ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+ get_sre_details_for_voucher,
+ )
+
+ for item in self.supplied_items:
+ if get_sre_details_for_voucher("Subcontracting Order", item.subcontracting_order):
+ return True
+
+ return False
+
@frappe.whitelist()
def make_subcontract_return_against_rejected_warehouse(source_name):