From 00b6b971978745f2a4e31e1ab22c983df9b561ff Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Sun, 13 Apr 2025 13:17:15 +0530 Subject: [PATCH 01/35] fix: check return_against exists before api call --- .../public/js/controllers/taxes_and_totals.js | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index a65fee43897..c7c727457bf 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -894,23 +894,25 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { it should set the return to that mode of payment only. */ - let return_against_mop = await frappe.call({ - method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data', - args: { - invoice: this.frm.doc.return_against - } - }); - - if (return_against_mop.message.length === 1) { - this.frm.doc.payments.forEach(payment => { - if (payment.mode_of_payment == return_against_mop.message[0].mode_of_payment) { - payment.amount = total_amount_to_pay; - } else { - payment.amount = 0; + if(this.frm.doc.return_against){ + let {message : return_against_mop } = await frappe.call({ + method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data', + args: { + invoice: this.frm.doc.return_against } }); - this.frm.refresh_fields(); - return; + + if (return_against_mop.length === 1) { + this.frm.doc.payments.forEach(payment => { + if (payment.mode_of_payment == return_against_mop[0].mode_of_payment) { + payment.amount = total_amount_to_pay; + } else { + payment.amount = 0; + } + }); + this.frm.refresh_fields(); + return; + } } this.frm.doc.payments.find(payment => { From c48db0b7c0391a6c87d552461756ad69f5866326 Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Sun, 13 Apr 2025 13:24:33 +0530 Subject: [PATCH 02/35] fix: ensure backend response is awaited before saving --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- erpnext/public/js/controllers/transaction.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index c7c727457bf..300f46f719c 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -64,7 +64,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { && this.frm.doc.is_pos && this.frm.doc.is_return ) { - this.set_total_amount_to_default_mop(); + await this.set_total_amount_to_default_mop(); this.calculate_paid_amount(); } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index f7a5babd4e2..152ceb10cad 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -843,8 +843,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length); } - validate() { - this.calculate_taxes_and_totals(false); + async validate() { + await this.calculate_taxes_and_totals(false); } update_stock() { From af283d0d9e14410040ed753f2a29965ac8f3655e Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Wed, 23 Apr 2025 20:26:36 +0530 Subject: [PATCH 03/35] perf: improved gle map init --- .../report/general_ledger/general_ledger.py | 87 +++++++++++-------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 6b569359f5b..c1cac812fe2 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -2,9 +2,6 @@ # License: GNU General Public License v3. See license.txt -import copy -from collections import OrderedDict - import frappe from frappe import _, _dict from frappe.query_builder import Criterion @@ -19,6 +16,15 @@ from erpnext.accounts.report.financial_statements import get_cost_centers_with_c from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency from erpnext.accounts.utils import get_account_currency +DEBIT_CREDIT_DICT = { + "debit": 0.0, + "credit": 0.0, + "debit_in_account_currency": 0.0, + "credit_in_account_currency": 0.0, + "debit_in_transaction_currency": None, + "credit_in_transaction_currency": None, +} + def execute(filters=None): if not filters: @@ -361,18 +367,35 @@ def set_bill_no(gl_entries): gl["bill_no"] = inv_details.get(gl.get("against_voucher"), "") +def get_translated_labels_for_totals(): + def wrap_in_quotes(label): + return f"'{label}'" + + return { + "opening": wrap_in_quotes(_("Opening")), + "total": wrap_in_quotes(_("Total")), + "closing": wrap_in_quotes(_("Closing (Opening + Total)")), + } + + def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): + def add_total_to_data(totals, key): + row = totals[key] + row["account"] = labels[key] + data.append(row) + + labels = get_translated_labels_for_totals() + data = [] - totals_dict = get_totals_dict() set_bill_no(gl_entries) - gle_map = initialize_gle_map(gl_entries, filters, totals_dict) + gle_map = initialize_gle_map(gl_entries, filters) - totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals_dict) + totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map) # Opening for filtered account - data.append(totals.opening) + add_total_to_data(totals, "opening") if filters.get("group_by") != "Group by Voucher (Consolidated)": for _acc, acc_dict in gle_map.items(): @@ -383,53 +406,42 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension if (not filters.get("group_by") and not filters.get("voucher_no")) or ( filters.get("group_by") and filters.get("group_by") != "Group by Voucher" ): - data.append(acc_dict.totals.opening) + add_total_to_data(acc_dict.totals, "opening") data += acc_dict.entries # totals if filters.get("group_by") or not filters.voucher_no: - data.append(acc_dict.totals.total) + add_total_to_data(acc_dict.totals, "total") # closing if (not filters.get("group_by") and not filters.get("voucher_no")) or ( filters.get("group_by") and filters.get("group_by") != "Group by Voucher" ): - data.append(acc_dict.totals.closing) + add_total_to_data(acc_dict.totals, "closing") data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None}) else: data += entries # totals - data.append(totals.total) + add_total_to_data(totals, "total") # closing - data.append(totals.closing) + add_total_to_data(totals, "closing") return data def get_totals_dict(): - def _get_debit_credit_dict(label): - return _dict( - account=f"'{label}'", - debit=0.0, - credit=0.0, - debit_in_account_currency=0.0, - credit_in_account_currency=0.0, - debit_in_transaction_currency=None, - credit_in_transaction_currency=None, - ) - return _dict( - opening=_get_debit_credit_dict(_("Opening")), - total=_get_debit_credit_dict(_("Total")), - closing=_get_debit_credit_dict(_("Closing (Opening + Total)")), + opening=_dict(DEBIT_CREDIT_DICT), + total=_dict(DEBIT_CREDIT_DICT), + closing=_dict(DEBIT_CREDIT_DICT), ) -def group_by_field(group_by): +def get_group_by_field(group_by): if group_by == "Group by Party": return "party" elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]: @@ -438,19 +450,25 @@ def group_by_field(group_by): return "voucher_no" -def initialize_gle_map(gl_entries, filters, totals_dict): - gle_map = OrderedDict() - group_by = group_by_field(filters.get("group_by")) +def initialize_gle_map(gl_entries, filters): + gle_map = {} + group_by = get_group_by_field(filters.get("group_by")) for gle in gl_entries: - gle_map.setdefault(gle.get(group_by), _dict(totals=copy.deepcopy(totals_dict), entries=[])) + group_by_value = gle.get(group_by) + if group_by_value not in gle_map: + gle_map[group_by_value] = _dict( + totals=get_totals_dict(), + entries=[], + ) + return gle_map -def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals): +def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): entries = [] - consolidated_gle = OrderedDict() - group_by = group_by_field(filters.get("group_by")) + consolidated_gle = {} + group_by = get_group_by_field(filters.get("group_by")) group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)" if filters.get("show_net_values_in_party_account"): @@ -496,6 +514,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) show_opening_entries = filters.get("show_opening_entries") + totals = get_totals_dict() for gle in gl_entries: group_by_value = gle.get(group_by) gle.voucher_subtype = _(gle.voucher_subtype) From 7131396ac06abcd9fe44f7af1d1d98134e1257ed Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:10:42 +0530 Subject: [PATCH 04/35] perf: evaluate conditions outside loop --- .../report/general_ledger/general_ledger.py | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index c1cac812fe2..a153b03d3ed 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -398,27 +398,29 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension add_total_to_data(totals, "opening") if filters.get("group_by") != "Group by Voucher (Consolidated)": - for _acc, acc_dict in gle_map.items(): - # acc - if acc_dict.entries: - # opening - data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None}) - if (not filters.get("group_by") and not filters.get("voucher_no")) or ( - filters.get("group_by") and filters.get("group_by") != "Group by Voucher" - ): - add_total_to_data(acc_dict.totals, "opening") + set_opening_closing = (not filters.get("group_by") and not filters.get("voucher_no")) or ( + filters.get("group_by") and filters.get("group_by") != "Group by Voucher" + ) + set_total = filters.get("group_by") or not filters.voucher_no - data += acc_dict.entries + for acc_dict in gle_map.values(): + if not acc_dict.entries: + continue - # totals - if filters.get("group_by") or not filters.voucher_no: - add_total_to_data(acc_dict.totals, "total") + # opening + data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None}) + if set_opening_closing: + add_total_to_data(acc_dict.totals, "opening") - # closing - if (not filters.get("group_by") and not filters.get("voucher_no")) or ( - filters.get("group_by") and filters.get("group_by") != "Group by Voucher" - ): - add_total_to_data(acc_dict.totals, "closing") + data += acc_dict.entries + + # totals + if set_total: + add_total_to_data(acc_dict.totals, "total") + + # closing + if set_opening_closing: + add_total_to_data(acc_dict.totals, "closing") data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None}) else: From 68e4be28873d1dce88ad8cedcffaf1c290b36200 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 6 May 2025 22:36:16 +0530 Subject: [PATCH 05/35] fix: Swedish translations --- erpnext/locale/sv.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/locale/sv.po b/erpnext/locale/sv.po index e38a919693d..75b55666beb 100644 --- a/erpnext/locale/sv.po +++ b/erpnext/locale/sv.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-05-04 09:35+0000\n" -"PO-Revision-Date: 2025-05-05 10:00\n" +"PO-Revision-Date: 2025-05-06 17:06\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Swedish\n" "MIME-Version: 1.0\n" @@ -19574,7 +19574,7 @@ msgstr "Befintlig Kund" #. Label of the exit (Tab Break) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Exit" -msgstr "Avgång" +msgstr "Avsluta" #: erpnext/selling/page/point_of_sale/pos_controller.js:254 msgid "Exit Full Screen" @@ -19583,7 +19583,7 @@ msgstr "Avsluta Helskärm" #. Label of the held_on (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Exit Interview Held On" -msgstr "Avgång Intervju" +msgstr "Avgång Intervju Datum" #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:154 #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:187 From 0bd6ffaee61dee2b673a8c544a7e7b5bae439e48 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 6 May 2025 22:36:20 +0530 Subject: [PATCH 06/35] fix: Turkish translations --- erpnext/locale/tr.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/locale/tr.po b/erpnext/locale/tr.po index 9356206bac3..e6304f55933 100644 --- a/erpnext/locale/tr.po +++ b/erpnext/locale/tr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-05-04 09:35+0000\n" -"PO-Revision-Date: 2025-05-05 17:02\n" +"PO-Revision-Date: 2025-05-06 17:06\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Turkish\n" "MIME-Version: 1.0\n" @@ -146,7 +146,7 @@ msgstr "% Bitmiş Ürün Miktarı" #. Label of the per_installed (Percent) field in DocType 'Delivery Note' #: erpnext/stock/doctype/delivery_note/delivery_note.json msgid "% Installed" -msgstr "% Kuruldu" +msgstr "% Tamamlandı" #: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js:70 #: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html:16 From 208ac196507d5f3bb33c3e191304ee8a3538bbe2 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 6 May 2025 22:36:31 +0530 Subject: [PATCH 07/35] fix: Persian translations --- erpnext/locale/fa.po | 58 ++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/erpnext/locale/fa.po b/erpnext/locale/fa.po index 5232482fd26..6825ecf9b2e 100644 --- a/erpnext/locale/fa.po +++ b/erpnext/locale/fa.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-05-04 09:35+0000\n" -"PO-Revision-Date: 2025-05-05 10:00\n" +"PO-Revision-Date: 2025-05-06 17:06\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Persian\n" "MIME-Version: 1.0\n" @@ -89,7 +89,7 @@ msgstr "\"آیتم تامین شده توسط مشتری\" نمی‌تواند #: erpnext/stock/doctype/item/item.py:237 msgid "\"Customer Provided Item\" cannot have Valuation Rate" -msgstr "\"آیتم تامین شده توسط مشتری\" نمی‌تواند دارای نرخ ارزیابی باشد" +msgstr "\"آیتم تامین شده توسط مشتری\" نمی‌تواند دارای نرخ ارزش‌گذاری باشد" #: erpnext/stock/doctype/item/item.py:313 msgid "\"Is Fixed Asset\" cannot be unchecked, as Asset record exists against the item" @@ -352,7 +352,7 @@ msgstr "(H) تغییر در ارزش موجودی (صف FIFO)" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:209 msgid "(H) Valuation Rate" -msgstr "(H) نرخ ارزش گذاری" +msgstr "(H) نرخ ارزش‌گذاری" #. Description of the 'Actual Operating Cost' (Currency) field in DocType 'Work #. Order Operation' @@ -363,12 +363,12 @@ msgstr "(نرخ ساعت / 60) * زمان عملیات واقعی" #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:273 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:176 msgid "(I) Valuation Rate" -msgstr "(I) نرخ ارزش گذاری" +msgstr "(I) نرخ ارزش‌گذاری" #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:278 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:181 msgid "(J) Valuation Rate as per FIFO" -msgstr "(J) نرخ ارزش گذاری مطابق با FIFO" +msgstr "(J) نرخ ارزش‌گذاری مطابق با FIFO" #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:288 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:191 @@ -4278,7 +4278,7 @@ msgstr "اجازه نرخ صفر" #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json #: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json msgid "Allow Zero Valuation Rate" -msgstr "نرخ ارزش گذاری صفر مجاز است" +msgstr "نرخ ارزش‌گذاری صفر مجاز است" #. Label of the allow_existing_serial_no (Check) field in DocType 'Stock #. Settings' @@ -13896,7 +13896,7 @@ msgstr "زمان کنونی" #. Reconciliation Item' #: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json msgid "Current Valuation Rate" -msgstr "نرخ ارزش گذاری فعلی" +msgstr "نرخ ارزش‌گذاری فعلی" #: erpnext/selling/report/sales_analytics/sales_analytics.js:90 msgid "Curves" @@ -23231,7 +23231,7 @@ msgstr "" #. field in DocType 'Stock Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json msgid "If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate." -msgstr "اگر فعال شود، نرخ آیتم در انتقالات داخلی با نرخ ارزیابی تنظیم نخواهد شد، اما حسابداری همچنان از نرخ ارزیابی استفاده خواهد کرد." +msgstr "اگر فعال شود، نرخ آیتم در انتقالات داخلی با نرخ ارزش‌گذاری تنظیم نخواهد شد، اما حسابداری همچنان از نرخ ارزش‌گذاری استفاده خواهد کرد." #. Description of the 'Allow UOM with Conversion Rate Defined in Item' (Check) #. field in DocType 'Stock Settings' @@ -23253,7 +23253,7 @@ msgstr "" #. DocType 'Stock Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json msgid "If enabled, the system will use the moving average valuation method to calculate the valuation rate for the batched items and will not consider the individual batch-wise incoming rate." -msgstr "اگر فعال شود، سیستم از روش ارزیابی میانگین متحرک برای محاسبه نرخ ارزیابی آیتم‌های دسته‌ای استفاده خواهد کرد و نرخ ورودی هر دسته را به‌طور جداگانه در نظر نخواهد گرفت." +msgstr "اگر فعال شود، سیستم از روش ارزیابی میانگین متحرک برای محاسبه نرخ ارزش‌گذاری آیتم‌های دسته‌ای استفاده خواهد کرد و نرخ ورودی هر دسته را به‌طور جداگانه در نظر نخواهد گرفت." #. Description of the 'Validate Applied Rule' (Check) field in DocType 'Pricing #. Rule' @@ -23310,7 +23310,7 @@ msgstr "اگر حساب مسدود شود، ورود به کاربران محد #: erpnext/stock/stock_ledger.py:1859 msgid "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table." -msgstr "اگر آیتم به عنوان یک آیتم نرخ ارزش گذاری صفر در این ثبت تراکنش می‌شود، لطفاً \"نرخ ارزش گذاری صفر مجاز\" را در جدول آیتم {0} فعال کنید." +msgstr "اگر آیتم به عنوان یک آیتم نرخ ارزش‌گذاری صفر در این ثبت تراکنش می‌شود، لطفاً \"نرخ ارزش‌گذاری صفر مجاز\" را در جدول آیتم {0} فعال کنید." #: erpnext/manufacturing/doctype/work_order/work_order.js:1088 msgid "If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed." @@ -23373,7 +23373,7 @@ msgstr "اگر علامت زده شود، می‌توان از چندین ماد #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js:36 msgid "If ticked, the BOM cost will be automatically updated based on Valuation Rate / Price List Rate / last purchase rate of raw materials." -msgstr "در صورت علامت زدن، هزینه BOM به طور خودکار بر اساس نرخ ارزش گذاری / نرخ لیست قیمت / آخرین نرخ خرید مواد اولیه به روز می‌شود." +msgstr "در صورت علامت زدن، هزینه BOM به طور خودکار بر اساس نرخ ارزش‌گذاری / نرخ لیست قیمت / آخرین نرخ خرید مواد اولیه به روز می‌شود." #: erpnext/accounts/doctype/loyalty_program/loyalty_program.js:14 msgid "If unlimited expiry for the Loyalty Points, keep the Expiry Duration empty or 0." @@ -27037,7 +27037,7 @@ msgstr "تعداد مورد را نمی‌توان به روز کرد زیرا #: erpnext/stock/doctype/stock_entry/stock_entry.py:865 msgid "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}" -msgstr "نرخ آیتم به صفر به‌روزرسانی شده است زیرا نرخ ارزش گذاری مجاز صفر برای آیتم صفر {0} بررسی می‌شود" +msgstr "نرخ آیتم به صفر به‌روزرسانی شده است زیرا نرخ ارزش‌گذاری مجاز صفر برای آیتم صفر {0} بررسی می‌شود" #. Label of the finished_good (Link) field in DocType 'Job Card' #: erpnext/manufacturing/doctype/job_card/job_card.json @@ -27317,7 +27317,7 @@ msgstr "آیتم‌ها برای درخواست مواد خام" #: erpnext/stock/doctype/stock_entry/stock_entry.py:861 msgid "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}" -msgstr "نرخ آیتم‌ها به صفر به‌روزرسانی شده است زیرا نرخ ارزش گذاری مجاز صفر برای آیتم‌های زیر بررسی می‌شود: {0}" +msgstr "نرخ آیتم‌ها به صفر به‌روزرسانی شده است زیرا نرخ ارزش‌گذاری مجاز صفر برای آیتم‌های زیر بررسی می‌شود: {0}" #. Label of the items_to_be_repost (Code) field in DocType 'Repost Item #. Valuation' @@ -30083,7 +30083,7 @@ msgstr "مگاوات" #: erpnext/stock/stock_ledger.py:1872 msgid "Mention Valuation Rate in the Item master." -msgstr "نرخ ارزش گذاری را در آیتم اصلی ذکر کنید." +msgstr "نرخ ارزش‌گذاری را در آیتم اصلی ذکر کنید." #. Description of the 'Accounts' (Table) field in DocType 'Customer' #: erpnext/selling/doctype/customer/customer.json @@ -31211,7 +31211,7 @@ msgstr "مقدار منفی مجاز نیست" #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:573 msgid "Negative Valuation Rate is not allowed" -msgstr "نرخ ارزش گذاری منفی مجاز نیست" +msgstr "نرخ ارزش‌گذاری منفی مجاز نیست" #: erpnext/setup/setup_wizard/data/sales_stage.txt:8 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:418 @@ -37542,7 +37542,7 @@ msgstr "لطفا حداقل یک ویژگی را در جدول Attributes مشخ #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:563 msgid "Please specify either Quantity or Valuation Rate or both" -msgstr "لطفاً مقدار یا نرخ ارزش گذاری یا هر دو را مشخص کنید" +msgstr "لطفاً مقدار یا نرخ ارزش‌گذاری یا هر دو را مشخص کنید" #: erpnext/stock/doctype/item_attribute/item_attribute.py:93 msgid "Please specify from/to range" @@ -44933,7 +44933,7 @@ msgstr "ردیف #{0}: زمان‌بندی با ردیف {1} در تضاد اس #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:97 msgid "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries." -msgstr "ردیف #{0}: نمی‌توانید از بعد موجودی «{1}» در تطبیق موجودی برای تغییر مقدار یا نرخ ارزیابی استفاده کنید. تطبیق موجودی با ابعاد موجودی صرفاً برای انجام ورودی های افتتاحیه در نظر گرفته شده است." +msgstr "ردیف #{0}: نمی‌توانید از بعد موجودی «{1}» در تطبیق موجودی برای تغییر مقدار یا نرخ ارزش‌گذاری استفاده کنید. تطبیق موجودی با ابعاد موجودی صرفاً برای انجام ورودی های افتتاحیه در نظر گرفته شده است." #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1526 msgid "Row #{0}: You must select an Asset for Item {1}." @@ -44965,7 +44965,7 @@ msgstr "ردیف #{idx}: هنگام تامین مواد خام به پیمانک #: erpnext/controllers/buying_controller.py:406 msgid "Row #{idx}: Item rate has been updated as per valuation rate since its an internal stock transfer." -msgstr "ردیف #{idx}: نرخ آیتم براساس نرخ ارزیابی به‌روزرسانی شده است، زیرا یک انتقال داخلی موجودی است." +msgstr "ردیف #{idx}: نرخ آیتم براساس نرخ ارزش‌گذاری به‌روزرسانی شده است، زیرا یک انتقال داخلی موجودی است." #: erpnext/controllers/buying_controller.py:870 msgid "Row #{idx}: Please enter a location for the asset item {item_code}." @@ -45224,7 +45224,7 @@ msgstr "ردیف {0}: الگوی مالیات آیتم بر اساس اعتبا #: erpnext/controllers/selling_controller.py:541 msgid "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" -msgstr "ردیف {0}: نرخ اقلام براساس نرخ ارزیابی به‌روزرسانی شده است، زیرا یک انتقال داخلی موجودی است" +msgstr "ردیف {0}: نرخ اقلام براساس نرخ ارزش‌گذاری به‌روزرسانی شده است، زیرا یک انتقال داخلی موجودی است" #: erpnext/controllers/subcontracting_controller.py:98 msgid "Row {0}: Item {1} must be a stock item." @@ -48276,7 +48276,7 @@ msgstr "تنظیم انبار هدف" #. Creator' #: erpnext/manufacturing/doctype/bom_creator/bom_creator.json msgid "Set Valuation Rate Based on Source Warehouse" -msgstr "نرخ ارزش گذاری را بر اساس انبار منبع تنظیم کنید" +msgstr "نرخ ارزش‌گذاری را بر اساس انبار منبع تنظیم کنید" #: erpnext/selling/doctype/sales_order/sales_order.js:216 msgid "Set Warehouse" @@ -56938,7 +56938,7 @@ msgstr "به روز رسانی هزینه BOM به صورت خودکار" #. 'Manufacturing Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json msgid "Update BOM cost automatically via scheduler, based on the latest Valuation Rate/Price List Rate/Last Purchase Rate of raw materials" -msgstr "به‌روزرسانی هزینه BOM به‌طور خودکار از طریق زمان‌بندی، بر اساس آخرین نرخ ارزیابی/نرخ فهرست قیمت/آخرین نرخ خرید مواد خام" +msgstr "به‌روزرسانی هزینه BOM به‌طور خودکار از طریق زمان‌بندی، بر اساس آخرین نرخ ارزش‌گذاری/نرخ فهرست قیمت/آخرین نرخ خرید مواد خام" #. Label of the update_billed_amount_in_delivery_note (Check) field in DocType #. 'POS Invoice' @@ -57551,7 +57551,7 @@ msgstr "" #. Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json msgid "Validate Selling Price for Item Against Purchase Rate or Valuation Rate" -msgstr "اعتبارسنجی قیمت فروش کالا در مقایسه با نرخ خرید یا نرخ ارزیابی" +msgstr "اعتبارسنجی قیمت فروش کالا در مقایسه با نرخ خرید یا نرخ ارزش‌گذاری" #. Label of the validate_stock_on_save (Check) field in DocType 'POS Profile' #: erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -57648,27 +57648,27 @@ msgstr "روش ارزش گذاری" #: erpnext/stock/report/stock_balance/stock_balance.py:489 #: erpnext/stock/report/stock_ledger/stock_ledger.py:297 msgid "Valuation Rate" -msgstr "نرخ ارزش گذاری" +msgstr "نرخ ارزش‌گذاری" #: erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py:166 msgid "Valuation Rate (In / Out)" -msgstr "نرخ ارزش گذاری (ورودی/خروجی)" +msgstr "نرخ ارزش‌گذاری (ورودی/خروجی)" #: erpnext/stock/stock_ledger.py:1875 msgid "Valuation Rate Missing" -msgstr "نرخ ارزیابی وجود ندارد" +msgstr "نرخ ارزش‌گذاری وجود ندارد" #: erpnext/stock/stock_ledger.py:1853 msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." -msgstr "نرخ ارزش گذاری برای آیتم {0}، برای انجام ثبت‌های حسابداری برای {1} {2} لازم است." +msgstr "نرخ ارزش‌گذاری برای آیتم {0}، برای انجام ثبت‌های حسابداری برای {1} {2} لازم است." #: erpnext/stock/doctype/item/item.py:266 msgid "Valuation Rate is mandatory if Opening Stock entered" -msgstr "در صورت ثبت موجودی افتتاحیه، نرخ ارزش گذاری الزامی است" +msgstr "در صورت ثبت موجودی افتتاحیه، نرخ ارزش‌گذاری الزامی است" #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:710 msgid "Valuation Rate required for Item {0} at row {1}" -msgstr "نرخ ارزش گذاری الزامی است برای آیتم {0} در ردیف {1}" +msgstr "نرخ ارزش‌گذاری الزامی است برای آیتم {0} در ردیف {1}" #. Option for the 'Consider Tax or Charge for' (Select) field in DocType #. 'Purchase Taxes and Charges' @@ -57678,7 +57678,7 @@ msgstr "ارزش گذاری و کل" #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:929 msgid "Valuation rate for customer provided items has been set to zero." -msgstr "نرخ ارزش گذاری برای آیتم‌های ارائه شده توسط مشتری صفر تعیین شده است." +msgstr "نرخ ارزش‌گذاری برای آیتم‌های ارائه شده توسط مشتری صفر تعیین شده است." #. Description of the 'Sales Incoming Rate' (Currency) field in DocType #. 'Purchase Invoice Item' From f4ffc57b5107f0d9da0b16824dbac23952befc9a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 2 May 2025 18:55:53 +0530 Subject: [PATCH 08/35] fix: added PR/PI overbilling validation --- .../doctype/purchase_invoice/test_purchase_invoice.py | 3 +++ .../stock/doctype/purchase_receipt/purchase_receipt.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 68fd44cfdd6..4c2ac0f44aa 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1702,6 +1702,9 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin): # Configure Buying Settings to allow rate change frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + # Configure Accounts Settings to allow 300% over billing + frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 300) + # Create PR: rate = 1000, qty = 5 pr = make_purchase_receipt( item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 49f04931932..d499ffd219a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1105,6 +1105,7 @@ def get_billed_amount_against_po(po_items): def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): # Update Billing % based on pending accepted qty buying_settings = frappe.get_single("Buying Settings") + over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") total_amount, total_billed_amount = 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) @@ -1143,6 +1144,14 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) + elif item.billed_amt > item.amount: + per_over_billed = (flt(item.billed_amt / item.amount, 2) * 100) - 100 + if per_over_billed > over_billing_allowance: + frappe.throw( + _("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format( + item.name, frappe.bold(item.item_code), per_over_billed - over_billing_allowance + ) + ) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) From 96071ae891cf426c3c57de1e99c68ed11710bd55 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 7 May 2025 22:59:04 +0530 Subject: [PATCH 09/35] fix: Persian translations --- erpnext/locale/fa.po | 128 +++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/erpnext/locale/fa.po b/erpnext/locale/fa.po index 6825ecf9b2e..b7e23d8cdee 100644 --- a/erpnext/locale/fa.po +++ b/erpnext/locale/fa.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-05-04 09:35+0000\n" -"PO-Revision-Date: 2025-05-06 17:06\n" +"PO-Revision-Date: 2025-05-07 17:29\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Persian\n" "MIME-Version: 1.0\n" @@ -2221,7 +2221,7 @@ msgstr "اقدام اگر بازرسی کیفیت رد شود" #. Settings' #: erpnext/buying/doctype/buying_settings/buying_settings.json msgid "Action If Same Rate is Not Maintained" -msgstr "اگر همان نرخ حفظ نشود، اقدام کنید" +msgstr "اقدام در صورت عدم حفظ نرخ یکسان" #: erpnext/quality_management/doctype/quality_review/quality_review_list.js:7 msgid "Action Initialised" @@ -4320,7 +4320,7 @@ msgstr "" #. 'Manufacturing Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json msgid "Allow transferring raw materials even after the Required Quantity is fulfilled" -msgstr "امکان انتقال مواد خام حتی پس از برآورده شدن مقدار مورد نیاز" +msgstr "اجازه انتقال مواد خام حتی پس از برآورده شدن مقدار مورد نیاز" #. Label of the allowed (Check) field in DocType 'Repost Allowed Types' #: erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json @@ -6061,7 +6061,7 @@ msgstr "در ردیف #{0}: مقدار انتخاب شده {1} برای آیتم #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:84 msgid "At least one account with exchange gain or loss is required" -msgstr "" +msgstr "حداقل یک حساب با سود یا زیان تبدیل مورد نیاز است" #: erpnext/assets/doctype/asset/asset.py:1123 msgid "At least one asset has to be selected." @@ -11551,7 +11551,7 @@ msgstr "پیکربندی اسمبلی محصول" #. DocType 'Buying Settings' #: erpnext/buying/doctype/buying_settings/buying_settings.json msgid "Configure the action to stop the transaction or just warn if the same rate is not maintained." -msgstr "کنش را طوری پیکربندی کنید که تراکنش را متوقف کند یا در صورت عدم حفظ همان نرخ فقط هشدار دهد." +msgstr "تنظیم کنید که در صورت عدم حفظ همان نرخ، تراکنش متوقف شود یا فقط هشدار داده شود." #: erpnext/buying/doctype/buying_settings/buying_settings.js:20 msgid "Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List." @@ -13693,21 +13693,21 @@ msgstr "تبدیل ارز" #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json #: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json msgid "Currency Exchange Settings" -msgstr "تنظیمات تبادل ارز" +msgstr "تنظیمات تبدیل ارز" #. Name of a DocType #: erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json msgid "Currency Exchange Settings Details" -msgstr "جزئیات تنظیمات تبادل ارز" +msgstr "جزئیات تنظیمات تبدیل ارز" #. Name of a DocType #: erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json msgid "Currency Exchange Settings Result" -msgstr "نتیجه تنظیمات تبادل ارز" +msgstr "نتیجه تنظیمات تبدیل ارز" #: erpnext/setup/doctype/currency_exchange/currency_exchange.py:55 msgid "Currency Exchange must be applicable for Buying or for Selling." -msgstr "مبادله ارز باید برای خرید یا فروش قابل اجرا باشد." +msgstr "تبدیل ارز باید برای خرید یا فروش قابل اجرا باشد." #. Label of the currency_and_price_list (Section Break) field in DocType 'POS #. Invoice' @@ -14297,7 +14297,7 @@ msgstr "گروه مشتریان" #. Name of a DocType #: erpnext/accounts/doctype/customer_group_item/customer_group_item.json msgid "Customer Group Item" -msgstr "مورد گروه مشتری" +msgstr "آیتم گروه مشتری" #. Label of the customer_group_name (Data) field in DocType 'Customer Group' #: erpnext/setup/doctype/customer_group/customer_group.json @@ -14316,7 +14316,7 @@ msgstr "گروه های مشتری" #. Name of a DocType #: erpnext/accounts/doctype/customer_item/customer_item.json msgid "Customer Item" -msgstr "مورد مشتری" +msgstr "آیتم مشتری" #. Label of the customer_items (Table) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json @@ -15756,7 +15756,7 @@ msgstr "روزهای تاخیری" #. Name of a report #: erpnext/stock/report/delayed_item_report/delayed_item_report.json msgid "Delayed Item Report" -msgstr "گزارش مورد تاخیر" +msgstr "گزارش آیتم با تاخیر" #. Name of a report #: erpnext/stock/report/delayed_order_report/delayed_order_report.json @@ -17682,7 +17682,7 @@ msgstr "" #. DocType 'Selling Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json msgid "Don't Reserve Sales Order Qty on Sales Return" -msgstr "تعداد سفارش فروش را در بازگشت فروش رزرو نکنید" +msgstr "عدم رزرو مقدار سفارش فروش در بازگشت فروش" #. Label of the mute_emails (Check) field in DocType 'Bank Statement Import' #: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json @@ -19305,17 +19305,17 @@ msgstr "زمان راه‌اندازی بیش از حد دستگاه" #. 'Company' #: erpnext/setup/doctype/company/company.json msgid "Exchange Gain / Loss" -msgstr "" +msgstr "سود / زیان تبدیل" #. Label of the exchange_gain_loss_account (Link) field in DocType 'Company' #: erpnext/setup/doctype/company/company.json msgid "Exchange Gain / Loss Account" -msgstr "حساب سود / زیان مبادله" +msgstr "حساب سود / زیان تبدیل" #. Option for the 'Entry Type' (Select) field in DocType 'Journal Entry' #: erpnext/accounts/doctype/journal_entry/journal_entry.json msgid "Exchange Gain Or Loss" -msgstr "سود یا ضرر مبادله" +msgstr "سود یا ضرر تبدیل" #. Label of the exchange_gain_loss (Currency) field in DocType 'Payment Entry #. Reference' @@ -19330,12 +19330,12 @@ msgstr "سود یا ضرر مبادله" #: erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json #: erpnext/setup/doctype/company/company.py:548 msgid "Exchange Gain/Loss" -msgstr "سود/زیان مبادله" +msgstr "سود/زیان تبدیل" #: erpnext/controllers/accounts_controller.py:1590 #: erpnext/controllers/accounts_controller.py:1674 msgid "Exchange Gain/Loss amount has been booked through {0}" -msgstr "مبلغ سود/زیان مبادله از طریق {0} رزرو شده است" +msgstr "مبلغ سود/زیان تبدیل از طریق {0} رزرو شده است" #. Label of the exchange_rate (Float) field in DocType 'Journal Entry Account' #. Label of the exchange_rate (Float) field in DocType 'Payment Entry @@ -19678,7 +19678,7 @@ msgstr "سر هزینه تغییر کرد" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:591 msgid "Expense account is mandatory for item {0}" -msgstr "حساب هزینه برای مورد {0} اجباری است" +msgstr "حساب هزینه برای آیتم {0} اجباری است" #: erpnext/assets/doctype/asset_repair/asset_repair.py:98 msgid "Expense account not present in Purchase Invoice {0}" @@ -20463,7 +20463,7 @@ msgstr "هزینه عملیاتی بر اساس کالاهای تمام شده" #: erpnext/stock/doctype/stock_entry/stock_entry.py:1340 msgid "Finished Item {0} does not match with Work Order {1}" -msgstr "مورد تمام شده {0} با دستور کار {1} مطابقت ندارد" +msgstr "آیتم تمام شده {0} با دستور کار {1} مطابقت ندارد" #. Label of the first_email (Time) field in DocType 'Project' #: erpnext/projects/doctype/project/project.json @@ -20809,11 +20809,11 @@ msgstr "برای دستور کار" #: erpnext/controllers/status_updater.py:267 msgid "For an item {0}, quantity must be negative number" -msgstr "برای یک مورد {0}، مقدار باید عدد منفی باشد" +msgstr "برای یک آیتم {0}، مقدار باید عدد منفی باشد" #: erpnext/controllers/status_updater.py:264 msgid "For an item {0}, quantity must be positive number" -msgstr "برای یک مورد {0}، مقدار باید عدد مثبت باشد" +msgstr "برای یک آیتم {0}، مقدار باید عدد مثبت باشد" #. Description of the 'Income Account' (Link) field in DocType 'Dunning' #: erpnext/accounts/doctype/dunning/dunning.json @@ -25436,7 +25436,7 @@ msgstr "تخفیف داده شده است" #. Deduction' #: erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json msgid "Is Exchange Gain / Loss?" -msgstr "" +msgstr "سود / زیان تبدیل است؟" #. Label of the is_existing_asset (Check) field in DocType 'Asset' #: erpnext/assets/doctype/asset/asset.json @@ -27067,7 +27067,7 @@ msgstr "آیتم {0} را نمی‌توان به عنوان یک زیر مونت #: erpnext/manufacturing/doctype/blanket_order/blanket_order.py:197 msgid "Item {0} cannot be ordered more than {1} against Blanket Order {2}." -msgstr "مورد {0} را نمی‌توان بیش از {1} در مقابل سفارش بلانکت {2} سفارش داد." +msgstr "آیتم {0} را نمی‌توان بیش از {1} در مقابل سفارش کلی {2} سفارش داد." #: erpnext/assets/doctype/asset/asset.py:268 #: erpnext/stock/doctype/item/item.py:630 @@ -27113,19 +27113,19 @@ msgstr "مورد {0} قبلاً در برابر سفارش فروش {1} رزرو #: erpnext/stock/doctype/item/item.py:1146 msgid "Item {0} is cancelled" -msgstr "مورد {0} لغو شده است" +msgstr "آیتم {0} لغو شده است" #: erpnext/stock/doctype/item/item.py:1130 msgid "Item {0} is disabled" -msgstr "مورد {0} غیرفعال است" +msgstr "آیتم {0} غیرفعال است" #: erpnext/selling/doctype/installation_note/installation_note.py:79 msgid "Item {0} is not a serialized Item" -msgstr "مورد {0} یک مورد سریالی نیست" +msgstr "آیتم {0} یک آیتم سریالی نیست" #: erpnext/stock/doctype/item/item.py:1138 msgid "Item {0} is not a stock Item" -msgstr "مورد {0} یک مورد موجودی نیست" +msgstr "آیتم {0} یک آیتم موجودی نیست" #: erpnext/manufacturing/doctype/production_plan/production_plan.py:875 msgid "Item {0} is not a subcontracted item" @@ -27133,7 +27133,7 @@ msgstr "آیتم {0} یک آیتم قرارداد فرعی شده نیست" #: erpnext/stock/doctype/stock_entry/stock_entry.py:1701 msgid "Item {0} is not active or end of life has been reached" -msgstr "مورد {0} فعال نیست یا به پایان عمر رسیده است" +msgstr "آیتم {0} فعال نیست یا به پایان عمر رسیده است" #: erpnext/assets/doctype/asset/asset.py:272 msgid "Item {0} must be a Fixed Asset Item" @@ -27157,7 +27157,7 @@ msgstr "مورد {0} در جدول \"مواد خام تامین شده\" در {1 #: erpnext/stock/doctype/item_price/item_price.py:56 msgid "Item {0} not found." -msgstr "مورد {0} یافت نشد." +msgstr "آیتم {0} یافت نشد." #: erpnext/buying/doctype/purchase_order/purchase_order.py:343 msgid "Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item)." @@ -27206,7 +27206,7 @@ msgstr "ثبت فروش بر حسب آیتم" #: erpnext/manufacturing/doctype/bom/bom.py:346 msgid "Item: {0} does not exist in the system" -msgstr "مورد: {0} در سیستم وجود ندارد" +msgstr "آیتم: {0} در سیستم وجود ندارد" #. Label of the items_section (Section Break) field in DocType 'POS Invoice' #. Label of the items (Table) field in DocType 'POS Invoice' @@ -28797,7 +28797,7 @@ msgstr "حفظ همان نرخ در کل چرخه فروش" #. Label of the maintain_same_rate (Check) field in DocType 'Buying Settings' #: erpnext/buying/doctype/buying_settings/buying_settings.json msgid "Maintain Same Rate Throughout the Purchase Cycle" -msgstr "همان نرخ را در طول چرخه خرید حفظ کنید" +msgstr "حفظ همان نرخ در طول چرخه خرید" #. Label of the is_stock_item (Check) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json @@ -30017,7 +30017,7 @@ msgstr "حداکثر تخفیف برای آیتم {0} {1}% است" #: erpnext/public/js/utils/barcode_scanner.js:99 msgid "Maximum quantity scanned for item {0}." -msgstr "حداکثر مقدار اسکن شده برای مورد {0}." +msgstr "حداکثر مقدار اسکن شده برای آیتم {0}." #. Description of the 'Max Sample Quantity' (Int) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json @@ -32286,7 +32286,7 @@ msgstr "یادداشت" #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js:21 msgid "Note: Automatic log deletion only applies to logs of type Update Cost" -msgstr "توجه: حذف خودکار لاگ فقط برای گزارش‌هایی از نوع به‌روزرسانی هزینه اعمال می‌شود" +msgstr "توجه: حذف خودکار لاگ فقط برای لاگ‌هایی از نوع به‌روزرسانی هزینه اعمال می‌شود" #: erpnext/accounts/party.py:691 msgid "Note: Due Date exceeds allowed customer credit days by {0} day(s)" @@ -34073,7 +34073,7 @@ msgstr "آیتم فاکتور POS" #. Name of a DocType #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json msgid "POS Invoice Merge Log" -msgstr "گزارش ادغام فاکتور POS" +msgstr "لاگ ادغام فاکتور POS" #. Name of a DocType #: erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json @@ -35961,13 +35961,13 @@ msgstr "تخصیص درصد باید برابر با 100٪ باشد" #. 'Buying Settings' #: erpnext/buying/doctype/buying_settings/buying_settings.json msgid "Percentage you are allowed to order beyond the Blanket Order quantity." -msgstr "درصدی که مجاز به سفارش آن هستید فراتر از مقدار سفارش کلی." +msgstr "درصدی که اجازه دارید بیش از مقدار سفارش کلی سفارش دهید." #. Description of the 'Blanket Order Allowance (%)' (Float) field in DocType #. 'Selling Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json msgid "Percentage you are allowed to sell beyond the Blanket Order quantity." -msgstr "درصدی که مجاز به فروش آن هستید فراتر از مقدار سفارش کلی." +msgstr "درصدی که اجازه دارید بیش از مقدار سفارش کلی بفروشید." #. Description of the 'Over Transfer Allowance (%)' (Float) field in DocType #. 'Buying Settings' @@ -36396,7 +36396,7 @@ msgstr "X روز قبل عملیات را برنامه ریزی کنید" #. Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json msgid "Plan time logs outside Workstation working hours" -msgstr "گزارش های زمان را خارج از ساعات کاری ایستگاه کاری برنامه ریزی کنید" +msgstr "برنامه ریزی لاگ‌های زمان خارج از ساعات کاری ایستگاه کاری" #. Label of the quantity (Float) field in DocType 'Material Request Plan Item' #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -37014,7 +37014,7 @@ msgstr "لطفا ابتدا شرکت را انتخاب کنید" #: erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py:52 msgid "Please select Completion Date for Completed Asset Maintenance Log" -msgstr "لطفاً تاریخ تکمیل را برای گزارش کامل تعمیر و نگهداری دارایی انتخاب کنید" +msgstr "لطفاً تاریخ تکمیل را برای لاگ تعمیر و نگهداری دارایی کامل شده انتخاب کنید" #: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js:84 #: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js:125 @@ -37337,7 +37337,7 @@ msgstr "لطفاً شناسه مالیاتی را برای مشتری \"%s\" ت #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:338 msgid "Please set Unrealized Exchange Gain/Loss Account in Company {0}" -msgstr "لطفاً حساب سود/زیان غیرواقعی مبادله را در شرکت تنظیم کنید {0}" +msgstr "لطفاً حساب سود/زیان تبدیل تحقق نیافته را در شرکت {0} تنظیم کنید" #: erpnext/regional/report/vat_audit_report/vat_audit_report.py:56 msgid "Please set VAT Accounts in {0}" @@ -37410,7 +37410,7 @@ msgstr "لطفاً حساب پیش‌فرض نقدی یا بانکی را در #: erpnext/accounts/utils.py:2218 msgid "Please set default Exchange Gain/Loss Account in Company {}" -msgstr "لطفاً حساب سود/زیان مبادله پیش‌فرض را در شرکت تنظیم کنید {}" +msgstr "لطفاً حساب سود/زیان تبدیل پیش‌فرض را در شرکت تنظیم کنید {}" #: erpnext/assets/doctype/asset_repair/asset_repair.py:363 msgid "Please set default Expense Account in Company {0}" @@ -37498,7 +37498,7 @@ msgstr "لطفاً {0} را در BOM Creator {1} تنظیم کنید" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1219 msgid "Please set {0} in Company {1} to account for Exchange Gain / Loss" -msgstr "" +msgstr "لطفاً {0} را در شرکت {1} برای محاسبه سود / زیان تبدیل تنظیم کنید" #: erpnext/controllers/accounts_controller.py:521 msgid "Please set {0} to {1}, the same account that was used in the original invoice {2}." @@ -38813,7 +38813,7 @@ msgstr "فرآیند تطبیق پرداخت" #. Name of a DocType #: erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json msgid "Process Payment Reconciliation Log" -msgstr "گزارش تطبیق پرداخت فرآیند" +msgstr "پردازش لاگ تطبیق پرداخت" #. Name of a DocType #: erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json @@ -45705,7 +45705,7 @@ msgstr "فاکتور پیش پرداخت فروش" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json msgid "Sales Invoice Item" -msgstr "مورد فاکتور فروش" +msgstr "آیتم فاکتور فروش" #. Label of the sales_invoice_no (Link) field in DocType 'Stock Entry' #: erpnext/stock/doctype/stock_entry/stock_entry.json @@ -46000,7 +46000,7 @@ msgstr "روند سفارش فروش" #: erpnext/stock/doctype/delivery_note/delivery_note.py:253 msgid "Sales Order required for Item {0}" -msgstr "سفارش فروش برای مورد {0} لازم است" +msgstr "سفارش فروش برای آیتم {0} لازم است" #: erpnext/selling/doctype/sales_order/sales_order.py:277 msgid "Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}" @@ -46443,7 +46443,7 @@ msgstr "همان کالا و ترکیب انبار قبلا وارد شده اس #: erpnext/buying/utils.py:61 msgid "Same item cannot be entered multiple times." -msgstr "یک مورد را نمی‌توان چندین بار وارد کرد." +msgstr "یک آیتم را نمی‌توان چندین بار وارد کرد." #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:79 msgid "Same supplier has been entered multiple times" @@ -46578,7 +46578,7 @@ msgstr "اسکن شماره سریال" #: erpnext/public/js/utils/barcode_scanner.js:179 msgid "Scan barcode for item {0}" -msgstr "اسکن بارکد برای مورد {0}" +msgstr "اسکن بارکد برای آیتم {0}" #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js:106 msgid "Scan mode enabled, existing quantity will not be fetched." @@ -48007,12 +48007,12 @@ msgstr "UOM آیتم خدمات" #: erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.py:64 msgid "Service Item {0} is disabled." -msgstr "مورد سرویس {0} غیرفعال است." +msgstr "آیتم خدمات {0} غیرفعال است." #: erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.py:67 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:160 msgid "Service Item {0} must be a non-stock item." -msgstr "مورد سرویس {0} باید یک کالای غیر موجودی باشد." +msgstr "آیتم خدمات {0} باید یک آیتم غیر موجودی باشد." #. Label of the service_items_section (Section Break) field in DocType #. 'Subcontracting Order' @@ -48192,7 +48192,7 @@ msgstr "تاریخ انتشار جدید را تنظیم کنید" #. DocType 'Manufacturing Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json msgid "Set Operating Cost / Scrap Items From Sub-assemblies" -msgstr "" +msgstr "تنظیم هزینه عملیاتی / آیتم‌های ضایعات از زیر مونتاژها" #. Label of the set_cost_based_on_bom_qty (Check) field in DocType 'BOM #. Operation' @@ -49301,7 +49301,7 @@ msgstr "نوع سند منبع" #. Label of the source_exchange_rate (Float) field in DocType 'Payment Entry' #: erpnext/accounts/doctype/payment_entry/payment_entry.json msgid "Source Exchange Rate" -msgstr "نرخ مبادله منبع" +msgstr "نرخ تبدیل منبع" #. Label of the source_fieldname (Data) field in DocType 'Inventory Dimension' #: erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -51680,7 +51680,7 @@ msgstr "گروه تامین کننده" #. Name of a DocType #: erpnext/accounts/doctype/supplier_group_item/supplier_group_item.json msgid "Supplier Group Item" -msgstr "مورد گروه تامین کننده" +msgstr "آیتم گروه تامین کننده" #. Label of the supplier_group_name (Data) field in DocType 'Supplier Group' #: erpnext/setup/doctype/supplier_group/supplier_group.json @@ -51726,7 +51726,7 @@ msgstr "فاکتور تامین کننده در فاکتور خرید وجود #. Name of a DocType #: erpnext/accounts/doctype/supplier_item/supplier_item.json msgid "Supplier Item" -msgstr "مورد تامین کننده" +msgstr "آیتم تامین کننده" #. Label of the supplier_items (Table) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json @@ -53401,7 +53401,7 @@ msgstr "قلمرو" #. Name of a DocType #: erpnext/accounts/doctype/territory_item/territory_item.json msgid "Territory Item" -msgstr "قلمرو مورد" +msgstr "آیتم قلمرو" #. Label of the territory_manager (Link) field in DocType 'Territory' #: erpnext/setup/doctype/territory/territory.json @@ -54861,7 +54861,7 @@ msgstr "گنجاندن آیتم‌های غیر موجودی در برنامه #. (Check) field in DocType 'Manufacturing Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json msgid "To include sub-assembly costs and scrap items in Finished Goods on a work order without using a job card, when the 'Use Multi-Level BOM' option is enabled." -msgstr "" +msgstr "برای گنجاندن هزینه‌های زیر مونتاژ و آیتم‌های ضایعات در کالاهای نهایی در یک دستور کار بدون استفاده از کارت کار، زمانی که گزینه «استفاده از BOM چند سطحی» فعال است." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2337 #: erpnext/controllers/accounts_controller.py:3002 @@ -56580,7 +56580,7 @@ msgstr "" #: erpnext/setup/utils.py:137 msgid "Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually" -msgstr "نرخ مبادله {0} تا {1} برای تاریخ کلیدی {2} یافت نشد. لطفاً یک رکورد تبادل ارز به صورت دستی ایجاد کنید" +msgstr "نرخ تبدیل {0} تا {1} برای تاریخ کلیدی {2} یافت نشد. لطفاً یک رکورد تبدیل ارز به صورت دستی ایجاد کنید" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:78 msgid "Unable to find score starting at {0}. You need to have standing scores covering 0 to 100" @@ -56738,7 +56738,7 @@ msgstr "فاقد صلاحیت" #. 'Company' #: erpnext/setup/doctype/company/company.json msgid "Unrealized Exchange Gain/Loss Account" -msgstr "حساب سود/زیان ارز تحقق نیافته" +msgstr "حساب سود/زیان تبدیل تحقق نیافته" #. Label of the unrealized_profit_loss_account (Link) field in DocType #. 'Purchase Invoice' @@ -57256,7 +57256,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/buying/doctype/buying_settings/buying_settings.json msgid "Use Transaction Date Exchange Rate" -msgstr "از نرخ مبادله تاریخ تراکنش استفاده کنید" +msgstr "استفاده از نرخ تبدیل تاریخ تراکنش" #: erpnext/projects/doctype/project/project.py:560 msgid "Use a name that is different from previous project name" @@ -57533,7 +57533,7 @@ msgstr "اعتبارسنجی قانون اعمال شده" #. 'Manufacturing Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json msgid "Validate Components and Quantities Per BOM" -msgstr "" +msgstr "اعتبارسنجی مقادیر و اجزاء در هر BOM" #. Label of the validate_negative_stock (Check) field in DocType 'Inventory #. Dimension' @@ -58040,7 +58040,7 @@ msgstr "مشاهده نمودار حساب ها" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:247 msgid "View Exchange Gain/Loss Journals" -msgstr "مشاهده دفترهای روزنامه سود/زیان تبادل" +msgstr "مشاهده دفترهای روزنامه سود/زیان تبدیل" #: erpnext/assets/doctype/asset/asset.js:164 #: erpnext/assets/doctype/asset_repair/asset_repair.js:76 @@ -59120,7 +59120,7 @@ msgstr "هنگام ایجاد حساب برای شرکت فرزند {0}، حسا #. DocType 'Buying Settings' #: erpnext/buying/doctype/buying_settings/buying_settings.json msgid "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice." -msgstr "هنگام تهیه فاکتور خرید از سفارش خرید، به جای ارث بردن آن از سفارش خرید، از نرخ مبادله در تاریخ تراکنش فاکتور استفاده کنید. فقط برای فاکتور خرید اعمال می‌شود." +msgstr "هنگام تهیه فاکتور خرید از سفارش خرید، به جای ارث بردن آن از سفارش خرید، از نرخ تبدیل در تاریخ تراکنش فاکتور استفاده کنید. فقط برای فاکتور خرید اعمال می‌شود." #: erpnext/setup/setup_wizard/operations/install_fixtures.py:269 msgid "White" @@ -59257,7 +59257,7 @@ msgstr "مواد مصرفی دستور کار" #. Name of a DocType #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json msgid "Work Order Item" -msgstr "مورد دستور کار" +msgstr "آیتم دستور کار" #. Name of a DocType #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -60498,11 +60498,11 @@ msgstr "{0} برای حساب {1} اجباری است" #: erpnext/public/js/controllers/taxes_and_totals.js:122 msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}" -msgstr "{0} اجباری است. شاید رکورد تبادل ارز برای {1} تا {2} ایجاد نشده باشد" +msgstr "{0} اجباری است. شاید رکورد تبدیل ارز برای {1} تا {2} ایجاد نشده باشد" #: erpnext/controllers/accounts_controller.py:2934 msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}." -msgstr "{0} اجباری است. شاید رکورد تبادل ارز برای {1} تا {2} ایجاد نشده باشد." +msgstr "{0} اجباری است. شاید رکورد تبدیل ارز برای {1} تا {2} ایجاد نشده باشد." #: erpnext/selling/doctype/customer/customer.py:202 msgid "{0} is not a company bank account" From 74f8d65021aef62550345549d9de4e919041e8f6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 8 May 2025 12:07:23 +0530 Subject: [PATCH 10/35] refactor(test): deterministic test suite for sales invoice --- .../accounting_dimension/test_accounting_dimension.py | 1 - .../doctype/sales_invoice/test_sales_invoice.py | 10 ++++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index ccc9f1353b6..c3d1e295c8b 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -121,7 +121,6 @@ def create_dimension(): "company": "_Test Company", "reference_document": "Location", "default_dimension": "Block 1", - "mandatory_for_bs": 1, }, ) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 367f3f82ccd..692ba62218b 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2230,13 +2230,13 @@ class TestSalesInvoice(ERPNextTestSuite): self.assertEqual(expected_account_values[1], gle.credit) def test_rounding_adjustment_3(self): - from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, - ) + from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension + # Dimension creates custom field, which does an implicit DB commit as it is a DDL command + # Ensure dimension don't have any mandatory fields create_dimension() + # rollback from tearDown() happens till here si = create_sales_invoice(do_not_save=True) si.items = [] for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: @@ -2317,8 +2317,6 @@ class TestSalesInvoice(ERPNextTestSuite): self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC") self.assertEqual(round_off_gle.location, "Block 1") - disable_dimension() - def test_sales_invoice_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule From 24184c21f0fbcdc2f4ac841c1835a0f694e4b284 Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 8 May 2025 12:27:05 +0530 Subject: [PATCH 11/35] fix: add options in report settings to become collapsible --- erpnext/accounts/report/general_ledger/general_ledger.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 6fa846910a6..5ab1bde89e8 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -218,6 +218,8 @@ frappe.query_reports["General Ledger"] = { fieldtype: "Check", }, ], + collapsible_filters: true, + seperate_check_filters: true, }; erpnext.utils.add_dimensions("General Ledger", 15); From fb921836794fa1f1c4ac4993d5f286006b47b3a8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 8 May 2025 13:17:21 +0530 Subject: [PATCH 12/35] refactor(test): set mandatory dimension at invidual test cases --- .../doctype/accounting_dimension/test_accounting_dimension.py | 4 ++++ .../doctype/pos_closing_entry/test_pos_closing_entry.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index c3d1e295c8b..769522e2642 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -58,6 +58,10 @@ class TestAccountingDimension(IntegrationTestCase): self.assertEqual(gle1.get("department"), "_Test Department - _TC") def test_mandatory(self): + location = frappe.get_doc("Accounting Dimension", "Location") + location.dimension_defaults[0].mandatory_for_bs = True + location.save() + si = create_sales_invoice(do_not_save=1) si.append( "items", diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index d2bd406f03b..fd620053728 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -159,6 +159,10 @@ class TestPOSClosingEntry(IntegrationTestCase): """ create_dimension() + location = frappe.get_doc("Accounting Dimension", "Location") + location.dimension_defaults[0].mandatory_for_bs = True + location.save() + pos_profile = make_pos_profile(do_not_insert=1, do_not_set_accounting_dimension=1) self.assertRaises(frappe.ValidationError, pos_profile.insert) From 19f1ffbdc2ee986a65089f82fa9bd05805afaae1 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Thu, 8 May 2025 13:53:51 +0530 Subject: [PATCH 13/35] fix(payment-reconciliation): use reconciliation_takes_effect_on from company --- .../doctype/payment_entry/payment_entry.json | 15 +++------------ .../doctype/payment_entry/payment_entry.py | 18 ++++++++++++------ erpnext/accounts/utils.py | 15 ++++++++++++--- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 776c1b1b10a..551c70cab38 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -21,7 +21,6 @@ "party_name", "book_advance_payments_in_separate_party_account", "reconcile_on_advance_payment_date", - "advance_reconciliation_takes_effect_on", "column_break_11", "bank_account", "party_bank_account", @@ -754,18 +753,9 @@ "options": "No\nYes", "print_hide": 1, "search_index": 1 - }, - { - "default": "Oldest Of Invoice Or Advance", - "fetch_from": "company.reconciliation_takes_effect_on", - "fieldname": "advance_reconciliation_takes_effect_on", - "fieldtype": "Select", - "hidden": 1, - "label": "Advance Reconciliation Takes Effect On", - "no_copy": 1, - "options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date" } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [ @@ -777,7 +767,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2025-03-24 16:18:19.920701", + "modified": "2025-05-08 11:18:10.238085", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", @@ -817,6 +807,7 @@ "write": 1 } ], + "row_format": "Dynamic", "show_name_in_global_search": 1, "sort_field": "creation", "sort_order": "DESC", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index d991cb128e0..f16dfda4594 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -80,9 +80,6 @@ class PaymentEntry(AccountsController): PaymentEntryReference, ) - advance_reconciliation_takes_effect_on: DF.Literal[ - "Advance Payment Date", "Oldest Of Invoice Or Advance", "Reconciliation Date" - ] amended_from: DF.Link | None apply_tax_withholding_amount: DF.Check auto_repeat: DF.Link | None @@ -1574,9 +1571,15 @@ class PaymentEntry(AccountsController): else: # For backwards compatibility # Supporting reposting on payment entries reconciled before select field introduction - if self.advance_reconciliation_takes_effect_on == "Advance Payment Date": + if ( + frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") + == "Advance Payment Date" + ): posting_date = self.posting_date - elif self.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": + elif ( + frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") + == "Oldest Of Invoice Or Advance" + ): date_field = "posting_date" if invoice.reference_doctype in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" @@ -1586,7 +1589,10 @@ class PaymentEntry(AccountsController): if getdate(posting_date) < getdate(self.posting_date): posting_date = self.posting_date - elif self.advance_reconciliation_takes_effect_on == "Reconciliation Date": + elif ( + frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") + == "Reconciliation Date" + ): posting_date = nowdate() frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index f117adb1f8f..37bd5e90ae8 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -732,9 +732,15 @@ def update_reference_in_payment_entry( # Update Reconciliation effect date in reference if payment_entry.book_advance_payments_in_separate_party_account: - if payment_entry.advance_reconciliation_takes_effect_on == "Advance Payment Date": + if ( + frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") + == "Advance Payment Date" + ): reconcile_on = payment_entry.posting_date - elif payment_entry.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": + elif ( + frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") + == "Oldest Of Invoice Or Advance" + ): date_field = "posting_date" if d.against_voucher_type in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" @@ -742,7 +748,10 @@ def update_reference_in_payment_entry( if getdate(reconcile_on) < getdate(payment_entry.posting_date): reconcile_on = payment_entry.posting_date - elif payment_entry.advance_reconciliation_takes_effect_on == "Reconciliation Date": + elif ( + frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") + == "Reconciliation Date" + ): reconcile_on = nowdate() reference_details.update({"reconcile_effect_on": reconcile_on}) From edee75c7576cd094e0e2d29845be46cd988032b5 Mon Sep 17 00:00:00 2001 From: Yaiphalemba Mangshatabam Date: Thu, 8 May 2025 14:09:06 +0530 Subject: [PATCH 14/35] fix: typo in event.js "Sales Partners" -> "Sales Partner" --- erpnext/public/js/event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/event.js b/erpnext/public/js/event.js index a6733915a5c..2950ace888d 100644 --- a/erpnext/public/js/event.js +++ b/erpnext/public/js/event.js @@ -47,7 +47,7 @@ frappe.ui.form.on("Event", { frm.add_custom_button( __("Add Sales Partners"), function () { - new frappe.desk.eventParticipants(frm, "Sales Partners"); + new frappe.desk.eventParticipants(frm, "Sales Partner"); }, __("Add Participants") ); From b406ec724b2f1703d8e5dbacf7bb82ffe3e0cc8a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 8 May 2025 14:44:01 +0530 Subject: [PATCH 15/35] test: added test --- .../purchase_invoice/test_purchase_invoice.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 4c2ac0f44aa..dfcaa49e4ea 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2776,6 +2776,42 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin): self.assertEqual(invoice.grand_total, 300) + def test_pr_pi_over_billing(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as make_purchase_invoice_from_pr, + ) + + # Configure Buying Settings to allow rate change + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + pr = make_purchase_receipt(qty=10, rate=10) + pi = make_purchase_invoice_from_pr(pr.name) + + pi.items[0].rate = 12 + + # Test 1 - This will fail because over billing is not allowed + self.assertRaises(frappe.ValidationError, pi.submit) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) + # Test 2 - This will now submit because over billing allowance is ignored when set_landed_cost_based_on_purchase_invoice_rate is checked + pi.submit() + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0) + frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 20) + pi.cancel() + pi = make_purchase_invoice_from_pr(pr.name) + pi.items[0].rate = 12 + + # Test 3 - This will now submit because over billing is allowed upto 20% + pi.submit() + + pi.reload() + pi.cancel() + pi = make_purchase_invoice_from_pr(pr.name) + pi.items[0].rate = 13 + + # Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail + self.assertRaises(frappe.ValidationError, pi.submit) def set_advance_flag(company, flag, default_account): frappe.db.set_value( From 27e842ba02a546043c8ed1c0be9888c9b1c132d9 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 8 May 2025 14:47:19 +0530 Subject: [PATCH 16/35] fix: linter error --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index dfcaa49e4ea..52bce93ec8b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2813,6 +2813,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin): # Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail self.assertRaises(frappe.ValidationError, pi.submit) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( "Company", From 74eb611563ac7ad01f3ce6fa44367163fd7ac2ea Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 29 Apr 2025 18:16:12 +0530 Subject: [PATCH 17/35] refactor: available serial no report --- erpnext/stock/doctype/serial_no/serial_no.py | 2 + .../available_serial_no.js | 38 ----- .../available_serial_no.py | 145 +++--------------- 3 files changed, 27 insertions(+), 158 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 928313576f1..0e8a531ff94 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -150,6 +150,8 @@ def get_serial_nos(serial_no): def get_serial_nos_from_sle_list(bundles): + if not bundles: + return {} table = frappe.qb.DocType("Serial and Batch Entry") query = frappe.qb.from_(table).select(table.parent, table.serial_no).where(table.parent.isin(bundles)) data = query.run(as_dict=True) diff --git a/erpnext/stock/report/available_serial_no/available_serial_no.js b/erpnext/stock/report/available_serial_no/available_serial_no.js index 17f8c666e04..c69c6503de8 100644 --- a/erpnext/stock/report/available_serial_no/available_serial_no.js +++ b/erpnext/stock/report/available_serial_no/available_serial_no.js @@ -51,49 +51,11 @@ frappe.query_reports["Available Serial No"] = { }; }, }, - { - fieldname: "item_group", - label: __("Item Group"), - fieldtype: "Link", - options: "Item Group", - }, - { - fieldname: "batch_no", - label: __("Batch No"), - fieldtype: "Link", - options: "Batch", - on_change() { - const batch_no = frappe.query_report.get_filter_value("batch_no"); - if (batch_no) { - frappe.query_report.set_filter_value("segregate_serial_batch_bundle", 1); - } else { - frappe.query_report.set_filter_value("segregate_serial_batch_bundle", 0); - } - }, - }, - { - fieldname: "brand", - label: __("Brand"), - fieldtype: "Link", - options: "Brand", - }, { fieldname: "voucher_no", label: __("Voucher #"), fieldtype: "Data", }, - { - fieldname: "project", - label: __("Project"), - fieldtype: "Link", - options: "Project", - }, - { - fieldname: "include_uom", - label: __("Include UOM"), - fieldtype: "Link", - options: "UOM", - }, { fieldname: "valuation_field_type", label: __("Valuation Field Type"), diff --git a/erpnext/stock/report/available_serial_no/available_serial_no.py b/erpnext/stock/report/available_serial_no/available_serial_no.py index bdde9c7f3b6..6f27978a92a 100644 --- a/erpnext/stock/report/available_serial_no/available_serial_no.py +++ b/erpnext/stock/report/available_serial_no/available_serial_no.py @@ -3,108 +3,64 @@ import frappe from frappe import _ -from frappe.query_builder.functions import Sum from frappe.utils import cint, flt -from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_serial_nos_from_sle_list -from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for from erpnext.stock.report.stock_ledger.stock_ledger import ( - check_inventory_dimension_filters_applied, get_item_details, - get_item_group_condition, get_opening_balance, - get_opening_balance_from_batch, get_stock_ledger_entries, ) -from erpnext.stock.utils import ( - is_reposting_item_valuation_in_progress, - update_included_uom_in_report, -) +from erpnext.stock.utils import is_reposting_item_valuation_in_progress def execute(filters=None): is_reposting_item_valuation_in_progress() - include_uom = filters.get("include_uom") columns = get_columns(filters) items = get_items(filters) sl_entries = get_stock_ledger_entries(filters, items) - item_details = get_item_details(items, sl_entries, include_uom) + item_details = get_item_details(items, sl_entries, False) opening_row, actual_qty, stock_value = get_opening_balance_data(filters, columns, sl_entries) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) - data, conversion_factors = process_stock_ledger_entries( - filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision - ) + data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision) - update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data def get_opening_balance_data(filters, columns, sl_entries): - if filters.get("batch_no"): - opening_row = get_opening_balance_from_batch(filters, columns, sl_entries) - else: - opening_row = get_opening_balance(filters, columns, sl_entries) - + opening_row = get_opening_balance(filters, columns, sl_entries) actual_qty = opening_row.get("qty_after_transaction") if opening_row else 0 stock_value = opening_row.get("stock_value") if opening_row else 0 return opening_row, actual_qty, stock_value -def process_stock_ledger_entries( - filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision -): +def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision): data = [] - conversion_factors = [] if opening_row: data.append(opening_row) - conversion_factors.append(0) - - batch_balance_dict = frappe._dict({}) - - if actual_qty and filters.get("batch_no"): - batch_balance_dict[filters.batch_no] = [actual_qty, stock_value] available_serial_nos = get_serial_nos_from_sle_list( [sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle] ) + if not available_serial_nos: + return [], [] + for sle in sl_entries: - update_stock_ledger_entry( - sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision - ) + update_stock_ledger_entry(sle, item_details, precision) update_available_serial_nos(available_serial_nos, sle) data.append(sle) - if filters.get("include_uom"): - conversion_factors.append(item_details[sle.item_code].conversion_factor) - - return data, conversion_factors + return data -def update_stock_ledger_entry( - sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision -): +def update_stock_ledger_entry(sle, item_details, precision): item_detail = item_details[sle.item_code] sle.update(item_detail) - if filters.get("batch_no") or check_inventory_dimension_filters_applied(filters): - actual_qty += flt(sle.actual_qty, precision) - stock_value += sle.stock_value_difference - - if sle.batch_no: - batch_balance_dict.setdefault(sle.batch_no, [0, 0]) - batch_balance_dict[sle.batch_no][0] += sle.actual_qty - - if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty: - actual_qty = sle.qty_after_transaction - stock_value = sle.stock_value - - sle.update({"qty_after_transaction": actual_qty, "stock_value": stock_value}) - sle.update({"in_qty": max(sle.actual_qty, 0), "out_qty": min(sle.actual_qty, 0)}) if sle.actual_qty: @@ -120,13 +76,10 @@ def update_available_serial_nos(available_serial_nos, sle): else available_serial_nos.get(sle.serial_and_batch_bundle) ) key = (sle.item_code, sle.warehouse) + sle.serial_no = "\n".join(serial_nos) if serial_nos else "" if key not in available_serial_nos: - stock_balance = get_stock_balance_for( - sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time - ) - serials = get_serial_nos(stock_balance["serial_nos"]) if stock_balance["serial_nos"] else [] - available_serial_nos.setdefault(key, serials) - sle.balance_serial_no = "\n".join(serials) + available_serial_nos.setdefault(key, serial_nos) + sle.balance_serial_no = "\n".join(serial_nos) return existing_serial_no = available_serial_nos[key] @@ -151,25 +104,14 @@ def get_columns(filters): }, {"label": _("Item Name"), "fieldname": "item_name", "width": 100}, { - "label": _("Stock UOM"), + "label": _("UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", - "width": 90, + "width": 60, }, ] - for dimension in get_inventory_dimensions(): - columns.append( - { - "label": _(dimension.doctype), - "fieldname": dimension.fieldname, - "fieldtype": "Link", - "options": dimension.doctype, - "width": 110, - } - ) - columns.extend( [ { @@ -201,20 +143,11 @@ def get_columns(filters): "width": 150, }, { - "label": _("Item Group"), - "fieldname": "item_group", - "fieldtype": "Link", - "options": "Item Group", - "width": 100, + "label": _("Serial No (In/Out)"), + "fieldname": "serial_no", + "width": 150, }, - { - "label": _("Brand"), - "fieldname": "brand", - "fieldtype": "Link", - "options": "Brand", - "width": 100, - }, - {"label": _("Description"), "fieldname": "description", "width": 200}, + {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 150}, { "label": _("Incoming Rate"), "fieldname": "incoming_rate", @@ -257,28 +190,6 @@ def get_columns(filters): "width": 110, "options": "Company:company:default_currency", }, - {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, - { - "label": _("Voucher #"), - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "options": "voucher_type", - "width": 100, - }, - { - "label": _("Batch"), - "fieldname": "batch_no", - "fieldtype": "Link", - "options": "Batch", - "width": 100, - }, - { - "label": _("Serial No"), - "fieldname": "serial_no", - "fieldtype": "Link", - "options": "Serial No", - "width": 100, - }, { "label": _("Serial and Batch Bundle"), "fieldname": "serial_and_batch_bundle", @@ -286,12 +197,12 @@ def get_columns(filters): "options": "Serial and Batch Bundle", "width": 100, }, - {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, { - "label": _("Project"), - "fieldname": "project", - "fieldtype": "Link", - "options": "Project", + "label": _("Voucher #"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", "width": 100, }, { @@ -314,12 +225,6 @@ def get_items(filters): if item_code := filters.get("item_code"): conditions.append(item.name == item_code) - else: - if brand := filters.get("brand"): - conditions.append(item.brand == brand) - if item_group := filters.get("item_group"): - if condition := get_item_group_condition(item_group, item): - conditions.append(condition) if conditions: for condition in conditions: From 653e0a2e3a17514335f0c6249221284c93c2f555 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 8 May 2025 15:24:38 +0530 Subject: [PATCH 18/35] chore: further optimizations --- erpnext/stock/doctype/serial_no/serial_no.py | 2 -- .../available_serial_no.py | 19 ++++++------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 0e8a531ff94..928313576f1 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -150,8 +150,6 @@ def get_serial_nos(serial_no): def get_serial_nos_from_sle_list(bundles): - if not bundles: - return {} table = frappe.qb.DocType("Serial and Batch Entry") query = frappe.qb.from_(table).select(table.parent, table.serial_no).where(table.parent.isin(bundles)) data = query.run(as_dict=True) diff --git a/erpnext/stock/report/available_serial_no/available_serial_no.py b/erpnext/stock/report/available_serial_no/available_serial_no.py index 6f27978a92a..6911b979ae4 100644 --- a/erpnext/stock/report/available_serial_no/available_serial_no.py +++ b/erpnext/stock/report/available_serial_no/available_serial_no.py @@ -21,7 +21,7 @@ def execute(filters=None): sl_entries = get_stock_ledger_entries(filters, items) item_details = get_item_details(items, sl_entries, False) - opening_row, actual_qty, stock_value = get_opening_balance_data(filters, columns, sl_entries) + opening_row = get_opening_balance_data(filters, columns, sl_entries) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision) @@ -31,9 +31,7 @@ def execute(filters=None): def get_opening_balance_data(filters, columns, sl_entries): opening_row = get_opening_balance(filters, columns, sl_entries) - actual_qty = opening_row.get("qty_after_transaction") if opening_row else 0 - stock_value = opening_row.get("stock_value") if opening_row else 0 - return opening_row, actual_qty, stock_value + return opening_row def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision): @@ -42,9 +40,9 @@ def process_stock_ledger_entries(sl_entries, item_details, opening_row, precisio if opening_row: data.append(opening_row) - available_serial_nos = get_serial_nos_from_sle_list( - [sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle] - ) + available_serial_nos = {} + if sabb_list := [sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle]: + available_serial_nos = get_serial_nos_from_sle_list(sabb_list) if not available_serial_nos: return [], [] @@ -221,13 +219,8 @@ def get_columns(filters): def get_items(filters): item = frappe.qb.DocType("Item") query = frappe.qb.from_(item).select(item.name).where(item.has_serial_no == 1) - conditions = [] if item_code := filters.get("item_code"): - conditions.append(item.name == item_code) - - if conditions: - for condition in conditions: - query = query.where(condition) + query = query.where(item.name == item_code) return query.run(pluck=True) From 7bc62cedc6313f511e353283c1d8d1b7b8cb111d Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Thu, 8 May 2025 16:32:30 +0530 Subject: [PATCH 19/35] chore: simplify repeated condition checks --- .../doctype/payment_entry/payment_entry.py | 18 ++++++------------ erpnext/accounts/utils.py | 18 ++++++------------ 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index f16dfda4594..dbce523438f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1571,15 +1571,12 @@ class PaymentEntry(AccountsController): else: # For backwards compatibility # Supporting reposting on payment entries reconciled before select field introduction - if ( - frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") - == "Advance Payment Date" - ): + reconciliation_takes_effect_on = frappe.get_cached_value( + "Company", self.company, "reconciliation_takes_effect_on" + ) + if reconciliation_takes_effect_on == "Advance Payment Date": posting_date = self.posting_date - elif ( - frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") - == "Oldest Of Invoice Or Advance" - ): + elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": date_field = "posting_date" if invoice.reference_doctype in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" @@ -1589,10 +1586,7 @@ class PaymentEntry(AccountsController): if getdate(posting_date) < getdate(self.posting_date): posting_date = self.posting_date - elif ( - frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") - == "Reconciliation Date" - ): + elif reconciliation_takes_effect_on == "Reconciliation Date": posting_date = nowdate() frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 37bd5e90ae8..290ce504c3a 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -731,16 +731,13 @@ def update_reference_in_payment_entry( update_advance_paid = [] # Update Reconciliation effect date in reference + reconciliation_takes_effect_on = frappe.get_cached_value( + "Company", payment_entry.company, "reconciliation_takes_effect_on" + ) if payment_entry.book_advance_payments_in_separate_party_account: - if ( - frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") - == "Advance Payment Date" - ): + if reconciliation_takes_effect_on == "Advance Payment Date": reconcile_on = payment_entry.posting_date - elif ( - frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") - == "Oldest Of Invoice Or Advance" - ): + elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": date_field = "posting_date" if d.against_voucher_type in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" @@ -748,10 +745,7 @@ def update_reference_in_payment_entry( if getdate(reconcile_on) < getdate(payment_entry.posting_date): reconcile_on = payment_entry.posting_date - elif ( - frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") - == "Reconciliation Date" - ): + elif reconciliation_takes_effect_on == "Reconciliation Date": reconcile_on = nowdate() reference_details.update({"reconcile_effect_on": reconcile_on}) From 7103cdd84a264c0a6acdab7fc66e0a427b312b68 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 8 May 2025 17:44:51 +0530 Subject: [PATCH 20/35] fix: better validation message with solution for BOM recursion --- erpnext/manufacturing/doctype/bom/bom.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index b151c92bfe6..8a35d08c75f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -7,7 +7,7 @@ from collections import deque from operator import itemgetter import frappe -from frappe import _ +from frappe import _, bold from frappe.core.doctype.version.version import get_diff from frappe.model.mapper import get_mapped_doc from frappe.utils import cint, cstr, flt, parse_json, today @@ -662,9 +662,16 @@ class BOM(WebsiteGenerator): def check_recursion(self, bom_list=None): """Check whether recursion occurs in any bom""" - def _throw_error(bom_name): + def _throw_error(bom_name, production_item=None): + msg = _("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name) + if production_item and bom_name != self.name: + msg += "

" + msg += _( + "Note: If you want to use the finished good {0} as a raw material, then enable the 'Do Not Explode' checkbox in the Items table against the same raw material." + ).format(bold(production_item)) + frappe.throw( - _("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name), + msg, exc=BOMRecursionError, ) @@ -681,7 +688,7 @@ class BOM(WebsiteGenerator): if self.item == item.item_code and item.bom_no: # Same item but with different BOM should not be allowed. # Same item can appear recursively once as long as it doesn't have BOM. - _throw_error(item.bom_no) + _throw_error(item.bom_no, self.item) if self.name in {d.bom_no for d in self.items}: _throw_error(self.name) From b04a07fda0627f94b389588993610063ae9b0390 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 9 May 2025 13:38:42 +0530 Subject: [PATCH 21/35] fix: timesheet portal showing total billing hours --- erpnext/projects/doctype/timesheet/timesheet.py | 2 +- erpnext/templates/includes/timesheet/timesheet_row.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 4b4454cf2a8..a43763177db 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -522,7 +522,7 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20 table.name, child_table.activity_type, table.status, - table.total_billable_hours, + child_table.billing_hours, (table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"), child_table.project, ) diff --git a/erpnext/templates/includes/timesheet/timesheet_row.html b/erpnext/templates/includes/timesheet/timesheet_row.html index 0f9cc77e89d..8905262a88e 100644 --- a/erpnext/templates/includes/timesheet/timesheet_row.html +++ b/erpnext/templates/includes/timesheet/timesheet_row.html @@ -5,7 +5,7 @@ {{ doc.name }} -
{{ doc.total_billable_hours }}
+
{{ doc.billing_hours }}
{{ doc.project or '' }}
{{ doc.sales_invoice or '' }}
{{ _(doc.activity_type) }}
From b6b4ac5b4a9e09153eea4abed030c41ff4e28a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ei=C3=9Fler?= <77415730+PatrickDEissler@users.noreply.github.com> Date: Fri, 9 May 2025 13:46:05 +0200 Subject: [PATCH 22/35] feat: add column "Item Name" to "BOM Stock Report" (#47116) --- .../manufacturing/report/bom_stock_report/bom_stock_report.py | 2 ++ .../report/bom_stock_report/test_bom_stock_report.py | 1 + 2 files changed, 3 insertions(+) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 48ffbac5820..d233643c244 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -23,6 +23,7 @@ def get_columns(): """return columns""" columns = [ _("Item") + ":Link/Item:150", + _("Item Name") + "::240", _("Description") + "::300", _("BOM Qty") + ":Float:160", _("BOM UoM") + "::160", @@ -73,6 +74,7 @@ def get_bom_stock(filters): .on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS)) .select( BOM_ITEM.item_code, + BOM_ITEM.item_name, BOM_ITEM.description, BOM_ITEM.stock_qty, BOM_ITEM.stock_uom, diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py index 4571129cdce..860ba3f57f7 100644 --- a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py @@ -94,6 +94,7 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False): expected_data.append( [ item.item_code, + item.item_name, item.description, item.stock_qty, item.stock_uom, From 0bc3cfe29d5777c1b600db41ed109e600ffc6b3d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 12 May 2025 12:19:23 +0530 Subject: [PATCH 23/35] feat: stock reservation for production plan (#47373) * feat: reservation for production plan * test: test case for serial/batch * feat: reserve & un-reserve options in the production plan --- .../material_request_plan_item.json | 49 +- .../material_request_plan_item.py | 1 + .../production_plan/production_plan.js | 86 +++- .../production_plan/production_plan.json | 38 +- .../production_plan/production_plan.py | 181 +++++--- .../production_plan_dashboard.py | 4 + .../production_plan/test_production_plan.py | 429 +++++++++++++++++- .../production_plan_item_reference.json | 8 +- .../production_plan_item_reference.py | 2 +- .../production_plan_sub_assembly_item.json | 66 ++- .../production_plan_sub_assembly_item.py | 2 + .../doctype/work_order/work_order.json | 18 +- .../doctype/work_order/work_order.py | 138 ++++-- erpnext/public/js/stock_reservation.js | 23 +- .../purchase_receipt/purchase_receipt.py | 67 ++- .../stock/doctype/stock_entry/stock_entry.py | 12 +- .../stock_reservation_entry.json | 10 +- .../stock_reservation_entry.py | 194 +++++++- erpnext/stock/utils.py | 2 + 19 files changed, 1153 insertions(+), 177 deletions(-) diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index db5e19a601f..f10db8d8a7c 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -10,13 +10,17 @@ "warehouse", "item_name", "material_request_type", - "quantity", - "required_bom_qty", "column_break_4", - "schedule_date", "uom", "conversion_factor", + "section_break_azee", + "required_bom_qty", + "projected_qty", + "column_break_wack", + "quantity", + "stock_reserved_qty", "item_details", + "schedule_date", "description", "min_order_qty", "section_break_8", @@ -27,7 +31,6 @@ "reserved_qty_for_production", "column_break_yhelv", "ordered_qty", - "projected_qty", "safety_stock" ], "fields": [ @@ -47,7 +50,7 @@ "label": "Item Name" }, { - "columns": 2, + "columns": 3, "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, @@ -58,7 +61,7 @@ "search_index": 1 }, { - "columns": 1, + "columns": 2, "fieldname": "material_request_type", "fieldtype": "Select", "in_list_view": 1, @@ -70,17 +73,19 @@ "fieldtype": "Column Break" }, { - "columns": 2, + "columns": 3, "fieldname": "quantity", "fieldtype": "Float", "in_list_view": 1, - "label": "Plan to Request Qty", + "label": "Required Qty", "no_copy": 1, "reqd": 1 }, { + "columns": 2, "fieldname": "projected_qty", "fieldtype": "Float", + "in_list_view": 1, "label": "Projected Qty", "read_only": 1 }, @@ -89,7 +94,6 @@ "default": "0", "fieldname": "actual_qty", "fieldtype": "Float", - "in_list_view": 1, "label": "Qty In Stock", "no_copy": 1, "read_only": 1 @@ -172,11 +176,11 @@ "read_only": 1 }, { - "columns": 2, + "columns": 3, "fieldname": "required_bom_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Qty As Per BOM", + "label": "Reqd Qty (BOM)", "no_copy": 1, "read_only": 1 }, @@ -187,7 +191,7 @@ "read_only": 1 }, { - "columns": 1, + "columns": 2, "fieldname": "schedule_date", "fieldtype": "Date", "in_list_view": 1, @@ -201,19 +205,36 @@ { "fieldname": "column_break_yhelv", "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_azee", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_wack", + "fieldtype": "Column Break" + }, + { + "fieldname": "stock_reserved_qty", + "fieldtype": "Float", + "label": "Stock Reserved Qty", + "no_copy": 1, + "read_only": 1 } ], + "grid_page_length": 50, "istable": 1, "links": [], - "modified": "2024-12-30 18:06:22.288340", + "modified": "2025-05-01 14:50:55.805442", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", "owner": "Administrator", "permissions": [], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py index 3d3130f77d8..2b6e0994f46 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py @@ -42,6 +42,7 @@ class MaterialRequestPlanItem(Document): safety_stock: DF.Float sales_order: DF.Link | None schedule_date: DF.Date | None + stock_reserved_qty: DF.Float uom: DF.Link | None warehouse: DF.Link # end: auto-generated types diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index ab09a0f02f9..5ab291bb56a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -9,6 +9,13 @@ frappe.ui.form.on("Production Plan", { }); }, + hide_reserve_stock_button(frm) { + frm.toggle_display("reserve_stock", false); + if (frm.doc.__onload?.enable_stock_reservation) { + frm.toggle_display("reserve_stock", true); + } + }, + setup(frm) { frm.trigger("setup_queries"); @@ -16,6 +23,9 @@ frappe.ui.form.on("Production Plan", { "Work Order": "Work Order / Subcontract PO", "Material Request": "Material Request", }; + + frm.set_df_property("sub_assembly_items", "cannot_delete_rows", true); + frm.set_df_property("mr_items", "cannot_delete_rows", true); }, setup_queries(frm) { @@ -140,12 +150,16 @@ frappe.ui.form.on("Production Plan", { ); } } + + if (frm.doc.status !== "Closed") { + frm.page.set_inner_btn_group_as_primary(__("Create")); + } } - if (frm.doc.status !== "Closed") { - frm.page.set_inner_btn_group_as_primary(__("Create")); - } frm.trigger("material_requirement"); + frm.trigger("hide_reserve_stock_button"); + frm.trigger("setup_stock_reservation_for_sub_assembly"); + frm.trigger("setup_stock_reservation_for_raw_materials"); const projected_qty_formula = `
@@ -193,6 +207,72 @@ frappe.ui.form.on("Production Plan", { set_field_options("projected_qty_formula", projected_qty_formula); }, + has_unreserved_stock(frm, table) { + let has_unreserved_stock = frm.doc[table].some( + (item) => flt(item.qty) > flt(item.stock_reserved_qty) + ); + + return has_unreserved_stock; + }, + + has_reserved_stock(frm, table) { + let has_reserved_stock = frm.doc[table].some((item) => flt(item.stock_reserved_qty) > 0); + + return has_reserved_stock; + }, + + setup_stock_reservation_for_sub_assembly(frm) { + if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) { + if (frm.events.has_unreserved_stock(frm, "sub_assembly_items")) { + frm.add_custom_button( + __("Reserve for Sub-assembly"), + () => erpnext.stock_reservation.make_entries(frm, "sub_assembly_items"), + __("Stock Reservation") + ); + } + + if (frm.events.has_reserved_stock(frm, "sub_assembly_items")) { + frm.add_custom_button( + __("Unreserve for Sub-assembly"), + () => erpnext.stock_reservation.unreserve_stock(frm), + __("Stock Reservation") + ); + + frm.add_custom_button( + __("Reserved Stock for Sub-assembly"), + () => erpnext.stock_reservation.show_reserved_stock(frm, "sub_assembly_items"), + __("Stock Reservation") + ); + } + } + }, + + setup_stock_reservation_for_raw_materials(frm) { + if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) { + if (frm.events.has_unreserved_stock(frm, "mr_items")) { + frm.add_custom_button( + __("Reserve for Raw Materials"), + () => erpnext.stock_reservation.make_entries(frm, "mr_items"), + __("Stock Reservation") + ); + } + + if (frm.events.has_reserved_stock(frm, "mr_items")) { + frm.add_custom_button( + __("Unreserve for Raw Materials"), + () => erpnext.stock_reservation.unreserve_stock(frm), + __("Stock Reservation") + ); + + frm.add_custom_button( + __("Reserved Stock for Raw Materials"), + () => erpnext.stock_reservation.show_reserved_stock(frm, "mr_items"), + __("Stock Reservation") + ); + } + } + }, + close_open_production_plan(frm, close = false) { frappe.call({ method: "set_status", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 01ab5fc7301..d733cb27845 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -11,6 +11,7 @@ "get_items_from", "column_break1", "posting_date", + "reserve_stock", "filters", "item_code", "customer", @@ -23,6 +24,7 @@ "from_delivery_date", "to_delivery_date", "sales_orders_detail", + "combine_items", "get_sales_orders", "sales_orders", "material_request_detail", @@ -30,16 +32,14 @@ "material_requests", "select_items_to_manufacture_section", "get_items", - "combine_items", "po_items", "section_break_25", "prod_plan_references", "section_break_24", - "combine_sub_items", "sub_assembly_warehouse", - "section_break_ucc4", - "skip_available_sub_assembly_item", "column_break_igxl", + "skip_available_sub_assembly_item", + "combine_sub_items", "get_sub_assembly_items", "section_break_g4ip", "sub_assembly_items", @@ -215,7 +215,8 @@ { "fieldname": "material_request_planning", "fieldtype": "Section Break", - "label": "Material Request Planning" + "hide_border": 1, + "label": "For Raw Materials" }, { "default": "1", @@ -231,10 +232,10 @@ }, { "default": "1", - "description": "If enabled, the system will consider items with a shortfall in quantity. \n
\nQty = Reqd Qty (BOM) - Projected Qty", + "description": "If enabled, formula for Required Qty:
\nRequired Qty (BOM) - Projected Qty.
This helps avoid over-ordering.", "fieldname": "ignore_existing_ordered_qty", "fieldtype": "Check", - "label": "Skip Available Raw Materials" + "label": "Consider Projected Qty in Calculation (RM)" }, { "fieldname": "column_break_25", @@ -314,7 +315,7 @@ { "fieldname": "for_warehouse", "fieldtype": "Link", - "label": "Raw Materials Warehouse", + "label": "For Warehouse", "options": "Warehouse" }, { @@ -375,11 +376,13 @@ "label": "Get Sub Assembly Items" }, { + "depends_on": "eval:doc.get_items_from == 'Sales Order'", "fieldname": "from_delivery_date", "fieldtype": "Date", "label": "From Delivery Date" }, { + "depends_on": "eval:doc.get_items_from == 'Sales Order'", "fieldname": "to_delivery_date", "fieldtype": "Date", "label": "To Delivery Date" @@ -404,15 +407,10 @@ }, { "default": "1", - "description": "If enabled, the system will consider items with a shortfall in quantity. \n
\nQty = Reqd Qty (BOM) - Projected Qty", + "description": "If enabled, formula for Qty to Order:
\nRequired Qty (BOM) - Projected Qty.
This helps avoid over-ordering.", "fieldname": "skip_available_sub_assembly_item", "fieldtype": "Check", - "label": "Skip Available Sub Assembly Items" - }, - { - "fieldname": "section_break_ucc4", - "fieldtype": "Column Break", - "hide_border": 1 + "label": "Consider Projected Qty in Calculation" }, { "fieldname": "section_break_g4ip", @@ -423,7 +421,7 @@ "fieldtype": "Column Break" }, { - "description": "When a parent warehouse is chosen, the system conducts stock checks against the associated child warehouses", + "description": "When a parent warehouse is chosen, the system conducts Project Qty checks against the associated child warehouses", "fieldname": "sub_assembly_warehouse", "fieldtype": "Link", "label": "Sub Assembly Warehouse", @@ -435,6 +433,12 @@ "fieldname": "consider_minimum_order_qty", "fieldtype": "Check", "label": "Consider Minimum Order Qty" + }, + { + "default": "0", + "fieldname": "reserve_stock", + "fieldtype": "Check", + "label": "Reserve Stock" } ], "grid_page_length": 50, @@ -442,7 +446,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-04-08 17:24:09.394056", + "modified": "2025-05-09 18:55:45.500257", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index bddf43a9dcc..899e28e0a1e 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -9,6 +9,8 @@ from collections import defaultdict import frappe from frappe import _, msgprint from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import Case from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( add_days, @@ -20,6 +22,7 @@ from frappe.utils import ( getdate, now_datetime, nowdate, + parse_json, ) from frappe.utils.csvutils import build_csv_response from pypika.terms import ExistsCriterion @@ -28,6 +31,7 @@ from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_childr from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults +from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.utils import get_or_make_bin from erpnext.utilities.transaction_base import validate_uom_is_integer @@ -84,6 +88,7 @@ class ProductionPlan(Document): posting_date: DF.Date prod_plan_references: DF.Table[ProductionPlanItemReference] project: DF.Link | None + reserve_stock: DF.Check sales_order_status: DF.Literal["", "To Deliver and Bill", "To Bill", "To Deliver"] sales_orders: DF.Table[ProductionPlanSalesOrder] skip_available_sub_assembly_item: DF.Check @@ -108,6 +113,12 @@ class ProductionPlan(Document): warehouses: DF.TableMultiSelect[ProductionPlanMaterialRequestWarehouse] # end: auto-generated types + def onload(self): + self.set_onload( + "enable_stock_reservation", + frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"), + ) + def validate(self): self.set_pending_qty_in_row_without_reference() self.calculate_total_planned_qty() @@ -116,6 +127,11 @@ class ProductionPlan(Document): validate_uom_is_integer(self, "stock_uom", "planned_qty") self.validate_sales_orders() self.validate_material_request_type() + self.enable_auto_reserve_stock() + + def enable_auto_reserve_stock(self): + if self.is_new() and frappe.db.get_single_value("Stock Settings", "auto_reserve_stock"): + self.reserve_stock = 1 def validate_material_request_type(self): for row in self.get("mr_items"): @@ -552,12 +568,20 @@ class ProductionPlan(Document): def on_submit(self): self.update_bin_qty() self.update_sales_order() + self.update_stock_reservation() def on_cancel(self): self.db_set("status", "Cancelled") self.delete_draft_work_order() self.update_bin_qty() self.update_sales_order() + self.update_stock_reservation() + + def update_stock_reservation(self): + if not self.reserve_stock: + return + + make_stock_reservation_entries(self) def update_sales_order(self): sales_orders = [row.sales_order for row in self.po_items if row.sales_order] @@ -851,6 +875,7 @@ class ProductionPlan(Document): wo = frappe.new_doc("Work Order") wo.update(item) + wo.reserve_stock = self.reserve_stock wo.planned_start_date = item.get("planned_start_date") or item.get("schedule_date") if item.get("warehouse"): @@ -1369,29 +1394,28 @@ def get_material_request_items( get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0 ) - if required_qty > 0: - return { - "item_code": row.item_code, - "item_name": row.item_name, - "quantity": required_qty / conversion_factor, - "conversion_factor": conversion_factor, - "required_bom_qty": total_qty, - "stock_uom": row.get("stock_uom"), - "warehouse": warehouse - or row.get("source_warehouse") - or row.get("default_warehouse") - or item_group_defaults.get("default_warehouse"), - "safety_stock": row.safety_stock, - "actual_qty": bin_dict.get("actual_qty", 0), - "projected_qty": bin_dict.get("projected_qty", 0), - "ordered_qty": bin_dict.get("ordered_qty", 0), - "reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0), - "min_order_qty": row["min_order_qty"], - "material_request_type": row.get("default_material_request_type"), - "sales_order": sales_order, - "description": row.get("description"), - "uom": row.get("purchase_uom") or row.get("stock_uom"), - } + return { + "item_code": row.item_code, + "item_name": row.item_name, + "quantity": required_qty / conversion_factor, + "conversion_factor": conversion_factor, + "required_bom_qty": total_qty, + "stock_uom": row.get("stock_uom"), + "warehouse": warehouse + or row.get("source_warehouse") + or row.get("default_warehouse") + or item_group_defaults.get("default_warehouse"), + "safety_stock": row.safety_stock, + "actual_qty": bin_dict.get("actual_qty", 0), + "projected_qty": bin_dict.get("projected_qty", 0), + "ordered_qty": bin_dict.get("ordered_qty", 0), + "reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0), + "min_order_qty": row["min_order_qty"], + "material_request_type": row.get("default_material_request_type"), + "sales_order": sales_order, + "description": row.get("description"), + "uom": row.get("purchase_uom") or row.get("stock_uom"), + } def get_sales_orders(self): @@ -1792,6 +1816,7 @@ def get_sub_assembly_items( if d.expandable: parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) + required_qty = stock_qty if skip_available_sub_assembly_item and d.item_code not in sub_assembly_items: bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse)) @@ -1808,40 +1833,43 @@ def get_sub_assembly_items( elif warehouse: bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse)) - if stock_qty > 0: - bom_data.append( - frappe._dict( - { - "actual_qty": bin_details[d.item_code][0].get("actual_qty", 0) - if bin_details.get(d.item_code) - else 0, - "parent_item_code": parent_item_code, - "description": d.description, - "production_item": d.item_code, - "item_name": d.item_name, - "stock_uom": d.stock_uom, - "uom": d.stock_uom, - "bom_no": d.value, - "is_sub_contracted_item": d.is_sub_contracted_item, - "bom_level": indent, - "indent": indent, - "stock_qty": stock_qty, - } - ) + bom_data.append( + frappe._dict( + { + "actual_qty": bin_details[d.item_code][0].get("actual_qty", 0) + if bin_details.get(d.item_code) + else 0, + "parent_item_code": parent_item_code, + "description": d.description, + "production_item": d.item_code, + "item_name": d.item_name, + "stock_uom": d.stock_uom, + "uom": d.stock_uom, + "bom_no": d.value, + "is_sub_contracted_item": d.is_sub_contracted_item, + "bom_level": indent, + "indent": indent, + "stock_qty": stock_qty, + "required_qty": required_qty, + "projected_qty": bin_details[d.item_code][0].get("projected_qty", 0) + if bin_details.get(d.item_code) + else 0, + } ) + ) - if d.value: - get_sub_assembly_items( - sub_assembly_items, - bin_details, - d.value, - bom_data, - stock_qty, - company, - warehouse, - indent=indent + 1, - skip_available_sub_assembly_item=skip_available_sub_assembly_item, - ) + if d.value: + get_sub_assembly_items( + sub_assembly_items, + bin_details, + d.value, + bom_data, + stock_qty, + company, + warehouse, + indent=indent + 1, + skip_available_sub_assembly_item=skip_available_sub_assembly_item, + ) def set_default_warehouses(row, default_warehouses): @@ -2034,7 +2062,12 @@ def get_reserved_qty_for_sub_assembly(item_code, warehouse): frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.qty - IfNull(child.wo_produced_qty, 0))) + .select( + Sum( + Case().when(child.qty > 0, child.qty).else_(child.required_qty) + - IfNull(child.wo_produced_qty, 0) + ) + ) .where( (table.docstatus == 1) & (child.production_item == item_code) @@ -2050,3 +2083,39 @@ def get_reserved_qty_for_sub_assembly(item_code, warehouse): qty = flt(query[0][0]) return qty if qty > 0 else 0.0 + + +@frappe.whitelist() +def make_stock_reservation_entries(doc, items=None, table_name=None, notify=False): + if isinstance(doc, str): + doc = parse_json(doc) + doc = frappe.get_doc("Work Order", doc.get("name")) + + if items and isinstance(items, str): + items = parse_json(items) + + mapper = { + "sub_assembly_items": { + "table_name": "sub_assembly_items", + "qty_field": "required_qty", + "warehouse_field": "fg_warehouse", + }, + "mr_items": { + "table_name": "mr_items", + "qty_field": "required_bom_qty", + "warehouse_field": "warehouse", + }, + } + + for child_table_name in mapper: + if table_name and table_name != child_table_name: + continue + + sre = StockReservation(doc, items=items, kwargs=mapper[child_table_name], notify=notify) + if doc.docstatus == 1: + sre.make_stock_reservation_entries() + frappe.msgprint(_("Stock Reservation Entries Created"), alert=True) + elif doc.docstatus == 2: + sre.cancel_stock_reservation_entries() + + doc.reload() diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py index 6fc28a30971..71ca6843e2b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py @@ -4,8 +4,12 @@ from frappe import _ def get_data(): return { "fieldname": "production_plan", + "non_standard_fieldnames": { + "Stock Reservation Entry": "voucher_no", + }, "transactions": [ {"label": _("Transactions"), "items": ["Work Order", "Material Request"]}, {"label": _("Subcontract"), "items": ["Purchase Order"]}, + {"label": _("Reservation"), "items": ["Stock Reservation Entry"]}, ], } diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index ab0abb4d48c..15342f9d6be 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -16,10 +16,15 @@ from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import create_item, make_item +from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + get_batch_from_bundle, + get_serial_nos_from_bundle, +) from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) +from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation class UnitTestProductionPlan(UnitTestCase): @@ -146,7 +151,7 @@ class TestProductionPlan(IntegrationTestCase): item_code="Raw Material Item 2", target="_Test Warehouse - _TC", qty=1, rate=120 ) - pln = create_production_plan(item_code="Test Production Item 1", ignore_existing_ordered_qty=1) + pln = create_production_plan(item_code="Test Production Item 1", ignore_existing_ordered_qty=0) self.assertTrue(len(pln.mr_items)) self.assertTrue(flt(pln.mr_items[0].quantity), 1.0) @@ -182,11 +187,17 @@ class TestProductionPlan(IntegrationTestCase): pln = create_production_plan( item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=1 ) - self.assertFalse(len(pln.mr_items)) + items = [] + for row in pln.mr_items: + if row.quantity > 0: + items.append(row.item_code) + + self.assertFalse(len(items)) + + pln.cancel() sr1.cancel() sr2.cancel() - pln.cancel() def test_production_plan_sales_orders(self): "Test if previously fulfilled SO (with WO) is pulled into Prod Plan." @@ -1910,6 +1921,417 @@ class TestProductionPlan(IntegrationTestCase): self.assertEqual(mr_items[0].get("quantity"), 80) self.assertEqual(mr_items[1].get("quantity"), 70) + def test_stock_reservation_against_production_plan(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + + bom_tree = { + "Finished Good For SR": { + "Sub Assembly For SR 1": {"Raw Material For SR 1": {}}, + "Sub Assembly For SR 2": {"Raw Material For SR 2": {}}, + "Sub Assembly For SR 3": {"Raw Material For SR 3": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + for item_code in [ + "Sub Assembly For SR 1", + "Sub Assembly For SR 2", + "Sub Assembly For SR 3", + "Raw Material For SR 1", + "Raw Material For SR 2", + "Raw Material For SR 3", + ]: + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=15, + skip_available_sub_assembly_item=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + sub_assembly_warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=1, + ) + + plan.get_sub_assembly_items() + plan.set("mr_items", []) + mr_items = get_items_for_material_requests(plan.as_dict()) + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + self.assertTrue(len(plan.sub_assembly_items) == 3) + for row in plan.sub_assembly_items: + self.assertEqual(row.required_qty, 15.0) + self.assertEqual(row.qty, 10.0) + + self.assertTrue(len(plan.mr_items) == 3) + for row in plan.mr_items: + self.assertEqual(row.required_bom_qty, 10.0) + self.assertEqual(row.quantity, 5.0) + + plan.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 6) + + for row in reserved_entries: + self.assertEqual(row.reserved_qty, 5.0) + + plan.submit_material_request = 1 + plan.make_material_request() + plan.make_work_order() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + + self.assertTrue(len(material_requests) > 0) + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 9) + + work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") + for wo_name in list(set(work_orders)): + wo_doc = frappe.get_doc("Work Order", wo_name) + self.assertEqual(wo_doc.reserve_stock, 1) + + wo_doc.source_warehouse = warehouse + wo_doc.wip_warehouse = warehouse + wo_doc.fg_warehouse = warehouse + wo_doc.submit() + + sre = StockReservation(wo_doc) + reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) + if wo_doc.production_item == "Finished Good For SR": + self.assertEqual(len(reserved_entries), 3) + else: + # For raw materials 2 stock reservation entries + # 5 qty was present already in stock and 5 added from new PO + self.assertEqual(len(reserved_entries), 2) + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 0) + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + + def test_stock_reservation_of_serial_nos_against_production_plan(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + + bom_tree = { + "Finished Good For SR": { + "SN Sub Assembly For SR 1": {"SN Raw Material For SR 1": {}}, + "SN Sub Assembly For SR 2": {"SN Raw Material For SR 2": {}}, + "SN Sub Assembly For SR 3": {"SN Raw Material For SR 3": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + for item_code in [ + "SN Sub Assembly For SR 1", + "SN Sub Assembly For SR 2", + "SN Sub Assembly For SR 3", + "SN Raw Material For SR 1", + "SN Raw Material For SR 2", + "SN Raw Material For SR 3", + ]: + doc = frappe.get_doc("Item", item_code) + doc.has_serial_no = 1 + doc.serial_no_series = f"SNN-{item_code}.-.#####" + doc.save() + + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=15, + skip_available_sub_assembly_item=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + sub_assembly_warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=1, + ) + + plan.get_sub_assembly_items() + plan.set("mr_items", []) + mr_items = get_items_for_material_requests(plan.as_dict()) + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + self.assertTrue(len(plan.sub_assembly_items) == 3) + for row in plan.sub_assembly_items: + self.assertEqual(row.required_qty, 15.0) + self.assertEqual(row.qty, 10.0) + + self.assertTrue(len(plan.mr_items) == 3) + for row in plan.mr_items: + self.assertEqual(row.required_bom_qty, 10.0) + self.assertEqual(row.quantity, 5.0) + + plan.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 6) + + for row in reserved_entries: + self.assertEqual(row.reserved_qty, 5.0) + + plan.submit_material_request = 1 + plan.make_material_request() + plan.make_work_order() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + + additional_serial_nos = [] + + for item_code in [ + "SN Sub Assembly For SR 1", + "SN Sub Assembly For SR 2", + "SN Sub Assembly For SR 3", + "SN Raw Material For SR 1", + "SN Raw Material For SR 2", + "SN Raw Material For SR 3", + ]: + se = make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + additional_serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + self.assertTrue(additional_serial_nos) + + self.assertTrue(len(material_requests) > 0) + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 9) + serial_nos_res_for_pp = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="serial_no", + ) + + work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") + for wo_name in list(set(work_orders)): + wo_doc = frappe.get_doc("Work Order", wo_name) + self.assertEqual(wo_doc.reserve_stock, 1) + + wo_doc.source_warehouse = warehouse + wo_doc.wip_warehouse = warehouse + wo_doc.fg_warehouse = warehouse + wo_doc.submit() + + sre = StockReservation(wo_doc) + reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) + serial_nos_res_for_wo = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="serial_no", + ) + + for serial_no in serial_nos_res_for_wo: + self.assertTrue(serial_no in serial_nos_res_for_pp) + self.assertFalse(serial_no in additional_serial_nos) + + if wo_doc.production_item == "Finished Good For SR": + self.assertEqual(len(reserved_entries), 3) + else: + # For raw materials 2 stock reservation entries + # 5 qty was present already in stock and 5 added from new PO + self.assertEqual(len(reserved_entries), 2) + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 0) + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + + def test_stock_reservation_of_batch_nos_against_production_plan(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + + bom_tree = { + "Finished Good For SR": { + "Batch Sub Assembly For SR 1": {"Batch Raw Material For SR 1": {}}, + "Batch Sub Assembly For SR 2": {"Batch Raw Material For SR 2": {}}, + "Batch Sub Assembly For SR 3": {"Batch Raw Material For SR 3": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + for item_code in [ + "Batch Sub Assembly For SR 1", + "Batch Sub Assembly For SR 2", + "Batch Sub Assembly For SR 3", + "Batch Raw Material For SR 1", + "Batch Raw Material For SR 2", + "Batch Raw Material For SR 3", + ]: + doc = frappe.get_doc("Item", item_code) + doc.has_batch_no = 1 + doc.create_new_batch = 1 + doc.batch_number_series = f"BCH-{item_code}.-.#####" + doc.save() + + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=15, + skip_available_sub_assembly_item=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + sub_assembly_warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=1, + ) + + plan.get_sub_assembly_items() + plan.set("mr_items", []) + mr_items = get_items_for_material_requests(plan.as_dict()) + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + self.assertTrue(len(plan.sub_assembly_items) == 3) + for row in plan.sub_assembly_items: + self.assertEqual(row.required_qty, 15.0) + self.assertEqual(row.qty, 10.0) + + self.assertTrue(len(plan.mr_items) == 3) + for row in plan.mr_items: + self.assertEqual(row.required_bom_qty, 10.0) + self.assertEqual(row.quantity, 5.0) + + plan.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 6) + + for row in reserved_entries: + self.assertEqual(row.reserved_qty, 5.0) + + plan.submit_material_request = 1 + plan.make_material_request() + plan.make_work_order() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + + additional_batches = [] + + for item_code in [ + "Batch Sub Assembly For SR 1", + "Batch Sub Assembly For SR 2", + "Batch Sub Assembly For SR 3", + "Batch Raw Material For SR 1", + "Batch Raw Material For SR 2", + "Batch Raw Material For SR 3", + ]: + se = make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + additional_batches.append(batch_no) + + self.assertTrue(additional_batches) + + self.assertTrue(len(material_requests) > 0) + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 9) + batches_reserved_for_pp = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="batch_no", + ) + + work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") + for wo_name in list(set(work_orders)): + wo_doc = frappe.get_doc("Work Order", wo_name) + self.assertEqual(wo_doc.reserve_stock, 1) + + wo_doc.source_warehouse = warehouse + wo_doc.wip_warehouse = warehouse + wo_doc.fg_warehouse = warehouse + wo_doc.submit() + + sre = StockReservation(wo_doc) + reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) + batches_reserved_for_wo = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="batch_no", + ) + + for batch_no in batches_reserved_for_wo: + self.assertTrue(batch_no in batches_reserved_for_pp) + self.assertFalse(batch_no in additional_batches) + + if wo_doc.production_item == "Finished Good For SR": + self.assertEqual(len(reserved_entries), 3) + else: + # For raw materials 2 stock reservation entries + # 5 qty was present already in stock and 5 added from new PO + self.assertEqual(len(reserved_entries), 2) + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 0) + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + def create_production_plan(**args): """ @@ -1931,6 +2353,7 @@ def create_production_plan(**args): "get_items_from": "Sales Order", "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, } ) diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json index c5330d74772..38fe30cc9de 100644 --- a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json +++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json @@ -26,7 +26,7 @@ }, { "fieldname": "qty", - "fieldtype": "Data", + "fieldtype": "Float", "in_list_view": 1, "label": "Qty" }, @@ -37,17 +37,19 @@ "label": "Item Reference" } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-03-27 13:10:20.410593", + "modified": "2025-05-07 17:47:36.244083", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Item Reference", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.py b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.py index d95bf8c006f..d72402201d3 100644 --- a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.py +++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.py @@ -19,7 +19,7 @@ class ProductionPlanItemReference(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data - qty: DF.Data | None + qty: DF.Float sales_order: DF.Link | None sales_order_item: DF.Data | None # end: auto-generated types diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json index 5b87373f243..6a5d7dcb3d2 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -9,29 +9,32 @@ "item_name", "fg_warehouse", "parent_item_code", - "schedule_date", "column_break_3", - "qty", "bom_no", "bom_level", "type_of_manufacturing", + "section_break_4rxf", + "required_qty", + "column_break_xfhm", + "projected_qty", + "qty", + "subcontracting_section", "supplier", - "work_order_details_section", - "wo_produced_qty", "purchase_order", + "work_order_details_section", "production_plan_item", + "wo_produced_qty", + "stock_reserved_qty", "column_break_7", "received_qty", "indent", "section_break_19", + "schedule_date", "uom", "stock_uom", - "column_break_22", - "description", - "section_break_4rxf", "actual_qty", - "column_break_xfhm", - "projected_qty" + "column_break_22", + "description" ], "fields": [ { @@ -49,18 +52,18 @@ "depends_on": "eval:doc.type_of_manufacturing == \"In House\"", "fieldname": "work_order_details_section", "fieldtype": "Section Break", - "label": "Reference" + "label": "Manufacturing" }, { "fieldname": "column_break_7", "fieldtype": "Column Break" }, { - "columns": 1, + "columns": 2, "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Required Qty", + "label": "Qty to Order", "read_only": 1 }, { @@ -152,7 +155,6 @@ "columns": 2, "fieldname": "fg_warehouse", "fieldtype": "Link", - "in_list_view": 1, "label": "Target Warehouse", "options": "Warehouse" }, @@ -167,22 +169,24 @@ { "fieldname": "supplier", "fieldtype": "Link", + "in_list_view": 1, "label": "Supplier", - "mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract'", + "mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract' && doc.qty > 0", "options": "Supplier" }, { - "columns": 1, + "columns": 2, "fieldname": "schedule_date", "fieldtype": "Datetime", - "in_list_view": 1, "label": "Schedule Date" }, { "fieldname": "section_break_4rxf", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Quantity" }, { + "columns": 2, "fieldname": "actual_qty", "fieldtype": "Float", "label": "Actual Qty", @@ -194,8 +198,10 @@ "fieldtype": "Column Break" }, { + "columns": 2, "fieldname": "projected_qty", "fieldtype": "Float", + "in_list_view": 1, "label": "Projected Qty", "no_copy": 1, "read_only": 1 @@ -205,20 +211,42 @@ "fieldtype": "Float", "label": "Produced Qty", "read_only": 1 + }, + { + "columns": 2, + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty" + }, + { + "fieldname": "subcontracting_section", + "fieldtype": "Section Break", + "label": "Subcontracting" + }, + { + "fieldname": "stock_reserved_qty", + "fieldtype": "Float", + "label": "Stock Reserved Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-01-01 17:50:32.273610", + "modified": "2025-05-01 14:28:35.979941", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", "owner": "Administrator", "permissions": [], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py index ad1d655de8b..41e5bf28b56 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py @@ -32,7 +32,9 @@ class ProductionPlanSubAssemblyItem(Document): purchase_order: DF.Link | None qty: DF.Float received_qty: DF.Float + required_qty: DF.Float schedule_date: DF.Datetime | None + stock_reserved_qty: DF.Float stock_uom: DF.Link | None supplier: DF.Link | None type_of_manufacturing: DF.Literal["In House", "Subcontract", "Material Request"] diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 3171bbd2741..7b7a670e6f0 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -43,11 +43,6 @@ "skip_transfer", "from_wip_warehouse", "update_consumed_material_cost_in_project", - "serial_no_and_batch_for_finished_good_section", - "has_serial_no", - "has_batch_no", - "column_break_18", - "batch_size", "time", "planned_start_date", "planned_end_date", @@ -63,6 +58,11 @@ "column_break_24", "corrective_operation_cost", "total_operating_cost", + "serial_no_and_batch_for_finished_good_section", + "has_serial_no", + "has_batch_no", + "column_break_18", + "batch_size", "more_info", "description", "stock_uom", @@ -505,7 +505,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "serial_no_and_batch_for_finished_good_section", "fieldtype": "Section Break", - "label": "Serial No and Batch for Finished Good" + "label": "Finished Good Serial / Batch" }, { "fieldname": "column_break_17", @@ -594,12 +594,13 @@ "label": " Reserve Stock" } ], + "grid_page_length": 50, "icon": "fa fa-cogs", "idx": 1, "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2024-09-23 16:56:00.483027", + "modified": "2025-04-25 11:46:38.739588", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", @@ -629,10 +630,11 @@ "role": "Stock User" } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "ASC", "states": [], "title_field": "production_item", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 26318b10634..0aaaf78f50c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -10,7 +10,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.query_builder import Case -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( cint, date_diff, @@ -1399,7 +1399,9 @@ class WorkOrder(Document): items = frappe._dict() stock_entry.reload() - if stock_entry.purpose == "Manufacture" and self.sales_order: + if stock_entry.purpose == "Manufacture" and ( + self.sales_order or self.production_plan_sub_assembly_item + ): items = self.get_finished_goods_for_reservation(stock_entry) elif stock_entry.purpose == "Material Transfer for Manufacture": items = self.get_list_of_materials_for_reservation(stock_entry) @@ -1440,41 +1442,97 @@ class WorkOrder(Document): def get_finished_goods_for_reservation(self, stock_entry): items = frappe._dict() - so_details = self.get_so_details() - if not so_details: - return items + if self.production_plan_sub_assembly_item: + # Reserve the sub-assembly item for the final product for the work order. + item_details = self.get_wo_details() + else: + # Reserve the final product for the sales order. + item_details = self.get_so_details() - qty = so_details.stock_qty - (so_details.stock_reserved_qty + so_details.delivered_qty) - if not qty: - return items - - for row in stock_entry.items: - if not row.t_warehouse or not row.is_finished_item: + for item in item_details: + qty_to_reserve = flt(item.stock_qty) - flt(item.stock_reserved_qty + item.delivered_qty) + if qty_to_reserve <= 0: continue - if qty > row.transfer_qty: - qty = row.transfer_qty + warehouse = item.warehouse + if ( + item.get("parenttype") == "Work Order" + and item.get("skip_transfer") + and item.get("from_wip_warehouse") + ): + warehouse = item.wip_warehouse - if row.item_code not in items: - items[row.item_code] = frappe._dict( - { - "voucher_no": self.sales_order, - "voucher_type": "Sales Order", - "voucher_detail_no": so_details.name, - "item_code": row.item_code, - "warehouse": row.t_warehouse, - "stock_qty": qty, - "from_voucher_no": stock_entry.name, - "from_voucher_type": stock_entry.doctype, - "from_voucher_detail_no": row.name, - "serial_and_batch_bundles": [row.serial_and_batch_bundle], - } - ) - else: - items[row.item_code]["stock_qty"] += qty + for row in stock_entry.items: + if ( + not row.t_warehouse + or not row.is_finished_item + or row.t_warehouse != warehouse + or row.item_code != item.item_code + ): + continue + + reserved_qty = qty_to_reserve + if qty_to_reserve > row.transfer_qty: + reserved_qty = row.transfer_qty + qty_to_reserve -= row.transfer_qty + else: + qty_to_reserve = 0 + + if row.item_code not in items: + items[row.item_code] = frappe._dict( + { + "voucher_no": item.voucher_no, + "voucher_type": item.voucher_type, + "voucher_detail_no": item.name, + "item_code": row.item_code, + "warehouse": row.t_warehouse, + "stock_qty": reserved_qty, + "from_voucher_no": stock_entry.name, + "from_voucher_type": stock_entry.doctype, + "from_voucher_detail_no": row.name, + "serial_and_batch_bundles": [row.serial_and_batch_bundle], + } + ) + else: + items[row.item_code]["stock_qty"] += reserved_qty return items + def get_wo_details(self): + doctype = frappe.qb.DocType("Work Order") + child_doctype = frappe.qb.DocType("Work Order Item") + + query = ( + frappe.qb.from_(doctype) + .inner_join(child_doctype) + .on(doctype.name == child_doctype.parent) + .select( + child_doctype.name, + child_doctype.required_qty.as_("stock_qty"), + child_doctype.transferred_qty.as_("delivered_qty"), + child_doctype.stock_reserved_qty, + child_doctype.source_warehouse.as_("warehouse"), + doctype.wip_warehouse, + doctype.skip_transfer, + doctype.from_wip_warehouse, + child_doctype.parenttype, + child_doctype.item_code, + child_doctype.parent.as_("voucher_no"), + child_doctype.parenttype.as_("voucher_type"), + ) + .where( + (child_doctype.item_code == self.production_item) + & (doctype.docstatus == 1) + & (doctype.production_plan == self.production_plan) + & ( + IfNull(doctype.production_plan_sub_assembly_item, "") + != self.production_plan_sub_assembly_item + ) + ) + ) + + return query.run(as_dict=1) + def get_so_details(self): return frappe.db.get_value( "Sales Order Item", @@ -1483,7 +1541,15 @@ class WorkOrder(Document): "item_code": self.production_item, "docstatus": 1, }, - ["name", "stock_qty", "stock_reserved_qty", "delivered_qty"], + [ + "name", + "stock_qty", + "stock_reserved_qty", + "warehouse", + "parent as voucher_no", + "parenttype as voucher_type", + "delivered_qty", + ], as_dict=1, ) @@ -1526,7 +1592,7 @@ class WorkOrder(Document): @frappe.whitelist() -def make_stock_reservation_entries(doc, items=None, notify=False): +def make_stock_reservation_entries(doc, items=None, table_name=None, notify=False): if isinstance(doc, str): doc = parse_json(doc) doc = frappe.get_doc("Work Order", doc.get("name")) @@ -1536,7 +1602,13 @@ def make_stock_reservation_entries(doc, items=None, notify=False): sre = StockReservation(doc, items=items, notify=notify) if doc.docstatus == 1: - sre.make_stock_reservation_entries() + if doc.production_plan: + sre.transfer_reservation_entries_to( + doc.production_plan, from_doctype="Production Plan", to_doctype="Work Order" + ) + else: + sre.make_stock_reservation_entries() + frappe.msgprint(_("Stock Reservation Entries Created"), alert=True) elif doc.docstatus == 2: sre.cancel_stock_reservation_entries() diff --git a/erpnext/public/js/stock_reservation.js b/erpnext/public/js/stock_reservation.js index 58d57aff499..854212ee5ae 100644 --- a/erpnext/public/js/stock_reservation.js +++ b/erpnext/public/js/stock_reservation.js @@ -14,7 +14,7 @@ $.extend(erpnext.stock_reservation, { fields: erpnext.stock_reservation.get_dialog_fields(frm, parms), primary_action_label: __("Reserve Stock"), primary_action: () => { - erpnext.stock_reservation.reserve_stock(frm, parms); + erpnext.stock_reservation.reserve_stock(frm, table_name, parms); }, }); @@ -32,9 +32,20 @@ $.extend(erpnext.stock_reservation, { "Work Order": "required_qty", }[frm.doc.doctype]; + if (frm.doc.doctype === "Production Plan") { + if (table_name === "sub_assembly_items") { + params["qty_field"] = "qty"; + params["item_code_field"] = "production_item"; + params["warehouse_field"] = "fg_warehouse"; + } else { + params["qty_field"] = "quantity"; + } + } + params["dispatch_qty_field"] = { "Sales Order": "delivered_qty", "Work Order": "transferred_qty", + "Production Plan": "delivered_qty", }[frm.doc.doctype]; params["method"] = { @@ -140,6 +151,9 @@ $.extend(erpnext.stock_reservation, { dispatch_qty_field = "consumed_qty"; } + let item_code_field = parms.item_code_field || "item_code"; + let warehouse_field = parms.warehouse_field || "warehouse"; + frm.doc[parms.table_name].forEach((item) => { if (frm.doc.reserve_stock) { let unreserved_qty = @@ -152,8 +166,8 @@ $.extend(erpnext.stock_reservation, { if (unreserved_qty > 0) { let args = { __checked: 1, - item_code: item.item_code, - warehouse: item.warehouse || item.source_warehouse, + item_code: item[item_code_field] || item.item_code, + warehouse: item[warehouse_field] || item.warehouse || item.source_warehouse, }; args[field] = item.name; @@ -167,7 +181,7 @@ $.extend(erpnext.stock_reservation, { dialog.show(); }, - reserve_stock(frm, parms) { + reserve_stock(frm, table_name, parms) { let dialog = erpnext.stock_reservation.dialog; var data = { items: dialog.fields_dict.items.grid.get_selected_children() }; @@ -177,6 +191,7 @@ $.extend(erpnext.stock_reservation, { args: { doc: frm.doc, items: data.items, + table_name: table_name, notify: true, }, freeze: true, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d499ffd219a..b6d87b7525c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -17,6 +17,7 @@ from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.accounts_controller import merge_taxes from erpnext.controllers.buying_controller import BuyingController from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction +from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -383,7 +384,7 @@ class PurchaseReceipt(BuyingController): self.make_gl_entries() self.repost_future_sle_and_gle() self.set_consumed_qty_in_subcontract_order() - self.reserve_stock_for_sales_order() + self.reserve_stock() self.update_received_qty_if_from_pp() def update_received_qty_if_from_pp(self): @@ -913,6 +914,10 @@ class PurchaseReceipt(BuyingController): pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) + def reserve_stock(self): + self.reserve_stock_for_sales_order() + self.reserve_stock_for_production_plan() + def reserve_stock_for_sales_order(self): if ( self.is_return @@ -953,6 +958,66 @@ class PurchaseReceipt(BuyingController): notify=True, ) + def reserve_stock_for_production_plan(self): + if self.is_return or not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"): + return + + production_plan_references = self.get_production_plan_references() + production_plan_items = [] + + docnames = [] + for row in self.items: + if row.material_request_item and row.material_request_item in production_plan_references: + _ref = production_plan_references[row.material_request_item] + docnames.append(_ref.production_plan) + row.update( + { + "voucher_type": "Production Plan", + "voucher_no": _ref.production_plan, + "voucher_detail_no": _ref.material_request_plan_item, + "from_voucher_no": self.name, + "from_voucher_detail_no": row.name, + "from_voucher_type": self.doctype, + } + ) + + production_plan_items.append(row) + + if not production_plan_items: + return + + sre = StockReservation(doc=self, items=production_plan_items) + sre.make_stock_reservation_entries() + if docnames: + sre.transfer_reservation_entries_to( + docnames, from_doctype="Production Plan", to_doctype="Work Order" + ) + + def get_production_plan_references(self): + production_plan_references = frappe._dict() + material_request_items = [] + + for row in self.items: + if row.material_request_item: + material_request_items.append(row.material_request_item) + + if not material_request_items: + return frappe._dict() + + items = frappe.get_all( + "Material Request Item", + fields=["material_request_plan_item", "production_plan", "name"], + filters={"name": ["in", material_request_items], "docstatus": 1}, + ) + + for item in items: + if not item.production_plan: + continue + + production_plan_references.setdefault(item.name, item) + + return production_plan_references + def enable_recalculate_rate_in_sles(self): rejected_warehouses = frappe.get_all( "Purchase Receipt Item", filters={"parent": self.name}, pluck="rejected_warehouse" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index fd4b4eda6dc..b8f23be201e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1646,7 +1646,11 @@ class StockEntry(StockController): def make_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) - if self.purpose == "Manufacture" and not pro_doc.sales_order: + if ( + self.purpose == "Manufacture" + and not pro_doc.sales_order + and not pro_doc.production_plan_sub_assembly_item + ): return pro_doc.set_reserved_qty_for_wip_and_fg(self) @@ -1654,7 +1658,11 @@ class StockEntry(StockController): 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) - if self.purpose == "Manufacture" and not pro_doc.sales_order: + if ( + self.purpose == "Manufacture" + and not pro_doc.sales_order + and not pro_doc.production_plan_sub_assembly_item + ): return pro_doc.cancel_reserved_qty_for_wip_and_fg(self) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json index daae6218fed..79837e5513f 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json @@ -84,7 +84,7 @@ "no_copy": 1, "oldfieldname": "voucher_type", "oldfieldtype": "Data", - "options": "\nSales Order\nWork Order", + "options": "\nSales Order\nWork Order\nProduction Plan", "print_width": "150px", "read_only": 1, "width": "150px" @@ -289,7 +289,7 @@ "fieldtype": "Select", "label": "From Voucher Type", "no_copy": 1, - "options": "\nPick List\nPurchase Receipt\nStock Entry\nWork Order", + "options": "\nPick List\nPurchase Receipt\nStock Entry\nWork Order\nProduction Plan", "print_hide": 1, "read_only": 1, "report_hide": 1 @@ -339,12 +339,13 @@ "label": "Qty in WIP Warehouse" } ], + "grid_page_length": 50, "hide_toolbar": 1, "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-09-19 15:28:24.726283", + "modified": "2025-04-30 22:15:22.998138", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reservation Entry", @@ -450,8 +451,9 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index de1dc49aabf..b9a5593afc1 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -30,7 +30,9 @@ class StockReservationEntry(Document): delivered_qty: DF.Float from_voucher_detail_no: DF.Data | None from_voucher_no: DF.DynamicLink | None - from_voucher_type: DF.Literal["", "Pick List", "Purchase Receipt", "Stock Entry", "Work Order"] + from_voucher_type: DF.Literal[ + "", "Pick List", "Purchase Receipt", "Stock Entry", "Work Order", "Production Plan" + ] has_batch_no: DF.Check has_serial_no: DF.Check item_code: DF.Link | None @@ -46,7 +48,7 @@ class StockReservationEntry(Document): voucher_detail_no: DF.Data | None voucher_no: DF.DynamicLink | None voucher_qty: DF.Float - voucher_type: DF.Literal["", "Sales Order", "Work Order"] + voucher_type: DF.Literal["", "Sales Order", "Work Order", "Production Plan"] warehouse: DF.Link | None # end: auto-generated types @@ -335,6 +337,7 @@ class StockReservationEntry(Document): item_doctype = { "Sales Order": "Sales Order Item", "Work Order": "Work Order Item", + "Production Plan": "Production Plan Sub Assembly Item", }.get(self.voucher_type, None) if item_doctype: @@ -350,6 +353,11 @@ class StockReservationEntry(Document): ) ).run(as_list=True)[0][0] or 0 + if self.voucher_type == "Production Plan" and frappe.db.exists( + "Material Request Plan Item", self.voucher_detail_no + ): + item_doctype = "Material Request Plan Item" + frappe.db.set_value( item_doctype, self.voucher_detail_no, @@ -968,13 +976,14 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st class StockReservation: - def __init__(self, doc, items=None, notify=True): + def __init__(self, doc, items=None, kwargs=None, notify=True): if isinstance(doc, str): doc = parse_json(doc) doc = frappe.get_doc("Work Order", doc.get("name")) self.doc = doc self.items = items + self.kwargs = kwargs self.initialize_fields() def initialize_fields(self) -> None: @@ -989,6 +998,9 @@ class StockReservation: self.warehouse_field = "source_warehouse" if self.doc.skip_transfer and self.doc.from_wip_warehouse: self.warehouse = self.doc.wip_warehouse + elif self.doc.doctype == "Production Plan" and self.kwargs: + for key, value in self.kwargs.items(): + setattr(self, key, value) def cancel_stock_reservation_entries(self, names=None) -> None: """Cancels Stock Reservation Entries for the Voucher.""" @@ -1037,11 +1049,16 @@ class StockReservation: if isinstance(item, dict): item = frappe._dict(item) + item_code = item.get("item_code") or item.get("production_item") + item_details = frappe.get_cached_value( - "Item", item.item_code, ["has_serial_no", "has_batch_no", "stock_uom"], as_dict=True + "Item", item_code, ["has_serial_no", "has_batch_no", "stock_uom"], as_dict=True ) warehouse = self.warehouse or item.get(self.warehouse_field) or item.get("warehouse") + if self.doc.doctype == "Production Plan" and item.get("from_warehouse"): + warehouse = item.get("from_warehouse") + if ( not warehouse and self.doc.doctype == "Work Order" @@ -1052,10 +1069,12 @@ class StockReservation: ) qty = item.get(self.qty_field) or item.get("stock_qty") + if not qty: + continue - self.available_qty_to_reserve = self.get_available_qty_to_reserve(item.item_code, warehouse) + self.available_qty_to_reserve = self.get_available_qty_to_reserve(item_code, warehouse) if not self.available_qty_to_reserve: - self.throw_stock_not_exists_error(item, warehouse) + self.throw_stock_not_exists_error(item.idx, item_code, warehouse) self.qty_to_be_reserved = ( qty if self.available_qty_to_reserve >= qty else self.available_qty_to_reserve @@ -1064,7 +1083,7 @@ class StockReservation: if not self.qty_to_be_reserved: continue - sre.item_code = item.item_code + sre.item_code = item_code sre.warehouse = warehouse sre.has_serial_no = item_details.has_serial_no sre.has_batch_no = item_details.has_batch_no @@ -1107,10 +1126,10 @@ class StockReservation: }, ) - def throw_stock_not_exists_error(self, item, warehouse): + def throw_stock_not_exists_error(self, idx, item_code, warehouse): frappe.msgprint( _("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format( - item.idx, frappe.bold(item.item_code), frappe.bold(warehouse) + idx, frappe.bold(item_code), frappe.bold(warehouse) ), title=_("Stock Reservation"), indicator="orange", @@ -1143,6 +1162,163 @@ class StockReservation: return available_qty + def transfer_reservation_entries_to(self, docnames, from_doctype, to_doctype): + delivery_qty_to_update = frappe._dict() + if isinstance(docnames, str): + docnames = [docnames] + + items_to_reserve = self.get_items_to_reserve(docnames, from_doctype, to_doctype) + if not items_to_reserve: + return + + reservation_entries = self.get_reserved_entries(from_doctype, docnames) + if not reservation_entries: + return + + entries_to_reserve = [] + for row in reservation_entries: + for entry in items_to_reserve: + if not ( + row.item_code == entry.item_code and row.warehouse == entry.warehouse and entry.qty > 0 + ): + continue + + available_qty = row.reserved_qty - row.delivered_qty + if available_qty <= 0: + continue + + # transfer qty + if available_qty > entry.qty: + qty_to_reserve = entry.qty + row.delivered_qty += available_qty - entry.qty + delivery_qty_to_update.setdefault(row.name, row.delivered_qty) + else: + qty_to_reserve = available_qty + row.delivered_qty += qty_to_reserve + delivery_qty_to_update.setdefault(row.name, row.delivered_qty) + + entries_to_reserve.append([entry, row, qty_to_reserve]) + + entry.qty -= qty_to_reserve + + if delivery_qty_to_update: + self.update_delivered_qty(delivery_qty_to_update) + + for entry, row, qty_to_reserve in entries_to_reserve: + self.make_stock_reservation_entry(entry, row, qty_to_reserve) + + def update_delivered_qty(self, delivery_qty_to_update): + for name, delivered_qty in delivery_qty_to_update.items(): + doctype = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.update(doctype) + .set(doctype.delivered_qty, delivered_qty) + .set( + doctype.status, + "Delivered" if doctype.reserved_qty == doctype.delivered_qty else "Reserved", + ) + .where(doctype.name == name) + ) + + query.run() + + def make_stock_reservation_entry(self, row, against_row, reserved_qty): + fields = [ + "item_code", + "warehouse", + "voucher_type", + "voucher_no", + "voucher_detail_no", + "company", + "stock_uom", + ] + + sre = frappe.new_doc("Stock Reservation Entry") + for row_field in fields: + sre.set(row_field, row.get(row_field)) + + sre.available_qty = reserved_qty + sre.reserved_qty = reserved_qty + sre.voucher_qty = row.required_qty + sre.from_voucher_no = against_row.voucher_no + sre.from_voucher_detail_no = against_row.voucher_detail_no + sre.from_voucher_type = against_row.voucher_type + + bundles = [against_row.name] + if row.serial_and_batch_bundles: + bundles = row.serial_and_batch_bundles + + self.set_serial_batch(sre, bundles) + + sre.save() + sre.submit() + + def get_reserved_entries(self, doctype, docnames): + filters = { + "docstatus": 1, + "status": ("not in", ["Delivered", "Cancelled", "Draft"]), + "voucher_type": doctype, + "voucher_no": docnames, + } + + if isinstance(docnames, list): + filters["voucher_no"] = ("in", docnames) + + return frappe.get_all("Stock Reservation Entry", fields=["*"], filters=filters) + + def get_items_to_reserve(self, docnames, from_doctype, to_doctype): + field = frappe.scrub(from_doctype) + + doctype = frappe.qb.DocType(to_doctype) + child_doctype = frappe.qb.DocType(to_doctype + " Item") + + query = ( + frappe.qb.from_(doctype) + .inner_join(child_doctype) + .on(doctype.name == child_doctype.parent) + .select( + doctype.name.as_("voucher_no"), + child_doctype.name.as_("voucher_detail_no"), + child_doctype.item_code, + doctype.company, + child_doctype.stock_uom, + ) + .where((doctype.docstatus == 1) & (doctype[field].isin(docnames))) + ) + + if to_doctype == "Work Order": + query = query.select( + child_doctype.source_warehouse, + doctype.wip_warehouse, + doctype.skip_transfer, + doctype.from_wip_warehouse, + child_doctype.required_qty, + (child_doctype.required_qty - child_doctype.transferred_qty).as_("qty"), + child_doctype.stock_reserved_qty, + ) + + query = query.where( + (doctype.qty > doctype.material_transferred_for_manufacturing) + & (doctype.status != "Completed") + ) + + data = query.run(as_dict=True) + items = [] + + for row in data: + if row.qty > row.stock_reserved_qty: + row.qty -= flt(row.stock_reserved_qty) + row.warehouse = row.source_warehouse + if row.skip_transfer and row.from_wip_warehouse: + row.warehouse = row.wip_warehouse + + if to_doctype == "Work Order": + row.voucher_type = "Work Order" + + items.append(row) + + return items + def create_stock_reservation_entries_for_so_items( sales_order: object, diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 15c356dde3d..6b93e0883cf 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -109,6 +109,8 @@ def get_stock_balance( from erpnext.stock.stock_ledger import get_previous_sle + frappe.has_permission("Item", "read") + if posting_date is None: posting_date = nowdate() if posting_time is None: From 3965e2f3de3a8829b1d942d6f686bbf8cd2be749 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Mon, 12 May 2025 14:06:03 +0530 Subject: [PATCH 24/35] fix: fiscal year error on new setup and continuous request (#47431) fix: fiscal year error on new setup --- erpnext/public/js/utils.js | 5 +++-- erpnext/startup/boot.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 0d6ee1ca2f5..f0adfe990e8 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -432,8 +432,9 @@ $.extend(erpnext.utils, { if (!frappe.boot.setup_complete) { return; } + const today = frappe.datetime.get_today(); if (!date) { - date = frappe.datetime.get_today(); + date = today; } let fiscal_year = ""; @@ -444,7 +445,7 @@ $.extend(erpnext.utils, { ) { if (with_dates) fiscal_year = frappe.boot.current_fiscal_year; else fiscal_year = frappe.boot.current_fiscal_year[0]; - } else { + } else if (today != date) { frappe.call({ method: "erpnext.accounts.utils.get_fiscal_year", type: "GET", // make it cacheable diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index 12de9273834..a5f469a58ec 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -3,6 +3,7 @@ import frappe +from frappe.defaults import get_user_default from frappe.utils import cint import erpnext.accounts.utils @@ -57,7 +58,9 @@ def boot_session(bootinfo): party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""") bootinfo.party_account_types = frappe._dict(party_account_types) - fiscal_year = erpnext.accounts.utils.get_fiscal_years(frappe.utils.nowdate(), raise_on_missing=False) + fiscal_year = erpnext.accounts.utils.get_fiscal_years( + frappe.utils.nowdate(), company=get_user_default("company"), raise_on_missing=False + ) if fiscal_year: bootinfo.current_fiscal_year = fiscal_year[0] From 57f3489dfab6f624ab6370eaa8d877af920976e8 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Mon, 12 May 2025 14:08:24 +0530 Subject: [PATCH 25/35] fix: POS non-stock item mistakenly hidden as unavailable (#47493) --- erpnext/selling/page/point_of_sale/point_of_sale.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index cfa26639fed..eb96101fce5 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -147,10 +147,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te bin_join_selection, bin_join_condition = "", "" if hide_unavailable_items: - bin_join_selection = ", `tabBin` bin" - bin_join_condition = ( - "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" - ) + bin_join_selection = "LEFT JOIN `tabBin` bin ON bin.item_code = item.name" + bin_join_condition = "AND item.is_stock_item = 0 OR (item.is_stock_item = 1 AND bin.warehouse = %(warehouse)s AND bin.actual_qty > 0)" items_data = frappe.db.sql( """ From 685132236128456fe9ed5e3677454f92cc631e08 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 24 Apr 2025 19:32:38 +0530 Subject: [PATCH 26/35] fix: accumulate values for all the fiscal years in Profit And Loss Statement --- .../profit_and_loss_statement/profit_and_loss_statement.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 2b6280c74b5..ccb4d26f77b 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -35,7 +35,6 @@ def execute(filters=None): filters=filters, accumulated_values=filters.accumulated_values, ignore_closing_entries=True, - ignore_accumulated_values_for_fy=True, ) expense = get_data( @@ -46,7 +45,6 @@ def execute(filters=None): filters=filters, accumulated_values=filters.accumulated_values, ignore_closing_entries=True, - ignore_accumulated_values_for_fy=True, ) net_profit_loss = get_net_profit_loss( From 61d13ce232c25122ac9a47104ccae0a23aea5f69 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 12 May 2025 15:07:02 +0530 Subject: [PATCH 27/35] fix: typo --- .../profit_and_loss_statement/test_profit_and_loss_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py index 7e2d4a7afe0..c9dfe515a08 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py @@ -58,7 +58,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, IntegrationTestCase): period_end_date=fy.year_end_date, filter_based_on="Fiscal Year", periodicity="Monthly", - accumulated_vallues=True, + accumulated_values=True, ) def test_profit_and_loss_output_and_summary(self): From afff6b84ce3c86e3a3621df1efc771adb59d02ed Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 12 May 2025 15:48:39 +0530 Subject: [PATCH 28/35] test: accumulate filter on P&L report --- .../test_profit_and_loss_statement.py | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py index c9dfe515a08..c2bb11b8f47 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py @@ -4,7 +4,7 @@ import frappe from frappe.desk.query_report import export_query from frappe.tests import IntegrationTestCase -from frappe.utils import getdate, today +from frappe.utils import add_days, getdate, today from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.financial_statements import get_period_list @@ -109,3 +109,64 @@ class TestProfitAndLossStatement(AccountsTestMixin, IntegrationTestCase): sales_account = frappe.db.get_value("Company", self.company, "default_income_account") self.assertIn(sales_account, contents) + + def test_accumulate_filter(self): + # ensure 2 fiscal years + cur_fy = self.get_fiscal_year() + find_for = add_days(cur_fy.year_start_date, -1) + _x = frappe.db.get_all( + "Fiscal Year", + filters={"disabled": 0, "year_start_date": ("<=", find_for), "year_end_date": (">=", find_for)}, + )[0] + prev_fy = frappe.get_doc("Fiscal Year", _x.name) + prev_fy.append("companies", {"company": self.company}) + prev_fy.save() + + # make SI on both of them + prev_fy_si = self.create_sales_invoice(qty=1, rate=450, do_not_submit=True) + prev_fy_si.posting_date = add_days(prev_fy.year_end_date, -1) + prev_fy_si.save().submit() + income_acc = prev_fy_si.items[0].income_account + + self.create_sales_invoice(qty=1, rate=120) + + # Unaccumualted + filters = frappe._dict( + company=self.company, + from_fiscal_year=prev_fy.name, + to_fiscal_year=cur_fy.name, + period_start_date=prev_fy.year_start_date, + period_end_date=cur_fy.year_end_date, + filter_based_on="Date Range", + periodicity="Yearly", + accumulated_values=False, + ) + result = execute(filters) + columns = [result[0][2], result[0][3]] + expected = { + "account": income_acc, + columns[0].get("fieldname"): 450.0, + columns[1].get("fieldname"): 120.0, + } + actual = [x for x in result[1] if x.get("account") == income_acc] + self.assertEqual(len(actual), 1) + actual = actual[0] + for key in expected.keys(): + with self.subTest(key=key): + self.assertEqual(expected.get(key), actual.get(key)) + + # accumualted + filters.update({"accumulated_values": True}) + expected = { + "account": income_acc, + columns[0].get("fieldname"): 450.0, + columns[1].get("fieldname"): 570.0, + } + result = execute(filters) + columns = [result[0][2], result[0][3]] + actual = [x for x in result[1] if x.get("account") == income_acc] + self.assertEqual(len(actual), 1) + actual = actual[0] + for key in expected.keys(): + with self.subTest(key=key): + self.assertEqual(expected.get(key), actual.get(key)) From 62cce38a069b66bee3c475a40595bd217bd87eb5 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Mon, 12 May 2025 16:07:22 +0530 Subject: [PATCH 29/35] fix: pos for small screen and checkout page (#47092) * feat: Prevent need for scrolling in lower screens (Point of Sale) * fix: pos checkout page * refactor: renaming variable in pos profile conf * fix: change variable name according to pos conf * fix: removing redundant api call * refactor: adding function * refactor: moving pos settings invoice fields to modal * fix: label for change amount and remaining amount * fix: always display numpad * refactor: function rename * fix: better information * fix: auto scroll to highlighted cart item * chore: added patch --------- Co-authored-by: devdiogenes --- .../doctype/pos_profile/pos_profile.json | 16 +- .../doctype/pos_profile/pos_profile.py | 2 +- erpnext/patches.txt | 1 + .../v15_0/set_grand_total_to_default_mop.py | 9 + .../public/js/controllers/taxes_and_totals.js | 4 +- erpnext/public/scss/point-of-sale.scss | 209 ++++++++++-------- .../page/point_of_sale/pos_controller.js | 23 ++ .../page/point_of_sale/pos_item_cart.js | 3 + .../selling/page/point_of_sale/pos_payment.js | 162 ++++++++------ 9 files changed, 258 insertions(+), 171 deletions(-) create mode 100644 erpnext/patches/v15_0/set_grand_total_to_default_mop.py diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index f774391fe2b..3714c500d13 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -31,7 +31,7 @@ "ignore_pricing_rule", "allow_rate_change", "allow_discount_change", - "disable_grand_total_to_default_mop", + "set_grand_total_to_default_mop", "section_break_23", "item_groups", "column_break_25", @@ -402,12 +402,6 @@ "fieldtype": "Check", "label": "Print Receipt on Order Complete" }, - { - "default": "0", - "fieldname": "disable_grand_total_to_default_mop", - "fieldtype": "Check", - "label": "Disable auto setting Grand Total to default Payment Mode" - }, { "fieldname": "project", "fieldtype": "Link", @@ -415,6 +409,12 @@ "oldfieldname": "cost_center", "oldfieldtype": "Link", "options": "Project" + }, + { + "default": "1", + "fieldname": "set_grand_total_to_default_mop", + "fieldtype": "Check", + "label": "Set Grand Total to Default Payment Method" } ], "grid_page_length": 50, @@ -443,7 +443,7 @@ "link_fieldname": "pos_profile" } ], - "modified": "2025-04-09 11:35:13.779613", + "modified": "2025-05-09 11:23:28.632136", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 399d78d3997..1d482ddb8e1 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -40,7 +40,6 @@ class POSProfile(Document): currency: DF.Link customer: DF.Link | None customer_groups: DF.Table[POSCustomerGroup] - disable_grand_total_to_default_mop: DF.Check disable_rounded_total: DF.Check disabled: DF.Check expense_account: DF.Link | None @@ -56,6 +55,7 @@ class POSProfile(Document): project: DF.Link | None select_print_heading: DF.Link | None selling_price_list: DF.Link | None + set_grand_total_to_default_mop: DF.Check tax_category: DF.Link | None taxes_and_charges: DF.Link | None tc_name: DF.Link | None diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ec3ae09117b..1d9dc0607e0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -411,3 +411,4 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices erpnext.patches.v15_0.rename_group_by_to_categorize_by execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor") erpnext.patches.v14_0.set_update_price_list_based_on +erpnext.patches.v15_0.set_grand_total_to_default_mop diff --git a/erpnext/patches/v15_0/set_grand_total_to_default_mop.py b/erpnext/patches/v15_0/set_grand_total_to_default_mop.py new file mode 100644 index 00000000000..742fee0b775 --- /dev/null +++ b/erpnext/patches/v15_0/set_grand_total_to_default_mop.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + POSProfile = frappe.qb.DocType("POS Profile") + + frappe.qb.update(POSProfile).set(POSProfile.set_grand_total_to_default_mop, 1).where( + POSProfile.disable_grand_total_to_default_mop == 0 + ).run() diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index bab34a3f665..a2b9ec1e7dc 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -947,9 +947,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { var me = this; var payment_status = true; if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { - let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop"); + let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop"); - if (r.message.disable_grand_total_to_default_mop) { + if (!r.message.set_grand_total_to_default_mop) { return; } diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index b8acaeae9a9..0308567d5d5 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -5,9 +5,9 @@ padding: 1%; section { - min-height: 45rem; - height: calc(100vh - 200px); - max-height: calc(100vh - 200px); + min-height: 30rem; + height: calc(100vh - 125px); + max-height: calc(100vh - 125px); } .frappe-control { @@ -375,6 +375,7 @@ flex-direction: column; flex: 1 1 0%; overflow-y: scroll; + min-height: 50px; > .cart-item-wrapper { @extend .pointer-no-select; @@ -775,6 +776,7 @@ .submit-order-btn { @extend .primary-action; + margin-top: 0%; background-color: var(--btn-primary); color: var(--neutral); } @@ -785,117 +787,136 @@ margin-bottom: var(--margin-md); } - > .payment-modes { + > .payment-split-container { display: flex; - padding-bottom: var(--padding-sm); - margin-bottom: var(--margin-sm); - overflow-x: scroll; - overflow-y: hidden; - flex-shrink: 0; - > .payment-mode-wrapper { - min-width: 40%; - padding: var(--padding-xs); + > .payment-container-left { + width: 50%; + margin-bottom: var(--margin-md); - > .mode-of-payment { - @extend .pos-card; - @extend .pointer-no-select; - padding: var(--padding-md) var(--padding-lg); + .payment-modes { + display: flex; + flex-direction: column; + padding-right: var(--padding-sm); + margin-right: var(--margin-sm); + min-height: 15rem; + overflow-y: scroll; + height: calc(100vh - 350px); - > .pay-amount { - display: inline; - float: right; - font-weight: 700; - } + > .payment-mode-wrapper { + min-width: 40%; + padding: var(--padding-xs); - > .mode-of-payment-control { - display: none; - align-items: center; - margin-top: var(--margin-sm); - margin-bottom: var(--margin-xs); - } - - > .loyalty-amount-name { - display: none; - float: right; - font-weight: 700; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - > .cash-shortcuts { - display: none; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: var(--margin-sm); - font-size: var(--text-sm); - text-align: center; - - > .shortcut { + > .mode-of-payment { + @extend .pos-card; @extend .pointer-no-select; - border-radius: var(--border-radius-sm); - background-color: var(--control-bg); - font-weight: 500; - padding: var(--padding-xs) var(--padding-sm); - transition: all 0.15s ease-in-out; + padding: var(--padding-md) var(--padding-lg); - &:hover { - background-color: var(--control-bg); + > .pay-amount { + display: inline; + float: right; + font-weight: 700; } + + > .mode-of-payment-control { + display: none; + align-items: center; + margin-top: var(--margin-sm); + margin-bottom: var(--margin-xs); + } + + > .loyalty-amount-name { + display: none; + float: right; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + > .cash-shortcuts { + display: none; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--margin-sm); + font-size: var(--text-sm); + text-align: center; + + > .shortcut { + @extend .pointer-no-select; + border-radius: var(--border-radius-sm); + background-color: var(--control-bg); + font-weight: 500; + padding: var(--padding-xs) var(--padding-sm); + transition: all 0.15s ease-in-out; + + &:hover { + background-color: var(--control-bg); + } + } + } + } + + > .loyalty-card { + display: flex; + flex-direction: column; } } } - - > .loyalty-card { - display: flex; - flex-direction: column; - } } - } - > .fields-numpad-container { - display: flex; - flex: 1; - height: 100%; - position: relative; - justify-content: flex-end; - - > .fields-section { - flex: 1; + > .payment-container-right { display: flex; flex-direction: column; width: 50%; - height: 100%; - padding-bottom: var(--margin-md); - .invoice-fields { - overflow-y: scroll; + .fields-numpad-container { + display: flex; + flex-direction: column; + flex: 1; height: 100%; - padding-right: var(--padding-sm); - } - } + position: relative; + justify-content: flex-end; - > .number-pad { - flex: 1; - display: flex; - justify-content: flex-end; - align-items: flex-end; - max-width: 50%; - - .numpad-container { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: var(--margin-md); - margin-bottom: var(--margin-md); - - > .numpad-btn { - @extend .pointer-no-select; - border-radius: var(--border-radius-md); + > .fields-section { + flex: 1; display: flex; - align-items: center; - justify-content: center; - padding: var(--padding-md); - box-shadow: var(--shadow-sm); + flex-direction: column; + padding-left: var(--margin-md); + + .invoice-fields { + height: 100%; + margin-left: auto; + padding: var(--padding-sm); + } + } + + .number-pad { + position: absolute; + z-index: 4; + right: 0px; + flex: 1; + display: flex; + align-items: flex-end; + + .numpad-container { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--margin-md); + margin-bottom: var(--margin-md); + background-color: var(--fg-color); + border-radius: var(--border-radius-md); + padding: var(--padding-sm); + + > .numpad-btn { + @extend .pointer-no-select; + border-radius: var(--border-radius-md); + display: flex; + align-items: center; + justify-content: center; + padding: var(--padding-md); + box-shadow: var(--shadow-base); + } + } } } } diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index c67ca67dac2..9cd61de95af 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -156,6 +156,28 @@ erpnext.PointOfSale.Controller = class { }, }); + this.fetch_invoice_fields(); + this.setup_listener_for_pos_closing(); + } + + fetch_invoice_fields() { + const me = this; + frappe.db.get_doc("POS Settings", undefined).then((doc) => { + me.settings.invoice_fields = doc.invoice_fields.map((field) => { + return { + fieldname: field.fieldname, + label: field.label, + fieldtype: field.fieldtype, + reqd: field.reqd, + options: field.options, + default_value: field.default_value, + read_only: field.read_only, + }; + }); + }); + } + + setup_listener_for_pos_closing() { frappe.realtime.on(`poe_${this.pos_opening}_closed`, (data) => { const route = frappe.get_route_str(); if (data && route == "point-of-sale") { @@ -426,6 +448,7 @@ erpnext.PointOfSale.Controller = class { init_payments() { this.payment = new erpnext.PointOfSale.Payment({ wrapper: this.$components_wrapper, + settings: this.settings, events: { get_frm: () => this.frm || {}, diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 3d70a63b579..f6a4fcbf509 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -171,6 +171,9 @@ erpnext.PointOfSale.ItemCart = class { me.toggle_item_highlight(this); + const scrollTop = $cart_item.offset().top - me.$cart_items_wrapper.offset().top; + me.$cart_items_wrapper.animate({ scrollTop }); + const payment_section_hidden = !me.$totals_section.find(".edit-cart-btn").is(":visible"); if (!payment_section_hidden) { // payment section is visible diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 56ca8b2154c..a0264753621 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -1,8 +1,10 @@ /* eslint-disable no-unused-vars */ erpnext.PointOfSale.Payment = class { - constructor({ events, wrapper }) { + constructor({ events, wrapper, settings }) { this.wrapper = wrapper; this.events = events; + this.set_gt_to_default_mop = settings.set_grand_total_to_default_mop; + this.invoice_fields = settings.invoice_fields; this.init_component(); } @@ -17,14 +19,23 @@ erpnext.PointOfSale.Payment = class { prepare_dom() { this.wrapper.append( `
- -
-
-
- -
+
+
+ +
+
+
+
+
+
+ +
+
+
+
-
@@ -40,48 +51,61 @@ erpnext.PointOfSale.Payment = class { this.$invoice_fields_section = this.$component.find(".fields-section"); } - make_invoice_fields_control() { - this.reqd_invoice_fields = []; - frappe.db.get_doc("POS Settings", undefined).then((doc) => { - const fields = doc.invoice_fields; - if (!fields.length) return; + make_invoice_field_dialog() { + const me = this; + if (!me.invoice_fields.length) return; + me.addl_dlg = new frappe.ui.Dialog({ + title: __("Additional Information"), + fields: me.invoice_fields, + size: "small", + primary_action_label: __("Save"), + primary_action(values) { + me.set_values_to_frm(values); + this.hide(); + }, + }); + me.add_btn_field_click_listener(); + me.set_value_on_dialog_fields(); + me.make_addl_info_dialog_btn_visible(); + } - this.$invoice_fields = this.$invoice_fields_section.find(".invoice-fields"); - this.$invoice_fields.html(""); - const frm = this.events.get_frm(); + set_values_to_frm(values) { + const frm = this.events.get_frm(); + for (const value in values) { + frm.set_value(value, values[value]); + } + frappe.show_alert({ + message: __("Additional Information updated successfully."), + indicator: "green", + }); + } - fields.forEach((df) => { - this.$invoice_fields.append( - `
` - ); - let df_events = { - onchange: function () { - frm.set_value(this.df.fieldname, this.get_value()); - }, - }; - if (df.fieldtype == "Button") { - df_events = { - click: function () { - if (frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype)) { - frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname); - } - }, - }; - } - if (df.reqd && (df.fieldtype !== "Button" || !df.read_only)) { - this.reqd_invoice_fields.push({ fieldname: df.fieldname, label: df.label }); - } - - this[`${df.fieldname}_field`] = frappe.ui.form.make_control({ - df: { - ...df, - ...df_events, - }, - parent: this.$invoice_fields.find(`.${df.fieldname}-field`), - render_input: true, + add_btn_field_click_listener() { + const frm = this.events.get_frm(); + this.addl_dlg.fields.forEach((df) => { + if (df.fieldtype === "Button") { + this.addl_dlg.fields_dict[df.fieldname].$input.on("click", function () { + if (frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype)) { + frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname); + } }); - this[`${df.fieldname}_field`].set_value(frm.doc[df.fieldname]); - }); + } + }); + } + + set_value_on_dialog_fields() { + const doc = this.events.get_frm().doc; + this.addl_dlg.fields.forEach((df) => { + if (doc[df.fieldname] || df.default_value) { + this.addl_dlg.set_value(df.fieldname, doc[df.fieldname] || df.default_value); + } + }); + } + + make_addl_info_dialog_btn_visible() { + this.$invoice_fields_section.find(".addl-fields").removeClass("hidden"); + this.$invoice_fields_section.find(".addl-fields").on("click", () => { + this.addl_dlg.show(); }); } @@ -164,6 +188,16 @@ erpnext.PointOfSale.Payment = class { } }); + frappe.ui.form.on("POS Invoice", "contact_mobile", (frm) => { + const contact = frm.doc.contact_mobile; + const request_button = $(this.request_for_payment_field?.$input[0]); + if (contact) { + request_button.removeClass("btn-default").addClass("btn-primary"); + } else { + request_button.removeClass("btn-primary").addClass("btn-default"); + } + }); + frappe.ui.form.on("POS Invoice", "coupon_code", (frm) => { this.bind_coupon_code_event(frm); }); @@ -355,9 +389,9 @@ erpnext.PointOfSale.Payment = class { render_payment_section() { this.render_payment_mode_dom(); - this.make_invoice_fields_control(); + this.make_invoice_field_dialog(); this.update_totals_section(); - this.unset_grand_total_to_default_mop(); + this.set_grand_total_to_default_mop(); } after_render() { @@ -610,7 +644,7 @@ erpnext.PointOfSale.Payment = class { const remaining = grand_total - doc.paid_amount; const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined; const currency = doc.currency; - const label = __("Change Amount"); + const label = doc.paid_amount > grand_total ? __("Change Amount") : __("Remaining Amount"); this.$totals.html( `
@@ -642,32 +676,28 @@ erpnext.PointOfSale.Payment = class { .toLowerCase(); } - async unset_grand_total_to_default_mop() { - const doc = this.events.get_frm().doc; - let r = await frappe.db.get_value( - "POS Profile", - doc.pos_profile, - "disable_grand_total_to_default_mop" - ); - - if (!r.message.disable_grand_total_to_default_mop) { + set_grand_total_to_default_mop() { + if (this.set_gt_to_default_mop) { this.focus_on_default_mop(); } } validate_reqd_invoice_fields() { const doc = this.events.get_frm().doc; - let validation_flag = true; - for (let field of this.reqd_invoice_fields) { - if (!doc[field.fieldname]) { - validation_flag = false; + for (const df of this.addl_dlg.fields) { + if (df.reqd && !doc[df.fieldname]) { frappe.show_alert({ - message: __("{0} is a mandatory field.", [field.label]), - indicator: "orange", + message: __( + "Invoice cannot be submitted without filling the mandatory Additional Information fields." + ), + indicator: "red", }); frappe.utils.play_sound("error"); + this.addl_dlg.show(); + this.addl_dlg.fields_dict[df.fieldname].$input.focus(); + return false; } } - return validation_flag; + return true; } }; From 54e4e7918ea54d145c732fa176eb19f42d4313d2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 12 May 2025 16:30:24 +0530 Subject: [PATCH 30/35] refactor(test): don't default to accumulate --- .../profit_and_loss_statement/test_profit_and_loss_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py index c2bb11b8f47..038f78ca2f3 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py @@ -58,7 +58,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, IntegrationTestCase): period_end_date=fy.year_end_date, filter_based_on="Fiscal Year", periodicity="Monthly", - accumulated_values=True, + accumulated_values=False, ) def test_profit_and_loss_output_and_summary(self): From a50251401fab047462c2b6b27f0a9441a5b80c15 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 12 May 2025 17:58:56 +0530 Subject: [PATCH 31/35] perf: move all hourly/daily jobs to maintenance queue (#47504) None of them need to strictly happen at 00:00 or *:00, so moving them all to maintenance queue which executes with same frequency but spaced out. --- erpnext/hooks.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 171261f78b5..b8d9f97d625 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -408,29 +408,29 @@ scheduler_events = { "0/15 * * * *": [ "erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs", ], - "0/30 * * * *": [ - "erpnext.utilities.doctype.video.video.update_youtube_data", - ], + "0/30 * * * *": [], # Hourly but offset by 30 minutes "30 * * * *": [ "erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs", ], # Daily but offset by 45 minutes - "45 0 * * *": [ - "erpnext.stock.reorder_item.reorder_item", - ], + "45 0 * * *": [], }, "hourly": [ - "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", - "erpnext.projects.doctype.project.project.project_status_update_reminder", "erpnext.projects.doctype.project.project.hourly_reminder", - "erpnext.projects.doctype.project.project.collect_project_status", ], - "hourly_long": [ + "hourly_long": [], + "hourly_maintenance": [ "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", "erpnext.utilities.bulk_transaction.retry", + "erpnext.projects.doctype.project.project.collect_project_status", + "erpnext.projects.doctype.project.project.project_status_update_reminder", + "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", + "erpnext.utilities.doctype.video.video.update_youtube_data", ], - "daily": [ + "daily": [], + "daily_long": [], + "daily_maintenance": [ "erpnext.support.doctype.issue.issue.auto_close_tickets", "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.controllers.accounts_controller.update_invoice_status", @@ -454,17 +454,16 @@ scheduler_events = { "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily", "erpnext.accounts.utils.run_ledger_health_checks", "erpnext.assets.doctype.asset_maintenance_log.asset_maintenance_log.update_asset_maintenance_log_status", - ], - "weekly": [ - "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly", - ], - "daily_long": [ + "erpnext.stock.reorder_item.reorder_item", "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process", "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms", "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", "erpnext.assets.doctype.asset.depreciation.post_depreciation_entries", ], + "weekly": [ + "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly", + ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly", From 963d1e502eb70b7867651c9ffc00892fef2d0221 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 12 May 2025 23:21:57 +0530 Subject: [PATCH 32/35] fix: not able to reserve extra items against the work order (#47511) --- .../doctype/work_order/work_order.py | 51 +++++++++++++++---- .../work_order_item/work_order_item.json | 26 ++++++++-- .../work_order_item/work_order_item.py | 4 +- .../work_order_consumed_materials.py | 4 ++ .../stock/doctype/stock_entry/stock_entry.py | 5 ++ 5 files changed, 77 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 0aaaf78f50c..58ac314f209 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1362,13 +1362,6 @@ class WorkOrder(Document): ) ) - if details.reserved_qty < details.transferred_qty: - frappe.throw( - _("Transferred Qty {0} cannot be greater than Reserved Qty {1} for item {2}").format( - details.transferred_qty, details.reserved_qty, item.item_code - ) - ) - @frappe.whitelist() def make_bom(self): data = frappe.db.sql( @@ -1414,7 +1407,7 @@ class WorkOrder(Document): def get_list_of_materials_for_reservation(self, stock_entry): items = frappe._dict() - vocher_detail_no = {d.item_code: d.name for d in self.required_items} + voucher_detail_no = {d.item_code: d.name for d in self.required_items} for row in stock_entry.items: if row.item_code not in items: @@ -1422,7 +1415,7 @@ class WorkOrder(Document): { "voucher_no": self.name, "voucher_type": self.doctype, - "voucher_detail_no": vocher_detail_no.get(row.item_code), + "voucher_detail_no": voucher_detail_no.get(row.item_code), "item_code": row.item_code, "warehouse": row.t_warehouse, "stock_qty": row.transfer_qty, @@ -1590,6 +1583,46 @@ class WorkOrder(Document): if sre_list: cancel_stock_reservation_entries(self, sre_list) + def remove_additional_items(self, stock_entry): + for row in stock_entry.items: + for item in self.required_items: + if row.item_code == item.item_code and row.name == item.voucher_detail_reference: + item.delete() + + def add_additional_items(self, stock_entry): + if stock_entry.purpose != "Material Transfer for Manufacture": + return + + required_items = [d.item_code for d in self.required_items] + + additional_items = frappe._dict() + for row in stock_entry.items: + if row.item_code not in required_items: + additional_items.setdefault(row.item_code, []).append(row) + + for item_code, rows in additional_items.items(): + for row in rows: + child_row = self.append( + "required_items", + { + "item_code": item_code, + "source_warehouse": row.s_warehouse, + "item_name": row.item_name, + "required_qty": row.transfer_qty, + "stock_uom": row.stock_uom, + "rate": row.basic_rate, + "amount": row.amount, + "description": row.description, + "docstatus": 1, + "is_additional_item": 1, + "voucher_detail_reference": row.name, + }, + ) + + child_row.insert() + + stock_entry.reload() + @frappe.whitelist() def make_stock_reservation_entries(doc, items=None, table_name=None, notify=False): diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index f74f60574de..81de6619422 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -27,7 +27,9 @@ "available_qty_at_source_warehouse", "available_qty_at_wip_warehouse", "column_break_jash", - "stock_reserved_qty" + "stock_reserved_qty", + "is_additional_item", + "voucher_detail_reference" ], "fields": [ { @@ -172,19 +174,37 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_additional_item", + "fieldtype": "Check", + "label": "Is Additional Item", + "no_copy": 1, + "read_only": 1 + }, + { + "depends_on": "is_additional_item", + "fieldname": "voucher_detail_reference", + "fieldtype": "Data", + "label": "Voucher Detail Reference", + "no_copy": 1, + "read_only": 1 } ], + "grid_page_length": 50, "istable": 1, "links": [], - "modified": "2024-11-20 15:48:16.823384", + "modified": "2025-05-12 17:36:00.115181", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", "owner": "Administrator", "permissions": [], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.py b/erpnext/manufacturing/doctype/work_order_item/work_order_item.py index 9bc62a8a832..6f48de25358 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.py +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.py @@ -22,6 +22,7 @@ class WorkOrderItem(Document): consumed_qty: DF.Float description: DF.Text | None include_item_in_manufacturing: DF.Check + is_additional_item: DF.Check item_code: DF.Link | None item_name: DF.Data | None operation: DF.Link | None @@ -33,9 +34,10 @@ class WorkOrderItem(Document): required_qty: DF.Float returned_qty: DF.Float source_warehouse: DF.Link | None - stock_uom: DF.Link | None stock_reserved_qty: DF.Float + stock_uom: DF.Link | None transferred_qty: DF.Float + voucher_detail_reference: DF.Data | None # end: auto-generated types pass diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py index a5690c477e4..adcdcb9a094 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -29,6 +29,9 @@ def get_data(report_filters): if d.consumed_qty and d.consumed_qty > d.required_qty: d.extra_consumed_qty = d.consumed_qty - d.required_qty + if d.is_additional_item: + d.extra_consumed_qty = d.consumed_qty + if d.extra_consumed_qty or not report_filters.show_extra_consumed_materials: wo_items.setdefault((d.name, d.production_item), []).append(d) @@ -81,6 +84,7 @@ def get_fields(): "`tabWork Order Item`.`required_qty`", "`tabWork Order Item`.`transferred_qty`", "`tabWork Order Item`.`consumed_qty`", + "`tabWork Order Item`.`is_additional_item`", "`tabWork Order`.`status`", "`tabWork Order`.`name`", "`tabWork Order`.`production_item`", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b8f23be201e..d15f1943895 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1635,6 +1635,11 @@ class StockEntry(StockController): _validate_work_order(pro_doc) if self.fg_completed_qty: + if self.docstatus == 1: + pro_doc.add_additional_items(self) + else: + pro_doc.remove_additional_items(self) + pro_doc.run_method("update_work_order_qty") if self.purpose == "Manufacture": pro_doc.run_method("update_planned_qty") From 898b5595a1e9850fe266c73011fe644dcb5986de Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 12 May 2025 23:29:19 +0530 Subject: [PATCH 33/35] fix: Persian translations --- erpnext/locale/fa.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/locale/fa.po b/erpnext/locale/fa.po index b7e23d8cdee..e8e2dea27fc 100644 --- a/erpnext/locale/fa.po +++ b/erpnext/locale/fa.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2025-05-04 09:35+0000\n" -"PO-Revision-Date: 2025-05-07 17:29\n" +"PO-Revision-Date: 2025-05-12 17:59\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Persian\n" "MIME-Version: 1.0\n" @@ -19170,7 +19170,7 @@ msgstr "لاگ خطا" #. Label of the error_message (Text) field in DocType 'Period Closing Voucher' #: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json msgid "Error Message" -msgstr "پیغام خطا" +msgstr "پیام خطا" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:274 msgid "Error Occurred" @@ -27622,7 +27622,7 @@ msgstr "ثبت دفتر روزنامه {0} دارای حساب {1} نیست یا #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:97 msgid "Journal entries have been created" -msgstr "" +msgstr "ثبت‌های دفتر روزنامه ایجاد شده است" #. Label of the journals_section (Section Break) field in DocType 'Accounts #. Settings' @@ -27762,7 +27762,7 @@ msgstr "" #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json msgid "Knot" -msgstr "" +msgstr "گره دریایی" #. Option for the 'Valuation Method' (Select) field in DocType 'Item' #. Option for the 'Default Valuation Method' (Select) field in DocType 'Stock @@ -30176,7 +30176,7 @@ msgstr "نمونه های پیام" #: erpnext/accounts/doctype/payment_request/payment_request.js:47 #: erpnext/setup/doctype/email_digest/email_digest.js:26 msgid "Message Sent" -msgstr "پیغام فرستاده شد" +msgstr "پیام فرستاده شد" #. Label of the message_for_supplier (Text Editor) field in DocType 'Request #. for Quotation' @@ -38725,7 +38725,7 @@ msgstr "احتمال (%)" #: erpnext/manufacturing/doctype/workstation/workstation.json #: erpnext/quality_management/doctype/quality_action_resolution/quality_action_resolution.json msgid "Problem" -msgstr "مسئله" +msgstr "مشکل" #. Label of the procedure (Link) field in DocType 'Non Conformance' #. Label of the procedure (Link) field in DocType 'Quality Action' @@ -48478,7 +48478,7 @@ msgstr "مستقر شده" #. Option for the 'Status' (Select) field in DocType 'Workstation' #: erpnext/manufacturing/doctype/workstation/workstation.json msgid "Setup" -msgstr "تنظیمات" +msgstr "راه‌اندازی" #: erpnext/public/js/setup_wizard.js:18 msgid "Setup your organization" From e4e0bb68ecc3ff2dce679481051ff53c0a77dcd1 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com> Date: Tue, 13 May 2025 11:26:21 +0530 Subject: [PATCH 34/35] Merge pull request #47367 from Abdeali099/gl-report-field-float-to-currency fix: Use `Currency` instead of `Float` in GL report to show details --- .../report/general_ledger/general_ledger.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index d7e0264093f..48193c87822 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -587,17 +587,20 @@ def get_account_type_map(company): def get_result_as_list(data, filters): - balance, _balance_in_account_currency = 0, 0 + balance = 0 for d in data: if not d.get("posting_date"): - balance, _balance_in_account_currency = 0, 0 + balance = 0 balance = get_balance(d, balance, "debit", "credit") + d["balance"] = balance d["account_currency"] = filters.account_currency + d["presentation_currency"] = filters.presentation_currency + return data @@ -623,11 +626,8 @@ def get_columns(filters): if filters.get("presentation_currency"): currency = filters["presentation_currency"] else: - if filters.get("company"): - currency = get_company_currency(filters["company"]) - else: - company = get_default_company() - currency = get_company_currency(company) + company = filters.get("company") or get_default_company() + filters["presentation_currency"] = currency = get_company_currency(company) columns = [ { @@ -648,19 +648,22 @@ def get_columns(filters): { "label": _("Debit ({0})").format(currency), "fieldname": "debit", - "fieldtype": "Float", + "fieldtype": "Currency", + "options": "presentation_currency", "width": 130, }, { "label": _("Credit ({0})").format(currency), "fieldname": "credit", - "fieldtype": "Float", + "fieldtype": "Currency", + "options": "presentation_currency", "width": 130, }, { "label": _("Balance ({0})").format(currency), "fieldname": "balance", - "fieldtype": "Float", + "fieldtype": "Currency", + "options": "presentation_currency", "width": 130, }, ] From ded46ce3d899f243dd201a9888bb4aa3c7abfb6e Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 13 May 2025 13:39:01 +0530 Subject: [PATCH 35/35] fix: condition for advance_account assignment --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index d991cb128e0..bf9c4c6ffaf 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2448,7 +2448,7 @@ def get_outstanding_reference_documents(args, validate=False): accounts = get_party_account( args.get("party_type"), args.get("party"), args.get("company"), include_advance=True ) - advance_account = accounts[1] if len(accounts) >= 1 else None + advance_account = accounts[1] if len(accounts) > 1 else None if party_account == advance_account: party_account = accounts[0]