From cd0a25ca174d88dac8cc8a9ae637189ceff8ed66 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Tue, 17 Feb 2026 13:14:31 +0530 Subject: [PATCH 01/90] feat(employee): Create User button and form. (cherry picked from commit 3b521b74ea41622ee20ea0e9725fa92dd66205c2) --- erpnext/setup/doctype/employee/employee.js | 66 ++++++--- erpnext/setup/doctype/employee/employee.json | 9 +- erpnext/setup/doctype/employee/employee.py | 140 ++++++++++++++++++- 3 files changed, 182 insertions(+), 33 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 21d67d70e78..2a525f18f7f 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -45,6 +45,54 @@ frappe.ui.form.on("Employee", { refresh: function (frm) { frm.fields_dict.date_of_birth.datepicker.update({ maxDate: new Date() }); + + if (!frm.is_new() && !frm.doc.user_id) { + frm.add_custom_button(__("Create User"), () => { + const dialog = new frappe.ui.Dialog({ + title: __("Create User"), + fields: [ + { + fieldtype: "Data", + fieldname: "email", + label: __("Email"), + reqd: 1, + default: frm.doc.company_email || frm.doc.personal_email || frm.doc.user_id, + }, + { + fieldtype: "Check", + fieldname: "create_user_permission", + label: __("Create User Permission"), + default: 0, + }, + ], + primary_action_label: __("Create"), + primary_action: (values) => { + if (!values.email) { + frappe.msgprint(__("Email is required to create a user.")); + return; + } + + frappe + .call({ + method: "erpnext.setup.doctype.employee.employee.create_user", + args: { + employee: frm.doc.name, + email: values.email, + create_user_permission: values.create_user_permission ? 1 : 0, + }, + freeze: true, + freeze_message: __("Creating User..."), + }) + .then(() => { + dialog.hide(); + frm.reload_doc(); + }); + }, + }); + + dialog.show(); + }); + } }, prefered_contact_email: function (frm) { @@ -77,24 +125,6 @@ frappe.ui.form.on("Employee", { }, }); }, - - create_user: function (frm) { - if (!frm.doc.prefered_email) { - frappe.throw(__("Please enter Preferred Contact Email")); - } - frappe.call({ - method: "erpnext.setup.doctype.employee.employee.create_user", - args: { - employee: frm.doc.name, - email: frm.doc.prefered_email, - }, - freeze: true, - freeze_message: __("Creating User..."), - callback: function (r) { - frm.reload_doc(); - }, - }); - }, }); cur_frm.cscript = new erpnext.setup.EmployeeController({ diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index e663913e8d7..4069813e318 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -28,7 +28,6 @@ "status", "erpnext_user", "user_id", - "create_user", "create_user_permission", "company_details_section", "company", @@ -285,12 +284,6 @@ "label": "User ID", "options": "User" }, - { - "depends_on": "eval:(!doc.user_id)", - "fieldname": "create_user", - "fieldtype": "Button", - "label": "Create User" - }, { "default": "1", "depends_on": "user_id", @@ -823,7 +816,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2025-08-29 11:52:12.819878", + "modified": "2026-02-16 13:06:01.752904", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 1ecbb4b9ac7..48819a70ef4 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -8,7 +8,7 @@ from frappe.permissions import ( get_doc_permissions, remove_user_permission, ) -from frappe.utils import cstr, getdate, today, validate_email_address +from frappe.utils import cint, cstr, getdate, today, validate_email_address from frappe.utils.nestedset import NestedSet from erpnext.utilities.transaction_base import delete_events @@ -23,6 +23,93 @@ class InactiveEmployeeStatusError(frappe.ValidationError): class Employee(NestedSet): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.setup.doctype.employee_education.employee_education import EmployeeEducation + from erpnext.setup.doctype.employee_external_work_history.employee_external_work_history import ( + EmployeeExternalWorkHistory, + ) + from erpnext.setup.doctype.employee_internal_work_history.employee_internal_work_history import ( + EmployeeInternalWorkHistory, + ) + + attendance_device_id: DF.Data | None + bank_ac_no: DF.Data | None + bank_name: DF.Data | None + bio: DF.TextEditor | None + blood_group: DF.Literal["", "A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"] + branch: DF.Link | None + cell_number: DF.Data | None + company: DF.Link + company_email: DF.Data | None + contract_end_date: DF.Date | None + create_user_permission: DF.Check + ctc: DF.Currency + current_accommodation_type: DF.Literal["", "Rented", "Owned"] + current_address: DF.SmallText | None + date_of_birth: DF.Date + date_of_issue: DF.Date | None + date_of_joining: DF.Date + date_of_retirement: DF.Date | None + department: DF.Link | None + designation: DF.Link | None + education: DF.Table[EmployeeEducation] + emergency_phone_number: DF.Data | None + employee: DF.Data | None + employee_name: DF.Data | None + employee_number: DF.Data | None + encashment_date: DF.Date | None + external_work_history: DF.Table[EmployeeExternalWorkHistory] + family_background: DF.SmallText | None + feedback: DF.SmallText | None + final_confirmation_date: DF.Date | None + first_name: DF.Data + gender: DF.Link + health_details: DF.SmallText | None + held_on: DF.Date | None + holiday_list: DF.Link | None + iban: DF.Data | None + image: DF.AttachImage | None + internal_work_history: DF.Table[EmployeeInternalWorkHistory] + last_name: DF.Data | None + leave_encashed: DF.Literal["", "Yes", "No"] + lft: DF.Int + marital_status: DF.Literal["", "Single", "Married", "Divorced", "Widowed"] + middle_name: DF.Data | None + naming_series: DF.Literal["HR-EMP-"] + new_workplace: DF.Data | None + notice_number_of_days: DF.Int + old_parent: DF.Data | None + passport_number: DF.Data | None + permanent_accommodation_type: DF.Literal["", "Rented", "Owned"] + permanent_address: DF.SmallText | None + person_to_be_contacted: DF.Data | None + personal_email: DF.Data | None + place_of_issue: DF.Data | None + prefered_contact_email: DF.Literal["", "Company Email", "Personal Email", "User ID"] + prefered_email: DF.Data | None + reason_for_leaving: DF.SmallText | None + relation: DF.Data | None + relieving_date: DF.Date | None + reports_to: DF.Link | None + resignation_letter_date: DF.Date | None + rgt: DF.Int + salary_currency: DF.Link | None + salary_mode: DF.Literal["", "Bank", "Cash", "Cheque"] + salutation: DF.Link | None + scheduled_confirmation_date: DF.Date | None + status: DF.Literal["Active", "Inactive", "Suspended", "Left"] + unsubscribed: DF.Check + user_id: DF.Link | None + valid_upto: DF.Date | None + # end: auto-generated types + nsm_parent_field = "reports_to" def autoname(self): @@ -310,9 +397,28 @@ def deactivate_sales_person(status=None, employee=None): @frappe.whitelist() -def create_user(employee, user=None, email=None): +def create_user(employee, user=None, email=None, create_user_permission=0): + if not employee: + frappe.throw(_("Employee is required")) + emp = frappe.get_doc("Employee", employee) + if email: + email = cstr(email).strip().lower() + else: + email = emp.prefered_email + + if not email: + frappe.throw(_("Email is required to create a user")) + + validate_email_address(email, True) + + if emp.user_id: + frappe.throw(_("Employee {0} already has a linked user").format(emp.name)) + + if frappe.db.exists("User", email): + frappe.throw(_("User {0} already exists").format(email)) + employee_name = emp.employee_name.split(" ") middle_name = last_name = "" @@ -324,14 +430,14 @@ def create_user(employee, user=None, email=None): first_name = employee_name[0] - if email: - emp.prefered_email = email + frappe.db.set_value("Employee", emp.name, "user_id", email, update_modified=False) + frappe.db.commit() user = frappe.new_doc("User") user.update( { "name": emp.employee_name, - "email": emp.prefered_email, + "email": email, "enabled": 1, "first_name": first_name, "middle_name": middle_name, @@ -340,11 +446,31 @@ def create_user(employee, user=None, email=None): "birth_date": emp.date_of_birth, "phone": emp.cell_number, "bio": emp.bio, + "send_welcome_email": 1, } ) - user.insert() - emp.user_id = user.name + user.append_roles("Employee") + user.insert(ignore_permissions=True) + + emp.reload() + emp.company_email = email + if not emp.prefered_contact_email: + emp.prefered_contact_email = "Company Email" emp.save() + + if cint(create_user_permission): + if not frappe.db.exists( + "User Permission", + {"allow": "Employee", "for_value": emp.name, "user": user.name}, + ): + add_user_permission("Employee", emp.name, user.name) + + if not frappe.db.exists( + "User Permission", + {"allow": "Company", "for_value": emp.company, "user": user.name}, + ): + add_user_permission("Company", emp.company, user.name) + return user.name From 8f8b48746baf0572a39c7a5f2272bd8b75131f50 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Thu, 19 Feb 2026 15:51:53 +0530 Subject: [PATCH 02/90] feat(employee): Add automatic user creation feature and related validations. Create User on Import. (cherry picked from commit 57f3048d2739479de9448ee347a421f306c5b558) --- erpnext/setup/doctype/employee/employee.json | 19 ++++++++++++++-- erpnext/setup/doctype/employee/employee.py | 24 +++++++++++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 4069813e318..94dca869eda 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -29,6 +29,8 @@ "erpnext_user", "user_id", "create_user_permission", + "column_break_xwnm", + "create_user_automatically", "company_details_section", "company", "department", @@ -286,12 +288,20 @@ }, { "default": "1", - "depends_on": "user_id", + "depends_on": "eval:doc.user_id || doc.create_user_automatically", "description": "This will restrict user access to other employee records", "fieldname": "create_user_permission", "fieldtype": "Check", "label": "Create User Permission" }, + { + "default": "0", + "depends_on": "eval:doc.__islocal", + "description": "This will create User for this employee depending on the Company Email.", + "fieldname": "create_user_automatically", + "fieldtype": "Check", + "label": "Create User Automatically" + }, { "allow_in_quick_entry": 1, "collapsible": 1, @@ -447,6 +457,7 @@ "fieldname": "company_email", "fieldtype": "Data", "label": "Company Email", + "mandatory_depends_on": "create_user_automatically", "oldfieldname": "company_email", "oldfieldtype": "Data", "options": "Email" @@ -809,6 +820,10 @@ "fieldtype": "Data", "label": "IBAN", "options": "IBAN" + }, + { + "fieldname": "column_break_xwnm", + "fieldtype": "Column Break" } ], "icon": "fa fa-user", @@ -816,7 +831,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2026-02-16 13:06:01.752904", + "modified": "2026-02-19 12:48:22.080419", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 48819a70ef4..cb5e2504b3e 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -49,6 +49,7 @@ class Employee(NestedSet): company: DF.Link company_email: DF.Data | None contract_end_date: DF.Date | None + create_user_automatically: DF.Check create_user_permission: DF.Check ctc: DF.Currency current_accommodation_type: DF.Literal["", "Rented", "Owned"] @@ -125,6 +126,7 @@ class Employee(NestedSet): self.set_employee_name() self.validate_date() self.validate_email() + self.validate_auto_user_creation() self.validate_status() self.validate_reports_to() self.set_preferred_email() @@ -159,6 +161,10 @@ class Employee(NestedSet): self.validate_for_enabled_user_id(data.get("enabled", 0)) self.validate_duplicate_user_id() + def validate_auto_user_creation(self): + if self.create_user_automatically and not self.company_email: + frappe.throw(_("Email is mandatory when Create User Automatically is enabled")) + def update_nsm_model(self): frappe.utils.nestedset.update_nsm(self) @@ -170,6 +176,19 @@ class Employee(NestedSet): self.update_user_permissions() self.reset_employee_emails_cache() + def after_insert(self): + if not self.create_user_automatically: + return + + if self.user_id: + return + + create_user( + employee=self.name, + email=self.company_email, + create_user_permission=self.create_user_permission, + ) + def update_user_permissions(self): if not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission"): return @@ -406,7 +425,7 @@ def create_user(employee, user=None, email=None, create_user_permission=0): if email: email = cstr(email).strip().lower() else: - email = emp.prefered_email + email = emp.company_email if not email: frappe.throw(_("Email is required to create a user")) @@ -436,7 +455,7 @@ def create_user(employee, user=None, email=None, create_user_permission=0): user = frappe.new_doc("User") user.update( { - "name": emp.employee_name, + "name": email, "email": email, "enabled": 1, "first_name": first_name, @@ -446,7 +465,6 @@ def create_user(employee, user=None, email=None, create_user_permission=0): "birth_date": emp.date_of_birth, "phone": emp.cell_number, "bio": emp.bio, - "send_welcome_email": 1, } ) user.append_roles("Employee") From f2c4a8b1c4c2fa40af3bc416c6b9b8a4d277a8fd Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Thu, 19 Feb 2026 17:00:35 +0530 Subject: [PATCH 03/90] refactor(employee): create user function -removed useless function calls (cherry picked from commit 6513185cb7e5a2b757bfe09478bb9ee0604055a0) --- erpnext/setup/doctype/employee/employee.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index cb5e2504b3e..3f1ca36c675 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -449,9 +449,6 @@ def create_user(employee, user=None, email=None, create_user_permission=0): first_name = employee_name[0] - frappe.db.set_value("Employee", emp.name, "user_id", email, update_modified=False) - frappe.db.commit() - user = frappe.new_doc("User") user.update( { @@ -467,8 +464,9 @@ def create_user(employee, user=None, email=None, create_user_permission=0): "bio": emp.bio, } ) + frappe.db.set_value("Employee", emp.name, "user_id", email) user.append_roles("Employee") - user.insert(ignore_permissions=True) + user.insert() emp.reload() emp.company_email = email @@ -477,17 +475,8 @@ def create_user(employee, user=None, email=None, create_user_permission=0): emp.save() if cint(create_user_permission): - if not frappe.db.exists( - "User Permission", - {"allow": "Employee", "for_value": emp.name, "user": user.name}, - ): - add_user_permission("Employee", emp.name, user.name) - - if not frappe.db.exists( - "User Permission", - {"allow": "Company", "for_value": emp.company, "user": user.name}, - ): - add_user_permission("Company", emp.company, user.name) + add_user_permission("Employee", emp.name, user.name) + add_user_permission("Company", emp.company, user.name) return user.name From b0145512edf4912ce85c5a749a0c406c13c89e78 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Thu, 19 Feb 2026 17:14:50 +0530 Subject: [PATCH 04/90] refactor(employee): reorganize joining and employee exit tabs at the end. (cherry picked from commit 870254b7104d33b5a04aea34093845274452f6bf) --- erpnext/setup/doctype/employee/employee.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 94dca869eda..1d717b2b4c6 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -40,14 +40,6 @@ "reports_to", "column_break_18", "branch", - "employment_details", - "scheduled_confirmation_date", - "column_break_32", - "final_confirmation_date", - "contract_end_date", - "col_break_22", - "notice_number_of_days", - "date_of_retirement", "contact_details", "cell_number", "column_break_40", @@ -118,6 +110,14 @@ "lft", "rgt", "old_parent", + "employment_details", + "scheduled_confirmation_date", + "column_break_32", + "final_confirmation_date", + "contract_end_date", + "col_break_22", + "notice_number_of_days", + "date_of_retirement", "connections_tab" ], "fields": [ @@ -831,7 +831,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2026-02-19 12:48:22.080419", + "modified": "2026-02-19 17:07:42.691107", "modified_by": "Administrator", "module": "Setup", "name": "Employee", From 0b3c9120c36aed755e496925f2e05527fe96176f Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Wed, 25 Feb 2026 13:21:06 +0530 Subject: [PATCH 05/90] feat(employee): Add birthdays and work anniversaries indicator in form ,list view enhancements and new empty state. (cherry picked from commit 4f43f655cfce7d86c07b9da5ed1c572fb1aedf18) --- erpnext/setup/doctype/employee/employee.js | 57 +++++++++++++++++++ erpnext/setup/doctype/employee/employee.json | 4 +- .../setup/doctype/employee/employee_list.js | 25 +++++++- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 2a525f18f7f..ed210f8e4fa 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -46,6 +46,8 @@ frappe.ui.form.on("Employee", { refresh: function (frm) { frm.fields_dict.date_of_birth.datepicker.update({ maxDate: new Date() }); + frm.trigger("add_anniversary_indicator"); + if (!frm.is_new() && !frm.doc.user_id) { frm.add_custom_button(__("Create User"), () => { const dialog = new frappe.ui.Dialog({ @@ -95,6 +97,61 @@ frappe.ui.form.on("Employee", { } }, + date_of_birth: function (frm) { + frm.trigger("add_anniversary_indicator"); + }, + + date_of_joining: function (frm) { + frm.trigger("add_anniversary_indicator"); + }, + + add_anniversary_indicator: function (frm) { + if (!frm.sidebar || !frm.sidebar.sidebar) return; + + let $sidebar = frm.sidebar.sidebar; + let $indicator_section = $sidebar.find(".anniversary-indicator-section"); + + if (!$indicator_section.length) { + $indicator_section = $(` + + `).insertAfter($sidebar.find(".sidebar-meta-details")); + } + + let content = ""; + let today = moment().startOf("day"); + + if (frm.doc.date_of_birth) { + let dob = moment(frm.doc.date_of_birth); + if (dob.date() === today.date() && dob.month() === today.month()) { + content += `
${__( + "Today is their Birthday!" + )}
`; + } + } + + if (frm.doc.date_of_joining) { + let doj = moment(frm.doc.date_of_joining); + if (doj.date() === today.date() && doj.month() === today.month()) { + let years = today.year() - doj.year(); + if (years > 0) { + content += `
${__( + "Today is their {0} Year Work Anniversary!", + [years] + )}
`; + } + } + } + + if (content) { + $indicator_section.find(".anniversary-content").html(content); + $indicator_section.show(); + } else { + $indicator_section.hide(); + } + }, + prefered_contact_email: function (frm) { frm.events.update_contact(frm); }, diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 1d717b2b4c6..b2176bd55e9 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -351,6 +351,7 @@ { "fieldname": "department", "fieldtype": "Link", + "in_list_view": 1, "in_standard_filter": 1, "label": "Department", "oldfieldname": "department", @@ -380,6 +381,7 @@ { "fieldname": "branch", "fieldtype": "Link", + "in_list_view": 1, "label": "Branch", "oldfieldname": "branch", "oldfieldtype": "Link", @@ -831,7 +833,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2026-02-19 17:07:42.691107", + "modified": "2026-02-25 11:23:10.689232", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/setup/doctype/employee/employee_list.js b/erpnext/setup/doctype/employee/employee_list.js index b50eb381c95..33856414537 100644 --- a/erpnext/setup/doctype/employee/employee_list.js +++ b/erpnext/setup/doctype/employee/employee_list.js @@ -1,11 +1,34 @@ frappe.listview_settings["Employee"] = { add_fields: ["status", "branch", "department", "designation", "image"], filters: [["status", "=", "Active"]], - get_indicator: function (doc) { + get_indicator(doc) { return [ __(doc.status, null, "Employee"), { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status], "status,=," + doc.status, ]; }, + + onload(listview) { + listview.get_no_result_message = () => { + return ` +
+
+ + + +
+

${__("No Active Employees Found. Prefer importing if you have many records.")}

+

+ + +

+
+ `; + }; + }, }; From b115913fc9ebc23bb55197c4a78cbd567c1e3dc0 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Fri, 27 Feb 2026 12:53:44 +0530 Subject: [PATCH 06/90] fix: add missing type hints to whitelisted function arguments (cherry picked from commit 124ec4d3c239cdc3a71166f0b080882c984d07a7) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- erpnext/setup/doctype/employee/employee.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 30b3968fc80..3dc32ef4dab 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -680,7 +680,7 @@ class ProductionPlan(Document): frappe.delete_doc("Work Order", d.name) @frappe.whitelist() - def set_status(self, close=None, update_bin=False): + def set_status(self, close: bool | None = None, update_bin: bool = False) -> None: self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus) if close: diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 3f1ca36c675..7b93e11a354 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -416,7 +416,9 @@ def deactivate_sales_person(status=None, employee=None): @frappe.whitelist() -def create_user(employee, user=None, email=None, create_user_permission=0): +def create_user( + employee: str, user: str | None = None, email: str | None = None, create_user_permission: int = 0 +) -> str: if not employee: frappe.throw(_("Employee is required")) From eadf78d69424e2fb64d0ef7c944e79d1ad9b6b64 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Mon, 9 Mar 2026 11:12:12 +0530 Subject: [PATCH 07/90] fix(employee): add 'set_only_once' property to 'Create User Automatically' field (cherry picked from commit 053242d5bd79842094732bd0186610a8d301e3a5) --- erpnext/setup/doctype/employee/employee.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index b2176bd55e9..bfa45a3178f 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -300,7 +300,8 @@ "description": "This will create User for this employee depending on the Company Email.", "fieldname": "create_user_automatically", "fieldtype": "Check", - "label": "Create User Automatically" + "label": "Create User Automatically", + "set_only_once": 1 }, { "allow_in_quick_entry": 1, @@ -833,7 +834,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2026-02-25 11:23:10.689232", + "modified": "2026-03-09 11:06:08.050335", "modified_by": "Administrator", "module": "Setup", "name": "Employee", From c33cd5ce15c2f4a50af15d9925264bd4af264a67 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Mon, 9 Mar 2026 12:50:56 +0530 Subject: [PATCH 08/90] refactor(employee): remove anniversary indicator logic from employee form (cherry picked from commit 1f19175fef68074ee5638faa951dcb3f95393186) --- erpnext/setup/doctype/employee/employee.js | 57 ---------------------- 1 file changed, 57 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index ed210f8e4fa..2a525f18f7f 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -46,8 +46,6 @@ frappe.ui.form.on("Employee", { refresh: function (frm) { frm.fields_dict.date_of_birth.datepicker.update({ maxDate: new Date() }); - frm.trigger("add_anniversary_indicator"); - if (!frm.is_new() && !frm.doc.user_id) { frm.add_custom_button(__("Create User"), () => { const dialog = new frappe.ui.Dialog({ @@ -97,61 +95,6 @@ frappe.ui.form.on("Employee", { } }, - date_of_birth: function (frm) { - frm.trigger("add_anniversary_indicator"); - }, - - date_of_joining: function (frm) { - frm.trigger("add_anniversary_indicator"); - }, - - add_anniversary_indicator: function (frm) { - if (!frm.sidebar || !frm.sidebar.sidebar) return; - - let $sidebar = frm.sidebar.sidebar; - let $indicator_section = $sidebar.find(".anniversary-indicator-section"); - - if (!$indicator_section.length) { - $indicator_section = $(` - - `).insertAfter($sidebar.find(".sidebar-meta-details")); - } - - let content = ""; - let today = moment().startOf("day"); - - if (frm.doc.date_of_birth) { - let dob = moment(frm.doc.date_of_birth); - if (dob.date() === today.date() && dob.month() === today.month()) { - content += `
${__( - "Today is their Birthday!" - )}
`; - } - } - - if (frm.doc.date_of_joining) { - let doj = moment(frm.doc.date_of_joining); - if (doj.date() === today.date() && doj.month() === today.month()) { - let years = today.year() - doj.year(); - if (years > 0) { - content += `
${__( - "Today is their {0} Year Work Anniversary!", - [years] - )}
`; - } - } - } - - if (content) { - $indicator_section.find(".anniversary-content").html(content); - $indicator_section.show(); - } else { - $indicator_section.hide(); - } - }, - prefered_contact_email: function (frm) { frm.events.update_contact(frm); }, From 7414a9a69450112c0ff5d65bba1c1ad083a99009 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 11:27:00 +0530 Subject: [PATCH 09/90] fix: move Joining section before Exit, relabel Employee Exit -> Exit (cherry picked from commit 000b5b72d5891fe8a08436b521155d4760d397b7) --- erpnext/setup/doctype/employee/employee.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index bfa45a3178f..6552e91621f 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -94,6 +94,14 @@ "external_work_history", "history_in_company", "internal_work_history", + "employment_details", + "scheduled_confirmation_date", + "column_break_32", + "final_confirmation_date", + "contract_end_date", + "col_break_22", + "notice_number_of_days", + "date_of_retirement", "exit", "resignation_letter_date", "relieving_date", @@ -110,14 +118,6 @@ "lft", "rgt", "old_parent", - "employment_details", - "scheduled_confirmation_date", - "column_break_32", - "final_confirmation_date", - "contract_end_date", - "col_break_22", - "notice_number_of_days", - "date_of_retirement", "connections_tab" ], "fields": [ @@ -607,7 +607,7 @@ "collapsible": 1, "fieldname": "exit", "fieldtype": "Tab Break", - "label": "Employee Exit", + "label": "Exit", "oldfieldtype": "Section Break" }, { @@ -834,7 +834,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2026-03-09 11:06:08.050335", + "modified": "2026-03-23 11:06:35.539765", "modified_by": "Administrator", "module": "Setup", "name": "Employee", From 341bfb0bd963a9ae3704c706888390c3fcc2af64 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 11:59:05 +0530 Subject: [PATCH 10/90] fix: reset employee listview empty state, add import btn instead (cherry picked from commit d99d16423a3789b254e996a4697a9a9b75865cb7) --- .../setup/doctype/employee/employee_list.js | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee_list.js b/erpnext/setup/doctype/employee/employee_list.js index 33856414537..33cf7225626 100644 --- a/erpnext/setup/doctype/employee/employee_list.js +++ b/erpnext/setup/doctype/employee/employee_list.js @@ -10,25 +10,16 @@ frappe.listview_settings["Employee"] = { }, onload(listview) { - listview.get_no_result_message = () => { - return ` -
-
- - - -
-

${__("No Active Employees Found. Prefer importing if you have many records.")}

-

- - -

-
- `; - }; + if (frappe.perm.has_perm("Employee", 0, "create")) { + frappe.db.count("Employee").then((count) => { + if (count === 0) { + listview.page.add_inner_button(__("Import Employees"), () => { + frappe.new_doc("Data Import", { + reference_doctype: "Employee", + }); + }); + } + }); + } }, }; From 1ddadb72b7a2762bb179fdf141065485b08d6001 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 12:52:23 +0530 Subject: [PATCH 11/90] fix: employee user creation - consider prefered email as default in employee creation - remove unused user parameter from `create_user` API - remove unnecessary validations on user ID, already checked by user doctype hooks - set company email only if empty (cherry picked from commit 613d36a1393b4e94144ad7a975512a3467dec906) --- erpnext/setup/doctype/employee/employee.js | 3 +- erpnext/setup/doctype/employee/employee.py | 33 +++++++--------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 2a525f18f7f..4422c9048d1 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -56,7 +56,8 @@ frappe.ui.form.on("Employee", { fieldname: "email", label: __("Email"), reqd: 1, - default: frm.doc.company_email || frm.doc.personal_email || frm.doc.user_id, + default: + frm.doc.prefered_email || frm.doc.company_email || frm.doc.personal_email, }, { fieldtype: "Check", diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 7b93e11a354..9cf1ce6aa35 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -416,31 +416,19 @@ def deactivate_sales_person(status=None, employee=None): @frappe.whitelist() -def create_user( - employee: str, user: str | None = None, email: str | None = None, create_user_permission: int = 0 -) -> str: - if not employee: - frappe.throw(_("Employee is required")) - +def create_user(employee: str, email: str | None = None, create_user_permission: int = 0) -> str: emp = frappe.get_doc("Employee", employee) - - if email: - email = cstr(email).strip().lower() - else: - email = emp.company_email - - if not email: - frappe.throw(_("Email is required to create a user")) - - validate_email_address(email, True) - if emp.user_id: frappe.throw(_("Employee {0} already has a linked user").format(emp.name)) - if frappe.db.exists("User", email): - frappe.throw(_("User {0} already exists").format(email)) + if not email: + email = emp.company_email + if not email: + frappe.throw(_("Email is required to create a user")) + email = validate_email_address(email, True) employee_name = emp.employee_name.split(" ") + first_name = employee_name[0] middle_name = last_name = "" if len(employee_name) >= 3: @@ -449,12 +437,9 @@ def create_user( elif len(employee_name) == 2: last_name = employee_name[1] - first_name = employee_name[0] - user = frappe.new_doc("User") user.update( { - "name": email, "email": email, "enabled": 1, "first_name": first_name, @@ -471,7 +456,9 @@ def create_user( user.insert() emp.reload() - emp.company_email = email + emp.user_id = user.name + if not emp.company_email: + emp.company_email = email if not emp.prefered_contact_email: emp.prefered_contact_email = "Company Email" emp.save() From 2f13b33e3da4a27645a0e8bc5f0cb67dc3b07020 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 13:37:07 +0530 Subject: [PATCH 12/90] fix: only validate auto user creation before insert (cherry picked from commit ee1aa10328f46fd868fce217831f40f36186764b) --- erpnext/setup/doctype/employee/employee.json | 3 +-- erpnext/setup/doctype/employee/employee.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 6552e91621f..fd9b5e26c3d 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -460,7 +460,6 @@ "fieldname": "company_email", "fieldtype": "Data", "label": "Company Email", - "mandatory_depends_on": "create_user_automatically", "oldfieldname": "company_email", "oldfieldtype": "Data", "options": "Email" @@ -834,7 +833,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2026-03-23 11:06:35.539765", + "modified": "2026-03-23 13:36:13.708549", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 9cf1ce6aa35..879aa80dd2b 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -126,7 +126,6 @@ class Employee(NestedSet): self.set_employee_name() self.validate_date() self.validate_email() - self.validate_auto_user_creation() self.validate_status() self.validate_reports_to() self.set_preferred_email() @@ -162,8 +161,14 @@ class Employee(NestedSet): self.validate_duplicate_user_id() def validate_auto_user_creation(self): - if self.create_user_automatically and not self.company_email: - frappe.throw(_("Email is mandatory when Create User Automatically is enabled")) + if self.create_user_automatically and not (self.prefered_email or self.company_email): + frappe.throw( + _( + "Company Email or Preferred Email is mandatory when 'Create User Automatically' is enabled" + ), + frappe.MandatoryError, + title=_("Auto User Creation Error"), + ) def update_nsm_model(self): frappe.utils.nestedset.update_nsm(self) @@ -176,6 +181,9 @@ class Employee(NestedSet): self.update_user_permissions() self.reset_employee_emails_cache() + def before_insert(self): + self.validate_auto_user_creation() + def after_insert(self): if not self.create_user_automatically: return @@ -185,7 +193,7 @@ class Employee(NestedSet): create_user( employee=self.name, - email=self.company_email, + email=self.prefered_email or self.company_email, create_user_permission=self.create_user_permission, ) From d093b719463685ff7150770ef0a806c98e345a25 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 14:06:24 +0530 Subject: [PATCH 13/90] fix: uncollapse User Details section in new form (cherry picked from commit 1466df91bdc4f439ddc4d45df78f21e84cbc5d41) --- erpnext/setup/doctype/employee/employee.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index fd9b5e26c3d..2a559876388 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -274,6 +274,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval:doc.__islocal", "fieldname": "erpnext_user", "fieldtype": "Section Break", "label": "User Details" @@ -833,7 +834,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2026-03-23 13:36:13.708549", + "modified": "2026-03-23 14:04:26.818864", "modified_by": "Administrator", "module": "Setup", "name": "Employee", From c12ad7910a62481f8df91a95c4353fa48f5000c8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 14:06:56 +0530 Subject: [PATCH 14/90] fix: hide Create User Automatically checkbox if user is already selected (cherry picked from commit ec3302d1c1e97a4a88d84fab472e59bf0255a537) --- erpnext/setup/doctype/employee/employee.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 2a559876388..dbaa1a168f2 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -297,7 +297,7 @@ }, { "default": "0", - "depends_on": "eval:doc.__islocal", + "depends_on": "eval:doc.__islocal && !doc.user_id", "description": "This will create User for this employee depending on the Company Email.", "fieldname": "create_user_automatically", "fieldtype": "Check", @@ -834,7 +834,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2026-03-23 14:04:26.818864", + "modified": "2026-03-23 14:05:42.144641", "modified_by": "Administrator", "module": "Setup", "name": "Employee", From e8ca394e8b3d918f405dacfb5ddfa7586940eddb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 14:23:01 +0530 Subject: [PATCH 15/90] fix: set create user perm to 1 by default + persist option while saving employee (cherry picked from commit 091899d0dfe32e2b09e7a132323665911f33591d) --- erpnext/setup/doctype/employee/employee.js | 2 +- erpnext/setup/doctype/employee/employee.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 4422c9048d1..2c121828034 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -63,7 +63,7 @@ frappe.ui.form.on("Employee", { fieldtype: "Check", fieldname: "create_user_permission", label: __("Create User Permission"), - default: 0, + default: 1, }, ], primary_action_label: __("Create"), diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 879aa80dd2b..b0a74a37c8c 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -459,12 +459,12 @@ def create_user(employee: str, email: str | None = None, create_user_permission: "bio": emp.bio, } ) - frappe.db.set_value("Employee", emp.name, "user_id", email) + emp.db_set("user_id", email) user.append_roles("Employee") user.insert() - emp.reload() emp.user_id = user.name + emp.create_user_permission = cint(create_user_permission) if not emp.company_email: emp.company_email = email if not emp.prefered_contact_email: From 3023302700d5638c45f3a7158ce56feff73179e6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 15:06:15 +0530 Subject: [PATCH 16/90] fix: avoid setting unnecessary fields (cherry picked from commit 97bb10001002d5bceeab42c2e149d8ad484cf322) --- erpnext/setup/doctype/employee/employee.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index b0a74a37c8c..b577b2f0885 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -429,8 +429,6 @@ def create_user(employee: str, email: str | None = None, create_user_permission: if emp.user_id: frappe.throw(_("Employee {0} already has a linked user").format(emp.name)) - if not email: - email = emp.company_email if not email: frappe.throw(_("Email is required to create a user")) @@ -465,10 +463,6 @@ def create_user(employee: str, email: str | None = None, create_user_permission: emp.user_id = user.name emp.create_user_permission = cint(create_user_permission) - if not emp.company_email: - emp.company_email = email - if not emp.prefered_contact_email: - emp.prefered_contact_email = "Company Email" emp.save() if cint(create_user_permission): From 553bc87ac7665dad83ec56eaafa6888da441e63f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 15:31:51 +0530 Subject: [PATCH 17/90] fix: fallback to Personal Email for user creation just like client-side (cherry picked from commit 31af13a5e6fa776041ef9ba2afcfc10c8f46eba2) --- erpnext/setup/doctype/employee/employee.json | 6 +++--- erpnext/setup/doctype/employee/employee.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index dbaa1a168f2..03f68b91dc5 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -43,8 +43,8 @@ "contact_details", "cell_number", "column_break_40", - "personal_email", "company_email", + "personal_email", "column_break4", "prefered_contact_email", "prefered_email", @@ -298,7 +298,7 @@ { "default": "0", "depends_on": "eval:doc.__islocal && !doc.user_id", - "description": "This will create User for this employee depending on the Company Email.", + "description": "Creates a User account for this employee using the Preferred, Company, or Personal email.", "fieldname": "create_user_automatically", "fieldtype": "Check", "label": "Create User Automatically", @@ -834,7 +834,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2026-03-23 14:05:42.144641", + "modified": "2026-03-23 15:26:05.149280", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index b577b2f0885..d66d091320b 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -161,11 +161,11 @@ class Employee(NestedSet): self.validate_duplicate_user_id() def validate_auto_user_creation(self): - if self.create_user_automatically and not (self.prefered_email or self.company_email): + if self.create_user_automatically and not ( + self.prefered_email or self.company_email or self.personal_email + ): frappe.throw( - _( - "Company Email or Preferred Email is mandatory when 'Create User Automatically' is enabled" - ), + _("Company or Personal Email is mandatory when 'Create User Automatically' is enabled"), frappe.MandatoryError, title=_("Auto User Creation Error"), ) @@ -193,7 +193,7 @@ class Employee(NestedSet): create_user( employee=self.name, - email=self.prefered_email or self.company_email, + email=self.prefered_email or self.company_email or self.personal_email, create_user_permission=self.create_user_permission, ) From af94ed865a8b157258cec5785b7ef84319b1e8e7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 15:42:13 +0530 Subject: [PATCH 18/90] fix: reset User ID and make it read-only if 'Create User Automatically' is set (cherry picked from commit 2be6bb694fd8cbb98babed517054ae4adc3fa5ef) --- erpnext/setup/doctype/employee/employee.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 2c121828034..b4adc01b102 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -96,6 +96,15 @@ frappe.ui.form.on("Employee", { } }, + create_user_automatically: function (frm) { + if (frm.doc.create_user_automatically) { + frm.set_value("user_id", ""); + frm.set_df_property("user_id", "read_only", 1); + } else { + frm.set_df_property("user_id", "read_only", 0); + } + }, + prefered_contact_email: function (frm) { frm.events.update_contact(frm); }, From dfd9aa56bedf333c159fdc1573e64b320621da06 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 16:09:58 +0530 Subject: [PATCH 19/90] test: Create User Automatically (cherry picked from commit d4ecede3c3a17eb4757794d13a19611538cb47d2) # Conflicts: # erpnext/setup/doctype/employee/test_employee.py --- .../setup/doctype/employee/test_employee.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/erpnext/setup/doctype/employee/test_employee.py b/erpnext/setup/doctype/employee/test_employee.py index c022f724a66..6560c2f883b 100644 --- a/erpnext/setup/doctype/employee/test_employee.py +++ b/erpnext/setup/doctype/employee/test_employee.py @@ -64,6 +64,64 @@ class TestEmployee(ERPNextTestSuite): self.assertEqual(qb_employee_list, employee_list) frappe.set_user("Administrator") +<<<<<<< HEAD +======= + def test_create_user_automatically(self): + def get_new_employee(email: str, create_user_permission: int): + return frappe.get_doc( + { + "doctype": "Employee", + "first_name": "Test Auto User 1", + "company": erpnext.get_default_company(), + "date_of_birth": "2000-05-08", + "date_of_joining": "2013-01-01", + "gender": "Female", + "personal_email": email, + "status": "Active", + "create_user_automatically": 1, + "create_user_permission": create_user_permission, + } + ).insert() + + employee1 = get_new_employee("test_auto_user1@example.com", True) + user = frappe.db.get_value("User", "test_auto_user1@example.com") + self.assertTrue(user) + self.assertEqual(employee1.user_id, user) + + # Verify user permissions are created + self.assertTrue( + frappe.db.exists( + "User Permission", {"allow": "Employee", "for_value": employee1.name, "user": user} + ) + ) + self.assertTrue( + frappe.db.exists( + "User Permission", {"allow": "Company", "for_value": employee1.company, "user": user} + ) + ) + + # Test disabled create_user_permission + employee2 = get_new_employee("test_auto_user2@example.com", False) + user2 = frappe.db.get_value("User", "test_auto_user2@example.com") + self.assertTrue(user2) + self.assertEqual(employee2.user_id, user2) + + # Verify user permissions are not created + self.assertFalse( + frappe.db.exists( + "User Permission", {"allow": "Employee", "for_value": employee2.name, "user": user2} + ) + ) + self.assertFalse( + frappe.db.exists( + "User Permission", {"allow": "Company", "for_value": employee2.company, "user": user2} + ) + ) + + def tearDown(self): + frappe.db.rollback() + +>>>>>>> d4ecede3c3 (test: Create User Automatically) def make_employee(user, company=None, **kwargs): if not frappe.db.get_value("User", user): From 33d868f41536ded4c7bfb179888f3059d4e437e5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 16:52:11 +0530 Subject: [PATCH 20/90] test(fix): set company in employee (cherry picked from commit a14f834589b5edb952a52c3b2f8a73642ff6b022) # Conflicts: # erpnext/setup/doctype/employee/test_employee.py --- erpnext/setup/doctype/employee/test_employee.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/employee/test_employee.py b/erpnext/setup/doctype/employee/test_employee.py index 6560c2f883b..c1361829cea 100644 --- a/erpnext/setup/doctype/employee/test_employee.py +++ b/erpnext/setup/doctype/employee/test_employee.py @@ -72,7 +72,7 @@ class TestEmployee(ERPNextTestSuite): { "doctype": "Employee", "first_name": "Test Auto User 1", - "company": erpnext.get_default_company(), + "company": "_Test Company", "date_of_birth": "2000-05-08", "date_of_joining": "2013-01-01", "gender": "Female", @@ -118,10 +118,13 @@ class TestEmployee(ERPNextTestSuite): ) ) +<<<<<<< HEAD def tearDown(self): frappe.db.rollback() >>>>>>> d4ecede3c3 (test: Create User Automatically) +======= +>>>>>>> a14f834589 (test(fix): set company in employee) def make_employee(user, company=None, **kwargs): if not frappe.db.get_value("User", user): From 03510d96be2da0fe2e85c6e45c9ab83e8865f1dd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 Mar 2026 17:57:30 +0530 Subject: [PATCH 21/90] chore: fix conflicts --- .../doctype/production_plan/production_plan.py | 2 +- erpnext/setup/doctype/employee/test_employee.py | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 3dc32ef4dab..30b3968fc80 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -680,7 +680,7 @@ class ProductionPlan(Document): frappe.delete_doc("Work Order", d.name) @frappe.whitelist() - def set_status(self, close: bool | None = None, update_bin: bool = False) -> None: + def set_status(self, close=None, update_bin=False): self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus) if close: diff --git a/erpnext/setup/doctype/employee/test_employee.py b/erpnext/setup/doctype/employee/test_employee.py index c1361829cea..b553898dc49 100644 --- a/erpnext/setup/doctype/employee/test_employee.py +++ b/erpnext/setup/doctype/employee/test_employee.py @@ -64,8 +64,6 @@ class TestEmployee(ERPNextTestSuite): self.assertEqual(qb_employee_list, employee_list) frappe.set_user("Administrator") -<<<<<<< HEAD -======= def test_create_user_automatically(self): def get_new_employee(email: str, create_user_permission: int): return frappe.get_doc( @@ -118,13 +116,6 @@ class TestEmployee(ERPNextTestSuite): ) ) -<<<<<<< HEAD - def tearDown(self): - frappe.db.rollback() - ->>>>>>> d4ecede3c3 (test: Create User Automatically) -======= ->>>>>>> a14f834589 (test(fix): set company in employee) def make_employee(user, company=None, **kwargs): if not frappe.db.get_value("User", user): From c36f9e9b1b72295fde10ebd551695e1e3e040e84 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:03:54 +0530 Subject: [PATCH 22/90] =?UTF-8?q?fix(manufacturing):=20close=20work=20orde?= =?UTF-8?q?r=20status=20when=20stock=20reservation=20is=E2=80=A6=20(backpo?= =?UTF-8?q?rt=20#53714)=20(#53721)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pandiyan P Co-authored-by: Mihir Kandoi fix(manufacturing): close work order status when stock reservation is… (#53714) --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 4c317203295..f9d380964bc 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -587,7 +587,7 @@ class WorkOrder(Document): if self.docstatus == 0: status = "Draft" elif self.docstatus == 1: - if status != "Stopped": + if status not in ["Closed", "Stopped"]: status = "Not Started" if flt(self.material_transferred_for_manufacturing) > 0: status = "In Process" From adc2960f5b4444149361d5d3ca69ab985f570582 Mon Sep 17 00:00:00 2001 From: Raghav0201 Date: Mon, 23 Mar 2026 22:13:29 +0530 Subject: [PATCH 23/90] fix: resolve POS crash and correct is_return typo in TransactionBase --- erpnext/utilities/transaction_base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index fb1628fe305..55d60fb389d 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -263,7 +263,7 @@ class TransactionBase(StatusUpdater): "company": self.get("company"), "order_type": self.get("order_type"), "is_pos": cint(self.get("is_pos")), - "is_return": cint(self.get("is_return)")), + "is_return": cint(self.get("is_return")), "is_subcontracted": self.get("is_subcontracted"), "ignore_pricing_rule": self.get("ignore_pricing_rule"), "doctype": self.get("doctype"), @@ -287,7 +287,8 @@ class TransactionBase(StatusUpdater): "child_docname": item.get("name"), "is_old_subcontracting_flow": self.get("is_old_subcontracting_flow"), } - ) + ), + self, ) @frappe.whitelist() From ab0e215290609fa887d42480931694c6670630f7 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 18 Mar 2026 15:26:57 +0530 Subject: [PATCH 24/90] feat: default print format for Request for Quotation (cherry picked from commit 2af0d9cf6c5a05095b46cb21fb3823ed673e2bdb) --- .../request_for_quotation.json | 33 ++++++++++++++++--- .../request_for_quotation.py | 2 ++ .../__init__.py | 0 ...request_for_quotation_with_item_image.json | 33 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 3 ++ erpnext/public/js/print.js | 2 ++ erpnext/setup/install.py | 1 + 7 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 erpnext/buying/print_format/request_for_quotation_with_item_image/__init__.py create mode 100644 erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 18e1356b263..de8b4d28547 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -9,8 +9,6 @@ "field_order": [ "naming_series", "company", - "billing_address", - "billing_address_display", "vendor", "column_break1", "transaction_date", @@ -43,7 +41,13 @@ "select_print_heading", "letter_head", "more_info", - "opportunity" + "opportunity", + "address_and_contact_tab", + "billing_address", + "billing_address_display", + "column_break_czul", + "shipping_address", + "shipping_address_display" ], "fields": [ { @@ -346,6 +350,27 @@ "fieldtype": "Check", "hidden": 1, "label": "Use HTML" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "column_break_czul", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address", + "fieldtype": "Link", + "label": "Company Shipping Address", + "options": "Address" + }, + { + "fieldname": "shipping_address_display", + "fieldtype": "Text Editor", + "label": "Shipping Address Details", + "read_only": 1 } ], "grid_page_length": 50, @@ -353,7 +378,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-03-09 17:15:29.774614", + "modified": "2026-03-19 15:27:56.730649", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 38ab9af4eab..1fc2cdf3386 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -56,6 +56,8 @@ class RequestforQuotation(BuyingController): select_print_heading: DF.Link | None send_attached_files: DF.Check send_document_print: DF.Check + shipping_address: DF.Link | None + shipping_address_display: DF.TextEditor | None status: DF.Literal["", "Draft", "Submitted", "Cancelled"] subject: DF.Data suppliers: DF.Table[RequestforQuotationSupplier] diff --git a/erpnext/buying/print_format/request_for_quotation_with_item_image/__init__.py b/erpnext/buying/print_format/request_for_quotation_with_item_image/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json b/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json new file mode 100644 index 00000000000..57b37290504 --- /dev/null +++ b/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json @@ -0,0 +1,33 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2026-03-19 15:17:39.094444", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Request for Quotation", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 14, + "html": "{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}\n\n{% if letter_head and not no_letterhead %}\n
{{ letter_head }}
\n{% endif %}\n{% if print_heading_template %}\n{{ frappe.render_template(print_heading_template, {\"doc\":doc}) }}\n{% endif %}\n{%- endmacro -%}\n\n{% for page in layout %}\n
\n\t
\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}\n\t
\n\t{%- if doc.meta.is_submittable and doc.docstatus==2-%}\n\t\t
\n\t\t\t

{{ _(\"CANCELLED\") }}

\n\t\t
\n\t{%- endif -%}\n\t{%- if doc.meta.is_submittable and doc.docstatus==0 and (print_settings==None or print_settings.add_draft_heading) -%}\n\t\t
\n\t\t\t

{{ _(\"DRAFT\") }}

\n\t\t
\n\t{%- endif -%}\n\n\t\n\n\t
\n\t\t\n\t\t\t\n\t\t\t\t\n\n\t\t\t\t\n\t\t\t\n\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Supplier Name:\") }}
\n\t\t\t\t\t\t
{{ _(\"Shipping Address:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ doc.suppliers[0].supplier }}
\n\t\t\t\t\t\t
\n \t\t\t\t\t{% if doc.shipping_address %}\n \t\t\t\t\t\t{% set shipping_address = frappe.db.get_value(\"Address\", doc.shipping_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\", \"country\"], as_dict=True) %}\n {{ doc.shipping_address }}
\n \t\t\t\t\t\t{{ shipping_address.address_line1 or \"\" }}
\n \t\t\t\t\t\t{% if shipping_address.address_line2 %}{{ shipping_address.address_line2 }}
{% endif %}\n \t\t\t\t\t\t{{ shipping_address.city or \"\" }} {{ shipping_address.state or \"\" }} {{ shipping_address.pincode or \"\" }} {{ shipping_address.country or \"\" }}
\n \t\t\t\t\t{% endif %}\n\t\t\t\t\t\t
\n\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Order Date:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ frappe.utils.format_date(doc.transaction_date) }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Required By:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ frappe.utils.format_date(doc.schedule_date) }}
\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\n\t\t{% set item_naming_by = frappe.db.get_single_value(\"Stock Settings\", \"item_naming_by\") %}\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{% for item in doc.items %}\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t\n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t{% endfor %}\n\t\t\t\n\t\t
{{ _(\"No\") }}{{ _(\"Item\") }}{{ _(\"Item Code\") }}{{ _(\"Quantity\") }}
{{ loop.index }}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{% if item.image %}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{{ item.item_name }}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t
{{ item.item_code }}{{ item.get_formatted(\"qty\", 0) }} {{ item.uom }}
\n\n\n\t\t\n\t\t{% if doc.terms %}\n\t\t
\n\t\t\t
{{ _(\"Terms and Conditions\") }}
\n\t\t\t{{ doc.terms}}\n\t\t
\n\t\t{% endif %}\n\t
\n
\n{% endfor %}\n", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 15.0, + "margin_left": 15.0, + "margin_right": 15.0, + "margin_top": 15.0, + "modified": "2026-03-19 15:39:20.868219", + "modified_by": "Administrator", + "module": "Buying", + "name": "Request for Quotation with Item Image", + "owner": "Administrator", + "page_number": "Hide", + "pdf_generator": "wkhtmltopdf", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_for": "DocType", + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 24819c3ee19..17d5d929bb5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -4324,6 +4324,8 @@ def get_missing_company_details(doctype, docname): company = frappe.db.get_value(doctype, docname, "company") if doctype in ["Purchase Order", "Purchase Invoice"]: company_address = frappe.db.get_value(doctype, docname, "billing_address") + elif doctype in ["Request for Quotation"]: + company_address = frappe.db.get_value(doctype, docname, "shipping_address") else: company_address = frappe.db.get_value(doctype, docname, "company_address") @@ -4423,6 +4425,7 @@ def update_doc_company_address(current_doctype, docname, company_address, detail "Sales Invoice": ("company_address", "company_address_display"), "Delivery Note": ("company_address", "company_address_display"), "POS Invoice": ("company_address", "company_address_display"), + "Request for Quotation": ("shipping_address", "shipping_address_display"), } address_field, display_field = address_field_map.get( diff --git a/erpnext/public/js/print.js b/erpnext/public/js/print.js index 4f397ef2047..43ba9afc148 100644 --- a/erpnext/public/js/print.js +++ b/erpnext/public/js/print.js @@ -5,6 +5,7 @@ const doctype_list = [ "Purchase Order", "Purchase Invoice", "POS Invoice", + "Request for Quotation", ]; const allowed_print_formats = [ "Sales Order Standard", @@ -19,6 +20,7 @@ const allowed_print_formats = [ "Purchase Invoice with Item Image", "POS Invoice Standard", "POS Invoice with Item Image", + "Request for Quotation with Item Image", ]; const allowed_letterheads = ["Company Letterhead", "Company Letterhead - Grey"]; diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 726906ac6cb..69fbe650f2a 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -310,6 +310,7 @@ def set_default_print_formats(): "Purchase Order": "Purchase Order with Item Image", "Purchase Invoice": "Purchase Invoice with Item Image", "POS Invoice": "POS Invoice with Item Image", + "Request for Quotation": "Request for Quotation with Item Image", } for doctype, print_format in default_map.items(): From a5250f88275399725a29474b4eb91bced2407bca Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Mon, 23 Mar 2026 15:03:11 +0530 Subject: [PATCH 25/90] fix: set default print format for when downlod pdf (cherry picked from commit 6b9fb777720dc889f2a9c34e7d149bed0c6854c8) --- .../doctype/request_for_quotation/request_for_quotation.js | 1 + .../request_for_quotation_with_item_image.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index b71d0dd3006..8baeba950b9 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -100,6 +100,7 @@ frappe.ui.form.on("Request for Quotation", { fieldname: "print_format", options: "Print Format", placeholder: "Standard", + default: frappe.get_meta("Request for Quotation").default_print_format || "", get_query: () => { return { filters: { diff --git a/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json b/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json index 57b37290504..26f131aec5b 100644 --- a/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json +++ b/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json @@ -9,14 +9,14 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "html": "{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}\n\n{% if letter_head and not no_letterhead %}\n
{{ letter_head }}
\n{% endif %}\n{% if print_heading_template %}\n{{ frappe.render_template(print_heading_template, {\"doc\":doc}) }}\n{% endif %}\n{%- endmacro -%}\n\n{% for page in layout %}\n
\n\t
\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}\n\t
\n\t{%- if doc.meta.is_submittable and doc.docstatus==2-%}\n\t\t
\n\t\t\t

{{ _(\"CANCELLED\") }}

\n\t\t
\n\t{%- endif -%}\n\t{%- if doc.meta.is_submittable and doc.docstatus==0 and (print_settings==None or print_settings.add_draft_heading) -%}\n\t\t
\n\t\t\t

{{ _(\"DRAFT\") }}

\n\t\t
\n\t{%- endif -%}\n\n\t\n\n\t
\n\t\t\n\t\t\t\n\t\t\t\t\n\n\t\t\t\t\n\t\t\t\n\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Supplier Name:\") }}
\n\t\t\t\t\t\t
{{ _(\"Shipping Address:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ doc.suppliers[0].supplier }}
\n\t\t\t\t\t\t
\n \t\t\t\t\t{% if doc.shipping_address %}\n \t\t\t\t\t\t{% set shipping_address = frappe.db.get_value(\"Address\", doc.shipping_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\", \"country\"], as_dict=True) %}\n {{ doc.shipping_address }}
\n \t\t\t\t\t\t{{ shipping_address.address_line1 or \"\" }}
\n \t\t\t\t\t\t{% if shipping_address.address_line2 %}{{ shipping_address.address_line2 }}
{% endif %}\n \t\t\t\t\t\t{{ shipping_address.city or \"\" }} {{ shipping_address.state or \"\" }} {{ shipping_address.pincode or \"\" }} {{ shipping_address.country or \"\" }}
\n \t\t\t\t\t{% endif %}\n\t\t\t\t\t\t
\n\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Order Date:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ frappe.utils.format_date(doc.transaction_date) }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Required By:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ frappe.utils.format_date(doc.schedule_date) }}
\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\n\t\t{% set item_naming_by = frappe.db.get_single_value(\"Stock Settings\", \"item_naming_by\") %}\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{% for item in doc.items %}\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t\n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t{% endfor %}\n\t\t\t\n\t\t
{{ _(\"No\") }}{{ _(\"Item\") }}{{ _(\"Item Code\") }}{{ _(\"Quantity\") }}
{{ loop.index }}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{% if item.image %}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{{ item.item_name }}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t
{{ item.item_code }}{{ item.get_formatted(\"qty\", 0) }} {{ item.uom }}
\n\n\n\t\t\n\t\t{% if doc.terms %}\n\t\t
\n\t\t\t
{{ _(\"Terms and Conditions\") }}
\n\t\t\t{{ doc.terms}}\n\t\t
\n\t\t{% endif %}\n\t
\n
\n{% endfor %}\n", + "html": "{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}\n\n{% if letter_head and not no_letterhead %}\n
{{ letter_head }}
\n{% endif %}\n{% if print_heading_template %}\n{{ frappe.render_template(print_heading_template, {\"doc\":doc}) }}\n{% endif %}\n{%- endmacro -%}\n\n{% for page in layout %}\n
\n\t
\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}\n\t
\n\t{%- if doc.meta.is_submittable and doc.docstatus==2-%}\n\t\t
\n\t\t\t

{{ _(\"CANCELLED\") }}

\n\t\t
\n\t{%- endif -%}\n\t{%- if doc.meta.is_submittable and doc.docstatus==0 and (print_settings==None or print_settings.add_draft_heading) -%}\n\t\t
\n\t\t\t

{{ _(\"DRAFT\") }}

\n\t\t
\n\t{%- endif -%}\n\n\t\n\n\t
\n\t\t\n\t\t\t\n\t\t\t\t\n\n\t\t\t\t\n\t\t\t\n\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Supplier Name:\") }}
\n\t\t\t\t\t\t
{{ _(\"Shipping Address:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ doc.vendor }}
\n\t\t\t\t\t\t
\n \t\t\t\t\t{% if doc.shipping_address %}\n \t\t\t\t\t\t{% set shipping_address = frappe.db.get_value(\"Address\", doc.shipping_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\", \"country\"], as_dict=True) %}\n {{ doc.shipping_address }}
\n \t\t\t\t\t\t{{ shipping_address.address_line1 or \"\" }}
\n \t\t\t\t\t\t{% if shipping_address.address_line2 %}{{ shipping_address.address_line2 }}
{% endif %}\n \t\t\t\t\t\t{{ shipping_address.city or \"\" }} {{ shipping_address.state or \"\" }} {{ shipping_address.pincode or \"\" }} {{ shipping_address.country or \"\" }}
\n \t\t\t\t\t{% endif %}\n\t\t\t\t\t\t
\n\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Order Date:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ frappe.utils.format_date(doc.transaction_date) }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Required By:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ frappe.utils.format_date(doc.schedule_date) }}
\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\n\t\t{% set item_naming_by = frappe.db.get_single_value(\"Stock Settings\", \"item_naming_by\") %}\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{% for item in doc.items %}\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t\n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t{% endfor %}\n\t\t\t\n\t\t
{{ _(\"No\") }}{{ _(\"Item\") }}{{ _(\"Item Code\") }}{{ _(\"Quantity\") }}
{{ loop.index }}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{% if item.image %}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{{ item.item_name }}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t
{{ item.item_code }}{{ item.get_formatted(\"qty\", 0) }} {{ item.uom }}
\n\n\n\t\t\n\t\t{% if doc.terms %}\n\t\t
\n\t\t\t
{{ _(\"Terms and Conditions\") }}
\n\t\t\t{{ doc.terms}}\n\t\t
\n\t\t{% endif %}\n\t
\n
\n{% endfor %}\n", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0, "margin_left": 15.0, "margin_right": 15.0, "margin_top": 15.0, - "modified": "2026-03-19 15:39:20.868219", + "modified": "2026-03-23 14:29:41.591636", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation with Item Image", From f80b974d6faaf29b7b7285ec4319ceec30005740 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Mon, 23 Mar 2026 00:35:24 +0530 Subject: [PATCH 26/90] fix(UX): improve party selection UX with party name field (cherry picked from commit 8fd9b88cd91d18b11d1c77c17c8fb9283b056f6e) --- .../opening_invoice_creation_tool.js | 27 +++++++++++++++++++ .../opening_invoice_creation_tool.json | 17 ++++++------ .../opening_invoice_creation_tool.py | 20 +++++++++++--- ...ening_invoice_creation_tool_dashboard.html | 4 +-- .../opening_invoice_creation_tool_item.json | 15 ++++++++--- .../opening_invoice_creation_tool_item.py | 3 ++- 6 files changed, 67 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 4938e6690e5..2f2b9e876a8 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -50,6 +50,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { refresh: function (frm) { frm.disable_save(); + frm.trigger("create_missing_party"); !frm.doc.import_in_progress && frm.trigger("make_dashboard"); frm.page.set_primary_action(__("Create Invoices"), () => { let btn_primary = frm.page.btn_primary.get(0); @@ -162,9 +163,35 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; }); }, + + create_missing_party: function (frm) { + if (frm.doc.create_missing_party) { + frm.fields_dict["invoices"].grid.update_docfield_property("party", "reqd", 0); + frm.fields_dict["invoices"].grid.update_docfield_property("party_name", "read_only", 0); + } else { + frm.fields_dict["invoices"].grid.update_docfield_property("party", "reqd", 1); + frm.fields_dict["invoices"].grid.update_docfield_property("party_name", "read_only", 1); + } + frm.refresh_field("invoices"); + }, }); frappe.ui.form.on("Opening Invoice Creation Tool Item", { + party: function (frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (!row.party) { + frappe.model.set_value(cdt, cdn, "party_name", ""); + return; + } + + let party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; + let name_field = party_type === "Customer" ? "customer_name" : "supplier_name"; + + frappe.db.get_value(party_type, row.party, name_field, (r) => { + frappe.model.set_value(cdt, cdn, "party_name", r?.[name_field] || ""); + }); + }, + invoices_add: (frm) => { frm.trigger("update_invoice_table"); }, diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json index deeee72c18a..8d1c3e87ba1 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json @@ -8,9 +8,9 @@ "engine": "InnoDB", "field_order": [ "company", - "create_missing_party", "column_break_3", "invoice_type", + "create_missing_party", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -29,7 +29,7 @@ }, { "default": "0", - "description": "Create missing customer or supplier.", + "description": "If party does not exist, create it using the Party Name field.", "fieldname": "create_missing_party", "fieldtype": "Check", "label": "Create Missing Party" @@ -65,10 +65,10 @@ "options": "Cost Center" }, { - "fieldname": "project", - "fieldtype": "Link", - "label": "Project", - "options": "Project" + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" }, { "collapsible": 1, @@ -84,7 +84,7 @@ "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2024-03-27 13:10:06.564397", + "modified": "2026-03-23 00:32:15.600086", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool", @@ -101,8 +101,9 @@ } ], "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/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 2f3a893a73f..04d9bd544f6 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -32,6 +32,7 @@ class OpeningInvoiceCreationTool(Document): create_missing_party: DF.Check invoice_type: DF.Literal["Sales", "Purchase"] invoices: DF.Table[OpeningInvoiceCreationToolItem] + project: DF.Link | None # end: auto-generated types def onload(self): @@ -102,10 +103,21 @@ class OpeningInvoiceCreationTool(Document): row.due_date = row.due_date or nowdate() def validate_mandatory_invoice_fields(self, row): - if not frappe.db.exists(row.party_type, row.party): - if self.create_missing_party: + if self.create_missing_party: + if not row.party and not row.party_name: + frappe.throw(_("Row #{}: Either Party ID or Party Name is required").format(row.idx)) + + if not row.party and row.party_name: + self.add_party(row.party_type, row.party_name) + row.party = row.party_name + + if row.party and not frappe.db.exists(row.party_type, row.party): self.add_party(row.party_type, row.party) - else: + + else: + if not row.party: + frappe.throw(_("Row #{}: Party ID is required").format(row.idx)) + if not frappe.db.exists(row.party_type, row.party): frappe.throw( _("Row #{}: {} {} does not exist.").format( row.idx, frappe.bold(row.party_type), frappe.bold(row.party) @@ -113,7 +125,7 @@ class OpeningInvoiceCreationTool(Document): ) mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices") - for d in ("Party", "Outstanding Amount", "Temporary Opening Account"): + for d in ("Outstanding Amount", "Temporary Opening Account"): if not row.get(scrub(d)): frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type)) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html index afbcfa5602a..43be52717c3 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html @@ -1,5 +1,5 @@ {% $.each(data, (company, summary) => { %} -
{{ company }}
+
{{ company }}
@@ -23,7 +23,7 @@ - + {% endif %} {% }); %} diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json index 29daab42439..74ce2a6fb67 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json @@ -8,6 +8,7 @@ "invoice_number", "party_type", "party", + "party_name", "temporary_opening_account", "column_break_3", "posting_date", @@ -35,9 +36,9 @@ "fieldname": "party", "fieldtype": "Dynamic Link", "in_list_view": 1, - "label": "Party", - "options": "party_type", - "reqd": 1 + "label": "Party ID", + "mandatory_depends_on": "eval: !parent.create_missing_party", + "options": "party_type" }, { "fieldname": "temporary_opening_account", @@ -118,11 +119,17 @@ "fieldname": "supplier_invoice_date", "fieldtype": "Date", "label": "Supplier Invoice Date" + }, + { + "fieldname": "party_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Party Name" } ], "istable": 1, "links": [], - "modified": "2025-12-01 16:18:07.997594", + "modified": "2026-03-20 02:11:42.023575", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool Item", diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py index 1ea025322b4..38e97672c96 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py @@ -22,7 +22,8 @@ class OpeningInvoiceCreationToolItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data - party: DF.DynamicLink + party: DF.DynamicLink | None + party_name: DF.Data | None party_type: DF.Link | None posting_date: DF.Date | None qty: DF.Data | None From a2057331e309da6605f625b5c09f0eb4503011bc Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Mon, 23 Mar 2026 01:41:12 +0530 Subject: [PATCH 27/90] fix: party name not updating correctly (cherry picked from commit 469bb0ba4e94809a11871f402861f5fe0039759f) --- .../opening_invoice_creation_tool.js | 3 ++- .../opening_invoice_creation_tool.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 2f2b9e876a8..466b38126d7 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -124,7 +124,8 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { invoice_type: function (frm) { $.each(frm.doc.invoices, (idx, row) => { row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; - row.party = ""; + frappe.model.set_value(row.doctype, row.name, "party", ""); + frappe.model.set_value(row.doctype, row.name, "party_name", ""); }); frm.refresh_fields(); }, diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 04d9bd544f6..6a3df141f52 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -108,11 +108,10 @@ class OpeningInvoiceCreationTool(Document): frappe.throw(_("Row #{}: Either Party ID or Party Name is required").format(row.idx)) if not row.party and row.party_name: - self.add_party(row.party_type, row.party_name) - row.party = row.party_name + row.party = self.add_party(row.party_type, row.party_name) if row.party and not frappe.db.exists(row.party_type, row.party): - self.add_party(row.party_type, row.party) + row.party = self.add_party(row.party_type, row.party) else: if not row.party: @@ -171,6 +170,7 @@ class OpeningInvoiceCreationTool(Document): party_doc.flags.ignore_mandatory = True party_doc.save(ignore_permissions=True) + return party_doc.name def get_invoice_dict(self, row=None): def get_item_dict(): From a35a3e9627355e86f14490df3796f40c05cc8226 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 24 Mar 2026 02:19:44 +0530 Subject: [PATCH 28/90] fix: skip overwriting existing asset fields with accounting dimensions (cherry picked from commit 2859a143f2a12d7f0fe7ad7733135a4934353b6d) --- erpnext/controllers/buying_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 07349a3363f..67ccc4c7fe4 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -1094,7 +1094,8 @@ class BuyingController(SubcontractingController): for dimension in accounting_dimensions[0]: fieldname = dimension["fieldname"] default_dimension = accounting_dimensions[1].get(self.company, {}).get(fieldname) - asset.update({fieldname: row.get(fieldname) or self.get(fieldname) or default_dimension}) + if not asset.get(fieldname): + asset.update({fieldname: row.get(fieldname) or self.get(fieldname) or default_dimension}) asset.flags.ignore_validate = True asset.flags.ignore_mandatory = True From d7902d0477c6002b63ef02bf57baffe15b06ce04 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Tue, 10 Mar 2026 18:18:58 +0530 Subject: [PATCH 29/90] fix: sanitize genericode import inputs and secure XML parser (cherry picked from commit 17eb983c4020a0e90159a5d997215bc4df90a8fd) --- erpnext/edi/doctype/code_list/code_list.py | 11 +++++++---- .../edi/doctype/code_list/code_list_import.py | 16 +++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/edi/doctype/code_list/code_list.py b/erpnext/edi/doctype/code_list/code_list.py index 8957c6565b9..e723157e7a0 100644 --- a/erpnext/edi/doctype/code_list/code_list.py +++ b/erpnext/edi/doctype/code_list/code_list.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING import frappe from frappe.model.document import Document +from frappe.utils import escape_html if TYPE_CHECKING: from lxml.etree import Element @@ -63,14 +64,16 @@ class CodeList(Document): def from_genericode(self, root: "Element"): """Extract Code List details from genericode XML""" - self.title = root.find(".//Identification/ShortName").text + self.title = escape_html(root.find(".//Identification/ShortName").text) self.version = root.find(".//Identification/Version").text self.canonical_uri = root.find(".//CanonicalUri").text # optionals - self.description = getattr(root.find(".//Identification/LongName"), "text", None) - self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None) + self.description = escape_html(getattr(root.find(".//Identification/LongName"), "text", None)) + self.publisher = escape_html(getattr(root.find(".//Identification/Agency/ShortName"), "text", None)) if not self.publisher: - self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None) + self.publisher = escape_html( + getattr(root.find(".//Identification/Agency/LongName"), "text", None) + ) self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None) self.url = getattr(root.find(".//Identification/LocationUri"), "text", None) diff --git a/erpnext/edi/doctype/code_list/code_list_import.py b/erpnext/edi/doctype/code_list/code_list_import.py index 3909eb22766..71cb7d0f82d 100644 --- a/erpnext/edi/doctype/code_list/code_list_import.py +++ b/erpnext/edi/doctype/code_list/code_list_import.py @@ -3,6 +3,7 @@ import json import frappe import requests from frappe import _ +from frappe.utils import escape_html from lxml import etree URL_PREFIXES = ("http://", "https://") @@ -32,7 +33,12 @@ def import_genericode(): content = f.read() # Parse the xml content - parser = etree.XMLParser(remove_blank_text=True) + parser = etree.XMLParser( + remove_blank_text=True, + resolve_entities=False, + load_dtd=False, + no_network=True, + ) try: root = etree.fromstring(content, parser=parser) except Exception as e: @@ -104,7 +110,7 @@ def get_genericode_columns_and_examples(root): # Get column names for column in root.findall(".//Column"): - column_id = column.get("Id") + column_id = escape_html(column.get("Id")) columns.append(column_id) example_values[column_id] = [] filterable_columns[column_id] = set() @@ -112,7 +118,7 @@ def get_genericode_columns_and_examples(root): # Get all values and count unique occurrences for row in root.findall(".//SimpleCodeList/Row"): for value in row.findall("Value"): - column_id = value.get("ColumnRef") + column_id = escape_html(value.get("ColumnRef")) if column_id not in columns: # Handle undeclared column columns.append(column_id) @@ -123,7 +129,7 @@ def get_genericode_columns_and_examples(root): if simple_value is None: continue - filterable_columns[column_id].add(simple_value.text) + filterable_columns[column_id].add(escape_html(simple_value.text)) # Get example values (up to 3) and filter columns with cardinality <= 5 for row in root.findall(".//SimpleCodeList/Row")[:3]: @@ -133,7 +139,7 @@ def get_genericode_columns_and_examples(root): if simple_value is None: continue - example_values[column_id].append(simple_value.text) + example_values[column_id].append(escape_html(simple_value.text)) filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5} From 25fa66f90c7204c5e7e9cf3a953d503afce4a4e7 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 19 Feb 2026 13:02:23 +0530 Subject: [PATCH 30/90] fix: Removed quick access link from selling workspace (cherry picked from commit d7c48d645ae03665abaf6f60add7e987365763b0) --- erpnext/selling/workspace/selling/selling.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json index 5cfe409d128..141b3634708 100644 --- a/erpnext/selling/workspace/selling/selling.json +++ b/erpnext/selling/workspace/selling/selling.json @@ -6,7 +6,7 @@ "label": "Sales Order Trends" } ], - "content": "[{\"id\":\"vBSf8Vi9U8\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"id\":\"aW2i5R5GRP\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"43fzlS1qZg\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Sales Orders\",\"col\":4}},{\"id\":\"jhtxl-XOGi\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Sales Amount\",\"col\":4}},{\"id\":\"0Ioq-P11FP\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Average Order Value\",\"col\":4}},{\"id\":\"1it3dCOnm6\",\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"id\":\"0BcePLg0g1\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"uze5dJ1ipL\",\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"id\":\"3j2fYwMAkq\",\"type\":\"card\",\"data\":{\"card_name\":\"Point of Sale\",\"col\":4}},{\"id\":\"xImm8NepFt\",\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"id\":\"6MjIe7KCQo\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"lBu2EKgmJF\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"1ARHrjg4kI\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", + "content": "[{\"id\":\"vBSf8Vi9U8\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"id\":\"aW2i5R5GRP\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"43fzlS1qZg\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Sales Orders\",\"col\":4}},{\"id\":\"jhtxl-XOGi\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Sales Amount\",\"col\":4}},{\"id\":\"0Ioq-P11FP\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Average Order Value\",\"col\":4}},{\"id\":\"0BcePLg0g1\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"uze5dJ1ipL\",\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"id\":\"3j2fYwMAkq\",\"type\":\"card\",\"data\":{\"card_name\":\"Point of Sale\",\"col\":4}},{\"id\":\"xImm8NepFt\",\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"id\":\"6MjIe7KCQo\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"lBu2EKgmJF\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"1ARHrjg4kI\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-01-28 11:49:12.092882", "custom_blocks": [], "docstatus": 0, @@ -622,7 +622,7 @@ "type": "Link" } ], - "modified": "2026-01-02 17:42:20.131214", + "modified": "2026-02-19 13:01:26.893303", "modified_by": "Administrator", "module": "Selling", "name": "Selling", From 0571830720b31d276349e134e27be6fbe3429699 Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Tue, 24 Mar 2026 11:02:23 +0530 Subject: [PATCH 31/90] refactor: item master ux improvements (cherry picked from commit be5508275132feaf8c72518b089474ac7379bfaa) --- erpnext/stock/doctype/item/item.json | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index b957b905258..e4996462278 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -47,7 +47,6 @@ "column_break_cqdk", "valuation_rate", "inventory_settings_section", - "shelf_life_in_days", "end_of_life", "default_material_request_type", "column_break1", @@ -64,6 +63,7 @@ "create_new_batch", "batch_number_series", "has_expiry_date", + "shelf_life_in_days", "retain_sample", "sample_quantity", "column_break_37", @@ -334,6 +334,7 @@ "options": "fa fa-truck" }, { + "depends_on": "has_expiry_date", "fieldname": "shelf_life_in_days", "fieldtype": "Int", "label": "Shelf Life In Days", @@ -343,11 +344,13 @@ { "default": "2099-12-31", "depends_on": "is_stock_item", + "description": "Defines the date after which the item can no longer be used in transactions or manufacturing", "fieldname": "end_of_life", "fieldtype": "Date", "label": "End of Life", "oldfieldname": "end_of_life", - "oldfieldtype": "Date" + "oldfieldtype": "Date", + "show_description_on_click": 1 }, { "default": "Purchase", @@ -467,9 +470,12 @@ { "default": "0", "depends_on": "has_batch_no", + "description": "Enable to reserve a small sample from each batch for any analysis arising ahead", + "documentation_url": "https://docs.frappe.io/erpnext/retain-sample-stock", "fieldname": "retain_sample", "fieldtype": "Check", - "label": "Retain Sample" + "label": "Retain Sample", + "show_description_on_click": 1 }, { "depends_on": "eval: (doc.retain_sample && doc.has_batch_no)", @@ -989,7 +995,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2026-03-17 20:39:05.218344", + "modified": "2026-03-24 15:45:40.207531", "modified_by": "Administrator", "module": "Stock", "name": "Item", From 2693ffe6801ad6bfb5ae510a99c2795c3fb86e3a Mon Sep 17 00:00:00 2001 From: Jatin3128 Date: Mon, 2 Feb 2026 12:46:49 +0530 Subject: [PATCH 32/90] fix(Payment Entry): split orders as per the schedules in the refrence table (cherry picked from commit a9e52833fec42b01c7a2ba8d7be2d8d5433e1683) --- .../doctype/payment_entry/payment_entry.py | 30 +++---- .../payment_entry/test_payment_entry.py | 86 +++++++++++++++++++ 2 files changed, 101 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 289c19a5885..d60f77120b2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2376,9 +2376,7 @@ def get_outstanding_reference_documents(args, validate=False): vouchers=args.get("vouchers") or None, ) - outstanding_invoices = split_invoices_based_on_payment_terms( - outstanding_invoices, args.get("company") - ) + outstanding_invoices = split_refdocs_based_on_payment_terms(outstanding_invoices, args.get("company")) for d in outstanding_invoices: d["exchange_rate"] = 1 @@ -2416,6 +2414,8 @@ def get_outstanding_reference_documents(args, validate=False): filters=args, ) + orders_to_be_billed = split_refdocs_based_on_payment_terms(orders_to_be_billed, args.get("company")) + data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed if not data: @@ -2438,13 +2438,13 @@ def get_outstanding_reference_documents(args, validate=False): return data -def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list: +def split_refdocs_based_on_payment_terms(refdocs, company) -> list: """Split a list of invoices based on their payment terms.""" - exc_rates = get_currency_data(outstanding_invoices, company) + exc_rates = get_currency_data(refdocs, company) - outstanding_invoices_after_split = [] - for entry in outstanding_invoices: - if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]: + outstanding_refdoc_after_split = [] + for entry in refdocs: + if entry.voucher_type in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]: if payment_term_template := frappe.db.get_value( entry.voucher_type, entry.voucher_no, "payment_terms_template" ): @@ -2459,25 +2459,25 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list ), alert=True, ) - outstanding_invoices_after_split += split_rows + outstanding_refdoc_after_split += split_rows continue # If not an invoice or no payment terms template, add as it is - outstanding_invoices_after_split.append(entry) + outstanding_refdoc_after_split.append(entry) - return outstanding_invoices_after_split + return outstanding_refdoc_after_split -def get_currency_data(outstanding_invoices: list, company: str | None = None) -> dict: +def get_currency_data(outstanding_refdocs: list, company: str | None = None) -> dict: """Get currency and conversion data for a list of invoices.""" exc_rates = frappe._dict() company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None - for doctype in ["Sales Invoice", "Purchase Invoice"]: - invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] + for doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]: + refdoc = [x.voucher_no for x in outstanding_refdocs if x.voucher_type == doctype] for x in frappe.db.get_all( doctype, - filters={"name": ["in", invoices]}, + filters={"name": ["in", refdoc]}, fields=["name", "currency", "conversion_rate", "party_account_currency"], ): exc_rates[x.name] = frappe._dict( diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 12f5276fbfb..9d2890f5e79 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -2019,6 +2019,92 @@ class TestPaymentEntry(ERPNextTestSuite): self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name) self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0]) + def test_outstanding_orders_split_by_payment_terms(self): + create_payment_terms_template() + + so = make_sales_order(do_not_save=1, qty=1, rate=200) + so.payment_terms_template = "Test Receivable Template" + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + self.assertEqual(len(references), 2) + self.assertEqual(references[0].voucher_no, so.name) + self.assertEqual(references[1].voucher_no, so.name) + self.assertEqual(references[0].payment_term, "Basic Amount Receivable") + self.assertEqual(references[1].payment_term, "Tax Receivable") + + def test_outstanding_orders_no_split_when_allocate_disabled(self): + create_payment_terms_template() + + template = frappe.get_doc("Payment Terms Template", "Test Receivable Template") + template.allocate_payment_based_on_payment_terms = 0 + template.save() + + so = make_sales_order(do_not_save=1, qty=1, rate=200) + so.payment_terms_template = "Test Receivable Template" + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + self.assertEqual(len(references), 1) + self.assertIsNone(references[0].payment_term) + + template.allocate_payment_based_on_payment_terms = 1 + template.save() + + def test_outstanding_multicurrency_sales_order_split(self): + create_payment_terms_template() + + so = make_sales_order( + customer="_Test Customer USD", + currency="USD", + qty=1, + rate=100, + do_not_submit=True, + ) + so.payment_terms_template = "Test Receivable Template" + so.conversion_rate = 50 + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + # Should split without throwing currency errors + self.assertEqual(len(references), 2) + for ref in references: + self.assertEqual(ref.voucher_no, so.name) + self.assertIsNotNone(ref.payment_term) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") From 9669a2c56fc5498ce18e241998f8958fefc14c61 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Mar 2026 11:46:43 +0530 Subject: [PATCH 33/90] refactor(test): move commits inside test guard clause (cherry picked from commit ed76d6699afbc35cfdb0cc5a9b6bf100f7525e02) --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 3 ++- erpnext/accounts/doctype/ledger_merge/ledger_merge.py | 3 ++- .../opening_invoice_creation_tool.py | 3 ++- .../accounts/doctype/payment_request/payment_request.py | 3 ++- erpnext/accounts/doctype/subscription/subscription.py | 3 ++- erpnext/assets/doctype/asset/depreciation.py | 7 +++++-- erpnext/buying/doctype/purchase_order/purchase_order.py | 3 ++- erpnext/crm/doctype/appointment/appointment.py | 3 ++- erpnext/setup/doctype/company/company.py | 6 ++++-- erpnext/stock/stock_balance.py | 3 ++- erpnext/telephony/doctype/call_log/call_log.py | 4 +++- 11 files changed, 28 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 3abfa176622..452164c728c 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -489,4 +489,5 @@ def rename_temporarily_named_docs(doctype): for hook in frappe.get_hooks(hook_type): frappe.call(hook, newname=newname, oldname=oldname) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py index 008b4115f5f..3dd3883608c 100644 --- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py +++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py @@ -71,7 +71,8 @@ def start_merge(docname): ledger_merge.account, ) row.db_set("merged", 1) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() successful_merges += 1 frappe.publish_realtime( "ledger_merge_progress", diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 6a3df141f52..e1e70e0f6cb 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -274,7 +274,8 @@ def start_import(invoices): doc.flags.ignore_mandatory = True doc.insert(set_name=invoice_number) doc.submit() - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() names.append(doc.name) except Exception: errors += 1 diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index c95945bf6e2..e16e132957f 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -750,7 +750,8 @@ def make_payment_request(**args): pr.submit() if args.order_type == "Shopping Cart": - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() frappe.local.response["type"] = "redirect" frappe.local.response["location"] = pr.get_payment_url() diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 0b3da559e39..642f918c3b1 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -772,7 +772,8 @@ def process_all(subscription: list, posting_date: DateTimeLikeObject | None = No try: subscription = frappe.get_doc("Subscription", subscription_name) subscription.process(posting_date) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() except frappe.ValidationError: frappe.db.rollback() subscription.log_error("Subscription failed") diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 832e3736e7d..1a9788be48e 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -61,7 +61,9 @@ def book_depreciation_entries(date): accounting_dimensions, ) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() + except Exception as e: frappe.db.rollback() failed_assets.append(asset_name) @@ -71,7 +73,8 @@ def book_depreciation_entries(date): if failed_assets: set_depr_entry_posting_status_for_failed_assets(failed_assets) notify_depr_entry_posting_error(failed_assets, error_logs) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() def get_depreciable_assets_data(date): diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index a0daeca51f2..bd22a21bd99 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -780,7 +780,8 @@ def make_purchase_invoice_from_portal(purchase_order_name): if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"): frappe.throw(_("Not Permitted"), frappe.PermissionError) doc.save() - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() frappe.response["type"] = "redirect" frappe.response.location = "/purchase-invoices/" + doc.name diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index b41064ce9e3..9a213aea5fc 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -120,7 +120,8 @@ class Appointment(Document): self.auto_assign() self.create_calendar_event() self.save(ignore_permissions=True) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() def create_lead_and_link(self): # Return if already linked diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 5da3ca40904..8111935a339 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -927,7 +927,7 @@ def update_transactions_annual_history(company, commit=False): transactions_history = get_all_transactions_annual_history(company) frappe.db.set_value("Company", company, "transactions_annual_history", json.dumps(transactions_history)) - if commit: + if commit and not frappe.in_test: frappe.db.commit() @@ -936,7 +936,9 @@ def cache_companies_monthly_sales_history(): for company in companies: update_company_monthly_sales(company) update_transactions_annual_history(company) - frappe.db.commit() + + if not frappe.in_test: + frappe.db.commit() @frappe.whitelist() diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index b2401da4f8f..7f6deda9b8c 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -31,7 +31,8 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, for d in item_warehouses: try: repost_stock(d[0], d[1], allow_zero_rate, only_actual, only_bin, allow_negative_stock) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() except Exception: frappe.db.rollback() diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 0b5fd5dc368..b2ae785e110 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -190,7 +190,9 @@ def link_existing_conversations(doc, state): call_log = frappe.get_doc("Call Log", log) call_log.add_link(link_type=doc.doctype, link_name=doc.name) call_log.save(ignore_permissions=True) - frappe.db.commit() + + if not frappe.in_test: + frappe.db.commit() except Exception: frappe.log_error(title=_("Error during caller information update")) From 941375877e57b1e1cdb95dc52180ab6bb7f7bfd1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Mar 2026 11:50:01 +0530 Subject: [PATCH 34/90] refactor(test): move dimension setup to test data bootstrap and remove create_dimension() and disable_dimension() (cherry picked from commit 342ce654019ad845282f4e6a1be5ea861c6e1492) --- .../test_accounting_dimension.py | 63 ------------------- .../test_accounting_dimension_filter.py | 5 -- .../test_opening_invoice_creation_tool.py | 6 +- .../test_pos_closing_entry.py | 6 -- .../sales_invoice/test_sales_invoice.py | 7 --- .../tests/test_accounts_controller.py | 18 ------ erpnext/tests/utils.py | 31 +++++++++ 7 files changed, 32 insertions(+), 104 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 81e639dc6b2..250442a3cd4 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestAccountingDimension(ERPNextTestSuite): - def setUp(self): - create_dimension() - def test_dimension_against_sales_invoice(self): si = create_sales_invoice(do_not_save=1) @@ -77,63 +74,3 @@ class TestAccountingDimension(ERPNextTestSuite): si.save() self.assertRaises(frappe.ValidationError, si.submit) - - -def create_dimension(): - frappe.set_user("Administrator") - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): - dimension = frappe.get_doc( - { - "doctype": "Accounting Dimension", - "document_type": "Department", - } - ) - dimension.append( - "dimension_defaults", - { - "company": "_Test Company", - "reference_document": "Department", - "default_dimension": "_Test Department - _TC", - }, - ) - dimension.insert() - dimension.save() - else: - dimension = frappe.get_doc("Accounting Dimension", "Department") - dimension.disabled = 0 - dimension.save() - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): - dimension1 = frappe.get_doc( - { - "doctype": "Accounting Dimension", - "document_type": "Location", - } - ) - - dimension1.append( - "dimension_defaults", - { - "company": "_Test Company", - "reference_document": "Location", - "default_dimension": "Block 1", - }, - ) - - dimension1.insert() - dimension1.save() - else: - dimension1 = frappe.get_doc("Accounting Dimension", "Location") - dimension1.disabled = 0 - dimension1.save() - - -def disable_dimension(): - dimension1 = frappe.get_doc("Accounting Dimension", "Department") - dimension1.disabled = 1 - dimension1.save() - - dimension2 = frappe.get_doc("Accounting Dimension", "Location") - dimension2.disabled = 1 - dimension2.save() diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index 9bed10824bb..fe7d4706967 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -5,10 +5,6 @@ import unittest import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.tests.utils import ERPNextTestSuite @@ -16,7 +12,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestAccountingDimensionFilter(ERPNextTestSuite): def setUp(self): - create_dimension() create_accounting_dimension_filter() self.invoice_list = [] diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 3d57c781983..1a61adad4cd 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -3,10 +3,6 @@ import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( get_temporary_opening_account, ) @@ -15,9 +11,9 @@ from erpnext.tests.utils import ERPNextTestSuite class TestOpeningInvoiceCreationTool(ERPNextTestSuite): def setUp(self): + # TODO: move to bootstrap if not frappe.db.exists("Company", "_Test Opening Invoice Company"): make_company() - create_dimension() def make_invoices( self, 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 af5f73a39ec..05e24d16a3a 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 @@ -4,10 +4,6 @@ import unittest import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import ( make_closing_entry_from_opening, ) @@ -162,7 +158,6 @@ class TestPOSClosingEntry(ERPNextTestSuite): test case to check whether we can create POS Closing Entry without mandatory accounting dimension """ - create_dimension() location = frappe.get_doc("Accounting Dimension", "Location") location.dimension_defaults[0].mandatory_for_bs = True location.save() @@ -198,7 +193,6 @@ class TestPOSClosingEntry(ERPNextTestSuite): ) accounting_dimension_department.mandatory_for_bs = 0 accounting_dimension_department.save() - disable_dimension() def test_merging_into_sales_invoice_for_batched_item(self): frappe.flags.print_message = False diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3616f196bb7..002cdc4a43c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2246,13 +2246,6 @@ class TestSalesInvoice(ERPNextTestSuite): @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": True}) def test_rounding_adjustment_3(self): - 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)]: diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 6bb9b2f91fc..021e034a70d 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1567,25 +1567,10 @@ class TestAccountsController(ERPNextTestSuite): frappe.db.set_value("Company", self.company, "cost_center", cc) - def setup_dimensions(self): - # create dimension - from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - ) - - create_dimension() - # make it non-mandatory - loc = frappe.get_doc("Accounting Dimension", "Location") - for x in loc.dimension_defaults: - x.mandatory_for_bs = False - x.mandatory_for_pl = False - loc.save() - def test_90_dimensions_filter(self): """ Test workings of dimension filters """ - self.setup_dimensions() rate_in_account_currency = 1 # Invoices @@ -1653,7 +1638,6 @@ class TestAccountsController(ERPNextTestSuite): self.assertEqual(len(pr.payments), 1) def test_91_cr_note_should_inherit_dimension(self): - self.setup_dimensions() rate_in_account_currency = 1 # Invoice @@ -1698,7 +1682,6 @@ class TestAccountsController(ERPNextTestSuite): def test_92_dimension_inhertiance_exc_gain_loss(self): # Sales Invoice in Foreign Currency - self.setup_dimensions() rate_in_account_currency = 1 dpt = "Research & Development - _TC" @@ -1734,7 +1717,6 @@ class TestAccountsController(ERPNextTestSuite): ) def test_93_dimension_inheritance_on_advance(self): - self.setup_dimensions() dpt = "Research & Development - _TC" adv = self.create_payment_entry(amount=1, source_exc_rate=85) diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 9485eb9af42..f5f9d7428cf 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -246,6 +246,10 @@ class BootStrapTestData: frappe.db.commit() # nosemgrep + # Dimensions + # DDL commands have implicit commit + self.make_dimensions() + # custom doctype # DDL commands have implicit commit self.make_custom_doctype() @@ -2794,6 +2798,33 @@ class BootStrapTestData: ] self.make_records(["address_title", "address_type"], records) + def make_dimensions(self): + records = [ + { + "doctype": "Accounting Dimension", + "document_type": "Department", + "dimension_defaults": [ + { + "company": "_Test Company", + "reference_document": "Department", + "default_dimension": "_Test Department - _TC", + } + ], + }, + { + "doctype": "Accounting Dimension", + "document_type": "Location", + "dimension_defaults": [ + { + "company": "_Test Company", + "reference_document": "Location", + "default_dimension": "Block 1", + } + ], + }, + ] + self.make_records(["document_type"], records) + BootStrapTestData() From ee72ed94d566133bdf636f37f9202ae93a7f3702 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Mar 2026 12:02:02 +0530 Subject: [PATCH 35/90] refactor(test): move company setup to bootstrap (cherry picked from commit 9ed072ac834e6986adae28100367ab9e0dbdc55b) --- .../test_opening_invoice_creation_tool.py | 18 ------------------ .../setup/doctype/company/test_records.json | 8 ++++++++ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 1a61adad4cd..c01ada6d317 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -10,11 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestOpeningInvoiceCreationTool(ERPNextTestSuite): - def setUp(self): - # TODO: move to bootstrap - if not frappe.db.exists("Company", "_Test Opening Invoice Company"): - make_company() - def make_invoices( self, invoice_type="Sales", @@ -179,19 +174,6 @@ def get_opening_invoice_creation_dict(**args): return invoice_dict -def make_company(): - if frappe.db.exists("Company", "_Test Opening Invoice Company"): - return frappe.get_doc("Company", "_Test Opening Invoice Company") - - company = frappe.new_doc("Company") - company.company_name = "_Test Opening Invoice Company" - company.abbr = "_TOIC" - company.default_currency = "INR" - company.country = "Pakistan" - company.insert() - return company - - def make_customer(customer=None): customer_name = customer or "Opening Customer" customer = frappe.get_doc( diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index 1b51d98413e..f26caa50fde 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -180,5 +180,13 @@ "default_currency": "ZAR", "doctype": "Company", "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "_TOIC", + "company_name": "_Test Opening Invoice Company", + "country": "Pakistan", + "default_currency": "INR", + "doctype": "Company", + "create_chart_of_accounts_based_on": "Standard Template" } ] From bb42d3ddbe24023d9e48d025c71e96e516dfc20a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Mar 2026 16:20:05 +0530 Subject: [PATCH 36/90] refactor(test): move purchase invoice dimension setup to bootstrap (cherry picked from commit 31ce09204f5a6bfdec64fa45a0c61bdba0bd137a) --- .../purchase_invoice/test_purchase_invoice.py | 18 ++++++++++-------- erpnext/tests/utils.py | 4 ++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 33117c639dc..09febdfd915 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2189,11 +2189,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): def test_offsetting_entries_for_accounting_dimensions(self): from erpnext.accounts.doctype.account.test_account import create_account - from erpnext.accounts.report.trial_balance.test_trial_balance import ( - clear_dimension_defaults, - create_accounting_dimension, - disable_dimension, - ) create_account( account_name="Offsetting", @@ -2201,7 +2196,16 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): parent_account="Temporary Accounts - _TC", ) - create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC") + dim = frappe.get_doc("Accounting Dimension", "Branch") + dim.append( + "dimension_defaults", + { + "company": "_Test Company", + "reference_document": "Branch", + "offsetting_account": "Offsetting - _TC", + }, + ) + dim.save() branch1 = frappe.new_doc("Branch") branch1.branch = "Location 1" @@ -2238,8 +2242,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): voucher_type="Purchase Invoice", additional_columns=["branch"], ) - clear_dimension_defaults("Branch") - disable_dimension() def test_repost_accounting_entries(self): # update repost settings diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index f5f9d7428cf..bfc0d5d89f6 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -2822,6 +2822,10 @@ class BootStrapTestData: } ], }, + { + "doctype": "Accounting Dimension", + "document_type": "Branch", + }, ] self.make_records(["document_type"], records) From ebe45add4c7207107f65b9c24731ff592fcb2800 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Mar 2026 16:38:55 +0530 Subject: [PATCH 37/90] refactor(test): move trial company creation to bootstrap (cherry picked from commit 11fb00c21d5d6caeb636ab7a2bcd00b8681eb0de) --- .../trial_balance/test_trial_balance.py | 69 ++++--------------- .../setup/doctype/company/test_records.json | 8 +++ 2 files changed, 20 insertions(+), 57 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index 42cf62af0a0..c37f9d5a46a 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -14,7 +14,6 @@ class TestTrialBalance(ERPNextTestSuite): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.utils import get_fiscal_year - self.company = create_company() create_cost_center( cost_center_name="Test Cost Center", company="Trial Balance Company", @@ -26,7 +25,16 @@ class TestTrialBalance(ERPNextTestSuite): parent_account="Temporary Accounts - TBC", ) self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0] - create_accounting_dimension() + dim = frappe.get_doc("Accounting Dimension", "Branch") + dim.append( + "dimension_defaults", + { + "company": "Trial Balance Company", + "automatically_post_balancing_accounting_entry": 1, + "offsetting_account": "Offsetting - TBC", + }, + ) + dim.save() def test_offsetting_entries_for_accounting_dimensions(self): """ @@ -45,7 +53,7 @@ class TestTrialBalance(ERPNextTestSuite): branch2.insert(ignore_if_duplicate=True) si = create_sales_invoice( - company=self.company, + company="Trial Balance Company", debit_to="Debtors - TBC", cost_center="Test Cost Center - TBC", income_account="Sales - TBC", @@ -57,60 +65,7 @@ class TestTrialBalance(ERPNextTestSuite): si.submit() filters = frappe._dict( - {"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} + {"company": "Trial Balance Company", "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} ) total_row = execute(filters)[1][-1] self.assertEqual(total_row["debit"], total_row["credit"]) - - -def create_company(**args): - args = frappe._dict(args) - company = frappe.get_doc( - { - "doctype": "Company", - "company_name": args.company_name or "Trial Balance Company", - "country": args.country or "India", - "default_currency": args.currency or "INR", - "parent_company": args.get("parent_company"), - "is_group": args.get("is_group"), - } - ) - company.insert(ignore_if_duplicate=True) - return company.name - - -def create_accounting_dimension(**args): - args = frappe._dict(args) - document_type = args.document_type or "Branch" - if frappe.db.exists("Accounting Dimension", document_type): - accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) - accounting_dimension.disabled = 0 - else: - accounting_dimension = frappe.new_doc("Accounting Dimension") - accounting_dimension.document_type = document_type - accounting_dimension.insert() - - accounting_dimension.set("dimension_defaults", []) - accounting_dimension.append( - "dimension_defaults", - { - "company": args.company or "Trial Balance Company", - "automatically_post_balancing_accounting_entry": 1, - "offsetting_account": args.offsetting_account or "Offsetting - TBC", - }, - ) - accounting_dimension.save() - - -def disable_dimension(**args): - args = frappe._dict(args) - document_type = args.document_type or "Branch" - dimension = frappe.get_doc("Accounting Dimension", document_type) - dimension.disabled = 1 - dimension.save() - - -def clear_dimension_defaults(dimension_name): - accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) - accounting_dimension.dimension_defaults = [] - accounting_dimension.save() diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index f26caa50fde..0378ba939c3 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -188,5 +188,13 @@ "default_currency": "INR", "doctype": "Company", "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "TBC", + "company_name": "Trial Balance Company", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "create_chart_of_accounts_based_on": "Standard Template" } ] From d41e7098bda8da839ca7031a7731491e58352fcf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Mar 2026 17:05:39 +0530 Subject: [PATCH 38/90] refactor(test): move tax category custom field creation to bootstrap (cherry picked from commit 4454af8efd57b702c328dd54806858e3a3375fec) --- .../test_tax_withholding_category.py | 16 -------------- erpnext/tests/utils.py | 21 +++++++++++++++++-- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 66dc090f7c7..bd633c94dc9 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -18,7 +18,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite): # create relevant supplier, etc create_records() create_tax_withholding_category_records() - make_pan_no_field() def validate_tax_withholding_entries(self, doctype, docname, expected_entries): """Validate tax withholding entries for a document""" @@ -3998,18 +3997,3 @@ def create_lower_deduction_certificate( "certificate_limit": limit, } ).insert() - - -def make_pan_no_field(): - pan_field = { - "Supplier": [ - { - "fieldname": "pan", - "label": "PAN", - "fieldtype": "Data", - "translatable": 0, - } - ] - } - - create_custom_fields(pan_field, update=1) diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index bfc0d5d89f6..b5c8e19f648 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -8,6 +8,7 @@ from typing import Any, NewType import frappe from frappe import _ from frappe.core.doctype.report.report import get_report_module_dotted_path +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.tests.utils import load_test_records_for from frappe.utils import now_datetime, today @@ -246,14 +247,16 @@ class BootStrapTestData: frappe.db.commit() # nosemgrep - # Dimensions # DDL commands have implicit commit + # Dimensions self.make_dimensions() # custom doctype - # DDL commands have implicit commit self.make_custom_doctype() + # custom field + self.make_custom_field() + def update_system_settings(self): system_settings = frappe.get_doc("System Settings") system_settings.time_zone = "Asia/Kolkata" @@ -2829,6 +2832,20 @@ class BootStrapTestData: ] self.make_records(["document_type"], records) + def make_custom_field(self): + pan_field = { + "Supplier": [ + { + "fieldname": "pan", + "label": "PAN", + "fieldtype": "Data", + "translatable": 0, + } + ] + } + + create_custom_fields(pan_field, update=1) + BootStrapTestData() From cdc77caf6a0f6c460cb5fa3f38686a5887d8df5d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Mar 2026 18:43:46 +0530 Subject: [PATCH 39/90] refactor(test): move custom doctype data setup to bootstrap (cherry picked from commit 934740205aa6729c93b75f7ea57d735d11628df1) --- .../test_inventory_dimension.py | 26 +-------- erpnext/tests/utils.py | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index cbf6059a812..fb62b0eb5c0 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -22,9 +22,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestInventoryDimension(ERPNextTestSuite): - def setUp(self): - prepare_test_data() - def test_validate_inventory_dimension(self): # Can not be child doc inv_dim1 = create_inventory_dimension( @@ -77,6 +74,7 @@ class TestInventoryDimension(ERPNextTestSuite): self.assertFalse(custom_field) def test_inventory_dimension(self): + create_warehouse("Shelf Warehouse") warehouse = "Shelf Warehouse - _TC" item_code = "_Test Item" @@ -556,28 +554,6 @@ def get_voucher_sl_entries(voucher_no, fields): ) -def prepare_test_data(): - for shelf in ["Shelf 1", "Shelf 2"]: - if not frappe.db.exists("Shelf", shelf): - frappe.get_doc({"doctype": "Shelf", "shelf_name": shelf}).insert(ignore_permissions=True) - - create_warehouse("Shelf Warehouse") - - for rack in ["Rack 1", "Rack 2"]: - if not frappe.db.exists("Rack", rack): - frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True) - - create_warehouse("Rack Warehouse") - - for site in ["Site 1", "Site 2"]: - if not frappe.db.exists("Inv Site", site): - frappe.get_doc({"doctype": "Inv Site", "site_name": site}).insert(ignore_permissions=True) - - for store in ["Store 1", "Store 2"]: - if not frappe.db.exists("Store", store): - frappe.get_doc({"doctype": "Store", "store_name": store}).insert(ignore_permissions=True) - - def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index b5c8e19f648..a9ab25bff1a 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -254,6 +254,12 @@ class BootStrapTestData: # custom doctype self.make_custom_doctype() + # data on custom doctype + self.make_shelf() + self.make_rack() + self.make_inv_site() + self.make_store() + # custom field self.make_custom_field() @@ -2846,6 +2852,58 @@ class BootStrapTestData: create_custom_fields(pan_field, update=1) + def make_shelf(self): + records = [ + { + "doctype": "Shelf", + "shelf_name": "Shelf 1", + }, + { + "doctype": "Shelf", + "shelf_name": "Shelf 2", + }, + ] + self.make_records(["shelf_name"], records) + + def make_rack(self): + records = [ + { + "doctype": "Rack", + "rack_name": "Rack 1", + }, + { + "doctype": "Rack", + "rack_name": "Rack 2", + }, + ] + self.make_records(["rack_name"], records) + + def make_inv_site(self): + records = [ + { + "doctype": "Inv Site", + "site_name": "Site 1", + }, + { + "doctype": "Inv Site", + "site_name": "Site 2", + }, + ] + self.make_records(["site_name"], records) + + def make_store(self): + records = [ + { + "doctype": "Store", + "store_name": "Store 1", + }, + { + "doctype": "Store", + "store_name": "Store 2", + }, + ] + self.make_records(["store_name"], records) + BootStrapTestData() From ad2cf0624f434ec0016233a2545b5f7303735203 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 23 Mar 2026 14:10:06 +0530 Subject: [PATCH 40/90] refactor(test): move webform custom dt creation to boostrap (cherry picked from commit 426b7db3c85c949b62179f11b23bb75ee9fde74d) --- erpnext/tests/test_webform.py | 37 -------------------------------- erpnext/tests/utils.py | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/erpnext/tests/test_webform.py b/erpnext/tests/test_webform.py index 8b4ed9ceec9..9ba780e4805 100644 --- a/erpnext/tests/test_webform.py +++ b/erpnext/tests/test_webform.py @@ -22,7 +22,6 @@ class TestWebsite(ERPNextTestSuite): po1 = create_purchase_order(supplier="Supplier1") po2 = create_purchase_order(supplier="Supplier2") - create_custom_doctype() create_webform() create_order_assignment(supplier="Supplier1", po=po1.name) create_order_assignment(supplier="Supplier2", po=po2.name) @@ -62,42 +61,6 @@ def create_user(name, email): ).insert(ignore_if_duplicate=True) -def create_custom_doctype(): - frappe.get_doc( - { - "doctype": "DocType", - "name": "Order Assignment", - "module": "Buying", - "custom": 1, - "autoname": "field:po", - "fields": [ - {"label": "PO", "fieldname": "po", "fieldtype": "Link", "options": "Purchase Order"}, - { - "label": "Supplier", - "fieldname": "supplier", - "fieldtype": "Data", - "fetch_from": "po.supplier", - }, - ], - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1, - }, - {"read": 1, "role": "Supplier"}, - ], - } - ).insert(ignore_if_duplicate=True) - - def create_webform(): frappe.get_doc( { diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index a9ab25bff1a..6b591cd9500 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -2756,6 +2756,46 @@ class BootStrapTestData: } ).insert(ignore_permissions=True) + if not frappe.db.exists("DocType", "Order Assignment"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Order Assignment", + "module": "Buying", + "custom": 1, + "autoname": "field:po", + "fields": [ + { + "label": "PO", + "fieldname": "po", + "fieldtype": "Link", + "options": "Purchase Order", + }, + { + "label": "Supplier", + "fieldname": "supplier", + "fieldtype": "Data", + "fetch_from": "po.supplier", + }, + ], + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1, + }, + {"read": 1, "role": "Supplier"}, + ], + } + ).insert(ignore_if_duplicate=True) + def make_address(self): records = [ { From 0ba03ce8516ea6528496c59ee5569b019b9d4f47 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 23 Mar 2026 15:37:08 +0530 Subject: [PATCH 41/90] refactor(test): SLA move company creation to bootstrap (cherry picked from commit 77f41e120d43ff804ae6cbbeac04b5c44d0d8154) --- .../setup/doctype/company/test_records.json | 10 +++++ .../test_service_level_agreement.py | 37 +++---------------- erpnext/tests/utils.py | 6 +++ 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index 0378ba939c3..bfcddfe0208 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -196,5 +196,15 @@ "default_currency": "INR", "doctype": "Company", "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "_TSS", + "company_name": "_Test Support SLA", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "chart_of_accounts": "Standard", + "create_chart_of_accounts_based_on": "Standard Template" } + ] diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index d7ada387f82..0f6c1262b69 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -14,32 +14,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestServiceLevelAgreement(ERPNextTestSuite): - def setUp(self): - self.create_company() - frappe.db.set_single_value("Support Settings", "track_service_level_agreement", 1) - lead = frappe.qb.DocType("Lead") - frappe.qb.from_(lead).delete().where(lead.company == self.company).run() - - def create_company(self): - name = "_Test Support SLA" - company = None - if frappe.db.exists("Company", name): - company = frappe.get_doc("Company", name) - else: - company = frappe.get_doc( - { - "doctype": "Company", - "company_name": name, - "country": "India", - "default_currency": "INR", - "create_chart_of_accounts_based_on": "Standard Template", - "chart_of_accounts": "Standard", - } - ) - company = company.save() - - self.company = company.name - def test_service_level_agreement(self): # Default Service Level Agreement create_default_service_level_agreement = create_service_level_agreement( @@ -220,10 +194,9 @@ class TestServiceLevelAgreement(ERPNextTestSuite): doctype=doctype, sla_fulfilled_on=[{"status": "Converted"}], ) - # make lead with default SLA creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=1, company=self.company) + lead = make_lead(creation=creation, index=1, company="_Test Support SLA") self.assertEqual(lead.service_level_agreement, lead_sla.name) self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) @@ -251,7 +224,7 @@ class TestServiceLevelAgreement(ERPNextTestSuite): ) creation = datetime.datetime(2020, 3, 4, 4, 0) - lead = make_lead(creation, index=2, company=self.company) + lead = make_lead(creation, index=2, company="_Test Support SLA") frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15) lead.reload() @@ -285,7 +258,7 @@ class TestServiceLevelAgreement(ERPNextTestSuite): ) creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=1, company=self.company) + lead = make_lead(creation=creation, index=1, company="_Test Support SLA") self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) # failed with response time only @@ -312,7 +285,7 @@ class TestServiceLevelAgreement(ERPNextTestSuite): # fulfilled with response time only creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=2, company=self.company) + lead = make_lead(creation=creation, index=2, company="_Test Support SLA") self.assertEqual(lead.service_level_agreement, lead_sla.name) self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) @@ -339,7 +312,7 @@ class TestServiceLevelAgreement(ERPNextTestSuite): apply_sla_for_resolution=0, ) creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=4, company=self.company) + lead = make_lead(creation=creation, index=4, company="_Test Support SLA") applied_sla = frappe.db.get_value("Lead", lead.name, "service_level_agreement") self.assertFalse(applied_sla) diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 6b591cd9500..df2d6c135d4 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -241,6 +241,7 @@ class BootStrapTestData: self.make_sales_person() self.make_activity_type() self.make_address() + self.update_support_settings() self.update_selling_settings() self.update_stock_settings() self.update_system_settings() @@ -271,6 +272,11 @@ class BootStrapTestData: system_settings.rounding_method = "Banker's Rounding" system_settings.save() + def update_support_settings(self): + support_settings = frappe.get_doc("Support Settings") + support_settings.track_service_level_agreement = True + support_settings.save() + def update_selling_settings(self): selling_settings = frappe.get_doc("Selling Settings") selling_settings.selling_price_list = "Standard Selling" From 8ea9133caa734c026c1ede0ec9cadfeed6685d25 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 23 Mar 2026 15:46:02 +0530 Subject: [PATCH 42/90] refactor(test): make ledger merge deterministic (cherry picked from commit d3cf8cb85173cab7007a1af821e1f41f8ea4ccb6) --- erpnext/accounts/doctype/ledger_merge/ledger_merge.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py index 3dd3883608c..dc3fd5a9d04 100644 --- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py +++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py @@ -79,7 +79,8 @@ def start_merge(docname): {"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total}, ) except Exception: - frappe.db.rollback() + if not frappe.in_test: + frappe.db.rollback() ledger_merge.log_error("Ledger merge failed") finally: if successful_merges == total: From 37ad0665c6a9e7a97fd895f2024fd729f57a493f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 23 Mar 2026 15:55:52 +0530 Subject: [PATCH 43/90] refactor(test): make asset capitalization deterministic (cherry picked from commit 2c53cf3902cb67f0bb1b91817a3db3fa93cecd91) --- .../doctype/asset_capitalization/test_asset_capitalization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index f245ac4f0a2..e37ac4c2bf3 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -380,6 +380,7 @@ class TestAssetCapitalization(ERPNextTestSuite): "asset_type": "Composite Component", "purchase_date": pr.posting_date, "available_for_use_date": pr.posting_date, + "location": "Test Location", } ) consumed_asset_doc.save() From f0aa82cc6daf77896ed518101c11e116d71486d2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 23 Mar 2026 16:14:11 +0530 Subject: [PATCH 44/90] refactor(test): make stock entry deterministic (cherry picked from commit 8fd65d7afa56b1c6b2b08d3b5c38eb0a824551c9) --- erpnext/setup/doctype/company/test_records.json | 10 +++++++++- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 3 +-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index bfcddfe0208..74615e60162 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -205,6 +205,14 @@ "doctype": "Company", "chart_of_accounts": "Standard", "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "TQC", + "company_name": "Test Quality Company", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "chart_of_accounts": "Standard", + "create_chart_of_accounts_based_on": "Standard Template" } - ] diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 5d8ebdda56f..48488a7c5b6 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2422,12 +2422,11 @@ class TestStockEntry(ERPNextTestSuite): Unit test case to check the document naming rule with company condition For Quality Inspection, when created from Stock Entry. """ - from erpnext.accounts.report.trial_balance.test_trial_balance import create_company from erpnext.controllers.stock_controller import make_quality_inspections from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse # create a separate company to handle document naming rule with company condition - qc_company = create_company(company_name="Test Quality Company") + qc_company = "Test Quality Company" # create document naming rule based on that for Quality Inspection Doctype qc_naming_rule = frappe.new_doc( From 7f29245eb6f27c979a07903c8f908325c54c738c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 23 Mar 2026 16:14:24 +0530 Subject: [PATCH 45/90] refactor(test): move location creation to bootstrap in asset movement (cherry picked from commit fd2b76a4d21f727c413b4db28587684ad60bf121) --- .../asset_movement/test_asset_movement.py | 17 ----------------- erpnext/tests/utils.py | 1 + 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 88dd93a93cb..76d37d3abb4 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -16,7 +16,6 @@ class TestAssetMovement(ERPNextTestSuite): frappe.db.set_value( "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" ) - make_location() def test_movement(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") @@ -40,10 +39,6 @@ class TestAssetMovement(ERPNextTestSuite): if asset.docstatus == 0: asset.submit() - # check asset movement is created - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - create_asset_movement( purpose="Transfer", company=asset.company, @@ -122,9 +117,6 @@ class TestAssetMovement(ERPNextTestSuite): if asset.docstatus == 0: asset.submit() - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - movement = frappe.get_doc({"doctype": "Asset Movement", "reference_name": pr.name}) self.assertRaises(frappe.ValidationError, movement.cancel) @@ -150,9 +142,6 @@ class TestAssetMovement(ERPNextTestSuite): asset = create_asset(item_code="Macbook Pro", do_not_save=1) asset.save().submit() - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - asset_creation_date = frappe.db.get_value( "Asset Movement", [["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]], @@ -197,9 +186,3 @@ def create_asset_movement(**args): movement.submit() return movement - - -def make_location(): - for location in ["Pune", "Mumbai", "Nagpur"]: - if not frappe.db.exists("Location", location): - frappe.get_doc({"doctype": "Location", "location_name": location}).insert(ignore_permissions=True) diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index df2d6c135d4..6aefc4da247 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -975,6 +975,7 @@ class BootStrapTestData: def make_location(self): records = [ {"doctype": "Location", "location_name": "Test Location"}, + {"doctype": "Location", "location_name": "Test Location 2"}, {"doctype": "Location", "location_name": "Test Location Area", "is_group": 1, "is_container": 1}, { "doctype": "Location", From e91cbd94b403330fb47edbaa6ee116e61708233b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 24 Mar 2026 16:05:54 +0530 Subject: [PATCH 46/90] refactor(test): process statement of acc remove commit (cherry picked from commit bc2b8da59716febb61fd217a53f63684c9e08952) --- .../process_statement_of_accounts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 9cf27216b1e..a40cd03240e 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -563,10 +563,10 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): new_from_date = add_months(new_to_date, -1 * doc.filter_duration) doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())) if doc.report == "General Ledger": - doc.db_set("to_date", new_to_date, commit=True) - doc.db_set("from_date", new_from_date, commit=True) + frappe.db.set_value(doc.doctype, doc.name, "to_date", new_to_date) + frappe.db.set_value(doc.doctype, doc.name, "from_date", new_from_date) else: - doc.db_set("posting_date", new_to_date, commit=True) + frappe.db.set_value(doc.doctype, doc.name, "posting_date", new_to_date) return True else: return False From 1872dccb0a59ba2b0d68d3a67a05a6fa95ff5239 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:10:16 +0530 Subject: [PATCH 47/90] fix: do not check for sub assembly reference for rm of fg (backport #53758) (#53759) Co-authored-by: Mihir Kandoi fix: do not check for sub assembly reference for rm of fg (#53758) --- .../doctype/production_plan/production_plan.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 30b3968fc80..1dfc064b2a4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -616,7 +616,12 @@ class ProductionPlan(Document): None, ): item.db_set("sub_assembly_item_reference", reference) - elif self.reserve_stock and item.main_item_code and item.from_bom: + elif ( + self.reserve_stock + and item.main_item_code + and item.from_bom + and item.main_item_code != frappe.get_cached_value("BOM", item.from_bom, "item") + ): frappe.throw( _( "Sub assembly item references are missing. Please fetch the sub assemblies and raw materials again." From 37b68a07aa22385cecde0b7f2d296d19b573fac3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 05:52:27 +0000 Subject: [PATCH 48/90] fix(manufacturing): apply work order status filter in job card (backport #53766) (#53768) Co-authored-by: Pandiyan P fix(manufacturing): apply work order status filter in job card (#53766) --- erpnext/manufacturing/doctype/job_card/job_card.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index b392a2aa02b..fd652a3a60f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -40,6 +40,14 @@ frappe.ui.form.on("Job Card", { }; }); + frm.set_query("work_order", function () { + return { + filters: { + status: ["not in", ["Cancelled", "Closed", "Stopped"]], + }, + }; + }); + frm.events.set_company_filters(frm, "target_warehouse"); frm.events.set_company_filters(frm, "source_warehouse"); frm.events.set_company_filters(frm, "wip_warehouse"); From a93d7159162f3e3a691ac344bbd402973c28ee02 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:29:08 +0000 Subject: [PATCH 49/90] fix(manufacturing): update condition for base hour rate calculation (backport #53753) (#53771) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> fix(manufacturing): update condition for base hour rate calculation (#53753) --- erpnext/manufacturing/doctype/bom/bom.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 58fce82c208..2ee62b06ad5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -944,12 +944,14 @@ class BOM(WebsiteGenerator): hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate ) - if row.hour_rate and row.time_in_mins: + if row.hour_rate: row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) - row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 - row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) - row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0) - row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0) + + if row.time_in_mins: + row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 + row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) + row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0) + row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0) if update_hour_rate: row.db_update() From 38bc5d69cdeccbd1030fcac72831e0eb36e92a72 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 25 Mar 2026 14:46:50 +0530 Subject: [PATCH 50/90] fix(templates): escape attachment `file_url` and `file_name` in `order.html` and `projects.html` (cherry picked from commit d9760bbf4f539628540f0b8f6a6706b0ca09f4f1) --- erpnext/templates/pages/order.html | 2 +- erpnext/templates/pages/projects.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index 0805a32ae33..5563a58b730 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -140,7 +140,7 @@
{% for attachment in attachments %}

- {{ attachment.file_name }} + {{ attachment.file_name|e }}

{% endfor %}
diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html index d88088c9819..6527036bb22 100644 --- a/erpnext/templates/pages/projects.html +++ b/erpnext/templates/pages/projects.html @@ -82,11 +82,11 @@
{% for attachment in doc.attachments %}
- +
- {{ attachment.file_name }} + {{ attachment.file_name|e }}
{{ attachment.file_size }} From c3cb9cc003a17c6b88a22687532652647755b8a6 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 25 Mar 2026 14:50:08 +0530 Subject: [PATCH 51/90] fix(templates): using correct syntax of `include` in `projects.html` (cherry picked from commit bc6561cdd05cb99329b1412d823dd384ea984447) --- erpnext/templates/pages/projects.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html index 6527036bb22..e671e91db2f 100644 --- a/erpnext/templates/pages/projects.html +++ b/erpnext/templates/pages/projects.html @@ -101,8 +101,8 @@
{% endblock %} From 737cb371d799066ca8f3dd4a8c63866aed6673fb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:26:34 +0000 Subject: [PATCH 52/90] fix(contract_template): restrict `create`, `write` and `delete` access only to `System Manager` (backport #53787) (#53789) Co-authored-by: diptanilsaha fix(contract_template): restrict `create`, `write` and `delete` access only to `System Manager` (#53787) --- .../contract_template/contract_template.json | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/erpnext/crm/doctype/contract_template/contract_template.json b/erpnext/crm/doctype/contract_template/contract_template.json index 223464d3eb8..baa6b289005 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.json +++ b/erpnext/crm/doctype/contract_template/contract_template.json @@ -56,7 +56,7 @@ } ], "links": [], - "modified": "2024-03-27 13:06:46.495091", + "modified": "2026-03-25 19:27:19.162421", "modified_by": "Administrator", "module": "CRM", "name": "Contract Template", @@ -75,44 +75,36 @@ "write": 1 }, { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "Sales Manager", - "share": 1, - "write": 1 + "share": 1 }, { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "Purchase Manager", - "share": 1, - "write": 1 + "share": 1 }, { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "share": 1, - "write": 1 + "share": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From 72efbc2b4257a2fba6760cc981409641be833160 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 04:24:43 +0530 Subject: [PATCH 53/90] fix: purchase invoice for internal transfers should not require PO (backport #53791) (#53793) Co-authored-by: Mihir Kandoi fix: purchase invoice for internal transfers should not require PO (#53791) --- .../doctype/purchase_invoice/purchase_invoice.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 527c33225c6..c22a20510c9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -617,12 +617,13 @@ class PurchaseInvoice(BuyingController): frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account) def po_required(self): - if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes": - if frappe.get_value( + if ( + frappe.db.get_single_value("Buying Settings", "po_required") == "Yes" + and not self.is_internal_transfer() + and not frappe.get_value( "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" - ): - return - + ) + ): for d in self.get("items"): if not d.purchase_order: msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code)) From 09a4f630e13ab8755517a3f0bf4fa2d18319fed6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 07:42:54 +0000 Subject: [PATCH 54/90] fix: keep from and to time blank until added explicitly (backport #53798) (#53801) Co-authored-by: Mihir Kandoi fix: keep from and to time blank until added explicitly (#53798) --- erpnext/manufacturing/doctype/job_card/job_card.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index fd652a3a60f..9fb7dcb51b2 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -788,26 +788,12 @@ frappe.ui.form.on("Job Card Time Log", { frm.events.set_total_completed_qty(frm); }, - - time_in_mins(frm, cdt, cdn) { - let d = locals[cdt][cdn]; - if (d.time_in_mins) { - d.to_time = add_mins_to_time(d.from_time, d.time_in_mins); - frappe.model.set_value(cdt, cdn, "to_time", d.to_time); - } - }, }); function get_seconds_diff(d1, d2) { return moment(d1).diff(d2, "seconds"); } -function add_mins_to_time(datetime, mins) { - let new_date = moment(datetime).add(mins, "minutes"); - - return new_date.format("YYYY-MM-DD HH:mm:ss"); -} - function get_last_completed_row(time_logs) { let completed_rows = time_logs.filter((d) => d.to_time); From f2195fa67d6507e049c0d9edd305c8186fa665ae Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Thu, 26 Mar 2026 19:00:10 +0530 Subject: [PATCH 55/90] fix(accounts): set supplier name as title field in Purchase Invoice (#53710) fix(accounts): update title field in purchase order and purchase invoice (cherry picked from commit 5b1fa81451319f4de0856ada37812a921b261136) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.json --- .../purchase_invoice/purchase_invoice.json | 15 ++------------- .../purchase_invoice/purchase_invoice.py | 1 - .../doctype/purchase_order/purchase_order.json | 17 +++++------------ .../doctype/purchase_order/purchase_order.py | 1 - 4 files changed, 7 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index d2021e0f9a4..adb7dad6726 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -8,7 +8,6 @@ "email_append_to": 1, "engine": "InnoDB", "field_order": [ - "title", "naming_series", "supplier", "supplier_name", @@ -209,16 +208,6 @@ "connections_tab" ], "fields": [ - { - "allow_on_submit": 1, - "default": "{supplier_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -1693,7 +1682,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2026-03-17 20:44:00.221219", + "modified": "2026-03-25 11:45:38.696888", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1756,6 +1745,6 @@ "sort_order": "DESC", "states": [], "timeline_field": "supplier", - "title_field": "title", + "title_field": "supplier_name", "track_changes": 1 } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c22a20510c9..7c076e197a5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -203,7 +203,6 @@ class PurchaseInvoice(BuyingController): taxes_and_charges_deducted: DF.Currency tc_name: DF.Link | None terms: DF.TextEditor | None - title: DF.Data | None to_date: DF.Date | None total: DF.Currency total_advance: DF.Currency diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 2a1b37aae2a..a4e50467baa 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -9,7 +9,11 @@ "engine": "InnoDB", "field_order": [ "supplier_section", +<<<<<<< HEAD "title", +======= + "company", +>>>>>>> 5b1fa81451 (fix(accounts): set supplier name as title field in Purchase Invoice (#53710)) "naming_series", "supplier", "supplier_name", @@ -172,17 +176,6 @@ "fieldtype": "Section Break", "options": "fa fa-user" }, - { - "allow_on_submit": 1, - "default": "{supplier_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1, - "reqd": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -1328,7 +1321,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2026-03-09 17:15:29.184682", + "modified": "2026-03-25 11:46:18.748951", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index bd22a21bd99..7e672da22b6 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -159,7 +159,6 @@ class PurchaseOrder(BuyingController): taxes_and_charges_deducted: DF.Currency tc_name: DF.Link | None terms: DF.TextEditor | None - title: DF.Data to_date: DF.Date | None total: DF.Currency total_net_weight: DF.Float From 3d79dce8b3f9b027639c5b49cb040f8bfdfa08d1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:09:13 +0000 Subject: [PATCH 56/90] fix: flaky currency exchange test (backport #53813) (#53817) --- erpnext/selling/doctype/quotation/test_quotation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index b841f456bc9..9c02879284c 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -1029,11 +1029,11 @@ class TestQuotation(ERPNextTestSuite): def test_make_quotation_qar_to_inr(self): quotation = make_quotation( currency="QAR", - transaction_date="2026-06-04", + transaction_date="2026-01-01", ) cache = frappe.cache() - key = "currency_exchange_rate_{}:{}:{}".format("2026-06-04", "QAR", "INR") + key = "currency_exchange_rate_{}:{}:{}".format("2026-01-01", "QAR", "INR") value = cache.get(key) expected_rate = flt(value) / 3.64 From 10f58112aea37a235d06ab88ea4f350827d91679 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 27 Mar 2026 09:54:24 +0530 Subject: [PATCH 57/90] test: fixed test case --- .../doctype/work_order/test_work_order.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 3b39c58fbac..bea542b7bfa 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -508,10 +508,28 @@ class TestWorkOrder(ERPNextTestSuite): def test_work_order_material_transferred_qty_with_process_loss(self): stock_entries = [] - bom = frappe.get_doc( - "BOM", {"docstatus": 1, "with_operations": 1, "company": "_Test Company", "has_variants": 0} + item_code = make_item("_Test Item For Process Loss", {"is_stock_item": 1}).name + rm_item_code = make_item("Test Item For Process Loss RM", {"is_stock_item": 1}).name + + bom = make_bom( + item=item_code, + raw_materials=[rm_item_code], + with_operations=1, + do_not_save=True, ) + operation = { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "description": "Test Data", + "operating_cost": 100, + "time_in_mins": 40, + } + + bom.append("operations", operation) + bom.save() + bom.submit() + work_order = make_wo_order_test_record( item=bom.item, qty=2, From 00bb07aaa3ee6c6b1d28679d6a4234e692bad289 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 12 Mar 2026 00:54:40 +0530 Subject: [PATCH 58/90] fix(email_campaign): prevent unsubscribing entire campaign when email group member unsubscribes (cherry picked from commit 56f597f5ad9e6cbe12c028687d353db80672ef5e) --- .../doctype/email_campaign/email_campaign.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 9e24a26caa8..4454ede5310 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -204,8 +204,22 @@ def send_mail(entry, email_campaign): # called from hooks on doc_event Email Unsubscribe def unsubscribe_recipient(unsubscribe, method): - if unsubscribe.reference_doctype == "Email Campaign": - frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed") + if unsubscribe.reference_doctype != "Email Campaign": + return + + email_campaign = frappe.get_doc("Email Campaign", unsubscribe.reference_name) + + if email_campaign.email_campaign_for == "Email Group": + if unsubscribe.email: + frappe.db.set_value( + "Email Group Member", + {"email_group": email_campaign.recipient, "email": unsubscribe.email}, + "unsubscribed", + 1, + ) + else: + # For Lead or Contact + frappe.db.set_value("Email Campaign", email_campaign.name, "status", "Unsubscribed") # called through hooks to update email campaign status daily From 407c3cd5752bdea8adf006dce86620a1a2a85f14 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Fri, 13 Mar 2026 20:41:41 +0530 Subject: [PATCH 59/90] feat(report): add service start/end date and amount with roll-ups in deferred revenue/expense report (cherry picked from commit 8e5692d8a36a7a36347672e509fd828540d7b451) --- .../deferred_revenue_and_expense.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index caa464c5447..22fabeabca6 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -48,6 +48,9 @@ class Deferred_Item: Generate report data for output """ ret_data = frappe._dict({"name": self.item_name}) + ret_data.service_start_date = self.service_start_date + ret_data.service_end_date = self.service_end_date + ret_data.amount = self.base_net_amount for period in self.period_total: ret_data[period.key] = period.total ret_data.indent = 1 @@ -205,6 +208,9 @@ class Deferred_Invoice: for item in self.uniq_items: self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item])) + # roll-up amount from all deferred items + self.amount_total = sum(item.base_net_amount for item in self.items) + def calculate_invoice_revenue_expense_for_period(self): """ calculate deferred revenue/expense for all items in invoice @@ -232,7 +238,7 @@ class Deferred_Invoice: generate report data for invoice, includes invoice total """ ret_data = [] - inv_total = frappe._dict({"name": self.name}) + inv_total = frappe._dict({"name": self.name, "amount": self.amount_total}) for x in self.period_total: inv_total[x.key] = x.total inv_total.indent = 0 @@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report: def get_columns(self): columns = [] columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1}) + columns.append( + { + "label": _("Service Start Date"), + "fieldname": "service_start_date", + "fieldtype": "Date", + "read_only": 1, + } + ) + columns.append( + { + "label": _("Service End Date"), + "fieldname": "service_end_date", + "fieldtype": "Date", + "read_only": 1, + } + ) + columns.append({"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "read_only": 1}) + for period in self.period_list: columns.append( { @@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report: elif self.filters.type == "Expense": total_row = frappe._dict({"name": "Total Deferred Expense"}) + total_row["amount"] = sum(inv.amount_total for inv in self.deferred_invoices) + for idx, period in enumerate(self.period_list, 0): total_row[period.key] = self.period_total[idx].total ret.append(total_row) From d12b54c50a3899c0c9c68c7ce3f030cb41dda6d7 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 27 Mar 2026 11:24:35 +0530 Subject: [PATCH 60/90] chore: resolve conflict --- erpnext/buying/doctype/purchase_order/purchase_order.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index a4e50467baa..260dc52ceac 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -9,11 +9,6 @@ "engine": "InnoDB", "field_order": [ "supplier_section", -<<<<<<< HEAD - "title", -======= - "company", ->>>>>>> 5b1fa81451 (fix(accounts): set supplier name as title field in Purchase Invoice (#53710)) "naming_series", "supplier", "supplier_name", From 8a5e2cc0a678a08fc22663f559401d2ac8808d0e Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Mon, 23 Mar 2026 16:18:59 +0530 Subject: [PATCH 61/90] feat: Bom stock analysis report (cherry picked from commit 5d088350dc328e4551a457dd89a35f6ec46599b4) --- .../report/bom_stock_analysis/__init__.py | 0 .../bom_stock_analysis/bom_stock_analysis.js | 43 +++ .../bom_stock_analysis.json | 31 ++ .../bom_stock_analysis/bom_stock_analysis.py | 282 ++++++++++++++++++ .../test_bom_stock_analysis.py | 119 ++++++++ 5 files changed, 475 insertions(+) create mode 100644 erpnext/manufacturing/report/bom_stock_analysis/__init__.py create mode 100644 erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js create mode 100644 erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json create mode 100644 erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py create mode 100644 erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py diff --git a/erpnext/manufacturing/report/bom_stock_analysis/__init__.py b/erpnext/manufacturing/report/bom_stock_analysis/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js new file mode 100644 index 00000000000..d97392a5afd --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js @@ -0,0 +1,43 @@ +// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["BOM Stock Analysis"] = { + filters: [ + { + fieldname: "bom", + label: __("BOM"), + fieldtype: "Link", + options: "BOM", + reqd: 1, + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + }, + { + fieldname: "qty_to_make", + label: __("FG Items to Make"), + fieldtype: "Float", + }, + { + fieldname: "show_exploded_view", + label: __("Show availability of exploded items"), + fieldtype: "Check", + default: false, + }, + ], + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.id == "producible_fg_item") { + if (data["producible_fg_item"] >= data["required_qty"]) { + value = `
${data["producible_fg_item"]}`; + } else { + value = `${data["producible_fg_item"]}`; + } + } + return value; + }, +}; diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json new file mode 100644 index 00000000000..b0e68f77ba7 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2026-03-23 15:42:06.064606", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": null, + "modified": "2026-03-23 15:48:56.933892", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Stock Analysis", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "BOM", + "report_name": "BOM Stock Analysis", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing Manager" + }, + { + "role": "Manufacturing User" + } + ], + "timeout": 0 +} diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py new file mode 100644 index 00000000000..d3220ee35b5 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py @@ -0,0 +1,282 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.query_builder.functions import Floor, IfNull, Sum +from frappe.utils.data import comma_and +from pypika.terms import ExistsCriterion + + +def execute(filters=None): + qty_to_make = filters.get("qty_to_make") + + if qty_to_make: + columns = get_columns_with_qty_to_make() + data = get_data_with_qty_to_make(filters) + return columns, data + else: + data = [] + columns = get_columns_without_qty_to_make() + bom_data = get_producible_fg_items(filters) + for row in bom_data: + data.append(row) + + return columns, data + + +def get_data_with_qty_to_make(filters): + data = [] + bom_data = get_bom_data(filters) + manufacture_details = get_manufacturer_records() + + for row in bom_data: + required_qty = filters.get("qty_to_make") * row.qty_per_unit + last_purchase_rate = frappe.db.get_value("Item", row.item_code, "last_purchase_rate") + + data.append(get_report_data(last_purchase_rate, required_qty, row, manufacture_details)) + + return data + + +def get_report_data(last_purchase_rate, required_qty, row, manufacture_details): + qty_per_unit = row.qty_per_unit if row.qty_per_unit > 0 else 0 + difference_qty = row.actual_qty - required_qty + return [ + row.item_code, + row.description, + row.from_bom_no, + comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False), + comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False), + qty_per_unit, + row.actual_qty, + required_qty, + difference_qty, + last_purchase_rate, + row.actual_qty // qty_per_unit if qty_per_unit else 0, + ] + + +def get_columns_with_qty_to_make(): + return [ + { + "fieldname": "item", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + { + "fieldname": "description", + "label": _("Description"), + "fieldtype": "Data", + "width": 150, + }, + { + "fieldname": "from_bom_no", + "label": _("From BOM No"), + "fieldtype": "Link", + "options": "BOM", + "width": 150, + }, + { + "fieldname": "manufacturer", + "label": _("Manufacturer"), + "fieldtype": "Data", + "width": 120, + }, + { + "fieldname": "manufacturer_part_number", + "label": _("Manufacturer Part Number"), + "fieldtype": "Data", + "width": 150, + }, + { + "fieldname": "qty_per_unit", + "label": _("Qty Per Unit"), + "fieldtype": "Float", + "width": 110, + }, + { + "fieldname": "available_qty", + "label": _("Available Qty"), + "fieldtype": "Float", + "width": 120, + }, + { + "fieldname": "required_qty", + "label": _("Required Qty"), + "fieldtype": "Float", + "width": 120, + }, + { + "fieldname": "difference_qty", + "label": _("Difference Qty"), + "fieldtype": "Float", + "width": 130, + }, + { + "fieldname": "last_purchase_rate", + "label": _("Last Purchase Rate"), + "fieldtype": "Float", + "width": 160, + }, + { + "fieldname": "producible_fg_item", + "label": _("Producible FG Item"), + "fieldtype": "Float", + "width": 200, + }, + ] + + +def get_columns_without_qty_to_make(): + return [ + _("Item") + ":Link/Item:150", + _("Item Name") + "::240", + _("Description") + "::300", + _("From BOM No") + "::200", + _("Required Qty") + ":Float:160", + _("Producible FG Item") + ":Float:200", + ] + + +def get_bom_data(filters): + bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" + + bom_item = frappe.qb.DocType(bom_item_table) + bin = frappe.qb.DocType("Bin") + + query = ( + frappe.qb.from_(bom_item) + .left_join(bin) + .on(bom_item.item_code == bin.item_code) + .select( + bom_item.item_code, + bom_item.description, + bom_item.parent.as_("from_bom_no"), + bom_item.qty_consumed_per_unit.as_("qty_per_unit"), + IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), + ) + .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) + .groupby(bom_item.item_code) + .orderby(bom_item.idx) + ) + + if filters.get("warehouse"): + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) + + if warehouse_details: + wh = frappe.qb.DocType("Warehouse") + query = query.where( + ExistsCriterion( + frappe.qb.from_(wh) + .select(wh.name) + .where( + (wh.lft >= warehouse_details.lft) + & (wh.rgt <= warehouse_details.rgt) + & (bin.warehouse == wh.name) + ) + ) + ) + else: + query = query.where(bin.warehouse == filters.get("warehouse")) + + if bom_item_table == "BOM Item": + query = query.select(bom_item.bom_no, bom_item.is_phantom_item) + + data = query.run(as_dict=True) + return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data + + +def explode_phantom_boms(data, filters): + original_bom = filters.get("bom") + replacements = [] + + for idx, item in enumerate(data): + if not item.is_phantom_item: + continue + + filters["bom"] = item.bom_no + children = get_bom_data(filters) + filters["bom"] = original_bom + + for child in children: + child.qty_per_unit = (child.qty_per_unit or 0) * (item.qty_per_unit or 0) + + replacements.append((idx, children)) + + for idx, children in reversed(replacements): + data.pop(idx) + data[idx:idx] = children + + filters["bom"] = original_bom + return data + + +def get_manufacturer_records(): + details = frappe.get_all( + "Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"] + ) + + manufacture_details = frappe._dict() + for detail in details: + dic = manufacture_details.setdefault(detail.get("item_code"), {}) + dic.setdefault("manufacturer", []).append(detail.get("manufacturer")) + dic.setdefault("manufacturer_part", []).append(detail.get("manufacturer_part_no")) + + return manufacture_details + + +def get_producible_fg_items(filters): + BOM_ITEM = frappe.qb.DocType("BOM Item") + BOM = frappe.qb.DocType("BOM") + BIN = frappe.qb.DocType("Bin") + WH = frappe.qb.DocType("Warehouse") + + warehouse = filters.get("warehouse") + warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) + + if not warehouse: + frappe.throw(_("Warehouse is required to get producible FG Items")) + + if warehouse_details: + bin_subquery = ( + frappe.qb.from_(BIN) + .join(WH) + .on(BIN.warehouse == WH.name) + .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) + .where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt)) + .groupby(BIN.item_code) + ) + else: + bin_subquery = ( + frappe.qb.from_(BIN) + .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) + .where(BIN.warehouse == warehouse) + .groupby(BIN.item_code) + ) + + query = ( + frappe.qb.from_(BOM_ITEM) + .join(BOM) + .on(BOM_ITEM.parent == BOM.name) + .left_join(bin_subquery) + .on(BOM_ITEM.item_code == bin_subquery.item_code) + .select( + BOM_ITEM.item_code, + BOM_ITEM.item_name, + BOM_ITEM.description, + BOM_ITEM.parent.as_("from_bom_no"), + (BOM_ITEM.stock_qty / BOM.quantity).as_("qty_per_unit"), + Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty)) / BOM.quantity)), + ) + .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) + .groupby(BOM_ITEM.item_code) + .orderby(BOM_ITEM.idx) + ) + + data = query.run(as_list=True) + return data diff --git a/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py b/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py new file mode 100644 index 00000000000..ebb1b85ac53 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py @@ -0,0 +1,119 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.report.bom_stock_analysis.bom_stock_analysis import ( + execute as bom_stock_analysis_report, +) +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.tests.utils import ERPNextTestSuite + + +class TestBOMStockAnalysis(ERPNextTestSuite): + def setUp(self): + self.fg_item, self.rm_items = create_items() + self.boms = create_boms(self.fg_item, self.rm_items) + + def test_bom_stock_analysis(self): + qty_to_make = 10 + + # Case 1: When Item(s) Qty and Stock Qty are equal. + data = bom_stock_analysis_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[0].name, + } + )[1] + expected_data = get_expected_data(self.boms[0], qty_to_make) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + # Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1. + data = bom_stock_analysis_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[1].name, + } + )[1] + expected_data = get_expected_data(self.boms[1], qty_to_make) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + # Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1. + data = bom_stock_analysis_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[2].name, + } + )[1] + expected_data = get_expected_data(self.boms[2], qty_to_make) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + +def create_items(): + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 100, + "opening_stock": 100, + "last_purchase_rate": 100, + "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], + } + ).name + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 200, + "opening_stock": 200, + "last_purchase_rate": 200, + "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], + } + ).name + + return fg_item, [rm_item1, rm_item2] + + +def create_boms(fg_item, rm_items): + def update_bom_items(bom, uom, conversion_factor): + for item in bom.items: + item.uom = uom + item.conversion_factor = conversion_factor + + return bom + + bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10) + + bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True) + bom2 = update_bom_items(bom2, "Box", 10) + bom2.save() + bom2.submit() + + bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True) + bom3 = update_bom_items(bom3, "Box", 10) + bom3.save() + bom3.submit() + + return [bom1, bom2, bom3] + + +def get_expected_data(bom, qty_to_make): + expected_data = [] + + for idx in range(len(bom.items)): + expected_data.append( + [ + bom.items[idx].item_code, + bom.items[idx].item_code, + bom.name, + "", + "", + float(bom.items[idx].stock_qty / bom.quantity), + float(100 * (idx + 1)), + float(qty_to_make * (bom.items[idx].stock_qty / bom.quantity)), + float((100 * (idx + 1)) - (qty_to_make * (bom.items[idx].stock_qty / bom.quantity))), + float(100 * (idx + 1)), + ] + ) + + return expected_data From 6d92792634eda965b5269cf161cb5c73d86c9225 Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Tue, 24 Mar 2026 12:02:09 +0530 Subject: [PATCH 62/90] fix: change in functionality (cherry picked from commit c1874cb7d58f4714f1ef92d705ea53343b4861b9) --- .../bom_stock_analysis/bom_stock_analysis.js | 28 ++- .../bom_stock_analysis/bom_stock_analysis.py | 237 +++++++++++------- .../test_bom_stock_analysis.py | 104 ++++++-- 3 files changed, 240 insertions(+), 129 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js index d97392a5afd..7c6ccfdf743 100644 --- a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js @@ -28,16 +28,32 @@ frappe.query_reports["BOM Stock Analysis"] = { default: false, }, ], - formatter: function (value, row, column, data, default_formatter) { + formatter(value, row, column, data, default_formatter) { + if (data && data.bold && column.fieldname === "item") { + return value ? `${value}` : ""; + } + value = default_formatter(value, row, column, data); - if (column.id == "producible_fg_item") { - if (data["producible_fg_item"] >= data["required_qty"]) { - value = `${data["producible_fg_item"]}`; - } else { - value = `${data["producible_fg_item"]}`; + if (column.fieldname === "difference_qty" && value !== "" && value !== undefined) { + const numeric = parseFloat(value.replace(/,/g, "")) || 0; + if (numeric < 0) { + value = `${value}`; + } else if (numeric > 0) { + value = `${value}`; } } + + if (data && data.bold) { + if (column.fieldname === "description" || column.fieldname === "item_name") { + const qty_to_make = frappe.query_report.get_filter_value("qty_to_make"); + const producible = parseFloat(value) || 0; + const colour = qty_to_make && producible < qty_to_make ? "red" : "green"; + return `${value}`; + } + return `${value}`; + } + return value; }, }; diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py index d3220ee35b5..78aa75aa7fa 100644 --- a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py @@ -4,74 +4,101 @@ import frappe from frappe import _ from frappe.query_builder.functions import Floor, IfNull, Sum +from frappe.utils import flt, fmt_money from frappe.utils.data import comma_and from pypika.terms import ExistsCriterion def execute(filters=None): - qty_to_make = filters.get("qty_to_make") - - if qty_to_make: + if filters.get("qty_to_make"): columns = get_columns_with_qty_to_make() data = get_data_with_qty_to_make(filters) - return columns, data else: - data = [] columns = get_columns_without_qty_to_make() - bom_data = get_producible_fg_items(filters) - for row in bom_data: - data.append(row) + data = get_data_without_qty_to_make(filters) - return columns, data + return columns, data + + +def fmt_qty(value): + """Format a float quantity for display as a string, so blank rows stay blank.""" + return frappe.utils.fmt_money(value, precision=2, currency=None) + + +def fmt_rate(value): + """Format a currency rate for display as a string.""" + currency = frappe.defaults.get_global_default("currency") + return frappe.utils.fmt_money(value, precision=2, currency=currency) def get_data_with_qty_to_make(filters): - data = [] bom_data = get_bom_data(filters) manufacture_details = get_manufacturer_records() + purchase_rates = batch_fetch_purchase_rates(bom_data) + qty_to_make = filters.get("qty_to_make") + data = [] for row in bom_data: - required_qty = filters.get("qty_to_make") * row.qty_per_unit - last_purchase_rate = frappe.db.get_value("Item", row.item_code, "last_purchase_rate") + qty_per_unit = flt(row.qty_per_unit) if row.qty_per_unit > 0 else 0 + required_qty = qty_to_make * qty_per_unit + difference_qty = flt(row.actual_qty) - required_qty + rate = purchase_rates.get(row.item_code, 0) - data.append(get_report_data(last_purchase_rate, required_qty, row, manufacture_details)) + data.append( + { + "item": row.item_code, + "description": row.description, + "from_bom_no": row.from_bom_no, + "manufacturer": comma_and( + manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False + ), + "manufacturer_part_number": comma_and( + manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False + ), + "qty_per_unit": fmt_qty(qty_per_unit), + "available_qty": fmt_qty(row.actual_qty), + "required_qty": fmt_qty(required_qty), + "difference_qty": fmt_qty(difference_qty), + "last_purchase_rate": fmt_rate(rate), + "_available_qty": flt(row.actual_qty), + "_qty_per_unit": qty_per_unit, + } + ) + + min_producible = ( + min(int(r["_available_qty"] // r["_qty_per_unit"]) for r in data if r["_qty_per_unit"]) if data else 0 + ) + + for row in data: + row.pop("_available_qty", None) + row.pop("_qty_per_unit", None) + + # blank spacer row + data.append({}) + + data.append( + { + "item": _("Maximum Producible Items"), + "description": min_producible, + "from_bom_no": "", + "manufacturer": "", + "manufacturer_part_number": "", + "qty_per_unit": "", + "available_qty": "", + "required_qty": "", + "difference_qty": "", + "last_purchase_rate": "", + "bold": 1, + } + ) return data -def get_report_data(last_purchase_rate, required_qty, row, manufacture_details): - qty_per_unit = row.qty_per_unit if row.qty_per_unit > 0 else 0 - difference_qty = row.actual_qty - required_qty - return [ - row.item_code, - row.description, - row.from_bom_no, - comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False), - comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False), - qty_per_unit, - row.actual_qty, - required_qty, - difference_qty, - last_purchase_rate, - row.actual_qty // qty_per_unit if qty_per_unit else 0, - ] - - def get_columns_with_qty_to_make(): return [ - { - "fieldname": "item", - "label": _("Item"), - "fieldtype": "Link", - "options": "Item", - "width": 120, - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 150, - }, + {"fieldname": "item", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 180}, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 160}, { "fieldname": "from_bom_no", "label": _("From BOM No"), @@ -79,68 +106,89 @@ def get_columns_with_qty_to_make(): "options": "BOM", "width": 150, }, - { - "fieldname": "manufacturer", - "label": _("Manufacturer"), - "fieldtype": "Data", - "width": 120, - }, + {"fieldname": "manufacturer", "label": _("Manufacturer"), "fieldtype": "Data", "width": 130}, { "fieldname": "manufacturer_part_number", "label": _("Manufacturer Part Number"), "fieldtype": "Data", - "width": 150, - }, - { - "fieldname": "qty_per_unit", - "label": _("Qty Per Unit"), - "fieldtype": "Float", - "width": 110, - }, - { - "fieldname": "available_qty", - "label": _("Available Qty"), - "fieldtype": "Float", - "width": 120, - }, - { - "fieldname": "required_qty", - "label": _("Required Qty"), - "fieldtype": "Float", - "width": 120, - }, - { - "fieldname": "difference_qty", - "label": _("Difference Qty"), - "fieldtype": "Float", - "width": 130, + "width": 170, }, + {"fieldname": "qty_per_unit", "label": _("Qty Per Unit"), "fieldtype": "Data", "width": 110}, + {"fieldname": "available_qty", "label": _("Available Qty"), "fieldtype": "Data", "width": 120}, + {"fieldname": "required_qty", "label": _("Required Qty"), "fieldtype": "Data", "width": 120}, + {"fieldname": "difference_qty", "label": _("Difference Qty"), "fieldtype": "Data", "width": 130}, { "fieldname": "last_purchase_rate", "label": _("Last Purchase Rate"), - "fieldtype": "Float", + "fieldtype": "Data", "width": 160, }, - { - "fieldname": "producible_fg_item", - "label": _("Producible FG Item"), - "fieldtype": "Float", - "width": 200, - }, ] +def get_data_without_qty_to_make(filters): + raw_rows = get_producible_fg_items(filters) + + data = [] + for row in raw_rows: + data.append( + { + "item": row[0], + "description": row[1], + "from_bom_no": row[2], + "qty_per_unit": fmt_qty(row[3]), + "available_qty": fmt_qty(row[4]), + } + ) + + min_producible = min((row[5] or 0) for row in raw_rows) if raw_rows else 0 + # blank spacer row + data.append({}) + + data.append( + { + "item": _("Maximum Producible Items"), + "description": min_producible, + "from_bom_no": "", + "qty_per_unit": "", + "available_qty": "", + "bold": 1, + } + ) + + return data + + def get_columns_without_qty_to_make(): return [ - _("Item") + ":Link/Item:150", - _("Item Name") + "::240", - _("Description") + "::300", - _("From BOM No") + "::200", - _("Required Qty") + ":Float:160", - _("Producible FG Item") + ":Float:200", + {"fieldname": "item", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 180}, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 200}, + { + "fieldname": "from_bom_no", + "label": _("From BOM No"), + "fieldtype": "Link", + "options": "BOM", + "width": 160, + }, + {"fieldname": "qty_per_unit", "label": _("Qty Per Unit"), "fieldtype": "Data", "width": 120}, + {"fieldname": "available_qty", "label": _("Available Qty"), "fieldtype": "Data", "width": 120}, ] +def batch_fetch_purchase_rates(bom_data): + if not bom_data: + return {} + item_codes = [row.item_code for row in bom_data] + return { + r.name: r.last_purchase_rate + for r in frappe.get_all( + "Item", + filters={"name": ["in", item_codes]}, + fields=["name", "last_purchase_rate"], + ) + } + + def get_bom_data(filters): bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" @@ -167,7 +215,6 @@ def get_bom_data(filters): warehouse_details = frappe.db.get_value( "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 ) - if warehouse_details: wh = frappe.qb.DocType("Warehouse") query = query.where( @@ -212,7 +259,6 @@ def explode_phantom_boms(data, filters): data.pop(idx) data[idx:idx] = children - filters["bom"] = original_bom return data @@ -220,13 +266,11 @@ def get_manufacturer_records(): details = frappe.get_all( "Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"] ) - manufacture_details = frappe._dict() for detail in details: dic = manufacture_details.setdefault(detail.get("item_code"), {}) dic.setdefault("manufacturer", []).append(detail.get("manufacturer")) dic.setdefault("manufacturer_part", []).append(detail.get("manufacturer_part_no")) - return manufacture_details @@ -237,11 +281,11 @@ def get_producible_fg_items(filters): WH = frappe.qb.DocType("Warehouse") warehouse = filters.get("warehouse") - warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) - if not warehouse: frappe.throw(_("Warehouse is required to get producible FG Items")) + warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) + if warehouse_details: bin_subquery = ( frappe.qb.from_(BIN) @@ -267,10 +311,10 @@ def get_producible_fg_items(filters): .on(BOM_ITEM.item_code == bin_subquery.item_code) .select( BOM_ITEM.item_code, - BOM_ITEM.item_name, BOM_ITEM.description, BOM_ITEM.parent.as_("from_bom_no"), (BOM_ITEM.stock_qty / BOM.quantity).as_("qty_per_unit"), + IfNull(bin_subquery.actual_qty, 0).as_("available_qty"), Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty)) / BOM.quantity)), ) .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) @@ -278,5 +322,4 @@ def get_producible_fg_items(filters): .orderby(BOM_ITEM.idx) ) - data = query.run(as_list=True) - return data + return query.run(as_list=True) diff --git a/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py b/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py index ebb1b85ac53..fd8a52afde0 100644 --- a/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py +++ b/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py @@ -1,7 +1,7 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import frappe +from frappe.utils import fmt_money from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.manufacturing.report.bom_stock_analysis.bom_stock_analysis import ( @@ -11,6 +11,15 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.tests.utils import ERPNextTestSuite +def fmt_qty(value): + return fmt_money(value, precision=2, currency=None) + + +def fmt_rate(value): + currency = frappe.defaults.get_global_default("currency") + return fmt_money(value, precision=2, currency=currency) + + class TestBOMStockAnalysis(ERPNextTestSuite): def setUp(self): self.fg_item, self.rm_items = create_items() @@ -20,34 +29,62 @@ class TestBOMStockAnalysis(ERPNextTestSuite): qty_to_make = 10 # Case 1: When Item(s) Qty and Stock Qty are equal. - data = bom_stock_analysis_report( + raw_data = bom_stock_analysis_report( filters={ "qty_to_make": qty_to_make, "bom": self.boms[0].name, } )[1] - expected_data = get_expected_data(self.boms[0], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + data, footer = split_data_and_footer(raw_data) + expected_data, expected_min = get_expected_data(self.boms[0], qty_to_make) + + self.assertSetEqual( + set(tuple(sorted(r.items())) for r in data), + set(tuple(sorted(r.items())) for r in expected_data), + ) + self.assertEqual(footer.get("description"), expected_min) # Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1. - data = bom_stock_analysis_report( + raw_data = bom_stock_analysis_report( filters={ "qty_to_make": qty_to_make, "bom": self.boms[1].name, } )[1] - expected_data = get_expected_data(self.boms[1], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + data, footer = split_data_and_footer(raw_data) + expected_data, expected_min = get_expected_data(self.boms[1], qty_to_make) + + self.assertSetEqual( + set(tuple(sorted(r.items())) for r in data), + set(tuple(sorted(r.items())) for r in expected_data), + ) + self.assertEqual(footer.get("description"), expected_min) # Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1. - data = bom_stock_analysis_report( + raw_data = bom_stock_analysis_report( filters={ "qty_to_make": qty_to_make, "bom": self.boms[2].name, } )[1] - expected_data = get_expected_data(self.boms[2], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + data, footer = split_data_and_footer(raw_data) + expected_data, expected_min = get_expected_data(self.boms[2], qty_to_make) + + self.assertSetEqual( + set(tuple(sorted(r.items())) for r in data), + set(tuple(sorted(r.items())) for r in expected_data), + ) + self.assertEqual(footer.get("description"), expected_min) + + +def split_data_and_footer(raw_data): + """Separate component rows from the footer row. Skips blank spacer rows.""" + data = [row for row in raw_data if row and not row.get("bold")] + footer = next((row for row in raw_data if row and row.get("bold")), {}) + return data, footer def create_items(): @@ -79,7 +116,6 @@ def create_boms(fg_item, rm_items): for item in bom.items: item.uom = uom item.conversion_factor = conversion_factor - return bom bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10) @@ -98,22 +134,38 @@ def create_boms(fg_item, rm_items): def get_expected_data(bom, qty_to_make): + """ + Returns (component_rows, min_producible). + Component rows are dicts matching what the report produces. + min_producible is the expected footer value. + """ expected_data = [] + producible_per_item = [] + + for idx, bom_item in enumerate(bom.items): + qty_per_unit = float(bom_item.stock_qty / bom.quantity) + available_qty = float(100 * (idx + 1)) + required_qty = float(qty_to_make * qty_per_unit) + difference_qty = available_qty - required_qty + last_purchase_rate = float(100 * (idx + 1)) - for idx in range(len(bom.items)): expected_data.append( - [ - bom.items[idx].item_code, - bom.items[idx].item_code, - bom.name, - "", - "", - float(bom.items[idx].stock_qty / bom.quantity), - float(100 * (idx + 1)), - float(qty_to_make * (bom.items[idx].stock_qty / bom.quantity)), - float((100 * (idx + 1)) - (qty_to_make * (bom.items[idx].stock_qty / bom.quantity))), - float(100 * (idx + 1)), - ] + { + "item": bom_item.item_code, + "description": bom_item.item_code, # description falls back to item_code in test items + "from_bom_no": bom.name, + "manufacturer": "", + "manufacturer_part_number": "", + "qty_per_unit": fmt_qty(qty_per_unit), + "available_qty": fmt_qty(available_qty), + "required_qty": fmt_qty(required_qty), + "difference_qty": fmt_qty(difference_qty), + "last_purchase_rate": fmt_rate(last_purchase_rate), + } ) - return expected_data + producible_per_item.append(int(available_qty // qty_per_unit) if qty_per_unit else 0) + + min_producible = min(producible_per_item) if producible_per_item else 0 + + return expected_data, min_producible From d1a357191830fb2c4b99184f7d6a6645427e4451 Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Thu, 26 Mar 2026 15:15:53 +0530 Subject: [PATCH 63/90] chore: Dropping bom stock report and bom stock calculated report (cherry picked from commit 3bedc6cf7ea69ca016681dd9b2b245d86192aca8) # Conflicts: # erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py --- .../report/bom_stock_calculated/__init__.py | 0 .../bom_stock_calculated.js | 33 --- .../bom_stock_calculated.json | 26 --- .../bom_stock_calculated.py | 199 ------------------ .../report/bom_stock_report/__init__.py | 0 .../bom_stock_report/bom_stock_report.html | 27 --- .../bom_stock_report/bom_stock_report.js | 41 ---- .../bom_stock_report/bom_stock_report.json | 28 --- .../bom_stock_report/bom_stock_report.py | 106 ---------- .../bom_stock_report/test_bom_stock_report.py | 112 ---------- 10 files changed, 572 deletions(-) delete mode 100644 erpnext/manufacturing/report/bom_stock_calculated/__init__.py delete mode 100644 erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js delete mode 100644 erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json delete mode 100644 erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py delete mode 100644 erpnext/manufacturing/report/bom_stock_report/__init__.py delete mode 100644 erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html delete mode 100644 erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js delete mode 100644 erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json delete mode 100644 erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py delete mode 100644 erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py diff --git a/erpnext/manufacturing/report/bom_stock_calculated/__init__.py b/erpnext/manufacturing/report/bom_stock_calculated/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js deleted file mode 100644 index 76a95127853..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2016, Epoch Consulting and contributors -// For license information, please see license.txt - -frappe.query_reports["BOM Stock Calculated"] = { - filters: [ - { - fieldname: "bom", - label: __("BOM"), - fieldtype: "Link", - options: "BOM", - reqd: 1, - }, - { - fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", - options: "Warehouse", - }, - { - fieldname: "qty_to_make", - label: __("Quantity to Make"), - fieldtype: "Float", - default: "1.0", - reqd: 1, - }, - { - fieldname: "show_exploded_view", - label: __("Show exploded view"), - fieldtype: "Check", - default: false, - }, - ], -}; diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json deleted file mode 100644 index 73421cebf0e..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "add_total_row": 0, - "creation": "2018-05-17 12:40:31.355049", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "letter_head": "", - "modified": "2018-06-18 13:33:18.103220", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Stock Calculated", - "owner": "Administrator", - "ref_doctype": "BOM", - "report_name": "BOM Stock Calculated", - "report_type": "Script Report", - "roles": [ - { - "role": "Manufacturing Manager" - }, - { - "role": "Manufacturing User" - } - ] -} diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py deleted file mode 100644 index 4b5df4df4b2..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.query_builder.functions import IfNull, Sum -from frappe.utils.data import comma_and -from pypika.terms import ExistsCriterion - - -def execute(filters=None): - columns = get_columns() - data = [] - - bom_data = get_bom_data(filters) - qty_to_make = filters.get("qty_to_make") - manufacture_details = get_manufacturer_records() - - for row in bom_data: - required_qty = qty_to_make * row.qty_per_unit - last_purchase_rate = frappe.db.get_value("Item", row.item_code, "last_purchase_rate") - - data.append(get_report_data(last_purchase_rate, required_qty, row, manufacture_details)) - - return columns, data - - -def get_report_data(last_purchase_rate, required_qty, row, manufacture_details): - qty_per_unit = row.qty_per_unit if row.qty_per_unit > 0 else 0 - difference_qty = row.actual_qty - required_qty - return [ - row.item_code, - row.description, - row.from_bom_no, - comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False), - comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False), - qty_per_unit, - row.actual_qty, - required_qty, - difference_qty, - last_purchase_rate, - ] - - -def get_columns(): - return [ - { - "fieldname": "item", - "label": _("Item"), - "fieldtype": "Link", - "options": "Item", - "width": 120, - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 150, - }, - { - "fieldname": "from_bom_no", - "label": _("From BOM No"), - "fieldtype": "Link", - "options": "BOM", - "width": 150, - }, - { - "fieldname": "manufacturer", - "label": _("Manufacturer"), - "fieldtype": "Data", - "width": 120, - }, - { - "fieldname": "manufacturer_part_number", - "label": _("Manufacturer Part Number"), - "fieldtype": "Data", - "width": 150, - }, - { - "fieldname": "qty_per_unit", - "label": _("Qty Per Unit"), - "fieldtype": "Float", - "width": 110, - }, - { - "fieldname": "available_qty", - "label": _("Available Qty"), - "fieldtype": "Float", - "width": 120, - }, - { - "fieldname": "required_qty", - "label": _("Required Qty"), - "fieldtype": "Float", - "width": 120, - }, - { - "fieldname": "difference_qty", - "label": _("Difference Qty"), - "fieldtype": "Float", - "width": 130, - }, - { - "fieldname": "last_purchase_rate", - "label": _("Last Purchase Rate"), - "fieldtype": "Float", - "width": 160, - }, - ] - - -def get_bom_data(filters): - bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" - - bom_item = frappe.qb.DocType(bom_item_table) - bin = frappe.qb.DocType("Bin") - - query = ( - frappe.qb.from_(bom_item) - .left_join(bin) - .on(bom_item.item_code == bin.item_code) - .select( - bom_item.item_code, - bom_item.description, - bom_item.parent.as_("from_bom_no"), - bom_item.qty_consumed_per_unit.as_("qty_per_unit"), - IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), - ) - .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) - .groupby(bom_item.item_code) - .orderby(bom_item.idx) - ) - - if filters.get("warehouse"): - warehouse_details = frappe.db.get_value( - "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 - ) - - if warehouse_details: - wh = frappe.qb.DocType("Warehouse") - query = query.where( - ExistsCriterion( - frappe.qb.from_(wh) - .select(wh.name) - .where( - (wh.lft >= warehouse_details.lft) - & (wh.rgt <= warehouse_details.rgt) - & (bin.warehouse == wh.name) - ) - ) - ) - else: - query = query.where(bin.warehouse == filters.get("warehouse")) - - if bom_item_table == "BOM Item": - query = query.select(bom_item.bom_no, bom_item.is_phantom_item) - - data = query.run(as_dict=True) - return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data - - -def explode_phantom_boms(data, filters): - original_bom = filters.get("bom") - replacements = [] - - for idx, item in enumerate(data): - if not item.is_phantom_item: - continue - - filters["bom"] = item.bom_no - children = get_bom_data(filters) - filters["bom"] = original_bom - - for child in children: - child.qty_per_unit = (child.qty_per_unit or 0) * (item.qty_per_unit or 0) - - replacements.append((idx, children)) - - for idx, children in reversed(replacements): - data.pop(idx) - data[idx:idx] = children - - filters["bom"] = original_bom - return data - - -def get_manufacturer_records(): - details = frappe.get_all( - "Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"] - ) - - manufacture_details = frappe._dict() - for detail in details: - dic = manufacture_details.setdefault(detail.get("item_code"), {}) - dic.setdefault("manufacturer", []).append(detail.get("manufacturer")) - dic.setdefault("manufacturer_part", []).append(detail.get("manufacturer_part_no")) - - return manufacture_details diff --git a/erpnext/manufacturing/report/bom_stock_report/__init__.py b/erpnext/manufacturing/report/bom_stock_report/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html deleted file mode 100644 index 2ae8848cc03..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html +++ /dev/null @@ -1,27 +0,0 @@ -

{%= __("BOM Stock Report") %}

-
{%= filters.bom %}
-
{%= filters.warehouse %}
-
- -
{{ format_currency(summary[doctype].outstanding_amount, summary.currency, 2) }}
- - - - - - - - - - - {% for(var i=0, l=data.length; i - - - - - - - {% } %} - -
{%= __("Item") %}{%= __("Description") %}{%= __("Required Qty") %}{%= __("In Stock Qty") %}{%= __("Enough Parts to Build") %}
{%= data[i][ __("Item")] %}{%= data[i][ __("Description")] %} {%= data[i][ __("Required Qty")] %} {%= data[i][ __("In Stock Qty")] %} {%= data[i][ __("Enough Parts to Build")] %}
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js deleted file mode 100644 index 91d73d0101c..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ /dev/null @@ -1,41 +0,0 @@ -frappe.query_reports["BOM Stock Report"] = { - filters: [ - { - fieldname: "bom", - label: __("BOM"), - fieldtype: "Link", - options: "BOM", - reqd: 1, - }, - { - fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", - options: "Warehouse", - reqd: 1, - }, - { - fieldname: "show_exploded_view", - label: __("Show exploded view"), - fieldtype: "Check", - }, - { - fieldname: "qty_to_produce", - label: __("Quantity to Produce"), - fieldtype: "Int", - default: "1", - }, - ], - formatter: function (value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data); - - if (column.id == "item") { - if (data["in_stock_qty"] >= data["required_qty"]) { - value = `${data["item"]}`; - } else { - value = `${data["item"]}`; - } - } - return value; - }, -}; diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json deleted file mode 100644 index c563b87686d..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2017-01-10 14:00:50.387244", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "letter_head": "", - "modified": "2017-06-23 04:46:43.209008", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Stock Report", - "owner": "Administrator", - "query": "SELECT \n\tbom_item.item_code as \"Item:Link/Item:200\",\n\tbom_item.description as \"Description:Data:300\",\n\tbom_item.qty as \"Required Qty:Float:100\",\n\tledger.actual_qty as \"In Stock Qty:Float:100\",\n\tFLOOR(ledger.actual_qty /bom_item.qty) as \"Enough Parts to Build:Int:100\"\nFROM\n\t`tabBOM Item` AS bom_item \n\tLEFT JOIN `tabBin` AS ledger\t\n\t\tON bom_item.item_code = ledger.item_code \n\t\tAND ledger.warehouse = %(warehouse)s\nWHERE\n\tbom_item.parent=%(bom)s\n\nGROUP BY bom_item.item_code", - "ref_doctype": "BOM", - "report_name": "BOM Stock Report", - "report_type": "Script Report", - "roles": [ - { - "role": "Manufacturing Manager" - }, - { - "role": "Manufacturing User" - } - ] -} \ No newline at end of file diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py deleted file mode 100644 index eeda32c64c7..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from frappe.query_builder.functions import Floor, Sum -from frappe.utils import cint - - -def execute(filters=None): - if not filters: - filters = {} - - columns = get_columns() - data = get_bom_stock(filters) - - return columns, data - - -def get_columns(): - return [ - _("Item") + ":Link/Item:150", - _("Item Name") + "::240", - _("Description") + "::300", - _("From BOM No") + "::200", - _("BOM Qty") + ":Float:160", - _("BOM UOM") + "::160", - _("Required Qty") + ":Float:120", - _("In Stock Qty") + ":Float:120", - _("Enough Parts to Build") + ":Float:200", - ] - - -def get_bom_stock(filters): - qty_to_produce = filters.get("qty_to_produce") - if cint(qty_to_produce) <= 0: - frappe.throw(_("Quantity to Produce should be greater than zero.")) - - bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" - - warehouse = filters.get("warehouse") - warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) - - BOM = frappe.qb.DocType("BOM") - BOM_ITEM = frappe.qb.DocType(bom_item_table) - BIN = frappe.qb.DocType("Bin") - WH = frappe.qb.DocType("Warehouse") - - if warehouse_details: - bin_subquery = ( - frappe.qb.from_(BIN) - .join(WH) - .on(BIN.warehouse == WH.name) - .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) - .where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt)) - .groupby(BIN.item_code) - ) - else: - bin_subquery = ( - frappe.qb.from_(BIN) - .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) - .where(BIN.warehouse == warehouse) - .groupby(BIN.item_code) - ) - - QUERY = ( - frappe.qb.from_(BOM) - .join(BOM_ITEM) - .on(BOM.name == BOM_ITEM.parent) - .left_join(bin_subquery) - .on(BOM_ITEM.item_code == bin_subquery.item_code) - .select( - BOM_ITEM.item_code, - BOM_ITEM.item_name, - BOM_ITEM.description, - BOM.name, - Sum(BOM_ITEM.stock_qty), - BOM_ITEM.stock_uom, - (Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity, - bin_subquery.actual_qty, - Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity)), - ) - .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) - .groupby(BOM_ITEM.item_code) - .orderby(BOM_ITEM.idx) - ) - - if bom_item_table == "BOM Item": - QUERY = QUERY.select(BOM_ITEM.bom_no, BOM_ITEM.is_phantom_item) - - data = QUERY.run(as_list=True) - return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data - - -def explode_phantom_boms(data, filters): - expanded = [] - for row in data: - if row[-1]: # last element is `is_phantom_item` - phantom_filters = filters.copy() - phantom_filters["qty_to_produce"] = row[-5] - phantom_filters["bom"] = row[-2] - expanded.extend(get_bom_stock(phantom_filters)) - else: - expanded.append(row) - - return expanded diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py deleted file mode 100644 index 43706fcb4de..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe.exceptions import ValidationError -from frappe.utils import floor - -from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom -from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import ( - get_bom_stock as bom_stock_report, -) -from erpnext.stock.doctype.item.test_item import make_item -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -from erpnext.tests.utils import ERPNextTestSuite - - -class TestBomStockReport(ERPNextTestSuite): - def setUp(self): - self.warehouse = "_Test Warehouse - _TC" - self.fg_item, self.rm_items = create_items() - make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100) - make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200) - self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10) - - def test_bom_stock_report(self): - # Test 1: When `qty_to_produce` is 0. - filters = frappe._dict( - { - "bom": self.bom.name, - "warehouse": "Stores - _TC", - "qty_to_produce": 0, - } - ) - self.assertRaises(ValidationError, bom_stock_report, filters) - - # Test 2: When stock is not available. - data = bom_stock_report( - frappe._dict( - { - "bom": self.bom.name, - "warehouse": "Stores - _TC", - "qty_to_produce": 1, - } - ) - ) - expected_data = get_expected_data(self.bom, "Stores - _TC", 1) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - # Test 3: When stock is available. - data = bom_stock_report( - frappe._dict( - { - "bom": self.bom.name, - "warehouse": self.warehouse, - "qty_to_produce": 1, - } - ) - ) - expected_data = get_expected_data(self.bom, self.warehouse, 1) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - -def create_items(): - fg_item = make_item(properties={"is_stock_item": 1}).name - rm_item1 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 100, - "opening_stock": 100, - "last_purchase_rate": 100, - } - ).name - rm_item2 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 200, - "opening_stock": 200, - "last_purchase_rate": 200, - } - ).name - - return fg_item, [rm_item1, rm_item2] - - -def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False): - expected_data = [] - - for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"): - in_stock_qty = frappe.get_cached_value( - "Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty" - ) - - expected_data.append( - [ - item.item_code, - item.item_name, - item.description, - bom.name, - item.stock_qty, - item.stock_uom, - item.stock_qty * qty_to_produce / bom.quantity, - in_stock_qty, - floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity)) - if in_stock_qty - else None, - item.bom_no, - item.is_phantom_item, - ] - ) - - return expected_data From 5039f896bf93a7c618cb3edbcff3ad9bad452f84 Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Thu, 26 Mar 2026 15:40:28 +0530 Subject: [PATCH 64/90] fix: test case (cherry picked from commit 3a78af7f422f81fd64119ab73a59feb30e174e53) --- .../report/bom_stock_analysis/bom_stock_analysis.js | 6 +++--- .../report/bom_stock_analysis/bom_stock_analysis.py | 5 +++-- erpnext/manufacturing/report/test_reports.py | 3 +-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js index 7c6ccfdf743..7629c102d7c 100644 --- a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js @@ -45,9 +45,9 @@ frappe.query_reports["BOM Stock Analysis"] = { } if (data && data.bold) { - if (column.fieldname === "description" || column.fieldname === "item_name") { - const qty_to_make = frappe.query_report.get_filter_value("qty_to_make"); - const producible = parseFloat(value) || 0; + if (column.fieldname === "description") { + const qty_to_make = Number(frappe.query_report.get_filter_value("qty_to_make")) || 0; + const producible = Number(String(data.description ?? "").replace(/,/g, "")) || 0; const colour = qty_to_make && producible < qty_to_make ? "red" : "green"; return `${value}`; } diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py index 78aa75aa7fa..59578127f9f 100644 --- a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py @@ -10,6 +10,7 @@ from pypika.terms import ExistsCriterion def execute(filters=None): + filters = filters or {} if filters.get("qty_to_make"): columns = get_columns_with_qty_to_make() data = get_data_with_qty_to_make(filters) @@ -35,7 +36,7 @@ def get_data_with_qty_to_make(filters): bom_data = get_bom_data(filters) manufacture_details = get_manufacturer_records() purchase_rates = batch_fetch_purchase_rates(bom_data) - qty_to_make = filters.get("qty_to_make") + qty_to_make = flt(filters.get("qty_to_make")) data = [] for row in bom_data: @@ -203,7 +204,7 @@ def get_bom_data(filters): bom_item.item_code, bom_item.description, bom_item.parent.as_("from_bom_no"), - bom_item.qty_consumed_per_unit.as_("qty_per_unit"), + Sum(bom_item.qty_consumed_per_unit).as_("qty_per_unit"), IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), ) .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py index adeddd996a5..505a82c3501 100644 --- a/erpnext/manufacturing/report/test_reports.py +++ b/erpnext/manufacturing/report/test_reports.py @@ -21,8 +21,7 @@ class TestManufacturingReports(ERPNextTestSuite): self.REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("BOM Explorer", {"bom": self.last_bom}), ("BOM Operations Time", {}), - ("BOM Stock Calculated", {"bom": self.last_bom, "qty_to_make": 2}), - ("BOM Stock Report", {"bom": self.last_bom, "qty_to_produce": 2}), + ("BOM Stock Analysis", {"bom": self.last_bom, "_optional": ["warehouse"]}), ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}), ("Downtime Analysis", {}), ( From dd0613a4a8be5b54b873e976a3eec79b8d947196 Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Fri, 6 Mar 2026 16:07:34 +0530 Subject: [PATCH 65/90] fix(stock): handle legacy single sle recon entries (cherry picked from commit 7e6bbcc3fb3accca18d21600fe3d8608267715df) --- .../stock/report/stock_ageing/stock_ageing.py | 71 +++++++++++++++++-- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index c6990c9492d..2ea52e91a8d 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -7,6 +7,7 @@ from operator import itemgetter import frappe from frappe import _ +from frappe.query_builder.functions import Count from frappe.utils import cint, date_diff, flt, get_datetime from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -240,9 +241,9 @@ class FIFOSlots: Returns dict of the foll.g structure: Key = Item A / (Item A, Warehouse A) Key: { - 'details' -> Dict: ** item details **, - 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, - consumed/updated and maintained via FIFO. ** + 'details' -> Dict: ** item details **, + 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, + consumed/updated and maintained via FIFO. ** } """ from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle @@ -253,16 +254,33 @@ class FIFOSlots: if stock_ledger_entries is None: bundle_wise_serial_nos = self.__get_bundle_wise_serial_nos() + # prepare single sle voucher detail lookup + self.prepare_stock_reco_voucher_wise_count() + with frappe.db.unbuffered_cursor(): if stock_ledger_entries is None: stock_ledger_entries = self.__get_stock_ledger_entries() for d in stock_ledger_entries: key, fifo_queue, transferred_item_key = self.__init_key_stores(d) + prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) - if d.voucher_type == "Stock Reconciliation": + if d.voucher_type == "Stock Reconciliation" and ( + not d.batch_no or d.serial_no or d.serial_and_batch_bundle + ): + if d.voucher_detail_no in self.stock_reco_voucher_wise_count: + # for legacy recon with single sle has qty_after_transaction and stock_value_difference without outward entry + # for exisitng handle emptying the existing queue and details. + d.stock_value_difference = flt(d.qty_after_transaction * d.valuation_rate) + d.actual_qty = d.qty_after_transaction + self.item_details[key]["qty_after_transaction"] = 0 + self.item_details[key]["total_qty"] = 0 + fifo_queue.clear() + else: + d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) + + elif d.voucher_type == "Stock Reconciliation": # get difference in qty shift as actual qty - prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] @@ -280,6 +298,14 @@ class FIFOSlots: self.__update_balances(d, key) + # handle serial nos misconsumption + if d.has_serial_no: + qty_after = cint(self.item_details[key]["qty_after_transaction"]) + if qty_after <= 0: + fifo_queue.clear() + elif len(fifo_queue) > qty_after: + fifo_queue[:] = fifo_queue[:qty_after] + # Note that stock_ledger_entries is an iterator, you can not reuse it like a list del stock_ledger_entries @@ -406,7 +432,6 @@ class FIFOSlots: def __update_balances(self, row: dict, key: tuple | str): self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction - if "total_qty" not in self.item_details[key]: self.item_details[key]["total_qty"] = row.actual_qty else: @@ -462,6 +487,7 @@ class FIFOSlots: sle.posting_date, sle.voucher_type, sle.voucher_no, + sle.voucher_detail_no, sle.serial_no, sle.batch_no, sle.qty_after_transaction, @@ -558,3 +584,36 @@ class FIFOSlots: warehouse_results = [x[0] for x in warehouse_results] return sle_query.where(sle.warehouse.isin(warehouse_results)) + + def prepare_stock_reco_voucher_wise_count(self): + self.stock_reco_voucher_wise_count = frappe._dict() + + doctype = frappe.qb.DocType("Stock Ledger Entry") + item = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(doctype) + .inner_join(item) + .on(doctype.item_code == item.name) + .select(doctype.voucher_detail_no, Count(doctype.name).as_("count")) + .where( + (doctype.voucher_type == "Stock Reconciliation") + & (doctype.docstatus < 2) + & (doctype.is_cancelled == 0) + ) + .groupby(doctype.voucher_detail_no) + ) + + data = query.run(as_dict=True) + if not data: + return + + for row in data: + if row.count != 1: + continue + + sr_item = frappe.db.get_value( + "Stock Reconciliation Item", row.voucher_detail_no, ["current_qty", "qty"], as_dict=True + ) + if sr_item.qty and sr_item.current_qty: + self.stock_reco_voucher_wise_count[row.voucher_detail_no] = sr_item.current_qty From 3a8e1e3faa57c016decd7a1e4c99d7b7cf4938e7 Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Fri, 27 Mar 2026 12:03:20 +0530 Subject: [PATCH 66/90] fix: test file deletion --- .../test_bom_stock_calculated.py | 119 ------------------ 1 file changed, 119 deletions(-) delete mode 100644 erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py diff --git a/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py deleted file mode 100644 index e0105b114c5..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe - -from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom -from erpnext.manufacturing.report.bom_stock_calculated.bom_stock_calculated import ( - execute as bom_stock_calculated_report, -) -from erpnext.stock.doctype.item.test_item import make_item -from erpnext.tests.utils import ERPNextTestSuite - - -class TestBOMStockCalculated(ERPNextTestSuite): - def setUp(self): - self.fg_item, self.rm_items = create_items() - self.boms = create_boms(self.fg_item, self.rm_items) - - def test_bom_stock_calculated(self): - qty_to_make = 10 - - # Case 1: When Item(s) Qty and Stock Qty are equal. - data = bom_stock_calculated_report( - filters={ - "qty_to_make": qty_to_make, - "bom": self.boms[0].name, - } - )[1] - expected_data = get_expected_data(self.boms[0], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - # Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1. - data = bom_stock_calculated_report( - filters={ - "qty_to_make": qty_to_make, - "bom": self.boms[1].name, - } - )[1] - expected_data = get_expected_data(self.boms[1], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - # Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1. - data = bom_stock_calculated_report( - filters={ - "qty_to_make": qty_to_make, - "bom": self.boms[2].name, - } - )[1] - expected_data = get_expected_data(self.boms[2], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - -def create_items(): - fg_item = make_item(properties={"is_stock_item": 1}).name - rm_item1 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 100, - "opening_stock": 100, - "last_purchase_rate": 100, - "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], - } - ).name - rm_item2 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 200, - "opening_stock": 200, - "last_purchase_rate": 200, - "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], - } - ).name - - return fg_item, [rm_item1, rm_item2] - - -def create_boms(fg_item, rm_items): - def update_bom_items(bom, uom, conversion_factor): - for item in bom.items: - item.uom = uom - item.conversion_factor = conversion_factor - - return bom - - bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10) - - bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True) - bom2 = update_bom_items(bom2, "Box", 10) - bom2.save() - bom2.submit() - - bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True) - bom3 = update_bom_items(bom3, "Box", 10) - bom3.save() - bom3.submit() - - return [bom1, bom2, bom3] - - -def get_expected_data(bom, qty_to_make): - expected_data = [] - - for idx in range(len(bom.items)): - expected_data.append( - [ - bom.items[idx].item_code, - bom.items[idx].item_code, - bom.name, - "", - "", - float(bom.items[idx].stock_qty / bom.quantity), - float(100 * (idx + 1)), - float(qty_to_make * (bom.items[idx].stock_qty / bom.quantity)), - float((100 * (idx + 1)) - (qty_to_make * (bom.items[idx].stock_qty / bom.quantity))), - float(100 * (idx + 1)), - ] - ) - - return expected_data From 5bbecbf7c4194969731de58cef363cd140bfb260 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 27 Jan 2026 23:35:35 +0530 Subject: [PATCH 67/90] refactor: reposting for better peformance (cherry picked from commit 20787ef5da3a71e3b4a9970470ef035d7c225786) --- .../repost_item_valuation.py | 2 +- erpnext/stock/stock_ledger.py | 240 ++++++++++-------- 2 files changed, 142 insertions(+), 100 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index f5b4ef3e8f5..c68d3ed329a 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -435,7 +435,7 @@ def repost_sl_entries(doc): ) else: repost_future_sle( - args=[ + item_wh_to_repost=[ frappe._dict( { "item_code": doc.item_code, diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 1e6cec59a5c..6810f45827c 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -16,6 +16,7 @@ from frappe.utils import ( cstr, flt, format_date, + get_datetime, get_link_to_form, getdate, now, @@ -242,15 +243,15 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): def repost_future_sle( - args=None, + item_wh_to_repost=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False, doc=None, ): - if not args: - args = [] # set args to empty list if None to avoid enumerate error + if not item_wh_to_repost: + item_wh_to_repost = [] # set args to empty list if None to avoid enumerate error reposting_data = {} if doc and doc.reposting_data_file: @@ -260,53 +261,34 @@ def repost_future_sle( voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data ) if items_to_be_repost: - args = items_to_be_repost - - distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data) - affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data) + item_wh_to_repost = items_to_be_repost + distinct_item_wh_sles = frappe._dict() + prev_sle_dict = frappe._dict() + vouchers_to_repost = set() + item_wh_idx = 0 i = get_current_index(doc) or 0 - while i < len(args): - validate_item_warehouse(args[i]) - obj = update_entries_after( + while i < len(item_wh_to_repost): + update_entries_after( { - "item_code": args[i].get("item_code"), - "warehouse": args[i].get("warehouse"), - "posting_date": args[i].get("posting_date"), - "posting_time": args[i].get("posting_time"), - "creation": args[i].get("creation"), - "distinct_item_warehouses": distinct_item_warehouses, - "items_to_be_repost": args, - "current_index": i, + "item_code": item_wh_to_repost[i].get("item_code"), + "warehouse": item_wh_to_repost[i].get("warehouse"), + "posting_date": item_wh_to_repost[i].get("posting_date"), + "posting_time": item_wh_to_repost[i].get("posting_time"), + "creation": item_wh_to_repost[i].get("creation"), + "distinct_item_wh_sles": distinct_item_wh_sles, + "prev_sle_dict": prev_sle_dict, + "vouchers_to_repost": vouchers_to_repost, + "item_wh_idx": item_wh_idx, + "current_idx": i, }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, ) - affected_transactions.update(obj.affected_transactions) - key = (args[i].get("item_code"), args[i].get("warehouse")) - if distinct_item_warehouses.get(key): - distinct_item_warehouses[key].reposting_status = True - - if obj.new_items_found: - for _item_wh, data in distinct_item_warehouses.items(): - if ("args_idx" not in data and not data.reposting_status) or ( - data.sle_changed and data.reposting_status - ): - data.args_idx = len(args) - args.append(data.sle) - elif data.sle_changed and not data.reposting_status: - args[data.args_idx] = data.sle - - data.sle_changed = False i += 1 - if doc: - update_args_in_repost_item_valuation( - doc, i, args, distinct_item_warehouses, affected_transactions - ) - def get_reposting_data(file_path) -> dict: file_name = frappe.db.get_value( @@ -552,6 +534,11 @@ class update_entries_after: self.allow_zero_rate = allow_zero_rate self.via_landed_cost_voucher = via_landed_cost_voucher self.item_code = args.get("item_code") + self.distinct_item_wh_sles = args.get("distinct_item_wh_sles", frappe._dict()) + self.prev_sle_dict = args.get("prev_sle_dict", frappe._dict()) + self.vouchers_to_repost = args.get("vouchers_to_repost", set()) + self.item_wh_idx = args.get("item_wh_idx", 0) + self.current_idx = args.get("current_idx", 0) self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed( item_code=self.item_code @@ -566,7 +553,6 @@ class update_entries_after: self.valuation_method = get_valuation_method(self.item_code, self.company) self.new_items_found = False - self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) self.affected_transactions: set[tuple[str, str]] = set() self.reserved_stock = self.get_reserved_stock() @@ -622,6 +608,8 @@ class update_entries_after: self.data.setdefault(args.warehouse, frappe._dict()) warehouse_dict = self.data[args.warehouse] previous_sle = get_previous_sle_of_current_voucher(args) + self.prev_sle_dict[(args.get("item_code"), args.get("warehouse"))] = previous_sle + warehouse_dict.previous_sle = previous_sle for key in ("qty_after_transaction", "valuation_rate", "stock_value"): @@ -642,28 +630,78 @@ class update_entries_after: self.process_sle_against_current_timestamp() if not future_sle_exists(self.args): self.update_bin() + elif self.vouchers_to_repost: + self.repost_vouchers() else: - entries_to_fix = self.get_future_entries_to_fix() + kwargs = frappe._dict( + { + "item_code": self.item_code, + "warehouse": self.args.warehouse, + "posting_date": self.args.posting_date, + "posting_time": self.args.posting_time, + "creation": self.args.creation, + } + ) - i = 0 - while i < len(entries_to_fix): - sle = entries_to_fix[i] - i += 1 + self.prepare_sles_to_repost(kwargs) + self.repost_vouchers() + self.update_bin() - self.process_sle(sle) - self.update_bin_data(sle) + def repost_vouchers(self): + vouchers = self.vouchers_to_repost or self.get_vouchere_to_repost() + for row in vouchers: + self.process_sle(row) - if sle.dependant_sle_voucher_detail_no: - entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle) - if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no): - # for repack entries, we need to repost both source and target warehouses - self.update_distinct_item_warehouses_for_repack(sle) + def get_vouchere_to_repost(self): + if not self.distinct_item_wh_sles: + return [] - if self.exceptions: - self.raise_exceptions() + sorted_rows = sorted( + (row for rows in self.distinct_item_wh_sles.values() for row in rows), + key=lambda d: (get_datetime(d.get("posting_datetime")), get_datetime(d.get("creation"))), + ) - def update_distinct_item_warehouses_for_repack(self, sle): - sles = ( + return sorted_rows + + def prepare_sles_to_repost(self, kwargs): + sles = self.get_future_entries_to_repost(kwargs) + for sle in sles: + item_wh_key = (sle.item_code, sle.warehouse) + if item_wh_key not in self.prev_sle_dict: + prev_sle = get_previous_sle_of_current_voucher(kwargs) + self.prev_sle_dict[item_wh_key] = prev_sle + + key = (sle.item_code, sle.warehouse, sle.voucher_detail_no) + if key not in self.distinct_item_wh_sles: + self.distinct_item_wh_sles.setdefault(key, []).append(sle) + + if sle.dependant_sle_voucher_detail_no: + self.prepare_dependent_sles_to_repost(sle) + + def prepare_dependent_sles_to_repost(self, sle): + if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no): + sles = self.get_sles_for_repack(sle) + for repack_sle in sles: + key = (repack_sle.item_code, repack_sle.warehouse, repack_sle.voucher_detail_no) + if key not in self.distinct_item_wh_sles: + self.distinct_item_wh_sles.setdefault(key, []).append(repack_sle) + + self.prepare_sles_to_repost(repack_sle) + + elif sle.dependant_sle_voucher_detail_no: + dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no) + + key = (dependant_sle.item_code, dependant_sle.warehouse, dependant_sle.voucher_detail_no) + if key not in self.distinct_item_wh_sles: + self.distinct_item_wh_sles.setdefault(key, []).append(dependant_sle) + + self.prepare_sles_to_repost(dependant_sle) + + def get_future_entries_to_repost(self, kwargs): + return get_stock_ledger_entries(kwargs, ">=", "asc", for_update=True, check_serial_no=False) + + def get_sles_for_repack(self, sle): + return ( frappe.get_all( "Stock Ledger Entry", filters={ @@ -671,16 +709,13 @@ class update_entries_after: "voucher_no": sle.voucher_no, "actual_qty": (">", 0), "is_cancelled": 0, - "voucher_detail_no": ("!=", sle.dependant_sle_voucher_detail_no), + "dependant_sle_voucher_detail_no": ("!=", sle.dependant_sle_voucher_detail_no), }, fields=["*"], ) or [] ) - for dependant_sle in sles: - self.update_distinct_item_warehouses(dependant_sle) - def has_stock_reco_with_serial_batch(self, sle): if ( sle.voucher_type == "Stock Reconciliation" @@ -691,33 +726,11 @@ class update_entries_after: return False def process_sle_against_current_timestamp(self): - sl_entries = self.get_sle_against_current_voucher() + sl_entries = get_sle_against_current_voucher(self.args) for sle in sl_entries: sle["timestamp"] = sle.posting_datetime self.process_sle(sle) - def get_sle_against_current_voucher(self): - self.args["posting_datetime"] = get_combine_datetime(self.args.posting_date, self.args.posting_time) - doctype = frappe.qb.DocType("Stock Ledger Entry") - - query = ( - frappe.qb.from_(doctype) - .select("*") - .where( - (doctype.item_code == self.args.item_code) - & (doctype.warehouse == self.args.warehouse) - & (doctype.is_cancelled == 0) - & (doctype.posting_datetime == self.args.posting_datetime) - ) - .orderby(doctype.creation, order=Order.asc) - .for_update() - ) - - if not self.args.get("cancelled"): - query = query.where(doctype.creation == self.args.creation) - - return query.run(as_dict=True) - def get_future_entries_to_fix(self): # includes current entry! args = self.data[self.args.warehouse].previous_sle or frappe._dict( @@ -797,7 +810,7 @@ class update_entries_after: return self.distinct_item_warehouses[key].dependent_voucher_detail_nos def validate_previous_sle_qty(self, sle): - previous_sle = self.data[sle.warehouse].previous_sle + previous_sle = self.prev_sle_dict.get((sle.item_code, sle.warehouse)) if previous_sle and previous_sle.get("qty_after_transaction") < 0 and sle.get("actual_qty") > 0: frappe.msgprint( _( @@ -816,7 +829,22 @@ class update_entries_after: def process_sle(self, sle): # previous sle data for this warehouse - self.wh_data = self.data[sle.warehouse] + key = (sle.item_code, sle.warehouse) + self.wh_data = self.prev_sle_dict.get(key) or frappe._dict( + { + "qty_after_transaction": 0.0, + "valuation_rate": 0.0, + "stock_value": 0.0, + "prev_stock_value": 0.0, + "stock_queue": [], + } + ) + + if self.wh_data.stock_queue and isinstance(self.wh_data.stock_queue, str): + self.wh_data.stock_queue = json.loads(self.wh_data.stock_queue) + + if not self.wh_data.prev_stock_value: + self.wh_data.prev_stock_value = self.wh_data.stock_value self.validate_previous_sle_qty(sle) self.affected_transactions.add((sle.voucher_type, sle.voucher_no)) @@ -946,6 +974,8 @@ class update_entries_after: sle.modified = now() frappe.get_doc(sle).db_update() + self.prev_sle_dict[key] = sle + if not self.args.get("sle_id") or ( sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle ): @@ -1728,8 +1758,8 @@ class update_entries_after: def update_bin(self): # update bin for each warehouse - for warehouse, data in self.data.items(): - bin_name = get_or_make_bin(self.item_code, warehouse) + for (item_code, warehouse), data in self.prev_sle_dict.items(): + bin_name = get_or_make_bin(item_code, warehouse) updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value} if data.valuation_rate is not None: @@ -1737,6 +1767,29 @@ class update_entries_after: frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True) +def get_sle_against_current_voucher(kwargs): + kwargs["posting_datetime"] = get_combine_datetime(kwargs.posting_date, kwargs.posting_time) + doctype = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(doctype) + .select("*") + .where( + (doctype.item_code == kwargs.item_code) + & (doctype.warehouse == kwargs.warehouse) + & (doctype.is_cancelled == 0) + & (doctype.posting_datetime == kwargs.posting_datetime) + ) + .orderby(doctype.creation, order=Order.asc) + .for_update() + ) + + if not kwargs.get("cancelled"): + query = query.where(doctype.creation == kwargs.creation) + + return query.run(as_dict=True) + + def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False): """get stock ledger entries filtered by specific posting datetime conditions""" @@ -1893,18 +1946,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): return frappe.db.get_value( "Stock Ledger Entry", {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0}, - [ - "item_code", - "warehouse", - "actual_qty", - "qty_after_transaction", - "posting_date", - "posting_time", - "voucher_detail_no", - "posting_datetime as timestamp", - "voucher_type", - "voucher_no", - ], + ["*"], as_dict=1, ) From f663f9b27ef9fd7e00c0e4f7f81c221f4715f487 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 28 Jan 2026 17:24:09 +0530 Subject: [PATCH 68/90] refactor: storing of current status of reposting (cherry picked from commit daa2420996f9fe020de3390eb08992386529f442) --- .../repost_item_valuation.js | 40 +- .../repost_item_valuation.json | 121 +++- .../repost_item_valuation.py | 18 +- .../stock_ledger_variance.py | 7 +- erpnext/stock/stock_ledger.py | 543 ++++++++---------- 5 files changed, 393 insertions(+), 336 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index 0f64949f621..134903f2309 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -69,9 +69,15 @@ frappe.ui.form.on("Repost Item Valuation", { } if (frm.doc.status == "In Progress") { - frm.doc.current_index = data.current_index; - frm.doc.items_to_be_repost = data.items_to_be_repost; - frm.doc.total_reposting_count = data.total_reposting_count; + if (data.current_index) { + frm.doc.current_index = data.current_index; + frm.doc.items_to_be_repost = data.items_to_be_repost; + } + + if (data.vouchers_posted) { + frm.doc.total_vouchers = data.total_vouchers; + frm.doc.vouchers_posted = data.vouchers_posted; + } frm.dashboard.reset(); frm.trigger("show_reposting_progress"); @@ -108,15 +114,31 @@ frappe.ui.form.on("Repost Item Valuation", { show_reposting_progress: function (frm) { var bars = []; - + let title = ""; + let progress = 0.0; let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0; - if (frm.doc?.total_reposting_count) { - total_count = frm.doc.total_reposting_count; + if (total_count > 1) { + progress = flt((cint(frm.doc.current_index) / total_count) * 100, 2) || 0.5; + title = __("Reposting for Item-Wh Completed {0}%", [progress]); + + bars.push({ + title: title, + width: progress + "%", + progress_class: "progress-bar-success", + }); + + frm.dashboard.add_progress(__("Reposting Progress"), bars); } - let progress = flt((cint(frm.doc.current_index) / total_count) * 100, 2) || 0.5; - var title = __("Reposting Completed {0}%", [progress]); + if (!frm.doc.vouchers_posted) { + return; + } + + // Show voucher posting progress if vouchers are being reposted + bars = []; + progress = flt((cint(frm.doc.vouchers_posted) / cint(frm.doc.total_vouchers)) * 100, 2) || 0.5; + title = __("Reposting for Vouchers Completed {0}%", [progress]); bars.push({ title: title, @@ -124,7 +146,7 @@ frappe.ui.form.on("Repost Item Valuation", { progress_class: "progress-bar-success", }); - frm.dashboard.add_progress(__("Reposting Progress"), bars); + frm.dashboard.add_progress(__("Reposting Vouchers Progress"), bars); }, restart_reposting: function (frm) { diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index e1ae6d00cd9..3b4ae7220a5 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -27,14 +27,23 @@ "error_section", "error_log", "reposting_info_section", - "reposting_data_file", "items_to_be_repost", - "distinct_item_and_warehouse", "column_break_o1sj", "total_reposting_count", "current_index", "gl_reposting_index", - "affected_transactions" + "vouchers_based_on_item_and_warehouse_section", + "total_vouchers", + "column_break_yqwo", + "vouchers_posted", + "last_sle_posted_section", + "reposted_item_code", + "reposted_warehouse", + "reposting_data_file", + "column_break_miwc", + "sle_posting_date", + "sle_posting_time", + "reposted_sle_creation" ], "fields": [ { @@ -167,15 +176,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "distinct_item_and_warehouse", - "fieldtype": "Code", - "hidden": 1, - "label": "Distinct Item and Warehouse", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "current_index", "fieldtype": "Int", @@ -185,14 +185,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "affected_transactions", - "fieldtype": "Code", - "hidden": 1, - "label": "Affected Transactions", - "no_copy": 1, - "read_only": 1 - }, { "default": "0", "fieldname": "gl_reposting_index", @@ -205,7 +197,7 @@ { "fieldname": "reposting_info_section", "fieldtype": "Section Break", - "label": "Reposting Info" + "label": "Reposting Item and Warehouse" }, { "fieldname": "column_break_o1sj", @@ -214,14 +206,7 @@ { "fieldname": "total_reposting_count", "fieldtype": "Int", - "label": "Total Reposting Count", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "reposting_data_file", - "fieldtype": "Attach", - "label": "Reposting Data File", + "label": "No of Items to Repost", "no_copy": 1, "read_only": 1 }, @@ -247,13 +232,89 @@ "fieldname": "repost_only_accounting_ledgers", "fieldtype": "Check", "label": "Repost Only Accounting Ledgers" + }, + { + "fieldname": "vouchers_based_on_item_and_warehouse_section", + "fieldtype": "Section Break", + "label": "Reposting Vouchers" + }, + { + "fieldname": "total_vouchers", + "fieldtype": "Int", + "label": "Total Ledgers", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_yqwo", + "fieldtype": "Column Break" + }, + { + "fieldname": "vouchers_posted", + "fieldtype": "Int", + "label": "Ledgers Posted", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "last_sle_posted_section", + "fieldtype": "Section Break", + "label": "Last SLE Posted" + }, + { + "fieldname": "reposted_item_code", + "fieldtype": "Link", + "label": "Reposted Item Code", + "no_copy": 1, + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "reposted_warehouse", + "fieldtype": "Link", + "label": "Reposted Warehouse", + "no_copy": 1, + "options": "Warehouse", + "read_only": 1 + }, + { + "fieldname": "column_break_miwc", + "fieldtype": "Column Break" + }, + { + "fieldname": "reposted_sle_creation", + "fieldtype": "Datetime", + "label": "Reposted SLE Creation", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "sle_posting_date", + "fieldtype": "Date", + "label": "SLE Posting Date", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "sle_posting_time", + "fieldtype": "Time", + "label": "SLE Posting Time", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "reposting_data_file", + "fieldtype": "Attach", + "label": "Reposting Data File", + "no_copy": 1, + "read_only": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-02-25 14:22:21.681549", + "modified": "2026-03-26 13:52:51.895504", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index c68d3ed329a..d7397682ee6 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -35,14 +35,12 @@ class RepostItemValuation(Document): if TYPE_CHECKING: from frappe.types import DF - affected_transactions: DF.Code | None allow_negative_stock: DF.Check allow_zero_rate: DF.Check amended_from: DF.Link | None based_on: DF.Literal["Transaction", "Item and Warehouse"] company: DF.Link | None current_index: DF.Int - distinct_item_and_warehouse: DF.Code | None error_log: DF.LongText | None gl_reposting_index: DF.Int item_code: DF.Link | None @@ -51,13 +49,20 @@ class RepostItemValuation(Document): posting_time: DF.Time | None recreate_stock_ledgers: DF.Check repost_only_accounting_ledgers: DF.Check + reposted_item_code: DF.Link | None + reposted_sle_creation: DF.Datetime | None + reposted_warehouse: DF.Link | None reposting_data_file: DF.Attach | None reposting_reference: DF.Data | None + sle_posting_date: DF.Date | None + sle_posting_time: DF.Time | None status: DF.Literal["Queued", "In Progress", "Completed", "Skipped", "Failed", "Cancelled"] total_reposting_count: DF.Int + total_vouchers: DF.Int via_landed_cost_voucher: DF.Check voucher_no: DF.DynamicLink | None voucher_type: DF.Link | None + vouchers_posted: DF.Int warehouse: DF.Link | None # end: auto-generated types @@ -261,6 +266,13 @@ class RepostItemValuation(Document): self.items_to_be_repost = None self.gl_reposting_index = 0 self.total_reposting_count = 0 + self.total_vouchers = 0 + self.vouchers_posted = 0 + self.reposted_item_code = None + self.reposted_warehouse = None + self.sle_posting_date = None + self.sle_posting_time = None + self.reposted_sle_creation = None self.clear_attachment() self.db_update() @@ -435,7 +447,7 @@ def repost_sl_entries(doc): ) else: repost_future_sle( - item_wh_to_repost=[ + items_to_be_repost=[ frappe._dict( { "item_code": doc.item_code, diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index 97243d57001..e0d39c5dc7a 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -248,12 +248,7 @@ def get_item_warehouse_combinations(filters: dict | None = None) -> dict: bin.warehouse, item.valuation_method, ) - .where( - (item.is_stock_item == 1) - & (item.has_serial_no == 0) - & (warehouse.is_group == 0) - & (warehouse.company == filters.company) - ) + .where((item.is_stock_item == 1) & (warehouse.is_group == 0) & (warehouse.company == filters.company)) ) if filters.item_code: diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 6810f45827c..21ccfb0f206 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -78,9 +78,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc future_sle_exists(args, sl_entries) for sle in sl_entries: - if sle.serial_no and not via_landed_cost_voucher: - validate_serial_no(sle) - if cancelled: sle["actual_qty"] = -flt(sle.get("actual_qty")) @@ -161,35 +158,6 @@ def get_args_for_future_sle(row): ) -def validate_serial_no(sle): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - - for sn in get_serial_nos(sle.serial_no): - args = copy.deepcopy(sle) - args.serial_no = sn - args.warehouse = "" - - vouchers = [] - for row in get_stock_ledger_entries(args, ">"): - voucher_type = frappe.bold(row.voucher_type) - voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no)) - vouchers.append(f"{voucher_type} {voucher_no}") - - if vouchers: - serial_no = frappe.bold(sn) - msg = ( - f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first. - The list of the transactions are as below.""" - + "

  • " - ) - - msg += "
  • ".join(vouchers) - msg += "
" - - title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel" - frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction) - - def validate_cancellation(kargs): if kargs[0].get("is_cancelled"): repost_entry = frappe.db.get_value( @@ -243,127 +211,77 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): def repost_future_sle( - item_wh_to_repost=None, + items_to_be_repost=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False, doc=None, ): - if not item_wh_to_repost: - item_wh_to_repost = [] # set args to empty list if None to avoid enumerate error - reposting_data = {} - if doc and doc.reposting_data_file: - reposting_data = get_reposting_data(doc.reposting_data_file) + if not items_to_be_repost: + items_to_be_repost = get_items_to_be_repost( + voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data + ) - items_to_be_repost = get_items_to_be_repost( - voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data - ) - if items_to_be_repost: - item_wh_to_repost = items_to_be_repost + repost_affected_transaction = get_affected_transactions(doc) or set() + if not items_to_be_repost: + return - distinct_item_wh_sles = frappe._dict() - prev_sle_dict = frappe._dict() - vouchers_to_repost = set() - item_wh_idx = 0 - i = get_current_index(doc) or 0 - - while i < len(item_wh_to_repost): - update_entries_after( + index = get_current_index(doc) or 0 + while index < len(items_to_be_repost): + obj = update_entries_after( { - "item_code": item_wh_to_repost[i].get("item_code"), - "warehouse": item_wh_to_repost[i].get("warehouse"), - "posting_date": item_wh_to_repost[i].get("posting_date"), - "posting_time": item_wh_to_repost[i].get("posting_time"), - "creation": item_wh_to_repost[i].get("creation"), - "distinct_item_wh_sles": distinct_item_wh_sles, - "prev_sle_dict": prev_sle_dict, - "vouchers_to_repost": vouchers_to_repost, - "item_wh_idx": item_wh_idx, - "current_idx": i, + "item_code": items_to_be_repost[index].get("item_code"), + "warehouse": items_to_be_repost[index].get("warehouse"), + "posting_date": items_to_be_repost[index].get("posting_date"), + "posting_time": items_to_be_repost[index].get("posting_time"), + "creation": items_to_be_repost[index].get("creation"), + "current_idx": index, + "items_to_be_repost": items_to_be_repost, + "repost_doc": doc, + "repost_affected_transaction": repost_affected_transaction, }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, ) - i += 1 + index += 1 + + repost_affected_transaction.update(obj.repost_affected_transaction) + update_args_in_repost_item_valuation(doc, index, items_to_be_repost, repost_affected_transaction) -def get_reposting_data(file_path) -> dict: - file_name = frappe.db.get_value( - "File", +def update_args_in_repost_item_valuation( + doc, index, items_to_be_repost, repost_affected_transaction, only_affected_transaction=False +): + file_name = "" + has_file = False + if doc.reposting_data_file: + has_file = True + + if doc.reposting_data_file: + file_name = get_reposting_file_name(doc.doctype, doc.name) + # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True) + + doc.reposting_data_file = create_json_gz_file( { - "file_url": file_path, - "attached_to_field": "reposting_data_file", + "repost_affected_transaction": repost_affected_transaction, }, - "name", + doc, + file_name, ) - if not file_name: - return frappe._dict() - - attached_file = frappe.get_doc("File", file_name) - - content = attached_file.get_content() - if isinstance(content, str): - content = content.encode("utf-8") - - try: - data = gzip.decompress(content) - except Exception: - return frappe._dict() - - if data := json.loads(data.decode("utf-8")): - data = data - - return parse_json(data) - - -def validate_item_warehouse(args): - for field in ["item_code", "warehouse", "posting_date", "posting_time"]: - if args.get(field) in [None, ""]: - validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting" - frappe.throw(_(validation_msg)) - - -def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses, affected_transactions): - if not doc.items_to_be_repost: - file_name = "" - if doc.reposting_data_file: - file_name = get_reposting_file_name(doc.doctype, doc.name) - # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True) - - doc.reposting_data_file = create_json_gz_file( - { - "items_to_be_repost": args, - "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()}, - "affected_transactions": affected_transactions, - }, - doc, - file_name, - ) - + if not only_affected_transaction or not has_file: doc.db_set( { "current_index": index, - "total_reposting_count": len(args), + "items_to_be_repost": frappe.as_json(items_to_be_repost), + "total_reposting_count": len(items_to_be_repost), "reposting_data_file": doc.reposting_data_file, } ) - else: - doc.db_set( - { - "items_to_be_repost": json.dumps(args, default=str), - "distinct_item_and_warehouse": json.dumps( - {str(k): v for k, v in distinct_item_warehouses.items()}, default=str - ), - "current_index": index, - "affected_transactions": frappe.as_json(affected_transactions), - } - ) - if not frappe.in_test: frappe.db.commit() @@ -371,9 +289,8 @@ def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehou "item_reposting_progress", { "name": doc.name, - "items_to_be_repost": json.dumps(args, default=str), "current_index": index, - "total_reposting_count": len(args), + "total_reposting_count": len(items_to_be_repost), }, doctype=doc.doctype, docname=doc.name, @@ -430,23 +347,27 @@ def create_file(doc, compressed_content): return _file.file_url -def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None): - if not reposting_data and doc and doc.reposting_data_file: - reposting_data = get_reposting_data(doc.reposting_data_file) +def validate_item_warehouse(args): + for field in ["item_code", "warehouse", "posting_date", "posting_time"]: + if args.get(field) in [None, ""]: + validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting" + frappe.throw(_(validation_msg)) + +def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None): if reposting_data and reposting_data.items_to_be_repost: return reposting_data.items_to_be_repost items_to_be_repost = [] if doc and doc.items_to_be_repost: - items_to_be_repost = json.loads(doc.items_to_be_repost) or [] + items_to_be_repost = json.loads(doc.items_to_be_repost) if not items_to_be_repost and voucher_type and voucher_no: items_to_be_repost = frappe.db.get_all( "Stock Ledger Entry", filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, - fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"], + fields=["item_code", "warehouse", "posting_date", "posting_time", "creation", "posting_datetime"], order_by="creation asc", group_by="item_code, warehouse", ) @@ -454,51 +375,44 @@ def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposti return items_to_be_repost or [] -def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None): - if not reposting_data and doc and doc.reposting_data_file: - reposting_data = get_reposting_data(doc.reposting_data_file) - - if reposting_data and reposting_data.distinct_item_and_warehouse: - return parse_distinct_items_and_warehouses(reposting_data.distinct_item_and_warehouse) - - distinct_item_warehouses = {} - - if doc and doc.distinct_item_and_warehouse: - distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse) - distinct_item_warehouses = { - frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items() - } - else: - for i, d in enumerate(args): - distinct_item_warehouses.setdefault( - (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i}) - ) - - return distinct_item_warehouses - - -def parse_distinct_items_and_warehouses(distinct_items_and_warehouses): - new_dict = frappe._dict({}) - - # convert string keys to tuple - for k, v in distinct_items_and_warehouses.items(): - new_dict[frappe.safe_eval(k)] = frappe._dict(v) - - return new_dict - - def get_affected_transactions(doc, reposting_data=None) -> set[tuple[str, str]]: if not reposting_data and doc and doc.reposting_data_file: reposting_data = get_reposting_data(doc.reposting_data_file) - if reposting_data and reposting_data.affected_transactions: - return {tuple(transaction) for transaction in reposting_data.affected_transactions} + if reposting_data and reposting_data.repost_affected_transaction: + return {tuple(transaction) for transaction in reposting_data.repost_affected_transaction} - if not doc.affected_transactions: - return set() + return set() - transactions = frappe.parse_json(doc.affected_transactions) - return {tuple(transaction) for transaction in transactions} + +def get_reposting_data(file_path) -> dict: + file_name = frappe.db.get_value( + "File", + { + "file_url": file_path, + "attached_to_field": "reposting_data_file", + }, + "name", + ) + + if not file_name: + return frappe._dict() + + attached_file = frappe.get_doc("File", file_name) + + content = attached_file.get_content() + if isinstance(content, str): + content = content.encode("utf-8") + + try: + data = gzip.decompress(content) + except Exception: + return frappe._dict() + + if data := json.loads(data.decode("utf-8")): + data = data + + return parse_json(data) def get_current_index(doc=None): @@ -534,11 +448,11 @@ class update_entries_after: self.allow_zero_rate = allow_zero_rate self.via_landed_cost_voucher = via_landed_cost_voucher self.item_code = args.get("item_code") - self.distinct_item_wh_sles = args.get("distinct_item_wh_sles", frappe._dict()) - self.prev_sle_dict = args.get("prev_sle_dict", frappe._dict()) - self.vouchers_to_repost = args.get("vouchers_to_repost", set()) - self.item_wh_idx = args.get("item_wh_idx", 0) + self.prev_sle_dict = frappe._dict({}) + self.stock_ledgers_to_repost = [] self.current_idx = args.get("current_idx", 0) + self.repost_doc = args.get("repost_doc") or None + self.items_to_be_repost = args.get("items_to_be_repost") or None self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed( item_code=self.item_code @@ -551,13 +465,16 @@ class update_entries_after: self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.set_precision() self.valuation_method = get_valuation_method(self.item_code, self.company) + self.repost_affected_transaction = args.get("repost_affected_transaction") or set() self.new_items_found = False - self.affected_transactions: set[tuple[str, str]] = set() self.reserved_stock = self.get_reserved_stock() self.data = frappe._dict() - self.initialize_previous_data(self.args) + + if not self.repost_doc or not self.repost_doc.reposted_item_code: + self.initialize_previous_data(self.args) + self.build() def get_reserved_stock(self): @@ -607,8 +524,13 @@ class update_entries_after: """ self.data.setdefault(args.warehouse, frappe._dict()) warehouse_dict = self.data[args.warehouse] + + if self.stock_ledgers_to_repost: + return + previous_sle = get_previous_sle_of_current_voucher(args) - self.prev_sle_dict[(args.get("item_code"), args.get("warehouse"))] = previous_sle + if previous_sle: + self.prev_sle_dict[(args.get("item_code"), args.get("warehouse"))] = previous_sle warehouse_dict.previous_sle = previous_sle @@ -630,59 +552,60 @@ class update_entries_after: self.process_sle_against_current_timestamp() if not future_sle_exists(self.args): self.update_bin() - elif self.vouchers_to_repost: - self.repost_vouchers() else: - kwargs = frappe._dict( - { - "item_code": self.item_code, - "warehouse": self.args.warehouse, - "posting_date": self.args.posting_date, - "posting_time": self.args.posting_time, - "creation": self.args.creation, - } - ) + ledgers_to_repost = self.get_sles_to_repost() + if not ledgers_to_repost: + return - self.prepare_sles_to_repost(kwargs) - self.repost_vouchers() + self.stock_ledgers_to_repost = ledgers_to_repost + self.repost_stock_ledger_entries() self.update_bin() + self.reset_vouchers_and_idx() + self.update_data_in_repost() - def repost_vouchers(self): - vouchers = self.vouchers_to_repost or self.get_vouchere_to_repost() - for row in vouchers: - self.process_sle(row) + if self.exceptions: + self.raise_exceptions() - def get_vouchere_to_repost(self): + def get_sles_to_repost(self): + self.distinct_item_wh_sles = frappe._dict() + + sle_dict = self.get_items_to_be_repost() + self.prepare_sles_to_repost(sle_dict) if not self.distinct_item_wh_sles: return [] - sorted_rows = sorted( + ledgers_to_repost = sorted( (row for rows in self.distinct_item_wh_sles.values() for row in rows), key=lambda d: (get_datetime(d.get("posting_datetime")), get_datetime(d.get("creation"))), ) - return sorted_rows + return ledgers_to_repost - def prepare_sles_to_repost(self, kwargs): - sles = self.get_future_entries_to_repost(kwargs) + def prepare_sles_to_repost(self, sle_dict): + sles = self.get_future_entries_to_repost(sle_dict) for sle in sles: item_wh_key = (sle.item_code, sle.warehouse) if item_wh_key not in self.prev_sle_dict: - prev_sle = get_previous_sle_of_current_voucher(kwargs) + prev_sle = get_previous_sle_of_current_voucher(sle) self.prev_sle_dict[item_wh_key] = prev_sle - key = (sle.item_code, sle.warehouse, sle.voucher_detail_no) + key = (sle.item_code, sle.warehouse, sle.voucher_detail_no, sle.name) if key not in self.distinct_item_wh_sles: self.distinct_item_wh_sles.setdefault(key, []).append(sle) - if sle.dependant_sle_voucher_detail_no: - self.prepare_dependent_sles_to_repost(sle) + if sle.dependant_sle_voucher_detail_no: + self.prepare_dependent_sles_to_repost(sle) def prepare_dependent_sles_to_repost(self, sle): if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no): sles = self.get_sles_for_repack(sle) for repack_sle in sles: - key = (repack_sle.item_code, repack_sle.warehouse, repack_sle.voucher_detail_no) + key = ( + repack_sle.item_code, + repack_sle.warehouse, + repack_sle.voucher_detail_no, + repack_sle.name, + ) if key not in self.distinct_item_wh_sles: self.distinct_item_wh_sles.setdefault(key, []).append(repack_sle) @@ -690,13 +613,115 @@ class update_entries_after: elif sle.dependant_sle_voucher_detail_no: dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no) + if not dependant_sle: + return - key = (dependant_sle.item_code, dependant_sle.warehouse, dependant_sle.voucher_detail_no) + key = ( + dependant_sle.item_code, + dependant_sle.warehouse, + dependant_sle.voucher_detail_no, + dependant_sle.name, + ) if key not in self.distinct_item_wh_sles: self.distinct_item_wh_sles.setdefault(key, []).append(dependant_sle) self.prepare_sles_to_repost(dependant_sle) + def get_items_to_be_repost(self): + if self.repost_doc and self.repost_doc.reposted_item_code: + return frappe._dict( + { + "item_code": self.repost_doc.reposted_item_code, + "warehouse": self.repost_doc.reposted_warehouse, + "posting_date": self.repost_doc.sle_posting_date, + "posting_time": self.repost_doc.sle_posting_time, + "creation": self.repost_doc.reposted_sle_creation, + } + ) + + return frappe._dict( + { + "item_code": self.args.item_code, + "warehouse": self.args.warehouse, + "posting_date": self.args.posting_date, + "posting_time": self.args.posting_time, + "creation": self.args.creation, + } + ) + + def repost_stock_ledger_entries(self): + i = 0 + while self.stock_ledgers_to_repost: + sle = self.stock_ledgers_to_repost.pop(0) + + if self.args.item_code != sle.item_code or self.args.warehouse != sle.warehouse: + self.repost_affected_transaction.add((sle.voucher_type, sle.voucher_no)) + + if isinstance(sle, dict): + sle = frappe._dict(sle) + + self.process_sle(sle) + i += 1 + if i % 500 == 0: + self.update_data_in_repost(sle, i) + + def reset_vouchers_and_idx(self): + self.stock_ledgers_to_repost = [] + self.prev_sle_dict = frappe._dict() + + def update_data_in_repost(self, sle=None, index=None): + if not self.repost_doc: + return + + values_to_update = { + "total_vouchers": len(self.stock_ledgers_to_repost) + cint(index), + "vouchers_posted": index or 0, + "reposted_item_code": None, + "reposted_warehouse": None, + "sle_posting_date": None, + "sle_posting_time": None, + "reposted_sle_creation": None, + } + + if sle: + values_to_update.update( + { + "reposted_item_code": sle.item_code, + "reposted_warehouse": sle.warehouse, + "sle_posting_date": sle.posting_date, + "sle_posting_time": sle.posting_time, + "reposted_sle_creation": sle.creation, + } + ) + + self.repost_doc.db_set(values_to_update) + + update_args_in_repost_item_valuation( + self.repost_doc, + self.current_idx, + self.items_to_be_repost, + self.repost_affected_transaction, + only_affected_transaction=True, + ) + + if not frappe.in_test: + # To maintain the state of the reposting, so if timeout happens, it can be resumed from the last posted voucher + frappe.db.commit() # nosemgrep + + self.publish_real_time_progress(index=index) + + def publish_real_time_progress(self, index=None): + frappe.publish_realtime( + "item_reposting_progress", + { + "name": self.repost_doc.name, + "total_vouchers": len(self.stock_ledgers_to_repost) + cint(index), + "vouchers_posted": index or 0, + }, + doctype=self.repost_doc.doctype, + docname=self.repost_doc.name, + ) + def get_future_entries_to_repost(self, kwargs): return get_stock_ledger_entries(kwargs, ">=", "asc", for_update=True, check_serial_no=False) @@ -739,76 +764,6 @@ class update_entries_after: return list(self.get_sle_after_datetime(args)) - def get_dependent_entries_to_fix(self, entries_to_fix, sle): - dependant_sle = get_sle_by_voucher_detail_no( - sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name - ) - - if not dependant_sle: - return entries_to_fix - elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: - return entries_to_fix - elif dependant_sle.item_code != self.item_code: - self.update_distinct_item_warehouses(dependant_sle) - return entries_to_fix - elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: - return entries_to_fix - else: - self.initialize_previous_data(dependant_sle) - self.update_distinct_item_warehouses(dependant_sle) - return entries_to_fix - - def update_distinct_item_warehouses(self, dependant_sle): - key = (dependant_sle.item_code, dependant_sle.warehouse) - val = frappe._dict({"sle": dependant_sle}) - - if key not in self.distinct_item_warehouses: - self.distinct_item_warehouses[key] = val - self.new_items_found = True - else: - existing_sle = self.distinct_item_warehouses[key].get("sle", {}) - if getdate(existing_sle.get("posting_date")) > getdate(dependant_sle.posting_date): - self.distinct_item_warehouses[key] = val - self.new_items_found = True - elif ( - dependant_sle.actual_qty > 0 - and dependant_sle.voucher_type == "Stock Entry" - and is_transfer_stock_entry(dependant_sle.voucher_no) - ): - if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"): - return - - val["transfer_entry_to_repost"] = True - self.distinct_item_warehouses[key] = val - self.new_items_found = True - - def is_dependent_voucher_reposted(self, dependant_sle) -> bool: - # Return False if the dependent voucher is not reposted - - if self.args.items_to_be_repost and self.args.current_index: - index = self.args.current_index - while index < len(self.args.items_to_be_repost): - if ( - self.args.items_to_be_repost[index].get("item_code") == dependant_sle.item_code - and self.args.items_to_be_repost[index].get("warehouse") == dependant_sle.warehouse - ): - if getdate(self.args.items_to_be_repost[index].get("posting_date")) > getdate( - dependant_sle.posting_date - ): - self.args.items_to_be_repost[index]["posting_date"] = dependant_sle.posting_date - - return False - - index += 1 - - return True - - def get_dependent_voucher_detail_nos(self, key): - if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]: - self.distinct_item_warehouses[key].dependent_voucher_detail_nos = [] - - return self.distinct_item_warehouses[key].dependent_voucher_detail_nos - def validate_previous_sle_qty(self, sle): previous_sle = self.prev_sle_dict.get((sle.item_code, sle.warehouse)) if previous_sle and previous_sle.get("qty_after_transaction") < 0 and sle.get("actual_qty") > 0: @@ -830,15 +785,23 @@ class update_entries_after: def process_sle(self, sle): # previous sle data for this warehouse key = (sle.item_code, sle.warehouse) - self.wh_data = self.prev_sle_dict.get(key) or frappe._dict( - { - "qty_after_transaction": 0.0, - "valuation_rate": 0.0, - "stock_value": 0.0, - "prev_stock_value": 0.0, - "stock_queue": [], - } - ) + if key not in self.prev_sle_dict: + prev_sle = get_previous_sle_of_current_voucher(sle) + if prev_sle: + self.prev_sle_dict[key] = prev_sle + + if not self.prev_sle_dict.get(key): + self.prev_sle_dict[key] = frappe._dict( + { + "qty_after_transaction": 0.0, + "valuation_rate": 0.0, + "stock_value": 0.0, + "prev_stock_value": 0.0, + "stock_queue": [], + } + ) + + self.wh_data = self.prev_sle_dict.get(key) if self.wh_data.stock_queue and isinstance(self.wh_data.stock_queue, str): self.wh_data.stock_queue = json.loads(self.wh_data.stock_queue) @@ -847,7 +810,6 @@ class update_entries_after: self.wh_data.prev_stock_value = self.wh_data.stock_value self.validate_previous_sle_qty(sle) - self.affected_transactions.add((sle.voucher_type, sle.voucher_no)) if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock): # validate negative stock for serialized items, fifo valuation @@ -950,6 +912,7 @@ class update_entries_after: sle.stock_queue = json.dumps(self.wh_data.stock_queue) sle.stock_value_difference = stock_value_difference + if ( sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0 @@ -1761,9 +1724,13 @@ class update_entries_after: for (item_code, warehouse), data in self.prev_sle_dict.items(): bin_name = get_or_make_bin(item_code, warehouse) - updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value} + updated_values = { + "actual_qty": flt(data.qty_after_transaction), + "stock_value": flt(data.stock_value), + } if data.valuation_rate is not None: - updated_values["valuation_rate"] = data.valuation_rate + updated_values["valuation_rate"] = flt(data.valuation_rate) + frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True) From 15739b5d81fa25fc0aa687af5f32fa4705a1c9d0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 26 Mar 2026 16:42:48 +0530 Subject: [PATCH 69/90] fix: pick correct dependant sle during reposting --- erpnext/stock/stock_ledger.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 21ccfb0f206..5d4c52312a4 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1909,10 +1909,14 @@ def get_stock_ledger_entries( ) -def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): +def get_sle_by_voucher_detail_no(voucher_detail_no): return frappe.db.get_value( "Stock Ledger Entry", - {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0}, + { + "voucher_detail_no": voucher_detail_no, + "is_cancelled": 0, + "dependant_sle_voucher_detail_no": ("is", "not set"), + }, ["*"], as_dict=1, ) From 675b94b7a20305f00af6670215c176fce2f0a7f3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:42:46 +0530 Subject: [PATCH 70/90] fix: support translated search in get_party_type and refactor raw sql to qb (backport #53191) (#53832) * fix: support translated search in get_party_type and refactor raw sql to qb (cherry picked from commit d9876880586e5f98b5a991cc1327db370a30396e) # Conflicts: # erpnext/setup/doctype/party_type/party_type.py * fix: resolve merge conflicts in party_type.py --------- Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com> --- .../setup/doctype/party_type/party_type.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index 6730d1cbdce..cf72eb3bbdc 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -4,6 +4,7 @@ import frappe from frappe.model.document import Document +from frappe.query_builder import DocType class PartyType(Document): @@ -24,29 +25,36 @@ class PartyType(Document): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_party_type(doctype, txt, searchfield, start, page_len, filters): - cond = "" - account_type = None +def get_party_type(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): + PartyType = DocType("Party Type") + get_party_type_query = frappe.qb.from_(PartyType).select(PartyType.name).orderby(PartyType.name) + + condition_list = [] if filters and filters.get("account"): account_type = frappe.db.get_value("Account", filters.get("account"), "account_type") if account_type: if account_type in ["Receivable", "Payable"]: # Include Employee regardless of its configured account_type, but still respect the text filter - cond = "and (account_type = %(account_type)s or name = 'Employee')" + condition_list.append( + (PartyType.account_type == account_type) | (PartyType.name == "Employee") + ) else: - cond = "and account_type = %(account_type)s" + condition_list.append(PartyType.account_type == account_type) - # Build parameters dictionary - params = {"txt": "%" + txt + "%", "start": start, "page_len": page_len} - if account_type: - params["account_type"] = account_type + for condition in condition_list: + get_party_type_query = get_party_type_query.where(condition) - result = frappe.db.sql( - f"""select name from `tabParty Type` - where `{searchfield}` LIKE %(txt)s {cond} - order by name limit %(page_len)s offset %(start)s""", - params, - ) + if frappe.local.lang == "en": + get_party_type_query = get_party_type_query.where(getattr(PartyType, searchfield).like(f"%{txt}%")) + get_party_type_query = get_party_type_query.limit(page_len) + get_party_type_query = get_party_type_query.offset(start) + + result = get_party_type_query.run() + else: + result = get_party_type_query.run() + test_str = txt.lower() + result = [row for row in result if test_str in frappe._(row[0]).lower()] + result = result[start : start + page_len] return result or [] From c9953580b2d50199ecc30911d52e8f830c132666 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:32:29 +0000 Subject: [PATCH 71/90] ci: semgrep to prevent test regression (backport #53837) (#53840) ci: semgrep to prevent test regression (cherry picked from commit be4496e4ab4258ab317984caeb8b3d9f1e5bd5fc) Co-authored-by: ruthra kumar --- .github/workflows/linters.yml | 3 +++ semgrep/test-correctness.yml | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 semgrep/test-correctness.yml diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 6701673cc7f..37d8363beaa 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -43,3 +43,6 @@ jobs: - name: Run Semgrep rules run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + - name: Semgrep for Test Correctness + run: semgrep ci --include=**/test_*.py --config ./semgrep/test-correctness.yml diff --git a/semgrep/test-correctness.yml b/semgrep/test-correctness.yml new file mode 100644 index 00000000000..34eb82fa1d6 --- /dev/null +++ b/semgrep/test-correctness.yml @@ -0,0 +1,18 @@ +rules: +- id: Dont-commit + pattern: frappe.db.commit() + message: Commiting inside test breaks idempotency. + languages: [python] + severity: ERROR +- id: Implicit-commit + pattern: frappe.db.truncate() + message: DB truncation does implict commit which breaks test idempotency. + languages: [python] + severity: ERROR +- id: Dont-override-teardown + pattern: | + def tearDown(...): + ... + message: ERPNextTestSuite forces rollback on each tearDown, which ensures idempotency. Don't override tearDown. + languages: [python] + severity: ERROR From 9a2851f221fc708d818e4f42f2836266128c3d85 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:34:18 +0000 Subject: [PATCH 72/90] fix: validate if quantity greater than 0 in item dashboard (backport #53846) (#53848) Co-authored-by: Mihir Kandoi fix: validate if quantity greater than 0 in item dashboard (#53846) --- erpnext/stock/dashboard/item_dashboard.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 17f65ce270c..018a4f86fa8 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -281,8 +281,13 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, stoc } dialog.set_primary_action(__("Create Stock Entry"), function () { - if (source && (dialog.get_value("qty") == 0 || dialog.get_value("qty") > actual_qty)) { - frappe.msgprint(__("Quantity must be greater than zero, and less or equal to {0}", [actual_qty])); + if (flt(dialog.get_value("qty")) <= 0) { + frappe.msgprint(__("Quantity must be greater than zero")); + return; + } + + if (source && dialog.get_value("qty") > actual_qty) { + frappe.msgprint(__("Quantity must be less than or equal to {0}", [actual_qty])); return; } From 1c1369fea8a20e7a62fc13bea669ad40dd481ec8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:45:50 +0000 Subject: [PATCH 73/90] fix: invalid dynamic link filter for address doctype (backport #53849) (#53852) --- erpnext/controllers/queries.py | 23 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.js | 8 +++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b7845bc5698..9b663c0d2b9 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -1002,3 +1002,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters): limit_page_length=page_len, as_list=1, ) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_warehouse_address(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): + table = frappe.qb.DocType(doctype) + child_table = frappe.qb.DocType("Dynamic Link") + + query = ( + frappe.qb.from_(table) + .inner_join(child_table) + .on((table.name == child_table.parent) & (child_table.parenttype == doctype)) + .select(table.name) + .where( + (child_table.link_name == filters.get("warehouse")) + & (table.disabled == 0) + & (child_table.link_doctype == "Warehouse") + & (table.name.like(f"%{txt}%")) + ) + .offset(start) + .limit(page_len) + ) + return query.run(as_list=1) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 4fdd9df1adf..f71b67e1127 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -38,18 +38,18 @@ frappe.ui.form.on("Stock Entry", { frm.set_query("source_warehouse_address", function () { return { + query: "erpnext.controllers.queries.get_warehouse_address", filters: { - link_doctype: "Warehouse", - link_name: frm.doc.from_warehouse, + warehouse: frm.doc.from_warehouse, }, }; }); frm.set_query("target_warehouse_address", function () { return { + query: "erpnext.controllers.queries.get_warehouse_address", filters: { - link_doctype: "Warehouse", - link_name: frm.doc.to_warehouse, + warehouse: frm.doc.to_warehouse, }, }; }); From 8fbb86d53e6bea3ea10b582252c4babbaae978ed Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 27 Mar 2026 19:02:31 +0530 Subject: [PATCH 74/90] fix: corrected logic to retry reposting if timeout occurs after dependent SLE processing (cherry picked from commit 90b9ab0bc86ad2f6ac1ca694a3a343dcc2afb67a) --- .../repost_item_valuation.json | 60 +--- .../repost_item_valuation.py | 10 - erpnext/stock/stock_ledger.py | 276 ++++++++++-------- 3 files changed, 159 insertions(+), 187 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 3b4ae7220a5..ce43ae3a54f 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -32,18 +32,11 @@ "total_reposting_count", "current_index", "gl_reposting_index", + "reposting_data_file", "vouchers_based_on_item_and_warehouse_section", "total_vouchers", "column_break_yqwo", - "vouchers_posted", - "last_sle_posted_section", - "reposted_item_code", - "reposted_warehouse", - "reposting_data_file", - "column_break_miwc", - "sle_posting_date", - "sle_posting_time", - "reposted_sle_creation" + "vouchers_posted" ], "fields": [ { @@ -236,6 +229,7 @@ { "fieldname": "vouchers_based_on_item_and_warehouse_section", "fieldtype": "Section Break", + "hidden": 1, "label": "Reposting Vouchers" }, { @@ -256,52 +250,6 @@ "no_copy": 1, "read_only": 1 }, - { - "fieldname": "last_sle_posted_section", - "fieldtype": "Section Break", - "label": "Last SLE Posted" - }, - { - "fieldname": "reposted_item_code", - "fieldtype": "Link", - "label": "Reposted Item Code", - "no_copy": 1, - "options": "Item", - "read_only": 1 - }, - { - "fieldname": "reposted_warehouse", - "fieldtype": "Link", - "label": "Reposted Warehouse", - "no_copy": 1, - "options": "Warehouse", - "read_only": 1 - }, - { - "fieldname": "column_break_miwc", - "fieldtype": "Column Break" - }, - { - "fieldname": "reposted_sle_creation", - "fieldtype": "Datetime", - "label": "Reposted SLE Creation", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "sle_posting_date", - "fieldtype": "Date", - "label": "SLE Posting Date", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "sle_posting_time", - "fieldtype": "Time", - "label": "SLE Posting Time", - "no_copy": 1, - "read_only": 1 - }, { "fieldname": "reposting_data_file", "fieldtype": "Attach", @@ -314,7 +262,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-03-26 13:52:51.895504", + "modified": "2026-03-27 18:59:58.637964", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index d7397682ee6..2b4d5c28692 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -49,13 +49,8 @@ class RepostItemValuation(Document): posting_time: DF.Time | None recreate_stock_ledgers: DF.Check repost_only_accounting_ledgers: DF.Check - reposted_item_code: DF.Link | None - reposted_sle_creation: DF.Datetime | None - reposted_warehouse: DF.Link | None reposting_data_file: DF.Attach | None reposting_reference: DF.Data | None - sle_posting_date: DF.Date | None - sle_posting_time: DF.Time | None status: DF.Literal["Queued", "In Progress", "Completed", "Skipped", "Failed", "Cancelled"] total_reposting_count: DF.Int total_vouchers: DF.Int @@ -268,11 +263,6 @@ class RepostItemValuation(Document): self.total_reposting_count = 0 self.total_vouchers = 0 self.vouchers_posted = 0 - self.reposted_item_code = None - self.reposted_warehouse = None - self.sle_posting_date = None - self.sle_posting_time = None - self.reposted_sle_creation = None self.clear_attachment() self.db_update() diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5d4c52312a4..c811e510b0c 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -4,6 +4,7 @@ import copy import gzip import json +from collections import deque import frappe from frappe import _, bold, scrub @@ -224,7 +225,13 @@ def repost_future_sle( voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data ) - repost_affected_transaction = get_affected_transactions(doc) or set() + if doc and doc.reposting_data_file: + reposting_data = get_reposting_data(doc.reposting_data_file) + + repost_affected_transaction = get_affected_transactions(doc, reposting_data) or set() + resume_item_wh_wise_last_posted_sle = ( + get_item_wh_wise_last_posted_sle_from_reposting_data(doc, reposting_data) or {} + ) if not items_to_be_repost: return @@ -241,6 +248,7 @@ def repost_future_sle( "items_to_be_repost": items_to_be_repost, "repost_doc": doc, "repost_affected_transaction": repost_affected_transaction, + "item_wh_wise_last_posted_sle": resume_item_wh_wise_last_posted_sle, }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, @@ -248,15 +256,25 @@ def repost_future_sle( index += 1 + resume_item_wh_wise_last_posted_sle = {} repost_affected_transaction.update(obj.repost_affected_transaction) update_args_in_repost_item_valuation(doc, index, items_to_be_repost, repost_affected_transaction) def update_args_in_repost_item_valuation( - doc, index, items_to_be_repost, repost_affected_transaction, only_affected_transaction=False + doc, + index, + items_to_be_repost, + repost_affected_transaction, + item_wh_wise_last_posted_sle=None, + only_affected_transaction=False, ): file_name = "" has_file = False + + if not item_wh_wise_last_posted_sle: + item_wh_wise_last_posted_sle = {} + if doc.reposting_data_file: has_file = True @@ -267,6 +285,8 @@ def update_args_in_repost_item_valuation( doc.reposting_data_file = create_json_gz_file( { "repost_affected_transaction": repost_affected_transaction, + "item_wh_wise_last_posted_sle": {str(k): v for k, v in item_wh_wise_last_posted_sle.items()} + or {}, }, doc, file_name, @@ -385,6 +405,16 @@ def get_affected_transactions(doc, reposting_data=None) -> set[tuple[str, str]]: return set() +def get_item_wh_wise_last_posted_sle_from_reposting_data(doc, reposting_data=None): + if not reposting_data and doc and doc.reposting_data_file: + reposting_data = get_reposting_data(doc.reposting_data_file) + + if reposting_data and reposting_data.item_wh_wise_last_posted_sle: + return frappe._dict(reposting_data.item_wh_wise_last_posted_sle) + + return frappe._dict() + + def get_reposting_data(file_path) -> dict: file_name = frappe.db.get_value( "File", @@ -448,7 +478,6 @@ class update_entries_after: self.allow_zero_rate = allow_zero_rate self.via_landed_cost_voucher = via_landed_cost_voucher self.item_code = args.get("item_code") - self.prev_sle_dict = frappe._dict({}) self.stock_ledgers_to_repost = [] self.current_idx = args.get("current_idx", 0) self.repost_doc = args.get("repost_doc") or None @@ -462,6 +491,7 @@ class update_entries_after: if self.args.sle_id: self.args["name"] = self.args.sle_id + self.prev_sle_dict = frappe._dict({}) self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.set_precision() self.valuation_method = get_valuation_method(self.item_code, self.company) @@ -472,7 +502,7 @@ class update_entries_after: self.data = frappe._dict() - if not self.repost_doc or not self.repost_doc.reposted_item_code: + if not self.repost_doc or not self.args.get("item_wh_wise_last_posted_sle"): self.initialize_previous_data(self.args) self.build() @@ -553,12 +583,14 @@ class update_entries_after: if not future_sle_exists(self.args): self.update_bin() else: - ledgers_to_repost = self.get_sles_to_repost() - if not ledgers_to_repost: - return + self.item_wh_wise_last_posted_sle = self.get_item_wh_wise_last_posted_sle() + _item_wh_sle = self.sort_sles(self.item_wh_wise_last_posted_sle.values()) + + while _item_wh_sle: + self.initialize_reposting() + sle_dict = _item_wh_sle.pop(0) + self.repost_stock_ledgers(sle_dict) - self.stock_ledgers_to_repost = ledgers_to_repost - self.repost_stock_ledger_entries() self.update_bin() self.reset_vouchers_and_idx() self.update_data_in_repost() @@ -566,134 +598,129 @@ class update_entries_after: if self.exceptions: self.raise_exceptions() - def get_sles_to_repost(self): - self.distinct_item_wh_sles = frappe._dict() + def initialize_reposting(self): + self._sles = [] + self.distinct_sles = set() + self.distinct_dependant_sle = set() + self.prev_sle_dict = frappe._dict({}) - sle_dict = self.get_items_to_be_repost() - self.prepare_sles_to_repost(sle_dict) - if not self.distinct_item_wh_sles: - return [] + def get_item_wh_wise_last_posted_sle(self): + if self.args and self.args.get("item_wh_wise_last_posted_sle"): + _sles = {} + for key, sle in self.args.get("item_wh_wise_last_posted_sle").items(): + _sles[frappe.safe_eval(key)] = frappe._dict(sle) - ledgers_to_repost = sorted( - (row for rows in self.distinct_item_wh_sles.values() for row in rows), - key=lambda d: (get_datetime(d.get("posting_datetime")), get_datetime(d.get("creation"))), - ) + return _sles - return ledgers_to_repost - - def prepare_sles_to_repost(self, sle_dict): - sles = self.get_future_entries_to_repost(sle_dict) - for sle in sles: - item_wh_key = (sle.item_code, sle.warehouse) - if item_wh_key not in self.prev_sle_dict: - prev_sle = get_previous_sle_of_current_voucher(sle) - self.prev_sle_dict[item_wh_key] = prev_sle - - key = (sle.item_code, sle.warehouse, sle.voucher_detail_no, sle.name) - if key not in self.distinct_item_wh_sles: - self.distinct_item_wh_sles.setdefault(key, []).append(sle) - - if sle.dependant_sle_voucher_detail_no: - self.prepare_dependent_sles_to_repost(sle) - - def prepare_dependent_sles_to_repost(self, sle): - if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no): - sles = self.get_sles_for_repack(sle) - for repack_sle in sles: - key = ( - repack_sle.item_code, - repack_sle.warehouse, - repack_sle.voucher_detail_no, - repack_sle.name, - ) - if key not in self.distinct_item_wh_sles: - self.distinct_item_wh_sles.setdefault(key, []).append(repack_sle) - - self.prepare_sles_to_repost(repack_sle) - - elif sle.dependant_sle_voucher_detail_no: - dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no) - if not dependant_sle: - return - - key = ( - dependant_sle.item_code, - dependant_sle.warehouse, - dependant_sle.voucher_detail_no, - dependant_sle.name, - ) - if key not in self.distinct_item_wh_sles: - self.distinct_item_wh_sles.setdefault(key, []).append(dependant_sle) - - self.prepare_sles_to_repost(dependant_sle) - - def get_items_to_be_repost(self): - if self.repost_doc and self.repost_doc.reposted_item_code: - return frappe._dict( + return { + (self.args.item_code, self.args.warehouse): frappe._dict( { - "item_code": self.repost_doc.reposted_item_code, - "warehouse": self.repost_doc.reposted_warehouse, - "posting_date": self.repost_doc.sle_posting_date, - "posting_time": self.repost_doc.sle_posting_time, - "creation": self.repost_doc.reposted_sle_creation, + "item_code": self.args.item_code, + "warehouse": self.args.warehouse, + "posting_datetime": get_combine_datetime(self.args.posting_date, self.args.posting_time), + "posting_date": self.args.posting_date, + "posting_time": self.args.posting_time, + "creation": self.args.creation, } ) + } - return frappe._dict( - { - "item_code": self.args.item_code, - "warehouse": self.args.warehouse, - "posting_date": self.args.posting_date, - "posting_time": self.args.posting_time, - "creation": self.args.creation, - } + def repost_stock_ledgers(self, sle_dict=None): + self._sles = self.get_future_entries_to_repost(sle_dict) + + if not isinstance(self._sles, deque): + self._sles = deque(self._sles) + + i = 0 + while self._sles: + sle = self._sles.popleft() + i += 1 + if sle.name in self.distinct_sles: + continue + + item_wh_key = (sle.item_code, sle.warehouse) + if item_wh_key not in self.prev_sle_dict: + self.prev_sle_dict[item_wh_key] = get_previous_sle_of_current_voucher(sle) + + if ( + sle.dependant_sle_voucher_detail_no + and sle.dependant_sle_voucher_detail_no not in self.distinct_dependant_sle + ): + self._sles.append(sle) + self.distinct_dependant_sle.add(sle.dependant_sle_voucher_detail_no) + self.include_dependant_sle_in_reposting(sle) + continue + + self.repost_stock_ledger_entry(sle) + + # To avoid duplicate reposting of same sle in case of multiple dependant sle + self.distinct_sles.add(sle.name) + + # if i % 1000 == 0: + self.update_data_in_repost(len(self._sles), i) + + def sort_sles(self, sles): + return sorted( + sles, + key=lambda d: ( + get_datetime(d.posting_datetime), + get_datetime(d.creation), + ), ) - def repost_stock_ledger_entries(self): - i = 0 - while self.stock_ledgers_to_repost: - sle = self.stock_ledgers_to_repost.pop(0) + def include_dependant_sle_in_reposting(self, sle): + if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no): + repack_sles = self.get_sles_for_repack(sle) + for repack_sle in repack_sles: + self._sles.extend(self.get_future_entries_to_repost(repack_sle)) + else: + dependant_sles = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no) + for depend_sle in dependant_sles: + self._sles.extend(self.get_future_entries_to_repost(depend_sle)) - if self.args.item_code != sle.item_code or self.args.warehouse != sle.warehouse: - self.repost_affected_transaction.add((sle.voucher_type, sle.voucher_no)) + self._sles = deque(self.sort_sles(self._sles)) - if isinstance(sle, dict): - sle = frappe._dict(sle) + def repost_stock_ledger_entry(self, sle): + if self.args.item_code != sle.item_code or self.args.warehouse != sle.warehouse: + self.repost_affected_transaction.add((sle.voucher_type, sle.voucher_no)) - self.process_sle(sle) - i += 1 - if i % 500 == 0: - self.update_data_in_repost(sle, i) + if isinstance(sle, dict): + sle = frappe._dict(sle) + + self.process_sle(sle) + self.update_item_wh_wise_last_posted_sle(sle) + + def update_item_wh_wise_last_posted_sle(self, sle): + if not self._sles: + self.item_wh_wise_last_posted_sle = frappe._dict() + return + + self.item_wh_wise_last_posted_sle[(sle.item_code, sle.warehouse)] = frappe._dict( + { + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.posting_date, + "posting_time": sle.posting_time, + "posting_datetime": sle.posting_datetime + or get_combine_datetime(sle.posting_date, sle.posting_time), + "creation": sle.creation, + } + ) def reset_vouchers_and_idx(self): self.stock_ledgers_to_repost = [] self.prev_sle_dict = frappe._dict() + self.item_wh_wise_last_posted_sle = frappe._dict() - def update_data_in_repost(self, sle=None, index=None): + def update_data_in_repost(self, total_sles=None, index=None): if not self.repost_doc: return values_to_update = { - "total_vouchers": len(self.stock_ledgers_to_repost) + cint(index), + "total_vouchers": cint(total_sles) + cint(index), "vouchers_posted": index or 0, - "reposted_item_code": None, - "reposted_warehouse": None, - "sle_posting_date": None, - "sle_posting_time": None, - "reposted_sle_creation": None, } - if sle: - values_to_update.update( - { - "reposted_item_code": sle.item_code, - "reposted_warehouse": sle.warehouse, - "sle_posting_date": sle.posting_date, - "sle_posting_time": sle.posting_time, - "reposted_sle_creation": sle.creation, - } - ) - self.repost_doc.db_set(values_to_update) update_args_in_repost_item_valuation( @@ -701,6 +728,7 @@ class update_entries_after: self.current_idx, self.items_to_be_repost, self.repost_affected_transaction, + self.item_wh_wise_last_posted_sle, only_affected_transaction=True, ) @@ -708,14 +736,14 @@ class update_entries_after: # To maintain the state of the reposting, so if timeout happens, it can be resumed from the last posted voucher frappe.db.commit() # nosemgrep - self.publish_real_time_progress(index=index) + self.publish_real_time_progress(total_sles=total_sles, index=index) - def publish_real_time_progress(self, index=None): + def publish_real_time_progress(self, total_sles=None, index=None): frappe.publish_realtime( "item_reposting_progress", { "name": self.repost_doc.name, - "total_vouchers": len(self.stock_ledgers_to_repost) + cint(index), + "total_vouchers": cint(total_sles) + cint(index), "vouchers_posted": index or 0, }, doctype=self.repost_doc.doctype, @@ -736,7 +764,14 @@ class update_entries_after: "is_cancelled": 0, "dependant_sle_voucher_detail_no": ("!=", sle.dependant_sle_voucher_detail_no), }, - fields=["*"], + fields=[ + "item_code", + "warehouse", + "posting_date", + "posting_time", + "posting_datetime", + "creation", + ], ) or [] ) @@ -1910,15 +1945,14 @@ def get_stock_ledger_entries( def get_sle_by_voucher_detail_no(voucher_detail_no): - return frappe.db.get_value( + return frappe.get_all( "Stock Ledger Entry", - { + filters={ "voucher_detail_no": voucher_detail_no, "is_cancelled": 0, "dependant_sle_voucher_detail_no": ("is", "not set"), }, - ["*"], - as_dict=1, + fields=["item_code", "warehouse", "posting_date", "posting_time", "posting_datetime", "creation"], ) From 2907c411f344053804f154358650939853f20ffa Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 07:10:14 +0000 Subject: [PATCH 75/90] fix: change shipment parcel dimension fields from Int to Float (backport #53867) (#53873) Co-authored-by: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> fix: change shipment parcel dimension fields from Int to Float (#53867) --- .../stock/doctype/shipment_parcel/shipment_parcel.json | 8 ++++---- .../shipment_parcel_template.json | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json index 321599e2b4b..32d0df2c873 100644 --- a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json +++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json @@ -14,19 +14,19 @@ "fields": [ { "fieldname": "length", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Length (cm)" }, { "fieldname": "width", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Width (cm)" }, { "fieldname": "height", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Height (cm)" }, @@ -49,7 +49,7 @@ ], "istable": 1, "links": [], - "modified": "2024-03-27 13:10:41.396354", + "modified": "2026-03-29 00:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Shipment Parcel", diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json index 9eb9ba46762..6e55b59a497 100644 --- a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json @@ -15,21 +15,21 @@ "fields": [ { "fieldname": "length", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Length (cm)", "reqd": 1 }, { "fieldname": "width", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Width (cm)", "reqd": 1 }, { "fieldname": "height", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Height (cm)", "reqd": 1 @@ -52,7 +52,7 @@ } ], "links": [], - "modified": "2024-03-27 13:10:41.521126", + "modified": "2026-03-29 00:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Shipment Parcel Template", From 544c91441ba2c9ba6eaed19b2b4246c7712b8503 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 29 Mar 2026 15:53:46 +0530 Subject: [PATCH 76/90] fix: maintain state during reposting (cherry picked from commit f8738a791bb0b059057bbf6e99d265053133381f) --- erpnext/stock/stock_ledger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index c811e510b0c..838244a7fc2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -656,8 +656,8 @@ class update_entries_after: # To avoid duplicate reposting of same sle in case of multiple dependant sle self.distinct_sles.add(sle.name) - # if i % 1000 == 0: - self.update_data_in_repost(len(self._sles), i) + if i % 1000 == 0: + self.update_data_in_repost(len(self._sles), i) def sort_sles(self, sles): return sorted( From 83cac1575549b5ae3b92f4939dcdb6f3396e307e Mon Sep 17 00:00:00 2001 From: MochaMind Date: Sun, 29 Mar 2026 18:25:38 +0530 Subject: [PATCH 77/90] chore: update POT file (#53875) --- erpnext/locale/main.pot | 1007 ++++++++++++++++++++------------------- 1 file changed, 529 insertions(+), 478 deletions(-) diff --git a/erpnext/locale/main.pot b/erpnext/locale/main.pot index f627ccf1b41..21fb2c6830e 100644 --- a/erpnext/locale/main.pot +++ b/erpnext/locale/main.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: ERPNext VERSION\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" -"POT-Creation-Date: 2026-03-22 09:44+0000\n" -"PO-Revision-Date: 2026-03-22 09:44+0000\n" +"POT-Creation-Date: 2026-03-29 09:46+0000\n" +"PO-Revision-Date: 2026-03-29 09:46+0000\n" "Last-Translator: hello@frappe.io\n" "Language-Team: hello@frappe.io\n" "MIME-Version: 1.0\n" @@ -262,7 +262,7 @@ msgstr "" msgid "% of materials delivered against this Sales Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:2371 +#: erpnext/controllers/accounts_controller.py:2381 msgid "'Account' in the Accounting section of Customer {0}" msgstr "" @@ -278,7 +278,7 @@ msgstr "" msgid "'Days Since Last Order' must be greater than or equal to zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:2376 +#: erpnext/controllers/accounts_controller.py:2386 msgid "'Default {0} Account' in Company {1}" msgstr "" @@ -865,11 +865,6 @@ msgstr "" msgid "Masters & Reports" msgstr "" -#. Header text in the Selling Workspace -#: erpnext/selling/workspace/selling/selling.json -msgid "Quick Access" -msgstr "" - #. Header text in the Invoicing Workspace #. Header text in the Assets Workspace #. Header text in the Buying Workspace @@ -915,11 +910,11 @@ msgstr "" msgid "Your Shortcuts" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1133 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1134 msgid "Grand Total: {0}" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1134 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1135 msgid "Outstanding Amount: {0}" msgstr "" @@ -1335,7 +1330,7 @@ msgid "Account Manager" msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1007 -#: erpnext/controllers/accounts_controller.py:2380 +#: erpnext/controllers/accounts_controller.py:2390 msgid "Account Missing" msgstr "" @@ -1568,7 +1563,7 @@ msgstr "" msgid "Account: {0} is not permitted under Payment Entry" msgstr "" -#: erpnext/controllers/accounts_controller.py:3269 +#: erpnext/controllers/accounts_controller.py:3279 msgid "Account: {0} with currency: {1} can not be selected" msgstr "" @@ -1862,7 +1857,7 @@ msgstr "" msgid "Accounting Entry for Landed Cost Voucher for SCR {0}" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:840 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:844 msgid "Accounting Entry for Service" msgstr "" @@ -1877,18 +1872,18 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1490 #: erpnext/controllers/stock_controller.py:727 #: erpnext/controllers/stock_controller.py:744 -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:933 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:937 #: erpnext/stock/doctype/stock_entry/stock_entry.py:1904 #: erpnext/stock/doctype/stock_entry/stock_entry.py:1918 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:708 msgid "Accounting Entry for Stock" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:737 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:741 msgid "Accounting Entry for {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2421 +#: erpnext/controllers/accounts_controller.py:2431 msgid "Accounting Entry for {0}: {1} can only be made in currency: {2}" msgstr "" @@ -1961,7 +1956,7 @@ msgstr "" #: erpnext/setup/doctype/email_digest/email_digest.json #: erpnext/setup/doctype/incoterm/incoterm.json #: erpnext/setup/doctype/supplier_group/supplier_group.json -#: erpnext/setup/install.py:368 +#: erpnext/setup/install.py:369 msgid "Accounts" msgstr "" @@ -2414,7 +2409,7 @@ msgstr "" msgid "Actual Operation Time" msgstr "" -#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:430 +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:456 msgid "Actual Posting" msgstr "" @@ -2670,7 +2665,7 @@ msgstr "" msgid "Add Sub Assembly" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:516 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:517 #: erpnext/public/js/event.js:32 msgid "Add Suppliers" msgstr "" @@ -3015,6 +3010,8 @@ msgstr "" #. Invoice' #. Label of the address_and_contact_tab (Tab Break) field in DocType 'Purchase #. Order' +#. Label of the address_and_contact_tab (Tab Break) field in DocType 'Request +#. for Quotation' #. Label of the contact_and_address_tab (Tab Break) field in DocType 'Supplier' #. Label of the address_and_contact_tab (Tab Break) field in DocType 'Supplier #. Quotation' @@ -3035,6 +3032,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/buying/doctype/purchase_order/purchase_order.json +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.json #: erpnext/crm/doctype/opportunity/opportunity.json @@ -3153,7 +3151,7 @@ msgstr "" msgid "Adjustment Against" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:665 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:669 msgid "Adjustment based on Purchase Invoice rate" msgstr "" @@ -3310,12 +3308,6 @@ msgstr "" msgid "Aerospace" msgstr "" -#. Label of the affected_transactions (Code) field in DocType 'Repost Item -#. Valuation' -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Affected Transactions" -msgstr "" - #. Label of the against (Text) field in DocType 'GL Entry' #: erpnext/accounts/doctype/gl_entry/gl_entry.json #: erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.html:20 @@ -3440,7 +3432,7 @@ msgstr "" msgid "Against Stock Entry" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:332 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:331 msgid "Against Supplier Invoice {0}" msgstr "" @@ -3741,11 +3733,11 @@ msgstr "" msgid "All communications including and above this shall be moved into the new Issue" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:967 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:972 msgid "All items are already requested" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1426 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1430 msgid "All items have already been Invoiced/Returned" msgstr "" @@ -4510,6 +4502,7 @@ msgstr "" #: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:93 #: erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py:48 #: erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py:79 +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:411 #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:44 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:273 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:327 @@ -4727,8 +4720,8 @@ msgstr "" msgid "Ampere-Second" msgstr "" -#: erpnext/controllers/trends.py:269 erpnext/controllers/trends.py:281 -#: erpnext/controllers/trends.py:290 +#: erpnext/controllers/trends.py:277 erpnext/controllers/trends.py:289 +#: erpnext/controllers/trends.py:298 msgid "Amt" msgstr "" @@ -4737,7 +4730,7 @@ msgstr "" msgid "An Item Group is a way to classify items based on types." msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:535 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:537 msgid "An error has been appeared while reposting item valuation via {0}" msgstr "" @@ -4803,7 +4796,7 @@ msgstr "" msgid "Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:883 +#: erpnext/accounts/doctype/payment_request/payment_request.py:884 msgid "Another Payment Request is already processed" msgstr "" @@ -5242,11 +5235,11 @@ msgstr "" msgid "As there are reserved stock, you cannot disable {0}." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1087 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1092 msgid "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1827 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1832 msgid "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." msgstr "" @@ -5664,7 +5657,7 @@ msgstr "" msgid "Asset cannot be cancelled, as it is already {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:393 +#: erpnext/assets/doctype/asset/depreciation.py:396 msgid "Asset cannot be scrapped before the last depreciation entry." msgstr "" @@ -5696,7 +5689,7 @@ msgstr "" msgid "Asset received at Location {0} and issued to Employee {1}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:454 +#: erpnext/assets/doctype/asset/depreciation.py:457 msgid "Asset restored" msgstr "" @@ -5708,11 +5701,11 @@ msgstr "" msgid "Asset returned" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:441 +#: erpnext/assets/doctype/asset/depreciation.py:444 msgid "Asset scrapped" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:443 +#: erpnext/assets/doctype/asset/depreciation.py:446 msgid "Asset scrapped via Journal Entry {0}" msgstr "" @@ -5737,7 +5730,7 @@ msgstr "" msgid "Asset updated due to Asset Repair {0} {1}." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:375 +#: erpnext/assets/doctype/asset/depreciation.py:378 msgid "Asset {0} cannot be scrapped, as it is already {1}" msgstr "" @@ -5778,7 +5771,7 @@ msgstr "" msgid "Asset {0} is not submitted. Please submit the asset before proceeding." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:373 +#: erpnext/assets/doctype/asset/depreciation.py:376 msgid "Asset {0} must be submitted" msgstr "" @@ -5828,7 +5821,7 @@ msgstr "" msgid "Assets {assets_link} created for {item_code}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:232 +#: erpnext/manufacturing/doctype/job_card/job_card.js:240 msgid "Assign Job to Employee" msgstr "" @@ -6195,6 +6188,10 @@ msgstr "" msgid "Auto Tax Settings Error" msgstr "" +#: erpnext/setup/doctype/employee/employee.py:170 +msgid "Auto User Creation Error" +msgstr "" + #. Description of the 'Close Replied Opportunity After Days' (Int) field in #. DocType 'CRM Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json @@ -6312,7 +6309,8 @@ msgstr "" #. Label of the available_quantity_section (Section Break) field in DocType #. 'Pick List Item' #: erpnext/manufacturing/doctype/workstation/workstation.js:505 -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:88 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:118 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:175 #: erpnext/public/js/utils.js:627 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json #: erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -6530,8 +6528,7 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json #: erpnext/manufacturing/report/bom_explorer/bom_explorer.js:8 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:67 -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js:8 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js:5 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:8 #: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:109 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/selling/doctype/sales_order/sales_order.js:1415 @@ -6551,7 +6548,7 @@ msgstr "" msgid "BOM 1" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1757 +#: erpnext/manufacturing/doctype/bom/bom.py:1760 msgid "BOM 1 {0} and BOM 2 {1} should not be same" msgstr "" @@ -6692,10 +6689,6 @@ msgstr "" msgid "BOM Operations Time" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:26 -msgid "BOM Qty" -msgstr "" - #: erpnext/stock/report/item_prices/item_prices.py:60 msgid "BOM Rate" msgstr "" @@ -6715,15 +6708,12 @@ msgid "BOM Search" msgstr "" #. Name of a report -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json -msgid "BOM Stock Calculated" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json +msgid "BOM Stock Analysis" msgstr "" -#. Name of a report #. Label of a Link in the Manufacturing Workspace #. Label of a Workspace Sidebar Item -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:1 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/workspace_sidebar/manufacturing.json msgid "BOM Stock Report" @@ -6734,10 +6724,6 @@ msgstr "" msgid "BOM Tree" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:27 -msgid "BOM UOM" -msgstr "" - #. Name of a DocType #: erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json msgid "BOM Update Batch" @@ -6816,15 +6802,15 @@ msgstr "" msgid "BOM recursion: {1} cannot be parent or child of {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1492 +#: erpnext/manufacturing/doctype/bom/bom.py:1494 msgid "BOM {0} does not belong to Item {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1474 +#: erpnext/manufacturing/doctype/bom/bom.py:1476 msgid "BOM {0} must be active" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1477 +#: erpnext/manufacturing/doctype/bom/bom.py:1479 msgid "BOM {0} must be submitted" msgstr "" @@ -7754,7 +7740,7 @@ msgstr "" #. Label of a Card Break in the Manufacturing Workspace #. Label of a Link in the Manufacturing Workspace #. Label of a Workspace Sidebar Item -#: erpnext/manufacturing/doctype/bom/bom.py:1324 +#: erpnext/manufacturing/doctype/bom/bom.py:1326 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/stock/doctype/material_request/material_request.js:139 #: erpnext/stock/doctype/stock_entry/stock_entry.js:695 @@ -8934,7 +8920,7 @@ msgid "Can only make payment against unbilled {0}" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1517 -#: erpnext/controllers/accounts_controller.py:3178 +#: erpnext/controllers/accounts_controller.py:3188 #: erpnext/public/js/controllers/accounts.js:103 msgid "Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'" msgstr "" @@ -8999,7 +8985,7 @@ msgstr "" msgid "Cannot Optimize Route as Driver Address is Missing." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:181 +#: erpnext/setup/doctype/employee/employee.py:295 msgid "Cannot Relieve Employee" msgstr "" @@ -9043,7 +9029,7 @@ msgstr "" msgid "Cannot cancel because submitted Stock Entry {0} exists" msgstr "" -#: erpnext/stock/stock_ledger.py:209 +#: erpnext/stock/stock_ledger.py:179 msgid "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet." msgstr "" @@ -9055,7 +9041,7 @@ msgstr "" msgid "Cannot cancel this document as it is linked with the submitted Asset Value Adjustment {0}. Please cancel the Asset Value Adjustment to continue." msgstr "" -#: erpnext/controllers/buying_controller.py:1136 +#: erpnext/controllers/buying_controller.py:1137 msgid "Cannot cancel this document as it is linked with the submitted asset {asset_link}. Please cancel the asset to continue." msgstr "" @@ -9103,7 +9089,7 @@ msgstr "" msgid "Cannot covert to Group because Account Type is selected." msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1014 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1018 msgid "Cannot create Stock Reservation Entries for future dated Purchase Receipts." msgstr "" @@ -9120,7 +9106,7 @@ msgstr "" msgid "Cannot create return for consolidated invoice {0}." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1175 +#: erpnext/manufacturing/doctype/bom/bom.py:1177 msgid "Cannot deactivate or cancel BOM as it is linked with other BOMs" msgstr "" @@ -9141,7 +9127,7 @@ msgstr "" msgid "Cannot delete Serial No {0}, as it is used in stock transactions" msgstr "" -#: erpnext/controllers/accounts_controller.py:3774 +#: erpnext/controllers/accounts_controller.py:3784 msgid "Cannot delete an item which has been ordered" msgstr "" @@ -9191,7 +9177,7 @@ msgstr "" msgid "Cannot find Item with this Barcode" msgstr "" -#: erpnext/controllers/accounts_controller.py:3726 +#: erpnext/controllers/accounts_controller.py:3736 msgid "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." msgstr "" @@ -9215,12 +9201,12 @@ msgstr "" msgid "Cannot receive from customer against negative outstanding" msgstr "" -#: erpnext/controllers/accounts_controller.py:3922 +#: erpnext/controllers/accounts_controller.py:3932 msgid "Cannot reduce quantity than ordered or purchased quantity" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1530 -#: erpnext/controllers/accounts_controller.py:3193 +#: erpnext/controllers/accounts_controller.py:3203 #: erpnext/public/js/controllers/accounts.js:120 msgid "Cannot refer row number greater than or equal to current row number for this Charge type" msgstr "" @@ -9236,7 +9222,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1523 #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1701 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1827 -#: erpnext/controllers/accounts_controller.py:3183 +#: erpnext/controllers/accounts_controller.py:3193 #: erpnext/public/js/controllers/accounts.js:112 #: erpnext/public/js/controllers/taxes_and_totals.js:531 msgid "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" @@ -9254,11 +9240,11 @@ msgstr "" msgid "Cannot set multiple Item Defaults for a company." msgstr "" -#: erpnext/controllers/accounts_controller.py:3888 +#: erpnext/controllers/accounts_controller.py:3898 msgid "Cannot set quantity less than delivered quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3889 +#: erpnext/controllers/accounts_controller.py:3899 msgid "Cannot set quantity less than received quantity" msgstr "" @@ -9270,7 +9256,7 @@ msgstr "" msgid "Cannot start deletion. Another deletion {0} is already queued/running. Please wait for it to complete." msgstr "" -#: erpnext/controllers/accounts_controller.py:3916 +#: erpnext/controllers/accounts_controller.py:3926 msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation" msgstr "" @@ -9451,7 +9437,7 @@ msgstr "" msgid "Cash In Hand" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:322 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:321 msgid "Cash or Bank Account is mandatory for making payment entry" msgstr "" @@ -9545,8 +9531,8 @@ msgstr "" msgid "Category-wise Asset Value" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:298 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:140 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:297 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:142 msgid "Caution" msgstr "" @@ -9683,7 +9669,7 @@ msgid "Channel Partner" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2256 -#: erpnext/controllers/accounts_controller.py:3246 +#: erpnext/controllers/accounts_controller.py:3256 msgid "Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount" msgstr "" @@ -10857,7 +10843,7 @@ msgstr "" msgid "Company Address Name" msgstr "" -#: erpnext/controllers/accounts_controller.py:4340 +#: erpnext/controllers/accounts_controller.py:4352 msgid "Company Address is missing. You don't have permission to update it. Please contact your System Manager." msgstr "" @@ -10927,7 +10913,7 @@ msgid "Company Field" msgstr "" #. Label of the company_logo (Attach Image) field in DocType 'Company' -#: erpnext/public/js/print.js:75 erpnext/setup/doctype/company/company.json +#: erpnext/public/js/print.js:77 erpnext/setup/doctype/company/company.json msgid "Company Logo" msgstr "" @@ -10939,7 +10925,10 @@ msgstr "" msgid "Company Not Linked" msgstr "" +#. Label of the shipping_address (Link) field in DocType 'Request for +#. Quotation' #. Label of the shipping_address (Link) field in DocType 'Subcontracting Order' +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json msgid "Company Shipping Address" msgstr "" @@ -10988,6 +10977,10 @@ msgstr "" msgid "Company of asset {0} and purchase document {1} doesn't matches." msgstr "" +#: erpnext/setup/doctype/employee/employee.py:168 +msgid "Company or Personal Email is mandatory when 'Create User Automatically' is enabled" +msgstr "" + #. Description of the 'Registration Details' (Code) field in DocType 'Company' #: erpnext/setup/doctype/company/company.json msgid "Company registration numbers for your reference. Tax numbers etc." @@ -11058,7 +11051,7 @@ msgstr "" msgid "Competitors" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:269 +#: erpnext/manufacturing/doctype/job_card/job_card.js:277 #: erpnext/manufacturing/doctype/workstation/workstation.js:151 msgid "Complete Job" msgstr "" @@ -11105,8 +11098,8 @@ msgstr "" msgid "Completed Qty cannot be greater than 'Qty to Manufacture'" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:317 -#: erpnext/manufacturing/doctype/job_card/job_card.js:438 +#: erpnext/manufacturing/doctype/job_card/job_card.js:325 +#: erpnext/manufacturing/doctype/job_card/job_card.js:446 #: erpnext/manufacturing/doctype/workstation/workstation.js:296 msgid "Completed Quantity" msgstr "" @@ -11787,15 +11780,15 @@ msgstr "" msgid "Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}." msgstr "" -#: erpnext/controllers/accounts_controller.py:2961 +#: erpnext/controllers/accounts_controller.py:2971 msgid "Conversion rate cannot be 0" msgstr "" -#: erpnext/controllers/accounts_controller.py:2968 +#: erpnext/controllers/accounts_controller.py:2978 msgid "Conversion rate is 1.00, but document currency is different from company currency" msgstr "" -#: erpnext/controllers/accounts_controller.py:2964 +#: erpnext/controllers/accounts_controller.py:2974 msgid "Conversion rate must be 1.00 if document currency is same as company currency" msgstr "" @@ -11861,13 +11854,13 @@ msgstr "" msgid "Corrective Action" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:495 +#: erpnext/manufacturing/doctype/job_card/job_card.js:503 msgid "Corrective Job Card" msgstr "" #. Label of the corrective_operation_section (Tab Break) field in DocType 'Job #. Card' -#: erpnext/manufacturing/doctype/job_card/job_card.js:502 +#: erpnext/manufacturing/doctype/job_card/job_card.js:510 #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "Corrective Operation" msgstr "" @@ -12106,7 +12099,7 @@ msgid "Cost Center is a part of Cost Center Allocation, hence cannot be converte msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1433 -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:899 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:903 msgid "Cost Center is required in row {0} in Taxes table for type {1}" msgstr "" @@ -12428,7 +12421,7 @@ msgstr "" msgid "Create Inter Company Journal Entry" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:54 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:55 msgid "Create Invoices" msgstr "" @@ -12666,7 +12659,7 @@ msgstr "" msgid "Create Supplier" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:180 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:181 msgid "Create Supplier Quotation" msgstr "" @@ -12697,13 +12690,19 @@ msgstr "" msgid "Create Transfer Entry" msgstr "" -#. Label of the create_user (Button) field in DocType 'Employee' -#: erpnext/setup/doctype/employee/employee.json +#: erpnext/setup/doctype/employee/employee.js:50 +#: erpnext/setup/doctype/employee/employee.js:52 #: erpnext/utilities/activation.py:117 msgid "Create User" msgstr "" +#. Label of the create_user_automatically (Check) field in DocType 'Employee' +#: erpnext/setup/doctype/employee/employee.json +msgid "Create User Automatically" +msgstr "" + #. Label of the create_user_permission (Check) field in DocType 'Employee' +#: erpnext/setup/doctype/employee/employee.js:65 #: erpnext/setup/doctype/employee/employee.json msgid "Create User Permission" msgstr "" @@ -12741,7 +12740,7 @@ msgstr "" msgid "Create a variant with the template image." msgstr "" -#: erpnext/stock/stock_ledger.py:2018 +#: erpnext/stock/stock_ledger.py:2065 msgid "Create an incoming stock transaction for the Item." msgstr "" @@ -12760,12 +12759,6 @@ msgstr "" msgid "Create in Draft Status" msgstr "" -#. Description of the 'Create Missing Party' (Check) field in DocType 'Opening -#. Invoice Creation Tool' -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json -msgid "Create missing customer or supplier." -msgstr "" - #. Label of an action in the Onboarding Step 'Create Supplier' #: erpnext/buying/onboarding_step/create_supplier/create_supplier.json msgid "Create supplier" @@ -12785,6 +12778,12 @@ msgstr "" msgid "Created {0} scorecards for {1} between:" msgstr "" +#. Description of the 'Create User Automatically' (Check) field in DocType +#. 'Employee' +#: erpnext/setup/doctype/employee/employee.json +msgid "Creates a User account for this employee using the Preferred, Company, or Personal email." +msgstr "" + #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:140 msgid "Creating Accounts..." msgstr "" @@ -12809,7 +12808,7 @@ msgstr "" msgid "Creating Packing Slip ..." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:60 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:61 msgid "Creating Purchase Invoices ..." msgstr "" @@ -12823,7 +12822,7 @@ msgstr "" msgid "Creating Purchase Receipt ..." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:58 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:59 msgid "Creating Sales Invoices ..." msgstr "" @@ -12844,11 +12843,11 @@ msgstr "" msgid "Creating Subcontracting Receipt ..." msgstr "" -#: erpnext/setup/doctype/employee/employee.js:92 +#: erpnext/setup/doctype/employee/employee.js:85 msgid "Creating User..." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:287 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:300 msgid "Creating {} out of {} {}" msgstr "" @@ -13061,9 +13060,9 @@ msgstr "" #. Label of the credit_to (Link) field in DocType 'Purchase Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:379 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:387 -#: erpnext/controllers/accounts_controller.py:2360 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:378 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:386 +#: erpnext/controllers/accounts_controller.py:2370 msgid "Credit To" msgstr "" @@ -14283,7 +14282,7 @@ msgstr "" msgid "Date of Birth" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:146 +#: erpnext/setup/doctype/employee/employee.py:260 msgid "Date of Birth cannot be greater than today." msgstr "" @@ -14535,7 +14534,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1011 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1022 -#: erpnext/controllers/accounts_controller.py:2360 +#: erpnext/controllers/accounts_controller.py:2370 msgid "Debit To" msgstr "" @@ -14713,7 +14712,7 @@ msgstr "" msgid "Default BOM for {0} not found" msgstr "" -#: erpnext/controllers/accounts_controller.py:3960 +#: erpnext/controllers/accounts_controller.py:3970 msgid "Default BOM not found for FG Item {0}" msgstr "" @@ -15202,6 +15201,11 @@ msgstr "" msgid "Define Project type." msgstr "" +#. Description of the 'End of Life' (Date) field in DocType 'Item' +#: erpnext/stock/doctype/item/item.json +msgid "Defines the date after which the item can no longer be used in transactions or manufacturing" +msgstr "" + #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json msgid "Dekagram/Litre" @@ -15788,7 +15792,7 @@ msgstr "" msgid "Depreciation Entry against asset {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:255 +#: erpnext/assets/doctype/asset/depreciation.py:258 msgid "Depreciation Entry against {0} worth {1}" msgstr "" @@ -15800,7 +15804,7 @@ msgstr "" msgid "Depreciation Expense Account" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:302 +#: erpnext/assets/doctype/asset/depreciation.py:305 msgid "Depreciation Expense Account should be an Income or Expense Account." msgstr "" @@ -16003,7 +16007,7 @@ msgstr "" msgid "Difference Posting Date" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:100 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:120 msgid "Difference Qty" msgstr "" @@ -16572,7 +16576,7 @@ msgstr "" msgid "Disposal Date" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:832 +#: erpnext/assets/doctype/asset/depreciation.py:835 msgid "Disposal date {0} cannot be before {1} date {2} of the asset." msgstr "" @@ -16610,12 +16614,6 @@ msgstr "" msgid "Distance from top edge" msgstr "" -#. Label of the distinct_item_and_warehouse (Code) field in DocType 'Repost -#. Item Valuation' -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Distinct Item and Warehouse" -msgstr "" - #. Description of a DocType #: erpnext/stock/doctype/serial_no/serial_no.json msgid "Distinct unit of an Item" @@ -16767,7 +16765,7 @@ msgstr "" msgid "Do you want to submit the material request" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:103 +#: erpnext/manufacturing/doctype/job_card/job_card.js:111 msgid "Do you want to submit the stock entry?" msgstr "" @@ -16823,10 +16821,6 @@ msgstr "" msgid "Document Type already used as a dimension" msgstr "" -#: erpnext/setup/install.py:189 -msgid "Documentation" -msgstr "" - #. Description of the 'Reconciliation Queue Size' (Int) field in DocType #. 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -16884,7 +16878,7 @@ msgstr "" msgid "Download CSV Template" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:144 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:145 msgid "Download PDF for Supplier" msgstr "" @@ -17450,10 +17444,18 @@ msgstr "" msgid "Email Receipt" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:371 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:373 msgid "Email Sent to Supplier {0}" msgstr "" +#: erpnext/setup/doctype/employee/employee.py:433 +msgid "Email is required to create a user" +msgstr "" + +#: erpnext/setup/doctype/employee/employee.js:72 +msgid "Email is required to create a user." +msgstr "" + #: erpnext/stock/doctype/shipment/shipment.js:174 msgid "Email or Phone/Mobile of the Contact are mandatory to continue." msgstr "" @@ -17584,11 +17586,6 @@ msgstr "" msgid "Employee Education" msgstr "" -#. Label of the exit (Tab Break) field in DocType 'Employee' -#: erpnext/setup/doctype/employee/employee.json -msgid "Employee Exit" -msgstr "" - #. Name of a DocType #: erpnext/setup/doctype/employee_external_work_history/employee_external_work_history.json msgid "Employee External Work History" @@ -17637,11 +17634,11 @@ msgstr "" msgid "Employee User Id" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:211 +#: erpnext/setup/doctype/employee/employee.py:325 msgid "Employee cannot report to himself." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:443 +#: erpnext/setup/doctype/employee/employee.py:567 msgid "Employee is required" msgstr "" @@ -17649,6 +17646,10 @@ msgstr "" msgid "Employee is required while issuing Asset {0}" msgstr "" +#: erpnext/setup/doctype/employee/employee.py:430 +msgid "Employee {0} already has a linked user" +msgstr "" + #: erpnext/assets/doctype/asset_movement/asset_movement.py:92 #: erpnext/assets/doctype/asset_movement/asset_movement.py:113 msgid "Employee {0} does not belong to the company {1}" @@ -17658,7 +17659,7 @@ msgstr "" msgid "Employee {0} is currently working on another workstation. Please assign another employee." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:468 +#: erpnext/setup/doctype/employee/employee.py:592 msgid "Employee {0} not found" msgstr "" @@ -17874,6 +17875,11 @@ msgstr "" msgid "Enable to apply SLA on every {0}" msgstr "" +#. Description of the 'Retain Sample' (Check) field in DocType 'Item' +#: erpnext/stock/doctype/item/item.json +msgid "Enable to reserve a small sample from each batch for any analysis arising ahead" +msgstr "" + #. Label of the enable_tracking_sales_commissions (Check) field in DocType #. 'Selling Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json @@ -17927,8 +17933,8 @@ msgstr "" #. Label of the end_time (Time) field in DocType 'Stock Reposting Settings' #. Label of the end_time (Time) field in DocType 'Service Day' #. Label of the end_time (Datetime) field in DocType 'Call Log' -#: erpnext/manufacturing/doctype/job_card/job_card.js:375 -#: erpnext/manufacturing/doctype/job_card/job_card.js:445 +#: erpnext/manufacturing/doctype/job_card/job_card.js:383 +#: erpnext/manufacturing/doctype/job_card/job_card.js:453 #: erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json #: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json #: erpnext/support/doctype/service_day/service_day.json @@ -17987,12 +17993,6 @@ msgstr "" msgid "Engineer" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:13 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:23 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:30 -msgid "Enough Parts to Build" -msgstr "" - #. Label of the ensure_delivery_based_on_produced_serial_no (Check) field in #. DocType 'Sales Order Item' #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -18003,11 +18003,11 @@ msgstr "" msgid "Enter API key in Google Settings." msgstr "" -#: erpnext/public/js/print.js:62 +#: erpnext/public/js/print.js:64 msgid "Enter Company Details" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:108 +#: erpnext/setup/doctype/employee/employee.js:148 msgid "Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched." msgstr "" @@ -18019,8 +18019,8 @@ msgstr "" msgid "Enter Serial Nos" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:402 -#: erpnext/manufacturing/doctype/job_card/job_card.js:471 +#: erpnext/manufacturing/doctype/job_card/job_card.js:410 +#: erpnext/manufacturing/doctype/job_card/job_card.js:479 #: erpnext/manufacturing/doctype/workstation/workstation.js:312 msgid "Enter Value" msgstr "" @@ -18161,11 +18161,11 @@ msgstr "" msgid "Error Description" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:277 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:290 msgid "Error Occurred" msgstr "" -#: erpnext/telephony/doctype/call_log/call_log.py:195 +#: erpnext/telephony/doctype/call_log/call_log.py:197 msgid "Error during caller information update" msgstr "" @@ -18181,7 +18181,7 @@ msgstr "" msgid "Error in party matching for Bank Transaction {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:319 +#: erpnext/assets/doctype/asset/depreciation.py:322 msgid "Error while posting depreciation entries" msgstr "" @@ -18189,7 +18189,7 @@ msgstr "" msgid "Error while processing deferred accounting for {0}" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:531 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:533 msgid "Error while reposting item valuation" msgstr "" @@ -18262,7 +18262,7 @@ msgstr "" msgid "Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings." msgstr "" -#: erpnext/stock/stock_ledger.py:2281 +#: erpnext/stock/stock_ledger.py:2328 msgid "Example: Serial No {0} reserved in {1}." msgstr "" @@ -18486,12 +18486,17 @@ msgstr "" msgid "Existing Customer" msgstr "" +#. Label of the exit (Tab Break) field in DocType 'Employee' +#: erpnext/setup/doctype/employee/employee.json +msgid "Exit" +msgstr "" + #. Label of the held_on (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Exit Interview Held On" msgstr "" -#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:444 +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:470 msgid "Expected" msgstr "" @@ -18605,7 +18610,7 @@ msgstr "" #: erpnext/accounts/doctype/cashier_closing/cashier_closing.json #: erpnext/accounts/doctype/ledger_merge/ledger_merge.json #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:605 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:604 #: erpnext/accounts/report/account_balance/account_balance.js:28 #: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:89 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:183 @@ -18676,13 +18681,13 @@ msgstr "" msgid "Expense Head" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:499 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:523 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:543 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:498 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:522 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:542 msgid "Expense Head Changed" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:601 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:600 msgid "Expense account is mandatory for item {0}" msgstr "" @@ -18804,6 +18809,10 @@ msgstr "" msgid "FG / Semi FG Item" msgstr "" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:21 +msgid "FG Items to Make" +msgstr "" + #. Option for the 'Default Stock Valuation Method' (Select) field in DocType #. 'Company' #. Option for the 'Valuation Method' (Select) field in DocType 'Item' @@ -19004,7 +19013,7 @@ msgstr "" msgid "Fetched only {0} available serial numbers." msgstr "" -#: erpnext/edi/doctype/code_list/code_list_import.py:27 +#: erpnext/edi/doctype/code_list/code_list_import.py:28 msgid "Fetching Error" msgstr "" @@ -19316,15 +19325,15 @@ msgstr "" msgid "Finished Good Item Quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3946 +#: erpnext/controllers/accounts_controller.py:3956 msgid "Finished Good Item is not specified for service item {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:3963 +#: erpnext/controllers/accounts_controller.py:3973 msgid "Finished Good Item {0} Qty can not be zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:3957 +#: erpnext/controllers/accounts_controller.py:3967 msgid "Finished Good Item {0} must be a sub-contracted item" msgstr "" @@ -19725,7 +19734,7 @@ msgid "For Job Card" msgstr "" #. Label of the for_operation (Link) field in DocType 'Job Card' -#: erpnext/manufacturing/doctype/job_card/job_card.js:515 +#: erpnext/manufacturing/doctype/job_card/job_card.js:523 #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "For Operation" msgstr "" @@ -19857,7 +19866,7 @@ msgstr "" msgid "For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1716 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1721 msgid "For row {0}: Enter Planned Qty" msgstr "" @@ -20034,8 +20043,8 @@ msgstr "" msgid "From BOM" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:63 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:25 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:105 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:169 msgid "From BOM No" msgstr "" @@ -20468,7 +20477,7 @@ msgstr "" msgid "Future Payments" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:382 +#: erpnext/assets/doctype/asset/depreciation.py:385 msgid "Future date is not allowed" msgstr "" @@ -20771,9 +20780,9 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1125 #: erpnext/buying/doctype/purchase_order/purchase_order.js:540 #: erpnext/buying/doctype/purchase_order/purchase_order.js:563 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:379 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:401 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:446 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:380 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:402 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:447 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:75 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:108 #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js:80 @@ -20816,7 +20825,7 @@ msgstr "" msgid "Get Items from BOM" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:418 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:419 msgid "Get Items from Material Requests against this Supplier" msgstr "" @@ -20906,12 +20915,12 @@ msgstr "" msgid "Get Sub Assembly Items" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:460 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:480 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:461 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:481 msgid "Get Suppliers" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:484 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:485 msgid "Get Suppliers By" msgstr "" @@ -21558,8 +21567,8 @@ msgstr "" msgid "Hectopascal" msgstr "" -#. Label of the height (Int) field in DocType 'Shipment Parcel' -#. Label of the height (Int) field in DocType 'Shipment Parcel Template' +#. Label of the height (Float) field in DocType 'Shipment Parcel' +#. Label of the height (Float) field in DocType 'Shipment Parcel Template' #: erpnext/stock/doctype/shipment_parcel/shipment_parcel.json #: erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json msgid "Height (cm)" @@ -21585,11 +21594,11 @@ msgstr "" msgid "Helps you distribute the Budget/Target across months if you have seasonality in your business." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:349 +#: erpnext/assets/doctype/asset/depreciation.py:352 msgid "Here are the error logs for the aforementioned failed depreciation entries: {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:2003 +#: erpnext/stock/stock_ledger.py:2050 msgid "Here are the options to proceed:" msgstr "" @@ -21604,7 +21613,7 @@ msgstr "" msgid "Here you can maintain height, weight, allergies, medical concerns etc" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:134 +#: erpnext/setup/doctype/employee/employee.js:174 msgid "Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated." msgstr "" @@ -21617,7 +21626,7 @@ msgstr "" msgid "Hertz" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:533 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:535 msgid "Hi," msgstr "" @@ -22174,10 +22183,16 @@ msgstr "" msgid "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template." msgstr "" -#: erpnext/stock/stock_ledger.py:2013 +#: erpnext/stock/stock_ledger.py:2060 msgid "If not, you can Cancel / Submit this entry" msgstr "" +#. Description of the 'Create Missing Party' (Check) field in DocType 'Opening +#. Invoice Creation Tool' +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json +msgid "If party does not exist, create it using the Party Name field." +msgstr "" + #. Description of the 'Free Item Rate' (Currency) field in DocType 'Pricing #. Rule' #: erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -22203,7 +22218,7 @@ msgstr "" msgid "If the account is frozen, entries are allowed to restricted users." msgstr "" -#: erpnext/stock/stock_ledger.py:2006 +#: erpnext/stock/stock_ledger.py:2053 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 "" @@ -22300,11 +22315,11 @@ msgstr "" msgid "If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1092 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1097 msgid "If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1832 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1837 msgid "If you still want to proceed, please enable {0}." msgstr "" @@ -22384,7 +22399,7 @@ msgstr "" msgid "Ignore Existing Ordered Qty" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1824 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1829 msgid "Ignore Existing Projected Quantity" msgstr "" @@ -22496,6 +22511,10 @@ msgstr "" msgid "Import Data" msgstr "" +#: erpnext/setup/doctype/employee/employee_list.js:16 +msgid "Import Employees" +msgstr "" + #: erpnext/edi/doctype/code_list/code_list.js:7 #: erpnext/edi/doctype/code_list/code_list_list.js:3 #: erpnext/edi/doctype/common_code/common_code_list.js:3 @@ -22607,12 +22626,6 @@ msgstr "" msgid "In Stock" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:12 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:22 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:29 -msgid "In Stock Qty" -msgstr "" - #. Option for the 'Status' (Select) field in DocType 'Delivery Trip' #. Option for the 'Transfer Status' (Select) field in DocType 'Material #. Request' @@ -23354,8 +23367,8 @@ msgstr "" msgid "Insufficient Capacity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3840 -#: erpnext/controllers/accounts_controller.py:3864 +#: erpnext/controllers/accounts_controller.py:3850 +#: erpnext/controllers/accounts_controller.py:3874 msgid "Insufficient Permissions" msgstr "" @@ -23364,12 +23377,12 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.py:162 #: erpnext/stock/doctype/pick_list/pick_list.py:1055 #: erpnext/stock/doctype/stock_entry/stock_entry.py:956 -#: erpnext/stock/serial_batch_bundle.py:1205 erpnext/stock/stock_ledger.py:1713 -#: erpnext/stock/stock_ledger.py:2172 +#: erpnext/stock/serial_batch_bundle.py:1205 erpnext/stock/stock_ledger.py:1741 +#: erpnext/stock/stock_ledger.py:2219 msgid "Insufficient Stock" msgstr "" -#: erpnext/stock/stock_ledger.py:2187 +#: erpnext/stock/stock_ledger.py:2234 msgid "Insufficient Stock for Batch" msgstr "" @@ -23604,14 +23617,14 @@ msgstr "" msgid "Interval should be between 1 to 59 MInutes" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:380 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:388 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:379 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:387 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1017 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1027 #: erpnext/assets/doctype/asset_category/asset_category.py:69 #: erpnext/assets/doctype/asset_category/asset_category.py:97 -#: erpnext/controllers/accounts_controller.py:3207 -#: erpnext/controllers/accounts_controller.py:3215 +#: erpnext/controllers/accounts_controller.py:3217 +#: erpnext/controllers/accounts_controller.py:3225 msgid "Invalid Account" msgstr "" @@ -23620,7 +23633,7 @@ msgid "Invalid Accounting Dimension" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:400 -#: erpnext/accounts/doctype/payment_request/payment_request.py:1004 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1005 msgid "Invalid Allocated Amount" msgstr "" @@ -23662,7 +23675,7 @@ msgstr "" #: erpnext/assets/doctype/asset/asset.py:361 #: erpnext/assets/doctype/asset/asset.py:368 -#: erpnext/controllers/accounts_controller.py:3230 +#: erpnext/controllers/accounts_controller.py:3240 msgid "Invalid Cost Center" msgstr "" @@ -23696,7 +23709,7 @@ msgid "Invalid Group By" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:499 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:955 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:960 msgid "Invalid Item" msgstr "" @@ -23748,7 +23761,7 @@ msgstr "" msgid "Invalid Priority" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1230 +#: erpnext/manufacturing/doctype/bom/bom.py:1232 msgid "Invalid Process Loss Configuration" msgstr "" @@ -23756,8 +23769,8 @@ msgstr "" msgid "Invalid Purchase Invoice" msgstr "" -#: erpnext/controllers/accounts_controller.py:3884 -#: erpnext/controllers/accounts_controller.py:3898 +#: erpnext/controllers/accounts_controller.py:3894 +#: erpnext/controllers/accounts_controller.py:3908 msgid "Invalid Qty" msgstr "" @@ -23865,7 +23878,7 @@ msgid "Invalid {0}: {1}" msgstr "" #. Label of the inventory_section (Tab Break) field in DocType 'Item' -#: erpnext/setup/install.py:358 erpnext/stock/doctype/item/item.json +#: erpnext/setup/install.py:359 erpnext/stock/doctype/item/item.json msgid "Inventory" msgstr "" @@ -24824,10 +24837,8 @@ msgstr "" #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:109 #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:25 -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:50 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:9 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:19 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:22 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:101 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:165 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:68 #: erpnext/manufacturing/report/process_loss_report/process_loss_report.js:15 #: erpnext/manufacturing/report/process_loss_report/process_loss_report.py:74 @@ -25217,7 +25228,7 @@ msgstr "" msgid "Item Code cannot be changed for Serial No." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:455 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:454 msgid "Item Code required at Row No {0}" msgstr "" @@ -25596,7 +25607,6 @@ msgstr "" #: erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.html:8 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:66 #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py:109 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:23 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:106 #: erpnext/manufacturing/report/job_card_summary/job_card_summary.py:158 #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:958 @@ -26033,7 +26043,7 @@ msgstr "" msgid "Item operation" msgstr "" -#: erpnext/controllers/accounts_controller.py:3938 +#: erpnext/controllers/accounts_controller.py:3948 msgid "Item qty can not be updated as raw materials are already processed." msgstr "" @@ -26108,7 +26118,7 @@ msgstr "" msgid "Item {0} has reached its end of life on {1}" msgstr "" -#: erpnext/stock/stock_ledger.py:118 +#: erpnext/stock/stock_ledger.py:117 msgid "Item {0} ignored since it is not a stock item" msgstr "" @@ -26132,7 +26142,7 @@ msgstr "" msgid "Item {0} is not a stock Item" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:954 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:959 msgid "Item {0} is not a subcontracted item" msgstr "" @@ -26164,7 +26174,7 @@ msgstr "" msgid "Item {0} not found." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:325 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:324 msgid "Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item)." msgstr "" @@ -26242,7 +26252,7 @@ msgstr "" msgid "Items Filter" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1678 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1683 #: erpnext/selling/doctype/sales_order/sales_order.js:1676 msgid "Items Required" msgstr "" @@ -26266,11 +26276,11 @@ msgstr "" msgid "Items and Pricing" msgstr "" -#: erpnext/controllers/accounts_controller.py:4198 +#: erpnext/controllers/accounts_controller.py:4208 msgid "Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4191 +#: erpnext/controllers/accounts_controller.py:4201 msgid "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." msgstr "" @@ -26292,7 +26302,7 @@ msgstr "" msgid "Items to Be Repost" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1677 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1682 msgid "Items to Manufacture are required to pull the Raw Materials associated with it." msgstr "" @@ -26868,7 +26878,7 @@ msgstr "" #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/bom_creator/bom_creator.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:106 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:123 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/report/item_prices/item_prices.py:56 msgid "Last Purchase Rate" @@ -27140,6 +27150,11 @@ msgstr "" msgid "Ledgers" msgstr "" +#. Label of the vouchers_posted (Int) field in DocType 'Repost Item Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "Ledgers Posted" +msgstr "" + #. Label of the left_child (Link) field in DocType 'Bisect Nodes' #: erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json msgid "Left Child" @@ -27170,8 +27185,8 @@ msgstr "" msgid "Legend" msgstr "" -#. Label of the length (Int) field in DocType 'Shipment Parcel' -#. Label of the length (Int) field in DocType 'Shipment Parcel Template' +#. Label of the length (Float) field in DocType 'Shipment Parcel' +#. Label of the length (Float) field in DocType 'Shipment Parcel Template' #: erpnext/stock/doctype/shipment_parcel/shipment_parcel.json #: erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json msgid "Length (cm)" @@ -27286,7 +27301,7 @@ msgstr "" msgid "Link to Material Request" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:451 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:452 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:80 msgid "Link to Material Requests" msgstr "" @@ -27924,7 +27939,7 @@ msgstr "" #. Label of the make (Data) field in DocType 'Vehicle' #: erpnext/accounts/doctype/journal_entry/journal_entry.js:123 -#: erpnext/manufacturing/doctype/job_card/job_card.js:536 +#: erpnext/manufacturing/doctype/job_card/job_card.js:544 #: erpnext/manufacturing/doctype/work_order/work_order.js:832 #: erpnext/manufacturing/doctype/work_order/work_order.js:866 #: erpnext/setup/doctype/vehicle/vehicle.json @@ -27984,12 +27999,12 @@ msgstr "" msgid "Make Serial No / Batch from Work Order" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:101 +#: erpnext/manufacturing/doctype/job_card/job_card.js:109 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:256 msgid "Make Stock Entry" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:410 +#: erpnext/manufacturing/doctype/job_card/job_card.js:418 msgid "Make Subcontracting PO" msgstr "" @@ -28193,7 +28208,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:70 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:110 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/item_manufacturer/item_manufacturer.json #: erpnext/stock/doctype/manufacturer/manufacturer.json @@ -28223,7 +28238,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:76 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:113 #: erpnext/stock/doctype/item_manufacturer/item_manufacturer.json #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -28257,7 +28272,7 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/selling/doctype/sales_order/sales_order_dashboard.py:29 -#: erpnext/setup/doctype/company/company.json erpnext/setup/install.py:363 +#: erpnext/setup/doctype/company/company.json erpnext/setup/install.py:364 #: erpnext/setup/setup_wizard/data/industry_type.txt:31 #: erpnext/stock/doctype/batch/batch.json erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/item_lead_time/item_lead_time.json @@ -28606,14 +28621,14 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/buying/doctype/purchase_order/purchase_order.js:519 #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:360 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:361 #: erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:56 #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json #: erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js:33 #: erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py:184 #: erpnext/buying/workspace/buying/buying.json -#: erpnext/manufacturing/doctype/job_card/job_card.js:160 +#: erpnext/manufacturing/doctype/job_card/job_card.js:168 #: erpnext/manufacturing/doctype/production_plan/production_plan.js:159 #: erpnext/manufacturing/doctype/production_plan/production_plan.json #: erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json @@ -28728,7 +28743,7 @@ msgstr "" msgid "Material Request used to make this Stock Entry" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1337 +#: erpnext/controllers/subcontracting_controller.py:1343 msgid "Material Request {0} is cancelled or stopped" msgstr "" @@ -28782,7 +28797,7 @@ msgstr "" #. Option for the 'Purpose' (Select) field in DocType 'Pick List' #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry' #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type' -#: erpnext/manufacturing/doctype/job_card/job_card.js:174 +#: erpnext/manufacturing/doctype/job_card/job_card.js:182 #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json #: erpnext/setup/setup_wizard/operations/install_fixtures.py:83 #: erpnext/stock/doctype/item/item.json @@ -28847,7 +28862,7 @@ msgstr "" msgid "Materials To Be Transferred" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1570 +#: erpnext/controllers/subcontracting_controller.py:1576 msgid "Materials are already received against the {0} {1}" msgstr "" @@ -28943,6 +28958,11 @@ msgstr "" msgid "Maximum Payment Amount" msgstr "" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:82 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:151 +msgid "Maximum Producible Items" +msgstr "" + #: erpnext/stock/doctype/stock_entry/stock_entry.py:3870 msgid "Maximum Samples - {0} can be retained for Batch {1} and Item {2}." msgstr "" @@ -29002,7 +29022,7 @@ msgstr "" msgid "Megawatt" msgstr "" -#: erpnext/stock/stock_ledger.py:2019 +#: erpnext/stock/stock_ledger.py:2066 msgid "Mention Valuation Rate in the Item master." msgstr "" @@ -29375,7 +29395,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:97 #: erpnext/accounts/doctype/pos_profile/pos_profile.py:200 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:597 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:596 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2422 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3030 #: erpnext/assets/doctype/asset_category/asset_category.py:116 @@ -29415,7 +29435,7 @@ msgstr "" msgid "Missing Item" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:443 +#: erpnext/setup/doctype/employee/employee.py:567 msgid "Missing Parameter" msgstr "" @@ -29439,7 +29459,7 @@ msgstr "" msgid "Missing required filter: {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1183 +#: erpnext/manufacturing/doctype/bom/bom.py:1185 #: erpnext/manufacturing/doctype/work_order/work_order.py:1476 msgid "Missing value" msgstr "" @@ -29721,7 +29741,7 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order/work_order.py:1423 #: erpnext/setup/doctype/uom/uom.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:267 -#: erpnext/utilities/transaction_base.py:567 +#: erpnext/utilities/transaction_base.py:568 msgid "Must be Whole Number" msgstr "" @@ -30426,7 +30446,7 @@ msgstr "" msgid "No Item with Serial No {0}" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1488 +#: erpnext/controllers/subcontracting_controller.py:1494 msgid "No Items selected for transfer." msgstr "" @@ -30461,7 +30481,7 @@ msgstr "" msgid "No Permission" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:787 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:792 msgid "No Purchase Orders were created" msgstr "" @@ -30470,7 +30490,7 @@ msgstr "" msgid "No Records for these settings." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:337 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:336 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1105 msgid "No Remarks" msgstr "" @@ -30515,12 +30535,12 @@ msgstr "" msgid "No Unreconciled Payments found for this party" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:784 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:789 #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:249 msgid "No Work Orders were created" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:829 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:833 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:860 msgid "No accounting entries for the following warehouses" msgstr "" @@ -30569,7 +30589,7 @@ msgstr "" msgid "No employee was scheduled for call popup" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1379 +#: erpnext/controllers/subcontracting_controller.py:1385 msgid "No item available for transfer." msgstr "" @@ -30594,7 +30614,7 @@ msgstr "" msgid "No matches occurred via auto reconciliation" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1036 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1041 msgid "No material request created" msgstr "" @@ -30627,6 +30647,12 @@ msgstr "" msgid "No of Interactions" msgstr "" +#. Label of the total_reposting_count (Int) field in DocType 'Repost Item +#. Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "No of Items to Repost" +msgstr "" + #. Label of the no_of_months_exp (Int) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json msgid "No of Months (Expense)" @@ -30806,7 +30832,7 @@ msgstr "" msgid "Non Profit" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1590 +#: erpnext/manufacturing/doctype/bom/bom.py:1593 msgid "Non stock items" msgstr "" @@ -31650,7 +31676,7 @@ msgstr "" msgid "Opening Entry can not be created after Period Closing Voucher is created." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:286 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:299 msgid "Opening Invoice Creation In Progress" msgstr "" @@ -31669,7 +31695,7 @@ msgstr "" msgid "Opening Invoice Creation Tool Item" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:100 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:101 msgid "Opening Invoice Item" msgstr "" @@ -31687,7 +31713,7 @@ msgstr "" msgid "Opening Invoices" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:140 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:142 msgid "Opening Invoices Summary" msgstr "" @@ -31767,7 +31793,7 @@ msgstr "" msgid "Operating Cost Per BOM Quantity" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1677 +#: erpnext/manufacturing/doctype/bom/bom.py:1680 msgid "Operating Cost as per Work Order / BOM" msgstr "" @@ -31858,7 +31884,7 @@ msgstr "" msgid "Operation time does not depend on quantity to produce" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:578 +#: erpnext/manufacturing/doctype/job_card/job_card.js:586 msgid "Operation {0} added multiple times in the work order {1}" msgstr "" @@ -31892,7 +31918,7 @@ msgstr "" msgid "Operations Routing" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1192 +#: erpnext/manufacturing/doctype/bom/bom.py:1194 msgid "Operations cannot be left blank" msgstr "" @@ -31941,7 +31967,7 @@ msgstr "" #. Label of the opportunity_name (Link) field in DocType 'Customer' #. Label of the opportunity (Link) field in DocType 'Quotation' #. Label of a Workspace Sidebar Item -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:384 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:385 #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.json #: erpnext/crm/doctype/crm_settings/crm_settings.json @@ -32457,7 +32483,7 @@ msgstr "" msgid "Over Billing Allowance (%)" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1317 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1321 msgid "Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%" msgstr "" @@ -32560,7 +32586,7 @@ msgstr "" msgid "Overlap in scoring between {0} and {1}" msgstr "" -#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:199 +#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:201 msgid "Overlapping conditions found between:" msgstr "" @@ -33098,7 +33124,7 @@ msgstr "" msgid "Paid To Account Type" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:327 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:326 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1151 msgid "Paid amount + Write Off Amount can not be greater than Grand Total" msgstr "" @@ -33302,7 +33328,7 @@ msgstr "" msgid "Parsed file is not in valid MT940 format or contains no transactions." msgstr "" -#: erpnext/edi/doctype/code_list/code_list_import.py:39 +#: erpnext/edi/doctype/code_list/code_list_import.py:45 msgid "Parsing Error" msgstr "" @@ -33475,8 +33501,6 @@ msgstr "" #. Label of the party (Dynamic Link) field in DocType 'Journal Entry Account' #. Label of the party (Dynamic Link) field in DocType 'Journal Entry Template #. Account' -#. Label of the party (Dynamic Link) field in DocType 'Opening Invoice Creation -#. Tool Item' #. Label of the party (Dynamic Link) field in DocType 'Payment Entry' #. Label of the party (Dynamic Link) field in DocType 'Payment Ledger Entry' #. Label of the party (Dynamic Link) field in DocType 'Payment Reconciliation' @@ -33495,7 +33519,6 @@ msgstr "" #: erpnext/accounts/doctype/gl_entry/gl_entry.json #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json #: erpnext/accounts/doctype/journal_entry_template_account/journal_entry_template_account.json -#: erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json #: erpnext/accounts/doctype/payment_entry/payment_entry.json #: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -33567,7 +33590,7 @@ msgstr "" msgid "Party Account No. (Bank Statement)" msgstr "" -#: erpnext/controllers/accounts_controller.py:2452 +#: erpnext/controllers/accounts_controller.py:2462 msgid "Party Account {0} currency ({1}) and document currency ({2}) should be same" msgstr "" @@ -33595,6 +33618,12 @@ msgstr "" msgid "Party IBAN (Bank Statement)" msgstr "" +#. Label of the party (Dynamic Link) field in DocType 'Opening Invoice Creation +#. Tool Item' +#: erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json +msgid "Party ID" +msgstr "" + #. Label of the section_break_7 (Section Break) field in DocType 'Pricing Rule' #. Label of the section_break_8 (Section Break) field in DocType 'Promotional #. Scheme' @@ -33617,10 +33646,13 @@ msgstr "" msgid "Party Mismatch" msgstr "" +#. Label of the party_name (Data) field in DocType 'Opening Invoice Creation +#. Tool Item' #. Label of the party_name (Data) field in DocType 'Payment Entry' #. Label of the party_name (Data) field in DocType 'Payment Request' #. Label of the party_name (Dynamic Link) field in DocType 'Contract' #. Label of the party (Dynamic Link) field in DocType 'Party Specific Item' +#: erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json #: erpnext/accounts/doctype/payment_entry/payment_entry.json #: erpnext/accounts/doctype/payment_request/payment_request.json #: erpnext/accounts/report/general_ledger/general_ledger.js:110 @@ -33780,7 +33812,7 @@ msgstr "" msgid "Pause" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:255 +#: erpnext/manufacturing/doctype/job_card/job_card.js:263 msgid "Pause Job" msgstr "" @@ -34274,7 +34306,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/controllers/accounts_controller.py:2732 +#: erpnext/controllers/accounts_controller.py:2742 #: erpnext/selling/doctype/quotation/quotation.json #: erpnext/selling/doctype/sales_order/sales_order.json msgid "Payment Schedule" @@ -34902,7 +34934,7 @@ msgstr "" #. Label of the phone_no (Data) field in DocType 'Company' #. Label of the phone_no (Data) field in DocType 'Warehouse' -#: erpnext/public/js/print.js:77 erpnext/setup/doctype/company/company.json +#: erpnext/public/js/print.js:79 erpnext/setup/doctype/company/company.json #: erpnext/stock/doctype/warehouse/warehouse.json msgid "Phone No" msgstr "" @@ -35278,7 +35310,7 @@ msgstr "" msgid "Please Set Priority" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:155 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:166 msgid "Please Set Supplier Group in Buying Settings." msgstr "" @@ -35298,7 +35330,7 @@ msgstr "" msgid "Please add Operations first." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:210 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:212 msgid "Please add Request for Quotation to the sidebar in Portal Settings." msgstr "" @@ -35306,7 +35338,7 @@ msgstr "" msgid "Please add Root Account for - {0}" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:302 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:315 msgid "Please add a Temporary Opening account in Chart of Accounts" msgstr "" @@ -35372,7 +35404,7 @@ msgstr "" msgid "Please check the 'Enable Serial and Batch No for Item' checkbox in the {0} to make Serial and Batch Bundle for the item." msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:539 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:541 msgid "Please check the error message and take necessary actions to fix the error and then restart the reposting again." msgstr "" @@ -35437,7 +35469,7 @@ msgstr "" msgid "Please delete Product Bundle {0}, before merging {1} into {2}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:556 +#: erpnext/assets/doctype/asset/depreciation.py:559 msgid "Please disable workflow temporarily for Journal Entry {0}" msgstr "" @@ -35473,11 +35505,11 @@ msgstr "" msgid "Please enable {} in {} to allow same item in multiple rows" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:377 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:376 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:385 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:384 msgid "Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account." msgstr "" @@ -35543,10 +35575,6 @@ msgstr "" msgid "Please enter Planned Qty for Item {0} at row {1}" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:83 -msgid "Please enter Preferred Contact Email" -msgstr "" - #: erpnext/manufacturing/doctype/work_order/work_order.js:73 msgid "Please enter Production Item first" msgstr "" @@ -35604,7 +35632,7 @@ msgstr "" msgid "Please enter company name first" msgstr "" -#: erpnext/controllers/accounts_controller.py:2958 +#: erpnext/controllers/accounts_controller.py:2968 msgid "Please enter default currency in Company Master" msgstr "" @@ -35624,7 +35652,7 @@ msgstr "" msgid "Please enter quantity for item {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:183 +#: erpnext/setup/doctype/employee/employee.py:297 msgid "Please enter relieving date." msgstr "" @@ -35644,7 +35672,7 @@ msgstr "" msgid "Please enter the phone number first" msgstr "" -#: erpnext/controllers/buying_controller.py:1184 +#: erpnext/controllers/buying_controller.py:1185 msgid "Please enter the {schedule_date}." msgstr "" @@ -35652,7 +35680,7 @@ msgstr "" msgid "Please enter valid Financial Year Start and End Dates" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:219 +#: erpnext/setup/doctype/employee/employee.py:333 msgid "Please enter {0}" msgstr "" @@ -35692,7 +35720,7 @@ msgstr "" msgid "Please import accounts against parent company or enable {} in company master." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:180 +#: erpnext/setup/doctype/employee/employee.py:294 msgid "Please make sure the employees above report to another Active employee." msgstr "" @@ -35847,7 +35875,7 @@ msgstr "" msgid "Please select Posting Date first" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1243 +#: erpnext/manufacturing/doctype/bom/bom.py:1245 msgid "Please select Price List" msgstr "" @@ -35875,11 +35903,11 @@ msgstr "" msgid "Please select Subcontracting Order instead of Purchase Order {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2807 +#: erpnext/controllers/accounts_controller.py:2817 msgid "Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1498 +#: erpnext/manufacturing/doctype/bom/bom.py:1500 msgid "Please select a BOM" msgstr "" @@ -36035,7 +36063,7 @@ msgstr "" msgid "Please select rows to create Reposting Entries" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:92 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:93 msgid "Please select the Company" msgstr "" @@ -36079,11 +36107,11 @@ msgstr "" msgid "Please set 'Apply Additional Discount On'" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:783 +#: erpnext/assets/doctype/asset/depreciation.py:786 msgid "Please set 'Asset Depreciation Cost Center' in Company {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:781 +#: erpnext/assets/doctype/asset/depreciation.py:784 msgid "Please set 'Gain/Loss Account on Asset Disposal' in Company {0}" msgstr "" @@ -36125,7 +36153,7 @@ msgstr "" msgid "Please set Customer Address to determine if the transaction is an export." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:745 +#: erpnext/assets/doctype/asset/depreciation.py:748 msgid "Please set Depreciation related Accounts in Asset Category {0} or Company {1}" msgstr "" @@ -36143,11 +36171,11 @@ msgstr "" msgid "Please set Fiscal Code for the public administration '%s'" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:731 +#: erpnext/assets/doctype/asset/depreciation.py:734 msgid "Please set Fixed Asset Account in Asset Category {0}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:594 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:593 msgid "Please set Fixed Asset Account in {} against {}." msgstr "" @@ -36193,7 +36221,7 @@ msgstr "" msgid "Please set a default Holiday List for Company {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:270 +#: erpnext/setup/doctype/employee/employee.py:384 msgid "Please set a default Holiday List for Employee {0} or Company {1}" msgstr "" @@ -36271,7 +36299,7 @@ msgstr "" msgid "Please set filter based on Item or Warehouse" msgstr "" -#: erpnext/controllers/accounts_controller.py:2368 +#: erpnext/controllers/accounts_controller.py:2378 msgid "Please set one of the following:" msgstr "" @@ -36287,7 +36315,7 @@ msgstr "" msgid "Please set the Customer Address" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:170 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:182 msgid "Please set the Default Cost Center in {0} company." msgstr "" @@ -36346,7 +36374,7 @@ msgstr "" msgid "Please setup and enable a group account with the Account Type - {0} for the company {1}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:354 +#: erpnext/assets/doctype/asset/depreciation.py:357 msgid "Please share this email with your support team so that they can find and fix the issue." msgstr "" @@ -36360,7 +36388,7 @@ msgstr "" msgid "Please specify Company to proceed" msgstr "" -#: erpnext/controllers/accounts_controller.py:3189 +#: erpnext/controllers/accounts_controller.py:3199 #: erpnext/public/js/controllers/accounts.js:117 msgid "Please specify a valid Row ID for row {0} in table {1}" msgstr "" @@ -36436,7 +36464,7 @@ msgstr "" msgid "Portal Users" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:406 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:407 msgid "Possible Supplier" msgstr "" @@ -36605,7 +36633,7 @@ msgstr "" msgid "Posting Date Inheritance for Exchange Gain / Loss" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:269 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:270 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:142 msgid "Posting Date cannot be future date" msgstr "" @@ -36814,7 +36842,7 @@ msgid "Preventive Maintenance" msgstr "" #. Label of the preview (Button) field in DocType 'Request for Quotation' -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:266 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:267 #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json msgid "Preview Email" msgstr "" @@ -37449,7 +37477,7 @@ msgstr "" msgid "Process Loss" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1226 +#: erpnext/manufacturing/doctype/bom/bom.py:1228 msgid "Process Loss Percentage cannot be greater than 100" msgstr "" @@ -37471,7 +37499,7 @@ msgstr "" msgid "Process Loss Qty" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:332 +#: erpnext/manufacturing/doctype/job_card/job_card.js:340 msgid "Process Loss Quantity" msgstr "" @@ -38050,7 +38078,7 @@ msgstr "" msgid "Project wise Stock Tracking " msgstr "" -#: erpnext/controllers/trends.py:421 +#: erpnext/controllers/trends.py:429 msgid "Project-wise data is not available for Quotation" msgstr "" @@ -38328,7 +38356,7 @@ msgstr "" #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json #: erpnext/projects/doctype/project/project_dashboard.py:16 -#: erpnext/setup/doctype/company/company.py:463 erpnext/setup/install.py:377 +#: erpnext/setup/doctype/company/company.py:463 erpnext/setup/install.py:378 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/item_lead_time/item_lead_time.json #: erpnext/stock/doctype/item_reorder/item_reorder.json @@ -38483,8 +38511,8 @@ msgstr "" msgid "Purchase Invoice cannot be made against an existing asset {0}" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:449 -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:463 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:453 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:467 msgid "Purchase Invoice {0} is already submitted" msgstr "" @@ -38637,7 +38665,7 @@ msgstr "" msgid "Purchase Order already created for all Sales Order items" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:335 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:339 msgid "Purchase Order number required for Item {0}" msgstr "" @@ -38664,7 +38692,7 @@ msgstr "" msgid "Purchase Orders Items Overdue" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:286 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:285 msgid "Purchase Orders are not allowed for {0} due to a scorecard standing of {1}." msgstr "" @@ -38981,8 +39009,8 @@ msgstr "" #: erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json #: erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py:240 #: erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py:224 -#: erpnext/controllers/trends.py:268 erpnext/controllers/trends.py:280 -#: erpnext/controllers/trends.py:285 +#: erpnext/controllers/trends.py:276 erpnext/controllers/trends.py:288 +#: erpnext/controllers/trends.py:293 #: erpnext/crm/doctype/opportunity_item/opportunity_item.json #: erpnext/manufacturing/doctype/bom/bom.js:1086 #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -39098,7 +39126,8 @@ msgstr "" msgid "Qty In Stock" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:82 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:117 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:174 msgid "Qty Per Unit" msgstr "" @@ -39217,7 +39246,7 @@ msgstr "" msgid "Qty to Fetch" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:304 +#: erpnext/manufacturing/doctype/job_card/job_card.js:312 #: erpnext/manufacturing/doctype/job_card/job_card.py:871 msgid "Qty to Manufacture" msgstr "" @@ -39728,7 +39757,11 @@ msgid "Quantity is required" msgstr "" #: erpnext/stock/dashboard/item_dashboard.js:285 -msgid "Quantity must be greater than zero, and less or equal to {0}" +msgid "Quantity must be greater than zero" +msgstr "" + +#: erpnext/stock/dashboard/item_dashboard.js:290 +msgid "Quantity must be less than or equal to {0}" msgstr "" #: erpnext/manufacturing/doctype/work_order/work_order.js:1037 @@ -39746,16 +39779,12 @@ msgid "Quantity required for Item {0} in row {1}" msgstr "" #: erpnext/manufacturing/doctype/bom/bom.py:678 -#: erpnext/manufacturing/doctype/job_card/job_card.js:385 -#: erpnext/manufacturing/doctype/job_card/job_card.js:455 +#: erpnext/manufacturing/doctype/job_card/job_card.js:393 +#: erpnext/manufacturing/doctype/job_card/job_card.js:463 #: erpnext/manufacturing/doctype/workstation/workstation.js:303 msgid "Quantity should be greater than 0" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js:21 -msgid "Quantity to Make" -msgstr "" - #: erpnext/manufacturing/doctype/work_order/work_order.js:343 msgid "Quantity to Manufacture" msgstr "" @@ -39768,14 +39797,6 @@ msgstr "" msgid "Quantity to Manufacture must be greater than 0." msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js:24 -msgid "Quantity to Produce" -msgstr "" - -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:37 -msgid "Quantity to Produce should be greater than zero." -msgstr "" - #: erpnext/public/js/utils/barcode_scanner.js:257 msgid "Quantity to Scan" msgstr "" @@ -39950,7 +39971,7 @@ msgstr "" msgid "RFQ and Purchase Order Settings" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:129 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:131 msgid "RFQs are not allowed for {0} due to a scorecard standing of {1}" msgstr "" @@ -40227,7 +40248,7 @@ msgstr "" msgid "Rate at which this tax is applied" msgstr "" -#: erpnext/controllers/accounts_controller.py:4064 +#: erpnext/controllers/accounts_controller.py:4074 msgid "Rate of '{}' items cannot be changed" msgstr "" @@ -41281,7 +41302,7 @@ msgstr "" msgid "Release Date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:318 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:317 msgid "Release date must be in the future" msgstr "" @@ -41623,7 +41644,7 @@ msgstr "" msgid "Repost Item Valuation" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:344 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:346 msgid "Repost Item Valuation restarted for selected failed records." msgstr "" @@ -41663,10 +41684,6 @@ msgstr "" msgid "Repost started in the background" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:119 -msgid "Reposting Completed {0}%" -msgstr "" - #. Label of the reposting_data_file (Attach) field in DocType 'Repost Item #. Valuation' #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -41676,10 +41693,10 @@ msgstr "" #. Label of the reposting_info_section (Section Break) field in DocType 'Repost #. Item Valuation' #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Reposting Info" +msgid "Reposting Item and Warehouse" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:127 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:131 msgid "Reposting Progress" msgstr "" @@ -41689,12 +41706,30 @@ msgstr "" msgid "Reposting Reference" msgstr "" +#. Label of the vouchers_based_on_item_and_warehouse_section (Section Break) +#. field in DocType 'Repost Item Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "Reposting Vouchers" +msgstr "" + +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:149 +msgid "Reposting Vouchers Progress" +msgstr "" + #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:216 #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:327 msgid "Reposting entries created: {0}" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:103 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:123 +msgid "Reposting for Item-Wh Completed {0}%" +msgstr "" + +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:141 +msgid "Reposting for Vouchers Completed {0}%" +msgstr "" + +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:109 msgid "Reposting has been started in the background." msgstr "" @@ -41781,8 +41816,8 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/buying/doctype/buying_settings/buying_settings.json #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:324 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:426 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:326 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:428 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:88 #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js:70 @@ -41929,10 +41964,7 @@ msgstr "" #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:94 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:11 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:21 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:28 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:119 #: erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py:58 #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1057 #: erpnext/manufacturing/report/production_planning_report/production_planning_report.py:426 @@ -42129,7 +42161,7 @@ msgstr "" msgid "Reserved Quantity for Production" msgstr "" -#: erpnext/stock/stock_ledger.py:2287 +#: erpnext/stock/stock_ledger.py:2334 msgid "Reserved Serial No." msgstr "" @@ -42145,13 +42177,13 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.js:170 #: erpnext/stock/report/reserved_stock/reserved_stock.json #: erpnext/stock/report/stock_balance/stock_balance.py:572 -#: erpnext/stock/stock_ledger.py:2271 +#: erpnext/stock/stock_ledger.py:2318 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:205 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:333 msgid "Reserved Stock" msgstr "" -#: erpnext/stock/stock_ledger.py:2316 +#: erpnext/stock/stock_ledger.py:2363 msgid "Reserved Stock for Batch" msgstr "" @@ -42352,7 +42384,7 @@ msgstr "" msgid "Rest Of The World" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:84 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:90 msgid "Restart" msgstr "" @@ -42417,7 +42449,7 @@ msgstr "" msgid "Resume" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:239 +#: erpnext/manufacturing/doctype/job_card/job_card.js:247 msgid "Resume Job" msgstr "" @@ -43143,7 +43175,7 @@ msgstr "" msgid "Row #{0}: Asset {1} is already sold" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:334 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:333 msgid "Row #{0}: BOM is not specified for subcontracting item {0}" msgstr "" @@ -43179,27 +43211,27 @@ msgstr "" msgid "Row #{0}: Cannot create entry with different taxable AND withholding document links." msgstr "" -#: erpnext/controllers/accounts_controller.py:3767 +#: erpnext/controllers/accounts_controller.py:3777 msgid "Row #{0}: Cannot delete item {1} which has already been billed." msgstr "" -#: erpnext/controllers/accounts_controller.py:3741 +#: erpnext/controllers/accounts_controller.py:3751 msgid "Row #{0}: Cannot delete item {1} which has already been delivered" msgstr "" -#: erpnext/controllers/accounts_controller.py:3760 +#: erpnext/controllers/accounts_controller.py:3770 msgid "Row #{0}: Cannot delete item {1} which has already been received" msgstr "" -#: erpnext/controllers/accounts_controller.py:3747 +#: erpnext/controllers/accounts_controller.py:3757 msgid "Row #{0}: Cannot delete item {1} which has work order assigned to it." msgstr "" -#: erpnext/controllers/accounts_controller.py:3753 +#: erpnext/controllers/accounts_controller.py:3763 msgid "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4074 +#: erpnext/controllers/accounts_controller.py:4084 msgid "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." msgstr "" @@ -43282,7 +43314,7 @@ msgstr "" msgid "Row #{0}: Dates overlapping with other row in group {1}" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:358 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:357 msgid "Row #{0}: Default BOM not found for FG Item {1}" msgstr "" @@ -43306,17 +43338,17 @@ msgstr "" msgid "Row #{0}: Expense account {1} is not valid for Purchase Invoice {2}. Only expense accounts from non-stock items are allowed." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:363 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:362 #: erpnext/selling/doctype/sales_order/sales_order.py:303 msgid "Row #{0}: Finished Good Item Qty can not be zero" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:345 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:344 #: erpnext/selling/doctype/sales_order/sales_order.py:283 msgid "Row #{0}: Finished Good Item is not specified for service item {1}" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:352 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:351 #: erpnext/selling/doctype/sales_order/sales_order.py:290 msgid "Row #{0}: Finished Good Item {1} must be a sub-contracted item" msgstr "" @@ -43444,11 +43476,11 @@ msgstr "" msgid "Row #{0}: Overconsumption of Customer Provided Item {1} against Work Order {2} is not allowed in the Subcontracting Inward process." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1051 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1056 msgid "Row #{0}: Please select Item Code in Assembly Items" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1054 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1059 msgid "Row #{0}: Please select the BOM No in Assembly Items" msgstr "" @@ -43456,7 +43488,7 @@ msgstr "" msgid "Row #{0}: Please select the Finished Good Item against which this Customer Provided Item will be used." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1048 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1053 msgid "Row #{0}: Please select the Sub Assembly Warehouse" msgstr "" @@ -43596,7 +43628,7 @@ msgstr "" msgid "Row #{0}: Set Supplier for item {1}" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1058 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1063 msgid "Row #{0}: Since 'Track Semi Finished Goods' is enabled, the BOM {1} cannot be used for Sub Assembly Items" msgstr "" @@ -43717,7 +43749,7 @@ msgstr "" msgid "Row #{0}: {1} is not a valid reading field. Please refer to the field description." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:115 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:126 msgid "Row #{0}: {1} is required to create the Opening {2} Invoices" msgstr "" @@ -43725,7 +43757,7 @@ msgstr "" msgid "Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account." msgstr "" -#: erpnext/controllers/accounts_controller.py:3881 +#: erpnext/controllers/accounts_controller.py:3891 msgid "Row #{0}:Quantity for Item {1} cannot be zero." msgstr "" @@ -43761,7 +43793,7 @@ msgstr "" msgid "Row #{idx}: {from_warehouse_field} and {to_warehouse_field} cannot be same." msgstr "" -#: erpnext/controllers/buying_controller.py:1176 +#: erpnext/controllers/buying_controller.py:1177 msgid "Row #{idx}: {schedule_date} cannot be before {transaction_date}." msgstr "" @@ -43769,6 +43801,10 @@ msgstr "" msgid "Row #{}: Currency of {} - {} doesn't matches company currency." msgstr "" +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:108 +msgid "Row #{}: Either Party ID or Party Name is required" +msgstr "" + #: erpnext/assets/doctype/asset/asset.py:421 msgid "Row #{}: Finance Book should not be empty since you're using multiple." msgstr "" @@ -43785,6 +43821,10 @@ msgstr "" msgid "Row #{}: POS Invoice {} is not submitted yet" msgstr "" +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:118 +msgid "Row #{}: Party ID is required" +msgstr "" + #: erpnext/assets/doctype/asset_maintenance/asset_maintenance.py:41 msgid "Row #{}: Please assign task to a member." msgstr "" @@ -43814,7 +43854,7 @@ msgstr "" msgid "Row #{}: {}" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:110 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:121 msgid "Row #{}: {} {} does not exist." msgstr "" @@ -43822,7 +43862,7 @@ msgstr "" msgid "Row #{}: {} {} doesn't belong to Company {}. Please select valid {}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:444 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:443 msgid "Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}" msgstr "" @@ -43888,7 +43928,7 @@ msgstr "" msgid "Row {0}: Conversion Factor is mandatory" msgstr "" -#: erpnext/controllers/accounts_controller.py:3227 +#: erpnext/controllers/accounts_controller.py:3237 msgid "Row {0}: Cost Center {1} does not belong to Company {2}" msgstr "" @@ -43916,7 +43956,7 @@ msgstr "" msgid "Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}." msgstr "" -#: erpnext/controllers/accounts_controller.py:2720 +#: erpnext/controllers/accounts_controller.py:2730 msgid "Row {0}: Due Date in the Payment Terms table cannot be before Posting Date" msgstr "" @@ -43941,19 +43981,19 @@ msgstr "" msgid "Row {0}: Expense Account {1} is linked to company {2}. Please select an account belonging to company {3}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:534 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:533 msgid "Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:491 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:490 msgid "Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:516 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:515 msgid "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:152 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:154 msgid "Row {0}: For Supplier {1}, Email Address is Required to send an email" msgstr "" @@ -44006,7 +44046,7 @@ msgstr "" msgid "Row {0}: Item {1}'s quantity cannot be higher than the available quantity." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1209 +#: erpnext/manufacturing/doctype/bom/bom.py:1211 msgid "Row {0}: Operation time should be greater than 0 for operation {1}" msgstr "" @@ -44122,7 +44162,7 @@ msgstr "" msgid "Row {0}: The item {1}, quantity must be positive number" msgstr "" -#: erpnext/controllers/accounts_controller.py:3204 +#: erpnext/controllers/accounts_controller.py:3214 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" msgstr "" @@ -44146,7 +44186,7 @@ msgstr "" msgid "Row {0}: Warehouse {1} is linked to company {2}. Please select a warehouse belonging to company {3}." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1203 +#: erpnext/manufacturing/doctype/bom/bom.py:1205 #: erpnext/manufacturing/doctype/work_order/work_order.py:415 msgid "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" msgstr "" @@ -44179,7 +44219,7 @@ msgstr "" msgid "Row {0}: {2} Item {1} does not exist in {2} {3}" msgstr "" -#: erpnext/utilities/transaction_base.py:562 +#: erpnext/utilities/transaction_base.py:563 msgid "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." msgstr "" @@ -44209,7 +44249,7 @@ msgstr "" msgid "Rows with Same Account heads will be merged on Ledger" msgstr "" -#: erpnext/controllers/accounts_controller.py:2731 +#: erpnext/controllers/accounts_controller.py:2741 msgid "Rows with duplicate due dates in other rows were found: {0}" msgstr "" @@ -44391,7 +44431,7 @@ msgstr "" #: erpnext/setup/doctype/company/company.py:649 #: erpnext/setup/doctype/company/company_dashboard.py:9 #: erpnext/setup/doctype/sales_person/sales_person_dashboard.py:12 -#: erpnext/setup/install.py:372 +#: erpnext/setup/install.py:373 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:297 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/pick_list/pick_list_dashboard.py:16 @@ -45215,7 +45255,7 @@ msgstr "" msgid "Same item cannot be entered multiple times." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:121 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:123 msgid "Same supplier has been entered multiple times" msgstr "" @@ -45505,7 +45545,7 @@ msgstr "" msgid "Scrap Warehouse" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:384 +#: erpnext/assets/doctype/asset/depreciation.py:387 msgid "Scrap date cannot be before purchase date" msgstr "" @@ -45653,11 +45693,11 @@ msgstr "" msgid "Select Company" msgstr "" -#: erpnext/public/js/print.js:113 +#: erpnext/public/js/print.js:115 msgid "Select Company Address" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:533 +#: erpnext/manufacturing/doctype/job_card/job_card.js:541 msgid "Select Corrective Operation" msgstr "" @@ -45667,11 +45707,11 @@ msgstr "" msgid "Select Customers By" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:120 +#: erpnext/setup/doctype/employee/employee.js:160 msgid "Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff." msgstr "" -#: erpnext/setup/doctype/employee/employee.js:127 +#: erpnext/setup/doctype/employee/employee.js:167 msgid "Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases." msgstr "" @@ -45693,7 +45733,7 @@ msgstr "" msgid "Select Dispatch Address " msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:221 +#: erpnext/manufacturing/doctype/job_card/job_card.js:229 msgid "Select Employees" msgstr "" @@ -45752,7 +45792,7 @@ msgstr "" msgid "Select Payment Schedule" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:410 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:411 msgid "Select Possible Supplier" msgstr "" @@ -45815,7 +45855,7 @@ msgstr "" msgid "Select a Company" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:115 +#: erpnext/setup/doctype/employee/employee.js:155 msgid "Select a Company this Employee belongs to." msgstr "" @@ -45869,7 +45909,7 @@ msgstr "" msgid "Select company name first." msgstr "" -#: erpnext/controllers/accounts_controller.py:2979 +#: erpnext/controllers/accounts_controller.py:2989 msgid "Select finance book for the item {0} at row {1}" msgstr "" @@ -46449,7 +46489,7 @@ msgstr "" msgid "Serial Nos are created successfully" msgstr "" -#: erpnext/stock/stock_ledger.py:2277 +#: erpnext/stock/stock_ledger.py:2324 msgid "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding." msgstr "" @@ -46744,6 +46784,7 @@ msgstr "" #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:405 msgid "Service End Date" msgstr "" @@ -46887,6 +46928,7 @@ msgstr "" #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:397 msgid "Service Start Date" msgstr "" @@ -46945,8 +46987,8 @@ msgstr "" msgid "Set Delivery Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:404 -#: erpnext/manufacturing/doctype/job_card/job_card.js:473 +#: erpnext/manufacturing/doctype/job_card/job_card.js:412 +#: erpnext/manufacturing/doctype/job_card/job_card.js:481 msgid "Set Finished Good Quantity" msgstr "" @@ -47246,7 +47288,7 @@ msgstr "" msgid "Setting up company" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1182 +#: erpnext/manufacturing/doctype/bom/bom.py:1184 #: erpnext/manufacturing/doctype/work_order/work_order.py:1475 msgid "Setting {0} is required" msgstr "" @@ -47471,10 +47513,13 @@ msgstr "" #. Label of the shipping_address_display (Text Editor) field in DocType #. 'Purchase Order' #. Label of the shipping_address_display (Text Editor) field in DocType +#. 'Request for Quotation' +#. Label of the shipping_address_display (Text Editor) field in DocType #. 'Supplier Quotation' #. Label of the shipping_address_display (Text Editor) field in DocType #. 'Subcontracting Order' #: erpnext/buying/doctype/purchase_order/purchase_order.json +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json msgid "Shipping Address Details" @@ -47829,9 +47874,8 @@ msgstr "" msgid "Show Warehouse-wise Stock" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js:28 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js:19 -msgid "Show exploded view" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:26 +msgid "Show availability of exploded items" msgstr "" #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.js:88 @@ -48061,7 +48105,7 @@ msgstr "" msgid "Solvency Ratios" msgstr "" -#: erpnext/controllers/accounts_controller.py:4332 +#: erpnext/controllers/accounts_controller.py:4344 msgid "Some required Company details are missing. You don't have permission to update them. Please contact your System Manager." msgstr "" @@ -48191,7 +48235,7 @@ msgstr "" msgid "Source and target warehouse cannot be same for row {0}" msgstr "" -#: erpnext/stock/dashboard/item_dashboard.js:290 +#: erpnext/stock/dashboard/item_dashboard.js:295 msgid "Source and target warehouse must be different" msgstr "" @@ -48344,7 +48388,7 @@ msgstr "" #: erpnext/setup/setup_wizard/operations/defaults_setup.py:70 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:485 -#: erpnext/tests/utils.py:297 +#: erpnext/tests/utils.py:316 msgid "Standard Buying" msgstr "" @@ -48358,8 +48402,8 @@ msgstr "" #: erpnext/setup/setup_wizard/operations/defaults_setup.py:70 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:493 -#: erpnext/stock/doctype/item/item.py:267 erpnext/tests/utils.py:305 -#: erpnext/tests/utils.py:2494 +#: erpnext/stock/doctype/item/item.py:267 erpnext/tests/utils.py:324 +#: erpnext/tests/utils.py:2514 msgid "Standard Selling" msgstr "" @@ -48415,7 +48459,7 @@ msgstr "" msgid "Start Date should be lower than End Date" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:215 +#: erpnext/manufacturing/doctype/job_card/job_card.js:223 #: erpnext/manufacturing/doctype/workstation/workstation.js:124 msgid "Start Job" msgstr "" @@ -48424,7 +48468,7 @@ msgstr "" msgid "Start Merge" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:99 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:105 msgid "Start Reposting" msgstr "" @@ -48991,7 +49035,7 @@ msgid "Stock Reservation Entries Cancelled" msgstr "" #: erpnext/controllers/subcontracting_inward_controller.py:1003 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2238 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2243 #: erpnext/manufacturing/doctype/work_order/work_order.py:2124 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1777 msgid "Stock Reservation Entries Created" @@ -49365,7 +49409,7 @@ msgstr "" #: erpnext/setup/doctype/company/company.py:384 #: erpnext/setup/setup_wizard/operations/defaults_setup.py:33 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:537 -#: erpnext/stock/doctype/item/item.py:304 erpnext/tests/utils.py:270 +#: erpnext/stock/doctype/item/item.py:304 erpnext/tests/utils.py:289 msgid "Stores" msgstr "" @@ -49423,7 +49467,7 @@ msgstr "" #. Label of the operation (Link) field in DocType 'Job Card Time Log' #. Name of a DocType -#: erpnext/manufacturing/doctype/job_card/job_card.js:349 +#: erpnext/manufacturing/doctype/job_card/job_card.js:357 #: erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json #: erpnext/manufacturing/doctype/sub_operation/sub_operation.json msgid "Sub Operation" @@ -49447,7 +49491,7 @@ msgstr "" msgid "Sub Total" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:621 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:626 msgid "Sub assembly item references are missing. Please fetch the sub assemblies and raw materials again." msgstr "" @@ -49661,7 +49705,7 @@ msgstr "" #. Receipt Supplied Item' #. Label of a Workspace Sidebar Item #: erpnext/buying/doctype/purchase_order/purchase_order.js:399 -#: erpnext/controllers/subcontracting_controller.py:1167 +#: erpnext/controllers/subcontracting_controller.py:1173 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/stock/doctype/stock_entry/stock_entry.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json @@ -49813,7 +49857,7 @@ msgstr "" msgid "Submit this Work Order for further processing." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:306 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:308 msgid "Submit your Quotation" msgstr "" @@ -50122,8 +50166,8 @@ msgstr "" #: erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js:37 #: erpnext/assets/doctype/asset/asset.json #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:184 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:269 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:185 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:270 #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json #: erpnext/buying/doctype/supplier/supplier.json @@ -50256,7 +50300,7 @@ msgstr "" #: erpnext/accounts/report/purchase_register/purchase_register.py:186 #: erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js:55 #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:502 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:503 #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py:105 #: erpnext/buying/workspace/buying/buying.json @@ -50455,7 +50499,7 @@ msgstr "" #. Name of a report #. Label of a Link in the Buying Workspace #. Label of a Workspace Sidebar Item -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:154 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:155 #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.json #: erpnext/buying/workspace/buying/buying.json #: erpnext/workspace_sidebar/buying.json @@ -50470,7 +50514,7 @@ msgstr "" msgid "Supplier Quotation Item" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:495 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:497 msgid "Supplier Quotation {0} Created" msgstr "" @@ -50559,7 +50603,7 @@ msgstr "" #. Label of the supplier_warehouse (Link) field in DocType 'Purchase Receipt' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/manufacturing/doctype/job_card/job_card.js:89 +#: erpnext/manufacturing/doctype/job_card/job_card.js:97 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Supplier Warehouse" msgstr "" @@ -51920,7 +51964,7 @@ msgstr "" msgid "The 'From Package No.' field must neither be empty nor it's value less than 1." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:411 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:413 msgid "The Access to Request for Quotation From Portal is Disabled. To Allow Access, Enable it in Portal Settings." msgstr "" @@ -51961,7 +52005,7 @@ msgstr "" msgid "The Loyalty Program isn't valid for the selected company" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1106 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1107 msgid "The Payment Request {0} is already paid, cannot process payment twice" msgstr "" @@ -52003,7 +52047,7 @@ msgstr "" msgid "The account head under Liability or Equity, in which Profit/Loss will be booked" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1001 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1002 msgid "The allocated amount is greater than the outstanding amount of Payment Request {0}" msgstr "" @@ -52076,7 +52120,7 @@ msgstr "" msgid "The following Purchase Invoices are not submitted:" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:344 +#: erpnext/assets/doctype/asset/depreciation.py:347 msgid "The following assets have failed to automatically post depreciation entries: {0}" msgstr "" @@ -52092,7 +52136,7 @@ msgstr "" msgid "The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:175 +#: erpnext/setup/doctype/employee/employee.py:289 msgid "The following employees are currently still reporting to {0}:" msgstr "" @@ -52123,7 +52167,7 @@ msgstr "" msgid "The holiday on {0} is not between From Date and To Date" msgstr "" -#: erpnext/controllers/buying_controller.py:1243 +#: erpnext/controllers/buying_controller.py:1244 msgid "The item {item} is not marked as {type_of} item. You can enable it as {type_of} item from its Item master." msgstr "" @@ -52131,7 +52175,7 @@ msgstr "" msgid "The items {0} and {1} are present in the following {2} :" msgstr "" -#: erpnext/controllers/buying_controller.py:1236 +#: erpnext/controllers/buying_controller.py:1237 msgid "The items {items} are not marked as {type_of} item. You can enable them as {type_of} item from their Item masters." msgstr "" @@ -52266,7 +52310,7 @@ msgstr "" msgid "The shares don't exist with the {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:803 +#: erpnext/stock/stock_ledger.py:806 msgid "The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the documentation." msgstr "" @@ -52304,7 +52348,7 @@ msgstr "" msgid "The uploaded file does not appear to be in valid MT940 format." msgstr "" -#: erpnext/edi/doctype/code_list/code_list_import.py:48 +#: erpnext/edi/doctype/code_list/code_list_import.py:54 msgid "The uploaded file does not match the selected Code List." msgstr "" @@ -52542,7 +52586,7 @@ msgstr "" msgid "This is a location where scraped materials are stored." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:318 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:319 msgid "This is a preview of the email to be sent. A PDF of the document will automatically be attached with the email." msgstr "" @@ -52590,7 +52634,7 @@ msgstr "" msgid "This is considered dangerous from accounting point of view." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:540 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:539 msgid "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice" msgstr "" @@ -52640,7 +52684,7 @@ msgstr "" msgid "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:458 +#: erpnext/assets/doctype/asset/depreciation.py:461 msgid "This schedule was created when Asset {0} was restored." msgstr "" @@ -52648,7 +52692,7 @@ msgstr "" msgid "This schedule was created when Asset {0} was returned through Sales Invoice {1}." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:417 +#: erpnext/assets/doctype/asset/depreciation.py:420 msgid "This schedule was created when Asset {0} was scrapped." msgstr "" @@ -53198,7 +53242,7 @@ msgid "To include sub-assembly costs and scrap items in Finished Goods on a work msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2247 -#: erpnext/controllers/accounts_controller.py:3237 +#: erpnext/controllers/accounts_controller.py:3247 msgid "To include tax in row {0} in Item rate, taxes in rows {1} must also be included" msgstr "" @@ -53648,6 +53692,11 @@ msgstr "" msgid "Total Landed Cost (Company Currency)" msgstr "" +#. Label of the total_vouchers (Int) field in DocType 'Repost Item Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "Total Ledgers" +msgstr "" + #: erpnext/accounts/report/balance_sheet/balance_sheet.py:219 msgid "Total Liability" msgstr "" @@ -53753,7 +53802,7 @@ msgstr "" msgid "Total Paid Amount" msgstr "" -#: erpnext/controllers/accounts_controller.py:2785 +#: erpnext/controllers/accounts_controller.py:2795 msgid "Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total" msgstr "" @@ -53842,12 +53891,6 @@ msgstr "" msgid "Total Repair Cost" msgstr "" -#. Label of the total_reposting_count (Int) field in DocType 'Repost Item -#. Valuation' -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Total Reposting Count" -msgstr "" - #: erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py:44 msgid "Total Revenue" msgstr "" @@ -54211,7 +54254,7 @@ msgstr "" msgid "Transaction Date" msgstr "" -#: erpnext/setup/doctype/company/company.py:1104 +#: erpnext/setup/doctype/company/company.py:1106 msgid "Transaction Deletion Document {0} has been triggered for company {1}" msgstr "" @@ -54821,7 +54864,7 @@ msgstr "" msgid "UOM Conversion Factor" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1463 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1468 msgid "UOM Conversion factor ({0} -> {1}) not found for item: {2}" msgstr "" @@ -54997,7 +55040,7 @@ msgstr "" msgid "Unit" msgstr "" -#: erpnext/controllers/accounts_controller.py:4064 +#: erpnext/controllers/accounts_controller.py:4074 msgid "Unit Price" msgstr "" @@ -55462,7 +55505,7 @@ msgstr "" msgid "Updating Work Order status" msgstr "" -#: erpnext/public/js/print.js:151 +#: erpnext/public/js/print.js:153 msgid "Updating details." msgstr "" @@ -55701,7 +55744,7 @@ msgstr "" msgid "User has not applied rule on the invoice {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:187 +#: erpnext/setup/doctype/employee/employee.py:301 msgid "User {0} does not exist" msgstr "" @@ -55709,15 +55752,15 @@ msgstr "" msgid "User {0} doesn't have any default POS Profile. Check Default at Row {1} for this User." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:205 +#: erpnext/setup/doctype/employee/employee.py:319 msgid "User {0} is already assigned to Employee {1}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:243 +#: erpnext/setup/doctype/employee/employee.py:357 msgid "User {0}: Removed Employee Self Service role as there is no mapped employee." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:238 +#: erpnext/setup/doctype/employee/employee.py:352 msgid "User {0}: Removed Employee role as there is no mapped employee." msgstr "" @@ -56010,11 +56053,11 @@ msgstr "" msgid "Valuation Rate (In / Out)" msgstr "" -#: erpnext/stock/stock_ledger.py:2022 +#: erpnext/stock/stock_ledger.py:2069 msgid "Valuation Rate Missing" msgstr "" -#: erpnext/stock/stock_ledger.py:2000 +#: erpnext/stock/stock_ledger.py:2047 msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." msgstr "" @@ -56046,7 +56089,7 @@ msgid "Valuation rate for the item as per Sales Invoice (Only for Internal Trans msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2271 -#: erpnext/controllers/accounts_controller.py:3261 +#: erpnext/controllers/accounts_controller.py:3271 msgid "Valuation type charges can not be marked as Inclusive" msgstr "" @@ -56820,6 +56863,10 @@ msgstr "" msgid "Warehouse is mandatory" msgstr "" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:286 +msgid "Warehouse is required to get producible FG Items" +msgstr "" + #: erpnext/stock/doctype/warehouse/warehouse.py:259 msgid "Warehouse not found against the account {0}" msgstr "" @@ -56959,7 +57006,7 @@ msgstr "" msgid "Warning - Row {0}: Billing Hours are more than Actual Hours" msgstr "" -#: erpnext/stock/stock_ledger.py:813 +#: erpnext/stock/stock_ledger.py:816 msgid "Warning on Negative Stock" msgstr "" @@ -57273,8 +57320,8 @@ msgstr "" msgid "Widowed" msgstr "" -#. Label of the width (Int) field in DocType 'Shipment Parcel' -#. Label of the width (Int) field in DocType 'Shipment Parcel Template' +#. Label of the width (Float) field in DocType 'Shipment Parcel' +#. Label of the width (Float) field in DocType 'Shipment Parcel Template' #: erpnext/stock/doctype/shipment_parcel/shipment_parcel.json #: erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json msgid "Width (cm)" @@ -57819,7 +57866,7 @@ msgstr "" msgid "You are importing data for the code list:" msgstr "" -#: erpnext/controllers/accounts_controller.py:3861 +#: erpnext/controllers/accounts_controller.py:3871 msgid "You are not allowed to update as per the conditions set in {} Workflow." msgstr "" @@ -57960,7 +58007,7 @@ msgstr "" msgid "You do not have permission to edit this document" msgstr "" -#: erpnext/controllers/accounts_controller.py:3837 +#: erpnext/controllers/accounts_controller.py:3847 msgid "You do not have permissions to {} items in a {}." msgstr "" @@ -57972,7 +58019,7 @@ msgstr "" msgid "You don't have enough points to redeem." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:273 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:286 msgid "You had {} errors while creating opening invoices. Check {} for more details" msgstr "" @@ -58012,7 +58059,7 @@ msgstr "" msgid "You need to cancel POS Closing Entry {} to be able to cancel this document." msgstr "" -#: erpnext/controllers/accounts_controller.py:3212 +#: erpnext/controllers/accounts_controller.py:3222 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." msgstr "" @@ -58080,7 +58127,7 @@ msgstr "" msgid "`Allow Negative rates for Items`" msgstr "" -#: erpnext/stock/stock_ledger.py:2014 +#: erpnext/stock/stock_ledger.py:2061 msgid "after" msgstr "" @@ -58120,7 +58167,7 @@ msgstr "" msgid "cannot be greater than 100" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:334 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:333 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1102 msgid "dated {0}" msgstr "" @@ -58270,7 +58317,7 @@ msgstr "" msgid "per hour" msgstr "" -#: erpnext/stock/stock_ledger.py:2015 +#: erpnext/stock/stock_ledger.py:2062 msgid "performing either one below:" msgstr "" @@ -58404,7 +58451,7 @@ msgstr "" msgid "{0} {1} has submitted Assets. Remove Item {2} from table to continue." msgstr "" -#: erpnext/controllers/accounts_controller.py:2367 +#: erpnext/controllers/accounts_controller.py:2377 msgid "{0} Account not found against Customer {1}." msgstr "" @@ -58432,7 +58479,7 @@ msgstr "" msgid "{0} Number {1} is already used in {2} {3}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1635 +#: erpnext/manufacturing/doctype/bom/bom.py:1638 msgid "{0} Operating Cost for operation {1}" msgstr "" @@ -58460,7 +58507,7 @@ msgstr "" msgid "{0} account is not of type {1}" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:515 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:519 msgid "{0} account not found while submitting purchase receipt" msgstr "" @@ -58493,6 +58540,10 @@ msgstr "" msgid "{0} asset cannot be transferred" msgstr "" +#: erpnext/controllers/trends.py:60 +msgid "{0} can be either {1} or {2}." +msgstr "" + #: erpnext/accounts/doctype/pricing_rule/pricing_rule.py:279 msgid "{0} can not be negative" msgstr "" @@ -58509,8 +58560,8 @@ msgstr "" msgid "{0} cannot be zero" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:918 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1034 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:923 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1039 #: erpnext/stock/doctype/pick_list/pick_list.py:1297 #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:322 msgid "{0} created" @@ -58524,11 +58575,11 @@ msgstr "" msgid "{0} currency must be same as company's default currency. Please select another account." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:295 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:294 msgid "{0} currently has a {1} Supplier Scorecard standing, and Purchase Orders to this supplier should be issued with caution." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:137 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:139 msgid "{0} currently has a {1} Supplier Scorecard standing, and RFQs to this supplier should be issued with caution." msgstr "" @@ -58570,7 +58621,7 @@ msgstr "" msgid "{0} hours" msgstr "" -#: erpnext/controllers/accounts_controller.py:2725 +#: erpnext/controllers/accounts_controller.py:2735 msgid "{0} in row {1}" msgstr "" @@ -58613,7 +58664,7 @@ msgstr "" msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}" msgstr "" -#: erpnext/controllers/accounts_controller.py:3169 +#: erpnext/controllers/accounts_controller.py:3179 msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}." msgstr "" @@ -58729,16 +58780,16 @@ msgstr "" msgid "{0} units of {1} are required in {2} with the inventory dimension: {3} on {4} {5} for {6} to complete the transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:1686 erpnext/stock/stock_ledger.py:2163 -#: erpnext/stock/stock_ledger.py:2177 +#: erpnext/stock/stock_ledger.py:1714 erpnext/stock/stock_ledger.py:2210 +#: erpnext/stock/stock_ledger.py:2224 msgid "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:2264 erpnext/stock/stock_ledger.py:2309 +#: erpnext/stock/stock_ledger.py:2311 erpnext/stock/stock_ledger.py:2356 msgid "{0} units of {1} needed in {2} on {3} {4} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:1680 +#: erpnext/stock/stock_ledger.py:1708 msgid "{0} units of {1} needed in {2} to complete this transaction." msgstr "" @@ -58804,7 +58855,7 @@ msgstr "" msgid "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:435 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:434 #: erpnext/selling/doctype/sales_order/sales_order.py:598 #: erpnext/stock/doctype/material_request/material_request.py:255 msgid "{0} {1} has been modified. Please refresh." @@ -58827,7 +58878,7 @@ msgid "{0} {1} is associated with {2}, but Party Account is {3}" msgstr "" #: erpnext/controllers/selling_controller.py:495 -#: erpnext/controllers/subcontracting_controller.py:1167 +#: erpnext/controllers/subcontracting_controller.py:1173 msgid "{0} {1} is cancelled or closed" msgstr "" From f6fa9726f9a5d068e5ce0afdd46cd9519445d45f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:43:26 +0000 Subject: [PATCH 78/90] fix(manufacturing): update the qty precision (backport #53874) (#53885) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> fix(manufacturing): update the qty precision (#53874) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 1dfc064b2a4..36364f6740b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1783,8 +1783,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d ) sales_order = data.get("sales_order") + qty_precision = frappe.get_precision("Material Request Plan Item", "quantity") for key, details in item_details.items(): + details.qty = flt(details.qty, qty_precision) so_item_details.setdefault(sales_order, frappe._dict()) if key in so_item_details.get(sales_order, {}): so_item_details[sales_order][key]["qty"] = so_item_details[sales_order][key].get( From b2cba0286e8a1672436b999e81516e0539ceb293 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:05:36 +0000 Subject: [PATCH 79/90] refactor: setup wizard stages and demo data creation (backport #53866) (#53868) Co-authored-by: diptanilsaha --- erpnext/hooks.py | 1 - erpnext/setup/demo.py | 56 ++++++++------ erpnext/setup/doctype/company/test_company.py | 4 +- erpnext/setup/setup_wizard/setup_wizard.py | 73 +++++++------------ 4 files changed, 64 insertions(+), 70 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c997443b41d..cc4a08d8b67 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -62,7 +62,6 @@ welcome_email = "erpnext.setup.utils.welcome_email" # setup wizard setup_wizard_requires = "assets/erpnext/js/setup_wizard.js" setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages" -setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo" after_install = "erpnext.setup.install.after_install" diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py index ec48a3e1447..c460b1520c4 100644 --- a/erpnext/setup/demo.py +++ b/erpnext/setup/demo.py @@ -7,7 +7,7 @@ from random import randint import frappe from frappe import _ -from frappe.utils import add_days, getdate +from frappe.utils import add_days, get_url_to_form, getdate from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.utils import get_fiscal_year @@ -16,21 +16,44 @@ from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account -def setup_demo_data(): +def setup_demo_data(company_name): from frappe.utils.telemetry import capture capture("demo_data_creation_started", "erpnext") try: - company = create_demo_company() + frappe.db.savepoint("demo_data") + company = create_demo_company(company_name) process_masters() make_transactions(company) - frappe.cache.delete_keys("bootinfo") - frappe.publish_realtime("demo_data_complete") + capture("demo_data_creation_completed", "erpnext") + frappe.clear_messages() except Exception: - frappe.log_error("Failed to create demo data") + frappe.db.rollback(save_point="demo_data") + error_log = frappe.log_error("Failed to create demo data") + log_demo_data_failed_notification(error_log) capture("demo_data_creation_failed", "erpnext", properties={"exception": frappe.get_traceback()}) - raise - capture("demo_data_creation_completed", "erpnext") + + +def log_demo_data_failed_notification(error_log): + from frappe.core.doctype.role.role import get_users + from frappe.desk.doctype.notification_log.notification_log import make_notification_logs + + frappe.msgprint( + _("Demo data creation failed. Check notifications for more info."), + alert=True, + indicator="red", + realtime=True, + ) + + users = get_users("System Manager") + + notif_log_doc = { + "subject": _("Demo Data creation failed."), + "type": "Alert", + "link": get_url_to_form("Error Log", error_log.name), + } + + make_notification_logs(notif_log_doc, users) @frappe.whitelist() @@ -56,21 +79,8 @@ def clear_demo_data(): ) -def create_demo_company(): - if frappe.flags.in_test: - hash = frappe.generate_hash(length=3) - company_doc = frappe._dict( - { - "company_name": "Test Company" + " " + hash, - "abbr": "TC" + hash, - "default_currency": "INR", - "country": "India", - "chart_of_accounts": "Standard", - } - ) - else: - company = frappe.db.get_all("Company")[0].name - company_doc = frappe.get_doc("Company", company).as_dict() +def create_demo_company(company): + company_doc = frappe.get_doc("Company", company).as_dict() # Make a dummy company new_company = frappe.new_doc("Company") diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index c3fb5dd6ff2..36606e90755 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -199,7 +199,9 @@ class TestCompany(ERPNextTestSuite): def test_demo_data(self): from erpnext.setup.demo import clear_demo_data, setup_demo_data - setup_demo_data() + self.load_test_records("Company") + + setup_demo_data(self.globalTestRecords["Company"][0]["company_name"]) company_name = frappe.db.get_value("Company", {"name": ("like", "%(Demo)")}) self.assertTrue(company_name) diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index 9a49af2b10e..20330d89631 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -10,39 +10,34 @@ from erpnext.setup.setup_wizard.operations import install_fixtures as fixtures def get_setup_stages(args=None): - if frappe.db.sql("select name from tabCompany"): - stages = [ + stages = [ + { + "status": _("Installing presets"), + "fail_msg": _("Failed to install presets"), + "tasks": [{"fn": stage_fixtures, "args": args, "fail_msg": _("Failed to install presets")}], + }, + { + "status": _("Setting up company"), + "fail_msg": _("Failed to setup company"), + "tasks": [{"fn": setup_company, "args": args, "fail_msg": _("Failed to setup company")}], + }, + { + "status": _("Setting defaults"), + "fail_msg": _("Failed to set defaults"), + "tasks": [ + {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, + ], + }, + ] + + if args.get("setup_demo"): + stages.append( { - "status": _("Wrapping up"), - "fail_msg": _("Failed to login"), - "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], + "status": _("Creating demo data"), + "fail_msg": _("Failed to create demo data"), + "tasks": [{"fn": setup_demo, "args": args, "fail_msg": _("Failed to create demo data")}], } - ] - else: - stages = [ - { - "status": _("Installing presets"), - "fail_msg": _("Failed to install presets"), - "tasks": [{"fn": stage_fixtures, "args": args, "fail_msg": _("Failed to install presets")}], - }, - { - "status": _("Setting up company"), - "fail_msg": _("Failed to setup company"), - "tasks": [{"fn": setup_company, "args": args, "fail_msg": _("Failed to setup company")}], - }, - { - "status": _("Setting defaults"), - "fail_msg": "Failed to set defaults", - "tasks": [ - {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, - ], - }, - { - "status": _("Wrapping up"), - "fail_msg": _("Failed to login"), - "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], - }, - ] + ) return stages @@ -59,19 +54,8 @@ def setup_defaults(args): fixtures.install_defaults(frappe._dict(args)) -def fin(args): - frappe.local.message_log = [] - login_as_first_user(args) - - -def setup_demo(args): - if args.get("setup_demo"): - frappe.enqueue(setup_demo_data, enqueue_after_commit=True, at_front=True) - - -def login_as_first_user(args): - if args.get("email") and hasattr(frappe.local, "login_manager"): - frappe.local.login_manager.login_as(args.get("email")) +def setup_demo(args): # nosemgrep + setup_demo_data(args.get("company_name")) # Only for programmatical use @@ -79,4 +63,3 @@ def setup_complete(args=None): stage_fixtures(args) setup_company(args) setup_defaults(args) - fin(args) From 319ba31b7708fa76365f8dc9616db84330c91048 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 06:59:29 +0000 Subject: [PATCH 80/90] fix(stock): ignore qty validation for pick list (backport #53871) (#53892) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> fix(stock): ignore qty validation for pick list (#53871) --- .../stock/doctype/material_request/test_material_request.py | 4 +++- erpnext/stock/doctype/pick_list/pick_list.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index e72637901b5..c25a6ecd62d 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -1083,7 +1083,9 @@ class TestMaterialRequest(ERPNextTestSuite): pl.locations[0].qty = 2 pl.locations[0].stock_qty = 2 - self.assertRaises(frappe.ValidationError, pl.submit) + + # System should allow picking qty for excess transfer + pl.submit() def test_mr_status_with_partial_and_excess_end_transit(self): material_request = make_material_request( diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index ef80966c2a6..9d01bb28fc3 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -86,6 +86,7 @@ class PickList(TransactionBase): "join_field": "material_request_item", "target_ref_field": "stock_qty", "source_field": "stock_qty", + "validate_qty": False, } ] From 9d46d8151a92e3d098c546504a62fdc8468d3d93 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 26 Mar 2026 12:56:59 +0530 Subject: [PATCH 81/90] fix(stock): update company validation for expense account in lcv (cherry picked from commit 913168e8b603c024eb5f4cf131df121d0c144ab3) --- .../stock/doctype/landed_cost_voucher/landed_cost_voucher.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index fc4bc589f9d..4332b7429a6 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn from frappe.utils import cint, flt import erpnext +from erpnext import is_perpetual_inventory_enabled from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -175,6 +176,9 @@ class LandedCostVoucher(Document): ) def validate_expense_accounts(self): + if not is_perpetual_inventory_enabled(self.company): + return + for t in self.taxes: company = frappe.get_cached_value("Account", t.expense_account, "company") From ad966468b16e8b009e97880970e8aa4c4b0065d0 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 26 Mar 2026 15:04:33 +0530 Subject: [PATCH 82/90] fix(test): enable perpetual inventory (cherry picked from commit 875a2e494769e779d494916979df8001945ef6f4) --- .../test_landed_cost_voucher.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index ee2b2051e8b..26df7f59135 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -180,6 +180,8 @@ class TestLandedCostVoucher(ERPNextTestSuite): self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) def test_lcv_validates_company(self): + from erpnext import is_perpetual_inventory_enabled + from erpnext.accounts.doctype.account.test_account import create_account from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import ( IncorrectCompanyValidationError, ) @@ -187,6 +189,20 @@ class TestLandedCostVoucher(ERPNextTestSuite): company_a = "_Test Company" company_b = "_Test Company with perpetual inventory" + srbnb = create_account( + account_name="Stock Received But Not Billed", + account_type="Stock Received But Not Billed", + parent_account="Stock Liabilities - _TC", + company=company_a, + account_currency="INR", + ) + + epi = is_perpetual_inventory_enabled(company_a) + company_doc = frappe.get_doc("Company", company_a) + company_doc.enable_perpetual_inventory = 1 + company_doc.stock_received_but_not_billed = srbnb + company_doc.save() + pr = make_purchase_receipt( company=company_a, warehouse="Stores - _TC", @@ -212,6 +228,9 @@ class TestLandedCostVoucher(ERPNextTestSuite): distribute_landed_cost_on_items(lcv) lcv.submit() + frappe.db.set_value("Company", company_a, "enable_perpetual_inventory", epi) + frappe.local.enable_perpetual_inventory = {} + def test_landed_cost_voucher_for_zero_purchase_rate(self): "Test impact of LCV on future stock balances." from erpnext.stock.doctype.item.test_item import make_item From ad3c1e520e3855eeb4ac116c0541151e3570b39d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:12:48 +0000 Subject: [PATCH 83/90] fix(stock): add warehouse filter to pick work order raw materials (backport #53748) (#53898) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> fix(stock): add warehouse filter to pick work order raw materials (#53748) --- erpnext/stock/doctype/pick_list/pick_list.py | 40 ++++++++++++++-- .../stock/doctype/pick_list/test_pick_list.py | 47 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 9d01bb28fc3..7c31ec7a672 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -523,8 +523,26 @@ class PickList(TransactionBase): self.item_location_map = frappe._dict() from_warehouses = [self.parent_warehouse] if self.parent_warehouse else [] - if self.parent_warehouse: - from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse)) + + if self.work_order: + root_warehouse = frappe.db.get_value( + "Warehouse", {"company": self.company, "parent_warehouse": ["IS", "NOT SET"], "is_group": 1} + ) + + from_warehouses = [root_warehouse] + + if from_warehouses: + from_warehouses.extend(get_descendants_of("Warehouse", from_warehouses[0])) + + item_warehouse_dict = frappe._dict() + if self.work_order: + item_warehouse_list = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order}, + fields=["item_code", "source_warehouse"], + ) + if item_warehouse_list: + item_warehouse_dict = {item.item_code: item.source_warehouse for item in item_warehouse_list} # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") @@ -542,6 +560,13 @@ class PickList(TransactionBase): len_idx = len(self.get("locations")) or 0 for item_doc in items: item_code = item_doc.item_code + priority_warehouses = [] + + if self.work_order and item_warehouse_dict.get(item_code): + source_warehouse = item_warehouse_dict.get(item_code) + priority_warehouses = [source_warehouse] + priority_warehouses.extend(get_descendants_of("Warehouse", source_warehouse)) + from_warehouses = list(dict.fromkeys(priority_warehouses + from_warehouses)) self.item_location_map.setdefault( item_code, @@ -552,6 +577,7 @@ class PickList(TransactionBase): self.company, picked_item_details=picked_items_details.get(item_code), consider_rejected_warehouses=self.consider_rejected_warehouses, + priority_warehouses=priority_warehouses, ), ) @@ -969,6 +995,7 @@ def get_available_item_locations( ignore_validation=False, picked_item_details=None, consider_rejected_warehouses=False, + priority_warehouses=None, ): locations = [] @@ -1009,7 +1036,7 @@ def get_available_item_locations( locations = filter_locations_by_picked_materials(locations, picked_item_details) if locations: - locations = get_locations_based_on_required_qty(locations, required_qty) + locations = get_locations_based_on_required_qty(locations, required_qty, priority_warehouses) if not ignore_validation: validate_picked_materials(item_code, required_qty, locations, picked_item_details) @@ -1017,9 +1044,14 @@ def get_available_item_locations( return locations -def get_locations_based_on_required_qty(locations, required_qty): +def get_locations_based_on_required_qty(locations, required_qty, priority_warehouses): filtered_locations = [] + if priority_warehouses: + priority_locations = [loc for loc in locations if loc.warehouse in priority_warehouses] + fallback_locations = [loc for loc in locations if loc.warehouse not in priority_warehouses] + locations = priority_locations + fallback_locations + for location in locations: if location.qty >= required_qty: location.qty = required_qty diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 283e0207d60..85a45f1686b 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -1050,6 +1050,53 @@ class TestPickList(ERPNextTestSuite): pl = create_pick_list(so.name) self.assertFalse(pl.locations) + def test_pick_list_warehouse_for_work_order(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + from erpnext.manufacturing.doctype.work_order.work_order import create_pick_list, make_work_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + # Create Warehouses for Work Order + source_warehouse = create_warehouse("_Test WO Warehouse") + wip_warehouse = create_warehouse("_Test WIP Warehouse", company="_Test Company") + fg_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company") + + # Create Finished Good Item + fg_item = make_item("Test Work Order Finished Good Item", properties={"is_stock_item": 1}).name + + # Create Raw Material Item + rm_item = make_item("Test Work Order Raw Material Item", properties={"is_stock_item": 1}).name + + # Create BOM + bom = make_bom(item=fg_item, rate=100, raw_materials=[rm_item]) + + # Create Inward entry for Raw Material + make_stock_entry(item=rm_item, to_warehouse=wip_warehouse, qty=10) + make_stock_entry(item=rm_item, to_warehouse=source_warehouse, qty=10) + + # Create Work Order + wo = make_work_order(item=fg_item, qty=5, bom_no=bom.name, company="_Test Company") + wo.required_items[0].source_warehouse = source_warehouse + wo.fg_warehouse = fg_warehouse + wo.skip_transfer = True + wo.submit() + + # Create Pick List + pl = create_pick_list(wo.name, for_qty=wo.qty) + + # System prioritises the Source Warehouse + self.assertEqual(pl.locations[0].warehouse, source_warehouse) + self.assertEqual(pl.locations[0].item_code, rm_item) + self.assertEqual(pl.locations[0].qty, 5) + + # Create Outward Entry from Source Warehouse + make_stock_entry(item=rm_item, from_warehouse=source_warehouse, qty=10) + pl.set_item_locations() + + # System should pick other available warehouses + self.assertEqual(pl.locations[0].warehouse, wip_warehouse) + self.assertEqual(pl.locations[0].item_code, rm_item) + self.assertEqual(pl.locations[0].qty, 5) + def test_pick_list_validation_for_serial_no(self): warehouse = "_Test Warehouse - _TC" item = make_item( From f01f7e7974149f64bd7622d0752cb5673704354b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:48:25 +0530 Subject: [PATCH 84/90] fix(warehouse_capacity_dashboard): escaping `warehouse`, `item_code` and `company` on `get_data` (backport #53894) (#53900) Co-authored-by: diptanilsaha fix(warehouse_capacity_dashboard): escaping `warehouse`, `item_code` and `company` on `get_data` (#53894) --- erpnext/stock/dashboard/warehouse_capacity_dashboard.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py index 75b2951e30b..39701ed3f0d 100644 --- a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -1,6 +1,6 @@ import frappe from frappe.desk.reportview import build_match_conditions -from frappe.utils import flt, nowdate +from frappe.utils import escape_html, flt, nowdate from erpnext.stock.utils import get_stock_balance @@ -75,6 +75,9 @@ def get_warehouse_capacity_data(filters, start): balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 entry.update( { + "warehouse": escape_html(entry.warehouse), + "item_code": escape_html(entry.item_code), + "company": escape_html(entry.company), "actual_qty": balance_qty, "percent_occupied": flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0), } From 8d2c4da9312c5b326e5818557a1262dff64e4bff Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 30 Mar 2026 13:23:36 +0530 Subject: [PATCH 85/90] fix: item-wh reposting, code cleanup (cherry picked from commit e0ca34ae392b66e440035a56a5201749cd859b59) --- erpnext/stock/stock_ledger.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 838244a7fc2..c9114355a58 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -601,7 +601,7 @@ class update_entries_after: def initialize_reposting(self): self._sles = [] self.distinct_sles = set() - self.distinct_dependant_sle = set() + self.distinct_dependant_item_wh = set() self.prev_sle_dict = frappe._dict({}) def get_item_wh_wise_last_posted_sle(self): @@ -642,20 +642,15 @@ class update_entries_after: if item_wh_key not in self.prev_sle_dict: self.prev_sle_dict[item_wh_key] = get_previous_sle_of_current_voucher(sle) - if ( - sle.dependant_sle_voucher_detail_no - and sle.dependant_sle_voucher_detail_no not in self.distinct_dependant_sle - ): - self._sles.append(sle) - self.distinct_dependant_sle.add(sle.dependant_sle_voucher_detail_no) - self.include_dependant_sle_in_reposting(sle) - continue - self.repost_stock_ledger_entry(sle) # To avoid duplicate reposting of same sle in case of multiple dependant sle self.distinct_sles.add(sle.name) + if sle.dependant_sle_voucher_detail_no: + self.include_dependant_sle_in_reposting(sle) + self.update_item_wh_wise_last_posted_sle(sle) + if i % 1000 == 0: self.update_data_in_repost(len(self._sles), i) @@ -669,16 +664,28 @@ class update_entries_after: ) def include_dependant_sle_in_reposting(self, sle): + repost_dependant_sle = False if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no): repack_sles = self.get_sles_for_repack(sle) for repack_sle in repack_sles: + if (repack_sle.item_code, repack_sle.warehouse) in self.distinct_dependant_item_wh: + continue + + repost_dependant_sle = True + self.distinct_dependant_item_wh.add((repack_sle.item_code, repack_sle.warehouse)) self._sles.extend(self.get_future_entries_to_repost(repack_sle)) else: dependant_sles = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no) for depend_sle in dependant_sles: + if (depend_sle.item_code, depend_sle.warehouse) in self.distinct_dependant_item_wh: + continue + + repost_dependant_sle = True + self.distinct_dependant_item_wh.add((depend_sle.item_code, depend_sle.warehouse)) self._sles.extend(self.get_future_entries_to_repost(depend_sle)) - self._sles = deque(self.sort_sles(self._sles)) + if repost_dependant_sle: + self._sles = deque(self.sort_sles(self._sles)) def repost_stock_ledger_entry(self, sle): if self.args.item_code != sle.item_code or self.args.warehouse != sle.warehouse: From bfb51326edef822a169ccf88766ddc9a416a2e72 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 30 Mar 2026 13:50:12 +0530 Subject: [PATCH 86/90] fix: purchase invoice missing item (cherry picked from commit af994c1a229ae067c456cc834968a16837b06f9d) --- .../doctype/purchase_order/purchase_order.py | 28 +++++++++++-------- .../purchase_order/test_purchase_order.py | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 7e672da22b6..2cb285c14f3 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -802,18 +802,18 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions target.set_payment_schedule() target.credit_to = get_party_account("Supplier", source.supplier, source.company) + def get_billed_qty(po_item_name): + from frappe.query_builder.functions import Sum + + table = frappe.qb.DocType("Purchase Invoice Item") + query = ( + frappe.qb.from_(table) + .select(Sum(table.qty).as_("qty")) + .where((table.docstatus == 1) & (table.po_detail == po_item_name)) + ) + return query.run(pluck="qty")[0] or 0 + def update_item(obj, target, source_parent): - def get_billed_qty(po_item_name): - from frappe.query_builder.functions import Sum - - table = frappe.qb.DocType("Purchase Invoice Item") - query = ( - frappe.qb.from_(table) - .select(Sum(table.qty).as_("qty")) - .where((table.docstatus == 1) & (table.po_detail == po_item_name)) - ) - return query.run(pluck="qty")[0] or 0 - billed_qty = flt(get_billed_qty(obj.name)) target.qty = flt(obj.qty) - billed_qty @@ -853,7 +853,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)) + "condition": lambda doc: ( + doc.base_amount == 0 + or abs(doc.billed_amt) < abs(doc.amount) + or doc.qty > flt(get_billed_qty(doc.name)) + ) and select_item(doc), }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index fe4bb12c3db..e6956111ea0 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1386,6 +1386,34 @@ class TestPurchaseOrder(ERPNextTestSuite): self.assertEqual(pi_2.status, "Paid") self.assertEqual(po.status, "Completed") + def test_purchase_order_over_billing_missing_item(self): + item1 = make_item( + "_Test Item for Overbilling", + ).name + + item2 = make_item( + "_Test Item for Overbilling 2", + ).name + + po = create_purchase_order(qty=10, rate=1000, item_code=item1, do_not_save=1) + po.append("items", {"item_code": item2, "qty": 5, "rate": 20, "warehouse": "_Test Warehouse - _TC"}) + po.taxes = [] + po.insert() + po.submit() + + pi1 = make_pi_from_po(po.name) + pi1.items[0].qty = 8 + pi1.items[0].rate = 1250 + pi1.remove(pi1.items[1]) + pi1.insert() + pi1.submit() + + self.assertEqual(pi1.grand_total, 10000.0) + self.assertTrue(len(pi1.items) == 1) + + pi2 = make_pi_from_po(po.name) + self.assertEqual(len(pi2.items), 2) + def create_po_for_sc_testing(): from erpnext.controllers.tests.test_subcontracting_controller import ( From 4ac6347cc56fc556fd83e3789d1a3cecb30300ad Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:52:17 +0000 Subject: [PATCH 87/90] fix(item_dashboard): escaping `warehouse`, `item_code`, `stock_uom` and `item_name` on `get_data` (backport #53904) (#53914) Co-authored-by: diptanilsaha fix(item_dashboard): escaping `warehouse`, `item_code`, `stock_uom` and `item_name` on `get_data` (#53904) --- erpnext/stock/dashboard/item_dashboard.py | 8 +++++--- erpnext/stock/dashboard/item_dashboard_list.html | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index d77ed7a6212..5de54c55461 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -1,6 +1,6 @@ import frappe from frappe.desk.reportview import build_match_conditions -from frappe.utils import cint, flt +from frappe.utils import cint, escape_html, flt from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( get_sre_reserved_qty_for_items_and_warehouses as get_reserved_stock_details, @@ -70,8 +70,10 @@ def get_data( for item in items: item.update( { - "item_name": frappe.get_cached_value("Item", item.item_code, "item_name"), - "stock_uom": frappe.get_cached_value("Item", item.item_code, "stock_uom"), + "item_code": escape_html(item.item_code), + "item_name": escape_html(frappe.get_cached_value("Item", item.item_code, "item_name")), + "stock_uom": escape_html(frappe.get_cached_value("Item", item.item_code, "stock_uom")), + "warehouse": escape_html(item.warehouse), "disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no") or frappe.get_cached_value("Item", item.item_code, "has_serial_no"), "projected_qty": flt(item.projected_qty, precision), diff --git a/erpnext/stock/dashboard/item_dashboard_list.html b/erpnext/stock/dashboard/item_dashboard_list.html index ae90ff80686..34d51814b2f 100644 --- a/erpnext/stock/dashboard/item_dashboard_list.html +++ b/erpnext/stock/dashboard/item_dashboard_list.html @@ -50,15 +50,15 @@ data-warehouse="{{ d.warehouse }}" data-actual_qty="{{ d.actual_qty }}" data-stock-uom="{{ d.stock_uom }}" - data-item="{{ escape(d.item_code) }}">{{ __("Move") }} + data-item="{{ d.item_code }}">{{ __("Move") }} {% endif %} {% endif %} From 8c35a939cb5e9a715402dc0ad4d697c0efeba320 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:47:25 +0000 Subject: [PATCH 88/90] fix(opening_invoice_creation_tool): sanitize summary content for dashboard (backport #53917) (#53924) Co-authored-by: diptanilsaha fix(opening_invoice_creation_tool): sanitize summary content for dashboard (#53917) --- .../opening_invoice_creation_tool.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index e1e70e0f6cb..3949e242567 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -5,7 +5,7 @@ import frappe from frappe import _, scrub from frappe.model.document import Document -from frappe.utils import flt, nowdate +from frappe.utils import escape_html, flt, nowdate from frappe.utils.background_jobs import enqueue, is_job_enqueued from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -86,6 +86,11 @@ class OpeningInvoiceCreationTool(Document): ) prepare_invoice_summary(doctype, invoices) + invoices_summary_companies = list(invoices_summary.keys()) + + for company in invoices_summary_companies: + invoices_summary[escape_html(company)] = invoices_summary.pop(company) + return invoices_summary, max_count def validate_company(self): From c6fe5be95a5f5b4bc4a8334ae5f017ea82999d36 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 13 Mar 2026 11:53:08 +0530 Subject: [PATCH 89/90] fix: correct item valuation when "Deduct" is used in Purchase Invoice and Receipt. (cherry picked from commit e68f149d3afa469d92ab6dc20a34ff8981ea4fdd) --- erpnext/controllers/buying_controller.py | 8 ++- .../purchase_receipt/test_purchase_receipt.py | 59 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 67ccc4c7fe4..6383049be9c 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -503,11 +503,15 @@ class BuyingController(SubcontractingController): if d.category not in ["Valuation", "Valuation and Total"]: continue + amount = flt(d.base_tax_amount_after_discount_amount) * ( + -1 if d.get("add_deduct_tax") == "Deduct" else 1 + ) + if d.charge_type == "On Net Total": - total_valuation_amount += flt(d.base_tax_amount_after_discount_amount) + total_valuation_amount += amount tax_accounts.append(d.account_head) else: - total_actual_tax_amount += flt(d.base_tax_amount_after_discount_amount) + total_actual_tax_amount += amount return tax_accounts, total_valuation_amount, total_actual_tax_amount diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 74cdfb38f78..6eba41c3883 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1240,6 +1240,65 @@ class TestPurchaseReceipt(ERPNextTestSuite): pr.cancel() + def test_item_valuation_with_deduct_valuation_and_total_tax(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + qty=5, + rate=100, + do_not_save=1, + ) + + pr.append( + "taxes", + { + "charge_type": "Actual", + "add_deduct_tax": "Deduct", + "account_head": "_Test Account Shipping Charges - TCP1", + "category": "Valuation and Total", + "cost_center": "Main - TCP1", + "description": "Valuation Discount", + "tax_amount": 20, + }, + ) + + pr.insert() + + self.assertAlmostEqual(pr.items[0].item_tax_amount, -20.0, places=2) + self.assertAlmostEqual(pr.items[0].valuation_rate, 96.0, places=2) + + pr.delete() + + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + qty=5, + rate=100, + do_not_save=1, + ) + + pr.append( + "taxes", + { + "charge_type": "On Net Total", + "add_deduct_tax": "Deduct", + "account_head": "_Test Account Shipping Charges - TCP1", + "category": "Valuation and Total", + "cost_center": "Main - TCP1", + "description": "Valuation Discount", + "rate": 10, + }, + ) + + pr.insert() + + self.assertAlmostEqual(pr.items[0].item_tax_amount, -50.0, places=2) + self.assertAlmostEqual(pr.items[0].valuation_rate, 90.0, places=2) + + pr.delete() + def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: - Create PO From 8cb8f66b22af3618f3e769b13665a67537162c4d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:56:31 +0530 Subject: [PATCH 90/90] fix(bank_account): added validation to fetch bank account details using `get_bank_account_details` (backport #53926) (#53930) Co-authored-by: diptanilsaha fix(bank_account): added validation to fetch bank account details using `get_bank_account_details` (#53926) --- erpnext/accounts/doctype/bank_account/bank_account.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index c0dc6467f8f..9fe5b4ba3fb 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -116,6 +116,7 @@ def get_default_company_bank_account(company, party_type, party): @frappe.whitelist() def get_bank_account_details(bank_account): + frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True) return frappe.get_cached_value( "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1 )