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") }}
- {{ data.description }} + {% if data.is_phantom_item %} +

{{ __("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):