Merge branch 'develop' into holiday-list-assignment

This commit is contained in:
Asmita Hase
2025-11-17 11:53:56 +05:30
committed by GitHub
82 changed files with 1700 additions and 600 deletions

View File

@@ -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"]:

View File

@@ -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",

View File

@@ -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(

View File

@@ -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

View File

@@ -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):

View File

@@ -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 (

View File

@@ -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:

View File

@@ -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():

View File

@@ -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"

View File

@@ -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'

View File

@@ -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"

View File

@@ -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 "شماره قطعه تولید کننده <b>{0}</b> نامعتبر اس
#. 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 <b>equal</b> 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"

View File

@@ -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 "<span class=\"h4\"><b>Masters &amp; Reports</b></span>"
msgstr "<span class=\"h4\"><b>Rapports & Fonctionnalités principales</b></span>"
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 lemployé"
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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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", {

View File

@@ -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",

View File

@@ -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",
)

View File

@@ -12,7 +12,10 @@
{{ __("Description") }}
</h4>
<div style="padding-top: 10px;">
{{ data.description }}
{% if data.is_phantom_item %}
<p><b>{{ __("Phantom Item") }}</b></p>
{% endif %}
<p>{{ data.description }}</p>
</div>
<hr style="margin: 15px -15px;">
<p>

View File

@@ -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"]

View File

@@ -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"):

View File

@@ -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": []
}
}

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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 (

View File

@@ -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. <br><br> 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"),

View File

@@ -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",

View File

@@ -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

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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,
}
)

View File

@@ -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",

View File

@@ -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");
},

View File

@@ -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")

View File

@@ -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);

View File

@@ -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,
}

View File

@@ -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},

View File

@@ -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():

View File

@@ -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),

View File

@@ -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

View File

@@ -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,
]
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;

View File

@@ -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") {

View File

@@ -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

View File

@@ -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",

View File

@@ -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()

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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:

View File

@@ -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()

View File

@@ -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)
)

View File

@@ -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);

View File

@@ -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)

View File

@@ -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",

View File

@@ -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 = []

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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):

View File

@@ -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"],
},
],
}

View File

@@ -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)

View File

@@ -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",

View File

@@ -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

View File

@@ -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": []
}
}

View File

@@ -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

View File

@@ -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):