From 254290a88eb9c00208856f0d3f34244771da87f6 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Sat, 6 Jun 2026 17:14:08 +0530 Subject: [PATCH 01/88] fix: updated role based permission for terms and conditions doctype (#55674) (cherry picked from commit 0ba29611033481008f873d3e98ed6cfbd699a877) --- .../terms_and_conditions/terms_and_conditions.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json index e33c638509b..be1d3c7a52e 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json @@ -89,7 +89,7 @@ "icon": "icon-legal", "idx": 1, "links": [], - "modified": "2026-04-29 22:51:49.285298", + "modified": "2026-06-06 16:35:34.394675", "modified_by": "Administrator", "module": "Setup", "name": "Terms and Conditions", @@ -135,7 +135,7 @@ "print": 1, "read": 1, "report": 1, - "role": "Accounts User", + "role": "Accounts Manager", "share": 1, "write": 1 }, @@ -152,6 +152,15 @@ "read": 1, "role": "HR Manager", "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 } ], "quick_entry": 1, From 6d038c5e71e2897ed3b14f036c55f13701f98286 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 6 Jun 2026 20:41:16 +0530 Subject: [PATCH 02/88] fix: set options Email for customer_email field in appointment (cherry picked from commit 9b1157c91411e12b210b9cc00784a72f99ffaa97) --- erpnext/crm/doctype/appointment/appointment.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 48dd49eae47..c600eb088c3 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -77,7 +77,8 @@ "fieldname": "customer_email", "fieldtype": "Data", "label": "Email", - "reqd": 1 + "reqd": 1, + "options": "Email" }, { "fieldname": "linked_docs_section", @@ -102,7 +103,7 @@ } ], "links": [], - "modified": "2024-03-27 13:05:59.300573", + "modified": "2026-06-06 13:05:59.300573", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", From b05abbc53b3655b02db17ba2e8165519f195c1c2 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:53:33 +0530 Subject: [PATCH 03/88] fix: apply user permissions to receivable/payable reports --- .../accounts_receivable.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 0cd34e030d6..56eec27c6d0 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -928,8 +928,28 @@ class ReceivablePayableReport: if self.filters.project: self.qb_selection_filter.append(self.ple.project.isin(self.filters.project)) + self.add_user_permission_filters() + self.add_accounting_dimensions_filters() + def add_user_permission_filters(self): + # Party is a dynamic link, so match conditions cannot auto-apply Customer/Supplier user permissions + from frappe.core.doctype.user_permission.user_permission import get_user_permissions + from frappe.permissions import get_allowed_docs_for_doctype + + user_permissions = get_user_permissions() + if not user_permissions: + return + + for party_type in self.party_type: + if party_type not in user_permissions: + continue + + allowed_parties = get_allowed_docs_for_doctype(user_permissions[party_type], party_type) + self.qb_selection_filter.append( + (self.ple.party_type != party_type) | self.ple.party.isin(allowed_parties or [""]) + ) + def get_cost_center_conditions(self): cost_center_list = get_cost_centers_with_children(self.filters.cost_center) self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list)) From 4200d17c9b208c72749e426a66a09cb48aeadbd9 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:58:21 +0530 Subject: [PATCH 04/88] test: cover user permission scoping in receivable report --- .../test_accounts_receivable.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 9b8b8b709db..1b7476c4907 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1245,3 +1245,44 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin): self.assertEqual(len(report[1]), 1) row = report[1][0] self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding]) + + def test_accounts_receivable_respects_user_permissions(self): + # Party is a dynamic link on Payment Ledger Entry, so user permissions on Customer + # must be applied explicitly. The report should only show permitted customers. + original_customer = self.customer + second_customer = "_Test AR Perm Customer" + + # create_customer overrides self.customer, so build the restricted invoice first + self.create_customer(customer_name=second_customer) + self.create_sales_invoice(no_payment_schedule=True) + + self.customer = original_customer + allowed_invoice = self.create_sales_invoice(no_payment_schedule=True) + + test_user = "test_ar_user_permission@example.com" + if not frappe.db.exists("User", test_user): + user = frappe.new_doc("User") + user.email = test_user + user.first_name = "AR Perm" + user.append("roles", {"role": "Accounts User"}) + user.save() + + frappe.permissions.add_user_permission("Customer", original_customer, test_user) + + filters = { + "company": self.company, + "party_type": "Customer", + "report_date": today(), + "range": "30, 60, 90, 120", + } + + frappe.set_user(test_user) + try: + report = execute(filters) + finally: + frappe.set_user("Administrator") + + parties = {row.party for row in report[1]} + self.assertIn(original_customer, parties) + self.assertNotIn(second_customer, parties) + self.assertEqual(allowed_invoice.customer, original_customer) From f43af6624610e874e61ad3faf8701e5e6be6271a Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Mon, 1 Jun 2026 21:24:42 +0530 Subject: [PATCH 05/88] fix(inactive_customers): add allowlist for doctype filter and migrate to qb (cherry picked from commit 2ecf8b0466143bca086f6e6b65dade5f4fc250b8) # Conflicts: # erpnext/selling/report/inactive_customers/inactive_customers.py --- .../inactive_customers/inactive_customers.py | 94 ++++++++++++------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index d21d11b2447..32f44e7d29f 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -4,6 +4,8 @@ import frappe from frappe import _ +from frappe.query_builder import Case, CustomFunction +from frappe.query_builder.functions import Count, Max, Sum from frappe.utils import cint @@ -14,7 +16,11 @@ def execute(filters=None): days_since_last_order = filters.get("days_since_last_order") doctype = filters.get("doctype") +<<<<<<< HEAD if doctype not in ("Sales Order", "Sales Invoice"): +======= + if doctype not in {"Sales Order", "Sales Invoice"}: +>>>>>>> 2ecf8b0466 (fix(inactive_customers): add allowlist for doctype filter and migrate to qb) frappe.throw(_("Invalid value {0} for 'Doctype'").format(doctype)) if cint(days_since_last_order) <= 0: @@ -24,50 +30,66 @@ def execute(filters=None): customers = get_sales_details(doctype) data = [] - for cust in customers: - if cint(cust[8]) >= cint(days_since_last_order): - cust.insert(7, get_last_sales_amt(cust[0], doctype)) - data.append(cust) + for C in customers: + if cint(C[8]) >= cint(days_since_last_order): + C.insert(7, get_last_sales_amt(C[0], doctype)) + data.append(C) return columns, data def get_sales_details(doctype): - cond = """sum(so.base_net_total) as 'total_order_considered', - max(so.posting_date) as 'last_order_date', - DATEDIFF(CURRENT_DATE, max(so.posting_date)) as 'days_since_last_order' """ - if doctype == "Sales Order": - cond = """sum(if(so.status = "Stopped", - so.base_net_total * so.per_delivered/100, - so.base_net_total)) as 'total_order_considered', - max(so.transaction_date) as 'last_order_date', - DATEDIFF(CURRENT_DATE, max(so.transaction_date)) as 'days_since_last_order'""" + C = frappe.qb.DocType("Customer") + DT = frappe.qb.DocType(doctype) - return frappe.db.sql( - f"""select - cust.name, - cust.customer_name, - cust.territory, - cust.customer_group, - count(distinct(so.name)) as 'num_of_order', - sum(base_net_total) as 'total_order_value', {cond} - from `tabCustomer` cust, `tab{doctype}` so - where cust.name = so.customer and so.docstatus = 1 - group by cust.name - order by 'days_since_last_order' desc """, - as_list=1, - ) + DateDiff = CustomFunction("DATEDIFF", ["d1", "d2"]) + CurDate = CustomFunction("CURRENT_DATE", []) + + if doctype == "Sales Order": + total_considered = Sum( + Case() + .when(DT.status == "Stopped", DT.base_net_total * DT.per_delivered / 100) + .else_(DT.base_net_total) + ) + date_col = DT.transaction_date + else: + total_considered = Sum(DT.base_net_total) + date_col = DT.posting_date + + last_order_date = Max(date_col) + days_since_last_order = DateDiff(CurDate(), last_order_date) + + return ( + frappe.qb.from_(C) + .inner_join(DT) + .on(C.name == DT.customer) + .select( + C.name, + C.customer_name, + C.territory, + C.customer_group, + Count(DT.name).distinct().as_("num_of_order"), + Sum(DT.base_net_total).as_("total_order_value"), + total_considered.as_("total_order_considered"), + last_order_date.as_("last_order_date"), + days_since_last_order.as_("days_since_last_order"), + ) + .where(DT.docstatus == 1) + .groupby(C.name) + .orderby(days_since_last_order, order=frappe.qb.desc) + ).run(as_list=True) def get_last_sales_amt(customer, doctype): - cond = "posting_date" - if doctype == "Sales Order": - cond = "transaction_date" - res = frappe.db.sql( - f"""select base_net_total from `tab{doctype}` - where customer = %s and docstatus = 1 order by {cond} desc - limit 1""", - customer, - ) + DT = frappe.qb.DocType(doctype) + date_col = DT.transaction_date if doctype == "Sales Order" else DT.posting_date + + res = ( + frappe.qb.from_(DT) + .select(DT.base_net_total) + .where((DT.customer == customer) & (DT.docstatus == 1)) + .orderby(date_col, order=frappe.qb.desc) + .limit(1) + ).run() return res and res[0][0] or 0 From 7a23a9347f7878c54f38013bf50249413cec274a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 8 Jun 2026 11:45:34 +0530 Subject: [PATCH 06/88] refactor(inactive_customers): use descriptive aliases and add tests Rename single-letter query-builder aliases (C, DT) to readable names (customer, sales) and add report tests covering the column contract, validation guards, and the days-since-last-order threshold. (cherry picked from commit 8f15dd4d5d1288958f301d4b7144572e3a42ba0f) --- .../inactive_customers/inactive_customers.py | 60 +++++++++---------- .../test_inactive_customers.py | 59 ++++++++++++++++++ 2 files changed, 89 insertions(+), 30 deletions(-) create mode 100644 erpnext/selling/report/inactive_customers/test_inactive_customers.py diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index 32f44e7d29f..ed55460736a 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -30,63 +30,63 @@ def execute(filters=None): customers = get_sales_details(doctype) data = [] - for C in customers: - if cint(C[8]) >= cint(days_since_last_order): - C.insert(7, get_last_sales_amt(C[0], doctype)) - data.append(C) + for row in customers: + if cint(row[8]) >= cint(days_since_last_order): + row.insert(7, get_last_sales_amt(row[0], doctype)) + data.append(row) return columns, data def get_sales_details(doctype): - C = frappe.qb.DocType("Customer") - DT = frappe.qb.DocType(doctype) + customer = frappe.qb.DocType("Customer") + sales = frappe.qb.DocType(doctype) - DateDiff = CustomFunction("DATEDIFF", ["d1", "d2"]) - CurDate = CustomFunction("CURRENT_DATE", []) + date_diff = CustomFunction("DATEDIFF", ["d1", "d2"]) + current_date = CustomFunction("CURRENT_DATE", []) if doctype == "Sales Order": total_considered = Sum( Case() - .when(DT.status == "Stopped", DT.base_net_total * DT.per_delivered / 100) - .else_(DT.base_net_total) + .when(sales.status == "Stopped", sales.base_net_total * sales.per_delivered / 100) + .else_(sales.base_net_total) ) - date_col = DT.transaction_date + date_col = sales.transaction_date else: - total_considered = Sum(DT.base_net_total) - date_col = DT.posting_date + total_considered = Sum(sales.base_net_total) + date_col = sales.posting_date last_order_date = Max(date_col) - days_since_last_order = DateDiff(CurDate(), last_order_date) + days_since_last_order = date_diff(current_date(), last_order_date) return ( - frappe.qb.from_(C) - .inner_join(DT) - .on(C.name == DT.customer) + frappe.qb.from_(customer) + .inner_join(sales) + .on(customer.name == sales.customer) .select( - C.name, - C.customer_name, - C.territory, - C.customer_group, - Count(DT.name).distinct().as_("num_of_order"), - Sum(DT.base_net_total).as_("total_order_value"), + customer.name, + customer.customer_name, + customer.territory, + customer.customer_group, + Count(sales.name).distinct().as_("num_of_order"), + Sum(sales.base_net_total).as_("total_order_value"), total_considered.as_("total_order_considered"), last_order_date.as_("last_order_date"), days_since_last_order.as_("days_since_last_order"), ) - .where(DT.docstatus == 1) - .groupby(C.name) + .where(sales.docstatus == 1) + .groupby(customer.name) .orderby(days_since_last_order, order=frappe.qb.desc) ).run(as_list=True) def get_last_sales_amt(customer, doctype): - DT = frappe.qb.DocType(doctype) - date_col = DT.transaction_date if doctype == "Sales Order" else DT.posting_date + sales = frappe.qb.DocType(doctype) + date_col = sales.transaction_date if doctype == "Sales Order" else sales.posting_date res = ( - frappe.qb.from_(DT) - .select(DT.base_net_total) - .where((DT.customer == customer) & (DT.docstatus == 1)) + frappe.qb.from_(sales) + .select(sales.base_net_total) + .where((sales.customer == customer) & (sales.docstatus == 1)) .orderby(date_col, order=frappe.qb.desc) .limit(1) ).run() diff --git a/erpnext/selling/report/inactive_customers/test_inactive_customers.py b/erpnext/selling/report/inactive_customers/test_inactive_customers.py new file mode 100644 index 00000000000..be7aa39e3be --- /dev/null +++ b/erpnext/selling/report/inactive_customers/test_inactive_customers.py @@ -0,0 +1,59 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.utils import add_days, getdate, today + +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.inactive_customers.inactive_customers import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestInactiveCustomers(ERPNextTestSuite): + def setUp(self): + self.customer = frappe.get_doc(doctype="Customer", customer_name="_Test Inactive Customer").insert() + self.last_order_date = add_days(today(), -120) + so = make_sales_order( + customer=self.customer.name, + transaction_date=self.last_order_date, + qty=5, + rate=200, + ) + so.submit() + self.sales_order = so + + def test_invalid_doctype_is_rejected(self): + self.assertRaises( + frappe.ValidationError, + execute, + {"doctype": "Purchase Order", "days_since_last_order": 30}, + ) + + def test_non_positive_days_is_rejected(self): + self.assertRaises( + frappe.ValidationError, + execute, + {"doctype": "Sales Order", "days_since_last_order": 0}, + ) + + def test_inactive_customer_is_listed_with_expected_columns(self): + columns, data = execute({"doctype": "Sales Order", "days_since_last_order": 30}) + + row = self.get_customer_row(data) + self.assertIsNotNone(row, "Inactive customer should be present in the report") + + # Column contract: the report relies on positional access. + self.assertEqual(row[0], self.customer.name) + self.assertEqual(row[7], 1000) # Last Order Amount inserted at index 7 (5 * 200) + self.assertEqual(getdate(row[8]), getdate(self.last_order_date)) # Last Order Date + self.assertGreaterEqual(row[9], 30) # Days Since Last Order + + def test_recent_customer_is_excluded(self): + _columns, data = execute({"doctype": "Sales Order", "days_since_last_order": 200}) + self.assertIsNone( + self.get_customer_row(data), + "Customer ordering within the threshold must be excluded", + ) + + def get_customer_row(self, data): + return next((row for row in data if row[0] == self.customer.name), None) From aaf2531a4e351dc09e6cb0c5c64c867c42dc5a86 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 8 Jun 2026 11:52:56 +0530 Subject: [PATCH 07/88] refactor(inactive_customers): rename sales alias to sales_doctype (cherry picked from commit 8d7edafc99364995927ca633eceffafa5b857ea4) --- .../inactive_customers/inactive_customers.py | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index ed55460736a..27bb71e332c 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -39,7 +39,7 @@ def execute(filters=None): def get_sales_details(doctype): customer = frappe.qb.DocType("Customer") - sales = frappe.qb.DocType(doctype) + sales_doctype = frappe.qb.DocType(doctype) date_diff = CustomFunction("DATEDIFF", ["d1", "d2"]) current_date = CustomFunction("CURRENT_DATE", []) @@ -47,46 +47,49 @@ def get_sales_details(doctype): if doctype == "Sales Order": total_considered = Sum( Case() - .when(sales.status == "Stopped", sales.base_net_total * sales.per_delivered / 100) - .else_(sales.base_net_total) + .when( + sales_doctype.status == "Stopped", + sales_doctype.base_net_total * sales_doctype.per_delivered / 100, + ) + .else_(sales_doctype.base_net_total) ) - date_col = sales.transaction_date + date_col = sales_doctype.transaction_date else: - total_considered = Sum(sales.base_net_total) - date_col = sales.posting_date + total_considered = Sum(sales_doctype.base_net_total) + date_col = sales_doctype.posting_date last_order_date = Max(date_col) days_since_last_order = date_diff(current_date(), last_order_date) return ( frappe.qb.from_(customer) - .inner_join(sales) - .on(customer.name == sales.customer) + .inner_join(sales_doctype) + .on(customer.name == sales_doctype.customer) .select( customer.name, customer.customer_name, customer.territory, customer.customer_group, - Count(sales.name).distinct().as_("num_of_order"), - Sum(sales.base_net_total).as_("total_order_value"), + Count(sales_doctype.name).distinct().as_("num_of_order"), + Sum(sales_doctype.base_net_total).as_("total_order_value"), total_considered.as_("total_order_considered"), last_order_date.as_("last_order_date"), days_since_last_order.as_("days_since_last_order"), ) - .where(sales.docstatus == 1) + .where(sales_doctype.docstatus == 1) .groupby(customer.name) .orderby(days_since_last_order, order=frappe.qb.desc) ).run(as_list=True) def get_last_sales_amt(customer, doctype): - sales = frappe.qb.DocType(doctype) - date_col = sales.transaction_date if doctype == "Sales Order" else sales.posting_date + sales_doctype = frappe.qb.DocType(doctype) + date_col = sales_doctype.transaction_date if doctype == "Sales Order" else sales_doctype.posting_date res = ( - frappe.qb.from_(sales) - .select(sales.base_net_total) - .where((sales.customer == customer) & (sales.docstatus == 1)) + frappe.qb.from_(sales_doctype) + .select(sales_doctype.base_net_total) + .where((sales_doctype.customer == customer) & (sales_doctype.docstatus == 1)) .orderby(date_col, order=frappe.qb.desc) .limit(1) ).run() From fa08501045c16125749f97be0a84d8c4718ed6af Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 8 Jun 2026 11:55:32 +0530 Subject: [PATCH 08/88] test(inactive_customers): remove non-positive days test case (cherry picked from commit 601f39dda7688203e697e2ba419fe27a743a6589) --- .../report/inactive_customers/test_inactive_customers.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/selling/report/inactive_customers/test_inactive_customers.py b/erpnext/selling/report/inactive_customers/test_inactive_customers.py index be7aa39e3be..c139f42a61f 100644 --- a/erpnext/selling/report/inactive_customers/test_inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/test_inactive_customers.py @@ -29,13 +29,6 @@ class TestInactiveCustomers(ERPNextTestSuite): {"doctype": "Purchase Order", "days_since_last_order": 30}, ) - def test_non_positive_days_is_rejected(self): - self.assertRaises( - frappe.ValidationError, - execute, - {"doctype": "Sales Order", "days_since_last_order": 0}, - ) - def test_inactive_customer_is_listed_with_expected_columns(self): columns, data = execute({"doctype": "Sales Order", "days_since_last_order": 30}) From b9eb52b1713e54a958e9a213e6d42b78c397507e Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sun, 31 May 2026 04:40:02 +0530 Subject: [PATCH 09/88] fix: add permission checks in accounts whitelisted methods (cherry picked from commit 5dbf3fdde0e11fa482a42bfa36559becdc814c6a) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 ++++ erpnext/accounts/utils.py | 1 + 2 files changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 68b54cd9d3a..1671d3980a4 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2294,6 +2294,9 @@ def get_outstanding_reference_documents(args, validate=False): if args.get("party_type") == "Member": return + if args.get("party_type") and args.get("party"): + frappe.has_permission(args["party_type"], "read", args["party"], throw=True) + if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"): args["get_outstanding_invoices"] = True @@ -2785,6 +2788,7 @@ def get_reference_details( ): total_amount = outstanding_amount = exchange_rate = account = None + frappe.has_permission(reference_doctype, "read", reference_name, throw=True) ref_doc = frappe.get_lazy_doc(reference_doctype, reference_name) company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index bdd0e455011..a12d470a1d2 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -303,6 +303,7 @@ def get_balance_on( ) if party_type and party: + frappe.has_permission(party_type, "read", party, throw=True) cond.append( f"""gle.party_type = {frappe.db.escape(party_type)} and gle.party = {frappe.db.escape(party)} """ ) From 10cfac865ee64865d3c5e5d72d4e1bb9645dd80c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 9 Jun 2026 09:48:38 +0530 Subject: [PATCH 10/88] chore: fix conflicts --- .../selling/report/inactive_customers/inactive_customers.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index 27bb71e332c..ea0831391d3 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -16,11 +16,7 @@ def execute(filters=None): days_since_last_order = filters.get("days_since_last_order") doctype = filters.get("doctype") -<<<<<<< HEAD if doctype not in ("Sales Order", "Sales Invoice"): -======= - if doctype not in {"Sales Order", "Sales Invoice"}: ->>>>>>> 2ecf8b0466 (fix(inactive_customers): add allowlist for doctype filter and migrate to qb) frappe.throw(_("Invalid value {0} for 'Doctype'").format(doctype)) if cint(days_since_last_order) <= 0: From 2bea9ae2a5d33e88edb9d5e60c514ad2cb7096fc Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 9 Jun 2026 17:57:54 +0530 Subject: [PATCH 11/88] fix: show inactive product bundles in item where used (#55769) (cherry picked from commit 6201fefdfba02f5255b91827a8ee77898dc50be6) # Conflicts: # erpnext/stock/report/item_where_used/item_where_used.py --- .../stock/report/item_where_used/item_where_used.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/stock/report/item_where_used/item_where_used.py b/erpnext/stock/report/item_where_used/item_where_used.py index 1d53ecad90b..730091ed42e 100644 --- a/erpnext/stock/report/item_where_used/item_where_used.py +++ b/erpnext/stock/report/item_where_used/item_where_used.py @@ -294,8 +294,13 @@ def get_product_bundle_component_rows(item): def get_product_bundle_parent_rows(item): rows = frappe.get_all( "Product Bundle", +<<<<<<< HEAD filters={"new_item_code": item, "disabled": 0, "docstatus": 0}, fields=["name", "new_item_code", "disabled"], +======= + filters={"new_item_code": item, "docstatus": 1}, + fields=["name", "new_item_code", "is_active", "disabled"], +>>>>>>> 6201fefdfb (fix: show inactive product bundles in item where used (#55769)) order_by="name asc", ) @@ -463,8 +468,13 @@ def get_product_bundle_map(bundle_names): row.name: row for row in frappe.get_all( "Product Bundle", +<<<<<<< HEAD filters={"name": ["in", bundle_names], "disabled": 0, "docstatus": 0}, fields=["name", "new_item_code", "disabled"], +======= + filters={"name": ["in", bundle_names], "docstatus": 1}, + fields=["name", "new_item_code", "is_active", "disabled"], +>>>>>>> 6201fefdfb (fix: show inactive product bundles in item where used (#55769)) ) } From 8e25189d7d2679f762952aa0f7cbe009c1f7b961 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 10 Jun 2026 10:14:23 +0530 Subject: [PATCH 12/88] chore: resolve conflicts --- .../stock/report/item_where_used/item_where_used.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/erpnext/stock/report/item_where_used/item_where_used.py b/erpnext/stock/report/item_where_used/item_where_used.py index 730091ed42e..a2b333bfb8f 100644 --- a/erpnext/stock/report/item_where_used/item_where_used.py +++ b/erpnext/stock/report/item_where_used/item_where_used.py @@ -294,13 +294,8 @@ def get_product_bundle_component_rows(item): def get_product_bundle_parent_rows(item): rows = frappe.get_all( "Product Bundle", -<<<<<<< HEAD - filters={"new_item_code": item, "disabled": 0, "docstatus": 0}, - fields=["name", "new_item_code", "disabled"], -======= filters={"new_item_code": item, "docstatus": 1}, fields=["name", "new_item_code", "is_active", "disabled"], ->>>>>>> 6201fefdfb (fix: show inactive product bundles in item where used (#55769)) order_by="name asc", ) @@ -468,13 +463,8 @@ def get_product_bundle_map(bundle_names): row.name: row for row in frappe.get_all( "Product Bundle", -<<<<<<< HEAD - filters={"name": ["in", bundle_names], "disabled": 0, "docstatus": 0}, - fields=["name", "new_item_code", "disabled"], -======= filters={"name": ["in", bundle_names], "docstatus": 1}, fields=["name", "new_item_code", "is_active", "disabled"], ->>>>>>> 6201fefdfb (fix: show inactive product bundles in item where used (#55769)) ) } From 018f06d8d1f3ae85625a1da80f80d93442ce4f8d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 10 Jun 2026 10:49:27 +0530 Subject: [PATCH 13/88] fix: prefetch batchwise valuations before streaming SLEs in stock ageing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stock Ageing iterates stock ledger entries through an unbuffered (streaming) cursor. _get_batchwise_valuation() lazily queried Batch.use_batchwise_valuation from inside that loop whenever a row carried the legacy batch_no field, and the nested query invalidated the active streaming result set — crashing the report (or silently dropping the remaining rows, depending on the driver version). Resolve the valuation flags in a single query before entering the unbuffered cursor block; the lazy lookup now only serves callers that pass stock ledger entries in directly, where no streaming is active. Fixes https://github.com/frappe/erpnext/issues/55786 Co-Authored-By: Claude Fable 5 (cherry picked from commit 060a5c4eeb1cf38b9fb5726f4a98182c6a229bbc) --- .../stock/report/stock_ageing/stock_ageing.py | 31 ++++++++ .../report/stock_ageing/test_stock_ageing.py | 74 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 2a2b5e14c51..2f44f771bd7 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -306,6 +306,11 @@ class FIFOSlots: # prepare single sle voucher detail lookup self.prepare_stock_reco_voucher_wise_count() + if stock_ledger_entries is None: + # nested queries invalidate the streaming cursor below, + # so batchwise valuation flags must be resolved beforehand + self._prefetch_batchwise_valuations() + with frappe.db.unbuffered_cursor(): if stock_ledger_entries is None: stock_ledger_entries = self._get_stock_ledger_entries() @@ -423,12 +428,38 @@ class FIFOSlots: def _get_batchwise_valuation(self, batch_no: str): if batch_no not in self.batchwise_valuation_by_batch: + # only reachable when stock ledger entries are passed in directly; + # the streaming path prefetches all flags before iteration self.batchwise_valuation_by_batch[batch_no] = frappe.db.get_value( "Batch", batch_no, "use_batchwise_valuation" ) return self.batchwise_valuation_by_batch[batch_no] + def _prefetch_batchwise_valuations(self) -> None: + sle = frappe.qb.DocType("Stock Ledger Entry") + batch = frappe.qb.DocType("Batch") + to_date = get_datetime(self.filters.get("to_date") + " 23:59:59") + + query = ( + frappe.qb.from_(sle) + .left_join(batch) + .on(sle.batch_no == batch.name) + .select(sle.batch_no, batch.use_batchwise_valuation) + .distinct() + .where( + (sle.batch_no.isnotnull()) + & (sle.company == self.filters.get("company")) + & (sle.posting_datetime <= to_date) + & (sle.is_cancelled != 1) + ) + ) + + query = self._apply_filter(query, sle, "item_code") + + for batch_no, use_batchwise_valuation in query.run(): + self.batchwise_valuation_by_batch[batch_no] = use_batchwise_valuation + def _init_key_stores(self, row: dict) -> tuple: "Initialise keys and FIFO Queue." diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index fc710b6884a..96cc5a58866 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -1434,6 +1434,80 @@ class TestStockAgeing(ERPNextTestSuite): item_result["fifo_queue"], [[batch_no.upper(), 1, 5.0, getdate(add_days(base_date, -2)), 50.0]] ) + def test_legacy_batch_no_sle_with_streaming_cursor(self): + """SLEs carrying the legacy batch_no field must not trigger nested + queries while entries stream through an unbuffered cursor.""" + from unittest.mock import patch + + from frappe.utils import add_days, nowdate + + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + get_batch_from_bundle, + ) + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + + suffix = frappe.generate_hash(length=8).upper() + item_code = make_item( + f"Test Stock Ageing Legacy Batch {suffix}", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": f"SA-LEG-{suffix}-.###", + "valuation_method": "FIFO", + }, + ).name + warehouse = "_Test Warehouse - _TC" + base_date = nowdate() + + reco = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=10, + rate=10, + posting_date=add_days(base_date, -2), + posting_time="10:00:00", + ) + batch_no = get_batch_from_bundle(reco.items[0].serial_and_batch_bundle) + frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1) + + create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=5, + rate=10, + batch_no=batch_no, + posting_date=add_days(base_date, -1), + posting_time="10:00:00", + ) + + # mimic pre-bundle data where SLEs carry batch_no directly + frappe.db.set_value( + "Stock Ledger Entry", + {"item_code": item_code}, + "batch_no", + batch_no, + ) + + filters = frappe._dict( + company="_Test Company", + to_date=base_date, + ranges=["30", "60", "90"], + item_code=item_code, + ) + fifo_slots = FIFOSlots(filters) + + # fetch row by row so the streaming result set is still active + # while each stock ledger entry is processed + with patch("frappe.database.database.SQL_ITERATOR_BATCH_SIZE", 1): + slots = fifo_slots.generate() + + self.assertEqual(fifo_slots.batchwise_valuation_by_batch.get(batch_no), 1) + self.assertEqual(slots[item_code]["total_qty"], 5.0) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" From 0c1a5082bd6aedfbd545a235a12d794988688824 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 10 Jun 2026 19:28:29 +0530 Subject: [PATCH 14/88] feat(selling): surface and respect disabled Product Bundles (backport #55791) Partial backport of frappe/erpnext#55791 to version-16-hotfix. On v16, Product Bundle is neither submittable nor versioned and every resolution path (packing, POS, item details, selling controller) already filters `disabled: 0`, so only the user-facing gaps are backported: - the "Get Items from Product Bundle" dialog no longer offers disabled bundles - list view indicator: Disabled (grey) / Active (green) - Product Bundle Balance report excludes disabled bundles - `disabled` field gets a description, standard filter and no_copy (a copied bundle starts enabled) The develop-only parts (un-deprecating the field, `is_active`/docstatus handling, the version picker filter and the disabled-version validation on transaction rows) have no v16 equivalent and are intentionally dropped. Co-Authored-By: Claude Fable 5 --- erpnext/public/js/controllers/buying.js | 3 +++ .../doctype/product_bundle/product_bundle.json | 7 +++++-- .../doctype/product_bundle/product_bundle_list.js | 12 ++++++++++++ .../product_bundle_balance/product_bundle_balance.py | 4 ++-- 4 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 erpnext/selling/doctype/product_bundle/product_bundle_list.js diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index aa4f5b70b9e..0e5b0031078 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -611,6 +611,9 @@ erpnext.buying.get_items_from_product_bundle = function (frm) { fieldname: "product_bundle", options: "Product Bundle", reqd: 1, + get_query: () => { + return { filters: { disabled: 0 } }; + }, }, { fieldtype: "Currency", diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json index 9cb95aefe05..de335c55fca 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.json +++ b/erpnext/selling/doctype/product_bundle/product_bundle.json @@ -65,9 +65,12 @@ }, { "default": "0", + "description": "A disabled Product Bundle cannot be selected in transactions.", "fieldname": "disabled", "fieldtype": "Check", - "label": "Disabled" + "in_standard_filter": 1, + "label": "Disabled", + "no_copy": 1 }, { "fieldname": "column_break_eonk", @@ -77,7 +80,7 @@ "icon": "fa fa-sitemap", "idx": 1, "links": [], - "modified": "2024-03-27 13:10:19.599302", + "modified": "2026-06-10 16:00:00.000000", "modified_by": "Administrator", "module": "Selling", "name": "Product Bundle", diff --git a/erpnext/selling/doctype/product_bundle/product_bundle_list.js b/erpnext/selling/doctype/product_bundle/product_bundle_list.js new file mode 100644 index 00000000000..b6788bacf10 --- /dev/null +++ b/erpnext/selling/doctype/product_bundle/product_bundle_list.js @@ -0,0 +1,12 @@ +// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +frappe.listview_settings["Product Bundle"] = { + add_fields: ["disabled"], + get_indicator(doc) { + if (doc.disabled) { + return [__("Disabled"), "grey", "disabled,=,1"]; + } + return [__("Active"), "green", "disabled,=,0"]; + }, +}; diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 5a8960396b3..c7ba791dfea 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -140,7 +140,7 @@ def get_items(filters): item.brand, item.stock_uom, ) - .where(IfNull(item.disabled, 0) == 0) + .where((IfNull(item.disabled, 0) == 0) & (IfNull(pb.disabled, 0) == 0)) ) if item_code := filters.get("item_code"): @@ -182,7 +182,7 @@ def get_items(filters): pbi.uom, pbi.qty, ) - .where(pb.new_item_code.isin(parent_items)) + .where(pb.new_item_code.isin(parent_items) & (IfNull(pb.disabled, 0) == 0)) ).run(as_dict=1) child_items = set() From b1895e9a9a0747715813fca59e2a196b6d2bf58f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 10 Jun 2026 19:29:42 +0530 Subject: [PATCH 15/88] fix: adapt Product Bundle queries to v16 schema On version-16-hotfix, Product Bundle has no `is_active` field and is not submittable (docstatus is always 0), so the backported queries crashed with "Unknown column 'is_active'" and would otherwise have matched no rows. Keep v16's `docstatus: 0` and derive activity from `disabled`, as the surrounding build_row calls already do. Co-Authored-By: Claude Fable 5 --- erpnext/stock/report/item_where_used/item_where_used.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/item_where_used/item_where_used.py b/erpnext/stock/report/item_where_used/item_where_used.py index a2b333bfb8f..2c193b4a6bd 100644 --- a/erpnext/stock/report/item_where_used/item_where_used.py +++ b/erpnext/stock/report/item_where_used/item_where_used.py @@ -294,8 +294,8 @@ def get_product_bundle_component_rows(item): def get_product_bundle_parent_rows(item): rows = frappe.get_all( "Product Bundle", - filters={"new_item_code": item, "docstatus": 1}, - fields=["name", "new_item_code", "is_active", "disabled"], + filters={"new_item_code": item, "docstatus": 0}, + fields=["name", "new_item_code", "disabled"], order_by="name asc", ) @@ -463,8 +463,8 @@ def get_product_bundle_map(bundle_names): row.name: row for row in frappe.get_all( "Product Bundle", - filters={"name": ["in", bundle_names], "docstatus": 1}, - fields=["name", "new_item_code", "is_active", "disabled"], + filters={"name": ["in", bundle_names], "docstatus": 0}, + fields=["name", "new_item_code", "disabled"], ) } From dbc62276d6aa95e1775fe8c7764f63a2894e6af7 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 10 Jun 2026 21:38:27 +0530 Subject: [PATCH 16/88] refactor: drop redundant disabled filter on child query `parent_items` already comes from the disabled-filtered parent query, and on v16 an item has at most one bundle, so the child query cannot reach a disabled bundle. Co-Authored-By: Claude Fable 5 --- .../report/product_bundle_balance/product_bundle_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index c7ba791dfea..34be8fc6a7f 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -182,7 +182,7 @@ def get_items(filters): pbi.uom, pbi.qty, ) - .where(pb.new_item_code.isin(parent_items) & (IfNull(pb.disabled, 0) == 0)) + .where(pb.new_item_code.isin(parent_items)) ).run(as_dict=1) child_items = set() From f3caed378b54a12e1ca4e993825d8638bdc9bbfd Mon Sep 17 00:00:00 2001 From: Mohammad Umair Sayed Date: Thu, 11 Jun 2026 11:50:00 +0530 Subject: [PATCH 17/88] fix(bom): fetch routing operations when Routing is selected (#55813) fix(bom): fetch routing operations when routing is selected frm.doc.operations is always an array in Frappe, so !frm.doc.operations was always false (empty array [] is truthy in JS), causing get_routing() to never fire when a Routing is selected on a BOM with no existing operations. Changed the guard to !frm.doc.operations.length so the fetch triggers correctly when the operations table is empty. Also wired the same fetch into the with_operations handler so that enabling the checkbox after a Routing is already set will populate operations without requiring the user to re-select the Routing. Co-authored-by: Umair Sayed (cherry picked from commit 9249fa89aa6bd846b94292ca22e1b6bdf6d4e4c8) --- erpnext/manufacturing/doctype/bom/bom.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 2bf7e34bd4e..9fbe4f1174c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -175,6 +175,9 @@ frappe.ui.form.on("BOM", { with_operations: function (frm) { frm.set_df_property("fg_based_operating_cost", "hidden", frm.doc.with_operations ? 1 : 0); frm.trigger("toggle_fields_for_semi_finished_goods"); + if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations.length) { + frm.trigger("routing"); + } }, fg_based_operating_cost: function (frm) { @@ -583,7 +586,7 @@ frappe.ui.form.on("BOM", { }, routing(frm) { - if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations) { + if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations.length) { frappe.call({ doc: frm.doc, method: "get_routing", From b2e7fd7957a257c6e201a01c4b6ed4c8dd8aca8e Mon Sep 17 00:00:00 2001 From: Shllokkk <140623894+Shllokkk@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:51:44 +0530 Subject: [PATCH 18/88] fix: remove ignore_permissions from get_party_details signature (#55491) (cherry picked from commit efb8336bf89b6bbf89d22e3e786e32571c798b1a) --- .../doctype/sales_invoice/sales_invoice.py | 2 +- erpnext/accounts/party.py | 22 ++++++++++++++++--- .../request_for_quotation.py | 4 ++-- .../buying/doctype/supplier/test_supplier.py | 6 ++--- erpnext/controllers/buying_controller.py | 4 ++-- .../selling/doctype/customer/test_customer.py | 12 +++++----- .../customer_wise_item_price.py | 4 ++-- 7 files changed, 35 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index a7967de15a2..856d6d01a2d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2994,7 +2994,7 @@ def update_taxes( master_doctype=None, ): # Update Party Details - party_details = get_party_details( + party_details = _get_party_details( party=party, party_type=party_type, company=company, diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index a40d1eb49aa..517b8cd412d 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -74,6 +74,7 @@ class DuplicatePartyAccountError(frappe.ValidationError): @frappe.whitelist() def get_party_details( +<<<<<<< HEAD party=None, account=None, party_type="Customer", @@ -90,11 +91,26 @@ def get_party_details( shipping_address=None, dispatch_address=None, pos_profile=None, +======= + party: str | None = None, + account: str | None = None, + party_type: str = "Customer", + company: str | None = None, + posting_date: str | None = None, + bill_date: str | None = None, + price_list: str | None = None, + currency: str | None = None, + doctype: str | None = None, + fetch_payment_terms_template: bool = True, + party_address: str | None = None, + company_address: str | None = None, + shipping_address: str | None = None, + dispatch_address: str | None = None, + pos_profile: str | None = None, +>>>>>>> efb8336bf8 (fix: remove ignore_permissions from get_party_details signature (#55491)) ): if not party: return frappe._dict() - if not frappe.db.exists(party_type, party): - frappe.throw(_("{0}: {1} does not exists").format(party_type, party)) return _get_party_details( party, account, @@ -105,7 +121,7 @@ def get_party_details( price_list, currency, doctype, - ignore_permissions, + False, fetch_payment_terms_template, party_address, company_address, 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 51e89e37636..2aefbded9d2 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -15,7 +15,7 @@ from frappe.utils import get_url from frappe.utils.print_format import download_pdf from frappe.utils.user import get_user_fullname -from erpnext.accounts.party import get_party_account_currency, get_party_details +from erpnext.accounts.party import _get_party_details, get_party_account_currency from erpnext.buying.utils import validate_for_items from erpnext.controllers.buying_controller import BuyingController from erpnext.stock.doctype.material_request.material_request import set_missing_values @@ -447,7 +447,7 @@ def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier= def postprocess(source, target_doc): if for_supplier: target_doc.supplier = for_supplier - args = get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True) + args = _get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True) target_doc.currency = args.currency or get_party_account_currency( "Supplier", for_supplier, source.company ) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 6a7675ffba9..ecdf85a3f89 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -119,12 +119,12 @@ class TestSupplier(ERPNextTestSuite): self.assertEqual(supplier.country, "Greece") def test_party_details_tax_category(self): - from erpnext.accounts.party import get_party_details + from erpnext.accounts.party import _get_party_details frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing") # Tax Category without Address - details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") + details = _get_party_details("_Test Supplier With Tax Category", party_type="Supplier") self.assertEqual(details.tax_category, "_Test Tax Category 1") address = frappe.get_doc( @@ -139,7 +139,7 @@ class TestSupplier(ERPNextTestSuite): ).insert() # Tax Category with Address - details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") + details = _get_party_details("_Test Supplier With Tax Category", party_type="Supplier") self.assertEqual(details.tax_category, "_Test Tax Category 2") # Rollback diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index e8b10211b0c..7aa722714b4 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -13,7 +13,7 @@ from frappe.utils.data import nowtime import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget -from erpnext.accounts.party import get_party_details +from erpnext.accounts.party import _get_party_details from erpnext.buying.utils import update_last_purchase_rate, validate_for_items from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.sales_and_purchase_return import get_rate_for_return @@ -218,7 +218,7 @@ class BuyingController(SubcontractingController): # set contact and address details for supplier, if they are not mentioned if getattr(self, "supplier", None): self.update_if_missing( - get_party_details( + _get_party_details( self.supplier, party_type="Supplier", doctype=self.doctype, diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index e3efd4a5b21..1a09518b01b 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -53,7 +53,7 @@ class TestCustomer(ERPNextTestSuite): doc.delete() def test_party_details(self): - from erpnext.accounts.party import get_party_details + from erpnext.accounts.party import _get_party_details to_check = { "selling_price_list": None, @@ -75,7 +75,7 @@ class TestCustomer(ERPNextTestSuite): "Contact", "_Test Contact for _Test Customer-_Test Customer", "is_primary_contact", 1 ) - details = get_party_details("_Test Customer") + details = _get_party_details("_Test Customer") for key, value in to_check.items(): val = details.get(key) @@ -85,10 +85,10 @@ class TestCustomer(ERPNextTestSuite): self.assertEqual(value, val) def test_party_details_tax_category(self): - from erpnext.accounts.party import get_party_details + from erpnext.accounts.party import _get_party_details # Tax Category without Address - details = get_party_details("_Test Customer With Tax Category") + details = _get_party_details("_Test Customer With Tax Category") self.assertEqual(details.tax_category, "_Test Tax Category 1") frappe.get_doc( @@ -120,13 +120,13 @@ class TestCustomer(ERPNextTestSuite): # Tax Category from Billing Address settings.determine_address_tax_category_from = "Billing Address" settings.save() - details = get_party_details("_Test Customer With Tax Category") + details = _get_party_details("_Test Customer With Tax Category") self.assertEqual(details.tax_category, "_Test Tax Category 2") # Tax Category from Shipping Address settings.determine_address_tax_category_from = "Shipping Address" settings.save() - details = get_party_details("_Test Customer With Tax Category") + details = _get_party_details("_Test Customer With Tax Category") self.assertEqual(details.tax_category, "_Test Tax Category 3") # Rollback diff --git a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py index d9caa9b8bad..f6783abfbe5 100644 --- a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py +++ b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py @@ -7,7 +7,7 @@ from frappe import _, qb from frappe.query_builder import Criterion from erpnext import get_default_company -from erpnext.accounts.party import get_party_details +from erpnext.accounts.party import _get_party_details def execute(filters=None): @@ -125,7 +125,7 @@ def get_data(filters=None): def get_customer_details(filters): - customer_details = get_party_details(party=filters.get("customer"), party_type="Customer") + customer_details = _get_party_details(party=filters.get("customer"), party_type="Customer") customer_details.update( {"company": get_default_company(), "price_list": customer_details.get("selling_price_list")} ) From c06046df8f05a778dee38ca4fa4da2f7e53f4677 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:12:27 +0000 Subject: [PATCH 19/88] fix: added doctype filter validation for sales person wise transaction summary report (backport #55812) (#55818) Co-authored-by: Diptanil Saha fix: added doctype filter validation for sales person wise transaction summary report (#55812) --- .../sales_person_wise_transaction_summary.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index f8cde141fe4..405159215cd 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -13,6 +13,8 @@ def execute(filters=None): if not filters: filters = {} + validate_filters(filters) + columns = get_columns(filters) entries = get_entries(filters) item_details = get_item_details() @@ -49,10 +51,17 @@ def execute(filters=None): return columns, data -def get_columns(filters): +def validate_filters(filters): + ALLOWED_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note"] + if not filters.get("doc_type"): msgprint(_("Please select the document type first"), raise_exception=1) + if filters.get("doc_type") not in ALLOWED_DOCTYPES: + frappe.throw(_("{0}, {1} or {2} are the only allowed options.").format(*ALLOWED_DOCTYPES)) + + +def get_columns(filters): columns = [ { "label": _(filters["doc_type"]), From 7e3be8c8c01f5f81faf25127f1962cbc1850fe31 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 9 Jun 2026 11:27:23 +0530 Subject: [PATCH 20/88] chore: resolve conflict --- .../doctype/sales_invoice/sales_invoice.py | 2 +- erpnext/accounts/party.py | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 856d6d01a2d..a7967de15a2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2994,7 +2994,7 @@ def update_taxes( master_doctype=None, ): # Update Party Details - party_details = _get_party_details( + party_details = get_party_details( party=party, party_type=party_type, company=company, diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 517b8cd412d..c97c1a9c1cd 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -74,7 +74,6 @@ class DuplicatePartyAccountError(frappe.ValidationError): @frappe.whitelist() def get_party_details( -<<<<<<< HEAD party=None, account=None, party_type="Customer", @@ -91,23 +90,6 @@ def get_party_details( shipping_address=None, dispatch_address=None, pos_profile=None, -======= - party: str | None = None, - account: str | None = None, - party_type: str = "Customer", - company: str | None = None, - posting_date: str | None = None, - bill_date: str | None = None, - price_list: str | None = None, - currency: str | None = None, - doctype: str | None = None, - fetch_payment_terms_template: bool = True, - party_address: str | None = None, - company_address: str | None = None, - shipping_address: str | None = None, - dispatch_address: str | None = None, - pos_profile: str | None = None, ->>>>>>> efb8336bf8 (fix: remove ignore_permissions from get_party_details signature (#55491)) ): if not party: return frappe._dict() From d51ad0d19f5b12558be1140fa47c24e46bc3ca97 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 11 Jun 2026 14:54:45 +0530 Subject: [PATCH 21/88] fix: create_raw_materials_supplied method not found --- erpnext/buying/doctype/purchase_order/purchase_order.py | 2 +- erpnext/controllers/accounts_controller.py | 2 +- erpnext/controllers/buying_controller.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 80b3d83f35a..049d4352ae3 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -218,7 +218,7 @@ class PurchaseOrder(BuyingController): if self.is_old_subcontracting_flow: self.validate_bom_for_subcontracting_items() - self.create_raw_materials_supplied() + self.create_raw_materials_supplied_or_received() self.validate_fg_item_for_subcontracting() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 49a950fb5bf..df827360e6e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -4213,7 +4213,7 @@ def update_child_qty_rate( if parent.is_old_subcontracting_flow: if should_update_supplied_items(parent): parent.update_reserved_qty_for_subcontract() - parent.create_raw_materials_supplied() + parent.create_raw_materials_supplied_or_received() parent.save() else: if not parent.can_update_items(): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index e8b10211b0c..3fd8110aaaf 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -63,7 +63,7 @@ class BuyingController(SubcontractingController): # sub-contracting self.validate_for_subcontracting() if self.get("is_old_subcontracting_flow"): - self.create_raw_materials_supplied() + self.create_raw_materials_supplied_or_received() self.set_landed_cost_voucher_amount() if self.doctype in ("Purchase Receipt", "Purchase Invoice"): From c10a331a22f0ce8b40c2e61d971290307d707d96 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 11 Jun 2026 17:11:44 +0530 Subject: [PATCH 22/88] fix: multiple issues related to BOM Creator (cherry picked from commit daf3f2e1420919699db43a801b248b0ca19be9a7) # Conflicts: # erpnext/manufacturing/doctype/bom_creator/bom_creator.py --- .../doctype/bom_creator/bom_creator.py | 63 ++++++++++++++----- .../doctype/bom_creator/test_bom_creator.py | 41 ++++++++++++ .../bom_configurator.bundle.js | 9 ++- 3 files changed, 97 insertions(+), 16 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 54d4fd48611..296fc597aad 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -245,10 +245,14 @@ class BOMCreator(Document): frappe.throw(_("Please set {0} in BOM Creator {1}").format(_(label), self.name)) def on_submit(self): - self.enqueue_create_boms() + self.enqueue_bom_creation() @frappe.whitelist() def enqueue_create_boms(self): + self.check_permission("submit") + self.enqueue_bom_creation() + + def enqueue_bom_creation(self): frappe.enqueue( self.create_boms, queue="short", @@ -395,7 +399,13 @@ class BOMCreator(Document): @frappe.whitelist() +<<<<<<< HEAD def get_children(doctype=None, parent=None, **kwargs): +======= +def get_children(parent: str | None = None, **kwargs): + frappe.has_permission("BOM Creator", "read", throw=True) + +>>>>>>> daf3f2e142 (fix: multiple issues related to BOM Creator) if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) @@ -431,6 +441,8 @@ def get_children(doctype=None, parent=None, **kwargs): @frappe.whitelist() def add_item(**kwargs): + frappe.has_permission("BOM Creator", "write", throw=True) + if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) @@ -463,6 +475,8 @@ def add_item(**kwargs): @frappe.whitelist() def add_sub_assembly(**kwargs): + frappe.has_permission("BOM Creator", "write", throw=True) + if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) @@ -552,39 +566,58 @@ def get_parent_row_no(doc, name): @frappe.whitelist() def delete_node(**kwargs): + frappe.has_permission("BOM Creator", "write", throw=True) + if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) - items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent) + updated = False if kwargs.docname: + if not frappe.db.exists("BOM Creator Item", {"name": kwargs.docname, "parent": kwargs.parent}): + frappe.throw(_("BOM Creator Item with name {0} does not exist").format(kwargs.docname)) + frappe.delete_doc("BOM Creator Item", kwargs.docname) + updated = True - for item in items: - frappe.delete_doc("BOM Creator Item", item.name) - if item.expandable: - delete_node(fg_item=item.value, parent=item.parent_id) + items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent) + if items: + for item in items: + updated = True + frappe.delete_doc("BOM Creator Item", item.name) + if item.expandable: + delete_node(fg_item=item.value, parent=item.parent_id) - doc = frappe.get_doc("BOM Creator", kwargs.parent) - doc.set_rate_for_items() - doc.save() + if updated: + doc = frappe.get_doc("BOM Creator", kwargs.parent) + doc.set_rate_for_items() + doc.save() - return doc + return doc + + return frappe._dict() @frappe.whitelist() -def edit_bom_creator(doctype: str, docname: str, data: str | dict, parent: str): - if not frappe.has_permission(doctype=doctype, ptype="write", parent_doctype="BOM Creator"): - frappe.throw(_("You do not have permission to edit this document"), frappe.PermissionError) +def edit_bom_creator(docname: str, data: str | dict, parent: str): + frappe.has_permission("BOM Creator", "write", throw=True) + + if not frappe.db.exists("BOM Creator Item", {"parent": parent, "name": docname}): + frappe.throw(_("BOM Creator Item with name {0} does not exist").format(docname)) if isinstance(data, str): data = frappe.parse_json(data) - frappe.db.set_value(doctype, docname, data) - doc = frappe.get_doc("BOM Creator", parent) + for row in doc.items: + if row.name == docname: + for key, value in data.items(): + if key in BOM_ITEM_FIELDS: + row.set(key, value) + break + doc.set_rate_for_items() doc.save() diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py index f7e7623b471..3c280f6baae 100644 --- a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py @@ -8,6 +8,8 @@ import frappe from erpnext.manufacturing.doctype.bom_creator.bom_creator import ( add_item, add_sub_assembly, + delete_node, + edit_bom_creator, ) from erpnext.stock.doctype.item.test_item import make_item from erpnext.tests.utils import ERPNextTestSuite @@ -251,6 +253,45 @@ class TestBOMCreator(ERPNextTestSuite): data = frappe.get_all("BOM", filters={"bom_creator": doc.name, "docstatus": 1}) self.assertEqual(len(data), 2) + def test_edit_and_delete_reject_unknown_item(self): + final_product = "Bicycle" + make_item( + final_product, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + doc = make_bom_creator( + name="Bicycle BOM Guarded", + company="_Test Company", + item_code=final_product, + qty=1, + rm_cosy_as_per="Valuation Rate", + currency="INR", + plc_conversion_rate=1, + conversion_rate=1, + ) + + # Editing a row that does not belong to this BOM Creator must be rejected. + self.assertRaises( + frappe.ValidationError, + edit_bom_creator, + docname="non-existent-row", + data={"qty": 5}, + parent=doc.name, + ) + + # Deleting a row that does not belong to this BOM Creator must be rejected. + self.assertRaises( + frappe.ValidationError, + delete_node, + parent=doc.name, + fg_item=final_product, + docname="non-existent-row", + ) + def create_items(): raw_materials = [ diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 49eee62e14d..33cf6f6f574 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -74,6 +74,7 @@ class BOMConfigurator { onload: function (me) { me.args["parent_id"] = frm_obj.frm.doc.name; me.args["parent"] = frm_obj.frm.doc.item_code; + delete me.args["doctype"]; me.parent = frm_obj.$wrapper.get(0); me.body = frm_obj.$wrapper.get(0); me.make_tree(); @@ -507,7 +508,6 @@ class BOMConfigurator { frappe.call({ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_bom_creator", args: { - doctype: doctype, docname: docname, data: data, parent: node.data.parent_id || this.frm.doc.name, @@ -540,6 +540,13 @@ class BOMConfigurator { } load_tree(response, node) { + // delete_node returns an empty response when nothing was removed; just + // refresh the node and bail out so we don't read undefined fields below. + if (!response?.message?.items) { + frappe.views.trees["BOM Configurator"].tree.load_children(node); + return; + } + let item_row = ""; let parent_dom = ""; let total_amount = response.message.raw_material_cost; From ca61e5a214c84130d1a2f950dd80d2ec5fa900f5 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Thu, 11 Jun 2026 13:13:09 +0530 Subject: [PATCH 23/88] fix: show user disable audit log (cherry picked from commit 73d1852773706efb6ec6305fd547f9c21c2643c3) --- erpnext/setup/doctype/employee/employee.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 1db49e7f8a4..282845bef85 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -150,15 +150,8 @@ class Employee(NestedSet): ) def validate_user_details(self): - if self.user_id: - data = frappe.db.get_value("User", self.user_id, ["enabled"], as_dict=1) - - if not data: - self.user_id = None - return - - self.validate_for_enabled_user_id(data.get("enabled", 0)) - self.validate_duplicate_user_id() + self.validate_for_enabled_user_id() + self.validate_duplicate_user_id() def validate_auto_user_creation(self): if self.create_user_automatically and not ( @@ -296,12 +289,15 @@ class Employee(NestedSet): if not self.relieving_date: throw(_("Please enter relieving date.")) - def validate_for_enabled_user_id(self, enabled): - if enabled is None: + def validate_for_enabled_user_id(self): + if not frappe.db.exists("User", self.user_id): frappe.throw(_("User {0} does not exist").format(self.user_id)) + user = frappe.get_doc("User", self.user_id) + enabled = user.enabled if self.status != "Active" and enabled or self.status == "Active" and enabled == 0: - frappe.db.set_value("User", self.user_id, "enabled", not enabled) + user.enabled = not enabled + user.save(ignore_permissions=True) def validate_duplicate_user_id(self): Employee = frappe.qb.DocType("Employee") From cf5e6da0a63846d7c1cc834c61841163edc2bed4 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 11 Jun 2026 19:52:47 +0530 Subject: [PATCH 24/88] chore: fix conflicts --- .../doctype/bom_creator/bom_creator.py | 56 +++++++++---------- .../bom_configurator.bundle.js | 5 +- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 296fc597aad..37cd9e02021 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -386,6 +386,30 @@ class BOMCreator(Document): production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name + @frappe.whitelist() + def edit_bom_creator(self, docname: str, data: str | dict): + frappe.has_permission("BOM Creator", "write", throw=True) + + if not frappe.db.exists("BOM Creator Item", {"parent": self.name, "name": docname}): + frappe.throw(_("BOM Creator Item with name {0} does not exist").format(docname)) + + if isinstance(data, str): + data = frappe.parse_json(data) + + for row in self.items: + if row.name == docname: + for key, value in data.items(): + if key in BOM_ITEM_FIELDS: + row.set(key, value) + break + + self.set_rate_for_items() + self.save() + + frappe.msgprint(_("Updated successfully"), alert=True) + + return self + def has_operations(self): for row in self.items: if row.operation: @@ -399,13 +423,9 @@ class BOMCreator(Document): @frappe.whitelist() -<<<<<<< HEAD -def get_children(doctype=None, parent=None, **kwargs): -======= -def get_children(parent: str | None = None, **kwargs): +def get_children(doctype: str | None = None, parent: str | None = None, **kwargs): frappe.has_permission("BOM Creator", "read", throw=True) ->>>>>>> daf3f2e142 (fix: multiple issues related to BOM Creator) if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) @@ -598,29 +618,3 @@ def delete_node(**kwargs): return doc return frappe._dict() - - -@frappe.whitelist() -def edit_bom_creator(docname: str, data: str | dict, parent: str): - frappe.has_permission("BOM Creator", "write", throw=True) - - if not frappe.db.exists("BOM Creator Item", {"parent": parent, "name": docname}): - frappe.throw(_("BOM Creator Item with name {0} does not exist").format(docname)) - - if isinstance(data, str): - data = frappe.parse_json(data) - - doc = frappe.get_doc("BOM Creator", parent) - for row in doc.items: - if row.name == docname: - for key, value in data.items(): - if key in BOM_ITEM_FIELDS: - row.set(key, value) - break - - doc.set_rate_for_items() - doc.save() - - frappe.msgprint(_("Updated successfully"), alert=True) - - return doc diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 33cf6f6f574..92960d967bc 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -74,7 +74,6 @@ class BOMConfigurator { onload: function (me) { me.args["parent_id"] = frm_obj.frm.doc.name; me.args["parent"] = frm_obj.frm.doc.item_code; - delete me.args["doctype"]; me.parent = frm_obj.$wrapper.get(0); me.body = frm_obj.$wrapper.get(0); me.make_tree(); @@ -506,11 +505,11 @@ class BOMConfigurator { let docname = node.data.name || this.frm.doc.name; frappe.call({ - method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_bom_creator", + method: "edit_bom_creator", + doc: me.frm.doc, args: { docname: docname, data: data, - parent: node.data.parent_id || this.frm.doc.name, }, callback: (r) => { for (let key in data) { From a83002aae60ad9d947a8e6c13a47de921aaf5943 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 11 Jun 2026 20:58:35 +0530 Subject: [PATCH 25/88] fix: sync employee user status after save --- erpnext/setup/doctype/employee/employee.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 282845bef85..b9def9a7df4 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -150,6 +150,9 @@ class Employee(NestedSet): ) def validate_user_details(self): + if not self.user_id: + return + self.validate_for_enabled_user_id() self.validate_duplicate_user_id() @@ -172,6 +175,7 @@ class Employee(NestedSet): if self.user_id: self.update_user() self.update_user_permissions() + self.update_user_status() self.reset_employee_emails_cache() def before_insert(self): @@ -293,10 +297,15 @@ class Employee(NestedSet): if not frappe.db.exists("User", self.user_id): frappe.throw(_("User {0} does not exist").format(self.user_id)) + def update_user_status(self): + if not self.user_id: + return + user = frappe.get_doc("User", self.user_id) enabled = user.enabled if self.status != "Active" and enabled or self.status == "Active" and enabled == 0: user.enabled = not enabled + # Keep linked User status in sync from the Employee lifecycle and record the audit log. user.save(ignore_permissions=True) def validate_duplicate_user_id(self): From 7f8c7d2f4459b3158726b869facc87a43229b3ff Mon Sep 17 00:00:00 2001 From: pandiyan Date: Thu, 11 Jun 2026 23:03:27 +0530 Subject: [PATCH 26/88] fix(stock): make uom mandatory in item uom table (cherry picked from commit a0177fdbe8db5b1217cf4747ac25c7dafb61acbe) --- .../doctype/uom_conversion_detail/uom_conversion_detail.json | 5 +++-- .../doctype/uom_conversion_detail/uom_conversion_detail.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.json b/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.json index 7bd92f326a1..2ab7f5e6600 100644 --- a/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.json +++ b/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.json @@ -18,7 +18,8 @@ "label": "UOM", "oldfieldname": "uom", "oldfieldtype": "Link", - "options": "UOM" + "options": "UOM", + "reqd": 1 }, { "fieldname": "conversion_factor", @@ -37,7 +38,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2026-04-27 02:22:52.652036", + "modified": "2026-06-11 23:02:54.800673", "modified_by": "Administrator", "module": "Stock", "name": "UOM Conversion Detail", diff --git a/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.py b/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.py index d73ba65ca95..3944e899029 100644 --- a/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.py +++ b/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.py @@ -18,7 +18,7 @@ class UOMConversionDetail(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data - uom: DF.Link | None + uom: DF.Link # end: auto-generated types pass From b61620684847bfa8b09edabaa5288ba15b07b0e4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 11 Jun 2026 22:23:24 +0530 Subject: [PATCH 27/88] fix: converted whitelist non class methods to class methods --- .../doctype/bom_creator/bom_creator.py | 292 +++++++++--------- .../doctype/bom_creator/test_bom_creator.py | 30 +- .../bom_configurator.bundle.js | 17 +- 3 files changed, 162 insertions(+), 177 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 37cd9e02021..9f3f1bed5a8 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -388,7 +388,7 @@ class BOMCreator(Document): @frappe.whitelist() def edit_bom_creator(self, docname: str, data: str | dict): - frappe.has_permission("BOM Creator", "write", throw=True) + frappe.has_permission("BOM Creator", "write", doc=self, throw=True) if not frappe.db.exists("BOM Creator Item", {"parent": self.name, "name": docname}): frappe.throw(_("BOM Creator Item with name {0} does not exist").format(docname)) @@ -396,15 +396,18 @@ class BOMCreator(Document): if isinstance(data, str): data = frappe.parse_json(data) + updated = False for row in self.items: if row.name == docname: for key, value in data.items(): - if key in BOM_ITEM_FIELDS: + if key in BOM_ITEM_FIELDS and row.get(key) != value: row.set(key, value) + updated = True break - self.set_rate_for_items() - self.save() + if updated: + self.set_rate_for_items() + self.save() frappe.msgprint(_("Updated successfully"), alert=True) @@ -421,6 +424,145 @@ class BOMCreator(Document): def get_default_bom(self, item_code) -> str: return frappe.get_cached_value("Item", item_code, "default_bom") + @frappe.whitelist() + def add_item(self, **kwargs): + frappe.has_permission("BOM Creator", "write", doc=self, throw=True) + + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + item_info = get_item_details(kwargs.item_code) + + parent_row_no = "" + if kwargs.fg_reference_id and self.name != kwargs.fg_reference_id: + parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id) + + kwargs.update( + { + "uom": item_info.stock_uom, + "stock_uom": item_info.stock_uom, + "conversion_factor": 1, + } + ) + + if parent_row_no: + kwargs.update({"parent_row_no": parent_row_no}) + + self.append("items", kwargs) + self.save() + + return self + + @frappe.whitelist() + def add_sub_assembly(self, **kwargs): + frappe.has_permission("BOM Creator", "write", doc=self, throw=True) + + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + bom_item = frappe.parse_json(kwargs.bom_item) + + name = kwargs.fg_reference_id + parent_row_no = "" + + if not kwargs.convert_to_sub_assembly: + item_info = get_item_details(bom_item.item_code) + parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id) + + item_row = self.append( + "items", + { + "item_code": bom_item.item_code, + "qty": bom_item.qty, + "uom": item_info.stock_uom, + "fg_item": kwargs.fg_item, + "conversion_factor": 1, + "parent_row_no": parent_row_no, + "fg_reference_id": name, + "stock_qty": bom_item.qty, + "do_not_explode": 1, + "is_expandable": 1, + "stock_uom": item_info.stock_uom, + "operation": bom_item.operation, + "is_phantom_item": sbool(kwargs.phantom), + }, + ) + + parent_row_no = item_row.idx + name = "" + else: + if sbool(kwargs.phantom): + parent_row = next(item for item in self.items if item.name == kwargs.fg_reference_id) + parent_row.db_set("is_phantom_item", 1) + parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id) + + for row in bom_item.get("items"): + row = frappe._dict(row) + item_info = get_item_details(row.item_code) + self.append( + "items", + { + "item_code": row.item_code, + "qty": row.qty, + "operation": row.operation, + "fg_item": bom_item.item_code, + "uom": item_info.stock_uom, + "fg_reference_id": name, + "parent_row_no": parent_row_no, + "conversion_factor": 1, + "do_not_explode": 1, + "stock_qty": row.qty, + "stock_uom": item_info.stock_uom, + }, + ) + + self.save() + + return self + + @frappe.whitelist() + def delete_node(self, **kwargs): + frappe.has_permission("BOM Creator", "write", doc=self, throw=True) + + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + updated = False + if kwargs.docname: + row = next((row for row in self.items if row.name == kwargs.docname), None) + if not row: + frappe.throw(_("BOM Creator Item with name {0} does not exist").format(kwargs.docname)) + + row.delete() + updated = True + + items = get_children(parent=kwargs.fg_item, parent_id=self.name) + if items: + for item in items: + updated = True + child_row = next((row for row in self.items if row.name == item.name), None) + if child_row: + child_row.delete() + if item.expandable: + self.delete_node(fg_item=item.value) + + if updated: + self.set_rate_for_items() + self.save() + + return self + + return frappe._dict() + @frappe.whitelist() def get_children(doctype: str | None = None, parent: str | None = None, **kwargs): @@ -459,112 +601,6 @@ def get_children(doctype: str | None = None, parent: str | None = None, **kwargs return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx") -@frappe.whitelist() -def add_item(**kwargs): - frappe.has_permission("BOM Creator", "write", throw=True) - - if isinstance(kwargs, str): - kwargs = frappe.parse_json(kwargs) - - if isinstance(kwargs, dict): - kwargs = frappe._dict(kwargs) - - doc = frappe.get_doc("BOM Creator", kwargs.parent) - item_info = get_item_details(kwargs.item_code) - - parent_row_no = "" - if kwargs.fg_reference_id and doc.name != kwargs.fg_reference_id: - parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id) - - kwargs.update( - { - "uom": item_info.stock_uom, - "stock_uom": item_info.stock_uom, - "conversion_factor": 1, - } - ) - - if parent_row_no: - kwargs.update({"parent_row_no": parent_row_no}) - - doc.append("items", kwargs) - doc.save() - - return doc - - -@frappe.whitelist() -def add_sub_assembly(**kwargs): - frappe.has_permission("BOM Creator", "write", throw=True) - - if isinstance(kwargs, str): - kwargs = frappe.parse_json(kwargs) - - if isinstance(kwargs, dict): - kwargs = frappe._dict(kwargs) - - doc = frappe.get_doc("BOM Creator", kwargs.parent) - bom_item = frappe.parse_json(kwargs.bom_item) - - name = kwargs.fg_reference_id - parent_row_no = "" - - if not kwargs.convert_to_sub_assembly: - item_info = get_item_details(bom_item.item_code) - parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id) - - item_row = doc.append( - "items", - { - "item_code": bom_item.item_code, - "qty": bom_item.qty, - "uom": item_info.stock_uom, - "fg_item": kwargs.fg_item, - "conversion_factor": 1, - "parent_row_no": parent_row_no, - "fg_reference_id": name, - "stock_qty": bom_item.qty, - "do_not_explode": 1, - "is_expandable": 1, - "stock_uom": item_info.stock_uom, - "operation": bom_item.operation, - "is_phantom_item": sbool(kwargs.phantom), - }, - ) - - parent_row_no = item_row.idx - name = "" - else: - if sbool(kwargs.phantom): - parent_row = next(item for item in doc.items if item.name == kwargs.fg_reference_id) - parent_row.db_set("is_phantom_item", 1) - parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id) - - for row in bom_item.get("items"): - row = frappe._dict(row) - item_info = get_item_details(row.item_code) - doc.append( - "items", - { - "item_code": row.item_code, - "qty": row.qty, - "operation": row.operation, - "fg_item": bom_item.item_code, - "uom": item_info.stock_uom, - "fg_reference_id": name, - "parent_row_no": parent_row_no, - "conversion_factor": 1, - "do_not_explode": 1, - "stock_qty": row.qty, - "stock_uom": item_info.stock_uom, - }, - ) - - doc.save() - - return doc - - def get_item_details(item_code): return frappe.get_cached_value( "Item", item_code, ["item_name", "description", "image", "stock_uom", "default_bom"], as_dict=1 @@ -582,39 +618,3 @@ def get_parent_row_no(doc, name): frappe.msgprint(_("Parent Row No not found for {0}").format(name), alert=True) return None - - -@frappe.whitelist() -def delete_node(**kwargs): - frappe.has_permission("BOM Creator", "write", throw=True) - - if isinstance(kwargs, str): - kwargs = frappe.parse_json(kwargs) - - if isinstance(kwargs, dict): - kwargs = frappe._dict(kwargs) - - updated = False - if kwargs.docname: - if not frappe.db.exists("BOM Creator Item", {"name": kwargs.docname, "parent": kwargs.parent}): - frappe.throw(_("BOM Creator Item with name {0} does not exist").format(kwargs.docname)) - - frappe.delete_doc("BOM Creator Item", kwargs.docname) - updated = True - - items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent) - if items: - for item in items: - updated = True - frappe.delete_doc("BOM Creator Item", item.name) - if item.expandable: - delete_node(fg_item=item.value, parent=item.parent_id) - - if updated: - doc = frappe.get_doc("BOM Creator", kwargs.parent) - doc.set_rate_for_items() - doc.save() - - return doc - - return frappe._dict() diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py index 3c280f6baae..94a8b0b607b 100644 --- a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py @@ -5,12 +5,6 @@ import random import frappe -from erpnext.manufacturing.doctype.bom_creator.bom_creator import ( - add_item, - add_sub_assembly, - delete_node, - edit_bom_creator, -) from erpnext.stock.doctype.item.test_item import make_item from erpnext.tests.utils import ERPNextTestSuite @@ -40,8 +34,7 @@ class TestBOMCreator(ERPNextTestSuite): conversion_rate=1, ) - add_sub_assembly( - parent=doc.name, + doc.add_sub_assembly( fg_item=final_product, fg_reference_id=doc.name, bom_item={ @@ -95,8 +88,7 @@ class TestBOMCreator(ERPNextTestSuite): conversion_rate=1, ) - add_item( - parent=doc.name, + doc.add_item( fg_item=final_product, fg_reference_id=doc.name, item_code="Pedal Assembly", @@ -139,8 +131,7 @@ class TestBOMCreator(ERPNextTestSuite): conversion_rate=1, ) - add_item( - parent=doc.name, + doc.add_item( fg_item=final_product, fg_reference_id=doc.name, item_code="Pedal Assembly", @@ -150,9 +141,8 @@ class TestBOMCreator(ERPNextTestSuite): doc.reload() self.assertEqual(doc.items[0].is_expandable, 0) - add_sub_assembly( + doc.add_sub_assembly( convert_to_sub_assembly=1, - parent=doc.name, fg_item=final_product, fg_reference_id=doc.items[0].name, bom_item={ @@ -207,8 +197,7 @@ class TestBOMCreator(ERPNextTestSuite): conversion_rate=1, ) - add_item( - parent=doc.name, + doc.add_item( fg_item=final_product, fg_reference_id=doc.name, item_code="Pedal Assembly", @@ -218,9 +207,8 @@ class TestBOMCreator(ERPNextTestSuite): doc.reload() self.assertEqual(doc.items[0].is_expandable, 0) - add_sub_assembly( + doc.add_sub_assembly( convert_to_sub_assembly=1, - parent=doc.name, fg_item=final_product, fg_reference_id=doc.items[0].name, bom_item={ @@ -277,17 +265,15 @@ class TestBOMCreator(ERPNextTestSuite): # Editing a row that does not belong to this BOM Creator must be rejected. self.assertRaises( frappe.ValidationError, - edit_bom_creator, + doc.edit_bom_creator, docname="non-existent-row", data={"qty": 5}, - parent=doc.name, ) # Deleting a row that does not belong to this BOM Creator must be rejected. self.assertRaises( frappe.ValidationError, - delete_node, - parent=doc.name, + doc.delete_node, fg_item=final_product, docname="non-existent-row", ) diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 92960d967bc..07871687006 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -240,9 +240,9 @@ class BOMConfigurator { } frappe.call({ - method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_item", + method: "add_item", + doc: this.frm.doc, args: { - parent: node.data.parent_id, fg_item: node.data.value, item_code: data.item_code, fg_reference_id: node.data.name || this.frm.doc.name, @@ -295,9 +295,9 @@ class BOMConfigurator { } frappe.call({ - method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly", + method: "add_sub_assembly", + doc: this.frm.doc, args: { - parent: node.data.parent_id, fg_item: node.data.value, fg_reference_id: node.data.name || this.frm.doc.name, bom_item: bom_item, @@ -442,9 +442,9 @@ class BOMConfigurator { } frappe.call({ - method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly", + method: "add_sub_assembly", + doc: this.frm.doc, args: { - parent: node.data.parent_id, fg_item: node.data.value, bom_item: bom_item, fg_reference_id: node.data.name || this.frm.doc.name, @@ -479,9 +479,9 @@ class BOMConfigurator { delete_node(node, view) { frappe.confirm(__("Are you sure you want to delete this Item?"), () => { frappe.call({ - method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.delete_node", + method: "delete_node", + doc: this.frm.doc, args: { - parent: node.data.parent_id, fg_item: node.data.value, doctype: node.data.doctype, docname: node.data.name, @@ -501,7 +501,6 @@ class BOMConfigurator { this.frm.edit_bom_dialog = frappe.prompt( fields, (data) => { - let doctype = node.data.doctype || this.frm.doc.doctype; let docname = node.data.name || this.frm.doc.name; frappe.call({ From 0fea93388d70106071672fa76c89ac98b2f51773 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 12 Jun 2026 13:01:44 +0530 Subject: [PATCH 28/88] fix: permissions in workstation file (cherry picked from commit cf127e89005fd45038870bac95feb7009e95f45f) # Conflicts: # erpnext/manufacturing/doctype/workstation/workstation.py --- .../doctype/workstation/workstation.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index eee5b5c0638..06aabd5bb0c 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -83,7 +83,7 @@ class Workstation(Document): def before_save(self): if self.has_value_changed("workstation_type"): - self.set_data_based_on_workstation_type() + self._set_data_based_on_workstation_type() self.set_hour_rate() self.set_total_working_hours() @@ -114,6 +114,10 @@ class Workstation(Document): @frappe.whitelist() def set_data_based_on_workstation_type(self): + self.check_permission("write") + self._set_data_based_on_workstation_type() + + def _set_data_based_on_workstation_type(self): if self.workstation_type: data = frappe.get_all( "Workstation Cost", @@ -211,6 +215,8 @@ class Workstation(Document): @frappe.whitelist() def start_job(self, job_card, from_time, employee): doc = frappe.get_doc("Job Card", job_card) + doc.check_permission("write") + doc.append("time_logs", {"from_time": from_time, "employee": employee}) doc.save(ignore_permissions=True) @@ -219,6 +225,8 @@ class Workstation(Document): @frappe.whitelist() def complete_job(self, job_card, qty, to_time): doc = frappe.get_doc("Job Card", job_card) + doc.check_permission("submit") + for row in doc.time_logs: if not row.to_time: row.to_time = to_time @@ -316,7 +324,13 @@ def get_status_color(status): @frappe.whitelist() +<<<<<<< HEAD def get_raw_materials(job_card): +======= +def get_raw_materials(job_card: str): + frappe.has_permission("Job Card", "read", doc=job_card, throw=True) + +>>>>>>> cf127e8900 (fix: permissions in workstation file) raw_materials = frappe.get_all( "Job Card", fields=[ @@ -460,6 +474,8 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime): @frappe.whitelist() def get_workstations(**kwargs): + frappe.has_permission("Workstation", "read", throw=True) + kwargs = frappe._dict(kwargs) _workstation = frappe.qb.DocType("Workstation") @@ -535,13 +551,8 @@ def update_job_card(job_card: str, method: str, **kwargs): title=_("Not Allowed"), ) - frappe.has_permission("Job Card", "read", throw=True) - doc = frappe.get_doc("Job Card", job_card) - - # These methods mutate the Job Card, but frappe.get_doc does not enforce permissions — - # require write access before running anything. - frappe.has_permission("Job Card", "write", doc=doc, throw=True) + doc.check_permission("write") if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -556,7 +567,13 @@ def update_job_card(job_card: str, method: str, **kwargs): @frappe.whitelist() +<<<<<<< HEAD def validate_job_card(job_card, status): +======= +def validate_job_card(job_card: str, status: str): + frappe.has_permission("Job Card", "read", doc=job_card, throw=True) + +>>>>>>> cf127e8900 (fix: permissions in workstation file) job_card_details = frappe.db.get_value("Job Card", job_card, ["status", "for_quantity"], as_dict=1) current_status = job_card_details.status From d04965b6b25e4f6079ca37270948a1639e69b1eb Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 12 Jun 2026 16:51:57 +0530 Subject: [PATCH 29/88] chore: fix conflicts --- erpnext/manufacturing/doctype/workstation/workstation.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 06aabd5bb0c..d2f908bddc8 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -324,13 +324,9 @@ def get_status_color(status): @frappe.whitelist() -<<<<<<< HEAD -def get_raw_materials(job_card): -======= def get_raw_materials(job_card: str): frappe.has_permission("Job Card", "read", doc=job_card, throw=True) ->>>>>>> cf127e8900 (fix: permissions in workstation file) raw_materials = frappe.get_all( "Job Card", fields=[ @@ -567,13 +563,9 @@ def update_job_card(job_card: str, method: str, **kwargs): @frappe.whitelist() -<<<<<<< HEAD -def validate_job_card(job_card, status): -======= def validate_job_card(job_card: str, status: str): frappe.has_permission("Job Card", "read", doc=job_card, throw=True) ->>>>>>> cf127e8900 (fix: permissions in workstation file) job_card_details = frappe.db.get_value("Job Card", job_card, ["status", "for_quantity"], as_dict=1) current_status = job_card_details.status From 11c7a35eaeda7e110817e4eeb71796a8fbe61145 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 12 Jun 2026 17:17:03 +0530 Subject: [PATCH 30/88] chore: fix linters issue --- erpnext/manufacturing/doctype/workstation/workstation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index d2f908bddc8..8d54667ea50 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -19,6 +19,7 @@ from frappe.utils import ( time_diff_in_seconds, to_timedelta, ) +from frappe.utils.data import DateTimeLikeObject from erpnext.support.doctype.issue.issue import get_holidays @@ -213,7 +214,7 @@ class Workstation(Document): return schedule_date @frappe.whitelist() - def start_job(self, job_card, from_time, employee): + def start_job(self, job_card: str, from_time: DateTimeLikeObject, employee: str): doc = frappe.get_doc("Job Card", job_card) doc.check_permission("write") @@ -223,7 +224,7 @@ class Workstation(Document): return doc @frappe.whitelist() - def complete_job(self, job_card, qty, to_time): + def complete_job(self, job_card: str, qty: float, to_time: DateTimeLikeObject): doc = frappe.get_doc("Job Card", job_card) doc.check_permission("submit") From 558415b1e7fd2b72f541b3d9137c7972e8459175 Mon Sep 17 00:00:00 2001 From: SandraFrappe Date: Fri, 12 Jun 2026 14:44:22 +0530 Subject: [PATCH 31/88] fix: pass source cost center to target cost center (cherry picked from commit 9ea766fc107fd294251dc00f390b6e833a8ab0f0) --- erpnext/controllers/sales_and_purchase_return.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 62eabb6f45a..51affeade12 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -598,6 +598,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai target_doc.so_detail = source_doc.so_detail target_doc.expense_account = source_doc.expense_account target_doc.dn_detail = source_doc.name + target_doc.cost_center = source_doc.cost_center if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return elif doctype == "Sales Invoice" or doctype == "POS Invoice": From 85e6b8d27b942bc0d115c9954e0be8ad49890060 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 09:04:23 +0000 Subject: [PATCH 32/88] fix: opportunity creation from contact us page (backport #55841) (#55867) * fix: opportunity creation from contact us page (#55841) (cherry picked from commit c933e34914e9ef0716451d4dc456e45d69364ff1) # Conflicts: # erpnext/templates/utils.py * chore: resolve conflicts --------- Co-authored-by: Diptanil Saha --- erpnext/crm/doctype/crm_settings/crm_settings.json | 10 ++++++++-- erpnext/crm/doctype/crm_settings/crm_settings.py | 13 +++++++++++++ erpnext/crm/utils.py | 5 +++++ erpnext/hooks.py | 3 +++ erpnext/templates/utils.py | 12 +++++++++++- 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json index 4760d504a57..b4f41fa6096 100644 --- a/erpnext/crm/doctype/crm_settings/crm_settings.json +++ b/erpnext/crm/doctype/crm_settings/crm_settings.json @@ -14,6 +14,7 @@ "opportunity_section", "close_opportunity_after_days", "column_break_9", + "enable_opportunity_creation_from_contact_us", "quotation_section", "default_valid_till", "section_break_13", @@ -98,15 +99,20 @@ "fieldname": "update_timestamp_on_new_communication", "fieldtype": "Check", "label": "Update timestamp on new communication" + }, + { + "default": "0", + "fieldname": "enable_opportunity_creation_from_contact_us", + "fieldtype": "Check", + "label": "Enable Opportunity Creation from Contact Us" } ], "grid_page_length": 50, - "hide_toolbar": 0, "icon": "fa fa-cog", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-03-16 13:28:19.573964", + "modified": "2026-06-11 23:09:49.750381", "modified_by": "Administrator", "module": "CRM", "name": "CRM Settings", diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py index 23992043145..01cdaf41bde 100644 --- a/erpnext/crm/doctype/crm_settings/crm_settings.py +++ b/erpnext/crm/doctype/crm_settings/crm_settings.py @@ -2,6 +2,7 @@ # For license information, please see license.txt import frappe +from frappe import _ from frappe.model.document import Document @@ -20,8 +21,20 @@ class CRMSettings(Document): carry_forward_communication_and_comments: DF.Check close_opportunity_after_days: DF.Int default_valid_till: DF.Data | None + enable_opportunity_creation_from_contact_us: DF.Check update_timestamp_on_new_communication: DF.Check # end: auto-generated types def validate(self): frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", "")) + self.validate_enable_opportunity_creation_from_contact_us() + + def validate_enable_opportunity_creation_from_contact_us(self): + contact_disabled = frappe.get_single_value("Contact Us Settings", "is_disabled") + + if self.enable_opportunity_creation_from_contact_us and contact_disabled: + frappe.throw( + _( + "Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled." + ) + ) diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py index 8a6ce8311b1..e68bfd8430e 100644 --- a/erpnext/crm/utils.py +++ b/erpnext/crm/utils.py @@ -5,6 +5,11 @@ from frappe.utils import cstr, now, today from pypika import functions +def disable_opportunity_creation_on_contact_us_disabled(doc, method): + if doc.is_disabled: + frappe.db.set_single_value("CRM Settings", "enable_opportunity_creation_from_contact_us", 0) + + def update_lead_phone_numbers(contact, method): if contact.phone_nos: contact_lead = contact.get_link_for("Lead") diff --git a/erpnext/hooks.py b/erpnext/hooks.py index ef05f059e22..bde6b5a7384 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -372,6 +372,9 @@ doc_events = { "Event": { "after_insert": "erpnext.crm.utils.link_events_with_prospect", }, + "Contact Us Settings": { + "on_update": "erpnext.crm.utils.disable_opportunity_creation_on_contact_us_disabled", + }, "Sales Invoice": { "on_submit": [ "erpnext.regional.italy.utils.sales_invoice_on_submit", diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py index 164a52f58c2..c65c1dccb79 100644 --- a/erpnext/templates/utils.py +++ b/erpnext/templates/utils.py @@ -3,10 +3,12 @@ import frappe +from frappe.rate_limiter import rate_limit from frappe.utils import escape_html -@frappe.whitelist(allow_guest=True) +@frappe.whitelist(allow_guest=True, methods=["POST"]) +@rate_limit(limit=10, seconds=3 * 60) def send_message(sender, message, subject="Website Query"): from frappe.www.contact import send_message as website_send_message @@ -14,6 +16,14 @@ def send_message(sender, message, subject="Website Query"): message = escape_html(message) + oppotunity_creation = frappe.get_single_value( + "CRM Settings", "enable_opportunity_creation_from_contact_us" + ) + + if not oppotunity_creation: + # Meant to silently fail instead of throwing error. + return + lead = customer = None customer = frappe.db.sql( """select distinct dl.link_name from `tabDynamic Link` dl From dd56e805126be2691df457c3fa53721d49995673 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 13 Jun 2026 17:41:48 +0530 Subject: [PATCH 33/88] fix: pemission for whitelist functions --- .../accounts/doctype/bank_clearance/bank_clearance.py | 1 + .../repost_accounting_ledger.py | 3 ++- erpnext/accounts/party.py | 3 ++- erpnext/crm/doctype/lead/lead.py | 3 +-- erpnext/crm/doctype/opportunity/opportunity.py | 6 ++++-- .../manufacturing/doctype/bom_creator/bom_creator.py | 10 +--------- .../manufacturing/doctype/workstation/workstation.py | 4 ++-- .../transaction_deletion_record.py | 2 ++ erpnext/stock/doctype/delivery_trip/delivery_trip.py | 8 ++++++-- erpnext/stock/doctype/shipment/shipment.py | 4 +++- .../incorrect_serial_and_batch_bundle.py | 5 ++++- .../subcontracting_inward_order.py | 4 +++- .../subcontracting_order/subcontracting_order.py | 4 +++- erpnext/support/doctype/issue/issue.py | 8 +++++--- 14 files changed, 39 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index f7451830e1d..112fdd0e84d 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -91,6 +91,7 @@ class BankClearance(Document): @frappe.whitelist() def update_clearance_date(self): + self.check_permission("write") invalid_document = [] invalid_cheque_date = [] entries_to_update = [] diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 807a1789881..8727aba48b0 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -154,12 +154,13 @@ class RepostAccountingLedger(Document): @frappe.whitelist() -def start_repost(account_repost_doc=str) -> None: +def start_repost(account_repost_doc: str | None = None) -> None: from erpnext.accounts.general_ledger import make_reverse_gl_entries frappe.flags.through_repost_accounting_ledger = True if account_repost_doc: repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc) + repost_doc.check_permission("write") if repost_doc.docstatus == 1: # Prevent repost on invoices with deferred accounting diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index c97c1a9c1cd..c70b6251ebb 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -508,7 +508,8 @@ def get_party_advance_account(party_type, party, company): @frappe.whitelist() -def get_party_bank_account(party_type, party): +def get_party_bank_account(party_type: str, party: str): + frappe.has_permission("Bank Account", "read", throw=True) return frappe.db.get_value("Bank Account", {"party_type": party_type, "party": party, "is_default": 1}) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 77ed755078b..f188b071655 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -481,7 +481,7 @@ def get_lead_details(lead, posting_date=None, company=None, doctype=None): @frappe.whitelist() -def make_lead_from_communication(communication, ignore_communication_links=False): +def make_lead_from_communication(communication: str, ignore_communication_links: bool = False): """raise a issue from email""" doc = frappe.get_doc("Communication", communication) @@ -500,7 +500,6 @@ def make_lead_from_communication(communication, ignore_communication_links=False } ) lead.flags.ignore_mandatory = True - lead.flags.ignore_permissions = True lead.insert() lead_name = lead.name diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index e96d57c8bb2..7368a800003 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -524,7 +524,9 @@ def auto_close_opportunity(): @frappe.whitelist() -def make_opportunity_from_communication(communication, company, ignore_communication_links=False): +def make_opportunity_from_communication( + communication: str, company: str, ignore_communication_links: bool = False +): from erpnext.crm.doctype.lead.lead import make_lead_from_communication doc = frappe.get_doc("Communication", communication) @@ -542,7 +544,7 @@ def make_opportunity_from_communication(communication, company, ignore_communica "opportunity_from": opportunity_from, "party_name": lead, } - ).insert(ignore_permissions=True) + ).insert() link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 9f3f1bed5a8..16198e653c6 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -388,8 +388,6 @@ class BOMCreator(Document): @frappe.whitelist() def edit_bom_creator(self, docname: str, data: str | dict): - frappe.has_permission("BOM Creator", "write", doc=self, throw=True) - if not frappe.db.exists("BOM Creator Item", {"parent": self.name, "name": docname}): frappe.throw(_("BOM Creator Item with name {0} does not exist").format(docname)) @@ -426,8 +424,6 @@ class BOMCreator(Document): @frappe.whitelist() def add_item(self, **kwargs): - frappe.has_permission("BOM Creator", "write", doc=self, throw=True) - if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) @@ -458,8 +454,6 @@ class BOMCreator(Document): @frappe.whitelist() def add_sub_assembly(self, **kwargs): - frappe.has_permission("BOM Creator", "write", doc=self, throw=True) - if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) @@ -499,7 +493,7 @@ class BOMCreator(Document): else: if sbool(kwargs.phantom): parent_row = next(item for item in self.items if item.name == kwargs.fg_reference_id) - parent_row.db_set("is_phantom_item", 1) + parent_row.is_phantom_item = 1 parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id) for row in bom_item.get("items"): @@ -528,8 +522,6 @@ class BOMCreator(Document): @frappe.whitelist() def delete_node(self, **kwargs): - frappe.has_permission("BOM Creator", "write", doc=self, throw=True) - if isinstance(kwargs, str): kwargs = frappe.parse_json(kwargs) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 8d54667ea50..98076e18074 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -219,7 +219,7 @@ class Workstation(Document): doc.check_permission("write") doc.append("time_logs", {"from_time": from_time, "employee": employee}) - doc.save(ignore_permissions=True) + doc.save() return doc @@ -234,7 +234,7 @@ class Workstation(Document): row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60 row.completed_qty = qty - doc.save(ignore_permissions=True) + doc.save() doc.submit() return doc diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index e77316eb957..92403d51f44 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -640,6 +640,8 @@ class TransactionDeletionRecord(Document): @frappe.whitelist() def start_deletion_tasks(self): + self.check_permission("write") + # This method is the entry point for the chain of events that follow self.db_set("status", "Running") self._set_deletion_cache() diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index ff5b30b2318..07359df830f 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -367,7 +367,9 @@ def get_default_address(out, name): @frappe.whitelist() -def get_contact_display(contact): +def get_contact_display(contact: str): + frappe.has_permission("Contact", "read", doc=contact, throw=True) + contact_info = frappe.db.get_value( "Contact", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1 ) @@ -469,7 +471,9 @@ def get_attachments(delivery_stop): @frappe.whitelist() -def get_driver_email(driver): +def get_driver_email(driver: str): + frappe.has_permission("Driver", "read", doc=driver, throw=True) + employee = frappe.db.get_value("Driver", driver, "employee") email = frappe.db.get_value("Employee", employee, "prefered_email") return {"email": email} diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index ae5a4214d24..782741241ba 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -126,7 +126,9 @@ def get_contact_name(ref_doctype, docname): @frappe.whitelist() -def get_company_contact(user): +def get_company_contact(user: str): + frappe.has_permission("User", "read", throw=True) + contact = frappe.db.get_value( "User", user, diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py index 0b27d697a4d..2a9640bab9e 100644 --- a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py @@ -134,12 +134,15 @@ def get_linked_cancelled_sabb(filters): @frappe.whitelist() -def fix_sabb_entries(selected_rows): +def fix_sabb_entries(selected_rows: str | list): + frappe.has_permission("Serial and Batch Bundle", "write", throw=True) + if isinstance(selected_rows, str): selected_rows = frappe.parse_json(selected_rows) for row in selected_rows: doc = frappe.get_doc("Serial and Batch Bundle", row.get("name")) + doc.check_permission("write") if doc.is_cancelled == 0 and not frappe.db.get_value( "Stock Ledger Entry", {"serial_and_batch_bundle": doc.name, "is_cancelled": 0}, diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py index aea08e18b34..79f2ed33ed2 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py @@ -3,6 +3,7 @@ import frappe from frappe import _ +from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.utils import comma_and, flt, get_link_to_form @@ -550,8 +551,9 @@ class SubcontractingInwardOrder(SubcontractingController): @frappe.whitelist() -def update_subcontracting_inward_order_status(scio, status=None): +def update_subcontracting_inward_order_status(scio: str | Document, status: str | None = None): if isinstance(scio, str): scio = frappe.get_doc("Subcontracting Inward Order", scio) + scio.check_permission("write") scio.update_status(status) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 40de8eb39d4..29233f68195 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -3,6 +3,7 @@ import frappe from frappe import _ +from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.utils import flt @@ -483,8 +484,9 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None, items=None): @frappe.whitelist() -def update_subcontracting_order_status(sco, status=None): +def update_subcontracting_order_status(sco: str | Document, status: str | None = None): if isinstance(sco, str): sco = frappe.get_doc("Subcontracting Order", sco) + sco.check_permission("write") sco.update_status(status) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index faa12bd5419..c35b76cf37d 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -118,7 +118,9 @@ class Issue(Document): communication.save() @frappe.whitelist() - def split_issue(self, subject, communication_id): + def split_issue(self, subject: str, communication_id: str): + self.check_permission("write") + # Bug: Pressing enter doesn't send subject from copy import deepcopy @@ -274,7 +276,7 @@ def make_task(source_name, target_doc=None): @frappe.whitelist() -def make_issue_from_communication(communication, ignore_communication_links=False): +def make_issue_from_communication(communication: str, ignore_communication_links: bool = False): """raise a issue from email""" doc = frappe.get_doc("Communication", communication) @@ -286,7 +288,7 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals "raised_by": doc.sender or "", "raised_by_phone": doc.phone_no or "", } - ).insert(ignore_permissions=True) + ).insert() link_communication_to_document(doc, "Issue", issue.name, ignore_communication_links) From c3b66bce1eec7dce2b267eac6eb98732553120eb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 13 Jun 2026 19:09:04 +0530 Subject: [PATCH 34/88] fix: permission in bom compare tool (cherry picked from commit e6fdb3702aa0fe017f498727fac8286074802471) --- .../bom_comparison_tool/bom_comparison_tool.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js b/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js index fcb7e884ecc..753fb4e896d 100644 --- a/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js +++ b/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js @@ -96,8 +96,8 @@ erpnext.BOMComparisonTool = class BOMComparisonTool { return ` ${frappe.meta.get_label(doctype, fieldname)} - ${value1} - ${value2} + ${frappe.utils.escape_html(cstr(value1))} + ${frappe.utils.escape_html(cstr(value2))} `; }) @@ -138,13 +138,17 @@ erpnext.BOMComparisonTool = class BOMComparisonTool { .map((change, i) => { let [fieldname, value1, value2] = change; let th = - i === 0 ? `${item_code}` : ""; + i === 0 + ? `${frappe.utils.escape_html( + cstr(item_code) + )}` + : ""; return ` ${th} ${frappe.meta.get_label(child_doctype, fieldname)} - ${value1} - ${value2} + ${frappe.utils.escape_html(cstr(value1))} + ${frappe.utils.escape_html(cstr(value2))} `; }) @@ -177,7 +181,9 @@ erpnext.BOMComparisonTool = class BOMComparisonTool { let html = rows .map((row) => { let [, doc] = row; - let cells = fields.map((df) => `${doc[df.fieldname]}`).join(""); + let cells = fields + .map((df) => `${frappe.utils.escape_html(cstr(doc[df.fieldname]))}`) + .join(""); return `${cells}`; }) .join(""); From 12be63229aad2973c85a43f7c8fda59fb36b9bd2 Mon Sep 17 00:00:00 2001 From: Dipen Gala <123350348+DipenFrappe@users.noreply.github.com> Date: Sat, 13 Jun 2026 20:48:03 +0530 Subject: [PATCH 35/88] feat(invoices): add tooltip description to Update Stock checkbox (#55868) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(invoices): add tooltip description to Update Stock checkbox Adds a description below the Update Stock checkbox on both Sales Invoice and Purchase Invoice so users understand when to use the field without consulting documentation. Co-Authored-By: Claude Sonnet 4.6 * feat(invoices): replace Update Stock description with hover info tooltip Removes the inline description text and adds an ℹ icon next to the Update Stock checkbox label on both Sales Invoice and Purchase Invoice. Hovering the icon shows the contextual tooltip via Bootstrap tooltip. Co-Authored-By: Claude Sonnet 4.6 * fix(invoices): use Frappe native tooltip-content class for Update Stock icon Replace Bootstrap .tooltip() (pure black bg) with Frappe's own .tooltip-content CSS class so the hover tooltip matches the rest of the ERPNext UI — uses var(--bg-dark-gray) and var(--text-dark). Co-Authored-By: Claude Sonnet 4.6 * fix(invoices): use frappe.ui.SidebarCard for Update Stock info tooltip Replace custom CSS tooltip with the same SidebarCard + Popper approach Frappe's InfoCard uses for field description tooltips — gives the native ERPNext card appearance (white card, border, shadow) on hover. Co-Authored-By: Claude Sonnet 4.6 * refactor(invoices): use built-in field description for Update Stock tooltip Replace custom SidebarCard JS tooltip with Frappe's native description + show_description_on_click field property on the update_stock field in Sales Invoice and Purchase Invoice. Co-Authored-By: Claude Sonnet 4.6 * fix: remove duplicate description in purchase_invoice update_stock field Co-Authored-By: Claude Sonnet 4.6 * revert: restore custom tooltip in purchase_invoice.js Co-Authored-By: Claude Sonnet 4.6 * revert: remove all changes from purchase_invoice.js Keep purchase_invoice.js identical to upstream develop. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 (cherry picked from commit a9029f83c7dc5e0c53104802baffde9b5860ba20) --- .../accounts/doctype/purchase_invoice/purchase_invoice.json | 6 ++++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 557c2940ef5..e26b5f5bbf9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -606,10 +606,12 @@ { "default": "0", "depends_on": "eval:doc.items.every((item) => !item.pr_detail)", + "description": "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Purchase Receipt is created separately.", "fieldname": "update_stock", "fieldtype": "Check", "label": "Update Stock", - "print_hide": 1 + "print_hide": 1, + "show_description_on_click": 1 }, { "fieldname": "scan_barcode", @@ -1698,7 +1700,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2026-05-28 12:36:55.215363", + "modified": "2026-06-13 18:36:46.704623", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 5c9289d3b51..dd09e7b1fd8 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -717,6 +717,7 @@ { "default": "0", "depends_on": "eval:doc.items.every((item) => !item.dn_detail)", + "description": "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Delivery Note is created separately.", "fieldname": "update_stock", "fieldtype": "Check", "hide_days": 1, @@ -724,7 +725,8 @@ "label": "Update Stock", "oldfieldname": "update_stock", "oldfieldtype": "Check", - "print_hide": 1 + "print_hide": 1, + "show_description_on_click": 1 }, { "fieldname": "scan_barcode", From 0b03f18a39b146b3591fec2d35c3d0ed61e8661a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 18:24:32 +0000 Subject: [PATCH 36/88] fix(Lead): stop storing Gravatar image URLs for Leads (backport #55880) (#55882) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix(Lead): stop storing Gravatar image URLs for Leads (#55880) --- erpnext/crm/doctype/lead/lead.py | 5 +---- erpnext/templates/includes/projects.css | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index f188b071655..69f7c31817d 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -11,7 +11,7 @@ from frappe.contacts.doctype.address.address import get_default_address from frappe.contacts.doctype.contact.contact import get_default_contact from frappe.email.inbox import link_communication_to_document from frappe.model.mapper import get_mapped_doc -from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address +from frappe.utils import comma_and, get_link_to_form, validate_email_address from erpnext.accounts.party import set_taxes from erpnext.controllers.selling_controller import SellingController @@ -175,9 +175,6 @@ class Lead(SellingController, CRMNote): if self.email_id == self.lead_owner: frappe.throw(_("Lead Owner cannot be same as the Lead Email Address")) - if self.is_new() or not self.image: - self.image = has_gravatar(self.email_id) - def link_to_contact(self): # update contact links if self.contact_doc: diff --git a/erpnext/templates/includes/projects.css b/erpnext/templates/includes/projects.css index 5d9fc50385e..0ee177442c9 100644 --- a/erpnext/templates/includes/projects.css +++ b/erpnext/templates/includes/projects.css @@ -79,10 +79,6 @@ padding: 8px; } -.gravatar-top{ - margin-top:8px; -} - .progress-hg{ margin-bottom: 30!important; height:2px; From d69464ebdf09fc22ea8e7f5a02ced1fc3297aff0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:06:13 +0000 Subject: [PATCH 37/88] ci: set disabledLabels and context for greptile (backport #55883) (#55885) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .greptile/config.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .greptile/config.json diff --git a/.greptile/config.json b/.greptile/config.json new file mode 100644 index 00000000000..8d9c41c662e --- /dev/null +++ b/.greptile/config.json @@ -0,0 +1,10 @@ +{ + "disabledLabels": [ + "conflicts" + ], + "context": { + "repos": [ + "frappe/frappe" + ] + } +} From ece43bd79b9705b9a95507117279c102c654c647 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 13 Jun 2026 19:32:39 +0530 Subject: [PATCH 38/88] feat: Allow to edit stock UOM qty for Stock Entry (cherry picked from commit b0e9ad198fc2384de7d915f670c634a7743b1b48) --- .../stock/doctype/stock_entry/stock_entry.js | 31 +++++++++++++++++++ .../stock_settings/stock_settings.json | 10 +++++- .../doctype/stock_settings/stock_settings.py | 20 +++++++++++- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 3688b38aef1..3a417c570c9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -9,6 +9,8 @@ frappe.ui.form.on("Stock Entry", { setup: function (frm) { frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; + frm.trigger("toggle_enable_for_stock_uom_qty"); + frm.set_indicator_formatter("item_code", function (doc) { if (!doc.s_warehouse) { return "blue"; @@ -276,6 +278,20 @@ frappe.ui.form.on("Stock Entry", { }); }, + toggle_enable_for_stock_uom_qty: function (frm) { + frappe.call({ + method: "erpnext.stock.doctype.stock_settings.stock_settings.get_enable_stock_uom_editing", + callback: (r) => { + if (r.message) { + frm.fields_dict["items"].grid.toggle_enable( + "transfer_qty", + r.message.allow_to_edit_stock_uom_qty_for_stock_entry + ); + } + }, + }); + }, + refresh: function (frm) { frm.trigger("get_items_from_transit_entry"); frm.trigger("toggle_warehouse_fields"); @@ -1016,6 +1032,21 @@ frappe.ui.form.on("Stock Entry Detail", { frm.events.set_basic_rate(frm, cdt, cdn); }, + transfer_qty(frm, cdt, cdn) { + let item = locals[cdt][cdn]; + let old_conversion_factor = item.conversion_factor; + let conversion_factor = 1.0; + if (flt(item.qty) && flt(item.transfer_qty)) { + conversion_factor = flt(item.transfer_qty) / flt(item.qty); + } + + if (old_conversion_factor !== conversion_factor) { + item.conversion_factor = conversion_factor; + refresh_field("conversion_factor", item.name, item.parentfield); + frm.events.set_basic_rate(frm, cdt, cdn); + } + }, + s_warehouse(frm, cdt, cdn) { frm.events.get_warehouse_details(frm, cdt, cdn); diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index fd6fb21adfb..f9b46cf6e4f 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -21,6 +21,7 @@ "stock_uom", "allow_to_edit_stock_uom_qty_for_sales", "allow_to_edit_stock_uom_qty_for_purchase", + "allow_to_edit_stock_uom_qty_for_stock_entry", "allow_uom_with_conversion_rate_defined_in_item", "warehouse_defaults_section", "default_warehouse", @@ -404,6 +405,13 @@ "fieldtype": "Check", "label": "Allow to edit stock UOM qty for Purchase documents" }, + { + "default": "0", + "documentation_url": "https://docs.frappe.io/erpnext/stock-settings#why-to-edit-stock-qty-qty-as-per-stock-uom", + "fieldname": "allow_to_edit_stock_uom_qty_for_stock_entry", + "fieldtype": "Check", + "label": "Allow to edit stock UOM qty for Stock Entry" + }, { "default": "0", "depends_on": "eval: doc.enable_stock_reservation", @@ -594,7 +602,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-06-03 12:38:02.202183", + "modified": "2026-06-13 12:38:02.202183", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 8250186dc6d..e7373802b1e 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -32,6 +32,7 @@ class StockSettings(Document): allow_partial_reservation: DF.Check allow_to_edit_stock_uom_qty_for_purchase: DF.Check allow_to_edit_stock_uom_qty_for_sales: DF.Check + allow_to_edit_stock_uom_qty_for_stock_entry: DF.Check allow_to_make_quality_inspection_after_purchase_or_delivery: DF.Check allow_uom_with_conversion_rate_defined_in_item: DF.Check auto_create_serial_and_batch_bundle_for_outward: DF.Check @@ -112,6 +113,7 @@ class StockSettings(Document): self.validate_auto_insert_price_list_rate_if_missing() self.change_precision_for_for_sales() self.change_precision_for_purchase() + self.change_precision_for_stock_entry() self.validate_do_not_use_batchwise_valuation() def validate_do_not_use_batchwise_valuation(self): @@ -290,6 +292,18 @@ class StockSettings(Document): ] self.make_property_setter_for_precision(doctypes) + def change_precision_for_stock_entry(self): + doc_before_save = self.get_doc_before_save() + if doc_before_save and ( + doc_before_save.allow_to_edit_stock_uom_qty_for_stock_entry + == self.allow_to_edit_stock_uom_qty_for_stock_entry + ): + return + + if self.allow_to_edit_stock_uom_qty_for_stock_entry: + doctypes = ["Stock Entry Detail"] + self.make_property_setter_for_precision(doctypes) + @staticmethod def make_property_setter_for_precision(doctypes): for doctype in doctypes: @@ -322,6 +336,10 @@ def clean_all_descriptions(): def get_enable_stock_uom_editing(): return frappe.get_single_value( "Stock Settings", - ["allow_to_edit_stock_uom_qty_for_sales", "allow_to_edit_stock_uom_qty_for_purchase"], + [ + "allow_to_edit_stock_uom_qty_for_sales", + "allow_to_edit_stock_uom_qty_for_purchase", + "allow_to_edit_stock_uom_qty_for_stock_entry", + ], as_dict=1, ) From 331715815c0490af84dc407f20584b5dcd8d4cf1 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Tue, 31 Mar 2026 15:07:25 +0530 Subject: [PATCH 39/88] feat: sticky columns in reports Co-authored-by: diptanilsaha (cherry picked from commit 03e4df7a1abc028503f887cdc2c10385066789d3) --- .../accounts_receivable/accounts_receivable.py | 14 ++++++++++++-- .../report/general_ledger/general_ledger.py | 9 ++++++++- .../item_wise_purchase_history.py | 2 ++ .../item_wise_sales_history.py | 1 + 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 56eec27c6d0..408f0262694 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1149,6 +1149,7 @@ class ReceivablePayableReport: fieldtype="Dynamic Link", options="party_type", width=180, + sticky=(self.party_naming_by not in ["Naming Series", "Auto Name"]), ) if self.account_type == "Receivable": label = _("Receivable Account") @@ -1163,6 +1164,7 @@ class ReceivablePayableReport: fieldtype="Link", options="Account", width=180, + sticky=True, ) if self.party_naming_by == "Naming Series": @@ -1176,6 +1178,7 @@ class ReceivablePayableReport: label=label, fieldname=fieldname, fieldtype="Data", + sticky=True, ) if self.account_type == "Receivable": @@ -1260,7 +1263,7 @@ class ReceivablePayableReport: if self.filters.show_remarks: self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200) - def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120): + def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120, sticky=False): if not fieldname: fieldname = scrub(label) if fieldtype == "Currency": @@ -1269,7 +1272,14 @@ class ReceivablePayableReport: width = 90 self.columns.append( - dict(label=label, fieldname=fieldname, fieldtype=fieldtype, options=options, width=width) + dict( + label=label, + fieldname=fieldname, + fieldtype=fieldtype, + options=options, + width=width, + sticky=sticky, + ) ) def setup_ageing_columns(self): diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 837b5ef9041..6f68ddd2c43 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -693,13 +693,20 @@ def get_columns(filters): "options": "GL Entry", "hidden": 1, }, - {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, + { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 120, + "sticky": True, + }, { "label": _("Account"), "fieldname": "account", "fieldtype": "Link", "options": "Account", "width": 180, + "sticky": True, }, { "label": _("Debit ({0})").format(currency), diff --git a/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py index a8950af3ea3..02c28b0114c 100644 --- a/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py +++ b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py @@ -28,6 +28,7 @@ def get_columns(filters): "fieldname": "item_code", "options": "Item", "width": 120, + "sticky": True, }, { "label": _("Item Name"), @@ -41,6 +42,7 @@ def get_columns(filters): "fieldname": "item_group", "options": "Item Group", "width": 120, + "sticky": True, }, { "label": _("Description"), diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 9cdb14caf46..c3e183c867e 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -29,6 +29,7 @@ def get_columns(filters): "fieldname": "item_code", "options": "Item", "width": 120, + "sticky": True, }, {"label": _("Item Name"), "fieldtype": "Data", "fieldname": "item_name", "width": 140}, { From bcd38c17d69cf7bd756bcac671f50f8e5b9f9cea Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Tue, 31 Mar 2026 15:34:46 +0530 Subject: [PATCH 40/88] fix: semgrep translation issue (cherry picked from commit df753676c647b37aa59b9d801e67ec7f8c0d0fd6) --- erpnext/accounts/report/general_ledger/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 6f68ddd2c43..8670a4fd175 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -748,7 +748,7 @@ def get_columns(filters): "options": "transaction_currency", }, { - "label": "Transaction Currency", + "label": _("Transaction Currency"), "fieldname": "transaction_currency", "fieldtype": "Link", "options": "Currency", From 70bb23d65ba0ba18c9fbdabca7fdff38832e3a7f Mon Sep 17 00:00:00 2001 From: MochaMind Date: Sun, 14 Jun 2026 16:39:56 +0530 Subject: [PATCH 41/88] chore: update POT file (#55893) --- erpnext/locale/main.pot | 1789 +++++++++++++++++++++------------------ 1 file changed, 965 insertions(+), 824 deletions(-) diff --git a/erpnext/locale/main.pot b/erpnext/locale/main.pot index b56303ca9b6..1e01a8f7d88 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-06-07 10:19+0000\n" -"PO-Revision-Date: 2026-06-07 10:19+0000\n" +"POT-Creation-Date: 2026-06-14 10:34+0000\n" +"PO-Revision-Date: 2026-06-14 10:34+0000\n" "Last-Translator: hello@frappe.io\n" "Language-Team: hello@frappe.io\n" "MIME-Version: 1.0\n" @@ -162,7 +162,7 @@ msgstr "" msgid "% Delivered" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1019 +#: erpnext/manufacturing/doctype/bom/bom.js:1022 #, python-format msgid "% Finished Item Quantity" msgstr "" @@ -279,7 +279,7 @@ msgstr "" msgid "'Based On' and 'Group By' can not be same" msgstr "" -#: erpnext/selling/report/inactive_customers/inactive_customers.py:21 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:23 msgid "'Days Since Last Order' must be greater than or equal to zero" msgstr "" @@ -313,9 +313,9 @@ msgstr "" msgid "'Inspection Required before Purchase' has disabled for the item {0}, no need to create the QI" msgstr "" -#: erpnext/stock/report/stock_ledger/stock_ledger.py:684 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:725 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:830 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:685 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:726 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:831 msgid "'Opening'" msgstr "" @@ -333,7 +333,7 @@ msgstr "" msgid "'Update Stock' can not be checked because items are not delivered via {0}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:428 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:434 msgid "'Update Stock' cannot be checked for fixed asset sale" msgstr "" @@ -607,8 +607,8 @@ msgstr "" msgid "90 Above" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1261 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1262 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1281 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1282 msgid "<0" msgstr "" @@ -784,7 +784,7 @@ msgstr "" msgid "" msgstr "" -#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:125 +#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:126 msgid "
  • Clearance date must be after cheque date for row(s): {0}
  • " msgstr "" @@ -796,11 +796,11 @@ msgstr "" msgid "
  • Packed Item {0}: Required {1}, Available {2}
  • " msgstr "" -#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:120 +#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:121 msgid "
  • Payment document required for row(s): {0}
  • " msgstr "" -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:163 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:164 #: erpnext/utilities/bulk_transaction.py:35 msgid "
  • {}
  • " msgstr "" @@ -809,7 +809,7 @@ msgstr "" msgid "

    Cannot overbill for the following Items:

    " msgstr "" -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:157 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:158 msgid "

    Following {0}s doesn't belong to Company {1} :

    " msgstr "" @@ -838,7 +838,7 @@ msgid "" "

    Apart from these, you can access all values in this RFQ, like {{ message_for_supplier }} or {{ terms }}.

    " msgstr "" -#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:118 +#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:119 msgid "

    Please correct the following row(s):

      " msgstr "" @@ -1039,6 +1039,11 @@ msgstr "" msgid "A customer must have primary contact email." msgstr "" +#. Description of the 'Disabled' (Check) field in DocType 'Product Bundle' +#: erpnext/selling/doctype/product_bundle/product_bundle.json +msgid "A disabled Product Bundle cannot be selected in transactions." +msgstr "" + #: erpnext/stock/doctype/delivery_trip/delivery_trip.py:59 msgid "A driver must be set to submit." msgstr "" @@ -1170,7 +1175,7 @@ msgstr "" msgid "Abbreviation: {0} must appear only once" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1258 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1278 msgid "Above" msgstr "" @@ -1248,7 +1253,7 @@ msgstr "" msgid "According to CEFACT/ICG/2010/IC013 or CEFACT/ICG/2010/IC010" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1159 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1264 msgid "According to the BOM {0}, the Item '{1}' is missing in the stock entry." msgstr "" @@ -1366,7 +1371,7 @@ msgstr "" msgid "Account Manager" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1040 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1050 #: erpnext/controllers/accounts_controller.py:2396 msgid "Account Missing" msgstr "" @@ -1418,7 +1423,7 @@ msgstr "" msgid "Account Paid To" msgstr "" -#: erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py:118 +#: erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py:120 msgid "Account Pay Only" msgstr "" @@ -1552,7 +1557,7 @@ msgstr "" msgid "Account {0} does not belong to company: {1}" msgstr "" -#: erpnext/accounts/doctype/account/account.py:588 +#: erpnext/accounts/doctype/account/account.py:590 msgid "Account {0} does not exist" msgstr "" @@ -1568,7 +1573,7 @@ msgstr "" msgid "Account {0} does not match with Company {1} in Mode of Account: {2}" msgstr "" -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:138 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:139 msgid "Account {0} doesn't belong to Company {1}" msgstr "" @@ -1620,7 +1625,7 @@ msgstr "" msgid "Account: {0} can only be updated via Stock Transactions" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2719 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2737 msgid "Account: {0} is not permitted under Payment Entry" msgstr "" @@ -1909,8 +1914,8 @@ msgstr "" msgid "Accounting Entry for Asset" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2143 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2163 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2248 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2268 msgid "Accounting Entry for LCV in Stock Entry {0}" msgstr "" @@ -1922,20 +1927,20 @@ msgstr "" msgid "Accounting Entry for Service" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1037 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1058 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1076 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1097 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1118 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1146 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1258 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1494 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1516 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1046 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1067 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1085 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1106 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1127 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1155 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1267 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1503 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1525 #: erpnext/controllers/stock_controller.py:733 #: erpnext/controllers/stock_controller.py:750 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:931 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2088 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2102 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2193 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2207 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:774 msgid "Accounting Entry for Stock" msgstr "" @@ -2666,7 +2671,7 @@ msgid "Add Quote" msgstr "" #. Label of the add_raw_materials (Button) field in DocType 'BOM Operation' -#: erpnext/manufacturing/doctype/bom/bom.js:1047 +#: erpnext/manufacturing/doctype/bom/bom.js:1050 #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json msgid "Add Raw Materials" msgstr "" @@ -3228,7 +3233,7 @@ msgstr "" msgid "Address used to determine Tax Category in transactions" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1160 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1179 msgid "Adjustment Against" msgstr "" @@ -3426,7 +3431,7 @@ msgstr "" msgid "Against Blanket Order" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1133 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1143 msgid "Against Customer Order {0}" msgstr "" @@ -3482,7 +3487,7 @@ msgid "Against Income Account" msgstr "" #: erpnext/accounts/doctype/journal_entry/journal_entry.py:739 -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:777 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:792 msgid "Against Journal Entry {0} does not have any unmatched {1} entry" msgstr "" @@ -3525,7 +3530,7 @@ msgstr "" msgid "Against Stock Entry" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:334 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:339 msgid "Against Supplier Invoice {0}" msgstr "" @@ -3570,11 +3575,11 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:154 #: erpnext/accounts/report/accounts_payable/accounts_payable.html:138 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:139 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1199 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1219 msgid "Age (Days)" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:259 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:265 msgid "Age ({0})" msgstr "" @@ -3672,7 +3677,7 @@ msgstr "" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:165 #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:185 #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:166 -#: erpnext/accounts/utils.py:1631 erpnext/public/js/setup_wizard.js:184 +#: erpnext/accounts/utils.py:1633 erpnext/public/js/setup_wizard.js:184 msgid "All Accounts" msgstr "" @@ -3696,7 +3701,7 @@ msgstr "" msgid "All Activities HTML" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:391 +#: erpnext/manufacturing/doctype/bom/bom.py:398 msgid "All BOMs" msgstr "" @@ -3848,7 +3853,7 @@ msgstr "" msgid "All items have already been received" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3429 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3580 msgid "All items have already been transferred for this Work Order." msgstr "" @@ -3856,11 +3861,11 @@ msgstr "" msgid "All items in this document already have a linked Quality Inspection." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1269 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1279 msgid "All items must be linked to a Sales Order or Subcontracting Inward Order for this Sales Invoice." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1280 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1290 msgid "All linked Sales Orders must be subcontracted." msgstr "" @@ -3959,11 +3964,11 @@ msgstr "" msgid "Allocated amount" msgstr "" -#: erpnext/accounts/utils.py:658 +#: erpnext/accounts/utils.py:659 msgid "Allocated amount cannot be greater than unadjusted amount" msgstr "" -#: erpnext/accounts/utils.py:656 +#: erpnext/accounts/utils.py:657 msgid "Allocated amount cannot be negative" msgstr "" @@ -4072,8 +4077,8 @@ msgstr "" #. Valuation' #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:215 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:227 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:217 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:229 msgid "Allow Negative Stock" msgstr "" @@ -4354,6 +4359,12 @@ msgstr "" msgid "Allow to edit stock UOM qty for Sales documents" msgstr "" +#. Label of the allow_to_edit_stock_uom_qty_for_stock_entry (Check) field in +#. DocType 'Stock Settings' +#: erpnext/stock/doctype/stock_settings/stock_settings.json +msgid "Allow to edit stock UOM qty for Stock Entry" +msgstr "" + #. Label of the allow_to_make_quality_inspection_after_purchase_or_delivery #. (Check) field in DocType 'Stock Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json @@ -4441,14 +4452,18 @@ msgstr "" msgid "Also you can't switch back to FIFO after setting the valuation method to Moving Average for this item." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:288 +#: erpnext/manufacturing/doctype/bom/bom.js:291 #: erpnext/manufacturing/doctype/work_order/work_order.js:158 #: erpnext/manufacturing/doctype/work_order/work_order.js:173 #: erpnext/public/js/utils.js:587 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:322 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:338 msgid "Alternate Item" msgstr "" +#: erpnext/stock/report/item_where_used/item_where_used.py:427 +msgid "Alternative For Item" +msgstr "" + #. Label of the alternative_item_code (Link) field in DocType 'Item #. Alternative' #: erpnext/stock/doctype/item_alternative/item_alternative.json @@ -4652,7 +4667,7 @@ msgstr "" #: erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py:164 #: erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py:43 #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py:68 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:109 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:118 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json #: erpnext/stock/doctype/landed_cost_item/landed_cost_item.json #: erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -4791,19 +4806,19 @@ msgstr "" msgid "Amount to Bill" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1259 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1274 msgid "Amount {0} {1} adjusted against {2} {3}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1270 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1285 msgid "Amount {0} {1} as adjustment to {2}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1234 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1249 msgid "Amount {0} {1} transferred from {2} to {3}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1240 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1255 msgid "Amount {0} {1} {2} {3}" msgstr "" @@ -5357,7 +5372,7 @@ msgstr "" msgid "As there are existing submitted transactions against item {0}, you can not change the value of {1}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:240 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:242 msgid "As there are reserved stock, you cannot disable {0}." msgstr "" @@ -5369,8 +5384,8 @@ msgstr "" msgid "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:214 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:226 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:216 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:228 msgid "As {0} is enabled, you can not enable {1}." msgstr "" @@ -5825,7 +5840,7 @@ msgstr "" msgid "Asset restored after Asset Capitalization {0} was cancelled" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1552 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1562 msgid "Asset returned" msgstr "" @@ -5837,8 +5852,8 @@ msgstr "" msgid "Asset scrapped via Journal Entry {0}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1552 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1555 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1562 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1565 msgid "Asset sold" msgstr "" @@ -5949,7 +5964,7 @@ msgstr "" msgid "Assets {assets_link} created for {item_code}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:709 +#: erpnext/manufacturing/doctype/job_card/job_card.js:713 msgid "Assign Job to Employee" msgstr "" @@ -5998,7 +6013,7 @@ msgid "At least one item should be entered with negative quantity in return docu msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:531 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:561 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:567 msgid "At least one mode of payment is required for POS invoice." msgstr "" @@ -6010,7 +6025,7 @@ msgstr "" msgid "At least one of the Selling or Buying must be selected" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:401 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:414 msgid "At least one raw material item must be present in the stock entry for the type {0}" msgstr "" @@ -6018,11 +6033,11 @@ msgstr "" msgid "At least one row is required for a financial report template" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:960 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:978 msgid "At least one warehouse is mandatory" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:863 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:881 msgid "At row #{0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account" msgstr "" @@ -6030,7 +6045,7 @@ msgstr "" msgid "At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:874 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:892 msgid "At row #{0}: you have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account" msgstr "" @@ -6058,7 +6073,7 @@ msgstr "" msgid "At row {0}: set Parent Row No for item {1}" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:225 +#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:226 msgid "Atleast one raw material for Finished Good Item {0} should be customer provided." msgstr "" @@ -6261,8 +6276,8 @@ msgstr "" msgid "Auto Reconciliation job trigger" msgstr "" -#: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py:150 -#: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py:198 +#: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py:153 +#: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py:201 msgid "Auto Reconciliation of Payments has been disabled. Enable it through {0}" msgstr "" @@ -6276,7 +6291,7 @@ msgstr "" msgid "Auto Tax Settings Error" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:170 +#: erpnext/setup/doctype/employee/employee.py:166 msgid "Auto User Creation Error" msgstr "" @@ -6454,7 +6469,7 @@ msgstr "" #: erpnext/public/js/utils.js:647 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json #: erpnext/stock/doctype/pick_list_item/pick_list_item.json -#: erpnext/stock/report/stock_ageing/stock_ageing.py:208 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:214 msgid "Available Qty" msgstr "" @@ -6543,7 +6558,7 @@ msgstr "" msgid "Available for use date is required" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1123 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1228 msgid "Available quantity is {0}, you need {1}" msgstr "" @@ -6555,9 +6570,9 @@ msgstr "" msgid "Available-for-use Date should be after purchase date" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:209 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:243 -#: erpnext/stock/report/stock_balance/stock_balance.py:590 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:215 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:249 +#: erpnext/stock/report/stock_balance/stock_balance.py:584 msgid "Average Age" msgstr "" @@ -6580,7 +6595,9 @@ msgstr "" msgid "Average Order Values" msgstr "" +#. Label of the valuation_rate (Currency) field in DocType 'Stock Ledger Entry' #: erpnext/accounts/report/share_balance/share_balance.py:60 +#: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json msgid "Average Rate" msgstr "" @@ -6604,7 +6621,7 @@ msgid "Avg Rate" msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:154 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:368 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:369 msgid "Avg Rate (Balance Stock)" msgstr "" @@ -6675,7 +6692,7 @@ msgstr "" #: erpnext/selling/doctype/sales_order/sales_order.js:1458 #: erpnext/stock/doctype/material_request/material_request.js:351 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:788 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:804 #: erpnext/stock/report/bom_search/bom_search.py:38 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:525 @@ -6689,7 +6706,7 @@ msgstr "" msgid "BOM 1" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1823 +#: erpnext/manufacturing/doctype/bom/bom.py:1830 msgid "BOM 1 {0} and BOM 2 {1} should not be same" msgstr "" @@ -6705,6 +6722,10 @@ msgstr "" msgid "BOM Comparison Tool" msgstr "" +#: erpnext/stock/report/item_where_used/item_where_used.py:178 +msgid "BOM Component" +msgstr "" + #. Label of the bom_conf_tab (Tab Break) field in DocType 'BOM' #: erpnext/manufacturing/doctype/bom/bom.json msgid "BOM Configuration" @@ -6731,6 +6752,11 @@ msgstr "" msgid "BOM Creator Item" msgstr "" +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:392 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:535 +msgid "BOM Creator Item with name {0} does not exist" +msgstr "" + #. Label of the bom_detail_no (Data) field in DocType 'Purchase Order Item #. Supplied' #. Label of the bom_detail_no (Data) field in DocType 'Purchase Receipt Item @@ -6835,6 +6861,10 @@ msgstr "" msgid "BOM Operations Time" msgstr "" +#: erpnext/stock/report/item_where_used/item_where_used.py:248 +msgid "BOM Output" +msgstr "" + #: erpnext/stock/report/item_prices/item_prices.py:60 msgid "BOM Rate" msgstr "" @@ -6852,6 +6882,7 @@ msgstr "" #. Label of the bom_secondary_item (Data) field in DocType 'Stock Entry Detail' #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +#: erpnext/stock/report/item_where_used/item_where_used.py:213 msgid "BOM Secondary Item" msgstr "" @@ -6922,7 +6953,7 @@ msgstr "" msgid "BOM Website Operation" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2535 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2686 msgid "BOM and Finished Good Quantity is mandatory for Disassembly" msgstr "" @@ -6933,7 +6964,7 @@ msgid "BOM and Production" msgstr "" #: erpnext/stock/doctype/material_request/material_request.js:386 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:840 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:856 msgid "BOM does not contain any stock item" msgstr "" @@ -6941,23 +6972,23 @@ msgstr "" msgid "BOM recursion: {0} cannot be child of {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:790 +#: erpnext/manufacturing/doctype/bom/bom.py:797 msgid "BOM recursion: {1} cannot be parent or child of {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1541 +#: erpnext/manufacturing/doctype/bom/bom.py:1548 msgid "BOM {0} does not belong to Item {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1523 +#: erpnext/manufacturing/doctype/bom/bom.py:1530 msgid "BOM {0} must be active" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1526 +#: erpnext/manufacturing/doctype/bom/bom.py:1533 msgid "BOM {0} must be submitted" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:878 +#: erpnext/manufacturing/doctype/bom/bom.py:885 msgid "BOM {0} not found for the item {1}" msgstr "" @@ -6966,15 +6997,15 @@ msgstr "" msgid "BOMs Updated" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:310 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:314 msgid "BOMs created successfully" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:320 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:324 msgid "BOMs creation failed" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:260 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:264 msgid "BOMs creation has been enqueued, kindly check the status after some time" msgstr "" @@ -7047,8 +7078,8 @@ msgstr "" #: erpnext/stock/report/available_batch_report/available_batch_report.py:63 #: erpnext/stock/report/available_serial_no/available_serial_no.py:126 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:84 -#: erpnext/stock/report/stock_balance/stock_balance.py:518 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:331 +#: erpnext/stock/report/stock_balance/stock_balance.py:512 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:332 msgid "Balance Qty" msgstr "" @@ -7074,7 +7105,7 @@ msgstr "" #: erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json #: erpnext/accounts/report/balance_sheet/balance_sheet.json #: erpnext/accounts/workspace/financial_reports/financial_reports.json -#: erpnext/public/js/financial_statements.js:314 +#: erpnext/public/js/financial_statements.js:327 #: erpnext/setup/doctype/email_digest/email_digest.json #: erpnext/workspace_sidebar/financial_reports.json msgid "Balance Sheet" @@ -7112,8 +7143,8 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:174 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:86 -#: erpnext/stock/report/stock_balance/stock_balance.py:525 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:388 +#: erpnext/stock/report/stock_balance/stock_balance.py:519 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:389 msgid "Balance Value" msgstr "" @@ -7610,7 +7641,7 @@ msgstr "" #: erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py:34 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:80 #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:158 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:418 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:419 #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:171 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:80 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js:19 @@ -7726,7 +7757,7 @@ msgstr "" msgid "Batch No is mandatory" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3448 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3457 msgid "Batch No {0} does not exists" msgstr "" @@ -7753,7 +7784,7 @@ msgstr "" msgid "Batch Nos are created successfully" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:1193 +#: erpnext/controllers/sales_and_purchase_return.py:1194 msgid "Batch Not Available for Return" msgstr "" @@ -7822,16 +7853,16 @@ msgstr "" msgid "Batch {0} and Warehouse" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:1192 +#: erpnext/controllers/sales_and_purchase_return.py:1193 msgid "Batch {0} is not available in warehouse {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3613 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3764 #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:290 msgid "Batch {0} of Item {1} has expired." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3619 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3770 msgid "Batch {0} of Item {1} is disabled." msgstr "" @@ -7867,14 +7898,14 @@ msgstr "" msgid "Beginning of the current subscription period" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:326 +#: erpnext/accounts/doctype/subscription/subscription.py:359 msgid "Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0}" msgstr "" #. Label of the bill_date (Date) field in DocType 'Journal Entry' #. Label of the bill_date (Date) field in DocType 'Subcontracting Receipt' #: erpnext/accounts/doctype/journal_entry/journal_entry.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1184 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1204 #: erpnext/accounts/report/purchase_register/purchase_register.py:214 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json msgid "Bill Date" @@ -7883,7 +7914,7 @@ msgstr "" #. Label of the bill_no (Data) field in DocType 'Journal Entry' #. Label of the bill_no (Data) field in DocType 'Subcontracting Receipt' #: erpnext/accounts/doctype/journal_entry/journal_entry.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1183 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1203 #: erpnext/accounts/report/purchase_register/purchase_register.py:213 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json msgid "Bill No" @@ -7898,10 +7929,10 @@ 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:1373 +#: erpnext/manufacturing/doctype/bom/bom.py:1380 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/stock/doctype/material_request/material_request.js:139 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:774 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:790 #: erpnext/workspace_sidebar/subcontracting.json msgid "Bill of Materials" msgstr "" @@ -8079,7 +8110,7 @@ msgstr "" msgid "Billing Interval Count cannot be less than 1" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:375 +#: erpnext/accounts/doctype/subscription/subscription.py:408 msgid "Billing Interval in Subscription Plan must be Month to follow calendar months" msgstr "" @@ -8108,7 +8139,7 @@ msgstr "" msgid "Billing Zipcode" msgstr "" -#: erpnext/accounts/party.py:597 +#: erpnext/accounts/party.py:615 msgid "Billing currency must be equal to either default company's currency or party account currency" msgstr "" @@ -8290,7 +8321,7 @@ msgstr "" msgid "Bold text for emphasis (totals, major headings)" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:286 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:287 msgid "Book Advance Payments as Liability option is chosen. Paid From account changed from {0} to {1}." msgstr "" @@ -8364,7 +8395,7 @@ msgstr "" msgid "Both Receivable Account: {0} and Advance Account: {1} must be of same currency for company: {2}" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:345 +#: erpnext/accounts/doctype/subscription/subscription.py:378 msgid "Both Trial Period Start Date and Trial Period End Date must be set" msgstr "" @@ -8432,7 +8463,7 @@ msgstr "" msgid "Brokerage" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:231 +#: erpnext/manufacturing/doctype/bom/bom.js:234 msgid "Browse BOM" msgstr "" @@ -9069,7 +9100,7 @@ msgstr "" msgid "Can be approved by {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2643 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2735 msgid "Can not close Work Order. Since {0} Job Cards are in Work In Progress state." msgstr "" @@ -9098,7 +9129,7 @@ msgid "Can not filter based on Voucher No, if grouped by Voucher" msgstr "" #: erpnext/accounts/doctype/journal_entry/journal_entry.py:1392 -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2873 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2892 msgid "Can only make payment against unbilled {0}" msgstr "" @@ -9109,7 +9140,7 @@ msgid "Can refer row only if the charge type is 'On Previous Row Amount' or 'Pre msgstr "" #: erpnext/setup/doctype/company/company.py:207 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:181 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:183 msgid "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method" msgstr "" @@ -9141,6 +9172,10 @@ msgstr "" msgid "Cancelation Date" msgstr "" +#: erpnext/manufacturing/doctype/job_card/job_card.py:1490 +msgid "Cancelled Job Card cannot be processed." +msgstr "" + #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:76 msgid "Cannot Assign Cashier" msgstr "" @@ -9168,7 +9203,7 @@ msgstr "" msgid "Cannot Optimize Route as Driver Address is Missing." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:295 +#: erpnext/setup/doctype/employee/employee.py:292 msgid "Cannot Relieve Employee" msgstr "" @@ -9228,7 +9263,7 @@ msgstr "" msgid "Cannot cancel this document as it is linked with the submitted asset {asset_link}. Please cancel the asset to continue." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:638 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:656 msgid "Cannot cancel transaction for Completed Work Order." msgstr "" @@ -9272,11 +9307,15 @@ msgstr "" msgid "Cannot covert to Group because Account Type is selected." msgstr "" +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2839 +msgid "Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. Please check the existing linked {2}s." +msgstr "" + #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1012 msgid "Cannot create Stock Reservation Entries for future dated Purchase Receipts." msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:1915 +#: erpnext/selling/doctype/sales_order/sales_order.py:1905 #: erpnext/stock/doctype/pick_list/pick_list.py:258 msgid "Cannot create a pick list for Sales Order {0} because it has reserved stock. Please unreserve the stock in order to create a pick list." msgstr "" @@ -9289,7 +9328,7 @@ msgstr "" msgid "Cannot create return for consolidated invoice {0}." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1211 +#: erpnext/manufacturing/doctype/bom/bom.py:1218 msgid "Cannot deactivate or cancel BOM as it is linked with other BOMs" msgstr "" @@ -9315,7 +9354,7 @@ msgid "Cannot delete an item which has been ordered" msgstr "" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:197 -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:783 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:785 msgid "Cannot delete protected core DocType: {0}" msgstr "" @@ -9323,7 +9362,7 @@ msgstr "" msgid "Cannot delete virtual DocType: {0}. Virtual DocTypes do not have database tables." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:146 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:148 msgid "Cannot disable Serial and Batch No for Item, as there are existing records for serial / batch." msgstr "" @@ -9331,7 +9370,7 @@ msgstr "" msgid "Cannot disable perpetual inventory, as there are existing Stock Ledger Entries for the company {0}. Please cancel the stock transactions first and try again." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:127 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:129 msgid "Cannot disable {0} as it may lead to incorrect stock valuation." msgstr "" @@ -9339,7 +9378,7 @@ msgstr "" msgid "Cannot disassemble more than produced quantity." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1003 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1021 msgid "Cannot disassemble {0} qty against Stock Entry {1}. Only {2} qty available to disassemble." msgstr "" @@ -9347,6 +9386,10 @@ msgstr "" msgid "Cannot enable Item-wise Inventory Account, as there are existing Stock Ledger Entries for the company {0} with Warehouse-wise Inventory Account. Please cancel the stock transactions first and try again." msgstr "" +#: erpnext/crm/doctype/crm_settings/crm_settings.py:37 +msgid "Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled." +msgstr "" + #: erpnext/selling/doctype/sales_order/sales_order.py:781 #: erpnext/selling/doctype/sales_order/sales_order.py:804 msgid "Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No." @@ -9368,7 +9411,7 @@ msgstr "" msgid "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." msgstr "" -#: erpnext/accounts/party.py:1063 +#: erpnext/accounts/party.py:1081 msgid "Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'." msgstr "" @@ -9384,7 +9427,7 @@ msgstr "" msgid "Cannot produce more than {0} items for {1}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:359 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:361 msgid "Cannot receive from customer against negative outstanding" msgstr "" @@ -9412,7 +9455,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1507 #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1685 -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1827 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1842 #: erpnext/controllers/accounts_controller.py:3194 #: erpnext/public/js/controllers/accounts.js:112 #: erpnext/public/js/controllers/taxes_and_totals.js:552 @@ -9447,11 +9490,15 @@ msgstr "" msgid "Cannot start deletion. Another deletion {0} is already queued/running. Please wait for it to complete." msgstr "" +#: erpnext/manufacturing/doctype/job_card/job_card.py:873 +msgid "Cannot submit Job Card {0} while it is On Hold. Please resume and complete the job before submission." +msgstr "" + #: erpnext/controllers/accounts_controller.py:3946 msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1937 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1952 msgid "Cannot {0} from {1} without any negative outstanding invoice" msgstr "" @@ -9607,19 +9654,19 @@ msgstr "" msgid "Cash Flow" msgstr "" -#: erpnext/public/js/financial_statements.js:346 +#: erpnext/public/js/financial_statements.js:359 msgid "Cash Flow Statement" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:179 +#: erpnext/accounts/report/cash_flow/cash_flow.py:186 msgid "Cash Flow from Financing" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:172 +#: erpnext/accounts/report/cash_flow/cash_flow.py:179 msgid "Cash Flow from Investing" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:160 +#: erpnext/accounts/report/cash_flow/cash_flow.py:167 msgid "Cash Flow from Operations" msgstr "" @@ -9628,7 +9675,7 @@ msgstr "" msgid "Cash In Hand" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:324 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:329 msgid "Cash or Bank Account is mandatory for making payment entry" msgstr "" @@ -9827,7 +9874,7 @@ msgstr "" msgid "Change in Stock Value" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1059 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1069 msgid "Change the account type to Receivable or select a different account." msgstr "" @@ -9841,7 +9888,7 @@ msgstr "" msgid "Changed customer name to '{}' as '{}' already exists." msgstr "" -#: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:156 +#: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:160 msgid "Changes in {0}" msgstr "" @@ -9865,7 +9912,7 @@ msgstr "" msgid "Channel Partner" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2256 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2271 #: erpnext/controllers/accounts_controller.py:3257 msgid "Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount" msgstr "" @@ -10198,16 +10245,16 @@ msgstr "" msgid "Clearance Date" msgstr "" -#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:134 +#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:135 msgid "Clearance Date not mentioned" msgstr "" -#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:179 +#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:180 msgid "Clearance Date updated" msgstr "" -#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:158 -#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:173 +#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:159 +#: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:174 msgid "Clearance date changed from {0} to {1} via Bank Clearance Tool" msgstr "" @@ -10277,7 +10324,7 @@ msgstr "" msgid "Closed Documents" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2566 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2658 msgid "Closed Work Order can not be stopped or Re-opened" msgstr "" @@ -10873,7 +10920,7 @@ msgstr "" #: erpnext/projects/doctype/timesheet/timesheet.json #: erpnext/projects/report/project_summary/project_summary.js:8 #: erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py:44 -#: erpnext/public/js/financial_statements.js:368 +#: erpnext/public/js/financial_statements.js:381 #: erpnext/public/js/purchase_trends_filters.js:8 #: erpnext/public/js/sales_trends_filters.js:51 #: erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.json @@ -10958,6 +11005,8 @@ msgstr "" #: erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js:7 #: erpnext/stock/report/item_shortage_report/item_shortage_report.js:8 #: erpnext/stock/report/item_shortage_report/item_shortage_report.py:137 +#: erpnext/stock/report/item_where_used/item_where_used.js:15 +#: erpnext/stock/report/item_where_used/item_where_used.py:95 #: erpnext/stock/report/landed_cost_report/landed_cost_report.js:8 #: erpnext/stock/report/negative_batch_report/negative_batch_report.js:8 #: erpnext/stock/report/product_bundle_balance/product_bundle_balance.js:8 @@ -10971,9 +11020,9 @@ msgstr "" #: erpnext/stock/report/stock_analytics/stock_analytics.js:41 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js:7 #: erpnext/stock/report/stock_balance/stock_balance.js:8 -#: erpnext/stock/report/stock_balance/stock_balance.py:579 +#: erpnext/stock/report/stock_balance/stock_balance.py:573 #: erpnext/stock/report/stock_ledger/stock_ledger.js:8 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:441 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:442 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:18 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:8 #: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:8 @@ -11165,12 +11214,12 @@ msgstr "" msgid "Company and Posting Date is mandatory" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2620 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2630 msgid "Company currencies of both the companies should match for Inter Company Transactions." msgstr "" #: erpnext/stock/doctype/material_request/material_request.js:380 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:834 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:850 msgid "Company field is required" msgstr "" @@ -11182,7 +11231,7 @@ msgstr "" msgid "Company is mandatory for company account" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:404 +#: erpnext/accounts/doctype/subscription/subscription.py:437 msgid "Company is mandatory for generating an invoice. Please set a default company in Global Defaults." msgstr "" @@ -11200,7 +11249,7 @@ msgstr "" msgid "Company of asset {0} and purchase document {1} doesn't matches." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:168 +#: erpnext/setup/doctype/employee/employee.py:164 msgid "Company or Personal Email is mandatory when 'Create User Automatically' is enabled" msgstr "" @@ -11278,7 +11327,7 @@ msgstr "" msgid "Competitors" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:661 +#: erpnext/manufacturing/doctype/job_card/job_card.js:665 #: erpnext/manufacturing/doctype/workstation/workstation.js:151 msgid "Complete Job" msgstr "" @@ -11582,7 +11631,7 @@ msgstr "" #. Log' #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:574 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:580 msgid "Consolidated Sales Invoice" msgstr "" @@ -11915,7 +11964,7 @@ msgid "Contract Terms and Conditions" msgstr "" #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py:77 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:122 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:131 msgid "Contribution %" msgstr "" @@ -11925,11 +11974,11 @@ msgid "Contribution (%)" msgstr "" #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py:89 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:130 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:139 msgid "Contribution Amount" msgstr "" -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:124 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:133 msgid "Contribution Qty" msgstr "" @@ -11997,7 +12046,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json -#: erpnext/public/js/utils.js:902 +#: erpnext/public/js/utils.js:903 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/stock/doctype/packed_item/packed_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -12278,7 +12327,7 @@ msgstr "" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:28 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:47 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:30 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1169 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1189 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:47 #: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js:42 #: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py:204 @@ -12309,7 +12358,7 @@ msgstr "" #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json #: erpnext/buying/report/procurement_tracker/procurement_tracker.js:15 #: erpnext/buying/report/procurement_tracker/procurement_tracker.py:32 -#: erpnext/public/js/financial_statements.js:462 +#: erpnext/public/js/financial_statements.js:475 #: erpnext/selling/doctype/sales_order/sales_order.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/stock/doctype/delivery_note/delivery_note.json @@ -12374,7 +12423,7 @@ msgstr "" msgid "Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1459 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1468 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:897 msgid "Cost Center is required in row {0} in Taxes table for type {1}" msgstr "" @@ -12421,7 +12470,7 @@ msgstr "" msgid "Cost Per Unit" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:442 +#: erpnext/manufacturing/doctype/bom/bom.py:449 msgid "Cost allocation between finished goods and secondary items should equal 100%" msgstr "" @@ -12457,7 +12506,7 @@ msgstr "" msgid "Cost of Goods Sold" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:877 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:895 msgid "Cost of Goods Sold Account in Items Table" msgstr "" @@ -12824,7 +12873,7 @@ msgstr "" msgid "Create Pick List" msgstr "" -#: erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js:10 +#: erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js:11 msgid "Create Print Format" msgstr "" @@ -13066,7 +13115,7 @@ msgstr "" msgid "Created By Migration" msgstr "" -#: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:250 +#: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:251 msgid "Created {0} scorecards for {1} between:" msgstr "" @@ -13328,7 +13377,7 @@ msgstr "" #: erpnext/accounts/doctype/journal_entry/journal_entry.json #: erpnext/accounts/doctype/journal_entry_template/journal_entry_template.json #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:176 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1193 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1213 #: erpnext/controllers/sales_and_purchase_return.py:453 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:303 #: erpnext/stock/doctype/delivery_note/delivery_note.js:89 @@ -13346,7 +13395,7 @@ msgstr "" #. Option for the 'Status' (Select) field in DocType 'Sales Invoice' #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:276 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:282 msgid "Credit Note Issued" msgstr "" @@ -13362,8 +13411,8 @@ 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:378 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:386 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:383 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:391 #: erpnext/controllers/accounts_controller.py:2376 msgid "Credit To" msgstr "" @@ -13564,9 +13613,9 @@ msgstr "" msgid "Currency filters are currently unsupported in Custom Financial Report." msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1604 -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1672 -#: erpnext/accounts/utils.py:2532 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1619 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1687 +#: erpnext/accounts/utils.py:2534 msgid "Currency for {0} must be {1}" msgstr "" @@ -13574,7 +13623,7 @@ msgstr "" msgid "Currency of the Closing Account must be {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:724 +#: erpnext/manufacturing/doctype/bom/bom.py:731 msgid "Currency of the price list {0} must be {1} or {2}" msgstr "" @@ -13839,7 +13888,7 @@ msgstr "" #: erpnext/accounts/doctype/pricing_rule/pricing_rule.json #: erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json #: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:392 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:411 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/sales_invoice_reference/sales_invoice_reference.json #: erpnext/accounts/doctype/tax_rule/tax_rule.json @@ -13850,7 +13899,7 @@ msgstr "" #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:29 #: erpnext/accounts/report/general_ledger/general_ledger.html:136 #: erpnext/accounts/report/gross_profit/gross_profit.py:416 -#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:37 +#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:38 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js:22 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:221 #: erpnext/accounts/report/pos_register/pos_register.js:44 @@ -13892,7 +13941,7 @@ msgstr "" #: erpnext/selling/report/customer_credit_balance/customer_credit_balance.js:16 #: erpnext/selling/report/customer_credit_balance/customer_credit_balance.py:64 #: erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.js:7 -#: erpnext/selling/report/inactive_customers/inactive_customers.py:77 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:98 #: erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js:47 #: erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py:72 #: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js:37 @@ -13906,7 +13955,7 @@ msgstr "" #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.js:40 #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py:54 #: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js:53 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:65 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:74 #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/doctype/authorization_rule/authorization_rule.json #: erpnext/setup/doctype/customer_group/customer_group.json @@ -13921,7 +13970,7 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.json #: erpnext/stock/doctype/serial_no/serial_no.json #: erpnext/stock/doctype/shipment/shipment.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:472 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:488 #: erpnext/stock/doctype/warehouse/warehouse.json #: erpnext/stock/report/delayed_item_report/delayed_item_report.js:36 #: erpnext/stock/report/delayed_item_report/delayed_item_report.py:121 @@ -14019,7 +14068,7 @@ msgstr "" #. Label of the customer_contact_display (Small Text) field in DocType #. 'Purchase Order' #. Label of the customer_contact (Small Text) field in DocType 'Delivery Stop' -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1163 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1183 #: erpnext/buying/doctype/purchase_order/purchase_order.json #: erpnext/stock/doctype/delivery_stop/delivery_stop.json msgid "Customer Contact" @@ -14125,7 +14174,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:115 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1221 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1241 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:96 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:185 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:56 @@ -14145,7 +14194,7 @@ msgstr "" #: erpnext/selling/doctype/party_specific_item/party_specific_item.json #: erpnext/selling/doctype/quotation/quotation.json #: erpnext/selling/doctype/sales_order/sales_order.json -#: erpnext/selling/report/inactive_customers/inactive_customers.py:80 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:101 #: erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py:80 #: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js:30 #: erpnext/selling/workspace/selling/selling.json @@ -14186,7 +14235,7 @@ msgstr "" msgid "Customer Items" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1212 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1232 msgid "Customer LPO" msgstr "" @@ -14238,7 +14287,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1153 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1173 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:156 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:92 #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:35 @@ -14255,7 +14304,7 @@ msgstr "" #: erpnext/selling/doctype/sales_order/sales_order.json #: erpnext/selling/doctype/selling_settings/selling_settings.json #: erpnext/selling/report/customer_credit_balance/customer_credit_balance.py:74 -#: erpnext/selling/report/inactive_customers/inactive_customers.py:78 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:99 #: erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py:78 #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/item_customer_detail/item_customer_detail.json @@ -14375,7 +14424,7 @@ msgstr "" msgid "Customer Warehouse (Optional)" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:145 +#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:146 msgid "Customer Warehouse {0} does not belong to Customer {1}." msgstr "" @@ -14401,7 +14450,7 @@ msgstr "" msgid "Customer required for 'Customerwise Discount'" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1173 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1183 #: erpnext/selling/doctype/sales_order/sales_order.py:436 #: erpnext/stock/doctype/delivery_note/delivery_note.py:406 msgid "Customer {0} does not belong to project {1}" @@ -14480,7 +14529,7 @@ msgstr "" msgid "Customers Without Any Sales Transactions" msgstr "" -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:106 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:107 msgid "Customers not selected." msgstr "" @@ -14597,7 +14646,7 @@ msgstr "" msgid "Date of Birth" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:260 +#: erpnext/setup/doctype/employee/employee.py:257 msgid "Date of Birth cannot be greater than today." msgstr "" @@ -14709,9 +14758,9 @@ msgstr "" msgid "Days" msgstr "" -#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:51 +#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:52 #: erpnext/selling/report/inactive_customers/inactive_customers.js:8 -#: erpnext/selling/report/inactive_customers/inactive_customers.py:86 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:107 msgid "Days Since Last Order" msgstr "" @@ -14822,7 +14871,7 @@ msgstr "" #: erpnext/accounts/doctype/journal_entry/journal_entry.json #: erpnext/accounts/doctype/journal_entry_template/journal_entry_template.json #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:178 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1196 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1216 #: erpnext/controllers/sales_and_purchase_return.py:457 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:304 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:45 @@ -14850,13 +14899,13 @@ msgstr "" #. Label of the debit_to (Link) field in DocType 'Sales Invoice' #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1044 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1055 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1054 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1065 #: erpnext/controllers/accounts_controller.py:2376 msgid "Debit To" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1040 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1050 msgid "Debit To is required" msgstr "" @@ -14893,11 +14942,11 @@ msgstr "" msgid "Debtor Turnover Ratio" msgstr "" -#: erpnext/accounts/party.py:604 +#: erpnext/accounts/party.py:622 msgid "Debtor/Creditor" msgstr "" -#: erpnext/accounts/party.py:607 +#: erpnext/accounts/party.py:625 msgid "Debtor/Creditor Advance" msgstr "" @@ -15026,7 +15075,7 @@ msgstr "" msgid "Default BOM ({0}) must be active for this item or its template" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2334 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2426 msgid "Default BOM for {0} not found" msgstr "" @@ -15034,7 +15083,7 @@ msgstr "" msgid "Default BOM not found for FG Item {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2331 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2423 msgid "Default BOM not found for Item {0} and Project {1}" msgstr "" @@ -15630,8 +15679,8 @@ msgstr "" msgid "Deleting {0} and all associated Common Code documents..." msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1100 -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1119 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1102 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1121 msgid "Deletion in Progress!" msgstr "" @@ -15783,7 +15832,7 @@ msgstr "" #: erpnext/manufacturing/doctype/master_production_schedule_item/master_production_schedule_item.json #: erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1069 -#: erpnext/public/js/utils.js:895 +#: erpnext/public/js/utils.js:896 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:632 #: erpnext/selling/doctype/sales_order/sales_order.js:1533 @@ -15826,7 +15875,7 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:129 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:415 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:434 #: erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js:36 #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.js:22 @@ -15888,11 +15937,11 @@ msgstr "" msgid "Delivery Note Trends" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1434 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1444 msgid "Delivery Note {0} is not submitted" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1216 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1236 #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:75 msgid "Delivery Notes" msgstr "" @@ -16089,7 +16138,7 @@ msgstr "" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:105 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:176 #: erpnext/accounts/report/account_balance/account_balance.js:44 -#: erpnext/accounts/report/cash_flow/cash_flow.py:162 +#: erpnext/accounts/report/cash_flow/cash_flow.py:169 #: erpnext/assets/doctype/asset/asset.json msgid "Depreciation" msgstr "" @@ -16297,15 +16346,15 @@ msgstr "" msgid "Difference Account" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:866 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:884 msgid "Difference Account in Items Table" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:855 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:873 msgid "Difference Account must be a Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry" msgstr "" -#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:991 +#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:975 msgid "Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry" msgstr "" @@ -16429,7 +16478,7 @@ msgstr "" msgid "Direct Income" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:359 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:365 msgid "Direct return is not allowed for Timesheet." msgstr "" @@ -16558,8 +16607,8 @@ msgstr "" #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry' #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type' #: erpnext/manufacturing/doctype/work_order/work_order.js:1068 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:370 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:413 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:386 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:429 #: erpnext/stock/doctype/stock_entry/stock_entry.json #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json msgid "Disassemble" @@ -16569,7 +16618,7 @@ msgstr "" msgid "Disassemble Order" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2477 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2628 msgid "Disassemble Qty cannot be less than or equal to 0." msgstr "" @@ -16788,7 +16837,7 @@ msgstr "" msgid "Discount must be less than 100" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:3351 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:3370 msgid "Discount of {} applied as per Payment Term" msgstr "" @@ -17084,7 +17133,7 @@ msgstr "" msgid "Do Not Explode" msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:128 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:130 msgid "Do Not Use Batchwise Valuation" msgstr "" @@ -17229,7 +17278,7 @@ msgstr "" msgid "Documents Processed on each trigger. Queue Size should be between 5 and 100" msgstr "" -#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:261 +#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:262 msgid "Documents: {0} have deferred revenue/expense enabled for them. Cannot repost." msgstr "" @@ -17390,11 +17439,11 @@ msgstr "" msgid "Drop Ship" msgstr "" -#: erpnext/accounts/party.py:690 +#: erpnext/accounts/party.py:708 msgid "Due Date cannot be after {0}" msgstr "" -#: erpnext/accounts/party.py:666 +#: erpnext/accounts/party.py:684 msgid "Due Date cannot be before {0}" msgstr "" @@ -17477,7 +17526,7 @@ msgstr "" msgid "Duplicate Item Under Same Parent" msgstr "" -#: erpnext/manufacturing/doctype/workstation/workstation.py:79 +#: erpnext/manufacturing/doctype/workstation/workstation.py:80 #: erpnext/manufacturing/doctype/workstation_type/workstation_type.py:37 msgid "Duplicate Operating Component {0} found in Operating Components" msgstr "" @@ -17632,11 +17681,11 @@ msgstr "" msgid "Each Transaction" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:215 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:221 msgid "Earliest" msgstr "" -#: erpnext/stock/report/stock_balance/stock_balance.py:591 +#: erpnext/stock/report/stock_balance/stock_balance.py:585 msgid "Earliest Age" msgstr "" @@ -17645,7 +17694,7 @@ msgstr "" msgid "Earnest Money" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:528 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:526 msgid "Edit BOM" msgstr "" @@ -17742,7 +17791,7 @@ msgstr "" msgid "Either target qty or target amount is mandatory." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:675 +#: erpnext/manufacturing/doctype/job_card/job_card.js:679 msgid "Elapsed Time" msgstr "" @@ -17847,7 +17896,7 @@ msgstr "" msgid "Email Sent to Supplier {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:434 +#: erpnext/setup/doctype/employee/employee.py:440 msgid "Email is required to create a user" msgstr "" @@ -17868,7 +17917,7 @@ msgstr "" msgid "Email sent to" msgstr "" -#: erpnext/stock/doctype/delivery_trip/delivery_trip.py:446 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.py:449 msgid "Email sent to {0}" msgstr "" @@ -18033,11 +18082,11 @@ msgstr "" msgid "Employee User Id" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:325 +#: erpnext/setup/doctype/employee/employee.py:330 msgid "Employee cannot report to himself." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:568 +#: erpnext/setup/doctype/employee/employee.py:574 msgid "Employee is required" msgstr "" @@ -18045,7 +18094,7 @@ msgstr "" msgid "Employee is required while issuing Asset {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:431 +#: erpnext/setup/doctype/employee/employee.py:437 msgid "Employee {0} already has a linked user" msgstr "" @@ -18058,7 +18107,7 @@ msgstr "" msgid "Employee {0} is currently working on another workstation. Please assign another employee." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:593 +#: erpnext/setup/doctype/employee/employee.py:599 msgid "Employee {0} not found" msgstr "" @@ -18070,7 +18119,7 @@ msgstr "" msgid "Empty" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:755 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:757 msgid "Empty To Delete List" msgstr "" @@ -18188,6 +18237,12 @@ msgstr "" msgid "Enable Loyalty Point Program" msgstr "" +#. Label of the enable_opportunity_creation_from_contact_us (Check) field in +#. DocType 'CRM Settings' +#: erpnext/crm/doctype/crm_settings/crm_settings.json +msgid "Enable Opportunity Creation from Contact Us" +msgstr "" + #. Label of the enable_parallel_reposting (Check) field in DocType 'Stock #. Reposting Settings' #: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json @@ -18413,7 +18468,7 @@ msgstr "" msgid "End Time" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:345 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:361 msgid "End Transit" msgstr "" @@ -18425,7 +18480,7 @@ msgstr "" #: erpnext/accounts/report/financial_ratios/financial_ratios.js:25 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.html:147 #: erpnext/assets/report/fixed_asset_register/fixed_asset_register.js:89 -#: erpnext/public/js/financial_statements.js:430 +#: erpnext/public/js/financial_statements.js:443 msgid "End Year" msgstr "" @@ -18576,7 +18631,7 @@ msgstr "" msgid "Enter the opening stock units." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:992 +#: erpnext/manufacturing/doctype/bom/bom.js:995 msgid "Enter the quantity of the Item that will be manufactured from this Bill of Materials." msgstr "" @@ -18754,7 +18809,7 @@ msgstr "" msgid "Exception Budget Approver Role" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1010 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1028 msgid "Excess Disassembly" msgstr "" @@ -18762,7 +18817,7 @@ msgstr "" msgid "Excess Materials Consumed" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1140 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1153 msgid "Excess Transfer" msgstr "" @@ -18903,7 +18958,7 @@ msgstr "" msgid "Excise Entry" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:1493 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:1524 msgid "Excise Invoice" msgstr "" @@ -19098,7 +19153,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:596 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:601 #: 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:184 @@ -19169,13 +19224,13 @@ msgstr "" msgid "Expense Head" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:490 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:514 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:534 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:495 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:519 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:539 msgid "Expense Head Changed" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:592 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:597 msgid "Expense account is mandatory for item {0}" msgstr "" @@ -19206,7 +19261,7 @@ msgid "Expenses Included In Valuation" msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.py:310 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:496 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:512 msgid "Expired Batches" msgstr "" @@ -19404,6 +19459,10 @@ msgstr "" msgid "Failed to setup defaults for country {0}. Please contact support." msgstr "" +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:521 +msgid "Failed to update subscription status for {0} {1}" +msgstr "" + #. Label of the failure_date (Datetime) field in DocType 'Asset Repair' #: erpnext/assets/doctype/asset_repair/asset_repair.json msgid "Failure Date" @@ -19483,7 +19542,7 @@ msgstr "" msgid "Fetch Subscription Updates" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:286 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:305 msgid "Fetch Timesheet" msgstr "" @@ -19500,7 +19559,7 @@ msgid "Fetch Value From" msgstr "" #: erpnext/stock/doctype/material_request/material_request.js:372 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:811 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:827 msgid "Fetch exploded BOM (including sub-assemblies)" msgstr "" @@ -19561,15 +19620,15 @@ msgstr "" msgid "Fields will be copied over only at time of creation." msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1067 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1069 msgid "File does not belong to this Transaction Deletion Record" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1061 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1063 msgid "File not found" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1075 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1077 msgid "File not found on server" msgstr "" @@ -19581,7 +19640,7 @@ msgstr "" #: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:231 #: erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js:16 #: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:16 -#: erpnext/public/js/financial_statements.js:382 +#: erpnext/public/js/financial_statements.js:395 msgid "Filter Based On" msgstr "" @@ -19687,7 +19746,7 @@ msgstr "" #: erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json #: erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json #: erpnext/assets/report/fixed_asset_register/fixed_asset_register.js:48 -#: erpnext/public/js/financial_statements.js:376 +#: erpnext/public/js/financial_statements.js:389 #: erpnext/workspace_sidebar/accounts_setup.json msgid "Finance Book" msgstr "" @@ -19758,7 +19817,7 @@ msgstr "" #. Label of a Card Break in the Financial Reports Workspace #: erpnext/accounts/workspace/financial_reports/financial_reports.json -#: erpnext/public/js/financial_statements.js:312 +#: erpnext/public/js/financial_statements.js:325 msgid "Financial Statements" msgstr "" @@ -19805,7 +19864,7 @@ msgstr "" #. Service Item' #. Label of the fg_item (Link) field in DocType 'Subcontracting Order Service #. Item' -#: erpnext/public/js/utils.js:921 +#: erpnext/public/js/utils.js:922 #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json #: erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json msgid "Finished Good Item" @@ -19818,7 +19877,7 @@ msgstr "" msgid "Finished Good Item Code" msgstr "" -#: erpnext/public/js/utils.js:939 +#: erpnext/public/js/utils.js:940 msgid "Finished Good Item Qty" msgstr "" @@ -19926,10 +19985,14 @@ msgstr "" msgid "Finished Goods based Operating Cost" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1854 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1959 msgid "Finished Item {0} does not match with Work Order {1}" msgstr "" +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1045 +msgid "Finished good quantity being consumed ({0} in stock UOM) must equal the quantity to disassemble ({1}). Do not change the UOM, conversion factor or quantity of the finished good row." +msgstr "" + #: erpnext/selling/doctype/sales_order/sales_order.js:585 msgid "First Delivery Date" msgstr "" @@ -20101,7 +20164,7 @@ msgstr "" msgid "Fixed Asset Turnover Ratio" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:781 +#: erpnext/manufacturing/doctype/bom/bom.py:788 msgid "Fixed Asset item {0} cannot be used in BOMs." msgstr "" @@ -20267,7 +20330,7 @@ msgstr "" msgid "For Production" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:977 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:995 msgid "For Quantity (Manufactured Qty) is mandatory" msgstr "" @@ -20349,11 +20412,11 @@ msgstr "" msgid "For legacy serial nos, do not fetch incoming rate from serial no and calculate it based on the inward transaction" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:368 +#: erpnext/manufacturing/doctype/bom/bom.py:375 msgid "For operation {0} at row {1}, please add raw materials or set a BOM against it." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2713 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2805 msgid "For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})" msgstr "" @@ -20370,7 +20433,7 @@ msgstr "" msgid "For projected and forecast quantities, the system will consider all child warehouses under the selected parent warehouse." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1886 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1991 msgid "For quantity {0} should not be greater than allowed quantity {1}" msgstr "" @@ -20403,7 +20466,7 @@ msgstr "" msgid "For the convenience of customers, these codes can be used in print formats like Invoices and Delivery Notes" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1148 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1253 msgid "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." msgstr "" @@ -20416,7 +20479,7 @@ msgstr "" msgid "For the {0}, no stock is available for the return in the warehouse {1}." msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:1244 +#: erpnext/controllers/sales_and_purchase_return.py:1245 msgid "For the {0}, the quantity is required to make the return entry" msgstr "" @@ -20977,13 +21040,13 @@ msgid "Further nodes can be only created under 'Group' type nodes" msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:188 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1208 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1228 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:177 msgid "Future Payment Amount" msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:187 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1207 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1227 msgid "Future Payment Ref" msgstr "" @@ -21291,10 +21354,10 @@ msgstr "" #. Label of the get_items_from (Select) field in DocType 'Production Plan' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:177 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:202 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:342 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:376 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:408 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:448 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:361 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:395 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:427 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:467 #: erpnext/buying/doctype/purchase_order/purchase_order.js:549 #: erpnext/buying/doctype/purchase_order/purchase_order.js:572 #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:380 @@ -21317,11 +21380,11 @@ msgstr "" #: erpnext/stock/doctype/material_request/material_request.js:238 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:144 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:244 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:439 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:486 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:519 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:610 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:778 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:455 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:502 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:535 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:626 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:794 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:165 msgid "Get Items From" msgstr "" @@ -21337,8 +21400,8 @@ msgid "Get Items for Purchase Only" msgstr "" #: erpnext/stock/doctype/material_request/material_request.js:346 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:814 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:827 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:830 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:843 msgid "Get Items from BOM" msgstr "" @@ -21441,7 +21504,7 @@ msgstr "" msgid "Get Suppliers By" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:338 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:357 msgid "Get Timesheets" msgstr "" @@ -21513,7 +21576,7 @@ msgstr "" msgid "Goods Transferred" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2404 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2555 msgid "Goods are already received against the outward entry {0}" msgstr "" @@ -21810,7 +21873,7 @@ msgstr "" msgid "Group Same Items" msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:156 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:158 msgid "Group Warehouses cannot be used in transactions. Please change the value of {0}" msgstr "" @@ -21927,7 +21990,7 @@ msgstr "" #: erpnext/accounts/report/budget_variance_report/budget_variance_report.js:72 #: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:77 #: erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js:59 -#: erpnext/public/js/financial_statements.js:443 +#: erpnext/public/js/financial_statements.js:456 #: erpnext/public/js/purchase_trends_filters.js:21 #: erpnext/public/js/sales_trends_filters.js:13 #: erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js:34 @@ -22523,6 +22586,17 @@ msgstr "" msgid "If checked, the tax amount will be considered as already included in the Print Rate / Print Amount" msgstr "" +#. Description of the 'Update Stock' (Check) field in DocType 'Sales Invoice' +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json +msgid "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Delivery Note is created separately." +msgstr "" + +#. Description of the 'Update Stock' (Check) field in DocType 'Purchase +#. Invoice' +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +msgid "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Purchase Receipt is created separately." +msgstr "" + #: erpnext/public/js/setup_wizard.js:56 msgid "If checked, we will create demo data for you to explore the system. This demo data can be erased later." msgstr "" @@ -22828,7 +22902,7 @@ msgstr "" msgid "If this is unchecked, direct GL entries will be created to book deferred revenue or expense" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:746 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:761 msgid "If this is undesirable please cancel the corresponding Payment Entry." msgstr "" @@ -23179,8 +23253,8 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:112 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:82 -#: erpnext/stock/report/stock_balance/stock_balance.py:546 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:317 +#: erpnext/stock/report/stock_balance/stock_balance.py:540 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:318 msgid "In Qty" msgstr "" @@ -23206,7 +23280,7 @@ msgstr "" msgid "In Transit Warehouse" msgstr "" -#: erpnext/stock/report/stock_balance/stock_balance.py:552 +#: erpnext/stock/report/stock_balance/stock_balance.py:546 msgid "In Value" msgstr "" @@ -23540,9 +23614,9 @@ msgstr "" #: erpnext/accounts/doctype/account_category/account_category.json #: erpnext/accounts/doctype/ledger_merge/ledger_merge.json #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:454 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:460 #: erpnext/accounts/report/account_balance/account_balance.js:27 -#: erpnext/accounts/report/financial_statements.py:773 +#: erpnext/accounts/report/financial_statements.py:776 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:182 #: erpnext/accounts/report/profitability_analysis/profitability_analysis.py:192 msgid "Income" @@ -23607,7 +23681,7 @@ msgstr "" #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/report/available_serial_no/available_serial_no.py:146 #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:167 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:360 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:361 #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:193 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:96 msgid "Incoming Rate" @@ -23647,7 +23721,7 @@ msgstr "" msgid "Incorrect Company" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1155 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1260 msgid "Incorrect Component Quantity" msgstr "" @@ -23660,7 +23734,7 @@ msgstr "" msgid "Incorrect Invoice" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:360 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:362 msgid "Incorrect Payment Type" msgstr "" @@ -23693,7 +23767,7 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.py:190 #: erpnext/stock/doctype/pick_list/pick_list.py:214 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:159 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:161 msgid "Incorrect Warehouse" msgstr "" @@ -23943,7 +24017,7 @@ msgstr "" msgid "Insufficient Capacity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3870 +#: erpnext/controllers/accounts_controller.py:3872 #: erpnext/controllers/accounts_controller.py:3894 #: erpnext/controllers/accounts_controller.py:4414 #: erpnext/controllers/accounts_controller.py:4420 @@ -23955,7 +24029,7 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.py:148 #: erpnext/stock/doctype/pick_list/pick_list.py:166 #: erpnext/stock/doctype/pick_list/pick_list.py:1093 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1127 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1232 #: erpnext/stock/serial_batch_bundle.py:1225 erpnext/stock/stock_ledger.py:1713 #: erpnext/stock/stock_ledger.py:2191 msgid "Insufficient Stock" @@ -24091,7 +24165,7 @@ msgstr "" msgid "Interest Income" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2985 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:3004 msgid "Interest and/or dunning fee" msgstr "" @@ -24196,10 +24270,10 @@ msgstr "" msgid "Interval should be between 1 to 59 MInutes" msgstr "" -#: 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:1050 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:384 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:392 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1060 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1070 #: erpnext/assets/doctype/asset_category/asset_category.py:69 #: erpnext/assets/doctype/asset_category/asset_category.py:97 #: erpnext/controllers/accounts_controller.py:3218 @@ -24211,7 +24285,7 @@ msgstr "" msgid "Invalid Accounting Dimension" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:400 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:402 #: erpnext/accounts/doctype/payment_request/payment_request.py:1005 msgid "Invalid Allocated Amount" msgstr "" @@ -24248,7 +24322,7 @@ msgstr "" msgid "Invalid Company Field" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2395 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2405 msgid "Invalid Company for Inter Company Transaction." msgstr "" @@ -24266,6 +24340,15 @@ msgstr "" msgid "Invalid Delivery Date" msgstr "" +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1084 +msgid "Invalid Disassembly Item" +msgstr "" + +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1050 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1099 +msgid "Invalid Disassembly Quantity" +msgstr "" + #: erpnext/selling/page/point_of_sale/pos_item_cart.js:414 msgid "Invalid Discount" msgstr "" @@ -24338,9 +24421,9 @@ msgstr "" msgid "Invalid Primary Role" msgstr "" -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:121 -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:123 -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:126 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:122 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:124 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:127 msgid "Invalid Print Format" msgstr "" @@ -24348,11 +24431,11 @@ msgstr "" msgid "Invalid Priority" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1276 +#: erpnext/manufacturing/doctype/bom/bom.py:1283 msgid "Invalid Process Loss Configuration" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:707 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:722 msgid "Invalid Purchase Invoice" msgstr "" @@ -24386,12 +24469,12 @@ msgstr "" msgid "Invalid Selling Price" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1929 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2034 msgid "Invalid Serial and Batch Bundle" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1189 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1211 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1294 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1316 msgid "Invalid Source and Target Warehouse" msgstr "" @@ -24420,7 +24503,7 @@ msgstr "" msgid "Invalid condition expression" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1056 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1058 msgid "Invalid file URL" msgstr "" @@ -24452,11 +24535,11 @@ msgstr "" msgid "Invalid search query" msgstr "" -#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:98 +#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:99 msgid "Invalid value {0} for 'Based On'" msgstr "" -#: erpnext/selling/report/inactive_customers/inactive_customers.py:18 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:20 msgid "Invalid value {0} for 'Doctype'" msgstr "" @@ -24471,7 +24554,7 @@ msgstr "" msgid "Invalid {0}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2393 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2403 msgid "Invalid {0} for Inter Company Transaction." msgstr "" @@ -24581,7 +24664,7 @@ msgstr "" msgid "Invoice Document Type Selection Error" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1188 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1208 msgid "Invoice Grand Total" msgstr "" @@ -24681,7 +24764,7 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:171 #: erpnext/accounts/report/accounts_payable/accounts_payable.html:139 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:140 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1190 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1210 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:164 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py:194 msgid "Invoiced Amount" @@ -24701,7 +24784,7 @@ msgstr "" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json #: erpnext/accounts/doctype/pos_profile/pos_profile.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2444 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2454 #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py:62 msgid "Invoices" @@ -25361,7 +25444,7 @@ msgstr "" #. Label of the issues_sb (Section Break) field in DocType 'Support Settings' #. Label of a Card Break in the Support Workspace -#: erpnext/support/doctype/issue/issue.py:181 +#: erpnext/support/doctype/issue/issue.py:183 #: erpnext/support/doctype/support_settings/support_settings.json #: erpnext/support/workspace/support/support.json msgid "Issues" @@ -25426,7 +25509,7 @@ msgstr "" #: erpnext/accounts/doctype/subscription_plan/subscription_plan.json #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js:15 -#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:32 +#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:33 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js:22 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js:59 #: erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -25438,7 +25521,7 @@ msgstr "" #: erpnext/buying/workspace/buying/buying.json #: erpnext/controllers/taxes_and_totals.py:1262 #: erpnext/manufacturing/doctype/blanket_order/blanket_order.json -#: erpnext/manufacturing/doctype/bom/bom.js:1085 +#: erpnext/manufacturing/doctype/bom/bom.js:1088 #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:109 #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:25 #: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:101 @@ -25483,6 +25566,7 @@ msgstr "" #: erpnext/stock/report/item_prices/item_prices.py:50 #: erpnext/stock/report/item_shortage_report/item_shortage_report.py:88 #: erpnext/stock/report/item_variant_details/item_variant_details.js:10 +#: erpnext/stock/report/item_where_used/item_where_used.js:8 #: erpnext/stock/report/item_wise_consumption/item_wise_consumption.py:57 #: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:53 #: erpnext/stock/report/product_bundle_balance/product_bundle_balance.js:24 @@ -25493,8 +25577,8 @@ msgstr "" #: erpnext/stock/report/stock_ageing/stock_ageing.js:46 #: erpnext/stock/report/stock_analytics/stock_analytics.js:15 #: erpnext/stock/report/stock_analytics/stock_analytics.py:43 -#: erpnext/stock/report/stock_balance/stock_balance.py:473 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:287 +#: erpnext/stock/report/stock_balance/stock_balance.py:467 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:288 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:27 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:51 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:28 @@ -25541,6 +25625,7 @@ msgstr "" #. Label of a Link in the Stock Workspace #. Label of a Workspace Sidebar Item #: erpnext/stock/doctype/item_alternative/item_alternative.json +#: erpnext/stock/report/item_where_used/item_where_used.py:410 #: erpnext/stock/workspace/stock/stock.json #: erpnext/workspace_sidebar/stock.json msgid "Item Alternative" @@ -25690,7 +25775,7 @@ msgstr "" #: erpnext/accounts/doctype/pricing_rule_item_code/pricing_rule_item_code.json #: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json #: erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:295 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:314 #: erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py:68 #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:37 #: erpnext/accounts/report/gross_profit/gross_profit.py:312 @@ -25732,7 +25817,7 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order_additional_item/work_order_additional_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json #: erpnext/manufacturing/doctype/workstation/workstation.js:471 -#: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:159 +#: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:163 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:60 #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js:8 #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py:103 @@ -25767,7 +25852,7 @@ msgstr "" #: erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py:19 #: erpnext/selling/report/sales_order_analysis/sales_order_analysis.py:241 #: erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py:33 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:87 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:96 #: erpnext/stock/doctype/bin/bin.json #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json #: erpnext/stock/doctype/item/item.json @@ -25809,7 +25894,7 @@ msgstr "" #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.js:8 #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:433 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:7 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:171 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:177 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:104 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:25 #: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:26 @@ -25842,7 +25927,7 @@ msgstr "" msgid "Item Code cannot be changed for Serial No." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:446 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:451 msgid "Item Code required at Row No {0}" msgstr "" @@ -25952,7 +26037,7 @@ msgstr "" #: erpnext/accounts/report/gross_profit/gross_profit.js:44 #: erpnext/accounts/report/gross_profit/gross_profit.py:325 #: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js:21 -#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:28 +#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:29 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js:28 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:162 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js:65 @@ -25983,7 +26068,7 @@ msgstr "" #: erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.js:41 #: erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py:35 #: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js:41 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:94 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:103 #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/doctype/authorization_rule/authorization_rule.json #: erpnext/setup/doctype/item_group/item_group.json @@ -26008,13 +26093,13 @@ msgstr "" #: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:55 #: erpnext/stock/report/product_bundle_balance/product_bundle_balance.js:37 #: erpnext/stock/report/product_bundle_balance/product_bundle_balance.py:100 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:180 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:186 #: erpnext/stock/report/stock_analytics/stock_analytics.js:8 #: erpnext/stock/report/stock_analytics/stock_analytics.py:52 #: erpnext/stock/report/stock_balance/stock_balance.js:32 -#: erpnext/stock/report/stock_balance/stock_balance.py:481 +#: erpnext/stock/report/stock_balance/stock_balance.py:475 #: erpnext/stock/report/stock_ledger/stock_ledger.js:71 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:345 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:346 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:39 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:113 #: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js:33 @@ -26182,7 +26267,7 @@ msgstr "" #: erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py:74 #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:71 #: erpnext/accounts/report/gross_profit/gross_profit.py:319 -#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:33 +#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:34 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:154 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:173 #: erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py:71 @@ -26232,7 +26317,7 @@ msgstr "" #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:92 #: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py:138 #: erpnext/public/js/controllers/transaction.js:2842 -#: erpnext/public/js/utils.js:831 +#: erpnext/public/js/utils.js:832 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:1286 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -26271,10 +26356,10 @@ msgstr "" #: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:54 #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:131 #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:440 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:177 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:183 #: erpnext/stock/report/stock_analytics/stock_analytics.py:45 -#: erpnext/stock/report/stock_balance/stock_balance.py:479 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:293 +#: erpnext/stock/report/stock_balance/stock_balance.py:473 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:294 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:110 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:31 #: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:32 @@ -26500,6 +26585,7 @@ msgstr "" #. Name of a DocType #: erpnext/stock/doctype/item_variant/item_variant.json +#: erpnext/stock/report/item_where_used/item_where_used.py:387 msgid "Item Variant" msgstr "" @@ -26575,6 +26661,11 @@ msgstr "" msgid "Item Weight Details" msgstr "" +#. Name of a report +#: erpnext/stock/report/item_where_used/item_where_used.json +msgid "Item Where Used" +msgstr "" + #. Label of a Link in the Buying Workspace #. Name of a report #. Label of a Workspace Sidebar Item @@ -26633,7 +26724,7 @@ msgstr "" msgid "Item and Warranty Details" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3592 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3743 msgid "Item for row {0} does not match Material Request" msgstr "" @@ -26667,7 +26758,7 @@ msgstr "" msgid "Item qty can not be updated as raw materials are already processed." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1326 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1431 msgid "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}" msgstr "" @@ -26711,7 +26802,7 @@ msgstr "" msgid "Item {0} does not exist" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:709 +#: erpnext/manufacturing/doctype/bom/bom.py:716 msgid "Item {0} does not exist in the system or has expired" msgstr "" @@ -26779,7 +26870,7 @@ msgstr "" msgid "Item {0} is not a template item." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2316 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2467 msgid "Item {0} is not active or end of life has been reached" msgstr "" @@ -26799,7 +26890,7 @@ msgstr "" msgid "Item {0} must be a non-stock item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1667 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1772 msgid "Item {0} not found in 'Raw Materials Supplied' table in {1} {2}" msgstr "" @@ -26815,7 +26906,7 @@ msgstr "" msgid "Item {0}: {1} qty produced. " msgstr "" -#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1377 +#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1218 msgid "Item {} does not exist." msgstr "" @@ -26865,7 +26956,7 @@ msgstr "" msgid "Item/Item Code required to get Item Tax Template." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:452 +#: erpnext/manufacturing/doctype/bom/bom.py:459 msgid "Item: {0} does not exist in the system" msgstr "" @@ -26925,7 +27016,7 @@ msgstr "" msgid "Items not found." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1322 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1427 msgid "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}" msgstr "" @@ -27000,7 +27091,7 @@ msgstr "" #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/job_card/job_card.json -#: erpnext/manufacturing/doctype/job_card/job_card.py:1003 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1016 #: erpnext/manufacturing/doctype/operation/operation.json #: erpnext/manufacturing/doctype/work_order/work_order.js:408 #: erpnext/manufacturing/doctype/work_order/work_order.json @@ -27029,6 +27120,10 @@ msgstr "" msgid "Job Card Item" msgstr "" +#: erpnext/manufacturing/doctype/job_card/job_card.py:876 +msgid "Job Card On Hold" +msgstr "" + #. Name of a DocType #: erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json msgid "Job Card Operation" @@ -27064,7 +27159,7 @@ msgstr "" msgid "Job Card and Capacity Planning" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1490 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1530 msgid "Job Card {0} has been completed" msgstr "" @@ -27140,7 +27235,7 @@ msgstr "" msgid "Job Worker Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2768 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2860 msgid "Job card {0} created" msgstr "" @@ -27167,7 +27262,7 @@ msgstr "" msgid "Journal Entries" msgstr "" -#: erpnext/accounts/utils.py:1064 +#: erpnext/accounts/utils.py:1065 msgid "Journal Entries {0} are un-linked" msgstr "" @@ -27357,7 +27452,7 @@ msgstr "" msgid "Kilowatt-Hour" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1005 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1018 msgid "Kindly cancel the Manufacturing Entries first against the work order {0}." msgstr "" @@ -27481,7 +27576,7 @@ msgstr "" msgid "Last Completion Date" msgstr "" -#: erpnext/accounts/doctype/account/account.py:659 +#: erpnext/accounts/doctype/account/account.py:661 msgid "Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying." msgstr "" @@ -27494,12 +27589,12 @@ msgstr "" msgid "Last Month Downtime Analysis" msgstr "" -#: erpnext/selling/report/inactive_customers/inactive_customers.py:84 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:105 msgid "Last Order Amount" msgstr "" -#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:44 -#: erpnext/selling/report/inactive_customers/inactive_customers.py:85 +#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:45 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:106 msgid "Last Order Date" msgstr "" @@ -27559,11 +27654,11 @@ msgstr "" msgid "Last transacted" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:216 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:222 msgid "Latest" msgstr "" -#: erpnext/stock/report/stock_balance/stock_balance.py:592 +#: erpnext/stock/report/stock_balance/stock_balance.py:586 msgid "Latest Age" msgstr "" @@ -27599,7 +27694,7 @@ msgstr "" msgid "Lead" msgstr "" -#: erpnext/crm/doctype/lead/lead.py:549 +#: erpnext/crm/doctype/lead/lead.py:545 msgid "Lead -> Prospect" msgstr "" @@ -27693,7 +27788,7 @@ msgstr "" msgid "Lead Type" msgstr "" -#: erpnext/crm/doctype/lead/lead.py:548 +#: erpnext/crm/doctype/lead/lead.py:544 msgid "Lead {0} has been added to prospect {1}." msgstr "" @@ -28248,7 +28343,7 @@ msgstr "" #: erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json #: erpnext/accounts/doctype/loyalty_program/loyalty_program.json #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1206 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1225 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/selling/doctype/customer/customer.json #: erpnext/selling/page/point_of_sale/pos_item_cart.js:963 @@ -28700,7 +28795,7 @@ msgstr "" msgid "Mandatory Accounting Dimension" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1912 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1922 msgid "Mandatory Field" msgstr "" @@ -28720,11 +28815,11 @@ msgstr "" msgid "Mandatory Missing" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:629 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:634 msgid "Mandatory Purchase Order" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:651 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:656 msgid "Mandatory Purchase Receipt" msgstr "" @@ -28799,8 +28894,8 @@ msgstr "" #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json #: erpnext/stock/doctype/stock_entry/stock_entry.json -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1411 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1427 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1516 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1532 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json #: erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -28949,7 +29044,7 @@ msgstr "" msgid "Manufacturing Manager" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2674 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2825 msgid "Manufacturing Quantity is mandatory" msgstr "" @@ -29026,7 +29121,7 @@ msgstr "" msgid "Mapping Subcontracting Order ..." msgstr "" -#: erpnext/public/js/utils.js:1066 +#: erpnext/public/js/utils.js:1067 msgid "Mapping {0} ..." msgstr "" @@ -29167,6 +29262,10 @@ msgstr "" msgid "Masters" msgstr "" +#: erpnext/stock/report/item_where_used/item_where_used.py:57 +msgid "Matched Field" +msgstr "" + #: erpnext/projects/doctype/project/project_dashboard.py:14 msgid "Material" msgstr "" @@ -29179,12 +29278,12 @@ msgstr "" #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type' #: erpnext/setup/setup_wizard/operations/install_fixtures.py:114 #: erpnext/stock/doctype/stock_entry/stock_entry.json -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1412 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1517 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json msgid "Material Consumption for Manufacture" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:666 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:682 msgid "Material Consumption is not set in Manufacturing Settings." msgstr "" @@ -29277,8 +29376,8 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.json #: erpnext/stock/doctype/pick_list_item/pick_list_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:287 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:443 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:303 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:459 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json #: erpnext/stock/doctype/stock_settings/stock_settings.js:153 #: erpnext/stock/workspace/stock/stock.json @@ -29362,7 +29461,7 @@ msgstr "" msgid "Material Request already created for the ordered quantity" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:1861 +#: erpnext/selling/doctype/sales_order/sales_order.py:1851 msgid "Material Request not created, as quantity for Raw Materials already available." msgstr "" @@ -29575,7 +29674,7 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order/work_order.js:1059 #: erpnext/manufacturing/doctype/work_order/work_order.js:1082 #: erpnext/stock/doctype/pick_list/pick_list.js:208 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:382 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:398 msgid "Max: {0}" msgstr "" @@ -29601,11 +29700,11 @@ msgstr "" msgid "Maximum Producible Items" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4203 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4354 msgid "Maximum Samples - {0} can be retained for Batch {1} and Item {2}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4194 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4345 msgid "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}." msgstr "" @@ -29711,7 +29810,7 @@ msgstr "" msgid "Merge similar Account Heads" msgstr "" -#: erpnext/public/js/utils.js:1098 +#: erpnext/public/js/utils.js:1099 msgid "Merge taxes from multiple documents" msgstr "" @@ -29724,7 +29823,7 @@ msgstr "" msgid "Merged" msgstr "" -#: erpnext/accounts/doctype/account/account.py:602 +#: erpnext/accounts/doctype/account/account.py:604 msgid "Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency" msgstr "" @@ -29773,6 +29872,10 @@ msgstr "" msgid "Meter/Second" msgstr "" +#: erpnext/manufacturing/doctype/workstation/workstation.py:546 +msgid "Method {0} is not allowed to be run on a Job Card." +msgstr "" + #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json msgid "Microbar" @@ -30045,20 +30148,20 @@ msgstr "" msgid "Mismatch" msgstr "" -#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1378 +#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1219 msgid "Missing" 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:588 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2461 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3069 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:593 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2471 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3087 #: erpnext/assets/doctype/asset_category/asset_category.py:116 msgid "Missing Account" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:445 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:451 msgid "Missing Asset" msgstr "" @@ -30067,7 +30170,7 @@ msgstr "" msgid "Missing Cost Center" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1148 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1163 msgid "Missing Default in Company" msgstr "" @@ -30079,7 +30182,7 @@ msgstr "" msgid "Missing Finance Book" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1864 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1969 msgid "Missing Finished Good" msgstr "" @@ -30087,11 +30190,11 @@ msgstr "" msgid "Missing Formula" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1162 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1267 msgid "Missing Item" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:568 +#: erpnext/setup/doctype/employee/employee.py:574 msgid "Missing Parameter" msgstr "" @@ -30119,7 +30222,7 @@ msgstr "" msgid "Missing required filter: {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1219 +#: erpnext/manufacturing/doctype/bom/bom.py:1226 #: erpnext/manufacturing/doctype/work_order/work_order.py:1563 msgid "Missing value" msgstr "" @@ -30361,7 +30464,7 @@ msgstr "" msgid "Multiple Loyalty Programs found for Customer {}. Please select manually." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1224 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1234 msgid "Multiple POS Opening Entry" msgstr "" @@ -30387,7 +30490,7 @@ msgstr "" msgid "Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1871 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1976 msgid "Multiple items cannot be marked as finished item" msgstr "" @@ -30399,7 +30502,7 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order/work_order.py:1510 #: erpnext/setup/doctype/uom/uom.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:267 -#: erpnext/utilities/transaction_base.py:629 +#: erpnext/utilities/transaction_base.py:634 msgid "Must be Whole Number" msgstr "" @@ -30488,7 +30591,7 @@ msgstr "" msgid "Naming Series updated" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:937 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:939 msgid "Naming series '{0}' for DocType '{1}' does not contain standard '.' or '{{' separator. Using fallback extraction." msgstr "" @@ -30630,40 +30733,40 @@ msgstr "" msgid "Net Asset value as on" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:178 +#: erpnext/accounts/report/cash_flow/cash_flow.py:185 msgid "Net Cash from Financing" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:171 +#: erpnext/accounts/report/cash_flow/cash_flow.py:178 msgid "Net Cash from Investing" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:159 +#: erpnext/accounts/report/cash_flow/cash_flow.py:166 msgid "Net Cash from Operations" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:164 +#: erpnext/accounts/report/cash_flow/cash_flow.py:171 msgid "Net Change in Accounts Payable" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:163 +#: erpnext/accounts/report/cash_flow/cash_flow.py:170 msgid "Net Change in Accounts Receivable" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:135 +#: erpnext/accounts/report/cash_flow/cash_flow.py:137 #: erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py:257 msgid "Net Change in Cash" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:180 +#: erpnext/accounts/report/cash_flow/cash_flow.py:187 msgid "Net Change in Equity" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:173 +#: erpnext/accounts/report/cash_flow/cash_flow.py:180 msgid "Net Change in Fixed Asset" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:165 +#: erpnext/accounts/report/cash_flow/cash_flow.py:172 msgid "Net Change in Inventory" msgstr "" @@ -31013,7 +31116,7 @@ msgstr "" msgid "New Task" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:244 +#: erpnext/manufacturing/doctype/bom/bom.js:247 msgid "New Version" msgstr "" @@ -31093,12 +31196,12 @@ msgstr "" msgid "No Answer" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2566 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2576 msgid "No Customer found for Inter Company Transactions which represents company {0}" msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:164 -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:430 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:431 msgid "No Customers found with selected options." msgstr "" @@ -31106,7 +31209,7 @@ msgstr "" msgid "No Delivery Note selected for Customer {}" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:754 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:756 msgid "No DocTypes in To Delete list. Please generate or import the list before submitting." msgstr "" @@ -31170,7 +31273,7 @@ msgstr "" msgid "No Selection" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:972 +#: erpnext/controllers/sales_and_purchase_return.py:973 msgid "No Serial / Batches are available for return" msgstr "" @@ -31182,7 +31285,7 @@ msgstr "" msgid "No Summary" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2550 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2560 msgid "No Supplier found for Inter Company Transactions which represents company {0}" msgstr "" @@ -31207,7 +31310,7 @@ msgid "No Unreconciled Payments found for this party" msgstr "" #: erpnext/manufacturing/doctype/production_plan/production_plan.py:790 -#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:249 +#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:250 msgid "No Work Orders were created" msgstr "" @@ -31232,11 +31335,11 @@ msgstr "" msgid "No available quantity to reserve for item {0} in warehouse {1}" msgstr "" -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:495 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:496 msgid "No billing email found for customer: {0}" msgstr "" -#: erpnext/stock/doctype/delivery_trip/delivery_trip.py:449 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.py:452 msgid "No contacts with email IDs found." msgstr "" @@ -31385,7 +31488,7 @@ msgstr "" msgid "No open Material Requests found for the given criteria." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1218 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1228 msgid "No open POS Opening Entry found for POS Profile {0}." msgstr "" @@ -31405,7 +31508,7 @@ msgstr "" msgid "No outstanding invoices require exchange rate revaluation" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2430 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2448 msgid "No outstanding {0} found for the {1} {2} which qualify the filters you have specified." msgstr "" @@ -31413,7 +31516,7 @@ msgstr "" msgid "No pending Material Requests found to link for the given items." msgstr "" -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:502 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:503 msgid "No primary email found for customer: {0}" msgstr "" @@ -31474,7 +31577,7 @@ msgstr "" msgid "No values" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2614 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2624 msgid "No {0} found for Inter Company Transactions." msgstr "" @@ -31516,7 +31619,7 @@ msgstr "" msgid "Non Profit" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1635 +#: erpnext/manufacturing/doctype/bom/bom.py:1642 msgid "Non stock items" msgstr "" @@ -31609,7 +31712,7 @@ msgstr "" msgid "Not Started" msgstr "" -#: erpnext/accounts/report/cash_flow/cash_flow.py:406 +#: erpnext/accounts/report/cash_flow/cash_flow.py:425 msgid "Not able to find the earliest Fiscal Year for the given company." msgstr "" @@ -31653,7 +31756,7 @@ msgstr "" msgid "Note: Automatic log deletion only applies to logs of type Update Cost" msgstr "" -#: erpnext/accounts/party.py:685 +#: erpnext/accounts/party.py:703 msgid "Note: Due Date exceeds allowed {0} credit days by {1} day(s)" msgstr "" @@ -31663,7 +31766,7 @@ msgstr "" msgid "Note: Email will not be sent to disabled users" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:793 +#: erpnext/manufacturing/doctype/bom/bom.py:800 msgid "Note: If you want to use the finished good {0} as a raw material, then enable the 'Do Not Explode' checkbox in the Items table against the same raw material." msgstr "" @@ -31803,7 +31906,7 @@ msgstr "" msgid "Number of Interaction" msgstr "" -#: erpnext/selling/report/inactive_customers/inactive_customers.py:81 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:102 msgid "Number of Order" msgstr "" @@ -32081,7 +32184,7 @@ msgstr "" msgid "Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1070 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1072 msgid "Only CSV files are allowed" msgstr "" @@ -32137,7 +32240,7 @@ msgstr "" msgid "Only one operation can have 'Is Final Finished Good' checked when 'Track Semi Finished Goods' is enabled." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1426 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1531 msgid "Only one {0} entry can be created against the Work Order {1}" msgstr "" @@ -32404,8 +32507,8 @@ msgstr "" msgid "Opening Invoice Tool" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1674 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2021 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1683 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2031 msgid "Opening Invoice has rounding adjustment of {0}.

      '{1}' account is required to post these values. Please set it in Company: {2}.

      Or, '{3}' can be enabled to not post any rounding adjustment." msgstr "" @@ -32431,7 +32534,7 @@ msgid "Opening Purchase Invoices have been created." msgstr "" #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:81 -#: erpnext/stock/report/stock_balance/stock_balance.py:532 +#: erpnext/stock/report/stock_balance/stock_balance.py:526 msgid "Opening Qty" msgstr "" @@ -32459,7 +32562,7 @@ msgstr "" msgid "Opening Time" msgstr "" -#: erpnext/stock/report/stock_balance/stock_balance.py:539 +#: erpnext/stock/report/stock_balance/stock_balance.py:533 msgid "Opening Value" msgstr "" @@ -32504,7 +32607,7 @@ msgstr "" msgid "Operating Cost Per BOM Quantity" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1740 +#: erpnext/manufacturing/doctype/bom/bom.py:1747 msgid "Operating Cost as per Work Order / BOM" msgstr "" @@ -32599,11 +32702,11 @@ msgstr "" msgid "Operation {0} added multiple times in the work order {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1254 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1267 msgid "Operation {0} does not belong to the work order {1}" msgstr "" -#: erpnext/manufacturing/doctype/workstation/workstation.py:432 +#: erpnext/manufacturing/doctype/workstation/workstation.py:443 msgid "Operation {0} longer than any available working hours in workstation {1}, break down the operation into multiple operations" msgstr "" @@ -32629,7 +32732,7 @@ msgstr "" msgid "Operations Routing" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1228 +#: erpnext/manufacturing/doctype/bom/bom.py:1235 msgid "Operations cannot be left blank" msgstr "" @@ -33058,12 +33161,12 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:119 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:83 -#: erpnext/stock/report/stock_balance/stock_balance.py:554 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:324 +#: erpnext/stock/report/stock_balance/stock_balance.py:548 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:325 msgid "Out Qty" msgstr "" -#: erpnext/stock/report/stock_balance/stock_balance.py:560 +#: erpnext/stock/report/stock_balance/stock_balance.py:554 msgid "Out Value" msgstr "" @@ -33097,7 +33200,7 @@ msgstr "" msgid "Out of stock" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1231 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1241 #: erpnext/selling/page/point_of_sale/pos_controller.js:208 msgid "Outdated POS Opening Entry" msgstr "" @@ -33116,6 +33219,7 @@ msgstr "" #. Label of the outgoing_rate (Currency) field in DocType 'Stock Ledger Entry' #: erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +#: erpnext/stock/report/stock_ledger/stock_ledger.py:379 msgid "Outgoing Rate" msgstr "" @@ -33160,7 +33264,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/report/accounts_payable/accounts_payable.html:140 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:141 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1197 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1217 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:167 #: erpnext/accounts/report/purchase_register/purchase_register.py:289 #: erpnext/accounts/report/sales_register/sales_register.py:319 @@ -33268,7 +33372,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:283 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:289 #: erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json #: erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json #: erpnext/projects/doctype/task/task.json @@ -33551,7 +33655,7 @@ msgstr "" msgid "POS Opening Entry" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1232 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1242 msgid "POS Opening Entry - {0} is outdated. Please close the POS and create a new POS Opening Entry." msgstr "" @@ -33572,7 +33676,7 @@ msgstr "" msgid "POS Opening Entry Exists" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1217 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1227 msgid "POS Opening Entry Missing" msgstr "" @@ -33608,7 +33712,7 @@ msgstr "" msgid "POS Profile" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1225 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1235 msgid "POS Profile - {0} has multiple open POS Opening Entries. Please close or cancel the existing entries before proceeding." msgstr "" @@ -33626,11 +33730,11 @@ msgstr "" msgid "POS Profile doesn't match {}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1185 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1195 msgid "POS Profile is mandatory to mark this invoice as POS Transaction." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1414 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1424 msgid "POS Profile required to make POS Entry" msgstr "" @@ -33796,7 +33900,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:289 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:295 msgid "Paid" msgstr "" @@ -33814,7 +33918,7 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:173 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1191 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1211 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:165 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py:201 #: erpnext/accounts/report/pos_register/pos_register.py:209 @@ -33849,7 +33953,7 @@ msgstr "" msgid "Paid Amount After Tax (Company Currency)" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1944 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1959 msgid "Paid Amount cannot be greater than total negative outstanding amount {0}" msgstr "" @@ -33863,8 +33967,8 @@ msgstr "" msgid "Paid To Account Type" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:329 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1181 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:334 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1191 msgid "Paid amount + Write Off Amount can not be greater than Grand Total" msgstr "" @@ -34020,7 +34124,7 @@ msgstr "" msgid "Parent Row No" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:548 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:610 msgid "Parent Row No not found for {0}" msgstr "" @@ -34076,7 +34180,7 @@ msgstr "" msgid "Partial Material Transferred" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1204 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1214 msgid "Partial Payment in POS Transactions are not allowed." msgstr "" @@ -34269,7 +34373,7 @@ msgstr "" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:105 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:82 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:65 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1127 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1147 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:82 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:147 #: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js:49 @@ -34297,7 +34401,7 @@ msgstr "" #. Name of a DocType #: erpnext/accounts/doctype/party_account/party_account.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1138 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1158 msgid "Party Account" msgstr "" @@ -34454,7 +34558,7 @@ msgstr "" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:92 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:69 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:52 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1121 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1141 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:69 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:141 #: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js:42 @@ -34477,7 +34581,7 @@ msgstr "" msgid "Party Type" msgstr "" -#: erpnext/accounts/party.py:814 +#: erpnext/accounts/party.py:832 msgid "Party Type and Party can only be set for Receivable / Payable account

      {0}" msgstr "" @@ -34489,8 +34593,8 @@ msgstr "" msgid "Party Type and Party is required for Receivable / Payable account {0}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:520 -#: erpnext/accounts/party.py:415 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:535 +#: erpnext/accounts/party.py:432 msgid "Party Type is mandatory" msgstr "" @@ -34503,7 +34607,7 @@ msgstr "" msgid "Party can only be one of {0}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:523 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:538 msgid "Party is mandatory" msgstr "" @@ -34546,7 +34650,7 @@ msgstr "" msgid "Pause" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:660 +#: erpnext/manufacturing/doctype/job_card/job_card.js:664 msgid "Pause Job" msgstr "" @@ -34597,7 +34701,7 @@ msgid "Payable" msgstr "" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:50 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1136 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1156 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:209 #: erpnext/accounts/report/purchase_register/purchase_register.py:194 #: erpnext/accounts/report/purchase_register/purchase_register.py:235 @@ -34707,7 +34811,7 @@ msgstr "" msgid "Payment Entries" msgstr "" -#: erpnext/accounts/utils.py:1151 +#: erpnext/accounts/utils.py:1152 msgid "Payment Entries {0} are un-linked" msgstr "" @@ -34752,7 +34856,7 @@ msgstr "" msgid "Payment Entry already exists" msgstr "" -#: erpnext/accounts/utils.py:650 +#: erpnext/accounts/utils.py:651 msgid "Payment Entry has been modified after you pulled it. Please pull it again." msgstr "" @@ -34795,7 +34899,7 @@ msgstr "" msgid "Payment Gateway Account" msgstr "" -#: erpnext/accounts/utils.py:1509 +#: erpnext/accounts/utils.py:1510 msgid "Payment Gateway Account not created, please create one manually." msgstr "" @@ -35066,7 +35170,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_schedule/payment_schedule.json #: erpnext/accounts/doctype/payment_term/payment_term.json #: erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1187 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1207 #: erpnext/accounts/report/gross_profit/gross_profit.py:449 #: erpnext/accounts/workspace/invoicing/invoicing.json #: erpnext/public/js/controllers/transaction.js:501 @@ -35164,7 +35268,7 @@ msgstr "" msgid "Payment Type" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:609 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:624 msgid "Payment Type must be one of Receive, Pay and Internal Transfer" msgstr "" @@ -35173,7 +35277,7 @@ msgstr "" msgid "Payment URL" msgstr "" -#: erpnext/accounts/utils.py:1139 +#: erpnext/accounts/utils.py:1140 msgid "Payment Unlink Error" msgstr "" @@ -35189,7 +35293,7 @@ msgstr "" msgid "Payment methods are mandatory. Please add at least one payment method." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3073 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3091 msgid "Payment methods refreshed. Please review before proceeding." msgstr "" @@ -35210,7 +35314,7 @@ msgstr "" msgid "Payment request failed" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:823 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:838 msgid "Payment term {0} not used in {1}" msgstr "" @@ -35368,11 +35472,11 @@ msgstr "" msgid "Pending processing" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1463 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1503 msgid "Pending quantity cannot be greater than the for quantity." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1457 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1497 msgid "Pending quantity cannot be negative." msgstr "" @@ -35626,7 +35730,7 @@ msgstr "" #: erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json #: erpnext/maintenance/doctype/maintenance_schedule_item/maintenance_schedule_item.json #: erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js:54 -#: erpnext/public/js/financial_statements.js:438 +#: erpnext/public/js/financial_statements.js:451 msgid "Periodicity" msgstr "" @@ -36058,7 +36162,7 @@ msgstr "" msgid "Please Select a Company." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:420 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:439 #: erpnext/stock/doctype/delivery_note/delivery_note.js:162 #: erpnext/stock/doctype/delivery_note/delivery_note.js:204 msgid "Please Select a Customer" @@ -36138,11 +36242,11 @@ msgstr "" msgid "Please attach CSV file" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3219 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3237 msgid "Please cancel and amend the Payment Entry" msgstr "" -#: erpnext/accounts/utils.py:1138 +#: erpnext/accounts/utils.py:1139 msgid "Please cancel payment entry manually first" msgstr "" @@ -36277,28 +36381,28 @@ msgstr "" msgid "Please enable {} in {} to allow same item in multiple rows" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:376 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:381 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:384 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:389 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 "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1044 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1054 msgid "Please ensure {} account is a Balance Sheet account." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1054 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1064 msgid "Please ensure {} account {} is a Receivable account." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:841 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:859 msgid "Please enter Difference Account or set default Stock Adjustment Account for company {0}" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:555 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1316 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1326 msgid "Please enter Account for Change Amount" msgstr "" @@ -36310,7 +36414,7 @@ msgstr "" msgid "Please enter Batch No" msgstr "" -#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:976 +#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:960 msgid "Please enter Cost Center" msgstr "" @@ -36322,12 +36426,12 @@ msgstr "" msgid "Please enter Employee Id of this sales person" msgstr "" -#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:985 +#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:969 msgid "Please enter Expense Account" msgstr "" #: erpnext/assets/doctype/asset_capitalization/asset_capitalization.js:84 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:97 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:99 msgid "Please enter Item Code to get Batch Number" msgstr "" @@ -36383,16 +36487,16 @@ msgstr "" msgid "Please enter Warehouse and Date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:655 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1312 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:660 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1322 msgid "Please enter Write Off Account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:665 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:670 msgid "Please enter a valid Write Off Account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:676 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:681 msgid "Please enter a valid Write Off Cost Center" msgstr "" @@ -36432,7 +36536,7 @@ msgstr "" msgid "Please enter quantity for item {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:297 +#: erpnext/setup/doctype/employee/employee.py:294 msgid "Please enter relieving date." msgstr "" @@ -36460,7 +36564,7 @@ msgstr "" msgid "Please enter valid Financial Year Start and End Dates" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:333 +#: erpnext/setup/doctype/employee/employee.py:338 msgid "Please enter {0}" msgstr "" @@ -36500,7 +36604,7 @@ msgstr "" msgid "Please import accounts against parent company or enable {} in company master." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:294 +#: erpnext/setup/doctype/employee/employee.py:291 msgid "Please make sure the employees above report to another Active employee." msgstr "" @@ -36567,7 +36671,7 @@ msgstr "" msgid "Please select Apply Discount On" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:1778 +#: erpnext/selling/doctype/sales_order/sales_order.py:1768 msgid "Please select BOM against item {0}" msgstr "" @@ -36621,8 +36725,8 @@ msgstr "" msgid "Please select Existing Company for creating Chart of Accounts" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:210 -#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:280 +#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:211 +#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:281 msgid "Please select Finished Good Item for Service Item {0}" msgstr "" @@ -36655,11 +36759,11 @@ msgstr "" msgid "Please select Posting Date first" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1292 +#: erpnext/manufacturing/doctype/bom/bom.py:1299 msgid "Please select Price List" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:1780 +#: erpnext/selling/doctype/sales_order/sales_order.py:1770 msgid "Please select Qty against item {0}" msgstr "" @@ -36679,7 +36783,7 @@ msgstr "" msgid "Please select Stock Asset Account" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1790 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1895 msgid "Please select Subcontracting Order instead of Purchase Order {0}" msgstr "" @@ -36687,17 +36791,17 @@ msgstr "" 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:1547 +#: erpnext/manufacturing/doctype/bom/bom.py:1554 msgid "Please select a BOM" msgstr "" -#: erpnext/accounts/party.py:417 +#: erpnext/accounts/party.py:434 #: erpnext/stock/doctype/pick_list/pick_list.py:1741 msgid "Please select a Company" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:268 -#: erpnext/manufacturing/doctype/bom/bom.js:727 +#: erpnext/manufacturing/doctype/bom/bom.js:730 #: erpnext/manufacturing/doctype/bom/bom.py:279 #: erpnext/public/js/controllers/accounts.js:277 #: erpnext/public/js/controllers/transaction.js:3315 @@ -36712,7 +36816,7 @@ msgstr "" msgid "Please select a Delivery Note" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:152 +#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:153 msgid "Please select a Subcontracting Purchase Order." msgstr "" @@ -36724,7 +36828,7 @@ msgstr "" msgid "Please select a Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1615 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1655 msgid "Please select a Work Order first." msgstr "" @@ -36773,11 +36877,11 @@ msgstr "" msgid "Please select a transaction." msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:141 +#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:142 msgid "Please select a valid Purchase Order that has Service Items." msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:138 +#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:139 msgid "Please select a valid Purchase Order that is configured for Subcontracting." msgstr "" @@ -36876,7 +36980,7 @@ msgid "Please select the customer." msgstr "" #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py:43 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:54 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:58 msgid "Please select the document type first" msgstr "" @@ -36921,7 +37025,7 @@ msgstr "" msgid "Please set Account" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1912 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1922 msgid "Please set Account for Change Amount" msgstr "" @@ -36943,7 +37047,7 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:58 #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:68 #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:78 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:884 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:903 msgid "Please set Company" msgstr "" @@ -36973,11 +37077,11 @@ msgstr "" msgid "Please set Fixed Asset Account in Asset Category {0}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:585 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:590 msgid "Please set Fixed Asset Account in {} against {}." msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:292 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:296 msgid "Please set Parent Row No for item {0}" msgstr "" @@ -37019,11 +37123,11 @@ msgstr "" msgid "Please set a default Holiday List for Company {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:384 +#: erpnext/setup/doctype/employee/employee.py:389 msgid "Please set a default Holiday List for Employee {0} or Company {1}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1137 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1146 msgid "Please set account in Warehouse {0}" msgstr "" @@ -37052,23 +37156,23 @@ msgstr "" msgid "Please set both the Tax ID and Fiscal Code on Company {0}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2458 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2468 msgid "Please set default Cash or Bank account in Mode of Payment {0}" msgstr "" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:94 #: erpnext/accounts/doctype/pos_profile/pos_profile.py:197 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3066 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3084 msgid "Please set default Cash or Bank account in Mode of Payment {}" msgstr "" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:96 #: erpnext/accounts/doctype/pos_profile/pos_profile.py:199 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3068 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3086 msgid "Please set default Cash or Bank account in Mode of Payments {}" msgstr "" -#: erpnext/accounts/utils.py:2527 +#: erpnext/accounts/utils.py:2529 msgid "Please set default Exchange Gain/Loss Account in Company {}" msgstr "" @@ -37088,8 +37192,8 @@ msgstr "" msgid "Please set default inventory account for item {0}, or their item group or brand." msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:278 -#: erpnext/accounts/utils.py:1160 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:279 +#: erpnext/accounts/utils.py:1161 msgid "Please set default {0} in Company {1}" msgstr "" @@ -37121,11 +37225,11 @@ msgstr "" msgid "Please set the Item Code first" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1678 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1718 msgid "Please set the Target Warehouse in the Job Card" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1682 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1722 msgid "Please set the WIP Warehouse in the Job Card" msgstr "" @@ -37160,7 +37264,7 @@ msgstr "" msgid "Please set {0} in BOM Creator {1}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1145 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1160 msgid "Please set {0} in Company {1} to account for Exchange Gain / Loss" msgstr "" @@ -37182,7 +37286,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.js:120 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:430 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:617 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:636 msgid "Please specify Company to proceed" msgstr "" @@ -37376,7 +37480,7 @@ msgstr "" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:16 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:15 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:18 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1119 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1139 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:15 #: erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py:38 #: erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.html:7 @@ -37402,7 +37506,7 @@ msgstr "" #: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py:25 #: erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py:68 #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py:67 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:85 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:94 #: erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -37498,7 +37602,7 @@ msgstr "" msgid "Posting Time" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2624 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2775 msgid "Posting date and posting time is mandatory" msgstr "" @@ -37923,7 +38027,7 @@ msgstr "" msgid "Price is not set for the item." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:605 +#: erpnext/manufacturing/doctype/bom/bom.py:612 msgid "Price not found for item {0} in price list {1}" msgstr "" @@ -38131,11 +38235,11 @@ msgstr "" msgid "Primary Settings" msgstr "" -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:123 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:124 msgid "Print Format Type should be Jinja." msgstr "" -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:127 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:128 msgid "Print Format must be an enabled Report Print Format matching the selected Report." msgstr "" @@ -38174,7 +38278,7 @@ msgstr "" msgid "Print and Stationery" msgstr "" -#: erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js:75 +#: erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js:77 msgid "Print settings updated in respective print format" msgstr "" @@ -38307,7 +38411,7 @@ msgstr "" msgid "Process Loss %" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1272 +#: erpnext/manufacturing/doctype/bom/bom.py:1279 msgid "Process Loss Percentage cannot be greater than 100" msgstr "" @@ -38415,7 +38519,7 @@ msgstr "" msgid "Process in Single Transaction" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1460 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1500 msgid "Process loss quantity cannot be negative." msgstr "" @@ -38529,6 +38633,10 @@ msgstr "" msgid "Product Bundle Balance" msgstr "" +#: erpnext/stock/report/item_where_used/item_where_used.py:278 +msgid "Product Bundle Component" +msgstr "" + #. Label of the product_bundle_help (HTML) field in DocType 'POS Invoice' #. Label of the product_bundle_help (HTML) field in DocType 'Sales Invoice' #. Label of the product_bundle_help (HTML) field in DocType 'Delivery Note' @@ -38550,6 +38658,10 @@ msgstr "" msgid "Product Bundle Item" msgstr "" +#: erpnext/stock/report/item_where_used/item_where_used.py:305 +msgid "Product Bundle Parent" +msgstr "" + #. Label of the product_discount_scheme_section (Section Break) field in #. DocType 'Pricing Rule' #: erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -38741,7 +38853,7 @@ msgstr "" #: erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json #: erpnext/accounts/workspace/financial_reports/financial_reports.json #: erpnext/accounts/workspace/invoicing/invoicing.json -#: erpnext/public/js/financial_statements.js:330 +#: erpnext/public/js/financial_statements.js:343 #: erpnext/workspace_sidebar/financial_reports.json msgid "Profit and Loss" msgstr "" @@ -39097,7 +39209,7 @@ msgstr "" msgid "Prospect Owner" msgstr "" -#: erpnext/crm/doctype/lead/lead.py:313 +#: erpnext/crm/doctype/lead/lead.py:310 msgid "Prospect {0} already exists" msgstr "" @@ -39115,7 +39227,7 @@ msgid "Prospects Engaged But Not Converted" msgstr "" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:198 -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:784 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:786 msgid "Protected DocType" msgstr "" @@ -39321,7 +39433,7 @@ msgstr "" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js:30 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json #: erpnext/stock/doctype/quality_inspection/quality_inspection.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:424 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:440 #: erpnext/workspace_sidebar/buying.json #: erpnext/workspace_sidebar/invoicing.json msgid "Purchase Invoice" @@ -39371,7 +39483,7 @@ msgstr "" msgid "Purchase Invoice {0} is already submitted" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1961 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1970 msgid "Purchase Invoices" msgstr "" @@ -39500,11 +39612,11 @@ msgstr "" msgid "Purchase Order Pricing Rule" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:625 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:630 msgid "Purchase Order Required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:620 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:625 msgid "Purchase Order Required for item {}" msgstr "" @@ -39530,7 +39642,7 @@ msgstr "" msgid "Purchase Order {0} created" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:685 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:690 msgid "Purchase Order {0} is not submitted" msgstr "" @@ -39650,11 +39762,11 @@ msgstr "" msgid "Purchase Receipt No" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:647 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:652 msgid "Purchase Receipt Required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:642 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:647 msgid "Purchase Receipt Required for item {}" msgstr "" @@ -39682,7 +39794,7 @@ msgstr "" msgid "Purchase Receipt {0} created." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:692 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:697 msgid "Purchase Receipt {0} is not submitted" msgstr "" @@ -39801,14 +39913,14 @@ msgstr "" #: erpnext/stock/doctype/item/item_list.js:41 #: erpnext/stock/doctype/material_request/material_request.json #: erpnext/stock/doctype/pick_list/pick_list.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:459 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:475 #: erpnext/stock/doctype/stock_entry/stock_entry.json #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json msgid "Purpose" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:658 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:676 msgid "Purpose must be one of {0}" msgstr "" @@ -39874,7 +39986,7 @@ msgstr "" #: erpnext/controllers/trends.py:282 erpnext/controllers/trends.py:294 #: erpnext/controllers/trends.py:299 #: erpnext/crm/doctype/opportunity_item/opportunity_item.json -#: erpnext/manufacturing/doctype/bom/bom.js:1105 +#: erpnext/manufacturing/doctype/bom/bom.js:1108 #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json @@ -39891,7 +40003,7 @@ msgstr "" #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:398 #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:499 #: erpnext/public/js/stock_reservation.js:134 -#: erpnext/public/js/stock_reservation.js:336 erpnext/public/js/utils.js:869 +#: erpnext/public/js/stock_reservation.js:336 erpnext/public/js/utils.js:870 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:398 @@ -39910,6 +40022,7 @@ msgstr "" #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json #: erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py:195 +#: erpnext/stock/report/item_where_used/item_where_used.py:69 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:74 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:271 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:370 @@ -40004,7 +40117,7 @@ msgstr "" #. Label of the for_quantity (Float) field in DocType 'Job Card' #. Label of the qty (Float) field in DocType 'Work Order' -#: erpnext/manufacturing/doctype/bom/bom.js:405 +#: erpnext/manufacturing/doctype/bom/bom.js:408 #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/manufacturing/report/process_loss_report/process_loss_report.py:82 @@ -40114,7 +40227,7 @@ msgstr "" msgid "Qty to Deliver" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:379 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:395 msgid "Qty to Disassemble" msgstr "" @@ -40123,7 +40236,7 @@ msgid "Qty to Fetch" msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.js:247 -#: erpnext/manufacturing/doctype/job_card/job_card.py:892 +#: erpnext/manufacturing/doctype/job_card/job_card.py:905 msgid "Qty to Manufacture" msgstr "" @@ -40273,7 +40386,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json -#: erpnext/manufacturing/doctype/bom/bom.js:274 +#: erpnext/manufacturing/doctype/bom/bom.js:277 #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/quality_management/workspace/quality/quality.json @@ -40372,7 +40485,7 @@ msgid "Quality Inspection {0} is rejected for the item: {1}" msgstr "" #: erpnext/public/js/controllers/transaction.js:384 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:206 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:208 msgid "Quality Inspection(s)" msgstr "" @@ -40496,7 +40609,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json #: erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.json -#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:47 +#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:48 #: erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json #: erpnext/buying/doctype/purchase_order/purchase_order.js:787 #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -40507,13 +40620,13 @@ msgstr "" #: erpnext/buying/report/purchase_analytics/purchase_analytics.js:28 #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py:213 #: erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json -#: erpnext/manufacturing/doctype/bom/bom.js:493 +#: erpnext/manufacturing/doctype/bom/bom.js:496 #: erpnext/manufacturing/doctype/bom_creator/bom_creator.js:76 #: erpnext/manufacturing/doctype/bom_creator/bom_creator.json #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:194 #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:218 #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json -#: erpnext/public/js/controllers/buying.js:617 +#: erpnext/public/js/controllers/buying.js:620 #: erpnext/public/js/stock_analytics.js:50 #: erpnext/public/js/utils/serial_no_batch_selector.js:499 #: erpnext/selling/doctype/quotation_item/quotation_item.json @@ -40528,7 +40641,7 @@ msgstr "" #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/packing_slip_item/packing_slip_item.json #: erpnext/stock/doctype/pick_list_item/pick_list_item.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:807 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:823 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json #: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json #: erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py:36 @@ -40663,11 +40776,11 @@ msgstr "" msgid "Quantity must not be more than {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:773 +#: erpnext/manufacturing/doctype/bom/bom.py:780 msgid "Quantity required for Item {0} in row {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:717 +#: erpnext/manufacturing/doctype/bom/bom.py:724 #: erpnext/manufacturing/doctype/job_card/job_card.js:342 #: erpnext/manufacturing/doctype/job_card/job_card.js:410 #: erpnext/manufacturing/doctype/workstation/workstation.js:303 @@ -40678,7 +40791,7 @@ msgstr "" msgid "Quantity to Manufacture" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2706 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2798 msgid "Quantity to Manufacture can not be zero for the operation {0}" msgstr "" @@ -40761,7 +40874,7 @@ msgstr "" #. Label of a Link in the Selling Workspace #. Option for the 'Transaction' (Select) field in DocType 'Authorization Rule' #. Label of a Workspace Sidebar Item -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:383 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:402 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:51 #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.html:20 #: erpnext/crm/doctype/contract/contract.json @@ -40957,7 +41070,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/work_order_additional_item/work_order_additional_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/public/js/utils.js:879 +#: erpnext/public/js/utils.js:880 #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -41279,8 +41392,8 @@ msgstr "" #. Label of the section_break_8 (Section Break) field in DocType 'Job Card' #. Label of the mr_items (Table) field in DocType 'Production Plan' -#: erpnext/manufacturing/doctype/bom/bom.js:446 -#: erpnext/manufacturing/doctype/bom/bom.js:1078 +#: erpnext/manufacturing/doctype/bom/bom.js:449 +#: erpnext/manufacturing/doctype/bom/bom.js:1081 #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/production_plan/production_plan.json #: erpnext/manufacturing/doctype/workstation/workstation.js:462 @@ -41309,7 +41422,7 @@ msgstr "" msgid "Raw Materials Consumption" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:404 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:417 msgid "Raw Materials Missing" msgstr "" @@ -41343,7 +41456,7 @@ msgstr "" msgid "Raw Materials Supplied Cost" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:765 +#: erpnext/manufacturing/doctype/bom/bom.py:772 msgid "Raw Materials cannot be blank." msgstr "" @@ -41540,7 +41653,7 @@ msgid "Receivable / Payable Account" msgstr "" #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:79 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1134 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1154 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:241 #: erpnext/accounts/report/sales_register/sales_register.py:217 #: erpnext/accounts/report/sales_register/sales_register.py:271 @@ -41602,7 +41715,7 @@ msgstr "" msgid "Received Amount After Tax (Company Currency)" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:965 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:980 msgid "Received Amount cannot be greater than Paid Amount" msgstr "" @@ -41662,7 +41775,7 @@ msgstr "" msgid "Received Quantity" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:355 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:371 msgid "Received Stock Entries" msgstr "" @@ -41916,7 +42029,7 @@ msgstr "" msgid "Reference Detail No" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:659 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:674 msgid "Reference Doctype must be one of {0}" msgstr "" @@ -41944,7 +42057,7 @@ msgstr "" msgid "Reference No & Reference Date is required for {0}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1226 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1241 msgid "Reference No and Reference Date is mandatory for Bank transaction" msgstr "" @@ -42050,7 +42163,7 @@ msgstr "" msgid "References to Sales Orders are Incomplete" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:739 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:754 msgid "References {0} of type {1} had no outstanding amount left before submitting the Payment Entry. Now they have a negative outstanding amount." msgstr "" @@ -42168,6 +42281,10 @@ msgstr "" msgid "Related" msgstr "" +#: erpnext/stock/report/item_where_used/item_where_used.py:50 +msgid "Related Item" +msgstr "" + #. Label of the relation (Data) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Relation" @@ -42183,7 +42300,7 @@ msgstr "" msgid "Release Date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:320 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:325 msgid "Release date must be in the future" msgstr "" @@ -42201,7 +42318,7 @@ msgid "Remaining Amount" msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:189 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1209 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1229 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:178 msgid "Remaining Balance" msgstr "" @@ -42253,7 +42370,7 @@ msgstr "" #: erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html:11 #: erpnext/accounts/report/accounts_payable/accounts_payable.html:135 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:136 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1241 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1261 #: erpnext/accounts/report/general_ledger/general_ledger.html:163 #: erpnext/accounts/report/general_ledger/general_ledger.py:811 #: erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py:112 @@ -42338,9 +42455,9 @@ msgstr "" msgid "Renaming it is only allowed via parent company {0}, to avoid mismatch." msgstr "" -#: erpnext/manufacturing/doctype/workstation/test_workstation.py:78 -#: erpnext/manufacturing/doctype/workstation/test_workstation.py:89 -#: erpnext/manufacturing/doctype/workstation/test_workstation.py:116 +#: erpnext/manufacturing/doctype/workstation/test_workstation.py:90 +#: erpnext/manufacturing/doctype/workstation/test_workstation.py:101 +#: erpnext/manufacturing/doctype/workstation/test_workstation.py:128 #: erpnext/patches/v16_0/make_workstation_operating_components.py:49 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:316 msgid "Rent" @@ -42659,7 +42776,7 @@ msgstr "" msgid "Reqd Qty (BOM)" msgstr "" -#: erpnext/public/js/utils.js:895 +#: erpnext/public/js/utils.js:896 msgid "Reqd by date" msgstr "" @@ -43062,7 +43179,7 @@ msgstr "" #: erpnext/stock/doctype/bin/bin.json #: erpnext/stock/doctype/pick_list/pick_list.js:178 #: erpnext/stock/report/reserved_stock/reserved_stock.json -#: erpnext/stock/report/stock_balance/stock_balance.py:572 +#: erpnext/stock/report/stock_balance/stock_balance.py:566 #: erpnext/stock/stock_ledger.py:2290 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:205 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:333 @@ -43335,7 +43452,7 @@ msgstr "" msgid "Resume" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:659 +#: erpnext/manufacturing/doctype/job_card/job_card.js:663 msgid "Resume Job" msgstr "" @@ -43384,7 +43501,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:285 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:291 #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/delivery_note/delivery_note_list.js:16 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -43472,7 +43589,7 @@ msgstr "" msgid "Return Raw Material to Customer" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1555 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1565 msgid "Return invoice of asset cancelled" msgstr "" @@ -44010,12 +44127,12 @@ msgid "Row #1: Sequence ID must be 1 for Operation {0}." msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:564 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2113 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2123 msgid "Row #{0} (Payment Table): Amount must be negative" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:562 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2108 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2118 msgid "Row #{0} (Payment Table): Amount must be positive" msgstr "" @@ -44044,16 +44161,16 @@ msgstr "" msgid "Row #{0}: Account {1} does not belong to company {2}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:397 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:399 msgid "Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:373 -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:478 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:375 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:480 msgid "Row #{0}: Allocated Amount cannot be greater than outstanding amount." msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:490 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:492 msgid "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}" msgstr "" @@ -44061,11 +44178,11 @@ msgstr "" msgid "Row #{0}: Amount must be a positive number" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:432 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:438 msgid "Row #{0}: Asset {1} cannot be sold, it is already {2}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:437 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:443 msgid "Row #{0}: Asset {1} is already sold" msgstr "" @@ -44085,7 +44202,7 @@ msgstr "" msgid "Row #{0}: Batch No(s) {1} is not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)." msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:869 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:884 msgid "Row #{0}: Cannot allocate more than {1} against payment term {2}" msgstr "" @@ -44129,7 +44246,7 @@ msgstr "" msgid "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1135 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1148 msgid "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}" msgstr "" @@ -44216,7 +44333,7 @@ msgstr "" msgid "Row #{0}: Depreciation Start Date is required" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:334 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:336 msgid "Row #{0}: Duplicate entry in References {1} {2}" msgstr "" @@ -44242,12 +44359,16 @@ msgstr "" msgid "Row #{0}: Finished Good Item is not specified for service item {1}" msgstr "" +#: erpnext/manufacturing/doctype/bom/bom.py:339 +msgid "Row #{0}: Finished Good Item {1} cannot be added in the Secondary Items table." +msgstr "" + #: erpnext/buying/doctype/purchase_order/purchase_order.py:354 #: erpnext/selling/doctype/sales_order/sales_order.py:292 msgid "Row #{0}: Finished Good Item {1} must be a sub-contracted item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:614 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:632 msgid "Row #{0}: Finished Good must be {1}" msgstr "" @@ -44276,7 +44397,7 @@ msgstr "" msgid "Row #{0}: From Date cannot be before To Date" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:880 +#: erpnext/manufacturing/doctype/job_card/job_card.py:893 msgid "Row #{0}: From Time and To Time fields are required" msgstr "" @@ -44284,7 +44405,7 @@ msgstr "" msgid "Row #{0}: Item added" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1721 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1826 msgid "Row #{0}: Item {1} cannot be transferred more than {2} against {3} {4}" msgstr "" @@ -44329,6 +44450,10 @@ msgstr "" msgid "Row #{0}: Item {1} is not a stock item" msgstr "" +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1080 +msgid "Row #{0}: Item {1} is not part of the source manufacture entry and cannot be added to this disassembly." +msgstr "" + #: erpnext/controllers/subcontracting_inward_controller.py:79 msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted, add another row instead." msgstr "" @@ -44337,7 +44462,11 @@ msgstr "" msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted." msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:765 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1089 +msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows." +msgstr "" + +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:780 msgid "Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher" msgstr "" @@ -44361,7 +44490,7 @@ msgstr "" msgid "Row #{0}: Opening Accumulated Depreciation must be less than or equal to {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1039 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1144 msgid "Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}." msgstr "" @@ -44394,7 +44523,7 @@ msgstr "" msgid "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:346 +#: erpnext/manufacturing/doctype/bom/bom.py:353 #, python-format msgid "Row #{0}: Process Loss Percentage should be less than 100% for {1} Item {2}" msgstr "" @@ -44436,7 +44565,7 @@ msgstr "" msgid "Row #{0}: Quantity of Item {1} cannot be more than {2} {3} against Subcontracting Inward Order {4}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:339 +#: erpnext/manufacturing/doctype/bom/bom.py:346 msgid "Row #{0}: Quantity should be greater than 0 for {1} Item {2}" msgstr "" @@ -44471,7 +44600,7 @@ msgstr "" msgid "Row #{0}: Repair cost {1} exceeds available amount {2} for Purchase Invoice {3} and Account {4}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:440 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:446 msgid "Row #{0}: Return Against is required for returning asset" msgstr "" @@ -44547,15 +44676,15 @@ msgstr "" msgid "Row #{0}: Source Warehouse {1} for item {2} must be same as Source Warehouse {3} in the Work Order." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1186 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1291 msgid "Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1208 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1313 msgid "Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer" msgstr "" -#: erpnext/manufacturing/doctype/workstation/workstation.py:107 +#: erpnext/manufacturing/doctype/workstation/workstation.py:108 msgid "Row #{0}: Start Time must be before End Time" msgstr "" @@ -44596,7 +44725,7 @@ msgstr "" msgid "Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1298 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1308 msgid "Row #{0}: Stock quantity {1} ({2}) for item {3} cannot exceed {4}" msgstr "" @@ -44612,7 +44741,7 @@ msgstr "" msgid "Row #{0}: The warehouse {1} is not a child warehouse of a group warehouse {2}" msgstr "" -#: erpnext/manufacturing/doctype/workstation/workstation.py:180 +#: erpnext/manufacturing/doctype/workstation/workstation.py:185 msgid "Row #{0}: Timings conflicts with row {1}" msgstr "" @@ -44640,7 +44769,7 @@ msgstr "" msgid "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:444 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:450 msgid "Row #{0}: You must select an Asset for Item {1}." msgstr "" @@ -44765,7 +44894,7 @@ msgstr "" msgid "Row #{}: {} {} doesn't belong to Company {}. Please select valid {}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:435 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:440 msgid "Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}" msgstr "" @@ -44777,7 +44906,7 @@ msgstr "" msgid "Row {0} picked quantity is less than the required quantity, additional {1} {2} required." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1745 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1850 msgid "Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" msgstr "" @@ -44809,7 +44938,7 @@ msgstr "" msgid "Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1406 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1511 msgid "Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials." msgstr "" @@ -44847,7 +44976,7 @@ msgstr "" msgid "Row {0}: Credit entry can not be linked with a {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:579 +#: erpnext/manufacturing/doctype/bom/bom.py:586 msgid "Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}" msgstr "" @@ -44888,15 +45017,15 @@ 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:525 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:530 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:482 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:487 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:507 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:512 msgid "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}" msgstr "" @@ -44953,7 +45082,7 @@ msgstr "" msgid "Row {0}: Item {1}'s quantity cannot be higher than the available quantity." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1245 +#: erpnext/manufacturing/doctype/bom/bom.py:1252 msgid "Row {0}: Operation time should be greater than 0 for operation {1}" msgstr "" @@ -45025,7 +45154,7 @@ msgstr "" msgid "Row {0}: Qty cannot be greater than {1} for the Item {2}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:705 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:723 msgid "Row {0}: Qty in Stock UOM can not be zero." msgstr "" @@ -45037,15 +45166,15 @@ msgstr "" msgid "Row {0}: Quantity cannot be negative." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1113 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1218 msgid "Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:916 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:926 msgid "Row {0}: Sales Invoice {1} is already created for {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:317 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:330 msgid "Row {0}: Serial/Batch has been reset to values linked with Work Order {1} because the previously selected serial/batch does not belong to this Work Order." msgstr "" @@ -45053,7 +45182,7 @@ msgstr "" msgid "Row {0}: Shift cannot be changed since the depreciation has already been processed" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1758 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1863 msgid "Row {0}: Subcontracted Item is mandatory for the raw material {1}" msgstr "" @@ -45069,7 +45198,7 @@ msgstr "" msgid "Row {0}: The entire expense amount for account {1} in {2} has already been allocated." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:751 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:769 msgid "Row {0}: The item {1}, quantity must be positive number" msgstr "" @@ -45081,15 +45210,15 @@ msgstr "" msgid "Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3687 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3838 msgid "Row {0}: Transferred quantity cannot be greater than the requested quantity." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:699 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:717 msgid "Row {0}: UOM Conversion Factor is mandatory" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:401 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:407 msgid "Row {0}: Update Stock must be checked for item {1} because it is against Pick List {2}." msgstr "" @@ -45101,7 +45230,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:1239 +#: erpnext/manufacturing/doctype/bom/bom.py:1246 #: erpnext/manufacturing/doctype/work_order/work_order.py:489 msgid "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" msgstr "" @@ -45134,7 +45263,7 @@ msgstr "" msgid "Row {0}: {2} Item {1} does not exist in {2} {3}" msgstr "" -#: erpnext/utilities/transaction_base.py:624 +#: erpnext/utilities/transaction_base.py:629 msgid "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." msgstr "" @@ -45150,11 +45279,11 @@ msgstr "" msgid "Row({0}): {1} is already discounted in {2}" msgstr "" -#: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:200 +#: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:206 msgid "Rows Added in {0}" msgstr "" -#: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:201 +#: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:207 msgid "Rows Removed in {0}" msgstr "" @@ -45233,7 +45362,7 @@ msgstr "" msgid "SLA Paused On" msgstr "" -#: erpnext/public/js/utils.js:1259 +#: erpnext/public/js/utils.js:1260 msgid "SLA is on hold since {0}" msgstr "" @@ -45256,7 +45385,7 @@ msgstr "" msgid "SO Qty" msgstr "" -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:107 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:116 msgid "SO Total Qty" msgstr "" @@ -45595,7 +45724,7 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json #: erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:361 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:380 #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:284 #: erpnext/accounts/report/sales_register/sales_register.py:238 @@ -45746,12 +45875,12 @@ msgstr "" msgid "Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:1815 -#: erpnext/selling/doctype/sales_order/sales_order.py:1828 +#: erpnext/selling/doctype/sales_order/sales_order.py:1805 +#: erpnext/selling/doctype/sales_order/sales_order.py:1818 msgid "Sales Order {0} is not available for production" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1428 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1438 msgid "Sales Order {0} is not submitted" msgstr "" @@ -45811,7 +45940,7 @@ msgstr "" #: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:130 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1230 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1250 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:114 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:194 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:74 @@ -45917,7 +46046,7 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:158 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:136 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1227 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1247 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:120 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:191 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:80 @@ -45931,7 +46060,7 @@ msgstr "" #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.js:8 #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py:70 #: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js:8 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:116 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:125 #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/doctype/sales_person/sales_person.json #: erpnext/workspace_sidebar/crm.json erpnext/workspace_sidebar/selling.json @@ -46169,7 +46298,7 @@ msgid "Sample Quantity" msgstr "" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:269 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:535 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:551 msgid "Sample Retention Stock Entry" msgstr "" @@ -46186,7 +46315,7 @@ msgstr "" msgid "Sample Size" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4185 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4336 msgid "Sample quantity {0} cannot be more than received quantity {1}" msgstr "" @@ -46312,11 +46441,11 @@ msgstr "" msgid "Scheduled Time Logs" msgstr "" -#: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py:188 +#: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py:191 msgid "Scheduler is Inactive. Can't trigger job now." msgstr "" -#: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py:237 +#: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py:240 msgid "Scheduler is Inactive. Can't trigger jobs now." msgstr "" @@ -46641,7 +46770,7 @@ msgstr "" msgid "Select Dispatch Address " msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:702 +#: erpnext/manufacturing/doctype/job_card/job_card.js:706 msgid "Select Employees" msgstr "" @@ -46691,7 +46820,7 @@ msgstr "" msgid "Select Job Worker Address" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1203 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1222 #: erpnext/selling/page/point_of_sale/pos_item_cart.js:966 msgid "Select Loyalty Program" msgstr "" @@ -46825,7 +46954,7 @@ msgstr "" msgid "Select item group" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:473 +#: erpnext/manufacturing/doctype/bom/bom.js:476 msgid "Select template item" msgstr "" @@ -46842,7 +46971,7 @@ msgstr "" msgid "Select the Item to be manufactured." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:985 +#: erpnext/manufacturing/doctype/bom/bom.js:988 msgid "Select the Item to be manufactured. The Item name, UoM, Company, and Currency will be fetched automatically." msgstr "" @@ -46863,11 +46992,11 @@ msgstr "" msgid "Select the date and your timezone" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1004 +#: erpnext/manufacturing/doctype/bom/bom.js:1007 msgid "Select the raw materials (Items) required to manufacture the Item" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:528 +#: erpnext/manufacturing/doctype/bom/bom.js:531 msgid "Select variant item code for the template item {0}" msgstr "" @@ -46891,15 +47020,15 @@ msgstr "" msgid "Selected POS Opening Entry should be open." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2609 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2619 msgid "Selected Price List should have buying and selling fields checked." msgstr "" -#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:121 +#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:122 msgid "Selected Print Format does not exist." msgstr "" -#: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py:163 +#: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py:166 msgid "Selected Serial and Batch Bundle entries have been fixed." msgstr "" @@ -46941,7 +47070,7 @@ msgstr "" msgid "Sell quantity cannot exceed the asset quantity" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1441 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1451 msgid "Sell quantity cannot exceed the asset quantity. Asset {0} has only {1} item(s)." msgstr "" @@ -46999,7 +47128,7 @@ msgstr "" #: erpnext/selling/doctype/selling_settings/selling_settings.json #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/workspace/erpnext_settings/erpnext_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:258 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:260 #: erpnext/workspace_sidebar/erpnext_settings.json msgid "Selling Settings" msgstr "" @@ -47213,7 +47342,7 @@ msgstr "" #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:450 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:38 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:61 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:426 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:427 #: erpnext/stock/workspace/stock/stock.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -47254,7 +47383,7 @@ msgstr "" msgid "Serial No Range" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2653 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2662 msgid "Serial No Reserved" msgstr "" @@ -47340,7 +47469,7 @@ msgstr "" msgid "Serial No {0} does not exist" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3442 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3451 msgid "Serial No {0} does not exists" msgstr "" @@ -47472,7 +47601,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:188 #: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py:31 #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:80 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:410 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:411 #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:177 #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json #: erpnext/workspace_sidebar/stock.json @@ -47483,7 +47612,7 @@ msgstr "" msgid "Serial and Batch Bundle created" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2252 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2261 msgid "Serial and Batch Bundle updated" msgstr "" @@ -47495,6 +47624,10 @@ msgstr "" msgid "Serial and Batch Bundle {0} is not submitted" msgstr "" +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2237 +msgid "Serial and Batch Bundle {0} is submitted and its entries cannot be modified." +msgstr "" + #. Label of the section_break_45 (Section Break) field in DocType #. 'Subcontracting Receipt Item' #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -47748,7 +47881,7 @@ msgid "Service Item {0} is disabled." msgstr "" #: erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.py:67 -#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:164 +#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:165 msgid "Service Item {0} must be a non-stock item." msgstr "" @@ -47886,7 +48019,7 @@ msgstr "" #. Label of the set_basic_rate_manually (Check) field in DocType 'Stock Entry #. Detail' -#: erpnext/stock/doctype/stock_entry/stock_entry.py:384 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:397 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json msgid "Set Basic Rate Manually" msgstr "" @@ -47937,7 +48070,7 @@ msgstr "" msgid "Set Landed Cost Based on Purchase Invoice Rate" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1215 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1234 msgid "Set Loyalty Program" msgstr "" @@ -47966,7 +48099,7 @@ msgstr "" msgid "Set Posting Date" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1031 +#: erpnext/manufacturing/doctype/bom/bom.js:1034 msgid "Set Process Loss Item Quantity" msgstr "" @@ -48089,7 +48222,7 @@ msgstr "" msgid "Set incoming rate as zero for expired Batch" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1021 +#: erpnext/manufacturing/doctype/bom/bom.js:1024 msgid "Set quantity of process loss item:" msgstr "" @@ -48206,7 +48339,7 @@ msgstr "" msgid "Setting up company" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1218 +#: erpnext/manufacturing/doctype/bom/bom.py:1225 #: erpnext/manufacturing/doctype/work_order/work_order.py:1562 msgid "Setting {0} is required" msgstr "" @@ -48922,7 +49055,7 @@ msgstr "" msgid "Simultaneous" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:829 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:847 msgid "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table." msgstr "" @@ -49100,11 +49233,11 @@ msgstr "" msgid "Source Stock Entry (Manufacture)" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:991 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1009 msgid "Source Stock Entry {0} belongs to Work Order {1}, not {2}. Please use a manufacture entry from the same Work Order." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2457 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2608 msgid "Source Stock Entry {0} has no finished goods quantity" msgstr "" @@ -49128,7 +49261,7 @@ msgstr "" #. Label of the s_warehouse (Link) field in DocType 'Stock Entry Detail' #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/manufacturing/doctype/bom/bom.js:500 +#: erpnext/manufacturing/doctype/bom/bom.js:503 #: erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -49142,7 +49275,7 @@ msgstr "" #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/stock/dashboard/item_dashboard.js:227 #: erpnext/stock/doctype/material_request_item/material_request_item.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:798 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:814 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json msgid "Source Warehouse" msgstr "" @@ -49170,7 +49303,7 @@ msgstr "" msgid "Source and Target Location cannot be same" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:957 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:975 msgid "Source and target warehouse cannot be same for row {0}" msgstr "" @@ -49183,9 +49316,9 @@ msgstr "" msgid "Source of Funds (Liabilities)" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:924 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:940 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:947 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:942 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:958 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:965 msgid "Source warehouse is mandatory for row {0}" msgstr "" @@ -49267,7 +49400,7 @@ msgstr "" msgid "Split Quantity must be less than Asset Quantity" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2456 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2474 msgid "Splitting {0} {1} into {2} rows as per Payment Terms" msgstr "" @@ -49397,7 +49530,7 @@ msgstr "" msgid "Start Date should be lower than End Date" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:658 +#: erpnext/manufacturing/doctype/job_card/job_card.js:662 #: erpnext/manufacturing/doctype/workstation/workstation.js:124 msgid "Start Job" msgstr "" @@ -49426,7 +49559,7 @@ msgstr "" #: erpnext/accounts/report/financial_ratios/financial_ratios.js:17 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.html:144 #: erpnext/assets/report/fixed_asset_register/fixed_asset_register.js:81 -#: erpnext/public/js/financial_statements.js:422 +#: erpnext/public/js/financial_statements.js:435 msgid "Start Year" msgstr "" @@ -49539,8 +49672,8 @@ msgstr "" #: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:96 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:158 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1384 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1410 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1393 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1419 #: erpnext/accounts/report/account_balance/account_balance.js:58 msgid "Stock Adjustment" msgstr "" @@ -49647,7 +49780,7 @@ msgstr "" msgid "Stock Details" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1081 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1186 msgid "Stock Entries already created for Work Order {0}: {1}" msgstr "" @@ -49715,7 +49848,7 @@ msgstr "" msgid "Stock Entry {0} created" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1543 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1583 msgid "Stock Entry {0} has created" msgstr "" @@ -49883,6 +50016,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/stock/doctype/material_request_item/material_request_item.json +#: erpnext/stock/report/item_where_used/item_where_used.py:82 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:34 #: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:34 msgid "Stock Qty" @@ -49975,9 +50109,9 @@ msgstr "" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1699 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1716 #: erpnext/stock/doctype/stock_settings/stock_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:215 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:227 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:241 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:217 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:229 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:243 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:182 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:195 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:207 @@ -49992,12 +50126,12 @@ msgstr "" #: erpnext/controllers/subcontracting_inward_controller.py:1026 #: erpnext/manufacturing/doctype/production_plan/production_plan.py:2245 -#: erpnext/manufacturing/doctype/work_order/work_order.py:2214 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2306 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1777 msgid "Stock Reservation Entries Created" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:411 +#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:412 msgid "Stock Reservation Entries created" msgstr "" @@ -50167,9 +50301,10 @@ msgstr "" #: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json #: erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py:35 +#: erpnext/stock/report/item_where_used/item_where_used.py:88 #: erpnext/stock/report/reserved_stock/reserved_stock.py:110 -#: erpnext/stock/report/stock_balance/stock_balance.py:511 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:295 +#: erpnext/stock/report/stock_balance/stock_balance.py:505 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:296 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json @@ -50195,7 +50330,7 @@ msgstr "" msgid "Stock Uom" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:753 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:758 msgid "Stock Update Not Allowed" msgstr "" @@ -50299,15 +50434,15 @@ msgstr "" msgid "Stock cannot be reserved in the group warehouse {0}." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1256 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1266 msgid "Stock cannot be updated against the following Delivery Notes: {0}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1325 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1335 msgid "Stock cannot be updated because the invoice contains a drop shipping item. Please disable 'Update Stock' or remove the drop shipping item." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:750 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:755 msgid "Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice." msgstr "" @@ -50590,6 +50725,10 @@ msgstr "" msgid "Subcontracting Delivery" msgstr "" +#: erpnext/stock/report/item_where_used/item_where_used.py:362 +msgid "Subcontracting Finished Good" +msgstr "" + #. Label of the subcontracting_inward_tab (Tab Break) field in DocType 'Selling #. Settings' #: erpnext/selling/doctype/sales_order/sales_order_dashboard.py:33 @@ -50770,6 +50909,10 @@ msgstr "" msgid "Subcontracting Sales Order" msgstr "" +#: erpnext/stock/report/item_where_used/item_where_used.py:336 +msgid "Subcontracting Service Item" +msgstr "" + #. Label of the subcontract (Tab Break) field in DocType 'Buying Settings' #: erpnext/buying/doctype/buying_settings/buying_settings.json msgid "Subcontracting Settings" @@ -50814,6 +50957,10 @@ msgstr "" msgid "Submit your Quotation" msgstr "" +#: erpnext/manufacturing/doctype/job_card/job_card.py:1493 +msgid "Submitted Job Card cannot be processed." +msgstr "" + #. Label of the subscription_section (Section Break) field in DocType 'Payment #. Request' #. Label of the subscription_section (Section Break) field in DocType 'POS @@ -50853,11 +51000,11 @@ msgstr "" msgid "Subscription End Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:372 +#: erpnext/accounts/doctype/subscription/subscription.py:405 msgid "Subscription End Date is mandatory to follow calendar months" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:362 +#: erpnext/accounts/doctype/subscription/subscription.py:395 msgid "Subscription End Date must be after {0} as per the subscription plan" msgstr "" @@ -50917,7 +51064,7 @@ msgstr "" msgid "Subscription Start Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:746 +#: erpnext/accounts/doctype/subscription/subscription.py:773 msgid "Subscription for Future dates cannot be processed." msgstr "" @@ -51245,7 +51392,7 @@ msgstr "" #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/accounts/report/accounts_payable/accounts_payable.js:119 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:102 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1234 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1254 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:198 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py:178 #: erpnext/accounts/report/purchase_register/purchase_register.js:27 @@ -51307,7 +51454,7 @@ msgstr "" msgid "Supplier Invoice No" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1803 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1812 msgid "Supplier Invoice No exists in Purchase Invoice {0}" msgstr "" @@ -51345,7 +51492,7 @@ msgstr "" #. Label of the supplier_name (Data) field in DocType 'Purchase Receipt' #. Label of the supplier_name (Data) field in DocType 'Stock Entry' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1150 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1170 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:156 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:196 #: erpnext/accounts/report/purchase_register/purchase_register.py:177 @@ -51679,7 +51826,7 @@ msgstr "" msgid "Synchronize all accounts every hour" msgstr "" -#: erpnext/accounts/doctype/account/account.py:662 +#: erpnext/accounts/doctype/account/account.py:664 msgid "System In Use" msgstr "" @@ -51727,7 +51874,7 @@ msgstr "" msgid "TDS Computation Summary" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1561 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1570 msgid "TDS Deducted" msgstr "" @@ -51866,7 +52013,7 @@ msgstr "" #: erpnext/stock/dashboard/item_dashboard.js:234 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json #: erpnext/stock/doctype/material_request_item/material_request_item.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:804 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:820 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json msgid "Target Warehouse" msgstr "" @@ -51902,9 +52049,9 @@ msgstr "" msgid "Target Warehouse {0} must be same as Delivery Warehouse {1} in the Subcontracting Inward Order Item." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:930 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:936 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:951 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:948 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:954 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:969 msgid "Target warehouse is mandatory for row {0}" msgstr "" @@ -52570,7 +52717,7 @@ msgstr "" msgid "Television" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:452 +#: erpnext/manufacturing/doctype/bom/bom.js:455 msgid "Template Item" msgstr "" @@ -52796,14 +52943,14 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/territory_item/territory_item.json #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:142 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1218 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1238 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:108 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:182 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:68 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py:169 #: erpnext/accounts/report/gross_profit/gross_profit.py:436 #: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js:8 -#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:21 +#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:22 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:259 #: erpnext/accounts/report/sales_register/sales_register.py:209 #: erpnext/crm/doctype/lead/lead.json @@ -52822,7 +52969,7 @@ msgstr "" #: erpnext/selling/doctype/quotation/quotation.json #: erpnext/selling/doctype/sales_order/sales_order.json #: erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py:103 -#: erpnext/selling/report/inactive_customers/inactive_customers.py:79 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:100 #: erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py:87 #: erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py:42 #: erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js:47 @@ -52832,7 +52979,7 @@ msgstr "" #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.js:46 #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py:61 #: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js:59 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:72 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:81 #: erpnext/selling/report/territory_wise_sales/territory_wise_sales.py:22 #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/doctype/sales_partner/sales_partner.json @@ -52948,7 +53095,7 @@ msgstr "" msgid "The Pick List having Stock Reservation Entries cannot be updated. If you need to make changes, we recommend canceling the existing Stock Reservation Entries before updating the Pick List." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2909 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3060 msgid "The Process Loss Qty has reset as per job cards Process Loss Qty" msgstr "" @@ -52960,11 +53107,11 @@ msgstr "" msgid "The Serial No at Row #{0}: {1} is not available in warehouse {2}." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2650 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2659 msgid "The Serial No {0} is reserved against the {1} {2} and cannot be used for any other transaction." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1926 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2031 msgid "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}" msgstr "" @@ -52998,7 +53145,7 @@ msgstr "" msgid "The company {0} is not in United Arab Emirates. UAE VAT 201 report is only available for companies in United Arab Emirates." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1327 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1348 msgid "The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}." msgstr "" @@ -53080,7 +53227,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:289 +#: erpnext/setup/doctype/employee/employee.py:286 msgid "The following employees are currently still reporting to {0}:" msgstr "" @@ -53129,11 +53276,11 @@ msgstr "" msgid "The items {items} are not marked as {type_of} item. You can enable them as {type_of} item from their Item masters." msgstr "" -#: erpnext/manufacturing/doctype/workstation/workstation.py:548 +#: erpnext/manufacturing/doctype/workstation/workstation.py:582 msgid "The job card {0} is in {1} state and you cannot complete." msgstr "" -#: erpnext/manufacturing/doctype/workstation/workstation.py:542 +#: erpnext/manufacturing/doctype/workstation/workstation.py:576 msgid "The job card {0} is in {1} state and you cannot start it again." msgstr "" @@ -53218,7 +53365,7 @@ msgstr "" msgid "The rate at which this item was last purchased via a Purchase Invoice. Auto-updated by the system." msgstr "" -#: erpnext/public/js/utils.js:967 +#: erpnext/public/js/utils.js:968 msgid "The reserved stock will be released when you update items. Are you certain you wish to proceed?" msgstr "" @@ -53289,11 +53436,11 @@ msgstr "" msgid "The system will create a Sales Invoice or a POS Invoice from the POS interface based on this setting. For high-volume transactions, it is recommended to use POS Invoice." msgstr "" -#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1032 +#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1011 msgid "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage" msgstr "" -#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1043 +#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1022 msgid "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage" msgstr "" @@ -53353,7 +53500,7 @@ msgstr "" msgid "The warehouse where your Items will be transferred when you begin production. Group Warehouse can also be selected as a Work in Progress warehouse." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:895 +#: erpnext/manufacturing/doctype/job_card/job_card.py:908 msgid "The {0} ({1}) must be equal to {2} ({3})" msgstr "" @@ -53373,7 +53520,7 @@ msgstr "" msgid "The {0} {1} does not match with the {0} {2} in the {3} {4}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1001 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1014 msgid "The {0} {1} is used to calculate the valuation cost for the finished good {2}." msgstr "" @@ -53417,7 +53564,7 @@ msgstr "" msgid "There can be multiple tiered collection factor based on the total spent. But the conversion factor for redemption will always be same for all the tier." msgstr "" -#: erpnext/accounts/party.py:575 +#: erpnext/accounts/party.py:593 msgid "There can only be 1 Account per Company in {0} {1}" msgstr "" @@ -53437,7 +53584,7 @@ msgstr "" msgid "There is no batch found against the {0}: {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1863 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1968 msgid "There must be atleast 1 Finished Good in this Stock Entry" msgstr "" @@ -53458,7 +53605,7 @@ msgstr "" msgid "There was an issue connecting to Plaid's authentication server. Check browser console for more information" msgstr "" -#: erpnext/accounts/utils.py:1136 +#: erpnext/accounts/utils.py:1137 msgid "There were issues unlinking payment entry {0}." msgstr "" @@ -53488,7 +53635,7 @@ msgstr "" msgid "This Purchase Order has been fully subcontracted." msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:2079 +#: erpnext/selling/doctype/sales_order/sales_order.py:2069 msgid "This Sales Order has been fully subcontracted." msgstr "" @@ -53542,7 +53689,7 @@ msgstr "" msgid "This invoice has already been paid." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:307 +#: erpnext/manufacturing/doctype/bom/bom.js:310 msgid "This is a Template BOM and will be used to make the work order for {0} of the item {1}" msgstr "" @@ -53615,7 +53762,7 @@ msgstr "" msgid "This is considered dangerous from accounting point of view." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:531 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:536 msgid "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice" msgstr "" @@ -53663,7 +53810,7 @@ msgstr "" msgid "This schedule was created when Asset {0} was repaired through Asset Repair {1}." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1532 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1542 msgid "This schedule was created when Asset {0} was restored due to Sales Invoice {1} cancellation." msgstr "" @@ -53675,7 +53822,7 @@ msgstr "" msgid "This schedule was created when Asset {0} was restored." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1528 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1538 msgid "This schedule was created when Asset {0} was returned through Sales Invoice {1}." msgstr "" @@ -53687,7 +53834,7 @@ msgstr "" msgid "This schedule was created when Asset {0} was {1} into new Asset {2}." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1504 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1514 msgid "This schedule was created when Asset {0} was {1} through Sales Invoice {2}." msgstr "" @@ -53853,7 +54000,7 @@ msgstr "" msgid "Time in mins." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:872 +#: erpnext/manufacturing/doctype/job_card/job_card.py:885 msgid "Time logs are required for {0} {1}" msgstr "" @@ -53884,7 +54031,7 @@ msgstr "" #. Name of a DocType #. Label of a Link in the Projects Workspace #. Label of a Workspace Sidebar Item -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:283 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:302 #: erpnext/projects/doctype/timesheet/timesheet.json #: erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py:26 #: erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py:59 @@ -53915,7 +54062,7 @@ msgstr "" msgid "Timesheet for tasks." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:925 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:935 msgid "Timesheet {0} cannot be invoiced in its current state" msgstr "" @@ -54181,7 +54328,7 @@ msgstr "" msgid "To Warehouse (Optional)" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:999 +#: erpnext/manufacturing/doctype/bom/bom.js:1002 msgid "To add Operations tick the 'With Operations' checkbox." msgstr "" @@ -54213,11 +54360,11 @@ msgstr "" msgid "To be Delivered to Customer" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:573 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:579 msgid "To cancel a {} you need to cancel the POS Closing Entry {}." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:586 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:592 msgid "To cancel this Sales Invoice you need to cancel the POS Closing Entry {}." msgstr "" @@ -54239,7 +54386,7 @@ msgstr "" msgid "To include sub-assembly costs and secondary items in Finished Goods on a work order without using a job card, when the 'Use Multi-Level BOM' option is enabled." msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2247 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2262 #: erpnext/controllers/accounts_controller.py:3248 msgid "To include tax in row {0} in Item rate, taxes in rows {1} must also be included" msgstr "" @@ -54260,11 +54407,11 @@ msgstr "" msgid "To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:622 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:627 msgid "To submit the invoice without purchase order please set {0} as {1} in {2}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:644 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:649 msgid "To submit the invoice without purchase receipt please set {0} as {1} in {2}" msgstr "" @@ -54529,7 +54676,7 @@ msgstr "" #. Label of the total_completed_qty (Float) field in DocType 'Job Card' #: erpnext/manufacturing/doctype/job_card/job_card.json -#: erpnext/manufacturing/doctype/job_card/job_card.py:891 +#: erpnext/manufacturing/doctype/job_card/job_card.py:904 #: erpnext/manufacturing/report/job_card_summary/job_card_summary.py:174 msgid "Total Completed Qty" msgstr "" @@ -54766,11 +54913,11 @@ msgstr "" msgid "Total Operation Time" msgstr "" -#: erpnext/selling/report/inactive_customers/inactive_customers.py:83 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:104 msgid "Total Order Considered" msgstr "" -#: erpnext/selling/report/inactive_customers/inactive_customers.py:82 +#: erpnext/selling/report/inactive_customers/inactive_customers.py:103 msgid "Total Order Value" msgstr "" @@ -55123,7 +55270,7 @@ msgid "Total hours: {0}" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:570 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:557 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:563 msgid "Total payments amount can't be greater than {}" msgstr "" @@ -55295,11 +55442,11 @@ msgstr "" msgid "Transaction Deletion Record To Delete" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1101 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1103 msgid "Transaction Deletion Record {0} is already running. {1}" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1120 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1122 msgid "Transaction Deletion Record {0} is currently deleting {1}. Cannot save documents until deletion completes." msgstr "" @@ -55385,11 +55532,11 @@ msgstr "" msgid "Transaction from which tax is withheld" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:865 +#: erpnext/manufacturing/doctype/job_card/job_card.py:862 msgid "Transaction not allowed against stopped Work Order {0}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1252 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1267 msgid "Transaction reference no {0} dated {1}" msgstr "" @@ -55414,7 +55561,7 @@ msgstr "" msgid "Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1189 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1199 msgid "Transactions using Sales Invoice in POS are disabled." msgstr "" @@ -55513,7 +55660,7 @@ msgstr "" msgid "Transit" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:589 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:605 msgid "Transit Entry" msgstr "" @@ -55615,7 +55762,7 @@ msgstr "" msgid "Trial Period End Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:342 +#: erpnext/accounts/doctype/subscription/subscription.py:375 msgid "Trial Period End Date Cannot be before Trial Period Start Date" msgstr "" @@ -55624,7 +55771,7 @@ msgstr "" msgid "Trial Period Start Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:348 +#: erpnext/accounts/doctype/subscription/subscription.py:381 msgid "Trial Period Start date cannot be after Subscription Start Date" msgstr "" @@ -55804,7 +55951,7 @@ msgstr "" #: erpnext/manufacturing/doctype/workstation/workstation.js:480 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:70 #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py:110 -#: erpnext/public/js/stock_analytics.js:94 erpnext/public/js/utils.js:840 +#: erpnext/public/js/stock_analytics.js:94 erpnext/public/js/utils.js:841 #: erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json #: erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json @@ -55834,9 +55981,10 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:101 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:87 #: erpnext/stock/report/item_prices/item_prices.py:55 +#: erpnext/stock/report/item_where_used/item_where_used.py:75 #: erpnext/stock/report/item_wise_consumption/item_wise_consumption.py:60 #: erpnext/stock/report/product_bundle_balance/product_bundle_balance.py:94 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:217 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:223 #: erpnext/stock/report/stock_analytics/stock_analytics.py:59 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:134 #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json @@ -55916,7 +56064,7 @@ msgstr "" msgid "UOM Name" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4107 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4258 msgid "UOM conversion factor required for UOM: {0} in Item: {1}" msgstr "" @@ -56060,7 +56208,7 @@ msgstr "" msgid "Under Working Hours table, you can add start and end times for a Workstation. For example, a Workstation may be active from 9 am to 1 pm, then 2 pm to 5 pm. You can also specify the working hours based on shifts. While scheduling a Work Order, the system will check for the availability of the Workstation based on the working hours specified." msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:936 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:938 msgid "Unexpected Naming Series Pattern" msgstr "" @@ -56132,7 +56280,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:280 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:286 #: erpnext/accounts/doctype/subscription/subscription.json #: erpnext/accounts/doctype/subscription/subscription_list.js:12 msgid "Unpaid" @@ -56405,7 +56553,7 @@ msgstr "" #. Option for the 'Update Type' (Select) field in DocType 'BOM Update Log' #. Label of the update_cost_section (Section Break) field in DocType 'BOM #. Update Tool' -#: erpnext/manufacturing/doctype/bom/bom.js:223 +#: erpnext/manufacturing/doctype/bom/bom.js:226 #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json #: erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.json msgid "Update Cost" @@ -56426,7 +56574,7 @@ msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order.js:324 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:43 -#: erpnext/public/js/utils.js:946 +#: erpnext/public/js/utils.js:947 #: erpnext/selling/doctype/quotation/quotation.js:136 #: erpnext/selling/doctype/sales_order/sales_order.js:90 #: erpnext/selling/doctype/sales_order/sales_order.js:946 @@ -56449,7 +56597,7 @@ msgstr "" msgid "Update Price List based on" msgstr "" -#: erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js:10 +#: erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js:11 msgid "Update Print Format" msgstr "" @@ -56637,7 +56785,7 @@ msgstr "" #. Label of the use_multi_level_bom (Check) field in DocType 'Work Order' #. Label of the use_multi_level_bom (Check) field in DocType 'Stock Entry' -#: erpnext/manufacturing/doctype/bom/bom.js:434 +#: erpnext/manufacturing/doctype/bom/bom.js:437 #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/stock/doctype/stock_entry/stock_entry.json msgid "Use Multi-Level BOM" @@ -56781,7 +56929,7 @@ msgstr "" msgid "User has not applied rule on the invoice {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:301 +#: erpnext/setup/doctype/employee/employee.py:298 msgid "User {0} does not exist" msgstr "" @@ -56789,15 +56937,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:319 +#: erpnext/setup/doctype/employee/employee.py:324 msgid "User {0} is already assigned to Employee {1}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:357 +#: erpnext/setup/doctype/employee/employee.py:362 msgid "User {0}: Removed Employee Self Service role as there is no mapped employee." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:352 +#: erpnext/setup/doctype/employee/employee.py:357 msgid "User {0}: Removed Employee role as there is no mapped employee." msgstr "" @@ -57063,7 +57211,6 @@ msgstr "" #. Label of the valuation_rate (Currency) field in DocType 'Stock Closing #. Balance' #. Label of the valuation_rate (Currency) field in DocType 'Stock Entry Detail' -#. Label of the valuation_rate (Currency) field in DocType 'Stock Ledger Entry' #. Label of the valuation_rate (Currency) field in DocType 'Stock #. Reconciliation Item' #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -57079,14 +57226,12 @@ msgstr "" #: erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json #: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json -#: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json #: erpnext/stock/report/available_serial_no/available_serial_no.py:164 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:85 #: erpnext/stock/report/item_prices/item_prices.py:57 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:68 -#: erpnext/stock/report/stock_balance/stock_balance.py:562 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:378 +#: erpnext/stock/report/stock_balance/stock_balance.py:556 msgid "Valuation Rate" msgstr "" @@ -57116,7 +57261,7 @@ msgstr "" msgid "Valuation and Total" msgstr "" -#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1009 +#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:993 msgid "Valuation rate for customer provided items has been set to zero." msgstr "" @@ -57129,7 +57274,7 @@ msgstr "" msgid "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2271 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2286 #: erpnext/controllers/accounts_controller.py:3272 msgid "Valuation type charges can not be marked as Inclusive" msgstr "" @@ -57142,7 +57287,7 @@ msgstr "" msgid "Value (G - D)" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:260 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:266 msgid "Value ({0})" msgstr "" @@ -57270,7 +57415,7 @@ msgstr "" msgid "Variant Attributes" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:264 +#: erpnext/manufacturing/doctype/bom/bom.js:267 msgid "Variant BOM" msgstr "" @@ -57292,8 +57437,8 @@ msgstr "" msgid "Variant Field" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:387 -#: erpnext/manufacturing/doctype/bom/bom.js:467 +#: erpnext/manufacturing/doctype/bom/bom.js:390 +#: erpnext/manufacturing/doctype/bom/bom.js:470 msgid "Variant Item" msgstr "" @@ -57578,7 +57723,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.js:56 #: erpnext/stock/report/available_serial_no/available_serial_no.py:196 #: erpnext/stock/report/stock_ledger/stock_ledger.js:97 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:403 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:404 msgid "Voucher #" msgstr "" @@ -57640,7 +57785,7 @@ msgstr "" #: erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json #: erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json #: erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1173 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1193 #: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js:56 #: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py:221 #: erpnext/accounts/report/general_ledger/general_ledger.js:49 @@ -57713,7 +57858,7 @@ msgstr "" #: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json #: erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json #: erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1171 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1191 #: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py:212 #: erpnext/accounts/report/general_ledger/general_ledger.py:753 #: erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py:31 @@ -57740,7 +57885,7 @@ msgstr "" #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:486 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:28 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:152 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:401 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:402 #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:159 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:68 msgid "Voucher Type" @@ -57803,7 +57948,7 @@ msgstr "" msgid "WIP Work Orders" msgstr "" -#: erpnext/manufacturing/doctype/workstation/test_workstation.py:125 +#: erpnext/manufacturing/doctype/workstation/test_workstation.py:137 #: erpnext/patches/v16_0/make_workstation_operating_components.py:50 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:317 msgid "Wages" @@ -57926,7 +58071,7 @@ msgstr "" msgid "Warehouse not found against the account {0}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1246 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1256 #: erpnext/stock/doctype/delivery_note/delivery_note.py:414 msgid "Warehouse required for stock Item {0}" msgstr "" @@ -58370,7 +58515,7 @@ msgstr "" msgid "When enabled, it adds a cutoff date filter to Delivery Notes created in bulk from Sales Orders. This allows you to process orders only with a transaction date up to the specified cutoff date, which is useful for period-end processing and batch fulfillment." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:381 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:394 msgid "When there are multiple finished goods ({0}) in a Repack stock entry, the basic rate for all finished goods must be set manually. To set rate manually, enable the checkbox 'Set Basic Rate Manually' in the respective finished good row." msgstr "" @@ -58513,7 +58658,7 @@ msgstr "" #. Option for the 'From Voucher Type' (Select) field in DocType 'Stock #. Reservation Entry' #. Label of a Workspace Sidebar Item -#: erpnext/manufacturing/doctype/bom/bom.js:255 +#: erpnext/manufacturing/doctype/bom/bom.js:258 #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/work_order/work_order.json @@ -58569,7 +58714,7 @@ msgstr "" msgid "Work Order Item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:994 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1012 msgid "Work Order Mismatch" msgstr "" @@ -58618,8 +58763,8 @@ msgstr "" msgid "Work Order cannot be raised against a Item Template" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2570 -#: erpnext/manufacturing/doctype/work_order/work_order.py:2650 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2662 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2742 msgid "Work Order has been {0}" msgstr "" @@ -58631,11 +58776,11 @@ msgstr "" msgid "Work Order {0} created" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2473 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2624 msgid "Work Order {0} has no produced qty" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1031 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1136 msgid "Work Order {0}: Job Card not found for the operation {1}" msgstr "" @@ -58787,7 +58932,7 @@ msgstr "" msgid "Workstation Working Hour" msgstr "" -#: erpnext/manufacturing/doctype/workstation/workstation.py:452 +#: erpnext/manufacturing/doctype/workstation/workstation.py:463 msgid "Workstation is closed on the following dates as per Holiday List: {0}" msgstr "" @@ -59007,7 +59152,7 @@ msgstr "" msgid "You can also use variables in the series name by putting them between (.) dots" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1047 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1057 msgid "You can change the parent account to a Balance Sheet account or select a different account." msgstr "" @@ -59015,12 +59160,12 @@ msgstr "" msgid "You can not enter current voucher in 'Against Journal Entry' column" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:173 +#: erpnext/accounts/doctype/subscription/subscription.py:206 msgid "You can only have Plans with the same billing cycle in a Subscription" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.js:423 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1023 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1042 msgid "You can only redeem max {0} points in this order." msgstr "" @@ -59040,7 +59185,7 @@ msgstr "" msgid "You can use {0} to reconcile against {1} later." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1339 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1360 msgid "You can't make any changes to Job Card since Work Order is closed." msgstr "" @@ -59052,7 +59197,7 @@ msgstr "" msgid "You can't redeem Loyalty Points having more value than the Total Amount." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:773 +#: erpnext/manufacturing/doctype/bom/bom.js:776 msgid "You cannot change the rate if BOM is mentioned against any Item." msgstr "" @@ -59096,7 +59241,7 @@ msgstr "" msgid "You cannot repost item valuation before {}" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:730 +#: erpnext/accounts/doctype/subscription/subscription.py:757 msgid "You cannot restart a Subscription that is not cancelled." msgstr "" @@ -59112,11 +59257,7 @@ msgstr "" msgid "You cannot {0} this document because another Period Closing Entry {1} exists after {2}" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:580 -msgid "You do not have permission to edit this document" -msgstr "" - -#: erpnext/controllers/accounts_controller.py:3867 +#: erpnext/controllers/accounts_controller.py:3869 msgid "You do not have permissions to {} items in a {}." msgstr "" @@ -59148,7 +59289,7 @@ msgstr "" msgid "You had {} errors while creating opening invoices. Check {} for more details" msgstr "" -#: erpnext/public/js/utils.js:1046 +#: erpnext/public/js/utils.js:1047 msgid "You have already selected items from {0} {1}" msgstr "" @@ -59156,7 +59297,7 @@ msgstr "" msgid "You have been invited to collaborate on the project {0}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:253 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:255 msgid "You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted in the transaction price list." msgstr "" @@ -59235,7 +59376,7 @@ msgstr "" msgid "Zero Rated" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:705 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:723 msgid "Zero quantity" msgstr "" @@ -59277,7 +59418,7 @@ msgstr "" msgid "as Title" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1023 +#: erpnext/manufacturing/doctype/bom/bom.js:1026 msgid "as a percentage of finished item quantity" msgstr "" @@ -59301,8 +59442,8 @@ msgstr "" msgid "cannot be greater than 100" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:336 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1135 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:341 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1145 msgid "dated {0}" msgstr "" @@ -59324,7 +59465,7 @@ msgid "discount applied" msgstr "" #: erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py:47 -#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:58 +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:67 msgid "doc_type" msgstr "" @@ -59420,7 +59561,7 @@ msgstr "" msgid "out of 5" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1245 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1260 msgid "paid to" msgstr "" @@ -59470,11 +59611,11 @@ msgstr "" msgid "ratings" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1245 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1260 msgid "received from" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1506 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1516 msgid "returned" msgstr "" @@ -59509,11 +59650,11 @@ msgstr "" msgid "sandbox" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1506 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1516 msgid "sold" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:706 +#: erpnext/accounts/doctype/subscription/subscription.py:733 msgid "subscription is already cancelled." msgstr "" @@ -59536,7 +59677,7 @@ msgstr "" msgid "to" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3221 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3239 msgid "to unallocate the amount of this Return Invoice before cancelling it." msgstr "" @@ -59571,7 +59712,7 @@ msgstr "" msgid "{0} '{1}' is disabled" msgstr "" -#: erpnext/accounts/utils.py:197 +#: erpnext/accounts/utils.py:199 msgid "{0} '{1}' not in Fiscal Year {2}" msgstr "" @@ -59612,11 +59753,11 @@ msgstr "" msgid "{0} Naming Series" msgstr "" -#: erpnext/accounts/utils.py:1569 +#: erpnext/accounts/utils.py:1571 msgid "{0} Number {1} is already used in {2} {3}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1694 +#: erpnext/manufacturing/doctype/bom/bom.py:1701 msgid "{0} Operating Cost for operation {1}" msgstr "" @@ -59700,7 +59841,7 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan/production_plan.py:921 #: erpnext/manufacturing/doctype/production_plan/production_plan.py:1037 #: erpnext/stock/doctype/pick_list/pick_list.py:1341 -#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:322 +#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:323 msgid "{0} created" msgstr "" @@ -59737,12 +59878,12 @@ msgstr "" msgid "{0} entered twice {1} in Item Taxes" msgstr "" -#: erpnext/accounts/utils.py:134 +#: erpnext/accounts/utils.py:136 #: erpnext/projects/doctype/activity_cost/activity_cost.py:40 msgid "{0} for {1}" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:453 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:455 msgid "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" msgstr "" @@ -59788,7 +59929,7 @@ msgstr "" msgid "{0} is in Draft. Submit it before creating the Asset." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1161 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1171 msgid "{0} is mandatory for Item {1}" msgstr "" @@ -59817,7 +59958,7 @@ msgstr "" msgid "{0} is not a group node. Please select a group node as parent cost center" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:757 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:775 msgid "{0} is not a stock Item" msgstr "" @@ -59849,7 +59990,7 @@ msgstr "" msgid "{0} is not the default supplier for any items." msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2947 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2966 msgid "{0} is on hold till {1}" msgstr "" @@ -59885,11 +60026,11 @@ msgstr "" msgid "{0} must be negative in return document" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2406 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2416 msgid "{0} not allowed to transact with {1}. Please change the Company or add the Company in the 'Allowed To Transact With'-Section in the Customer record." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:612 +#: erpnext/manufacturing/doctype/bom/bom.py:619 msgid "{0} not found for item {1}" msgstr "" @@ -59958,7 +60099,7 @@ msgstr "" msgid "{0} will be set as the {1} in subsequently scanned items" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1010 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1023 msgid "{0} {1}" msgstr "" @@ -59978,21 +60119,21 @@ msgstr "" msgid "{0} {1} created" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:613 -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:666 -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2689 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:628 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:681 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2707 msgid "{0} {1} does not exist" msgstr "" -#: erpnext/accounts/party.py:555 +#: erpnext/accounts/party.py:573 msgid "{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}." msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:463 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:465 msgid "{0} {1} has already been fully paid." msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:473 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:475 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 "" @@ -60014,7 +60155,7 @@ msgstr "" msgid "{0} {1} is already linked to Common Code {2}." msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:696 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:711 msgid "{0} {1} is associated with {2}, but Party Account is {3}" msgstr "" @@ -60035,11 +60176,11 @@ msgstr "" msgid "{0} {1} is closed" msgstr "" -#: erpnext/accounts/party.py:793 +#: erpnext/accounts/party.py:811 msgid "{0} {1} is disabled" msgstr "" -#: erpnext/accounts/party.py:799 +#: erpnext/accounts/party.py:817 msgid "{0} {1} is frozen" msgstr "" @@ -60047,15 +60188,15 @@ msgstr "" msgid "{0} {1} is fully billed" msgstr "" -#: erpnext/accounts/party.py:803 +#: erpnext/accounts/party.py:821 msgid "{0} {1} is not active" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:673 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:688 msgid "{0} {1} is not associated with {2} {3}" msgstr "" -#: erpnext/accounts/utils.py:130 +#: erpnext/accounts/utils.py:132 msgid "{0} {1} is not in any active Fiscal Year" msgstr "" @@ -60064,15 +60205,15 @@ msgstr "" msgid "{0} {1} is not submitted" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:706 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:721 msgid "{0} {1} is on hold" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.py:712 +#: erpnext/accounts/doctype/payment_entry/payment_entry.py:727 msgid "{0} {1} must be submitted" msgstr "" -#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:276 +#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:277 msgid "{0} {1} not allowed to be reposted. You can enable it by adding it '{2}' table in {3}." msgstr "" @@ -60156,11 +60297,15 @@ msgstr "" msgid "{0}'s {1} cannot be after {2}'s Expected End Date." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1311 -#: erpnext/manufacturing/doctype/job_card/job_card.py:1319 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1332 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1340 msgid "{0}, complete the operation {1} before the operation {2}." msgstr "" +#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:61 +msgid "{0}, {1} or {2} are the only allowed options." +msgstr "" + #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:523 msgid "{0}: Child table (auto-deleted with parent)" msgstr "" @@ -60181,14 +60326,10 @@ msgstr "" msgid "{0}: {1} does not belong to the Company: {2}" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1323 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1343 msgid "{0}: {1} does not exist" msgstr "" -#: erpnext/accounts/party.py:78 -msgid "{0}: {1} does not exists" -msgstr "" - #: erpnext/setup/doctype/company/company.py:279 msgid "{0}: {1} is a group account." msgstr "" @@ -60221,7 +60362,7 @@ msgstr "" msgid "{}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2172 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2182 msgid "{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}" msgstr "" From be1aa0e5ebcc05e3b3587333651a95aea55756cf Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 14 Jun 2026 23:41:59 +0530 Subject: [PATCH 42/88] fix: regression issues related to security fixes --- erpnext/accounts/party.py | 6 +- .../doctype/purchase_order/purchase_order.py | 2 +- .../subcontracting_inward_controller.py | 4 +- .../doctype/sales_order/sales_order.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 6 +- .../subcontracting_inward_order.py | 10 +++- .../subcontracting_order.py | 10 +++- .../test_subcontracting_order.py | 56 +++++++++++++++++++ 8 files changed, 85 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index c70b6251ebb..15645602965 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -507,10 +507,10 @@ def get_party_advance_account(party_type, party, company): return account -@frappe.whitelist() def get_party_bank_account(party_type: str, party: str): - frappe.has_permission("Bank Account", "read", throw=True) - return frappe.db.get_value("Bank Account", {"party_type": party_type, "party": party, "is_default": 1}) + return frappe.db.get_value( + "Bank Account", {"party_type": party_type, "party": party, "is_default": 1, "disabled": 0}, "name" + ) def get_party_account_currency(party_type, party, company): diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 049d4352ae3..c5975b1a35e 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -700,7 +700,7 @@ class PurchaseOrder(BuyingController): def update_subcontracting_order_status(self): from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( - update_subcontracting_order_status as update_sco_status, + set_subcontracting_order_status as update_sco_status, ) if self.is_subcontracted and not self.is_old_subcontracting_flow: diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 490f7204d2a..abc91afd5b8 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -1119,10 +1119,10 @@ class SubcontractingInwardController: def update_inward_order_status(self): if self.subcontracting_inward_order: from erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order import ( - update_subcontracting_inward_order_status, + set_subcontracting_inward_order_status, ) - update_subcontracting_inward_order_status(self.subcontracting_inward_order) + set_subcontracting_inward_order_status(self.subcontracting_inward_order) @frappe.whitelist() diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 48f2bbf5e6d..37cc541f411 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -614,7 +614,7 @@ class SalesOrder(SellingController): def update_subcontracting_order_status(self): from erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order import ( - update_subcontracting_inward_order_status as update_scio_status, + set_subcontracting_inward_order_status as update_scio_status, ) if self.is_subcontracted: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 7784e9f98c0..ae536b8dc42 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3972,10 +3972,12 @@ class StockEntry(StockController, SubcontractingInwardController): def update_subcontracting_order_status(self): if self.subcontracting_order and self.purpose in ["Send to Subcontractor", "Material Transfer"]: from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( - update_subcontracting_order_status, + set_subcontracting_order_status, ) - update_subcontracting_order_status(self.subcontracting_order) + # Trusted submit/cancel flow — a Stock operation must not require Subcontracting Order + # write permission, so use the no-check internal helper (not the whitelisted boundary). + set_subcontracting_order_status(self.subcontracting_order) def update_pick_list_status(self): from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py index 79f2ed33ed2..9687a070bda 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py @@ -550,10 +550,18 @@ class SubcontractingInwardOrder(SubcontractingController): return stock_entry.as_dict() +def set_subcontracting_inward_order_status(scio: str | Document, status: str | None = None): + if isinstance(scio, str): + scio = frappe.get_doc("Subcontracting Inward Order", scio) + + scio.update_status(status) + + @frappe.whitelist() def update_subcontracting_inward_order_status(scio: str | Document, status: str | None = None): + """Whitelisted boundary for direct API/UI calls — enforces write permission, then delegates.""" if isinstance(scio, str): scio = frappe.get_doc("Subcontracting Inward Order", scio) scio.check_permission("write") - scio.update_status(status) + set_subcontracting_inward_order_status(scio, status) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 29233f68195..043391145d2 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -483,10 +483,18 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None, items=None): return target_doc +def set_subcontracting_order_status(sco: str | Document, status: str | None = None): + if isinstance(sco, str): + sco = frappe.get_doc("Subcontracting Order", sco) + + sco.update_status(status) + + @frappe.whitelist() def update_subcontracting_order_status(sco: str | Document, status: str | None = None): + """Whitelisted boundary for direct API/UI calls — enforces write permission, then delegates.""" if isinstance(sco, str): sco = frappe.get_doc("Subcontracting Order", sco) sco.check_permission("write") - sco.update_status(status) + set_subcontracting_order_status(sco, status) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 96d3b861cf0..bf803fc3d9a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -336,6 +336,62 @@ class TestSubcontractingOrder(ERPNextTestSuite): bin_after_cancel_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract ) + def test_send_to_subcontractor_ste_submit_without_sco_write_permission(self): + """A Stock-only user (can submit Stock Entries but has no Subcontracting Order write) must be + able to submit and cancel a 'Send to Subcontractor' Stock Entry. The SCO status update on the + on_submit/on_cancel path goes through the no-permission-check internal helper, not the + whitelisted API boundary. + + Regression: the permission hardening put check_permission('write') on the shared status + function, so a Stock Manager (no SCO write) hit PermissionError submitting/cancelling the + Stock Entry. The suite otherwise runs as Administrator and never caught it.""" + from frappe.core.doctype.user_permission.test_user_permission import create_user + + make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + + rm_items = [ + { + "item_code": "_Test FG Item", + "rm_item_code": "_Test Item", + "item_name": "_Test Item", + "qty": 10, + "warehouse": "_Test Warehouse - _TC", + "rate": 100, + "amount": 1000, + "stock_uom": "Nos", + }, + ] + ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) + ste.to_warehouse = "_Test Warehouse 1 - _TC" + ste.save() + + stock_user = create_user("test_sco_stock_only@example.com", "Stock Manager") + self.assertFalse( + frappe.has_permission("Subcontracting Order", "write", user=stock_user.name), + "Precondition: the Stock-only user must not have Subcontracting Order write permission.", + ) + + frappe.set_user(stock_user.name) + try: + ste.reload() + ste.submit() # must not raise PermissionError on the SCO status update + ste.reload() + ste.cancel() # same on the cancel path + finally: + frappe.set_user("Administrator") + def test_exploded_items(self): item_code = "_Test Subcontracted FG Item 11" make_subcontracted_item(item_code=item_code) From ede13cb3bdd4aa6ec16fecdf49cd107617c5616e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 14 Jun 2026 23:50:23 +0530 Subject: [PATCH 43/88] refactor: consolidate duplicate get_party_bank_account into bank_account.py --- erpnext/accounts/doctype/payment_request/payment_request.py | 3 ++- erpnext/accounts/party.py | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index e16e132957f..d2dcb2ea795 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -11,11 +11,12 @@ from erpnext import get_company_currency from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) +from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account from erpnext.accounts.doctype.payment_entry.payment_entry import ( get_payment_entry, ) from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate -from erpnext.accounts.party import get_party_account, get_party_bank_account +from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import get_account_currency, get_advance_payment_doctypes, get_currency_precision from erpnext.utilities import payment_app_import_guard diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 15645602965..7f0583f0d1d 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -507,12 +507,6 @@ def get_party_advance_account(party_type, party, company): return account -def get_party_bank_account(party_type: str, party: str): - return frappe.db.get_value( - "Bank Account", {"party_type": party_type, "party": party, "is_default": 1, "disabled": 0}, "name" - ) - - def get_party_account_currency(party_type, party, company): def generator(): party_account = get_party_account(party_type, party, company) From 6a3c973b0f3a47aedc75b728815e96827c8b2cd7 Mon Sep 17 00:00:00 2001 From: Raghav Ruia Date: Mon, 15 Jun 2026 09:26:09 +0530 Subject: [PATCH 44/88] fix: show company name in delete transactions confirmation dialog Display the actual company name in bold within the confirmation dialog label so users immediately know which company they must type to confirm, reducing the risk of accidental data loss. Co-Authored-By: Claude Sonnet 4.6 (cherry picked from commit 87d26a2d678a762461b0d85c4f8071890c7f6d3e) --- erpnext/setup/doctype/company/company.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index f8daf3c6f31..b7bd146c7e6 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -211,7 +211,9 @@ frappe.ui.form.on("Company", { { fieldtype: "Data", fieldname: "company_name", - label: __("Please enter the company name to confirm"), + label: __('Please enter the company name "{0}" to confirm', [ + frappe.utils.escape_html(frm.doc.name), + ]), reqd: 1, description: __( "Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone." From e804bf33ba388debd6a601220aa6709cbf19daee Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Tue, 9 Jun 2026 11:58:30 +0530 Subject: [PATCH 45/88] feat(currency exchange settings): frankfurter v2 support (cherry picked from commit 56bfe6b6a695b2d93ceedf8d50082aa732789b2a) --- .../currency_exchange_settings.js | 18 ++++++++++++------ .../currency_exchange_settings.json | 6 +++--- .../currency_exchange_settings.py | 19 +++++++++++++++++-- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js index 40f0938ee1c..950092a2382 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js @@ -11,22 +11,28 @@ frappe.ui.form.on("Currency Exchange Settings", { }, callback: function (r) { if (r && r.message) { + let result = [], + params = {}; if (frm.doc.service_provider == "exchangerate.host") { - let result = ["result"]; - let params = { + result = ["result"]; + params = { date: "{transaction_date}", from: "{from_currency}", to: "{to_currency}", }; - add_param(frm, r.message, params, result); } else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) { - let result = ["rates", "{to_currency}"]; - let params = { + result = ["rates", "{to_currency}"]; + params = { base: "{from_currency}", symbols: "{to_currency}", }; - add_param(frm, r.message, params, result); + } else if (frm.doc.service_provider == "frankfurter.dev - v2") { + result = ["rate"]; + params = { + date: "{transaction_date}", + }; } + add_param(frm, r.message, params, result); } }, }); diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 9f0852bb686..2fbb0086245 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "creation": "2022-01-10 13:03:26.237081", "doctype": "DocType", "editable_grid": 1, @@ -78,7 +79,7 @@ "fieldname": "service_provider", "fieldtype": "Select", "label": "Service Provider", - "options": "frankfurter.dev\nexchangerate.host\nCustom", + "options": "frankfurter.dev\nexchangerate.host\nfrankfurter.dev - v2\nCustom", "reqd": 1 }, { @@ -101,11 +102,10 @@ "label": "Use HTTP Protocol" } ], - "hide_toolbar": 0, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-03-16 13:28:21.075743", + "modified": "2026-06-09 11:34:10.432378", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py index 55f967fc788..3d7651cb485 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document): disabled: DF.Check req_params: DF.Table[CurrencyExchangeSettingsDetails] result_key: DF.Table[CurrencyExchangeSettingsResult] - service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"] + service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "frankfurter.dev - v2", "Custom"] url: DF.Data | None use_http: DF.Check # end: auto-generated types @@ -70,6 +70,14 @@ class CurrencyExchangeSettings(Document): self.append("req_params", {"key": "base", "value": "{from_currency}"}) self.append("req_params", {"key": "symbols", "value": "{to_currency}"}) + elif self.service_provider == "frankfurter.dev - v2": + self.set("result_key", []) + self.set("req_params", []) + + self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http) + self.append("result_key", {"key": "rate"}) + self.append("req_params", {"key": "date", "value": "{transaction_date}"}) + def validate_parameters(self): params = {} for row in self.req_params: @@ -105,13 +113,20 @@ class CurrencyExchangeSettings(Document): @frappe.whitelist() def get_api_endpoint(service_provider: str | None = None, use_http: bool = False): - if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]: + if service_provider and service_provider in [ + "exchangerate.host", + "frankfurter.dev", + "frankfurter.app", + "frankfurter.dev - v2", + ]: if service_provider == "exchangerate.host": api = "api.exchangerate.host/convert" elif service_provider == "frankfurter.app": api = "api.frankfurter.app/{transaction_date}" elif service_provider == "frankfurter.dev": api = "api.frankfurter.dev/v1/{transaction_date}" + elif service_provider == "frankfurter.dev - v2": + api = "api.frankfurter.dev/v2/rate/{from_currency}/{to_currency}" protocol = "https://" if use_http: From 471ab662f6ffac1bfcc2e96d2131ca778a58c763 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Tue, 9 Jun 2026 12:17:18 +0530 Subject: [PATCH 46/88] fix: use frankfurter v2 by default for new install (cherry picked from commit 479f9f63c9b3e26410e67190b1e8cf4e47832b19) --- erpnext/setup/install.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index b8162bb648c..08ce8d98a28 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -87,14 +87,7 @@ def setup_repost_defaults(): def setup_currency_exchange(): ces = frappe.get_single("Currency Exchange Settings") try: - ces.set("result_key", []) - ces.set("req_params", []) - - ces.api_endpoint = "https://api.frankfurter.dev/v1/{transaction_date}" - ces.append("result_key", {"key": "rates"}) - ces.append("result_key", {"key": "{to_currency}"}) - ces.append("req_params", {"key": "base", "value": "{from_currency}"}) - ces.append("req_params", {"key": "symbols", "value": "{to_currency}"}) + ces.service_provider = "frankfurter.dev - v2" ces.save() except frappe.ValidationError: pass From 6f25d915c77b26285fcd727543a8073b2b4619e5 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Tue, 9 Jun 2026 23:00:41 +0530 Subject: [PATCH 47/88] test: fixed currency exchange test for frankfurter v2 api (cherry picked from commit 138f683a68acb6c698beb26821299264cc1e4e73) --- .../doctype/currency_exchange/test_currency_exchange.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index 79df2a7ab32..ba8b685ebb6 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -66,13 +66,16 @@ def patched_requests_get(*args, **kwargs): if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"): if test_exchange_values.get(kwargs["params"]["date"]): return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200) - elif args[0].startswith("https://api.frankfurter.dev") and kwargs.get("params"): + elif args[0].startswith("https://api.frankfurter.dev/v1") and kwargs.get("params"): if kwargs["params"].get("base") and kwargs["params"].get("symbols"): date = args[0].replace("https://api.frankfurter.dev/v1/", "") if test_exchange_values.get(date): return PatchResponse( {"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200 ) + elif args[0].startswith("https://api.frankfurter.dev/v2") and kwargs.get("params"): + if kwargs["params"].get("date") and test_exchange_values.get(kwargs["params"]["date"]): + return PatchResponse({"rate": test_exchange_values.get(kwargs["params"]["date"])}, 200) return PatchResponse({"rates": None}, 404) From 79fd176a8e13674428ce144556ce0cb1a0876ff4 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Mon, 15 Jun 2026 11:27:26 +0530 Subject: [PATCH 48/88] fix: restricting currency_exchange_settings write permission only to system manager (cherry picked from commit 0c2d5488a64325724afe4b3f925a9be92e000a4f) --- .../currency_exchange_settings.json | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 2fbb0086245..a3aea6016b9 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_bulk_edit": 1, "creation": "2022-01-10 13:03:26.237081", "doctype": "DocType", "editable_grid": 1, @@ -105,7 +104,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-06-09 11:34:10.432378", + "modified": "2026-06-15 11:25:55.873110", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", @@ -122,24 +121,11 @@ "write": 1 }, { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "Accounts Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, "email": 1, "print": 1, "read": 1, "role": "Accounts User", - "share": 1, - "write": 1 + "share": 1 } ], "row_format": "Dynamic", From 2606d660af5480b9675eb9c741faf372587824f7 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Mon, 15 Jun 2026 13:50:38 +0530 Subject: [PATCH 49/88] fix(get_exchange_rate): using get_single_value to fetch `disabled` value from `currency_exchange_settings` (cherry picked from commit abb579e2db7ac7705b859f8fb037fac7a0de96be) --- erpnext/setup/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 03938ebb94e..d5d3081bac4 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -95,7 +95,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if entries: return flt(entries[0].exchange_rate) - if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"): + if frappe.get_single_value("Currency Exchange Settings", "disabled"): return 0.00 pegged_currencies = {} From 67cc59c5ca5826c9d47f2094c18cc69527fdd39e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:31:22 +0530 Subject: [PATCH 50/88] feat: new banking module (backport #54720) (#55917) * feat: new banking module (#54720) * feat: initial SPA setup for banking * wip: bring over new banking module * feat: added Espresso design tokens * feat: button styles * fix: add all ink colors * wip: espresso design system changes * feat: button and badge espresso components * fix: button styling for reconcile * feat: Espresso progress bar * feat: Espresso toggle switch * feat: Espresso tabs design * fix: vertical tab support * fix: button sizing across modals * feat: Espresso style table layout * feat: Espresso tooltip * feat: Espresso elevations and checkbox * feat: Dialog with Espresso styles * feat: Espresso textarea * fix: input styles * fix: colors on bank picker * fix: breadcrumb styling * fix: bank picker styling * feat: create doctypes and fields for bank reconciliation * feat: APIs for banking * fix: use date format parser * fix: font styling to match Espresso * wip: settings modal * feat: settings dialog component * fix: icons and invalid requests * feat: preferences tab * fix: adjust icon stroke width to 1.5 * feat: rule configuration in settings * fix: remove sheet component * feat: alert and error banner component * feat: dropdown in Espresso * feat: popover and select in Espresso * fix: cleanup more styles * fix: match size of link fields * feat: command styling * fix: remove unused style tokens * fix: styles for global date picker dropdown * fix: styles for match and reconcile * feat: table Espresso component * feat: remove all other design tokens * fix: remove unused tokens * fix: form elements * fix: remove unused styles and fix filters in bank transaction list * feat: fetch bank rec doctypes for filtering * fix: record payment modal * feat: support for dark mode switching * fix: move bank logos to public folder * feat: add support for RTL * feat: support for RTL * chore: send layout direction in dev boot * fix: make checkbox work in RTL * feat: dark mode support * fix: dark mode style * feat: bank logos in dark mode * feat: dark mode bank logos * chore: use dark mode bank logos everywhere * chore: move rule evaluation to controller * chore: add tests for bank transaction rules * fix: move deps to fix actions errors * fix: move tw-animate-css to deps * fix: remove shadcn * fix: do not open modal if no transactions selected * fix: add translation strings * feat: add banner on existing bank reconciliation tool * feat: bank statement import * fix: translations and layout directions * fix: validation for transaction matching rule * fix: styles * fix: show conflicting transactions in alert * fix: show help text for new banking module forms * feat: show total debits and credits * fix: dark mode colors in automatic config * feat: add keyboard shortcuts help * feat: added keyboard shortcut for settings * fix: decrease size of progress bar * chore: bump packages * feat: add tests for statement import * fix: settings dialog * fix: show banner on small screens * fix: show banner when no bank account set (cherry picked from commit 6de5367f12dd31111cb6b2bd9c144a3c2d764d2a) # Conflicts: # erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py * chore: resolve conflicts * fix: add type hints to whitelisted methods --------- Co-authored-by: Nikhil Kothari --- .gitignore | 4 + babel_extractors.csv | 2 + banking/.env.production | 1 + banking/.gitignore | 24 + banking/README.md | 73 + banking/eslint.config.js | 24 + banking/index.html | 50 + banking/package.json | 66 + banking/proxyOptions.ts | 13 + banking/src/App.tsx | 65 + .../components/common/AccountsDropdown.tsx | 228 + banking/src/components/common/BankLogo.tsx | 26 + .../components/common/FileUploadBanner.tsx | 17 + .../components/common/LinkFieldCombobox.tsx | 301 + .../components/common/PartyTypeDropdown.tsx | 82 + .../features/ActionLog/ActionLog.tsx | 475 + .../BankReconciliation/BankBalance.tsx | 334 + .../BankClearanceSummary.tsx | 355 + .../BankReconciliation/BankEntryModal.tsx | 831 ++ .../BankReconciliation/BankPicker.tsx | 124 + .../BankReconciliation/BankRecDateFilter.tsx | 275 + .../BankReconciliationStatement.tsx | 315 + .../BankTransactionList.tsx | 419 + .../BankTransactionUnreconcileModal.tsx | 125 + .../BankReconciliation/CompanySelector.tsx | 92 + .../IncorrectlyClearedEntries.tsx | 229 + .../BankReconciliation/MatchAndReconcile.tsx | 949 ++ .../BankReconciliation/MatchFilters.tsx | 93 + .../MissingFiltersBanner.tsx | 10 + .../BankReconciliation/RecordPaymentModal.tsx | 1301 +++ .../Rules/CreateNewRule.tsx | 89 + .../BankReconciliation/Rules/EditRule.tsx | 101 + .../BankReconciliation/Rules/RuleForm.tsx | 799 ++ .../SelectedTransactionDetails.tsx | 73 + .../SelectedTransactionsTable.tsx | 47 + .../BankReconciliation/TransferModal.tsx | 555 + .../BankReconciliation/bankRecAtoms.ts | 83 + .../features/BankReconciliation/logos.ts | 397 + .../features/BankReconciliation/utils.ts | 457 + .../BankStatementImporter/CSV/CSVImport.tsx | 22 + .../CSV/CSVRawDataPreview.tsx | 151 + .../CSV/StatementDetails.tsx | 351 + .../BankStatementImporter/import_utils.ts | 42 + .../features/Settings/KeyboardShortcuts.tsx | 115 + .../features/Settings/MatchingRules.tsx | 46 + .../features/Settings/Preferences.tsx | 261 + .../features/Settings/Rules/RuleList.tsx | 314 + .../components/features/Settings/Settings.tsx | 95 + banking/src/components/ui/alert-dialog.tsx | 196 + banking/src/components/ui/alert.tsx | 104 + banking/src/components/ui/badge.tsx | 188 + banking/src/components/ui/breadcrumb.tsx | 109 + banking/src/components/ui/button.tsx | 263 + banking/src/components/ui/calendar.tsx | 218 + banking/src/components/ui/card.tsx | 92 + banking/src/components/ui/checkbox.tsx | 44 + banking/src/components/ui/command.tsx | 183 + banking/src/components/ui/dialog.tsx | 156 + banking/src/components/ui/direction.tsx | 20 + banking/src/components/ui/dropdown-menu.tsx | 262 + banking/src/components/ui/empty.tsx | 85 + banking/src/components/ui/error-banner.tsx | 64 + banking/src/components/ui/file-dropzone.tsx | 289 + banking/src/components/ui/form-elements.tsx | 383 + banking/src/components/ui/form.tsx | 174 + banking/src/components/ui/hover-card.tsx | 42 + banking/src/components/ui/input-group.tsx | 161 + banking/src/components/ui/input.tsx | 49 + banking/src/components/ui/kbd.tsx | 28 + banking/src/components/ui/keyboard-keys.tsx | 8 + banking/src/components/ui/label.tsx | 22 + banking/src/components/ui/list-view.tsx | 510 + banking/src/components/ui/loaders.tsx | 27 + banking/src/components/ui/markdown.tsx | 28 + banking/src/components/ui/popover.tsx | 87 + banking/src/components/ui/progress.tsx | 67 + banking/src/components/ui/radio-group.tsx | 43 + banking/src/components/ui/select.tsx | 221 + banking/src/components/ui/separator.tsx | 26 + banking/src/components/ui/settings-dialog.tsx | 273 + banking/src/components/ui/skeleton.tsx | 13 + banking/src/components/ui/sonner.tsx | 53 + banking/src/components/ui/stats.tsx | 13 + banking/src/components/ui/switch.tsx | 45 + banking/src/components/ui/table.tsx | 114 + banking/src/components/ui/tabs.tsx | 168 + banking/src/components/ui/textarea.tsx | 43 + banking/src/components/ui/theme-provider.tsx | 89 + banking/src/components/ui/tooltip.tsx | 56 + banking/src/components/ui/typography.tsx | 45 + banking/src/hooks/use-mobile.ts | 19 + banking/src/hooks/useCurrentCompany.ts | 9 + banking/src/hooks/useDocType.ts | 30 + banking/src/hooks/useFiscalYear.ts | 13 + .../src/hooks/usePaymentEntryCalculations.tsx | 138 + banking/src/index.css | 1208 ++ banking/src/lib/checks.ts | 20 + banking/src/lib/company.ts | 15 + banking/src/lib/currency.ts | 24 + banking/src/lib/date.ts | 184 + banking/src/lib/file.ts | 68 + banking/src/lib/frappe.ts | 85 + banking/src/lib/namespace/defaults.js | 12 + banking/src/lib/namespace/index.js | 3 + banking/src/lib/namespace/namespace.js | 22 + banking/src/lib/namespace/sync.js | 187 + banking/src/lib/numbers.ts | 249 + banking/src/lib/permissions.ts | 78 + banking/src/lib/translate.ts | 45 + banking/src/lib/utils.ts | 18 + banking/src/main.tsx | 42 + banking/src/pages/BankReconciliation.tsx | 140 + banking/src/pages/BankStatementImporter.tsx | 255 + .../pages/BankStatementImporterContainer.tsx | 37 + .../src/pages/ViewBankStatementImportLog.tsx | 46 + .../src/types/Accounts/AccountsSettings.ts | 134 + .../types/Accounts/AdvanceTaxesandCharges.ts | 41 + banking/src/types/Accounts/BankAccount.ts | 49 + .../src/types/Accounts/BankAccountBalance.ts | 19 + .../types/Accounts/BankStatementImportLog.ts | 50 + .../BankStatementImportLogColumnMap.ts | 21 + banking/src/types/Accounts/BankTransaction.ts | 64 + .../types/Accounts/BankTransactionPayments.ts | 23 + .../src/types/Accounts/BankTransactionRule.ts | 43 + .../Accounts/BankTransactionRuleAccounts.ts | 25 + ...ankTransactionRuleDescriptionConditions.ts | 17 + banking/src/types/Accounts/JournalEntry.ts | 96 + .../src/types/Accounts/JournalEntryAccount.ts | 53 + banking/src/types/Accounts/PaymentEntry.ts | 148 + .../types/Accounts/PaymentEntryDeduction.ts | 23 + .../types/Accounts/PaymentEntryReference.ts | 47 + banking/src/types/custom/Reports.ts | 20 + banking/src/vite-env.d.ts | 6 + banking/tsconfig.app.json | 34 + banking/tsconfig.json | 19 + banking/tsconfig.node.json | 28 + banking/vite.config.ts | 25 + banking/yarn.lock | 3796 +++++++ .../accounts_settings/accounts_settings.json | 17 + .../accounts_settings/accounts_settings.py | 2 + .../doctype/bank_account/bank_account.json | 10 +- .../doctype/bank_account/bank_account.py | 89 + .../doctype/bank_account_balance/__init__.py | 0 .../bank_account_balance.js | 8 + .../bank_account_balance.json | 96 + .../bank_account_balance.py | 22 + .../test_bank_account_balance.py | 20 + .../bank_reconciliation_tool.js | 5 + .../bank_reconciliation_tool.py | 674 +- .../bank_statement_import_log/__init__.py | 0 .../bank_statement_import_log.js | 12 + .../bank_statement_import_log.json | 211 + .../bank_statement_import_log.py | 743 ++ .../test_bank_statement_import_log.py | 349 + .../__init__.py | 0 .../bank_statement_import_log_column_map.json | 60 + .../bank_statement_import_log_column_map.py | 42 + .../bank_transaction/bank_transaction.json | 20 +- .../bank_transaction/bank_transaction.py | 61 +- .../bank_transaction_payments.json | 16 +- .../bank_transaction_payments.py | 1 + .../doctype/bank_transaction_rule/__init__.py | 0 .../bank_transaction_rule.js | 20 + .../bank_transaction_rule.json | 194 + .../bank_transaction_rule.py | 249 + .../test_bank_transaction_rule.py | 231 + .../__init__.py | 0 .../bank_transaction_rule_accounts.json | 68 + .../bank_transaction_rule_accounts.py | 28 + .../__init__.py | 0 ...ansaction_rule_description_conditions.json | 44 + ...transaction_rule_description_conditions.py | 24 + erpnext/hooks.py | 2 + erpnext/public/images/bank-logos/ABSA.png | Bin 0 -> 13854 bytes erpnext/public/images/bank-logos/ANZ.png | Bin 0 -> 9644 bytes .../images/bank-logos/Airwallex-dark.png | Bin 0 -> 10130 bytes .../public/images/bank-logos/Airwallex.png | Bin 0 -> 10460 bytes .../public/images/bank-logos/Alpha_Bank.svg | 17 + erpnext/public/images/bank-logos/Amex.svg | Bin 0 -> 60376 bytes .../bank-logos/Australian_Tax_Office.png | Bin 0 -> 9066 bytes .../public/images/bank-logos/Avanz-dark.svg | 43 + erpnext/public/images/bank-logos/Avanz.svg | 43 + .../public/images/bank-logos/Axis_Bank.svg | 105 + .../images/bank-logos/BAC_Credomatic.svg | 17 + .../images/bank-logos/BNP_Paribas-Dark.svg | 124 + .../public/images/bank-logos/BNP_Paribas.svg | 124 + .../images/bank-logos/BNY_Mellon-Dark.svg | 18 + .../public/images/bank-logos/BNY_Mellon.svg | 18 + .../images/bank-logos/Banco_Atlantida.png | Bin 0 -> 4800 bytes .../public/images/bank-logos/Banco_Lafise.png | Bin 0 -> 33550 bytes .../images/bank-logos/Banco_de_Finanzas.svg | 1 + .../images/bank-logos/Bank_of_America.png | Bin 0 -> 39122 bytes .../images/bank-logos/Bank_of_Baroda.svg | 101 + .../images/bank-logos/Bank_of_India.png | Bin 0 -> 121911 bytes .../images/bank-logos/Bank_of_Maharashtra.png | Bin 0 -> 45473 bytes erpnext/public/images/bank-logos/Barclays.svg | 63 + .../public/images/bank-logos/Capital_One.png | Bin 0 -> 19135 bytes .../images/bank-logos/Charles_Schwab.svg | 1 + erpnext/public/images/bank-logos/Citi.svg | 11 + .../images/bank-logos/Commonwealth_Bank.svg | 1 + .../images/bank-logos/Deutsche_Bank.svg | 125 + .../images/bank-logos/Diamond_Trust_Bank.png | Bin 0 -> 2144 bytes .../images/bank-logos/Equity_Bank-dark.png | Bin 0 -> 11380 bytes .../public/images/bank-logos/Equity_Bank.png | Bin 0 -> 12473 bytes .../images/bank-logos/Federal_Bank-Dark.png | Bin 0 -> 75120 bytes .../public/images/bank-logos/Federal_Bank.png | Bin 0 -> 79299 bytes erpnext/public/images/bank-logos/Fi_Bank.svg | 1 + erpnext/public/images/bank-logos/Ficohsa.svg | 13 + .../images/bank-logos/Goldman_Sachs.svg | 3 + erpnext/public/images/bank-logos/HDFC.svg | 37 + .../public/images/bank-logos/HSBC-dark.svg | 21 + erpnext/public/images/bank-logos/HSBC.svg | 21 + erpnext/public/images/bank-logos/I&M.png | Bin 0 -> 164234 bytes .../public/images/bank-logos/ICICI-dark.svg | 567 + erpnext/public/images/bank-logos/ICICI.svg | 567 + .../public/images/bank-logos/IDBI_Bank.svg | 1 + .../images/bank-logos/IDFC_First_Bank.svg | 26 + .../images/bank-logos/IndusInd_Bank.svg | 83 + .../images/bank-logos/Judo_Bank-dark.svg | 1 + .../public/images/bank-logos/Judo_Bank.svg | 1 + .../images/bank-logos/KCB_Bank_Kenya.png | Bin 0 -> 20736 bytes .../images/bank-logos/Kotak_Mahindra.svg | 1 + .../public/images/bank-logos/Macquarie.svg | 1 + .../images/bank-logos/Morgan_Stanley.png | Bin 0 -> 10729 bytes .../images/bank-logos/Oakstar-dark.webp | Bin 0 -> 44796 bytes erpnext/public/images/bank-logos/Oakstar.png | Bin 0 -> 9557 bytes erpnext/public/images/bank-logos/PNC.png | Bin 0 -> 13923 bytes .../images/bank-logos/PlainsCapitalBank.png | Bin 0 -> 32099 bytes .../public/images/bank-logos/Prime_Bank.png | Bin 0 -> 5257 bytes .../bank-logos/Punjab_National_Bank.svg | 1 + .../images/bank-logos/RBL_Bank-dark.svg | 17 + erpnext/public/images/bank-logos/RBL_Bank.svg | 9704 +++++++++++++++++ .../images/bank-logos/Razorpay-dark.svg | 21 + erpnext/public/images/bank-logos/Razorpay.svg | 21 + erpnext/public/images/bank-logos/Revolut.png | Bin 0 -> 4039 bytes .../public/images/bank-logos/Santander.svg | 12 + .../public/images/bank-logos/Sparkasse.png | Bin 0 -> 36141 bytes erpnext/public/images/bank-logos/Stanbic.png | Bin 0 -> 70873 bytes .../bank-logos/Standard_Chartered-dark.png | Bin 0 -> 38100 bytes .../images/bank-logos/Standard_Chartered.png | Bin 0 -> 54760 bytes .../images/bank-logos/Starling_Bank-dark.png | Bin 0 -> 13680 bytes .../images/bank-logos/Starling_Bank.png | Bin 0 -> 13452 bytes .../images/bank-logos/State_Bank_of_India.svg | 86 + .../bank-logos/State_bank_of_India-Dark.svg | 86 + .../bank-logos/Toronto_Dominion_Bank.png | Bin 0 -> 5802 bytes erpnext/public/images/bank-logos/Truist.svg | 13 + erpnext/public/images/bank-logos/UBS-dark.svg | 1 + erpnext/public/images/bank-logos/UBS.svg | 1 + .../public/images/bank-logos/USBank-dark.svg | 1 + erpnext/public/images/bank-logos/USBank.svg | 1 + .../images/bank-logos/Union_Bank_of_India.svg | 156 + .../Volksbanken_Raiffeisenbanken.svg | 33 + .../public/images/bank-logos/Wells_Fargo.svg | 44 + erpnext/public/images/bank-logos/Westpac.svg | 1 + .../images/bank-logos/Yes_Bank-dark.svg | 4 + erpnext/public/images/bank-logos/Yes_Bank.svg | 4 + .../public/images/bank-logos/chase-Dark.svg | 28 + erpnext/public/images/bank-logos/chase.svg | 28 + erpnext/public/images/bank-logos/jpmc.svg | 68 + erpnext/www/banking.py | 54 + package.json | 7 +- pyproject.toml | 5 + 262 files changed, 39511 insertions(+), 50 deletions(-) create mode 100644 banking/.env.production create mode 100644 banking/.gitignore create mode 100644 banking/README.md create mode 100644 banking/eslint.config.js create mode 100644 banking/index.html create mode 100644 banking/package.json create mode 100644 banking/proxyOptions.ts create mode 100644 banking/src/App.tsx create mode 100644 banking/src/components/common/AccountsDropdown.tsx create mode 100644 banking/src/components/common/BankLogo.tsx create mode 100644 banking/src/components/common/FileUploadBanner.tsx create mode 100644 banking/src/components/common/LinkFieldCombobox.tsx create mode 100644 banking/src/components/common/PartyTypeDropdown.tsx create mode 100644 banking/src/components/features/ActionLog/ActionLog.tsx create mode 100644 banking/src/components/features/BankReconciliation/BankBalance.tsx create mode 100644 banking/src/components/features/BankReconciliation/BankClearanceSummary.tsx create mode 100644 banking/src/components/features/BankReconciliation/BankEntryModal.tsx create mode 100644 banking/src/components/features/BankReconciliation/BankPicker.tsx create mode 100644 banking/src/components/features/BankReconciliation/BankRecDateFilter.tsx create mode 100644 banking/src/components/features/BankReconciliation/BankReconciliationStatement.tsx create mode 100644 banking/src/components/features/BankReconciliation/BankTransactionList.tsx create mode 100644 banking/src/components/features/BankReconciliation/BankTransactionUnreconcileModal.tsx create mode 100644 banking/src/components/features/BankReconciliation/CompanySelector.tsx create mode 100644 banking/src/components/features/BankReconciliation/IncorrectlyClearedEntries.tsx create mode 100644 banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx create mode 100644 banking/src/components/features/BankReconciliation/MatchFilters.tsx create mode 100644 banking/src/components/features/BankReconciliation/MissingFiltersBanner.tsx create mode 100644 banking/src/components/features/BankReconciliation/RecordPaymentModal.tsx create mode 100644 banking/src/components/features/BankReconciliation/Rules/CreateNewRule.tsx create mode 100644 banking/src/components/features/BankReconciliation/Rules/EditRule.tsx create mode 100644 banking/src/components/features/BankReconciliation/Rules/RuleForm.tsx create mode 100644 banking/src/components/features/BankReconciliation/SelectedTransactionDetails.tsx create mode 100644 banking/src/components/features/BankReconciliation/SelectedTransactionsTable.tsx create mode 100644 banking/src/components/features/BankReconciliation/TransferModal.tsx create mode 100644 banking/src/components/features/BankReconciliation/bankRecAtoms.ts create mode 100644 banking/src/components/features/BankReconciliation/logos.ts create mode 100644 banking/src/components/features/BankReconciliation/utils.ts create mode 100644 banking/src/components/features/BankStatementImporter/CSV/CSVImport.tsx create mode 100644 banking/src/components/features/BankStatementImporter/CSV/CSVRawDataPreview.tsx create mode 100644 banking/src/components/features/BankStatementImporter/CSV/StatementDetails.tsx create mode 100644 banking/src/components/features/BankStatementImporter/import_utils.ts create mode 100644 banking/src/components/features/Settings/KeyboardShortcuts.tsx create mode 100644 banking/src/components/features/Settings/MatchingRules.tsx create mode 100644 banking/src/components/features/Settings/Preferences.tsx create mode 100644 banking/src/components/features/Settings/Rules/RuleList.tsx create mode 100644 banking/src/components/features/Settings/Settings.tsx create mode 100644 banking/src/components/ui/alert-dialog.tsx create mode 100644 banking/src/components/ui/alert.tsx create mode 100644 banking/src/components/ui/badge.tsx create mode 100644 banking/src/components/ui/breadcrumb.tsx create mode 100644 banking/src/components/ui/button.tsx create mode 100644 banking/src/components/ui/calendar.tsx create mode 100644 banking/src/components/ui/card.tsx create mode 100644 banking/src/components/ui/checkbox.tsx create mode 100644 banking/src/components/ui/command.tsx create mode 100644 banking/src/components/ui/dialog.tsx create mode 100644 banking/src/components/ui/direction.tsx create mode 100644 banking/src/components/ui/dropdown-menu.tsx create mode 100644 banking/src/components/ui/empty.tsx create mode 100644 banking/src/components/ui/error-banner.tsx create mode 100644 banking/src/components/ui/file-dropzone.tsx create mode 100644 banking/src/components/ui/form-elements.tsx create mode 100644 banking/src/components/ui/form.tsx create mode 100644 banking/src/components/ui/hover-card.tsx create mode 100644 banking/src/components/ui/input-group.tsx create mode 100644 banking/src/components/ui/input.tsx create mode 100644 banking/src/components/ui/kbd.tsx create mode 100644 banking/src/components/ui/keyboard-keys.tsx create mode 100644 banking/src/components/ui/label.tsx create mode 100644 banking/src/components/ui/list-view.tsx create mode 100644 banking/src/components/ui/loaders.tsx create mode 100644 banking/src/components/ui/markdown.tsx create mode 100644 banking/src/components/ui/popover.tsx create mode 100644 banking/src/components/ui/progress.tsx create mode 100644 banking/src/components/ui/radio-group.tsx create mode 100644 banking/src/components/ui/select.tsx create mode 100644 banking/src/components/ui/separator.tsx create mode 100644 banking/src/components/ui/settings-dialog.tsx create mode 100644 banking/src/components/ui/skeleton.tsx create mode 100644 banking/src/components/ui/sonner.tsx create mode 100644 banking/src/components/ui/stats.tsx create mode 100644 banking/src/components/ui/switch.tsx create mode 100644 banking/src/components/ui/table.tsx create mode 100644 banking/src/components/ui/tabs.tsx create mode 100644 banking/src/components/ui/textarea.tsx create mode 100644 banking/src/components/ui/theme-provider.tsx create mode 100644 banking/src/components/ui/tooltip.tsx create mode 100644 banking/src/components/ui/typography.tsx create mode 100644 banking/src/hooks/use-mobile.ts create mode 100644 banking/src/hooks/useCurrentCompany.ts create mode 100644 banking/src/hooks/useDocType.ts create mode 100644 banking/src/hooks/useFiscalYear.ts create mode 100644 banking/src/hooks/usePaymentEntryCalculations.tsx create mode 100644 banking/src/index.css create mode 100644 banking/src/lib/checks.ts create mode 100644 banking/src/lib/company.ts create mode 100644 banking/src/lib/currency.ts create mode 100644 banking/src/lib/date.ts create mode 100644 banking/src/lib/file.ts create mode 100644 banking/src/lib/frappe.ts create mode 100644 banking/src/lib/namespace/defaults.js create mode 100644 banking/src/lib/namespace/index.js create mode 100644 banking/src/lib/namespace/namespace.js create mode 100644 banking/src/lib/namespace/sync.js create mode 100644 banking/src/lib/numbers.ts create mode 100644 banking/src/lib/permissions.ts create mode 100644 banking/src/lib/translate.ts create mode 100644 banking/src/lib/utils.ts create mode 100644 banking/src/main.tsx create mode 100644 banking/src/pages/BankReconciliation.tsx create mode 100644 banking/src/pages/BankStatementImporter.tsx create mode 100644 banking/src/pages/BankStatementImporterContainer.tsx create mode 100644 banking/src/pages/ViewBankStatementImportLog.tsx create mode 100644 banking/src/types/Accounts/AccountsSettings.ts create mode 100644 banking/src/types/Accounts/AdvanceTaxesandCharges.ts create mode 100644 banking/src/types/Accounts/BankAccount.ts create mode 100644 banking/src/types/Accounts/BankAccountBalance.ts create mode 100644 banking/src/types/Accounts/BankStatementImportLog.ts create mode 100644 banking/src/types/Accounts/BankStatementImportLogColumnMap.ts create mode 100644 banking/src/types/Accounts/BankTransaction.ts create mode 100644 banking/src/types/Accounts/BankTransactionPayments.ts create mode 100644 banking/src/types/Accounts/BankTransactionRule.ts create mode 100644 banking/src/types/Accounts/BankTransactionRuleAccounts.ts create mode 100644 banking/src/types/Accounts/BankTransactionRuleDescriptionConditions.ts create mode 100644 banking/src/types/Accounts/JournalEntry.ts create mode 100644 banking/src/types/Accounts/JournalEntryAccount.ts create mode 100644 banking/src/types/Accounts/PaymentEntry.ts create mode 100644 banking/src/types/Accounts/PaymentEntryDeduction.ts create mode 100644 banking/src/types/Accounts/PaymentEntryReference.ts create mode 100644 banking/src/types/custom/Reports.ts create mode 100644 banking/src/vite-env.d.ts create mode 100644 banking/tsconfig.app.json create mode 100644 banking/tsconfig.json create mode 100644 banking/tsconfig.node.json create mode 100644 banking/vite.config.ts create mode 100644 banking/yarn.lock create mode 100644 erpnext/accounts/doctype/bank_account_balance/__init__.py create mode 100644 erpnext/accounts/doctype/bank_account_balance/bank_account_balance.js create mode 100644 erpnext/accounts/doctype/bank_account_balance/bank_account_balance.json create mode 100644 erpnext/accounts/doctype/bank_account_balance/bank_account_balance.py create mode 100644 erpnext/accounts/doctype/bank_account_balance/test_bank_account_balance.py create mode 100644 erpnext/accounts/doctype/bank_statement_import_log/__init__.py create mode 100644 erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.js create mode 100644 erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.json create mode 100644 erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py create mode 100644 erpnext/accounts/doctype/bank_statement_import_log/test_bank_statement_import_log.py create mode 100644 erpnext/accounts/doctype/bank_statement_import_log_column_map/__init__.py create mode 100644 erpnext/accounts/doctype/bank_statement_import_log_column_map/bank_statement_import_log_column_map.json create mode 100644 erpnext/accounts/doctype/bank_statement_import_log_column_map/bank_statement_import_log_column_map.py create mode 100644 erpnext/accounts/doctype/bank_transaction_rule/__init__.py create mode 100644 erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.js create mode 100644 erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.json create mode 100644 erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py create mode 100644 erpnext/accounts/doctype/bank_transaction_rule/test_bank_transaction_rule.py create mode 100644 erpnext/accounts/doctype/bank_transaction_rule_accounts/__init__.py create mode 100644 erpnext/accounts/doctype/bank_transaction_rule_accounts/bank_transaction_rule_accounts.json create mode 100644 erpnext/accounts/doctype/bank_transaction_rule_accounts/bank_transaction_rule_accounts.py create mode 100644 erpnext/accounts/doctype/bank_transaction_rule_description_conditions/__init__.py create mode 100644 erpnext/accounts/doctype/bank_transaction_rule_description_conditions/bank_transaction_rule_description_conditions.json create mode 100644 erpnext/accounts/doctype/bank_transaction_rule_description_conditions/bank_transaction_rule_description_conditions.py create mode 100644 erpnext/public/images/bank-logos/ABSA.png create mode 100644 erpnext/public/images/bank-logos/ANZ.png create mode 100644 erpnext/public/images/bank-logos/Airwallex-dark.png create mode 100644 erpnext/public/images/bank-logos/Airwallex.png create mode 100644 erpnext/public/images/bank-logos/Alpha_Bank.svg create mode 100644 erpnext/public/images/bank-logos/Amex.svg create mode 100644 erpnext/public/images/bank-logos/Australian_Tax_Office.png create mode 100644 erpnext/public/images/bank-logos/Avanz-dark.svg create mode 100644 erpnext/public/images/bank-logos/Avanz.svg create mode 100644 erpnext/public/images/bank-logos/Axis_Bank.svg create mode 100644 erpnext/public/images/bank-logos/BAC_Credomatic.svg create mode 100644 erpnext/public/images/bank-logos/BNP_Paribas-Dark.svg create mode 100644 erpnext/public/images/bank-logos/BNP_Paribas.svg create mode 100644 erpnext/public/images/bank-logos/BNY_Mellon-Dark.svg create mode 100644 erpnext/public/images/bank-logos/BNY_Mellon.svg create mode 100644 erpnext/public/images/bank-logos/Banco_Atlantida.png create mode 100644 erpnext/public/images/bank-logos/Banco_Lafise.png create mode 100644 erpnext/public/images/bank-logos/Banco_de_Finanzas.svg create mode 100644 erpnext/public/images/bank-logos/Bank_of_America.png create mode 100644 erpnext/public/images/bank-logos/Bank_of_Baroda.svg create mode 100644 erpnext/public/images/bank-logos/Bank_of_India.png create mode 100644 erpnext/public/images/bank-logos/Bank_of_Maharashtra.png create mode 100644 erpnext/public/images/bank-logos/Barclays.svg create mode 100644 erpnext/public/images/bank-logos/Capital_One.png create mode 100644 erpnext/public/images/bank-logos/Charles_Schwab.svg create mode 100644 erpnext/public/images/bank-logos/Citi.svg create mode 100644 erpnext/public/images/bank-logos/Commonwealth_Bank.svg create mode 100644 erpnext/public/images/bank-logos/Deutsche_Bank.svg create mode 100644 erpnext/public/images/bank-logos/Diamond_Trust_Bank.png create mode 100644 erpnext/public/images/bank-logos/Equity_Bank-dark.png create mode 100644 erpnext/public/images/bank-logos/Equity_Bank.png create mode 100644 erpnext/public/images/bank-logos/Federal_Bank-Dark.png create mode 100644 erpnext/public/images/bank-logos/Federal_Bank.png create mode 100644 erpnext/public/images/bank-logos/Fi_Bank.svg create mode 100644 erpnext/public/images/bank-logos/Ficohsa.svg create mode 100644 erpnext/public/images/bank-logos/Goldman_Sachs.svg create mode 100644 erpnext/public/images/bank-logos/HDFC.svg create mode 100644 erpnext/public/images/bank-logos/HSBC-dark.svg create mode 100644 erpnext/public/images/bank-logos/HSBC.svg create mode 100644 erpnext/public/images/bank-logos/I&M.png create mode 100644 erpnext/public/images/bank-logos/ICICI-dark.svg create mode 100644 erpnext/public/images/bank-logos/ICICI.svg create mode 100644 erpnext/public/images/bank-logos/IDBI_Bank.svg create mode 100644 erpnext/public/images/bank-logos/IDFC_First_Bank.svg create mode 100644 erpnext/public/images/bank-logos/IndusInd_Bank.svg create mode 100644 erpnext/public/images/bank-logos/Judo_Bank-dark.svg create mode 100644 erpnext/public/images/bank-logos/Judo_Bank.svg create mode 100644 erpnext/public/images/bank-logos/KCB_Bank_Kenya.png create mode 100644 erpnext/public/images/bank-logos/Kotak_Mahindra.svg create mode 100644 erpnext/public/images/bank-logos/Macquarie.svg create mode 100644 erpnext/public/images/bank-logos/Morgan_Stanley.png create mode 100644 erpnext/public/images/bank-logos/Oakstar-dark.webp create mode 100644 erpnext/public/images/bank-logos/Oakstar.png create mode 100644 erpnext/public/images/bank-logos/PNC.png create mode 100644 erpnext/public/images/bank-logos/PlainsCapitalBank.png create mode 100644 erpnext/public/images/bank-logos/Prime_Bank.png create mode 100644 erpnext/public/images/bank-logos/Punjab_National_Bank.svg create mode 100644 erpnext/public/images/bank-logos/RBL_Bank-dark.svg create mode 100644 erpnext/public/images/bank-logos/RBL_Bank.svg create mode 100644 erpnext/public/images/bank-logos/Razorpay-dark.svg create mode 100644 erpnext/public/images/bank-logos/Razorpay.svg create mode 100644 erpnext/public/images/bank-logos/Revolut.png create mode 100644 erpnext/public/images/bank-logos/Santander.svg create mode 100644 erpnext/public/images/bank-logos/Sparkasse.png create mode 100644 erpnext/public/images/bank-logos/Stanbic.png create mode 100644 erpnext/public/images/bank-logos/Standard_Chartered-dark.png create mode 100644 erpnext/public/images/bank-logos/Standard_Chartered.png create mode 100644 erpnext/public/images/bank-logos/Starling_Bank-dark.png create mode 100644 erpnext/public/images/bank-logos/Starling_Bank.png create mode 100644 erpnext/public/images/bank-logos/State_Bank_of_India.svg create mode 100644 erpnext/public/images/bank-logos/State_bank_of_India-Dark.svg create mode 100644 erpnext/public/images/bank-logos/Toronto_Dominion_Bank.png create mode 100644 erpnext/public/images/bank-logos/Truist.svg create mode 100644 erpnext/public/images/bank-logos/UBS-dark.svg create mode 100644 erpnext/public/images/bank-logos/UBS.svg create mode 100644 erpnext/public/images/bank-logos/USBank-dark.svg create mode 100644 erpnext/public/images/bank-logos/USBank.svg create mode 100644 erpnext/public/images/bank-logos/Union_Bank_of_India.svg create mode 100644 erpnext/public/images/bank-logos/Volksbanken_Raiffeisenbanken.svg create mode 100644 erpnext/public/images/bank-logos/Wells_Fargo.svg create mode 100644 erpnext/public/images/bank-logos/Westpac.svg create mode 100644 erpnext/public/images/bank-logos/Yes_Bank-dark.svg create mode 100644 erpnext/public/images/bank-logos/Yes_Bank.svg create mode 100644 erpnext/public/images/bank-logos/chase-Dark.svg create mode 100644 erpnext/public/images/bank-logos/chase.svg create mode 100644 erpnext/public/images/bank-logos/jpmc.svg create mode 100644 erpnext/www/banking.py diff --git a/.gitignore b/.gitignore index f9f70d0c643..217091c99b7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ node_modules/ .backportrc.json # Aider AI Chat .aider* + +# Banking SPA +erpnext/public/banking +erpnext/www/banking.html \ No newline at end of file diff --git a/babel_extractors.csv b/babel_extractors.csv index 4c9f885d911..98e6a32c14e 100644 --- a/babel_extractors.csv +++ b/babel_extractors.csv @@ -1,3 +1,5 @@ **/setup/setup_wizard/data/uom_data.json,erpnext.gettext.extractors.uom_data.extract **/setup/doctype/incoterm/incoterms.csv,erpnext.gettext.extractors.incoterms.extract **/setup/setup_wizard/data/*.txt,erpnext.gettext.extractors.lines_from_txt_file.extract +**.tsx,frappe.gettext.extractors.html_template.extract +**.ts,frappe.gettext.extractors.html_template.extract diff --git a/banking/.env.production b/banking/.env.production new file mode 100644 index 00000000000..e6f44cb7ce0 --- /dev/null +++ b/banking/.env.production @@ -0,0 +1 @@ +VITE_BASE_NAME="banking" \ No newline at end of file diff --git a/banking/.gitignore b/banking/.gitignore new file mode 100644 index 00000000000..a547bf36d8d --- /dev/null +++ b/banking/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/banking/README.md b/banking/README.md new file mode 100644 index 00000000000..d2e77611fd3 --- /dev/null +++ b/banking/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/banking/eslint.config.js b/banking/eslint.config.js new file mode 100644 index 00000000000..9cc2a204656 --- /dev/null +++ b/banking/eslint.config.js @@ -0,0 +1,24 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; + +export default defineConfig([ + globalIgnores(["dist"]), + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + onlyExportComponents: false, + }, +]); diff --git a/banking/index.html b/banking/index.html new file mode 100644 index 00000000000..0f30097886a --- /dev/null +++ b/banking/index.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + Banking | {{ app_name }} + + + +
      + + + + + \ No newline at end of file diff --git a/banking/package.json b/banking/package.json new file mode 100644 index 00000000000..984c364bb80 --- /dev/null +++ b/banking/package.json @@ -0,0 +1,66 @@ +{ + "name": "banking", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build --base=/assets/erpnext/banking/ && yarn copy-html-entry", + "lint": "eslint .", + "preview": "vite preview", + "copy-html-entry": "cp ../erpnext/public/banking/index.html ../erpnext/www/banking.html" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.24", + "@vitejs/plugin-react": "^6.0.1", + "chrono-node": "^2.9.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "dayjs": "^1.11.20", + "frappe-react-sdk": "^1.14.0", + "fuse.js": "^7.3.0", + "jotai": "^2.20.0", + "jotai-family": "^1.0.1", + "lodash.isplainobject": "^4.0.6", + "lucide-react": "^1.14.0", + "radix-ui": "^1.4.3", + "react": "^19.2.6", + "react-currency-input-field": "^4.0.5", + "react-day-picker": "9.14.0", + "react-dom": "^19.2.6", + "react-dropzone": "^15.0.0", + "react-hook-form": "^7.75.0", + "react-hotkeys-hook": "^5.3.2", + "react-markdown": "^10.1.0", + "react-router": "^7.15.0", + "react-router-dom": "^7.15.0", + "react-virtuoso": "^4.18.6", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.3.0", + "tw-animate-css": "^1.4.0", + "usehooks-ts": "^3.1.1", + "vite": "^8.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^25.3.0", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0" + } +} diff --git a/banking/proxyOptions.ts b/banking/proxyOptions.ts new file mode 100644 index 00000000000..e1aeca81094 --- /dev/null +++ b/banking/proxyOptions.ts @@ -0,0 +1,13 @@ +const common_site_config = require('../../../sites/common_site_config.json'); +const { webserver_port } = common_site_config; + +export default { + '^/(app|api|assets|files|private)': { + target: `http://127.0.0.1:${webserver_port}`, + ws: true, + router: function(req) { + const site_name = req.headers.host.split(':')[0]; + return `http://${site_name}:${webserver_port}`; + } + } +}; diff --git a/banking/src/App.tsx b/banking/src/App.tsx new file mode 100644 index 00000000000..2e6f8339ce9 --- /dev/null +++ b/banking/src/App.tsx @@ -0,0 +1,65 @@ +import { useEffect } from 'react' +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' +import { FrappeProvider } from 'frappe-react-sdk' +import { Toaster } from '@/components/ui/sonner' +import BankReconciliation from '@/pages/BankReconciliation' +import { TooltipProvider } from './components/ui/tooltip' +import BankStatementImporter from '@/pages/BankStatementImporter' +import { LucideProvider } from 'lucide-react' +import { ThemeProvider } from './components/ui/theme-provider' +import ViewBankStatementImportLog from './pages/ViewBankStatementImportLog' +import BankStatementImporterContainer from './pages/BankStatementImporterContainer' + +function App() { + useEffect(() => { + // Check if user is logged in by checking the Cookie "user_id" + // In Frappe, unauthenticated users are "Guest" + const userId = document.cookie?.split('; ').find(row => row.startsWith('user_id='))?.split('=')[1]?.trim() + const isLoggedIn = userId !== 'Guest' + + if (!isLoggedIn) { + if (import.meta.env.DEV) { + return + } + // Redirect to Frappe login page + window.location.href = '/login?redirect-to=/banking' + return + } + }, []) + + return ( + + + + + {window.frappe?.boot?.user?.name && window.frappe?.boot?.user?.name !== 'Guest' && + + + + } /> + }> + } /> + } /> + + } /> + + + } + + + + + + ) +} + +export default App diff --git a/banking/src/components/common/AccountsDropdown.tsx b/banking/src/components/common/AccountsDropdown.tsx new file mode 100644 index 00000000000..a98ace578c3 --- /dev/null +++ b/banking/src/components/common/AccountsDropdown.tsx @@ -0,0 +1,228 @@ +import { Button } from "@/components/ui/button" +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { useCurrentCompany } from "@/hooks/useCurrentCompany" +import _ from "@/lib/translate" +import { cn } from "@/lib/utils" +import { useFrappeGetDocList } from "frappe-react-sdk" +import Fuse from "fuse.js" +import { ChevronDownIcon } from "lucide-react" +import { useLayoutEffect, useMemo, useRef, useState } from "react" +import { FormControl } from "../ui/form" + + +export interface AccountsDropdownProps { + root_type?: ('Asset' | 'Liability' | 'Equity' | 'Income' | 'Expense')[], + report_type?: 'Balance Sheet' | 'Profit and Loss', + account_type?: string[], + value?: string, + onChange?: (value: string) => void, + readOnly?: boolean, + disabled?: boolean, + company?: string, + filterFunction?: (account: Account) => boolean, + // If true, the component will be wrapped in a FormControl component + useInForm?: boolean, + buttonClassName?: string, + size?: 'sm' | 'md' | 'lg', +} +/** + * Component to select an account - supports fuzzy search + * @param root_type - The root type of the account + * @param report_type - The report type of the account + * @param account_type - The type of the account + * @param value - The value of the account field + * @param onChange - The function to call when the value changes + * @returns + */ +const AccountsDropdown = ({ root_type, report_type, account_type, value, onChange, readOnly, disabled, company, filterFunction, useInForm, buttonClassName, size = 'md' }: AccountsDropdownProps) => { + + const { data } = useGetAccounts(root_type, report_type, account_type, company, filterFunction) + + const groupedAccounts = useMemo(() => { + if (!data) return [] + + const grouped: Record = data.reduce((acc, account) => { + const parentAccount = account.parent_account + if (!parentAccount) return acc + + if (!acc[parentAccount]) { + acc[parentAccount] = [] + } + + acc[parentAccount].push(account) + return acc + }, {} as Record) + + + return Object.entries(grouped).map(([parentAccount, accounts]) => ({ + // Remove the last abbreviation from the parent account name like "Assets - TCC" should be "Assets", and "Assets - USD - TCC" should be "Assets - USD" + parentAccount: parentAccount.split(" - ").slice(0, -1).join(" - "), + accounts + })) + + }, [data]) + + const searchIndex = useMemo(() => { + + if (!data) { + return null + } + + return new Fuse(data, { + keys: ['name'], + threshold: 0.5, + includeScore: true + }) + }, [data]) + + const [search, setSearch] = useState("") + + const recommendedAccounts = useMemo(() => { + + if (!searchIndex || !search) { + return [] + } + + return searchIndex.search(search).map((result) => result.item) + + }, [searchIndex, search]) + + const [open, setOpen] = useState(false) + + const onOpenChange = (open: boolean) => { + if (readOnly) return + setOpen(open) + // setSearch("") + } + + const onSelect = (value: string) => { + onChange?.(value) + setOpen(false) + setSearch(value) + } + + const buttonRef = useRef(null) + + const [width, setWidth] = useState(320) + + useLayoutEffect(() => { + if (buttonRef.current) { + setWidth(buttonRef.current.getBoundingClientRect().width) + } + }, []) + + return ( + + + {useInForm ? + + + : } + + + + + + {_("No accounts found.")} + + {recommendedAccounts.length > 0 && ( + + {recommendedAccounts.map((account) => ( + onSelect(account.name)}>{account.name} + ))} + + )} + + {!search && groupedAccounts.map((group) => ( + + {group.accounts.map((account) => ( + onSelect(account.name)}>{account.name} + ))} + + ))} + + + + + + + ) +} + + +interface Account { + name: string + root_type: 'Asset' | 'Liability' | 'Equity' | 'Income' | 'Expense' + report_type: 'Balance Sheet' | 'Profit and Loss' + account_type: string + account_currency: string + parent_account: string +} + +export const useGetAccounts = (root_type?: ('Asset' | 'Liability' | 'Equity' | 'Income' | 'Expense')[], report_type?: 'Balance Sheet' | 'Profit and Loss', account_type?: string[], company?: string, + filterFunction?: (account: Account) => boolean) => { + + const currentCompany = useCurrentCompany() + const { data, isLoading, error, mutate } = useFrappeGetDocList("Account", { + fields: ["name", "root_type", "report_type", "account_type", "account_currency", "parent_account"], + filters: [["is_group", "=", 0], ["disabled", "=", 0], ["company", "=", company ?? currentCompany]], + limit: 1000, + orderBy: { + "field": "root_type", + // @ts-expect-error - we can pass in additional fields to orderBy + "order": "asc, account_number asc" + } + }, `accounts-${company ?? currentCompany}`, { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + }) + + const filteredData = useMemo(() => { + + return data?.filter((account) => { + if (root_type && !root_type.includes(account.root_type)) return false + if (report_type && account.report_type !== report_type) return false + if (account_type && !account_type.includes(account.account_type)) return false + + if (filterFunction) return filterFunction(account) + return true + }) ?? [] + + }, [data, root_type, report_type, account_type, filterFunction]) + + return { data: filteredData, isLoading, error, mutate } +} + +export default AccountsDropdown \ No newline at end of file diff --git a/banking/src/components/common/BankLogo.tsx b/banking/src/components/common/BankLogo.tsx new file mode 100644 index 00000000000..9dd650dee4e --- /dev/null +++ b/banking/src/components/common/BankLogo.tsx @@ -0,0 +1,26 @@ +import { cn } from '@/lib/utils' +import { SelectedBank } from '../features/BankReconciliation/bankRecAtoms' +import { useTheme } from '../ui/theme-provider' +import { Landmark } from 'lucide-react' +import { H4 } from '../ui/typography' + +const BankLogo = ({ bank, className, imageClassName, iconSize = '18px', iconClassName }: { bank?: SelectedBank | null, className?: string, imageClassName?: string, iconSize?: string, iconClassName?: string }) => { + + const { themeValue } = useTheme() + return ( +
      {bank?.logo ? {bank.bank : <> + +

      {bank?.bank ?? ''}

      + + }
      + ) +} + +export default BankLogo \ No newline at end of file diff --git a/banking/src/components/common/FileUploadBanner.tsx b/banking/src/components/common/FileUploadBanner.tsx new file mode 100644 index 00000000000..dfa0ec7f8f3 --- /dev/null +++ b/banking/src/components/common/FileUploadBanner.tsx @@ -0,0 +1,17 @@ +import { CheckCircle } from 'lucide-react' +import { Progress } from '../ui/progress' +import _ from '@/lib/translate' + +const FileUploadBanner = ({ + uploadProgress, +}: { uploadProgress: number }) => { + return
      +
      + + {_("The document has been created and reconciled. Uploading attachments...")} + +
      +
      +} + +export default FileUploadBanner \ No newline at end of file diff --git a/banking/src/components/common/LinkFieldCombobox.tsx b/banking/src/components/common/LinkFieldCombobox.tsx new file mode 100644 index 00000000000..a41105b05d7 --- /dev/null +++ b/banking/src/components/common/LinkFieldCombobox.tsx @@ -0,0 +1,301 @@ +import { useDocType } from "@/hooks/useDocType"; +import { getSystemDefault, slug } from "@/lib/frappe"; +import { Filter, useFrappeGetCall } from "frappe-react-sdk" +import { useLayoutEffect, useMemo, useRef, useState } from "react"; +import { canCreateDocument } from "@/lib/permissions"; +import { useDebounceValue } from "usehooks-ts"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { FormControl } from "../ui/form"; +import { ChevronDownIcon, ExternalLink } from "lucide-react"; +import { Button } from "../ui/button"; +import { cn } from "@/lib/utils"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../ui/command"; +import _ from "@/lib/translate"; +import ErrorBanner from "../ui/error-banner"; +import MarkdownRenderer from "../ui/markdown"; + +export interface ResultItem { + value: string, + description: string, + label?: string +} + +export interface LinkFieldComboboxProps { + /** DocType to be fetched */ + doctype: string; + /** Filters to be applied. Default: none */ + filters?: Filter[] + /** Number of records to paginate with. Default: Comes from System Settings or 10 */ + limit?: number; + /** + * API to call to fetch records. + * + * Default: `frappe.desk.search.search_link` + * + * If you want to use a custom API, you can pass the path to the API here. + * + * The API should return a list of documents in the following format: + * [{value: string, description: string, label?: string}] - where the value is the ID of the document. + * + * If the API sends a label, it will be used as the label in the dropdown. + */ + searchAPIPath?: string; + /** + * Field you want to search against in the doctype. + * + * Default: `name` + * + * If you want to search against a different field, you can pass the fieldname here. + * + * If you want to search against multiple fields, you can try using the `searchAPIPath` prop to call a custom API, + * or use a custom query in the `customQuery` prop. + */ + searchfield?: string; + /** + * Custom query to be used to fetch records. + * + * If you want to use a custom query, you can pass the query here. + * + * The query should be in the following format: + * { + * query: string, + * filters: { + * fieldname: string, + * operator: string, + * value: string + * } + * } + */ + customQuery?: { + /** Path to function for the query. + * + * Refer: Item/Supplier query + */ + query: string, + /** Filters are usually an object instead of an array in a custom query */ + filters?: Record, + }, + /** + * Used for certain queries where a reference doctype is needed. + * + * For example when searching a supplier in a "Purchase Invoice", the reference_doctype is "Purchase Invoice" + */ + reference_doctype?: string, + /** Placeholder for the dropdown. Default: `doctype` */ + placeholder?: string; + /** + * Should the field be read-only. + */ + readOnly?: boolean; + /** Should the field be disabled. Default: false */ + disabled?: boolean; + /** + * Function to filter the options based on the input value/other criteria. + * + * For example, you might want to limit the companies shown in the dropdown since they have been already added (like in Cost Codes) + */ + filterFn?: (option: ResultItem, inputValue: string) => boolean, + value?: string, + onChange: (value: string) => void, + /** If true, the component will be wrapped in a FormControl component */ + useInForm?: boolean, + /** Button Class name */ + buttonClassName?: string, + size?: 'sm' | 'md' | 'lg', +} +const LinkFieldCombobox = ({ + doctype, + reference_doctype, + filters = [], + value, + onChange, + readOnly, + disabled, + filterFn, + placeholder = `Select ${doctype}`, + customQuery, + searchfield, + searchAPIPath = "frappe.desk.search.search_link", + limit, + useInForm, + buttonClassName, + size = 'md' +}: LinkFieldComboboxProps) => { + + const pageLimit = useMemo(() => limit || getSystemDefault('link_field_results_limit') || 20, [limit]) + + /** Load the Doctype meta so that we can determine the search fields + the name of the title field */ + const { data: meta } = useDocType(doctype) + + const userCanCreate = useMemo(() => canCreateDocument(doctype), [doctype]) + + const [open, setOpen] = useState(false) + + const [searchInput, setSearchInput] = useDebounceValue('', 400) + + const { data: linkTitleData } = useFrappeGetCall('frappe.client.get_value', { + doctype, + filters: JSON.stringify({ + name: value + }), + fieldname: meta?.title_field + }, (meta?.show_title_field_in_link ?? false) && (meta?.title_field) && value ? `link_title::${doctype}::${value}` : null, { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + }) + + const linkTitle = meta?.title_field && meta?.show_title_field_in_link ? (linkTitleData?.message?.[meta?.title_field] ?? value) : value + + const buttonRef = useRef(null) + + const [width, setWidth] = useState(320) + + useLayoutEffect(() => { + if (buttonRef.current) { + setWidth(buttonRef.current.getBoundingClientRect().width) + } + }, []) + + const { data, error, isLoading } = useFrappeGetCall<{ message: ResultItem[] }>(searchAPIPath, { + doctype, + txt: searchInput, + page_length: pageLimit, + query: customQuery?.query, + searchfield, + filters: JSON.stringify(customQuery?.filters || filters || []), + reference_doctype, + }, () => { + if (!open) { + return null + } else { + let key = `${searchAPIPath}_${doctype}_${searchInput}` + + if (pageLimit) { + key += `_${pageLimit}` + } + + if (customQuery?.filters) { + key += `_${JSON.stringify(customQuery.filters)}` + } else if (filters) { + key += `_${JSON.stringify(filters)}` + } + + if (customQuery && customQuery.query) { + key += `_${customQuery.query}` + } + + if (reference_doctype) { + key += `_${reference_doctype}` + } + + if (searchfield && searchfield !== 'name') { + key += `_${searchfield}` + } + + return key + + } + }, { + revalidateOnFocus: false, + revalidateIfStale: false, + shouldRetryOnError: false, + revalidateOnReconnect: false, + }) + + const onOpenChange = (open: boolean) => { + if (readOnly) return + setOpen(open) + setSearchInput("") + } + + const onSelect = (value: string) => { + onChange?.(value) + setOpen(false) + } + + const items = filterFn ? data?.message?.slice(0, 50).filter((item) => filterFn(item, searchInput)) : data?.message + + const buttonProps = { + variant: "subtle", + type: 'button', + size: size, + role: "combobox", + "data-state": open ? "open" : "closed", + ref: buttonRef, + tabIndex: 0, + disabled: disabled || readOnly, + "aria-expanded": open, + "aria-readonly": readOnly, + className: cn("w-full justify-between font-normal group border border-transparent outline-none", + "data-[state=open]:bg-surface-white data-[state=open]:border-outline-gray-4 data-[state=open]:shadow-sm", + readOnly ? "bg-surface-gray-1" : "", + // Placeholder and value styling + linkTitle ? "text-ink-gray-7" : "text-ink-gray-4", + buttonClassName) + } as const + + return ( + + + {useInForm ? + + + : } + + + {error && } + + + + {isLoading ? _("Loading...") : _("No results found.")} + + {items?.map((result) => ( + onSelect(result.value)} className="flex flex-col items-start gap-0.5"> + + {result.label || result.value} + + {result.description && + + } + + ))} + {userCanCreate && + + {_("Create New {0}", [doctype])} + + + + + } + + + + + + + + + ) +} + +export default LinkFieldCombobox \ No newline at end of file diff --git a/banking/src/components/common/PartyTypeDropdown.tsx b/banking/src/components/common/PartyTypeDropdown.tsx new file mode 100644 index 00000000000..7bc6addf9de --- /dev/null +++ b/banking/src/components/common/PartyTypeDropdown.tsx @@ -0,0 +1,82 @@ +import { Select, SelectValue, SelectTrigger, SelectContent, SelectItem } from '@/components/ui/select' +import _ from '@/lib/translate' +import { useFrappeGetDocList } from 'frappe-react-sdk' +import { ComponentProps, useMemo } from 'react' +import { FormControl } from '../ui/form' + +export type PartyTypeDropdownProps = { + value?: string, + onChange?: (value: string) => void, + readOnly?: boolean, + disabled?: boolean, + /** Set this to order the parties so that suggested types are shown first */ + type?: 'Receivable' | 'Payable' + /** Set this to true if you want to hide other options by type. e.g. - if type is Receivable, Payable options like "Supplier" will be hidden */ + hideOptionsByType?: boolean, + valueProps?: ComponentProps, + triggerProps?: ComponentProps, + // If true, the component will be wrapped in a FormControl component + useInForm?: boolean +} + +const PartyTypeDropdown = ({ value, onChange, readOnly, disabled, type, hideOptionsByType, valueProps, triggerProps, useInForm }: PartyTypeDropdownProps) => { + + const { data } = useFrappeGetDocList("Party Type", { + fields: ['name', 'account_type'], + orderBy: { + field: 'creation', + order: 'asc' + } + }, `party_types`, { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + }) + + const filteredData = useMemo(() => { + + let options = data ?? [ + { name: "Customer", account_type: "Receivable" }, + { name: "Supplier", account_type: "Payable" }, + { name: "Employee", account_type: "Payable" }, + { name: "Shareholder", account_type: "Payable" }, + ] + + if (hideOptionsByType && type) { + options = options.filter((option) => option.account_type === type) + } + + // Order by type if type is set + if (type) { + options = options.sort((a) => a.account_type === type ? -1 : 1) + } + + return options + }, [data, type, hideOptionsByType]) + + const onSelect = (value: string) => { + if (!readOnly) { + onChange?.(value) + } + } + + return ( + + ) +} + +export default PartyTypeDropdown \ No newline at end of file diff --git a/banking/src/components/features/ActionLog/ActionLog.tsx b/banking/src/components/features/ActionLog/ActionLog.tsx new file mode 100644 index 00000000000..e8ed9ae234a --- /dev/null +++ b/banking/src/components/features/ActionLog/ActionLog.tsx @@ -0,0 +1,475 @@ +import { Button } from '@/components/ui/button' +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import _ from '@/lib/translate' +import { useAtomValue, useSetAtom } from 'jotai' +import { ArrowDownRight, ArrowRightLeftIcon, ArrowUpRight, CalendarIcon, CircleXIcon, GitCompareIcon, HistoryIcon, LandmarkIcon, Loader2Icon, ReceiptIcon, ReceiptTextIcon, UserIcon, WalletIcon } from 'lucide-react' +import { useMemo, useState } from 'react' +import { ActionLogItem, ActionLog as ActionLogType, bankRecActionLog, bankRecDateAtom, bankRecMatchFilters, SelectedBank, selectedBankAccountAtom } from '../BankReconciliation/bankRecAtoms' +import { useHotkeys } from 'react-hotkeys-hook' +import { useGetBankAccounts } from '../BankReconciliation/utils' +import { getCompanyCurrency } from '@/lib/company' +import { formatCurrency } from '@/lib/numbers' +import dayjs from 'dayjs' +import { cn } from '@/lib/utils' +import { formatDate } from '@/lib/date' +import { Separator } from '@/components/ui/separator' +import { slug } from '@/lib/frappe' +import { PaymentEntry } from '@/types/Accounts/PaymentEntry' +import { JournalEntry } from '@/types/Accounts/JournalEntry' +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' +import { Table, TableCell, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog' +import { useFrappePostCall, useSWRConfig } from 'frappe-react-sdk' +import { toast } from 'sonner' +import { getErrorMessage } from '@/lib/frappe' +import ErrorBanner from '@/components/ui/error-banner' +import SelectedTransactionDetails from '../BankReconciliation/SelectedTransactionDetails' +import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty' +import BankLogo from '@/components/common/BankLogo' + +const ActionLog = () => { + + const [isOpen, setIsOpen] = useState(false) + + useHotkeys('meta+z', () => { + setIsOpen(true) + }, { + enabled: true, + enableOnFormTags: false, + preventDefault: true + }) + + return ( + + + + + + + + + {_("Reconciliation History")} + + + + + {_("Reconciliation History")} + {_("View all reconciliation actions taken in this session.")} + + + + + + + + + + ) +} + +const ActionLogDialogContent = () => { + + const actionLog = useAtomValue(bankRecActionLog) + + return
      + {actionLog.map((action) => ( +
      + +
      +
      +
      + {action.items.map((item, index) => ( + + ))} +
      +
      +
      +
      + ))} + + {actionLog.length === 0 && + + + + + {_("No reconciliation actions found")} + {_("You have not performed any reconciliations in this session yet.")} + + } +
      +} + + + +const ActionGroupHeader = ({ action }: { action: ActionLogType }) => { + + const label = useMemo(() => { + switch (action.type) { + case 'match': + return _("Matched") + case 'payment': + if (action.isBulk) { + return _("Bulk Payment") + } + return _("Payment") + + case 'transfer': + if (action.isBulk) { + return _("Bulk Transfer") + } + return _("Transfer") + + case 'bank_entry': + if (action.isBulk) { + return _("Bulk Bank Entry") + } + return _("Bank Entry") + + default: + return _("Action") + } + }, [action]) + + return
      + {action.type === 'match' && } + {action.type === 'payment' && } + {action.type === 'transfer' && } + {action.type === 'bank_entry' && } + + {label} - {dayjs(action.timestamp).fromNow()} + +
      +} + +const Row = ({ item, index, isLast, action }: { item: ActionLogItem, index: number, isLast: boolean, action: ActionLogType }) => { + + const isWithdrawal = item.bankTransaction.withdrawal && item.bankTransaction.withdrawal > 0 + + const { banks } = useGetBankAccounts() + + const bank = useMemo(() => { + if (item.bankTransaction.bank_account) { + return banks?.find((bank) => bank.name === item.bankTransaction.bank_account) + } + return null + }, [item.bankTransaction.bank_account, banks]) + + const amount = item.bankTransaction.withdrawal ? item.bankTransaction.withdrawal : item.bankTransaction.deposit + + const currency = item.bankTransaction.currency || getCompanyCurrency(item.bankTransaction.company ?? '') + + return
      +
      +
      +
      +

      {item.bankTransaction.description}

      +
      +
      + + {item.bankTransaction.bank_account} +
      + +
      + + {formatDate(item.bankTransaction.date, 'Do MMM YYYY')} +
      + +
      +
      + {isWithdrawal ? : } + {formatCurrency(amount, currency)} +
      +
      +
      +
      +
      +
      + + {["Payment Entry", "Journal Entry"].includes(item.voucher.reference_doctype) ? "" : _("{} :", [item.voucher.reference_doctype])} {item.voucher.reference_name} + + {item.voucher.reference_doctype === "Payment Entry" && item.voucher.doc && } + {item.voucher.reference_doctype === "Journal Entry" && } +
      +
      +
      +
      +
      + +
      +
      +} + +const JournalEntryDetails = ({ item, bank }: { item: ActionLogItem, bank?: SelectedBank | null }) => { + + return
      + + +
      +} + +const JournalEntryAccountsTable = ({ item, bank }: { item: ActionLogItem, bank?: SelectedBank | null }) => { + + const accounts = useMemo(() => { + + const allAccounts = (item.voucher.doc as JournalEntry).accounts + + return allAccounts.filter((acc) => bank ? acc.account !== bank.account : true) + + }, [item, bank]) + + return <> + {accounts.length === 1 ? {accounts[0].account} : + + + {_("Split across {} accounts", [accounts.length.toString()])} + + + + + + {_("Account")} + {_("Debit")} + {_("Credit")} + + + + {accounts.map((account) => ( + + {account.account} + {formatCurrency(account.debit ?? 0, account.account_currency ?? '')} + {formatCurrency(account.credit ?? 0, account.account_currency ?? '')} + + ))} + +
      +
      +
      + } +} + +const PaymentEntryDetails = ({ item, className }: { item: ActionLogItem, className?: string }) => { + if ((item.voucher.doc as PaymentEntry).payment_type === "Internal Transfer") { + return + } + + const invoices = (item.voucher.doc as PaymentEntry).references ?? [] + + const currency = item.bankTransaction.withdrawal && item.bankTransaction.withdrawal > 0 ? (item.voucher.doc as PaymentEntry)?.paid_to_account_currency : (item.voucher.doc as PaymentEntry)?.paid_from_account_currency + + return
      +
      + + {(item.voucher.doc as PaymentEntry).party_name} +
      + + + +
      + + {invoices.length === 0 ? _("No invoice linked") : invoices.length === 1 ? _("1 invoice") : _("{} invoices", [invoices.length.toString()])} +
      +
      + +
      + {invoices.map((invoice) => ( + + + + {_("Document")} + {_("Invoice No")} + {_("Due Date")} + {_("Grand Total")} + {_("Allocated")} + + + + + {invoice.reference_doctype}: {invoice.reference_name} + {invoice.bill_no ?? "-"} + {formatDate(invoice.due_date)} + {formatCurrency(invoice.total_amount, currency ?? '')} + {formatCurrency(invoice.allocated_amount, currency ?? '')} + + +
      + ))} +
      +
      +
      + +
      +} + +const TransferDetails = ({ item, className }: { item: ActionLogItem, className?: string }) => { + + const { banks } = useGetBankAccounts() + + const bank = useMemo(() => { + + const isWithdrawal = item.bankTransaction.withdrawal && item.bankTransaction.withdrawal > 0 + + let transferAccount = "" + + if (isWithdrawal) { + transferAccount = (item.voucher.doc as PaymentEntry).paid_to + } else { + transferAccount = (item.voucher.doc as PaymentEntry).paid_from + } + + const transferBankAccount = banks?.find((bank) => bank.account === transferAccount) + + return transferBankAccount + + }, [banks, item]) + + return
      + + {bank?.account} +
      +} + +const ACTION_TYPE_MAP = { + 'bank_entry': _("Bank Entry"), + 'payment': _("Payment"), + 'transfer': _("Transfer"), + 'match': _("Match"), +} + +const CancelActionLogItem = ({ item, type, timestamp, bank }: { item: ActionLogItem, type: ActionLogType['type'], timestamp: number, bank?: SelectedBank | null }) => { + + const [isOpen, setIsOpen] = useState(false) + + const { call, loading, error } = useFrappePostCall('erpnext.accounts.doctype.bank_transaction.bank_transaction.unreconcile_transaction_entry') + const { mutate } = useSWRConfig() + const actionLog = useSetAtom(bankRecActionLog) + const dates = useAtomValue(bankRecDateAtom) + const matchFilters = useAtomValue(bankRecMatchFilters) + const selectedBank = useAtomValue(selectedBankAccountAtom) + + const onUndo = () => { + call({ + bank_transaction_id: item.bankTransaction.name, + voucher_type: item.voucher.reference_doctype, + voucher_id: item.voucher.reference_name, + }).then(() => { + toast.success(type === 'match' ? _("Unmatched") : _("Cancelled")) + + if (selectedBank?.name === item.bankTransaction.bank_account) { + mutate(`bank-reconciliation-unreconciled-transactions-${selectedBank?.name}-${dates.fromDate}-${dates.toDate}`) + mutate(`bank-reconciliation-account-closing-balance-${selectedBank?.name}-${dates.toDate}`) + // Update the matching vouchers for the selected transaction + mutate(`bank-reconciliation-vouchers-${item.bankTransaction.name}-${dates.fromDate}-${dates.toDate}-${matchFilters.join(',')}`) + } + + setTimeout(() => { + actionLog((prev) => { + // Find the action and then remove the item from the action. If the action is empty, remove the action from the array + const action = prev.find((action) => action.timestamp === timestamp) + + if (action) { + action.items = action.items.filter((i) => i.bankTransaction.name !== item.bankTransaction.name) + } + // If the action is empty, remove the action from the array + if (action && action.items.length === 0) { + return prev.filter((a) => a.timestamp !== timestamp) + } else { + return prev.map((a) => a.timestamp === timestamp ? { ...a, items: action?.items ?? [] } : a) + } + }) + }, 100) + + setIsOpen(false) + + }).catch((error) => { + toast.error(_("There was an error while performing the action."), { + duration: 5000, + description: getErrorMessage(error), + }) + }) + } + + return + + + + + + + + {_("Cancel")} + + + + + {type === 'match' ? _("Unmatch Transaction?") : _("Undo {}?", [item.voucher.reference_doctype])} + {type === 'match' ? _("Are you sure you want to unmatch the voucher from this transaction?") : _("Are you sure you want to cancel this {} {}?", [_(item.voucher.reference_doctype), item.voucher.reference_name])} + + {error && } +
      + + + + {_("Action Type")} + {ACTION_TYPE_MAP[type]} + + + {_("Voucher Type")} + {_(item.voucher.reference_doctype)} + + + {_("Voucher Name")} + {item.voucher.reference_name} + + + {_("Posting Date")} + {formatDate(item.voucher.posting_date, 'Do MMM YYYY')} + + {type === 'transfer' && item.voucher.doc && + {_("Transfer Account")} + + + + } + {type === 'payment' && item.voucher.doc && + {_("Payment Details")} + + + + } + {type === 'bank_entry' && item.voucher.doc && + {_("Account")} + + } +
      +
      + + + {_("Close")} + + + +
      +
      +} + +export default ActionLog \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/BankBalance.tsx b/banking/src/components/features/BankReconciliation/BankBalance.tsx new file mode 100644 index 00000000000..7a7b0a4925c --- /dev/null +++ b/banking/src/components/features/BankReconciliation/BankBalance.tsx @@ -0,0 +1,334 @@ +import { useAtomValue, useSetAtom } from "jotai" +import { bankRecClosingBalanceAtom, bankRecDateAtom, SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms" +import { FrappeConfig, FrappeContext, useFrappeGetDocCount, useFrappeGetDocList, useFrappePostCall, useSWRConfig } from "frappe-react-sdk" +import { BankTransaction } from "@/types/Accounts/BankTransaction" +import { Progress } from "@/components/ui/progress" +import { useGetAccountClosingBalance, useGetAccountClosingBalanceAsPerStatement, useGetAccountOpeningBalance, useGetUnreconciledTransactions } from "./utils" +import { flt, formatCurrency } from "@/lib/numbers" +import { Skeleton } from "@/components/ui/skeleton" +import { StatContainer, StatLabel, StatValue } from "@/components/ui/stats" +import { Edit, Info, Trash2 } from "lucide-react" +import { H4, Paragraph } from "@/components/ui/typography" +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card" +import { getCompanyCurrency } from "@/lib/company" +import _ from "@/lib/translate" +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { formatDate } from "@/lib/date" +import { Form } from "@/components/ui/form" +import { CurrencyFormField } from "@/components/ui/form-elements" +import { useForm } from "react-hook-form" +import { Button } from "@/components/ui/button" +import { useContext, useState } from "react" +import { Separator } from "@/components/ui/separator" +import { BankAccountBalance } from "@/types/Accounts/BankAccountBalance" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { toast } from "sonner" +import ErrorBanner from "@/components/ui/error-banner" + +const BankBalance = () => { + + const bankAccount = useAtomValue(selectedBankAccountAtom) + + if (!bankAccount) { + return null + } + return ( +
      +
      + + + + +
      + + +
      + ) +} + +const OpeningBalance = () => { + const bankAccount = useAtomValue(selectedBankAccountAtom) + const { data, isLoading } = useGetAccountOpeningBalance() + + return + {_("Opening Balance")} + {isLoading ? : {formatCurrency(flt(data?.message, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}} + +} + +const ClosingBalance = () => { + const bankAccount = useAtomValue(selectedBankAccountAtom) + const { data, isLoading } = useGetAccountClosingBalance() + + return ( + +
      + + {_("Closing Balance as per system")} + + + + + + +

      {_("Closing balance as per system")}

      + + {_("This is what the system expects the closing balance to be in your bank statement.")} +
      + {_("It takes into account all the transactions that have been posted and subtracts the transactions that have not cleared yet.")} +
      + {_("If your bank statement shows a different closing balance, it is because all transactions have not reconciled yet.")} +

      + For more information, click on the Bank Reconciliation Statement tab below. +
      +
      +
      + +
      + {isLoading ? : {formatCurrency(flt(data?.message, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}} +
      + ) +} + +const Difference = () => { + const bankAccount = useAtomValue(selectedBankAccountAtom) + + const { data, isLoading } = useGetAccountClosingBalance() + + const value = useAtomValue(bankRecClosingBalanceAtom(bankAccount?.name ?? '')) + + const difference = flt(value.value - (data?.message ?? 0)) + + const isError = difference !== 0 + + return + {_("Difference")} + {isLoading ? : + {formatCurrency(difference, + bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? '')) + }} + +} + +const ReconcileProgress = () => { + + const bankAccount = useAtomValue(selectedBankAccountAtom) + + const dates = useAtomValue(bankRecDateAtom) + + const { data: totalCount } = useFrappeGetDocCount('Bank Transaction', [ + ["bank_account", "=", bankAccount?.name ?? ''], + ['docstatus', '=', 1], + ['date', '<=', dates?.toDate], + ['date', '>=', dates?.fromDate] + ], false, undefined, { + revalidateOnFocus: false + }) + + const { data: unreconciledTransactions, } = useGetUnreconciledTransactions() + + const reconciledCount = (totalCount ?? 0) - (unreconciledTransactions?.message?.length ?? 0) + + const progress = (totalCount ? reconciledCount / totalCount : 0) * 100 + + return
      +
      + +
      +
      +} + +const ClosingBalanceAsPerStatement = () => { + + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + const setValue = useSetAtom(bankRecClosingBalanceAtom(bankAccount?.name ?? '')) + + const { data, isLoading } = useGetAccountClosingBalanceAsPerStatement({ + onSuccess: (data) => { + if (data?.message && data?.message?.balance) { + setValue({ + value: data?.message?.balance, + stringValue: data?.message?.balance.toString() + }) + } + } + }) + + const isDateSame = data?.message?.date === dates.toDate + + const [isOpen, setIsOpen] = useState(false) + + + return + {_("Closing Balance as per statement")} +
      + + + + +
      + {isLoading ? : {formatCurrency(flt(data?.message?.balance, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}} + +
      +
      + + {_("Click to set the closing balance as per statement")} + +
      +
      + + setIsOpen(false)} + /> + + + +
      + {!isDateSame && data?.message.date && {_("As of {0}", [formatDate(data?.message?.date ?? '', 'Do MMM YYYY')])}} +
      +
      + +} + +const ClosingBalanceForm = ({ defaultBalance, date, bankAccount, onClose }: { defaultBalance: number, date: string, bankAccount: SelectedBank | null, onClose: VoidFunction }) => { + + const { mutate } = useSWRConfig() + + const form = useForm<{ balance: number }>({ + defaultValues: { + balance: defaultBalance + } + }) + + const setValue = useSetAtom(bankRecClosingBalanceAtom(bankAccount?.name ?? '')) + + const { call, loading, error } = useFrappePostCall("erpnext.accounts.doctype.bank_account.bank_account.set_closing_balance_as_per_statement") + + const onSubmit = (data: { balance: number }) => { + if (data.balance) { + call({ + bank_account: bankAccount?.name ?? '', + date: date, + balance: data.balance + }) + .then(() => { + // Mutate the closing balance as per statement + mutate(`bank-reconciliation-account-closing-balance-as-per-statement-${bankAccount?.name}-${date}`) + setValue({ + value: data.balance, + stringValue: data.balance.toString() + }) + toast.success(_("Closing balance set.")) + onClose() + + + }) + } else { + toast.error(_("Closing balance is required.")) + } + } + + const currency = bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? '') + + + return
      + + + {_("Set closing balance as per bank statement")} + + {_("Enter the closing balance you see in your bank statement for {0} as of the {1}", [bankAccount?.account_name ?? bankAccount?.name ?? '', formatDate(date, 'Do MMM YYYY')])} + + + {error && } +
      + +
      + + + + + + + + + + + +} + +const ClosingBalancesList = ({ bankAccount, date }: { bankAccount: SelectedBank | null, date: string }) => { + + const { data, mutate } = useFrappeGetDocList("Bank Account Balance", { + filters: [["bank_account", "=", bankAccount?.name ?? ''], ["date", "<=", date]], + orderBy: { + field: "date", + order: "desc" + }, + fields: ["date", "balance", "name"], + limit: 10 + }) + + const { db } = useContext(FrappeContext) as FrappeConfig + + const onDelete = (name: string) => { + toast.promise(db.deleteDoc("Bank Account Balance", name).then(() => { + mutate() + }), { + loading: _("Deleting closing balance..."), + success: _("Closing balance deleted."), + error: _("Failed to delete closing balance.") + }) + } + + if (data?.length === 0) { + return null + } + + return
      + +

      {_("Balances as per bank statement before {0}", [formatDate(date, 'Do MMM YYYY')])}

      + + + + {_("Date")} + {_("Balance")} + + + + + {data?.map((item) => ( + + {formatDate(item.date, 'Do MMM YYYY')} + {formatCurrency(flt(item.balance, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))} + + + + + ))} + +
      +
      + +} + +export default BankBalance \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/BankClearanceSummary.tsx b/banking/src/components/features/BankReconciliation/BankClearanceSummary.tsx new file mode 100644 index 00000000000..077ee41ccd2 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/BankClearanceSummary.tsx @@ -0,0 +1,355 @@ +import { useAtomValue } from "jotai" +import { MissingFiltersBanner } from "./MissingFiltersBanner" +import { bankRecDateAtom, SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms" +import { useCurrentCompany } from "@/hooks/useCurrentCompany" +import { Paragraph } from "@/components/ui/typography" +import type { ColumnDef } from "@tanstack/react-table" +import { useCallback, useMemo, useState } from "react" +import { useFrappeGetCall, useFrappePostCall, useSWRConfig } from "frappe-react-sdk" +import { QueryReportReturnType } from "@/types/custom/Reports" +import { formatDate } from "@/lib/date" +import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view" +import { Table, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { formatCurrency } from "@/lib/numbers" +import { getCompanyCurrency } from "@/lib/company" +import { slug } from "@/lib/frappe" +import { CheckCircle2, ReceiptTextIcon, XCircle } from "lucide-react" +import ErrorBanner from "@/components/ui/error-banner" +import { Badge } from "@/components/ui/badge" +import _ from "@/lib/translate" +import { useCopyToClipboard } from "usehooks-ts" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" +import { Form } from "@/components/ui/form" +import { useForm } from "react-hook-form" +import { DateField } from "@/components/ui/form-elements" +import { Empty, EmptyMedia, EmptyHeader, EmptyTitle, EmptyDescription } from "@/components/ui/empty" + +const BankClearanceSummary = () => { + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + + if (!bankAccount) { + return + } + + if (!dates) { + return + } + + return +} +interface BankClearanceSummaryEntry { + payment_document_type: string + payment_entry: string + posting_date: string, + cheque_no?: string, + amount: number, + against: string, + clearance_date: string, +} + +const BankClearanceSummaryView = () => { + + const companyID = useCurrentCompany() + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + + const filters = useMemo(() => { + return JSON.stringify({ + account: bankAccount?.account, + from_date: dates.fromDate, + to_date: dates.toDate + }) + }, [bankAccount, dates]) + + const { data, error, mutate } = useFrappeGetCall<{ message: QueryReportReturnType }>('frappe.desk.query_report.run', { + report_name: 'Bank Clearance Summary', + filters, + ignore_prepared_report: 1, + are_default_filters: false, + }, `Report-Bank Clearance Summary-${filters}`, { keepPreviousData: true, revalidateOnFocus: false }, 'POST') + + const formattedFromDate = formatDate(dates.fromDate) + const formattedToDate = formatDate(dates.toDate) + + const [, copyToClipboard] = useCopyToClipboard() + + const onCopy = useCallback( + (text: string) => { + copyToClipboard(text).then(() => { + toast.success(_("Copied to clipboard")) + }) + }, + [copyToClipboard, _], + ) + + const accountCurrency = useMemo( + () => bankAccount?.account_currency ?? getCompanyCurrency(companyID), + [bankAccount?.account_currency, companyID], + ) + + const clearanceColumns = useMemo[]>( + () => [ + { + accessorKey: "payment_document_type", + header: _("Document Type"), + size: 140, + cell: ({ row }) => _(row.original.payment_document_type), + }, + { + id: "payment_entry", + header: _("Payment Document"), + size: 160, + meta: { + getTooltipText: (r) => { + const x = r as BankClearanceSummaryEntry + return [x.payment_document_type, x.payment_entry].filter(Boolean).join(" · ") || undefined + }, + } satisfies ListViewColumnMeta, + cell: ({ row }) => ( + + {row.original.payment_entry} + + ), + }, + { + accessorKey: "posting_date", + header: _("Posting Date"), + size: 118, + meta: { tabularNums: true } satisfies ListViewColumnMeta, + cell: ({ row }) => formatDate(row.original.posting_date), + }, + { + accessorKey: "cheque_no", + header: _("Cheque/Reference Number"), + size: 160, + cell: ({ row }) => { + const ref = row.original.cheque_no ?? "" + return ( + + + + + + {ref} + + + + ) + }, + }, + { + accessorKey: "clearance_date", + header: _("Clearance Date"), + size: 118, + meta: { tabularNums: true } satisfies ListViewColumnMeta, + cell: ({ row }) => formatDate(row.original.clearance_date), + }, + { + accessorKey: "against", + header: _("Against Account"), + size: 250, + }, + { + accessorKey: "amount", + header: _("Amount"), + size: 150, + meta: { align: "right" } satisfies ListViewColumnMeta, + cell: ({ row }) => {formatCurrency(row.original.amount, accountCurrency)}, + }, + { + id: "status", + header: _("Status"), + size: 200, + meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta, + cell: ({ row }) => { + const r = row.original + return r.clearance_date ? ( + + + {_("Cleared")} + + ) : ( +
      + + + {_("Not Cleared")} + + +
      + ) + }, + }, + ], + [_, accountCurrency, bankAccount, companyID, mutate, onCopy], + ) + + return
      + +
      + + ${bankAccount?.account}`, `${formattedFromDate}`, `${formattedToDate}`]) + }} /> + +
      + + {error && } + + {data && data.message.result.length > 0 ? ( + `${row.payment_entry}-${row.posting_date}`} + maxHeight="calc(100vh - 200px)" + scrollAreaClassName="min-h-[calc(100vh-200px)]" + emptyState={_("No rows to display.")} + /> + ) : null} + + {data && data.message.result.length == 0 && + + + + + + {_("No entries found")} + {_("There are no accounting entries in the system for the selected account and dates.")} + + + } + + +
      +} + +const SetClearanceDateButton = ({ voucher, bankAccount, companyID, mutate }: { voucher: BankClearanceSummaryEntry, bankAccount: SelectedBank | null, companyID: string, mutate: VoidFunction }) => { + + const [open, setOpen] = useState(false) + + const onClose = () => { + setOpen(false) + mutate() + } + + return + + + + + + + {_("Set the clearance date for this voucher without reconciling with a bank transaction.")} + + + + + {bankAccount && } + + +} + +const ForceClearVoucherForm = ({ voucher, bankAccount, companyID, onClose }: { voucher: BankClearanceSummaryEntry, bankAccount: SelectedBank, companyID: string, onClose: () => void }) => { + + const { mutate } = useSWRConfig() + + const dates = useAtomValue(bankRecDateAtom) + const form = useForm<{ clearance_date: string }>({ + defaultValues: { + clearance_date: voucher.posting_date, + } + }) + + const { call, loading, error } = useFrappePostCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.update_clearance_date') + + const onSubmit = (data: { clearance_date: string }) => { + call({ + payment_document: voucher.payment_document_type, + payment_entry: voucher.payment_entry, + account: bankAccount.account, + clearance_date: data.clearance_date, + }) + .then(() => { + toast.success(_("Clearance date updated")) + onClose() + mutate(`bank-reconciliation-account-closing-balance-${bankAccount?.name}-${dates.toDate}`) + }) + } + + return
      + + +
      + + + {_("Force Clear Voucher")} + + {_("Set the clearance date for this voucher without reconciling with a bank transaction.")} + + + {error && } +
      + + + + {_("Payment Document")} + {_(voucher.payment_document_type)} : {voucher.payment_entry} + + + {_("Posting Date")} + {formatDate(voucher.posting_date)} + + + {_("Cheque/Reference Number")} + {voucher.cheque_no?.slice(0, 40)}{voucher.cheque_no?.length && voucher.cheque_no?.length > 40 ? "..." : ""} + + + {_("Amount")} + {formatCurrency(voucher.amount, bankAccount?.account_currency ?? getCompanyCurrency(companyID))} + + + {_("Against Account")} + {voucher.against} + + +
      +
      + + + + + + + + +
      +
      + +} + +export default BankClearanceSummary diff --git a/banking/src/components/features/BankReconciliation/BankEntryModal.tsx b/banking/src/components/features/BankReconciliation/BankEntryModal.tsx new file mode 100644 index 00000000000..e6514e5d19c --- /dev/null +++ b/banking/src/components/features/BankReconciliation/BankEntryModal.tsx @@ -0,0 +1,831 @@ +import { useAtom, useAtomValue, useSetAtom } from "jotai" +import { bankRecRecordJournalEntryModalAtom, bankRecSelectedTransactionAtom, bankRecUnreconcileModalAtom, selectedBankAccountAtom } from "./bankRecAtoms" +import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogHeader, DialogFooter, DialogClose } from "@/components/ui/dialog" +import _ from "@/lib/translate" +import { UnreconciledTransaction, useGetRuleForTransaction, useRefreshUnreconciledTransactions, useUpdateActionLog } from "./utils" +import { useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form" +import { JournalEntry } from "@/types/Accounts/JournalEntry" +import { getCompanyCostCenter, getCompanyCurrency } from "@/lib/company" +import { FrappeConfig, FrappeContext, useFrappePostCall } from "frappe-react-sdk" +import { toast } from "sonner" +import ErrorBanner from "@/components/ui/error-banner" +import { Button } from "@/components/ui/button" +import SelectedTransactionDetails from "./SelectedTransactionDetails" +import { AccountFormField, CurrencyFormField, DataField, DateField, LinkFormField, PartyTypeFormField, SmallTextField } from "@/components/ui/form-elements" +import { Form } from "@/components/ui/form" +import { useCallback, useContext, useMemo, useRef, useState } from "react" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Checkbox } from "@/components/ui/checkbox" +import { ArrowDownRight, ArrowUpRight, Plus, Trash2 } from "lucide-react" +import { flt, formatCurrency } from "@/lib/numbers" +import { cn } from "@/lib/utils" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import SelectedTransactionsTable from "./SelectedTransactionsTable" +import { JournalEntryAccount } from "@/types/Accounts/JournalEntryAccount" +import { BankTransaction } from "@/types/Accounts/BankTransaction" +import FileUploadBanner from "@/components/common/FileUploadBanner" +import { Label } from "@/components/ui/label" +import { FileDropzone } from "@/components/ui/file-dropzone" +import { useGetAccounts } from "@/components/common/AccountsDropdown" +import { useHotkeys } from "react-hotkeys-hook" + +const BankEntryModal = () => { + + const [isOpen, setIsOpen] = useAtom(bankRecRecordJournalEntryModalAtom) + + return ( + + + + {_("Bank Entry")} + + {_("Record a journal entry for expenses, income or split transactions.")} + + + + + + ) +} + +const RecordBankEntryModalContent = () => { + + const selectedBankAccount = useAtomValue(selectedBankAccountAtom) + + const selectedTransaction = useAtomValue(bankRecSelectedTransactionAtom(selectedBankAccount?.name ?? '')) + + if (!selectedTransaction || !selectedBankAccount || selectedTransaction.length === 0) { + return
      + {_("No transaction selected")} +
      + } + + if (selectedTransaction.length === 1) { + return + } + + return + +} + +const BulkBankEntryForm = ({ selectedTransactions }: { selectedTransactions: UnreconciledTransaction[] }) => { + + const form = useForm<{ + account: string + }>({ + defaultValues: { + account: '' + } + }) + + const { call, loading, error } = useFrappePostCall<{ message: { transaction: BankTransaction, journal_entry: JournalEntry }[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_bulk_bank_entry_and_reconcile') + + const onReconcile = useRefreshUnreconciledTransactions() + const addToActionLog = useUpdateActionLog() + + const setIsOpen = useSetAtom(bankRecRecordJournalEntryModalAtom) + + const onSubmit = (data: { account: string }) => { + + call({ + bank_transactions: selectedTransactions.map(transaction => transaction.name), + account: data.account + }).then(({ message }) => { + + addToActionLog({ + type: 'bank_entry', + timestamp: (new Date()).getTime(), + isBulk: true, + items: message.map((item) => ({ + bankTransaction: item.transaction, + voucher: { + reference_doctype: "Journal Entry", + reference_name: item.journal_entry.name, + doc: item.journal_entry, + posting_date: item.journal_entry.posting_date, + } + })), + bulkCommonData: { + account: data.account, + } + }) + + toast.success(_("Bank Entries Created"), { + duration: 4000, + }) + + // Set this to the last selected transaction + onReconcile(selectedTransactions[selectedTransactions.length - 1]) + setIsOpen(false) + }) + } + + return
      + +
      + {error && } + + +
      + { + // Do not allow payable and receivable accounts + return acc.account_type !== 'Payable' && acc.account_type !== 'Receivable' + }} + label={_('Account')} + isRequired + /> +
      + + + + + + + +
      +
      + +} + + +interface BankEntryFormData extends Pick { + entries: JournalEntry['accounts'] +} + + +const BankEntryForm = ({ selectedTransaction }: { selectedTransaction: UnreconciledTransaction }) => { + + const selectedBankAccount = useAtomValue(selectedBankAccountAtom) + + const { data: rule } = useGetRuleForTransaction(selectedTransaction) + + const setIsOpen = useSetAtom(bankRecRecordJournalEntryModalAtom) + + const onClose = () => { + setIsOpen(false) + } + + const isWithdrawal = (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) ? true : false + + const defaultAccounts = useMemo(() => { + + const isWithdrawal = (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) ? true : false + + const accounts: Partial[] = [ + { + account: selectedBankAccount?.account ?? '', + bank_account: selectedTransaction.bank_account, + // Bank is debited if it's a deposit + debit: isWithdrawal ? 0 : selectedTransaction.unallocated_amount, + credit: isWithdrawal ? selectedTransaction.unallocated_amount : 0, + party_type: '', + party: '', + cost_center: '' + }] + + // If there is no rule, we can just add the entries for the bank account transaction and the other side will be the reverse + if (!rule) { + accounts.push( + { + account: '', + // Amounts will be the reverse of the bank account transaction + debit: isWithdrawal ? selectedTransaction.unallocated_amount : 0, + credit: isWithdrawal ? 0 : selectedTransaction.unallocated_amount, + cost_center: getCompanyCostCenter(selectedTransaction.company ?? '') ?? '', + } + ) + } else { + // Rule exists, so we need to check the type of rule + if (!rule.bank_entry_type || rule.bank_entry_type === "Single Account") { + // Only a single account needs to be added + accounts.push({ + account: rule.account ?? '', + // Amounts will be the reverse of the bank account transaction + debit: isWithdrawal ? selectedTransaction.unallocated_amount : 0, + credit: isWithdrawal ? 0 : selectedTransaction.unallocated_amount, + cost_center: getCompanyCostCenter(selectedTransaction.company ?? '') ?? '', + }) + } else { + // For multiple accounts, we need to loop over and add entries for each + // The last row will just be the remaining amount + let hasTotallyEmptyRowEarlier = false; + + let totalDebits = isWithdrawal ? 0 : selectedTransaction.unallocated_amount ?? 0 + let totalCredits = isWithdrawal ? selectedTransaction.unallocated_amount ?? 0 : 0 + + for (let i = 0; i < (rule.accounts?.length ?? 0); i++) { + + const acc = rule.accounts?.[i] + // If it's the last row, add the difference amount + if (i === (rule.accounts?.length ?? 0) - 1 && !hasTotallyEmptyRowEarlier) { + + const differenceAmount = flt(totalDebits - totalCredits, 2) + accounts.push({ + account: acc?.account ?? '', + debit: differenceAmount > 0 ? 0 : Math.abs(differenceAmount), + credit: differenceAmount > 0 ? Math.abs(differenceAmount) : 0, + cost_center: getCompanyCostCenter(selectedTransaction.company ?? '') ?? '', + user_remark: acc?.user_remark ?? '', + }) + } else { + + /** + * The debit and credit amounts can also be expressions - like "transaction_amount * 0.5" + * So we need to compute the value of the expression + * We can use the eval function to do this. But we need to expose certain variables to the expression. + * One of them is transaction_amount which is the unallocated amount of the selected transaction + * @param expression - The expression to compute + * @returns The computed value + */ + const computeExpression = (expression: string) => { + + const script = ` + const transaction_amount = ${selectedTransaction.unallocated_amount ?? 0} + ${expression}; + ` + + let value = 0; + + try { + value = window.eval(script); + } catch (error: unknown) { + console.error(error); + value = 0; + } + + return value; + } + if (!acc?.debit && !acc?.credit) { + hasTotallyEmptyRowEarlier = true; + } + + const computedDebit = acc?.debit ? flt(computeExpression(acc.debit), 2) : 0 + const computedCredit = acc?.credit ? flt(computeExpression(acc.credit), 2) : 0 + + totalDebits = flt(totalDebits + computedDebit, 2) + totalCredits = flt(totalCredits + computedCredit, 2) + accounts.push({ + account: acc?.account ?? '', + debit: computedDebit, + credit: computedCredit, + cost_center: getCompanyCostCenter(selectedTransaction.company ?? '') ?? '', + user_remark: acc?.user_remark ?? '', + }) + } + } + } + } + + return accounts + + }, [rule, selectedTransaction, selectedBankAccount]) + + const form = useForm({ + defaultValues: { + voucher_type: selectedBankAccount?.is_credit_card ? 'Credit Card Entry' : 'Bank Entry', + cheque_date: selectedTransaction.date, + posting_date: selectedTransaction.date, + cheque_no: (selectedTransaction.reference_number || selectedTransaction.description || '').slice(0, 140), + user_remark: selectedTransaction.description, + entries: defaultAccounts, + } + }) + + const onReconcile = useRefreshUnreconciledTransactions() + + const { call: createBankEntry, loading, error, isCompleted } = useFrappePostCall<{ message: { transaction: BankTransaction, journal_entry: JournalEntry } }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_bank_entry_and_reconcile') + + const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom) + const addToActionLog = useUpdateActionLog() + + const { file: frappeFile } = useContext(FrappeContext) as FrappeConfig + + const [isUploading, setIsUploading] = useState(false) + const [uploadProgress, setUploadProgress] = useState(0) + + const [files, setFiles] = useState([]) + + const onSubmit = (data: BankEntryFormData) => { + + createBankEntry({ + bank_transaction_name: selectedTransaction.name, + ...data + }).then(async ({ message }) => { + + addToActionLog({ + type: 'bank_entry', + isBulk: false, + timestamp: (new Date()).getTime(), + items: [ + { + bankTransaction: message.transaction, + voucher: { + reference_doctype: "Journal Entry", + reference_name: message.journal_entry.name, + reference_no: message.journal_entry.cheque_no, + reference_date: message.journal_entry.cheque_date, + posting_date: message.journal_entry.posting_date, + doc: message.journal_entry, + } + } + ] + }) + toast.success(_("Bank Entry Created"), { + duration: 4000, + closeButton: true, + action: { + label: _("Undo"), + onClick: () => setBankRecUnreconcileModalAtom(selectedTransaction.name) + }, + actionButtonStyle: { + backgroundColor: "rgb(0, 138, 46)" + } + }) + + if (files.length > 0) { + setIsUploading(true) + + const uploadPromises = files.map(f => { + return frappeFile.uploadFile(f, { + isPrivate: true, + doctype: "Journal Entry", + docname: message.journal_entry.name, + }, (_bytesUploaded, _totalBytes, progress) => { + + setUploadProgress((currentProgress) => { + //If there are multiple files, we need to add the progress to the current progress + return currentProgress + ((progress?.progress ?? 0) / files.length) + }) + + }) + }) + + return Promise.all(uploadPromises).then(() => { + setUploadProgress(0) + setIsUploading(false) + }).catch((error) => { + console.error(error) + toast.error(_("Error uploading attachments"), { + duration: 4000, + }) + setIsUploading(false) + }) + } else { + return Promise.resolve() + } + + }).then(() => { + onReconcile(selectedTransaction) + onClose() + }) + } + + + useHotkeys('meta+s', () => { + form.handleSubmit(onSubmit)() + }, { + enabled: true, + preventDefault: true, + enableOnFormTags: true + }) + + if (isUploading && isCompleted) { + return + } + + return
      + +
      + {error && } +
      + + +
      +
      + + +
      + +
      +
      + +
      + +
      +
      +
      + +
      + + +
      +
      +
      + + + + + + + +
      +
      + + +} + +const Entries = ({ company, isWithdrawal, currency }: { company: string, isWithdrawal: boolean, currency: string }) => { + + const { getValues, setValue, control } = useFormContext() + + const { call } = useContext(FrappeContext) as FrappeConfig + + const partyMapRef = useRef>({}) + + const onPartyChange = (value: string, index: number) => { + // Get the account for the party type + if (value) { + if (partyMapRef.current[value]) { + setValue(`entries.${index}.account`, partyMapRef.current[value]) + } else { + call.get('erpnext.accounts.party.get_party_account', { + party: value, + party_type: getValues(`entries.${index}.party_type`), + company: company + }).then((result: { message: string }) => { + setValue(`entries.${index}.account`, result.message) + partyMapRef.current[value] = result.message + }) + } + } else { + setValue(`entries.${index}.account`, '') + } + } + + const { data: accounts } = useGetAccounts() + + const onAccountChange = (value: string, index: number) => { + // If it's an income or expense account, get the default cost center + if (value) { + const account = accounts?.find((acc) => acc.name === value) + if (account && account.report_type === "Profit and Loss") { + // Set the default company cost center + setValue(`entries.${index}.cost_center`, getCompanyCostCenter(company) ?? '') + return + } + } + + setValue(`entries.${index}.cost_center`, '') + } + + const { fields, append, remove } = useFieldArray({ + control: control, + name: 'entries' + }) + + const onAdd = useCallback(() => { + const existingEntries = getValues('entries') + const totalDebits = existingEntries.reduce((acc, curr) => flt(acc + (curr.debit ?? 0), 2), 0) + const totalCredits = existingEntries.reduce((acc, curr) => flt(acc + (curr.credit ?? 0), 2), 0) + + const remainingAmount = flt(totalDebits - totalCredits, 2) + + // Remaining amount is credit if it's positive - since some debit is pending to be cleared. + const debitAmount = remainingAmount > 0 ? 0 : Math.abs(remainingAmount) + const creditAmount = remainingAmount > 0 ? Math.abs(remainingAmount) : 0 + + append({ + party_type: '', + party: '', + account: '', + debit: debitAmount, + credit: creditAmount, + cost_center: getCompanyCostCenter(company) ?? '' + } as JournalEntryAccount, { + focusName: `entries.${existingEntries.length}.account` + }) + }, [company, append, getValues]) + + const [selectedRows, setSelectedRows] = useState([]) + + const onSelectRow = useCallback((index: number) => { + setSelectedRows(prev => { + if (prev.includes(index)) { + return prev.filter(i => i !== index) + } + return [...prev, index] + }) + }, []) + + const onSelectAll = useCallback(() => { + setSelectedRows(prev => { + if (prev.length === fields.length) { + return [] + } + return [...fields.map((_, index) => index)] + }) + }, [fields]) + + const onRemove = useCallback(() => { + remove(selectedRows) + setSelectedRows([]) + }, [remove, selectedRows]) + + /** + * When add difference is clicked, check if the last row has nothing filled in. + * If last row is empty (no debit or credit), then set that row's amount. Else, add a new row with the difference amount. + */ + const onAddDifferenceClicked = () => { + + const existingEntries = getValues('entries') + const totalDebits = existingEntries.reduce((acc, curr) => flt(acc + (curr.debit ?? 0), 2), 0) + const totalCredits = existingEntries.reduce((acc, curr) => flt(acc + (curr.credit ?? 0), 2), 0) + + const lastIndex = existingEntries.length - 1 + + const isLastRowEmpty = (existingEntries[lastIndex]?.debit === 0 || existingEntries[lastIndex]?.debit === undefined) && (existingEntries[lastIndex]?.credit === 0 || existingEntries[lastIndex]?.credit === undefined) + + const remainingAmount = flt(totalDebits - totalCredits, 2) + + // Remaining amount is credit if it's positive - since some debit is pending to be cleared. + const debitAmount = remainingAmount > 0 ? 0 : Math.abs(remainingAmount) + const creditAmount = remainingAmount > 0 ? Math.abs(remainingAmount) : 0 + + if (isLastRowEmpty) { + setValue(`entries.${lastIndex}.debit`, debitAmount) + setValue(`entries.${lastIndex}.credit`, creditAmount) + } else { + append({ + party_type: '', + party: '', + account: '', + debit: debitAmount, + credit: creditAmount, + cost_center: getCompanyCostCenter(company) ?? '' + } as JournalEntryAccount, { + focusName: `entries.${existingEntries.length}.account` + }) + } + } + + + + return
      + + + + 0 && selectedRows.length === fields.length} + onCheckedChange={onSelectAll} /> + {_("Party")} + {_("Account")} + {_("Cost Center")} + {_("Remarks")} + {_("Debit")} + {_("Credit")} + + + + {fields.map((field, index) => ( + + + onSelectRow(index)} + // Make this accessible to screen readers + aria-label={_("Select row {0}", [String(index + 1)])} + disabled={index === 0} + /> + + + +
      + + +
      + +
      + + { + onAccountChange(event.target.value, index) + } + }} + buttonClassName="min-w-64" + readOnly={index === 0} + isRequired + hideLabel + /> + + + + + + + + + + + {_("Bank account debit for deposit")} + : undefined} + /> + + + + + {_("Bank account credit for withdrawal")} + : undefined} + /> + +
      + ))} +
      +
      +
      +
      +
      + +
      + {selectedRows.length > 0 &&
      + +
      } +
      + +
      +
      + +} + +const PartyField = ({ index, onChange, readOnly }: { index: number, onChange: (value: string, index: number) => void, readOnly: boolean }) => { + + const { control } = useFormContext() + + const party_type = useWatch({ + control, + name: `entries.${index}.party_type` + }) + + if (!party_type) { + return + } + + return { + onChange(event.target.value, index) + }, + }} + hideLabel + readOnly={readOnly} + buttonClassName="rounded-s-none border-s-0 min-w-64" + doctype={party_type} + + /> +} + +const Summary = ({ currency, addRow }: { currency: string, addRow: () => void }) => { + + const { control } = useFormContext() + + const entries = useWatch({ control, name: 'entries' }) + + const { total, totalCredits, totalDebits } = useMemo(() => { + // Do a total debits - total credits + const totalDebits = entries.reduce((acc, curr) => flt(acc + (curr.debit ?? 0), 2), 0) + const totalCredits = entries.reduce((acc, curr) => flt(acc + (curr.credit ?? 0), 2), 0) + return { total: flt(totalDebits - totalCredits, 2), totalDebits, totalCredits } + }, [entries]) + + const onAddRow = useCallback(() => { + addRow() + }, [addRow]) + + const TextComponent = ({ className, children }: { className?: string, children: React.ReactNode }) => { + return {children} + } + + return
      +
      + {_("Total Debit")} + {formatCurrency(totalDebits, currency)} +
      +
      + {_("Total Credit")} + {formatCurrency(totalCredits, currency)} +
      + {total !== 0 &&
      + {_("Difference")} + + + + + + {_("Add a row with the difference amount")} + + +
      } + +
      + +} + + +export default BankEntryModal diff --git a/banking/src/components/features/BankReconciliation/BankPicker.tsx b/banking/src/components/features/BankReconciliation/BankPicker.tsx new file mode 100644 index 00000000000..9150103fd32 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/BankPicker.tsx @@ -0,0 +1,124 @@ +import { useAtom, useSetAtom } from "jotai" +import { SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms" +import { useCallback } from "react" +import { useGetBankAccounts, useGetUnreconciledTransactions } from "./utils" +import { cn } from "@/lib/utils" +import { getTimeago } from "@/lib/date" +import ErrorBanner from "@/components/ui/error-banner" +import _ from "@/lib/translate" +import { Badge } from "@/components/ui/badge" +import { useTheme } from "@/components/ui/theme-provider" +import BankLogo from "@/components/common/BankLogo" +import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty" +import { LandmarkIcon } from "lucide-react" +import { Button } from "@/components/ui/button" +import { useCurrentCompany } from "@/hooks/useCurrentCompany" + +const BankPicker = ({ className }: { className?: string }) => { + + const setSelectedBank = useSetAtom(selectedBankAccountAtom) + + const onLoadingSuccess = useCallback((data?: SelectedBank[]) => { + if (!data) return + if (data.length === 1) { + setSelectedBank(data[0]) + } else if (data.length > 1) { + const defaultBank = data.find((bank: SelectedBank) => bank.is_default) + if (defaultBank) { + setSelectedBank(defaultBank) + } + } + }, [setSelectedBank]) + + const selectedCompany = useCurrentCompany() + + const { banks, isLoading, error } = useGetBankAccounts(onLoadingSuccess) + + const { themeValue } = useTheme() + + if (isLoading) { + return null + } + + if (error) { + return + } + + if (banks?.length === 0) { + return + + + + + {_("No bank accounts found")} + {_("You have not added any bank accounts to your company.")} + + + + + + } + return ( +
      4 ? 'pb-2' : '', className, + )} + style={{ + scrollbarWidth: 'thin', + scrollbarColor: themeValue === 'Dark' ? 'var(--surface-gray-2) var(--surface-gray-1)' : 'rgb(209 213 219) rgb(243 244 246)', + }} + > + { + banks?.map((bank) => ( + + )) + } +
      + ) +} + +const BankPickerItem = ({ bank }: { bank: SelectedBank }) => { + + const [selectedBank, setSelectedBank] = useAtom(selectedBankAccountAtom) + + const isSelected = selectedBank?.name === bank.name + + const { mutate } = useGetUnreconciledTransactions() + + const onSelect = () => { + setSelectedBank(bank) + mutate() + } + + return
      + + + + +
      +
      + {bank.account_name} + {bank.account_type && + {bank.account_type?.slice(0, 24)} + } +
      + + {bank.account} + {bank.last_integration_date && {_("Last Synced Transaction")}: {getTimeago(bank.last_integration_date)}} +
      + +
      +} + +export default BankPicker \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/BankRecDateFilter.tsx b/banking/src/components/features/BankReconciliation/BankRecDateFilter.tsx new file mode 100644 index 00000000000..84bd5278ccc --- /dev/null +++ b/banking/src/components/features/BankReconciliation/BankRecDateFilter.tsx @@ -0,0 +1,275 @@ +import { useAtom } from 'jotai' +import { bankRecDateAtom } from './bankRecAtoms' +import { useMemo, useState } from 'react' +import { AVAILABLE_TIME_PERIODS, formatDate, getDatesForTimePeriod, TimePeriod } from '@/lib/date' +import { Button } from '@/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { ChevronDownIcon, ChevronLeftIcon, ChevronRight } from 'lucide-react' +import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from '@/components/ui/command' +import { parse } from "chrono-node" +import { Calendar } from '@/components/ui/calendar' +import useFiscalYear from '@/hooks/useFiscalYear' +import dayjs from 'dayjs' +import _ from '@/lib/translate' +import { useDirection } from '@/components/ui/direction' + +const BankRecDateFilter = () => { + + const [bankRecDate, setBankRecDate] = useAtom(bankRecDateAtom) + + const { data: fiscalYear } = useFiscalYear() + + const timePeriodOptions = useMemo(() => { + const standardOptions = AVAILABLE_TIME_PERIODS.map((period) => { + const dates = getDatesForTimePeriod(period) + return { + label: period, + fromDate: dates.fromDate, + toDate: dates.toDate, + format: dates.format, + translatedLabel: dates.translatedLabel + } + }) + + if (fiscalYear?.message) { + // For a fiscal year, we need to replace "Last Year", "This Year", and add options for quarters + const fiscalYearStart = fiscalYear.message.year_start_date + const fiscalYearEnd = fiscalYear.message.year_end_date + + const q1 = { + label: `Q1: ${fiscalYear.message.name}`, + translatedLabel: `${_("Q1")}: ${fiscalYear.message.name}`, + fromDate: fiscalYearStart, + toDate: dayjs(fiscalYearStart).add(3, 'month').format('YYYY-MM-DD'), + format: 'MMM YYYY' + } + + const q2 = { + label: `Q2: ${fiscalYear.message.name}`, + translatedLabel: `${_("Q2")}: ${fiscalYear.message.name}`, + fromDate: dayjs(fiscalYearStart).add(3, 'month').format('YYYY-MM-DD'), + toDate: dayjs(fiscalYearStart).add(6, 'month').format('YYYY-MM-DD'), + format: 'MMM YYYY' + } + + const q3 = { + label: `Q3: ${fiscalYear.message.name}`, + translatedLabel: `${_("Q3")}: ${fiscalYear.message.name}`, + fromDate: dayjs(fiscalYearStart).add(6, 'month').format('YYYY-MM-DD'), + toDate: dayjs(fiscalYearStart).add(9, 'month').format('YYYY-MM-DD'), + format: 'MMM YYYY' + } + + const q4 = { + label: `Q4: ${fiscalYear.message.name}`, + translatedLabel: `${_("Q4")}: ${fiscalYear.message.name}`, + fromDate: dayjs(fiscalYearStart).add(9, 'month').format('YYYY-MM-DD'), + toDate: fiscalYearEnd, + format: 'MMM YYYY' + } + + const thisYear = { + label: `This Fiscal Year`, + translatedLabel: `${_("This Fiscal Year")}`, + fromDate: fiscalYearStart, + toDate: fiscalYearEnd, + format: 'MMM YYYY' + } + + const lastYear = { + label: `Last Fiscal Year`, + translatedLabel: `${_("Last Fiscal Year")}`, + fromDate: dayjs(fiscalYearStart).subtract(1, 'year').format('YYYY-MM-DD'), + toDate: dayjs(fiscalYearEnd).subtract(1, 'year').format('YYYY-MM-DD'), + format: 'MMM YYYY' + } + // Sort the options so that we get "This Month", "Last Month", quarters, fiscal year, then the rest of the standard options + + const topRankedItems = standardOptions.filter((option) => { + return option.label === "This Month" || option.label === "Last Month" + }) + + const bottomRankedItems = standardOptions.filter((option) => { + return option.label !== "This Month" && option.label !== "Last Month" + }) + + return [...topRankedItems, q1, q2, q3, q4, thisYear, lastYear, ...bottomRankedItems] + } + + return standardOptions + }, [fiscalYear]) + + const [open, setOpen] = useState(false) + const [value, setValue] = useState("") + + const timePeriod: TimePeriod | string = useMemo(() => { + if (bankRecDate.fromDate && bankRecDate.toDate) { + // Check if the from and to dates match any predefined time period + for (const period of timePeriodOptions) { + if (period.fromDate === bankRecDate.fromDate && period.toDate === bankRecDate.toDate) { + return period.label; + } + } + return "Date Range"; + } else { + return "Date Range"; + } + }, [bankRecDate.fromDate, bankRecDate.toDate, timePeriodOptions]); + + const handleTimePeriodChange = (fromDate: string, toDate: string) => { + setBankRecDate({ fromDate, toDate }) + setOpen(false) + } + + const dateObj = useMemo(() => { + return { + from: new Date(bankRecDate.fromDate), + to: new Date(bankRecDate.toDate) + } + }, [bankRecDate.fromDate, bankRecDate.toDate]) + + const direction = useDirection() + + + + return
      + + + + + + + + + + + + + + {timePeriodOptions.map((period) => ( + handleTimePeriodChange(period.fromDate, period.toDate)}> + + {period.translatedLabel ?? _(period.label)} + + + {formatDate(period.fromDate, period.format)} {direction === 'ltr' ? : } {formatDate(period.toDate, period.format)} + + + ))} + + + + + + + + + + + + { + if (date) { + setBankRecDate({ fromDate: formatDate(date.from, 'YYYY-MM-DD'), toDate: formatDate(date.to, 'YYYY-MM-DD') }) + } + }} + /> + + +
      +} + +const referentialKeywords = ["last", "this", "next", "previous"] +const EmptyState = ({ onSelect, value }: { onSelect: (fromDate: string, toDate: string) => void, value: string }) => { + + const dates = useMemo(() => { + if (value) { + // Try parsing the value + const parsedDate = parse(value, undefined, { forwardDate: false }) + + if (parsedDate && parsedDate.length > 0) { + const startDate = parsedDate[0].start.date() + const endDate = parsedDate[0].end?.date() + + if (!endDate) { + const today = new Date() + // If today is greater than the start date, use today as the end date + if (startDate.getTime() > today.getTime()) { + return { fromDate: today, toDate: startDate } + } else { + // Check if the user only wants a specific month like "May 2025" + // If the "known values" just has month and year, then we need to get the first day of the month and the last day of the month + // @ts-expect-error - "Known Values" is available in the start "ParsingComponents" + if (parsedDate[0].start.knownValues?.month && !parsedDate[0].start.knownValues?.day) { + return { + fromDate: startDate, + toDate: dayjs(startDate).endOf('month').toDate() + } + // @ts-expect-error - "Known Values" is available in the start "ParsingComponents" + } else if (parsedDate[0].start.knownValues?.month && parsedDate[0].start.knownValues?.day && !referentialKeywords.some(keyword => value.toLowerCase().includes(keyword))) { + // If month and day is known, then we should not assume that the user wants to get everything until today + return { + fromDate: startDate, + toDate: startDate, + } + } + + return { + fromDate: startDate, + toDate: today + } + } + } else { + return { fromDate: startDate, toDate: endDate } + } + } + + } + }, [value]) + + const onClick = (fromDate: Date, toDate: Date) => { + onSelect(formatDate(fromDate, 'YYYY-MM-DD'), formatDate(toDate, 'YYYY-MM-DD')) + } + + const isEqual = dates?.fromDate && dates?.toDate && dayjs(dates.fromDate).isSame(dates.toDate, 'date') + + return
      + {dates ? +
      onClick(dates.fromDate, dates.toDate)}> + + {value} + + {isEqual ? + {formatDate(dates.fromDate, 'Do MMM YYYY')} + : + + {formatDate(dates.fromDate, 'Do MMM YY')} {formatDate(dates.toDate, 'Do MMM YY')} + } +
      : + + No results found + + } +
      +} + +export default BankRecDateFilter \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/BankReconciliationStatement.tsx b/banking/src/components/features/BankReconciliation/BankReconciliationStatement.tsx new file mode 100644 index 00000000000..acfed95aa15 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/BankReconciliationStatement.tsx @@ -0,0 +1,315 @@ +import { useAtomValue } from "jotai" +import { MissingFiltersBanner } from "./MissingFiltersBanner" +import { bankRecDateAtom, selectedBankAccountAtom } from "./bankRecAtoms" +import { useCurrentCompany } from "@/hooks/useCurrentCompany" +import { Paragraph } from "@/components/ui/typography" +import { useCallback, useMemo } from "react" +import type { ColumnDef } from "@tanstack/react-table" +import { useFrappeGetCall } from "frappe-react-sdk" +import { QueryReportReturnType } from "@/types/custom/Reports" +import { formatDate } from "@/lib/date" +import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view" +import { formatCurrency } from "@/lib/numbers" +import { getCompanyCurrency } from "@/lib/company" +import { slug } from "@/lib/frappe" +import { ScrollTextIcon } from "lucide-react" +import ErrorBanner from "@/components/ui/error-banner" +import { StatContainer, StatLabel, StatValue } from "@/components/ui/stats" +import _ from "@/lib/translate" +import { toast } from "sonner" +import { useCopyToClipboard } from "usehooks-ts" +import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty" + +const BankReconciliationStatement = () => { + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + + if (!bankAccount) { + return + } + + if (!dates) { + return + } + + return +} +interface BankClearanceSummaryEntry { + payment_document: string + payment_entry: string + posting_date: string, + reference_no: string, + credit: number, + debit: number, + against_account: string, + ref_date: string, + account_currency: string, + clearance_date: string +} + +const BankReconciliationStatementView = () => { + + const companyID = useCurrentCompany() + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + + const filters = useMemo(() => { + return JSON.stringify({ + account: bankAccount?.account, + report_date: dates.toDate, + company: companyID + }) + }, [bankAccount, dates, companyID]) + + const { data, error } = useFrappeGetCall<{ message: QueryReportReturnType }>('frappe.desk.query_report.run', { + report_name: 'Bank Reconciliation Statement', + filters, + ignore_prepared_report: 1, + are_default_filters: false, + }, `Report-Bank Reconciliation Statement-${filters}`, { keepPreviousData: true, revalidateOnFocus: false }, 'POST') + + const [, copyToClipboard] = useCopyToClipboard() + + const onCopy = useCallback( + (text: string) => { + copyToClipboard(text).then(() => { + toast.success(_("Copied to clipboard")) + }) + }, + [copyToClipboard, _], + ) + + const statementColumns = useMemo[]>( + () => [ + { + accessorKey: "posting_date", + header: _("Posting Date"), + size: 118, + meta: { tabularNums: true } satisfies ListViewColumnMeta, + cell: ({ row }) => formatDate(row.original.posting_date), + }, + { + accessorKey: "payment_document", + header: _("Document Type"), + size: 140, + cell: ({ row }) => _(row.original.payment_document), + }, + { + id: "payment_entry", + header: _("Payment Document"), + size: 300, + meta: { + getTooltipText: (r) => { + const x = r as BankClearanceSummaryEntry + const parts = [x.payment_document, x.payment_entry].filter(Boolean) + return parts.length ? parts.join(" · ") : undefined + }, + } satisfies ListViewColumnMeta, + cell: ({ row }) => { + const { payment_document, payment_entry } = row.original + return payment_document ? ( + + {payment_entry} + + ) : ( + payment_entry + ) + }, + }, + { + accessorKey: "debit", + header: _("Debit"), + size: 112, + meta: { align: "right" } satisfies ListViewColumnMeta, + cell: ({ row }) => {formatCurrency(row.original.debit, row.original.account_currency)}, + }, + { + accessorKey: "credit", + header: _("Credit"), + size: 112, + meta: { align: "right" } satisfies ListViewColumnMeta, + cell: ({ row }) => {formatCurrency(row.original.credit, row.original.account_currency)}, + }, + { + accessorKey: "against_account", + header: _("Against Account"), + meta: { gridWidth: "minmax(0,1.25fr)" } satisfies ListViewColumnMeta, + cell: ({ row }) => ( + + {row.original.against_account} + + ), + }, + { + accessorKey: "reference_no", + header: _("Reference #"), + cell: ({ row }) => { + const ref = row.original.reference_no + return ( + + ) + }, + }, + { + accessorKey: "ref_date", + header: _("Reference Date"), + size: 118, + meta: { tabularNums: true } satisfies ListViewColumnMeta, + cell: ({ row }) => formatDate(row.original.ref_date), + }, + { + accessorKey: "clearance_date", + header: _("Clearance Date"), + size: 118, + meta: { tabularNums: true } satisfies ListViewColumnMeta, + cell: ({ row }) => formatDate(row.original.clearance_date), + }, + ], + [_, onCopy], + ) + + const statementRows = useMemo(() => { + if (!data?.message.result) return [] + return data.message.result.filter((row: BankClearanceSummaryEntry) => Boolean(row.payment_entry)) + }, [data]) + + return
      + +
      + + ${bankAccount?.account}`, `${formatDate(dates.toDate)}`]) + }} /> + +
      + + {error && } + + {data && } + + {data && data.message.result.length > 0 && ( +
      +

      {_("Bank Reconciliation Statement")}

      + row.payment_entry} + maxHeight="min(70vh, 640px)" + emptyState={_("No entries with a payment document in this list.")} + /> +
      + )} + + {data && data.message.result.length === 0 && + + + + + + {_("No entries found")} + {_("There are no accounting entries in the system for the selected account and dates.")} + + + } + + +
      +} + +const SummarySection = ({ data }: { data: { message: QueryReportReturnType } }) => { + + const company = useCurrentCompany() + const bankAccount = useAtomValue(selectedBankAccountAtom) + + const { bankStatementBalanceAsPerGL, outstandingChecksDebit, outstandingChecksCredit, incorrectlyClearedEntriesDebit, incorrectlyClearedEntriesCredit, calculatedBankStatementBalance } = useMemo(() => { + + // Loop over the results and find the corresponding rows + + let bankStatementBalanceAsPerGL = 0 + + let outstandingChecksDebit = 0 + let outstandingChecksCredit = 0 + + let incorrectlyClearedEntriesDebit = 0 + let incorrectlyClearedEntriesCredit = 0 + + let calculatedBankStatementBalance = 0 + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?.message.result.forEach((r: any) => { + if (r.payment_entry === 'Bank Statement balance as per General Ledger') { + bankStatementBalanceAsPerGL = r.debit - r.credit + } + + if (r.payment_entry === 'Outstanding Checks and Deposits to clear') { + outstandingChecksDebit = r.debit + outstandingChecksCredit = r.credit + } + + if (r.payment_entry === 'Checks and Deposits incorrectly cleared') { + incorrectlyClearedEntriesDebit = r.debit + incorrectlyClearedEntriesCredit = r.credit + } + + if (r.payment_entry === 'Calculated Bank Statement balance') { + calculatedBankStatementBalance = r.debit - r.credit + } + }) + + return { + bankStatementBalanceAsPerGL, + outstandingChecksDebit, + outstandingChecksCredit, + incorrectlyClearedEntriesDebit, + incorrectlyClearedEntriesCredit, + calculatedBankStatementBalance + } + + }, [data]) + + const currency = bankAccount?.account_currency ?? getCompanyCurrency(company) + + return
      + + {_("Bank Statement Balance as per General Ledger")} + {formatCurrency(bankStatementBalanceAsPerGL, currency)} + + + + {_("Outstanding Checks and Deposits to clear")} + {formatCurrency(outstandingChecksDebit - outstandingChecksCredit, currency)} + + + {(incorrectlyClearedEntriesDebit > 0 || incorrectlyClearedEntriesCredit > 0) && + {_("Checks and Deposits incorrectly cleared")} + {formatCurrency(incorrectlyClearedEntriesDebit - incorrectlyClearedEntriesCredit)} + {/*
      }> + {incorrectlyClearedEntriesDebit !== 0 && Debit: {formatCurrency(incorrectlyClearedEntriesDebit)}} + {incorrectlyClearedEntriesCredit !== 0 && Credit: {formatCurrency(incorrectlyClearedEntriesCredit)}} +
      */} +
      } + + {_("Calculated Bank Statement Balance")} + {formatCurrency(calculatedBankStatementBalance)} + + +
      +} + +export default BankReconciliationStatement diff --git a/banking/src/components/features/BankReconciliation/BankTransactionList.tsx b/banking/src/components/features/BankReconciliation/BankTransactionList.tsx new file mode 100644 index 00000000000..05e35f0f289 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/BankTransactionList.tsx @@ -0,0 +1,419 @@ +import { useAtomValue, useSetAtom } from "jotai" +import { MissingFiltersBanner } from "./MissingFiltersBanner" +import { bankRecDateAtom, bankRecUnreconcileModalAtom, selectedBankAccountAtom } from "./bankRecAtoms" +import { Paragraph } from "@/components/ui/typography" +import { formatDate } from "@/lib/date" +import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view" +import { formatCurrency, getCurrencyFormatInfo } from "@/lib/numbers" +import { getCompanyCurrency } from "@/lib/company" +import { ArrowDownRight, ArrowUpRight, CheckCircle2, ChevronDown, DollarSign, ExternalLink, ImportIcon, ListIcon, Search, Undo2, XCircle } from "lucide-react" +import ErrorBanner from "@/components/ui/error-banner" +import { Badge } from "@/components/ui/badge" +import { useGetBankTransactions } from "./utils" +import { BankTransaction } from "@/types/Accounts/BankTransaction" +import { Button } from "@/components/ui/button" +import _ from "@/lib/translate" +import { Input } from "@/components/ui/input" +import CurrencyInput from "react-currency-input-field" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { getCurrencySymbol } from "@/lib/currency" +import { useDebounceValue } from "usehooks-ts" +import type { ColumnDef } from "@tanstack/react-table" +import { useCallback, useMemo, useState } from "react" +import { Link } from "react-router" +import { Empty, EmptyTitle, EmptyHeader, EmptyMedia, EmptyDescription } from "@/components/ui/empty" +import { InputGroup, InputGroupAddon } from "@/components/ui/input-group" + +const BankTransactions = () => { + const selectedBank = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + + if (!selectedBank || !dates) { + return + } + + return <> + + +} + +const BankTransactionListView = () => { + + const { data, error } = useGetBankTransactions() + + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + + const formattedFromDate = formatDate(dates.fromDate) + const formattedToDate = formatDate(dates.toDate) + + const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom) + + const onUndo = useCallback( + (transaction: BankTransaction) => { + setBankRecUnreconcileModalAtom(transaction.name) + }, + [setBankRecUnreconcileModalAtom], + ) + + const accountCurrency = useMemo( + () => bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ""), + [bankAccount?.account_currency, bankAccount?.company], + ) + + const transactionColumns = useMemo[]>( + () => [ + { + accessorKey: "date", + header: _("Date"), + size: 112, + meta: { tabularNums: true } satisfies ListViewColumnMeta, + cell: ({ row }) => formatDate(row.original.date), + }, + { + accessorKey: "description", + header: _("Description"), + size: 250, + // meta: { gridWidth: "minmax(0,2fr)" } satisfies ListViewColumnMeta, + cell: ({ row }) => row.original.description, + }, + { + accessorKey: "reference_number", + header: _("Reference #"), + size: 128, + cell: ({ row }) => row.original.reference_number, + }, + { + accessorKey: "withdrawal", + header: _("Withdrawal"), + size: 120, + meta: { align: "right" } satisfies ListViewColumnMeta, + cell: ({ row }) => {formatCurrency(row.original.withdrawal, accountCurrency)}, + }, + { + accessorKey: "deposit", + header: _("Deposit"), + size: 120, + meta: { align: "right" } satisfies ListViewColumnMeta, + cell: ({ row }) => {formatCurrency(row.original.deposit, accountCurrency)}, + }, + { + accessorKey: "unallocated_amount", + header: _("Unallocated"), + size: 120, + meta: { align: "right" } satisfies ListViewColumnMeta, + cell: ({ row }) => {formatCurrency(row.original.unallocated_amount, accountCurrency)}, + }, + { + accessorKey: "transaction_type", + header: _("Type"), + size: 112, + cell: ({ row }) => + row.original.transaction_type ? {row.original.transaction_type} : null, + }, + { + id: "status", + header: _("Status"), + size: 168, + meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta, + cell: ({ row }) => { + const tx = row.original + if (!tx.allocated_amount || (tx.allocated_amount && tx.allocated_amount === 0)) { + return ( + + + {_("Not Reconciled")} + + ) + } + if (tx.allocated_amount && tx.allocated_amount > 0 && tx.unallocated_amount !== 0) { + return ( + + + {_("Partially Reconciled")} + + ) + } + return ( + + + {_("Reconciled")} + + ) + }, + }, + { + id: "actions", + header: _("Actions"), + size: 200, + enableResizing: false, + meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta, + cell: ({ row }) => ( +
      + + {row.original.allocated_amount && row.original.allocated_amount > 0 ? ( + + ) : null} +
      + ), + }, + ], + [_, accountCurrency, onUndo], + ) + + const [search, setSearch] = useDebounceValue('', 250) + const [amountFilter, setAmountFilter] = useState<{ value: number, stringValue?: string | number }>({ value: 0, stringValue: '0.00' }) + const [typeFilter, setTypeFilter] = useState('All') + const [status, setStatus] = useState<'Reconciled' | 'Unreconciled' | 'All' | 'Partially Reconciled'>('All') + + const onSearchChange = (e: React.ChangeEvent) => { + setSearch(e.target.value) + } + + const filteredResults = useMemo(() => { + if (!data) { + return [] + } + + return data.message.filter((transaction) => { + + if (search && !transaction.description?.toLowerCase().includes(search.toLowerCase())) { + return false + } + + if (typeFilter !== 'All') { + if (typeFilter === 'Debits' && transaction.deposit && transaction.deposit > 0) { + return false + } + if (typeFilter === 'Credits' && transaction.withdrawal && transaction.withdrawal > 0) { + return false + } + } + + if (status !== 'All') { + if (status === 'Reconciled' && transaction.status !== 'Reconciled') { + return false + } + if (status === 'Unreconciled') { + if (transaction.status === 'Reconciled') { + return false + } + // Filter out partially reconciled transactions + if (transaction.allocated_amount && transaction.allocated_amount > 0 && transaction.unallocated_amount !== 0) { + return false + } + } + if (status === 'Partially Reconciled') { + + if (transaction.status === 'Reconciled') { + return false + } + if ((transaction.allocated_amount ?? 0) === 0) { + return false + } + } + + } + + if (amountFilter.value > 0 && transaction.withdrawal !== amountFilter.value && transaction.deposit !== amountFilter.value) { + return false + } + + return true + }) + + + }, [data, search, amountFilter, typeFilter, status]) + + return
      + +
      + + ${bankAccount?.account_name}`, `${formattedFromDate}`, `${formattedToDate}`]) + }} /> + + + +
      + + {error && } + + {data && data.message.length > 0 && } + + {data && data.message.length > 0 ? ( + row.name} + maxHeight="calc(100vh - 200px)" + scrollAreaClassName="min-h-[calc(100vh-200px)]" + emptyState={ + + + + + {_("No bank transactions found")} + {_("There are no transactions in the system for the selected bank account and dates that match the filters.")} + + } + /> + ) : null} + + +
      +} + +interface FilterProps { + onSearchChange: (e: React.ChangeEvent) => void + search: string + results: BankTransaction[] + setAmountFilter: (value: { value: number, stringValue?: string | number }) => void + amountFilter: { value: number, stringValue?: string | number } + onTypeFilterChange: (type: string) => void + typeFilter: string + status: 'Reconciled' | 'Unreconciled' | 'All' | 'Partially Reconciled' + setStatus: (status: 'Reconciled' | 'Unreconciled' | 'All' | 'Partially Reconciled') => void +} + + +const Filters = ({ + onSearchChange, + search, + results, + setAmountFilter, + amountFilter, + onTypeFilterChange, + typeFilter, + status, + setStatus, + +}: FilterProps) => { + const bankAccount = useAtomValue(selectedBankAccountAtom) + + const currency = bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? '') + const currencySymbol = getCurrencySymbol(currency) + const formatInfo = getCurrencyFormatInfo(currency) + const groupSeparator = formatInfo.group_sep || "," + const decimalSeparator = formatInfo.decimal_str || "." + + return
      + + + + + + + + {results?.length} {_(results?.length === 1 ? "result" : "results")} + + + +
      + + { + // If the input ends with a decimal or a decimal with trailing zeroes, store the string since we need the user to be able to type the decimals. + // When the user eventually types the decimals or blurs out, the value is formatted anyway. + // Otherwise store the float value + // Check if the value ends with a decimal or a decimal with trailing zeroes + const isDecimal = v?.endsWith(decimalSeparator) || v?.endsWith(decimalSeparator + '0') + const newValue = isDecimal ? v : values?.float ?? '' + setAmountFilter({ + value: Number(newValue), + stringValue: newValue + }) + }} + // @ts-expect-error - CurrencyInputProps doesn't have a variant prop but Input does + variant={"outline"} + customInput={Input} + /> +
      +
      + + + + + + onTypeFilterChange('All')}> {_("All")} + onTypeFilterChange('Debits')}> {_("Debits")} + onTypeFilterChange('Credits')}> {_("Credits")} + + +
      +
      + + + + + + setStatus('All')}>{} {_("All")} + setStatus('Reconciled')}>{} {_("Reconciled")} + setStatus('Unreconciled')}>{} {_("Unreconciled")} + setStatus('Partially Reconciled')}>{} {_("Partially Reconciled")} + + +
      +
      +} + +export default BankTransactions diff --git a/banking/src/components/features/BankReconciliation/BankTransactionUnreconcileModal.tsx b/banking/src/components/features/BankReconciliation/BankTransactionUnreconcileModal.tsx new file mode 100644 index 00000000000..a9996a2ddef --- /dev/null +++ b/banking/src/components/features/BankReconciliation/BankTransactionUnreconcileModal.tsx @@ -0,0 +1,125 @@ +import { AlertDialog, AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from "@/components/ui/alert-dialog" +import { useAtom, useAtomValue } from "jotai" +import { bankRecDateAtom, bankRecUnreconcileModalAtom, selectedBankAccountAtom } from "./bankRecAtoms" +import { useMemo } from "react" +import { useFrappeGetDoc, useFrappePostCall, useSWRConfig } from "frappe-react-sdk" +import { BankTransaction } from "@/types/Accounts/BankTransaction" +import { toast } from "sonner" +import ErrorBanner from "@/components/ui/error-banner" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { formatCurrency } from "@/lib/numbers" +import { Badge } from "@/components/ui/badge" +import { slug } from "@/lib/frappe" +import SelectedTransactionDetails from "./SelectedTransactionDetails" +import _ from "@/lib/translate" + +const BankTransactionUnreconcileModal = () => { + + const [unreconcileModal, setBankRecUnreconcileModal] = useAtom(bankRecUnreconcileModalAtom) + + const onOpenChange = (v: boolean) => { + if (!v) { + setBankRecUnreconcileModal('') + } + } + + return + + + + {_("Undo Transaction Reconciliation")} + + {_("Are you sure you want to unreconcile this transaction?")} + + + + + + + +} + +const BankTransactionUnreconcileModalContent = () => { + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + + const { mutate } = useSWRConfig() + + const [unreconcileModal, setBankRecUnreconcileModal] = useAtom(bankRecUnreconcileModalAtom) + + const { data: transaction, error } = useFrappeGetDoc('Bank Transaction', unreconcileModal) + + const { call, loading, error: unreconcileError } = useFrappePostCall('erpnext.accounts.doctype.bank_transaction.bank_transaction.unreconcile_transaction') + + const onUnreconcile = (event: React.MouseEvent) => { + call({ + transaction_name: unreconcileModal + }).then(() => { + // Mutate the transactions list, unreconciled transactions list and account closing balance + mutate(`bank-reconciliation-bank-transactions-${bankAccount?.name}-${dates.fromDate}-${dates.toDate}`) + mutate(`bank-reconciliation-unreconciled-transactions-${bankAccount?.name}-${dates.fromDate}-${dates.toDate}`) + mutate(`bank-reconciliation-account-closing-balance-${bankAccount?.name}-${dates.toDate}`) + toast.success(_("Transaction Unreconciled")) + setBankRecUnreconcileModal('') + }) + + event.preventDefault() + } + + const vouchersWhichWillBeCancelled = useMemo(() => { + return transaction?.payment_entries?.filter((payment) => payment.reconciliation_type === 'Voucher Created') + }, [transaction]) + + return
      +
      + {error && } + {unreconcileError && } + {transaction && } + {_("This transaction has been reconciled with the following document(s):")} + + + + {_("Document")} + {_("Amount")} + {_("Reconciliation Type")} + + + + {transaction?.payment_entries?.map((voucher) => { + return + + + {`${_(voucher.payment_document)}: ${voucher.payment_entry}`} + + + {formatCurrency(voucher.allocated_amount)} + {voucher.reconciliation_type === 'Voucher Created' ? + {_(voucher.reconciliation_type)} : + {_(voucher.reconciliation_type ?? "Matched")}} + + })} + +
      +
      + {vouchersWhichWillBeCancelled && vouchersWhichWillBeCancelled?.length > 0 && The following documents will be cancelled:} + {vouchersWhichWillBeCancelled && vouchersWhichWillBeCancelled?.length > 0 &&
        + {vouchersWhichWillBeCancelled?.map((voucher) => { + return
      1. {_(voucher.payment_document)}: {voucher.payment_entry}
      2. + })} +
      } +
      +
      + + {_("Cancel")} + + {_("Unreconcile")} + + +
      +} + +export default BankTransactionUnreconcileModal \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/CompanySelector.tsx b/banking/src/components/features/BankReconciliation/CompanySelector.tsx new file mode 100644 index 00000000000..5496ec9f851 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/CompanySelector.tsx @@ -0,0 +1,92 @@ +import { Button } from "@/components/ui/button" +import { selectedCompanyAtom, useCurrentCompany } from "@/hooks/useCurrentCompany" +import { useSetAtom } from "jotai" +import { Building2, Check, ChevronDown } from "lucide-react" +import { useState } from "react" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { cn } from "@/lib/utils" +import _ from "@/lib/translate" +import { selectedBankAccountAtom } from "./bankRecAtoms" + +const CompanySelector = ({ onChange }: { onChange?: (company: string) => void }) => { + const [open, setOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const options = window.frappe?.boot?.docs?.filter((doc: Record) => doc.doctype === ":Company").map((company: Record) => company.name) || [] + + const setSelectedCompany = useSetAtom(selectedCompanyAtom) + const setSelectedBankAccount = useSetAtom(selectedBankAccountAtom) + const selectedCompany = useCurrentCompany() + + const handleSelectCompany = (company: string) => { + setSelectedCompany(company) + setSearchQuery("") + setOpen(false) + // Only reset bank account if the company is changed + if (selectedCompany !== company) { + setSelectedBankAccount(null) + onChange?.(company) + } + } + + return ( + + + + + + {options.length > 5 && } + + {_("No company found.")} + + {options.map((option: string) => ( + { + handleSelectCompany(currentValue) + }} + > + {option} + + + ))} + + + + + ) +} + +export default CompanySelector \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/IncorrectlyClearedEntries.tsx b/banking/src/components/features/BankReconciliation/IncorrectlyClearedEntries.tsx new file mode 100644 index 00000000000..58940aa6f91 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/IncorrectlyClearedEntries.tsx @@ -0,0 +1,229 @@ +import { useAtomValue } from "jotai" +import { MissingFiltersBanner } from "./MissingFiltersBanner" +import { bankRecDateAtom, selectedBankAccountAtom } from "./bankRecAtoms" +import { useCurrentCompany } from "@/hooks/useCurrentCompany" +import { Paragraph } from "@/components/ui/typography" +import type { ColumnDef } from "@tanstack/react-table" +import { useCallback, useMemo } from "react" +import { useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk" +import { QueryReportReturnType } from "@/types/custom/Reports" +import { formatDate } from "@/lib/date" +import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view" +import { formatCurrency } from "@/lib/numbers" +import { getCompanyCurrency } from "@/lib/company" +import { getErrorMessage, slug } from "@/lib/frappe" +import { Button } from "@/components/ui/button" +import { toast } from "sonner" +import { PartyPopper } from "lucide-react" +import ErrorBanner from "@/components/ui/error-banner" +import _ from "@/lib/translate" +import { Empty, EmptyTitle, EmptyDescription, EmptyMedia, EmptyHeader } from "@/components/ui/empty" + +const IncorrectlyClearedEntries = () => { + const companyID = useCurrentCompany() + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + + if (!companyID || !bankAccount || !dates) { + const missingFields = [] + if (!companyID) { + missingFields.push('Company') + } + if (!bankAccount) { + missingFields.push('Bank Account') + } + if (!dates) { + missingFields.push('Dates') + } + return + } + + return +} + +interface IncorrectlyClearedEntry { + payment_document: string + payment_entry: string + debit: number + credit: number + posting_date: string, + clearance_date: string, +} + +const IncorrectlyClearedEntriesView = () => { + + const companyID = useCurrentCompany() + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + + const filters = useMemo(() => { + return JSON.stringify({ + company: companyID, + account: bankAccount?.account, + report_date: dates.toDate + }) + }, [companyID, bankAccount, dates]) + + const { data, error, mutate } = useFrappeGetCall<{ message: QueryReportReturnType }>('frappe.desk.query_report.run', { + report_name: 'Cheques and Deposits Incorrectly cleared', + filters, + ignore_prepared_report: 1, + are_default_filters: false, + }, `Report-Cheques and Deposits Incorrectly cleared-${filters}`, { keepPreviousData: true, revalidateOnFocus: false }, 'POST') + + const formattedToDate = formatDate(dates.toDate) + + const { call: clearClearingDate } = useFrappePostCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.clear_clearing_date') + + const onClearClick = useCallback( + (voucher_type: string, voucher_name: string) => { + clearClearingDate({ voucher_type, voucher_name }) + .then(() => { + toast.success(_("Cleared"), { + duration: 1000, + }) + mutate() + }) + .catch((e) => { + toast.error(_("There was an error while performing the action."), { + description: getErrorMessage(e), + duration: 5000, + }) + }) + }, + [clearClearingDate, mutate, _], + ) + + const accountCurrency = useMemo( + () => bankAccount?.account_currency ?? getCompanyCurrency(companyID), + [bankAccount?.account_currency, companyID], + ) + + const incorrectlyClearedColumns = useMemo[]>( + () => [ + { + accessorKey: "payment_document", + header: _("Document Type"), + size: 128, + cell: ({ row }) => _(row.original.payment_document), + }, + { + id: "payment_entry", + header: _("Payment Document"), + size: 160, + meta: { + getTooltipText: (r) => { + const x = r as IncorrectlyClearedEntry + return [x.payment_document, x.payment_entry].filter(Boolean).join(" · ") || undefined + }, + } satisfies ListViewColumnMeta, + cell: ({ row }) => ( + + {row.original.payment_entry} + + ), + }, + { + accessorKey: "debit", + header: _("Debit"), + size: 120, + meta: { align: "right" } satisfies ListViewColumnMeta, + cell: ({ row }) => formatCurrency(row.original.debit, accountCurrency), + }, + { + accessorKey: "credit", + header: _("Credit"), + size: 120, + meta: { align: "right" } satisfies ListViewColumnMeta, + cell: ({ row }) => formatCurrency(row.original.credit, accountCurrency), + }, + { + accessorKey: "posting_date", + header: _("Posting Date"), + size: 118, + meta: { tabularNums: true } satisfies ListViewColumnMeta, + cell: ({ row }) => formatDate(row.original.posting_date), + }, + { + accessorKey: "clearance_date", + header: _("Clearance Date"), + size: 118, + meta: { tabularNums: true } satisfies ListViewColumnMeta, + cell: ({ row }) => formatDate(row.original.clearance_date), + }, + { + id: "actions", + header: _("Actions"), + size: 180, + enableResizing: false, + meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta, + cell: ({ row }) => ( + + ), + }, + ], + [_, accountCurrency, onClearClick], + ) + + return
      + +
      + + clearance date is before the posting date which is incorrect.") + }} /> +
      + {data && data.message.result.length > 0 && + ${formattedToDate}`, `${formattedToDate}`]) + }} /> +
      + {_("You can reset the clearing dates of these entries here.")} +
      } +
      +
      + + {error && } + + {data && data.message.result.length > 0 && ( +
      +

      {_("Incorrectly cleared entries as per the report.")}

      + `${row.payment_entry}-${row.posting_date}`} + maxHeight="min(70vh, 640px)" + emptyState={_("No rows to display.")} + /> +
      + )} + + {data && data.message.result.length === 0 && + + + + + + {_("It's all good!")} + {_("There are no entries in the system where the clearance date is before the posting date.")} + + + } + + +
      +} + +export default IncorrectlyClearedEntries diff --git a/banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx b/banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx new file mode 100644 index 00000000000..006668ac50d --- /dev/null +++ b/banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx @@ -0,0 +1,949 @@ +import { useAtom, useAtomValue, useSetAtom } from "jotai" +import { bankRecAmountFilter, bankRecDateAtom, bankRecRecordJournalEntryModalAtom, bankRecRecordPaymentModalAtom, bankRecSelectedTransactionAtom, bankRecTransactionTypeFilter, bankRecTransferModalAtom, selectedBankAccountAtom } from "./bankRecAtoms" +import { H4 } from "@/components/ui/typography" +import { useMemo, useRef } from "react" +import { getCompanyCurrency } from "@/lib/company" +import ErrorBanner from "@/components/ui/error-banner" +import { Separator } from "@/components/ui/separator" +import Fuse from 'fuse.js' +import { getSearchResults, LinkedPayment, UnreconciledTransaction, useGetRuleForTransaction, useGetUnreconciledTransactions, useGetVouchersForTransaction, useIsTransactionWithdrawal, useReconcileTransaction, useTransactionSearch } from "./utils" +import { Input } from "@/components/ui/input" +import { AlertCircleIcon, ArrowDownRight, ArrowRightIcon, ArrowRightLeft, ArrowUpRight, BadgeCheck, ChevronDown, DollarSign, Landmark, LandmarkIcon, ListIcon, Loader2, Receipt, ReceiptIcon, Search, User, XCircle, ZapIcon } from "lucide-react" +import { cn } from "@/lib/utils" +import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu" +import { Button } from "@/components/ui/button" +import CurrencyInput from 'react-currency-input-field' +import { getCurrencySymbol } from "@/lib/currency" +import { Virtuoso } from 'react-virtuoso' +import { formatDate } from "@/lib/date" +import { Badge } from "@/components/ui/badge" +import { formatCurrency, getCurrencyFormatInfo } from "@/lib/numbers" +import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "@/components/ui/tooltip" +import { Skeleton } from "@/components/ui/skeleton" +import { slug } from "@/lib/frappe" +import _ from "@/lib/translate" +import TransferModal from "./TransferModal" +import BankEntryModal from "./BankEntryModal" +import RecordPaymentModal from "./RecordPaymentModal" +import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import SelectedTransactionsTable from "./SelectedTransactionsTable" +import MatchFilters from "./MatchFilters" +import { useHotkeys } from "react-hotkeys-hook" +import { KeyboardMetaKeyIcon } from "@/components/ui/keyboard-keys" +import { Kbd, KbdGroup } from "@/components/ui/kbd" +import { useFrappeGetCall } from "frappe-react-sdk" +import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty" +import { Link } from "react-router" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { InputGroup, InputGroupAddon, InputGroupText } from "@/components/ui/input-group" + +const MatchAndReconcile = ({ contentHeight }: { contentHeight: number }) => { + const selectedBank = useAtomValue(selectedBankAccountAtom) + + if (!selectedBank) { + return + + + + + {_("Select a bank account to reconcile")} + + + } + + return <> +
      +
      +

      {_("Unreconciled Transactions")}

      + +
      + +
      +

      {_("Match or Create")}

      + +
      +
      + + + + +} + + +const UnreconciledTransactions = ({ contentHeight }: { contentHeight: number }) => { + const bankAccount = useAtomValue(selectedBankAccountAtom) + + const currency = bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? '') + const currencySymbol = getCurrencySymbol(currency) + const formatInfo = getCurrencyFormatInfo(currency) + const groupSeparator = formatInfo.group_sep || "," + const decimalSeparator = formatInfo.decimal_str || "." + + const inputRef = useRef(null) + + const { data: unreconciledTransactions, isLoading, error } = useGetUnreconciledTransactions() + + const [typeFilter, setTypeFilter] = useAtom(bankRecTransactionTypeFilter) + const [amountFilter, setAmountFilter] = useAtom(bankRecAmountFilter) + + const [search, setSearch] = useTransactionSearch() + + const searchIndex = useMemo(() => { + + if (!unreconciledTransactions) { + return null + } + + return new Fuse(unreconciledTransactions.message, { + keys: ['description', 'reference_number'], + threshold: 0.5, + includeScore: true + }) + }, [unreconciledTransactions]) + + const results = useMemo(() => { + + return getSearchResults(searchIndex, search, typeFilter, amountFilter.value, unreconciledTransactions?.message) + + }, [searchIndex, search, typeFilter, amountFilter.value, unreconciledTransactions?.message]) + + const setSelectedTransaction = useSetAtom(bankRecSelectedTransactionAtom(bankAccount?.name || '')) + + const onFilterChange = () => { + setSelectedTransaction([]) + } + + const onSearchChange = (e: React.ChangeEvent) => { + setSearch(e.target.value) + onFilterChange() + } + + const onTypeFilterChange = (type: string) => { + setTypeFilter(type) + onFilterChange() + } + + const onClearFilters = () => { + setSearch('') + if (inputRef.current) { + inputRef.current.value = '' + } + setTypeFilter('All') + setAmountFilter({ value: 0, stringValue: '' }) + onFilterChange() + } + + const hasFilters = search !== '' || typeFilter !== 'All' || amountFilter.value !== 0 + + if (isLoading) { + return + } + + return
      +
      + + + + + + + + + {results?.length} {_(results?.length === 1 ? "result" : "results")} + + +
      + + { + // If the input ends with a decimal or a decimal with trailing zeroes, store the string since we need the user to be able to type the decimals. + // When the user eventually types the decimals or blurs out, the value is formatted anyway. + // Otherwise store the float value + // Check if the value ends with a decimal or a decimal with trailing zeroes + const isDecimal = v?.endsWith(decimalSeparator) || v?.endsWith(decimalSeparator + '0') + const newValue = isDecimal ? v : values?.float ?? '' + const nextAmountFilter = { + value: Number(newValue), + stringValue: newValue + } + const hasAmountFilterChanged = amountFilter.value !== nextAmountFilter.value || amountFilter.stringValue !== nextAmountFilter.stringValue + + setAmountFilter(nextAmountFilter) + + // `onValueChange` also fires on blur; avoid clearing selected transaction unless filter value actually changed. + if (hasAmountFilterChanged) { + onFilterChange() + } + }} + // @ts-expect-error - CurrencyInputProps doesn't have a variant prop but Input does + variant={"outline"} + customInput={Input} + /> +
      +
      + + + + + + onTypeFilterChange('All')}> {_("All")} + onTypeFilterChange('Debits')}> {_("Debits")} + onTypeFilterChange('Credits')}> {_("Credits")} + + +
      +
      + + {error && } + + + + {results.length === 0 && } + + ( + + )} + style={{ minHeight: Math.max(contentHeight - 80, 400) }} + totalCount={results?.length} + /> + +
      +} + +const NoTransactionsFoundBanner = ({ text, description, onClearFilters }: { text: string, description?: string, onClearFilters?: () => void }) => { + + return + + + + + {text} + {description && {description}} + + + {onClearFilters ? : + } + + +} + +const UnreconciledTransactionsLoadingState = () => { + + return
      +
      + + + +
      + {Array.from({ length: 6 }).map((_, index) => ( + + ))} +
      +} + +const UnreconciledTransactionItem = ({ transaction }: { transaction: UnreconciledTransaction }) => { + + const selectedBank = useAtomValue(selectedBankAccountAtom) + + const [selectedTransaction, setSelectedTransaction] = useAtom(bankRecSelectedTransactionAtom(selectedBank?.name || '')) + + const { amount, isWithdrawal } = useIsTransactionWithdrawal(transaction) + + const isSelected = selectedTransaction?.some((t) => t.name === transaction.name) + + const currency = transaction.currency ?? selectedBank?.account_currency ?? getCompanyCurrency(selectedBank?.company ?? '') + + const handleSelectTransaction = (event: React.MouseEvent) => { + // If the user is pressing the shift key, add/remove the transaction from the selected transactions + if (event.shiftKey) { + setSelectedTransaction(isSelected ? selectedTransaction.filter((t) => t.name !== transaction.name) : [...selectedTransaction, transaction]) + } else { + setSelectedTransaction([transaction]) + } + } + + return
      +
      +
      +
      +
      + {formatDate(transaction.date)} + {transaction.transaction_type && + {transaction.transaction_type}} + {transaction.reference_number && + {_("Ref")}: {transaction.reference_number}} + + {transaction.matched_transaction_rule && + {transaction.matched_transaction_rule}} +
      + {transaction.description} +
      +
      + {isWithdrawal ? : } + {amount && amount > 0 && {formatCurrency(amount, currency)}} + {amount !== transaction.unallocated_amount && {formatCurrency(transaction.unallocated_amount, currency)} {_("Unallocated")}} +
      +
      +
      +
      +} + + +const VouchersSection = ({ contentHeight }: { contentHeight: number }) => { + + const selectedBank = useAtomValue(selectedBankAccountAtom) + const selectedTransactions = useAtomValue(bankRecSelectedTransactionAtom(selectedBank?.name || '')) + + + if (selectedTransactions.length === 0) { + return + + + + + {_("Select a transaction to match and reconcile with vouchers")} + + + } + + if (selectedTransactions.length > 1) { + return + } + + return
      + +
      +} + +const useKeyboardShortcuts = () => { + const setTransferModalOpen = useSetAtom(bankRecTransferModalAtom) + const setRecordPaymentModalOpen = useSetAtom(bankRecRecordPaymentModalAtom) + const setRecordJournalEntryModalOpen = useSetAtom(bankRecRecordJournalEntryModalAtom) + + useHotkeys('meta+p', () => { + // + setRecordPaymentModalOpen(true) + }, { + enabled: true, + enableOnFormTags: false, + preventDefault: true + }) + + useHotkeys('meta+b', () => { + // + setRecordJournalEntryModalOpen(true) + }, { + enabled: true, + enableOnFormTags: false, + preventDefault: true + }) + + useHotkeys('meta+i', () => { + // + setTransferModalOpen(true) + }, { + enabled: true, + enableOnFormTags: false, + preventDefault: true + }) + + return { + setTransferModalOpen, + setRecordPaymentModalOpen, + setRecordJournalEntryModalOpen + } +} + +const OptionsForMultipleTransactions = ({ transactions }: { transactions: UnreconciledTransaction[] }) => { + + const { setTransferModalOpen, setRecordPaymentModalOpen, setRecordJournalEntryModalOpen } = useKeyboardShortcuts() + + return
      + + + +
      + {transactions.length} {_(transactions.length === 1 ? _("transaction selected") : _("transactions selected"))} + + {formatCurrency(transactions.reduce((acc, transaction) => acc + (transaction.unallocated_amount ?? 0), 0), transactions[0].currency ?? '')} + +
      +
      +
      + + + + +
      + +
      + + + + + + {_("Record a journal entry for expenses, income or split transactions")} + + + B + + + + + + + + + {_("Record a payment entry against a customer or supplier")} + + + P + + + + + + + + + + {_("Record an internal transfer to another bank/credit card/cash account")} + + + I + + + + +
      +
      +
      +
      +
      +
      + +
      +} + + +const OptionsForSingleTransaction = ({ transaction, contentHeight }: { transaction: UnreconciledTransaction, contentHeight: number }) => { + + const { setTransferModalOpen, setRecordPaymentModalOpen, setRecordJournalEntryModalOpen } = useKeyboardShortcuts() + + return
      + +
      +
      + + + + + + {_("Record a payment entry against a customer or supplier")} + + + P + + + + + + + + + {_("Record a journal entry for expenses, income or split transactions")} + + + B + + + + + + + + + {_("Record an internal transfer to another bank/credit card/cash account")} + + + I + + + +
      + +
      +
      + {transaction.matched_transaction_rule && } + +
      +} + +const RuleAction = ({ transaction }: { transaction: UnreconciledTransaction }) => { + + const { data: rule } = useGetRuleForTransaction(transaction) + const setTransferModalOpen = useSetAtom(bankRecTransferModalAtom) + const setRecordPaymentModalOpen = useSetAtom(bankRecRecordPaymentModalAtom) + const setRecordJournalEntryModalOpen = useSetAtom(bankRecRecordJournalEntryModalAtom) + + if (!rule) { + return null + } + + const getActionIcon = () => { + switch (rule.classify_as) { + case "Bank Entry": + return + case "Payment Entry": + return + case "Transfer": + return + default: + return + } + } + + const getActionStyles = () => { + switch (rule.classify_as) { + case "Bank Entry": + return { + border: "border-outline-blue-3", + bg: "bg-surface-blue-1/50", + text: "text-ink-blue-4", + theme: "blue", + } + case "Payment Entry": + return { + border: "border-outline-green-3", + bg: "bg-surface-green-1/50", + text: "text-ink-green-4", + theme: "green", + } + case "Transfer": + return { + border: "border-outline-violet-3", + bg: "bg-surface-violet-2/50", + text: "text-ink-violet-4", + theme: "violet", + } + default: + return { + border: "border-outline-amber-3", + bg: "bg-surface-amber-1/50", + text: "text-ink-amber-4", + theme: "orange", + } + } + } + + const handleActionClick = () => { + switch (rule.classify_as) { + case "Bank Entry": + setRecordJournalEntryModalOpen(true) + break + case "Payment Entry": + setRecordPaymentModalOpen(true) + break + case "Transfer": + setTransferModalOpen(true) + break + } + } + + const getActionDescription = () => { + switch (rule.classify_as) { + case "Bank Entry": + return _("Create a journal entry for expenses, income or split transactions") + case "Payment Entry": + return _("Record a payment entry against a customer or supplier") + case "Transfer": + return _("Record an internal transfer to another bank/credit card/cash account") + default: + return _("Create a new entry based on the rule") + } + } + + useHotkeys('meta+r', () => { + // + handleActionClick() + }, { + enabled: true, + enableOnFormTags: false, + preventDefault: true + }) + + const styles = getActionStyles() + + return ( + + + +
      +
      + {getActionIcon()} +
      +
      + {rule.rule_name} + + {rule.rule_description || _("Rule matched based on transaction description and other criteria.")} + +
      +
      +
      + + {rule.classify_as} + +
      +
      +
      + +
      +
      + + {_("Recommended Action")} +
      + + {_("Priority")} {rule.priority} + +
      + +
      + + {rule.account && ( +
      + {_("Account")}: + {rule.account} +
      + )} + + {rule.party_type && rule.party && ( +
      + {_("Party")}: + {rule.party} ({_(rule.party_type)}) +
      + )} +
      + +
      + +

      + {getActionDescription()} +

      +
      +
      +
      + ) +} + +const VouchersForTransaction = ({ transaction, contentHeight }: { transaction: UnreconciledTransaction, contentHeight: number }) => { + + const { data: vouchers, isLoading, error } = useGetVouchersForTransaction(transaction) + + if (error) { + return + } + + if (isLoading) { + return
      +
      + + or + +
      + + + + + + +
      + } + + return
      +
      + + or + +
      + {vouchers?.message.length === 0 && + + + + + + {_("No vouchers found for this transaction")} + + } + ( + + )} + style={{ height: contentHeight }} + totalCount={vouchers?.message.length} + /> +
      +} + +const VoucherItem = ({ voucher, index }: { voucher: LinkedPayment, index: number }) => { + + const selectedBank = useAtomValue(selectedBankAccountAtom) + const selectedTransaction = useAtomValue(bankRecSelectedTransactionAtom(selectedBank?.name || '')) + + const { amountMatches, postingDateMatches, referenceDateMatches, referenceMatchesFull, referenceMatchesPartial, isSuggested } = useMemo(() => { + + const transaction = selectedTransaction?.[0] + + // We need to check if the following details match: + // Amount + // Date + // Reference/Description: Full or partial + // Whether this is suggested or not - depends on the above scores + + const amountMatches = voucher.paid_amount === transaction?.unallocated_amount + const postingDateMatches = voucher.posting_date === transaction?.date + const referenceDateMatches = voucher.reference_date === transaction?.date + const referenceMatchesFull = voucher.reference_no === transaction?.reference_number || voucher.reference_no === transaction?.description + + const referenceMatchesPartial = transaction?.reference_number?.includes(voucher.reference_no) || transaction?.description?.includes(voucher.reference_no) + + + const isSuggested = amountMatches && (postingDateMatches || referenceDateMatches || referenceMatchesPartial) && index === 0 + + return { isSelected: false, amountMatches, postingDateMatches, referenceDateMatches, referenceMatchesFull, referenceMatchesPartial, isSuggested: isSuggested } + + }, [voucher, selectedTransaction, index]) + + const { reconcileTransaction, loading } = useReconcileTransaction() + + const onClick = () => { + if (!selectedTransaction) { + return + } + reconcileTransaction(selectedTransaction[0], voucher) + } + + return
      +
      + +
      +
      +
      + {_(voucher.doctype)} + {voucher.name} +
      + {voucher.party && voucher.party_type &&
      + + {_(voucher.party_type)} + {voucher.party} +
      } + +
      +
      +
      {_("Amount")}
      +
      {formatCurrency(voucher.paid_amount, voucher.currency)} {amountMatches ? : }
      +
      + +
      +
      {_("Posted On")}
      +
      {formatDate(voucher.posting_date)} {postingDateMatches ? : }
      +
      + + {voucher.reference_date &&
      +
      {_("Reference Date")}
      +
      {formatDate(voucher.reference_date)} {referenceDateMatches ? : }
      +
      } + +
      + {voucher.reference_no &&
      + + {voucher.reference_no} +    + + + + {referenceMatchesFull ? `${_("Complete Match")}` : referenceMatchesPartial ? `${_("Partial Match")}` : `${_("No Match")}`} + + + {referenceMatchesFull ? `${_("Reference matches the selected transaction")}` : referenceMatchesPartial ? `${_("Reference matches the selected transaction partially")}` : `${_("Reference does not match the selected transaction")}`} + + + +
      } +
      +
      +
      + +
      +
      + + {isSuggested &&
      + {_("Suggested")} +
      } + +
      +
      +} + + +const MatchBadge = ({ matchType, label }: { matchType: 'full' | 'partial' | 'none', label: string }) => { + return + + {matchType === 'full' ? : matchType === 'partial' ? + {_("Partial Match")} : + } + + + {label} + + +} + +const OlderUnreconciledTransactionsBanner = () => { + + // A banner to show when there are unreconciled transactions for the given bank account before the current selected date + const [dates, setDates] = useAtom(bankRecDateAtom) + const selectedBank = useAtomValue(selectedBankAccountAtom) + + const { data } = useFrappeGetCall<{ + message: { + count: number, + oldest_date: string + } + }>("erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_older_unreconciled_transactions", { + bank_account: selectedBank?.name, + from_date: dates.fromDate, + }, undefined, { + revalidateOnFocus: false, + }) + + if (data && data.message.count > 0) { + + return + +
      +
      + {data.message.count > 1 ? ( + {_("There are {0} unreconciled transactions before {1}.", [data.message.count.toString(), formatDate(dates.fromDate)])} + ) : ( + {_("There is one unreconciled transaction before {0}.", [formatDate(dates.fromDate)])} + )} + + {_("The opening balance might not match your bank statement. Would you like to reconcile them?")} + +
      +
      + +
      +
      +
      + } + + return null + +} + +export default MatchAndReconcile \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/MatchFilters.tsx b/banking/src/components/features/BankReconciliation/MatchFilters.tsx new file mode 100644 index 00000000000..2f91cf0a6b0 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/MatchFilters.tsx @@ -0,0 +1,93 @@ +import { Button } from '@/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import _ from '@/lib/translate' +import { FilterIcon } from 'lucide-react' +import { bankRecMatchFilters } from './bankRecAtoms' +import { useAtom } from 'jotai' +import { Switch } from '@/components/ui/switch' +import { Label } from '@/components/ui/label' +import { Separator } from '@/components/ui/separator' +import { useFrappeGetCall } from 'frappe-react-sdk' +import { scrub } from '@/lib/frappe' +import { useMemo } from 'react' + +const MatchFilters = () => { + return ( + + + + + + + + + {_("Configure match filters for vouchers")} + + + +
      + + + + +
      +
      +
      + ) +} + +const MatchFiltersContent = () => { + + const { data } = useFrappeGetCall<{ message: string[] }>("erpnext.accounts.doctype.bank_transaction.bank_transaction.get_doctypes_for_bank_reconciliation", undefined, + "bank_rec_doctypes", { + revalidateOnFocus: false, + revalidateIfStale: false, + revalidateOnReconnect: false, + } + ) + + const doctypes = useMemo(() => { + const STANDARD_DOCTYPES = ["Payment Entry", "Journal Entry", "Purchase Invoice", "Sales Invoice"] + if (data) { + return data.message.map(doctype => ({ + label: doctype, + id: scrub(doctype), + })) + + } else { + return STANDARD_DOCTYPES.map(doctype => ({ + label: doctype, + id: scrub(doctype), + })) + } + }, [data]) + + return ( +
      + {doctypes.map((doctype) => ( + + ))} +
      + ) +} + +const ToggleSwitch = ({ label, id }: { label: string, id: string }) => { + + const [matchFilters, setMatchFilters] = useAtom(bankRecMatchFilters) + + return
      + { + if (checked) { + setMatchFilters([...matchFilters, id]) + } else { + setMatchFilters(matchFilters.filter(filter => filter !== id)) + } + }} /> + +
      +} + +export default MatchFilters \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/MissingFiltersBanner.tsx b/banking/src/components/features/BankReconciliation/MissingFiltersBanner.tsx new file mode 100644 index 00000000000..0510b3412f3 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/MissingFiltersBanner.tsx @@ -0,0 +1,10 @@ +import { Paragraph } from "@/components/ui/typography" +import { cn } from "@/lib/utils" +import { ReactNode } from "react" + + +export const MissingFiltersBanner = ({ text, className }: { text: ReactNode, className?: string }) => { + return
      + {text} +
      +} \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/RecordPaymentModal.tsx b/banking/src/components/features/BankReconciliation/RecordPaymentModal.tsx new file mode 100644 index 00000000000..cebb82cd640 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/RecordPaymentModal.tsx @@ -0,0 +1,1301 @@ +import { atom, useAtom, useAtomValue, useSetAtom } from "jotai" +import { bankRecRecordPaymentModalAtom, bankRecSelectedTransactionAtom, bankRecUnreconcileModalAtom, SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms" +import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogHeader, DialogFooter, DialogClose, DialogTrigger } from "@/components/ui/dialog" +import _ from "@/lib/translate" +import { UnreconciledTransaction, useGetRuleForTransaction, useRefreshUnreconciledTransactions, useUpdateActionLog } from "./utils" +import { useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form" +import { getCompanyCostCenter, getCompanyCurrency } from "@/lib/company" +import { FrappeConfig, FrappeContext, useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk" +import { toast } from "sonner" +import ErrorBanner from "@/components/ui/error-banner" +import { Button } from "@/components/ui/button" +import SelectedTransactionDetails from "./SelectedTransactionDetails" +import { AccountFormField, CurrencyFormField, DataField, DateField, LinkFormField, PartyTypeFormField, SmallTextField } from "@/components/ui/form-elements" +import { Form } from "@/components/ui/form" +import { ChangeEvent, useCallback, useContext, useEffect, useMemo, useState } from "react" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Checkbox } from "@/components/ui/checkbox" +import { AlertCircleIcon, Plus, Trash2 } from "lucide-react" +import { flt, formatCurrency } from "@/lib/numbers" +import { cn } from "@/lib/utils" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { PaymentEntry } from "@/types/Accounts/PaymentEntry" +import { H4 } from "@/components/ui/typography" +import { usePaymentEntryCalculations } from "@/hooks/usePaymentEntryCalculations" +import { MissingFiltersBanner } from "./MissingFiltersBanner" +import { formatDate, today } from "@/lib/date" +import { slug } from "@/lib/frappe" +import MarkdownRenderer from "@/components/ui/markdown" +import { Separator } from "@/components/ui/separator" +import { PaymentEntryDeduction } from "@/types/Accounts/PaymentEntryDeduction" +import { TableLoader } from "@/components/ui/loaders" +import SelectedTransactionsTable from "./SelectedTransactionsTable" +import { useCurrentCompany } from "@/hooks/useCurrentCompany" +import { Label } from "@/components/ui/label" +import { FileDropzone } from "@/components/ui/file-dropzone" +import { BankTransaction } from "@/types/Accounts/BankTransaction" +import FileUploadBanner from "@/components/common/FileUploadBanner" +import { useHotkeys } from "react-hotkeys-hook" + +const RecordPaymentModal = () => { + + const [isOpen, setIsOpen] = useAtom(bankRecRecordPaymentModalAtom) + + return ( + + + + {_("Record Payment")} + + {_("Record a payment entry against a customer or supplier")} + + + + + + ) +} + + +const RecordPaymentModalContent = () => { + + const selectedBankAccount = useAtomValue(selectedBankAccountAtom) + + const selectedTransaction = useAtomValue(bankRecSelectedTransactionAtom(selectedBankAccount?.name ?? '')) + + if (!selectedTransaction || !selectedBankAccount || selectedTransaction.length === 0) { + return
      + {_("No transaction selected")} +
      + } + + if (selectedTransaction.length === 1) { + return + } + + return + +} + +const BulkPaymentEntryForm = ({ transactions }: { transactions: UnreconciledTransaction[] }) => { + + + const setIsOpen = useSetAtom(bankRecRecordPaymentModalAtom) + + const form = useForm<{ + party_type: PaymentEntry['party_type'], + party: PaymentEntry['party'], + party_name: PaymentEntry['party_name'], + /** GL account that's paid from or paid to */ + account: string + mode_of_payment: PaymentEntry['mode_of_payment'] + }>() + + const { call: createPaymentEntry, loading, error } = useFrappePostCall<{ message: { transaction: BankTransaction, payment_entry: PaymentEntry }[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_bulk_payment_entry_and_reconcile') + + const onReconcile = useRefreshUnreconciledTransactions() + + const addToActionLog = useUpdateActionLog() + + const onSubmit = (data: { party_type: PaymentEntry['party_type'], party: PaymentEntry['party'], account: string, mode_of_payment: PaymentEntry['mode_of_payment'] }) => { + + createPaymentEntry({ + bank_transaction_names: transactions.map((transaction) => transaction.name), + party_type: data.party_type, + party: data.party, + account: data.account + }).then(({ message }) => { + + addToActionLog({ + type: 'payment', + timestamp: (new Date()).getTime(), + isBulk: true, + items: message.map((item) => ({ + bankTransaction: item.transaction, + voucher: { + reference_doctype: "Payment Entry", + reference_name: item.payment_entry.name, + reference_no: item.payment_entry.reference_no, + reference_date: item.payment_entry.reference_date, + posting_date: item.payment_entry.posting_date, + party_type: item.payment_entry.party_type, + party: item.payment_entry.party, + doc: item.payment_entry, + } + })), + bulkCommonData: { + party_type: data.party_type, + party: data.party, + account: data.account, + } + }) + + toast.success(_("Payment Recorded"), { + duration: 4000, + closeButton: true, + }) + onReconcile(transactions[transactions.length - 1]) + setIsOpen(false) + }) + } + + const party_type = useWatch({ control: form.control, name: 'party_type' }) + + const party_name = useWatch({ control: form.control, name: 'party_name' }) + + const party = useWatch({ control: form.control, name: 'party' }) + + const { call } = useContext(FrappeContext) as FrappeConfig + + const currentCompany = useCurrentCompany() + + const company = transactions && transactions.length > 0 ? transactions[0].company : (currentCompany ?? '') + + const onPartyChange = (event: ChangeEvent) => { + // Fetch the party and account + if (event.target.value) { + call.get('erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details', { + company: company, + party_type: party_type, + party: event.target.value, + date: today() + }).then((res) => { + form.setValue('party_name', res.message.party_name) + form.setValue('account', res.message.party_account) + }) + } else { + // Clear the party and account + form.setValue('party_name', '') + form.setValue('account', '') + } + + } + + return
      + +
      + + {error && } + + + +
      +
      + +
      +
      + {party_type ? : + } + + +
      + +
      + { + if (party_type === 'Supplier' || party_type === 'Employee' || party_type === 'Shareholder') { + return acc.account_type === 'Payable' + } else if (party_type === 'Customer') { + return acc.account_type === 'Receivable' + } + return true + }} + /> +
      + +
      + +
      + +
      + + + + + + + + +
      +
      + + +} + +const PaymentEntryForm = ({ selectedTransaction, selectedBankAccount }: { selectedTransaction: UnreconciledTransaction, selectedBankAccount: SelectedBank }) => { + + const setIsOpen = useSetAtom(bankRecRecordPaymentModalAtom) + + const onClose = () => { + setIsOpen(false) + } + + const { data: rule } = useGetRuleForTransaction(selectedTransaction) + + const isWithdrawal = (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) ? true : false + + const form = useForm({ + defaultValues: { + payment_type: isWithdrawal ? 'Pay' : 'Receive', + bank_account: selectedTransaction.bank_account, + company: selectedTransaction?.company, + // If the money is paid, it's usually to a supplier. If it's received, it's usually from a customer + party_type: rule?.party_type ?? (isWithdrawal ? 'Supplier' : 'Customer'), + party: rule?.party ?? '', + // If the transaction is a withdrawal, set the paid from to the selected bank account + paid_from: isWithdrawal ? selectedBankAccount.account : (rule?.account ?? ''), + // If the transaction is a deposit, set the paid to to the selected bank account + paid_to: !isWithdrawal ? selectedBankAccount.account : (rule?.account ?? ''), + // Set the amount to the amount of the selected transaction + paid_amount: selectedTransaction.unallocated_amount, + base_paid_amount: selectedTransaction.unallocated_amount, + received_amount: selectedTransaction.unallocated_amount, + base_received_amount: selectedTransaction.unallocated_amount, + reference_date: selectedTransaction.date, + posting_date: selectedTransaction.date, + reference_no: (selectedTransaction.reference_number || selectedTransaction.description || '').slice(0, 140), + target_exchange_rate: 1, + source_exchange_rate: 1, + } + }) + + const onReconcile = useRefreshUnreconciledTransactions() + + const setUnpaidInvoiceOpen = useSetAtom(isUnpaidInvoicesButtonOpen) + + useEffect(() => { + if (rule && rule.party && rule.party_type && rule.account) { + setUnpaidInvoiceOpen(true) + } + + }, [rule, setUnpaidInvoiceOpen]) + + const { call: createPaymentEntry, loading, error, isCompleted } = useFrappePostCall<{ message: { transaction: BankTransaction, payment_entry: PaymentEntry } }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_and_reconcile') + + const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom) + + const addToActionLog = useUpdateActionLog() + + const { file: frappeFile } = useContext(FrappeContext) as FrappeConfig + + const [isUploading, setIsUploading] = useState(false) + const [uploadProgress, setUploadProgress] = useState(0) + + const [files, setFiles] = useState([]) + + const onSubmit = (data: PaymentEntry) => { + + createPaymentEntry({ + bank_transaction_name: selectedTransaction.name, + payment_entry_doc: { + ...data, + custom_remarks: data.remarks ? true : false + } + }).then(async ({ message }) => { + addToActionLog({ + type: 'payment', + timestamp: (new Date()).getTime(), + isBulk: false, + items: [ + { + bankTransaction: message.transaction, + voucher: { + reference_doctype: "Payment Entry", + reference_name: message.payment_entry.name, + reference_no: message.payment_entry.reference_no, + reference_date: message.payment_entry.reference_date, + posting_date: message.payment_entry.posting_date, + doc: message.payment_entry, + } + } + ] + }) + toast.success(_("Payment Entry Created"), { + duration: 4000, + closeButton: true, + action: { + label: _("Undo"), + onClick: () => setBankRecUnreconcileModalAtom(selectedTransaction.name) + }, + actionButtonStyle: { + backgroundColor: "rgb(0, 138, 46)" + } + }) + + if (files.length > 0) { + setIsUploading(true) + + const uploadPromises = files.map(f => { + return frappeFile.uploadFile(f, { + isPrivate: true, + doctype: "Payment Entry", + docname: message.payment_entry.name, + }, (_bytesUploaded, _totalBytes, progress) => { + + setUploadProgress((currentProgress) => { + //If there are multiple files, we need to add the progress to the current progress + return currentProgress + ((progress?.progress ?? 0) / files.length) + }) + + }) + }) + + return Promise.all(uploadPromises).then(() => { + setUploadProgress(0) + setIsUploading(false) + }) + } else { + return Promise.resolve() + } + + }).then(() => { + setUploadProgress(0) + setIsUploading(false) + onReconcile(selectedTransaction) + onClose() + }) + } + + + useHotkeys('meta+s', () => { + form.handleSubmit(onSubmit)() + }, { + enabled: true, + preventDefault: true, + enableOnFormTags: true + }) + + if (isUploading && isCompleted) { + return + } + + return
      + +
      + {error && } +
      + +
      +

      {isWithdrawal ? _("Paid to") : _("Received from")}

      +
      +
      + +
      +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      +
      + + + + + + + + + + + +
      +
      + +
      + + +
      + +
      + + +
      +
      + + +
      + + + + + + +
      +
      + +} + +const isUnpaidInvoicesButtonOpen = atom(false) + +const PartyField = () => { + + const { control, setValue } = useFormContext() + + const party_type = useWatch({ + control, + name: `party_type` + }) + + const { call } = useContext(FrappeContext) as FrappeConfig + + const company = useWatch({ control, name: 'company' }) + + const party_name = useWatch({ control, name: 'party_name' }) + + const type = useWatch({ control, name: 'payment_type' }) + + const party = useWatch({ control, name: 'party' }) + + const setIsOpen = useSetAtom(isUnpaidInvoicesButtonOpen) + + const onChange = (event: ChangeEvent) => { + // Fetch the party and account + if (event.target.value) { + call.get('erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details', { + company: company, + party_type: party_type, + party: event.target.value, + date: today() + }).then((res) => { + setValue('party_name', res.message.party_name) + if (type === 'Pay') { + setValue('paid_to', res.message.party_account) + } else { + setValue('paid_from', res.message.party_account) + } + setIsOpen(true) + }) + } else { + // Clear the party and account + setValue('party_name', '') + if (type === 'Pay') { + setValue('paid_to', '') + } else { + setValue('paid_from', '') + } + } + + } + + if (!party_type) { + return + } + + return +} + + +const AccountDropdown = ({ isWithdrawal }: { isWithdrawal: boolean }) => { + + // If it's a withdrawal, then we need to show the "Paid to" account + // If it's a deposit, then we need to show the "Paid from" account + + const { control, setValue } = useFormContext() + + const party_type = useWatch({ control, name: 'party_type' }) + + const setIsOpen = useSetAtom(isUnpaidInvoicesButtonOpen) + + const accountTypes: string[] | undefined = useMemo(() => { + if (party_type === 'Supplier' || party_type === 'Employee' || party_type === 'Shareholder') { + return ['Payable'] + } else if (party_type === 'Customer') { + return ['Receivable'] + } + return undefined + }, [party_type]) + + const onAccountChange = (event: ChangeEvent) => { + if (event.target.value) { + setValue('unallocated_amount', 0) + setValue('total_allocated_amount', 0) + setValue('difference_amount', 0) + setValue('references', []) + setIsOpen(true) + } + } + + + if (isWithdrawal) { + return + + } else { + return + } + +} + + +const InvoicesSection = ({ currency }: { currency: string }) => { + + const { setTotalAllocatedAmount } = usePaymentEntryCalculations() + + const { control } = useFormContext() + const { fields, remove } = useFieldArray({ + control, + name: 'references' + }) + + const [selectedRows, setSelectedRows] = useState([]) + + const onSelectRow = useCallback((index: number) => { + setSelectedRows(prev => { + if (prev.includes(index)) { + return prev.filter(i => i !== index) + } + return [...prev, index] + }) + }, []) + + const onSelectAll = useCallback(() => { + setSelectedRows(prev => { + if (prev.length === fields.length) { + return [] + } + return [...fields.map((_, index) => index)] + }) + }, [fields]) + + const onRemove = useCallback(() => { + remove(selectedRows) + setSelectedRows([]) + }, [remove, selectedRows]) + + return
      +
      +

      {_("Invoices")}

      + +
      + + + + 0 && selectedRows.length === fields.length} + onCheckedChange={onSelectAll} /> + {_("Reference Document")} + {_("Invoice No")} + {_("Due Date")} + {_("Grand Total")} + {_("Outstanding")} + {_("Allocated")} + + + + + {fields.map((field, index) => ( + + + onSelectRow(index)} + // Make this accessible to screen readers + aria-label={_("Select row {0}", [String(index + 1)])} + /> + + + + {field.reference_doctype}: {field.reference_name} + + + {field.bill_no ?? "-"} + + + {formatDate(field.due_date)} + + + {formatCurrency(field.total_amount, currency)} + + + {formatCurrency(field.outstanding_amount, currency)} + + + setTotalAllocatedAmount() + }} + hideLabel + currency={currency} + /> + + + + + + ))} + +
      +
      +
      + {selectedRows.length > 0 &&
      + +
      } +
      + +
      +
      + +} + +const DifferenceButton = ({ index, currency }: { index: number, currency: string }) => { + + const { setTotalAllocatedAmount } = usePaymentEntryCalculations() + + const { control, setValue } = useFormContext() + + const outstandingAmount = useWatch({ + control, + name: `references.${index}.outstanding_amount` + }) ?? 0 + + const allocatedAmount = useWatch({ + control, + name: `references.${index}.allocated_amount` + }) ?? 0 + + const difference = flt(outstandingAmount - allocatedAmount, 2) + + const onPayInFull = useCallback(() => { + setValue(`references.${index}.allocated_amount`, outstandingAmount, { shouldDirty: true }) + setTotalAllocatedAmount() + }, [outstandingAmount, index, setValue, setTotalAllocatedAmount]) + + if (difference !== 0) { + + return + + + + + {_("The invoice is not fully allocated as there is a difference of {0}.", [formatCurrency(difference, currency) ?? ''])} +
      + {_("Click to pay in full.")} +
      +
      + + } + + return null +} + +const Summary = ({ currency }: { currency: string }) => { + + const { control, setValue, getValues } = useFormContext() + + const { setUnallocatedAmount } = usePaymentEntryCalculations() + + const amount = useWatch({ + control, + name: 'paid_amount' + }) + + const unallocatedAmount = useWatch({ + control, + name: 'unallocated_amount' + }) + + const allocatedAmount = useWatch({ + control, + name: 'total_allocated_amount' + }) + + const differenceAmount = useWatch({ + control, + name: 'difference_amount' + }) + + const onAddRow = useCallback((amount?: number) => { + if (amount) { + const deductions = getValues('deductions') ?? [] + + setValue('deductions', [...deductions, { + amount: amount, + account: '', + cost_center: getCompanyCostCenter(getValues('company')), + description: '' + } as PaymentEntryDeduction]) + + setUnallocatedAmount() + } + }, [setUnallocatedAmount, getValues, setValue]) + + const TextComponent = ({ className, children }: { className?: string, children: React.ReactNode }) => { + return {children} + } + + return
      +
      + {_("Total Amount")} + {formatCurrency(amount, currency)} +
      +
      + {_("Allocated")} + {formatCurrency(allocatedAmount, currency)} +
      + + {(unallocatedAmount && unallocatedAmount !== 0) ?
      + {_("Unallocated")} + + + + + + {_("Add a charge to the payment entry with the unallocated amount")} + + + + +
      : null} + + {(differenceAmount && differenceAmount !== 0) ?
      + {_("Difference")} + + + + + + {_("Add a charge to the payment entry with the difference amount")} + + + + +
      : null} + +
      +} +const GetUnpaidInvoicesButton = () => { + + const [isOpen, setIsOpen] = useAtom(isUnpaidInvoicesButtonOpen) + + const { control } = useFormContext() + + const partyType = useWatch({ control, name: 'party_type' }) + const party = useWatch({ control, name: 'party' }) + const partyName = useWatch({ control, name: 'party_name' }) + const amount = useWatch({ control, name: 'paid_amount' }) + + return <> + + + {partyType && party && + + } + + + Select Invoices + Unpaid invoices from {partyName} for {formatCurrency(amount)}. + + setIsOpen(false)} /> + + + +} + +interface OutstandingInvoice { + voucher_type: string + voucher_no: string + bill_no?: string + due_date: string + invoice_amount: number + outstanding_amount: number, + payment_term?: string, + payment_term_outstanding?: string, + account?: string, + allocated_amount?: number, +} +const FetchInvoicesModal = ({ onClose }: { onClose: () => void }) => { + + const { getValues, setValue } = useFormContext() + + const { allocatePartyAmount } = usePaymentEntryCalculations() + + const { data, isLoading, error } = useFrappeGetCall<{ + message: OutstandingInvoice[], + _server_messages?: string + }>('erpnext.accounts.doctype.payment_entry.payment_entry.get_outstanding_reference_documents', { + args: { + company: getValues('company'), + posting_date: getValues('posting_date'), + party_type: getValues('party_type'), + party: getValues('party'), + party_account: getValues('payment_type') === 'Pay' ? getValues('paid_to') : getValues('paid_from'), + get_outstanding_invoices: true, + allocate_payment_amount: 1 + } + }) + + const message = useMemo(() => { + if (data && data._server_messages) { + const message = JSON.parse(JSON.parse(data._server_messages)[0]) + + return message.message + } + return '' + }, [data]) + + const [selectedInvoices, setSelectedInvoices] = useState([]) + + const onSelectRow = (row: OutstandingInvoice) => { + if (selectedInvoices.includes(row)) { + setSelectedInvoices(selectedInvoices.filter((invoice) => invoice !== row)) + } else { + setSelectedInvoices([...selectedInvoices, row]) + } + } + + const { call: allocateAmountToReferences, loading: allocateAmountToReferencesLoading, error: allocateAmountToReferencesError } = useFrappePostCall('run_doc_method') + + const onSelect = () => { + + allocateAmountToReferences({ + args: { + paid_amount: getValues("payment_type") === "Pay" ? getValues("paid_amount") : getValues("received_amount"), + allocate_payment_amount: 1, + paid_amount_change: false + }, + method: 'allocate_amount_to_references', + docs: { + doctype: 'Payment Entry', + ...getValues(), + name: "new-payment-entry-1", + __unsaved: 1, + __islocal: 1, + references: selectedInvoices.map((ref: OutstandingInvoice) => ({ + reference_doctype: ref.voucher_type, + reference_name: ref.voucher_no, + due_date: ref.due_date, + total_amount: ref.invoice_amount, + outstanding_amount: ref.outstanding_amount, + bill_no: ref.bill_no, + payment_term: ref.payment_term, + payment_term_outstanding: ref.payment_term_outstanding, + allocated_amount: ref.allocated_amount, + account: ref.account, + exchange_rate: 1, + })) + } + }).then((res) => { + const doc = res.docs[0] + setValue('references', doc.references) + setValue('unallocated_amount', doc.unallocated_amount) + setValue('total_allocated_amount', doc.total_allocated_amount) + setValue('difference_amount', doc.difference_amount) + + allocatePartyAmount(getValues("payment_type") === "Pay" ? getValues("paid_amount") : getValues("received_amount")) + + onClose() + }) + } + return
      + {isLoading ? : null} + {error && } + {error && } + {message ? } /> : null} + + {data?.message && data?.message?.length > 0 ? + + + + { + if (checked) { + setSelectedInvoices(data?.message) + } else { + setSelectedInvoices([]) + } + }} /> + + + Type + + + Name + + + Invoice No + + + Due Date + + + Grand Total + + + Outstanding + + + + + {data.message.map((ref) => ( + { + const target = e.target as HTMLElement + // Do not select the checkbox if the user clicks on the checkbox or the link + if (target.tagName !== 'INPUT' && !target.className.includes('chakra-checkbox') && !target.className.includes('chakra-link')) { + onSelectRow(ref) + } + }} + className="cursor-pointer"> + + { + if (checked) { + setSelectedInvoices([...selectedInvoices, ref]) + } else { + setSelectedInvoices(selectedInvoices.filter((invoice) => invoice !== ref)) + } + }} + /> + + + {ref.voucher_type} + + + {ref.voucher_no} + + + {ref.bill_no ?? "-"} + + + {formatDate(ref.due_date)} + + + {formatCurrency(ref.invoice_amount)} + + + {formatCurrency(ref.outstanding_amount)} + + + ))} + +
      : null} +
      +
      + Invoices: {selectedInvoices.length} / + Total: {formatCurrency(selectedInvoices.reduce((acc, invoice) => acc + invoice.outstanding_amount, 0))} +
      + + + + + + +
      + +
      +} + + + +const OtherChargesSection = ({ currency }: { currency: string }) => { + + const { setTotalAllocatedAmount } = usePaymentEntryCalculations() + const { getValues, control } = useFormContext() + + const { fields, append, remove } = useFieldArray({ + control: control, + name: 'deductions' + }) + + + const [selectedRows, setSelectedRows] = useState([]) + + const onSelectRow = useCallback((index: number) => { + setSelectedRows(prev => { + if (prev.includes(index)) { + return prev.filter(i => i !== index) + } + return [...prev, index] + }) + }, []) + + const onSelectAll = useCallback(() => { + setSelectedRows(prev => { + if (prev.length === fields.length) { + return [] + } + return [...fields.map((_, index) => index)] + }) + }, [fields]) + + const onRemove = useCallback(() => { + remove(selectedRows) + setSelectedRows([]) + setTotalAllocatedAmount() + }, [remove, selectedRows, setTotalAllocatedAmount]) + + const onAdd = () => { + + append({ + account: '', + cost_center: getCompanyCostCenter(getValues('company')), + description: '', + amount: 0 + } as PaymentEntryDeduction) + + + } + + return
      +
      +

      Other Charges / Deductions

      + +
      + + + + 0 && selectedRows.length === fields.length} + onCheckedChange={onSelectAll} /> + {_("Account")} * + {_("Cost Center")} * + {_("Description")} + {_("Amount")} * + + + + {fields.map((field, index) => ( + + + onSelectRow(index)} + // Make this accessible to screen readers + aria-label={_("Select row {0}", [String(index + 1)])} + /> + + + + + + + + + + + + + { + setTotalAllocatedAmount() + } + }} + /> + + + ))} + +
      +
      +
      +
      + +
      + {selectedRows.length > 0 &&
      + +
      } +
      +
      +
      +} + +const TotalDeductions = ({ currency }: { currency: string }) => { + + const { control } = useFormContext() + + const total_deductions = useWatch({ control, name: 'deductions' })?.reduce((acc: number, row: PaymentEntryDeduction) => acc + row.amount, 0) ?? 0 + + return ({formatCurrency(total_deductions, currency)}) +} +export default RecordPaymentModal \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/Rules/CreateNewRule.tsx b/banking/src/components/features/BankReconciliation/Rules/CreateNewRule.tsx new file mode 100644 index 00000000000..1f2d29c630e --- /dev/null +++ b/banking/src/components/features/BankReconciliation/Rules/CreateNewRule.tsx @@ -0,0 +1,89 @@ +import { Button } from "@/components/ui/button" +import ErrorBanner from "@/components/ui/error-banner" +import { Form } from "@/components/ui/form" +import { useCurrentCompany } from "@/hooks/useCurrentCompany" +import _ from "@/lib/translate" +import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule" +import { useFrappeCreateDoc } from "frappe-react-sdk" +import { toast } from "sonner" +import { RuleForm } from "./RuleForm" +import { useForm } from "react-hook-form" +import { SettingsPanelHeader, SettingsPanelDescription, SettingsPanelTitle, SettingsPanelContent } from "@/components/ui/settings-dialog" +import { useHotkeys } from "react-hotkeys-hook" + +type Props = { + onCreate: VoidFunction +} + +const CreateNewRule = ({ onCreate }: Props) => { + + const currentCompany = useCurrentCompany() + + const form = useForm({ + defaultValues: { + rule_name: "", + company: currentCompany, + rule_description: "", + transaction_type: "Any", + classify_as: 'Bank Entry', + bank_entry_type: "Single Account", + description_rules: [{ + check: "Contains", + }] + } + }) + + const { createDoc, loading, error } = useFrappeCreateDoc() + + const onSubmit = (data: BankTransactionRule) => { + createDoc("Bank Transaction Rule", data) + .then(() => { + toast.success(_("Rule created successfully")) + onCreate() + }) + } + + + useHotkeys('meta+s', () => { + form.handleSubmit(onSubmit)() + }, { + enabled: true, + preventDefault: true, + enableOnFormTags: true + }) + + return ( + <> + + + + + } + > + + {_("New Rule")} + + + {_("Create a new rule to automatically classify transactions.")} + + + +
      + +
      + {error && } + +
      +
      + +
      + + + ) +} + +export default CreateNewRule \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/Rules/EditRule.tsx b/banking/src/components/features/BankReconciliation/Rules/EditRule.tsx new file mode 100644 index 00000000000..96f749fe016 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/Rules/EditRule.tsx @@ -0,0 +1,101 @@ +import { Button } from "@/components/ui/button" +import ErrorBanner from "@/components/ui/error-banner" +import { Form } from "@/components/ui/form" +import _ from "@/lib/translate" +import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule" +import { FrappeError, useFrappeGetDoc, useFrappeUpdateDoc } from "frappe-react-sdk" +import { toast } from "sonner" +import { RuleForm } from "./RuleForm" +import { useForm } from "react-hook-form" +import { Skeleton } from "@/components/ui/skeleton" +import { SettingsPanelContent, SettingsPanelDescription, SettingsPanelHeader, SettingsPanelTitle } from "@/components/ui/settings-dialog" +import { useHotkeys } from "react-hotkeys-hook" + +type Props = { + onClose: VoidFunction, + ruleID: string +} + +const EditRule = ({ onClose, ruleID }: Props) => { + + const { data: rule, isValidating, error, mutate } = useFrappeGetDoc("Bank Transaction Rule", ruleID, undefined, { + revalidateOnMount: true + }) + + const { updateDoc, loading, error: updateError } = useFrappeUpdateDoc() + + const onSubmit = (data: BankTransactionRule) => { + updateDoc("Bank Transaction Rule", ruleID, data) + .then(() => { + toast.success(_("Rule updated.")) + mutate() + onClose() + }) + } + + return <> + + + + + } + > + + {rule?.rule_name} + + + {_("Edit this rule")} + + + + {isValidating &&
      + + + + + +
      } + + {error &&
      + +
      } + {rule && } +
      + + + +} + +const EditRuleForm = ({ rule, onSubmit, error }: { rule: BankTransactionRule, onSubmit: (data: BankTransactionRule) => void, error?: FrappeError | null }) => { + + const form = useForm({ + defaultValues: { + ...rule, + } + }) + + useHotkeys('meta+s', () => { + form.handleSubmit(onSubmit)() + }, { + enabled: true, + preventDefault: true, + enableOnFormTags: true + }) + + return ( +
      + +
      + {error && } + +
      +
      + + ) +} + +export default EditRule \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/Rules/RuleForm.tsx b/banking/src/components/features/BankReconciliation/Rules/RuleForm.tsx new file mode 100644 index 00000000000..1655de9ec83 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/Rules/RuleForm.tsx @@ -0,0 +1,799 @@ +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { Dialog, DialogTitle, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog" +import { FormField, FormItem, FormLabel, FormControl } from "@/components/ui/form" +import { AccountFormField, CurrencyFormField, DataField, LinkFormField, PartyTypeFormField, SelectFormField, SmallTextField } from "@/components/ui/form-elements" +import { Label } from "@/components/ui/label" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { SelectItem } from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { H4, Paragraph } from "@/components/ui/typography" +import { today } from "@/lib/date" +import _ from "@/lib/translate" +import { cn } from "@/lib/utils" +import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule" +import { BankTransactionRuleAccounts } from "@/types/Accounts/BankTransactionRuleAccounts" +import { FrappeConfig, FrappeContext } from "frappe-react-sdk" +import { ArrowDownRight, ArrowDownUp, ArrowRightLeftIcon, ArrowUpRight, LandmarkIcon, Plus, PlusCircleIcon, ReceiptIcon, Settings, Trash2 } from "lucide-react" +import { ChangeEvent, useCallback, useContext, useMemo, useRef, useState } from "react" +import { useFieldArray, useFormContext, useWatch } from "react-hook-form" + +export const RuleForm = ({ isEdit = false }: { isEdit?: boolean }) => { + + return
      + + + + + + + + +
      + + + +
      + + + + + + +
      +} + +const CompanySelector = () => { + + const { setValue } = useFormContext() + + return { + setValue('account', '') + } + }} + /> + +} + +/** Component to render a radio group as a toggle group with options for All, Withdrawal, Deposit */ +const TransactionTypeSelector = () => { + + const { control } = useFormContext() + + return ( + ( + + + {_("Transaction Type")}* + + + + + + + + + + {_("All")} + + + + + + + + + {_("Withdrawal")} + + + + + + + + + {_("Deposit")} + + + + + + )} + /> + ) +} + +const DescriptionRules = () => { + + const { control } = useFormContext() + + const { fields, append, remove } = useFieldArray({ + control, + name: "description_rules" + }) + + const addRow = () => { + // @ts-expect-error - we don't need all fields here + append({ check: "Contains" }) + } + + return ( +
      + {_("Rules to match against the transaction description")} * + {fields.map((field, index) => ( +
      +
      + + {_("Contains")} + {_("Starts with")} + {_("Ends with")} + {_("Regex")} + +
      +
      + +
      +
      + +
      +
      + ))} + +
      + +
      + +
      + ) +} + +const RuleAction = () => { + + const { control } = useFormContext() + + const classify_as = useWatch({ control, name: "classify_as" }) + const party_type = useWatch({ control, name: "party_type" }) + const bank_entry_type = useWatch({ control, name: "bank_entry_type" }) + + const accountType = useMemo(() => { + if (classify_as === "Payment Entry") { + return party_type === "Supplier" ? ["Payable"] : ["Receivable"] + } + + if (classify_as === "Transfer") { + return ["Bank", "Cash", "Temporary"] + } + + return undefined + + }, [classify_as, party_type]) + + return ( +
      +

      {_("If rule matches, then:")}

      + + + {_("Bank Entry")} + {_("Payment Entry")} + {_("Transfer")} + + + {classify_as === "Bank Entry" && ( + {_("Single Account")} + {_("Multiple Accounts (Journal Template)")} + )} + + + {classify_as === "Payment Entry" && ( +
      +
      + +
      +
      + +
      +
      + )} + + {(((bank_entry_type === "Single Account" || !bank_entry_type) && classify_as === "Bank Entry") || classify_as !== "Bank Entry") && ()} + + {bank_entry_type === "Multiple Accounts" && classify_as === "Bank Entry" && } +
      + ) +} + +const PartyField = () => { + + const { control, setValue } = useFormContext() + + const party_type = useWatch({ + control, + name: `party_type` + }) + + const { call } = useContext(FrappeContext) as FrappeConfig + + const company = useWatch({ control, name: 'company' }) + + const onChange = (event: ChangeEvent) => { + // Fetch the party and account + if (event.target.value) { + call.get('erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details', { + company: company, + party_type: party_type, + party: event.target.value, + date: today() + }).then((res) => { + setValue('account', res.message.party_account) + }) + } else { + // Clear the account + setValue('account', '') + } + + } + + if (!party_type) { + return + } + + return +} + +const MultipleAccountsSelection = () => { + + + const { control } = useFormContext() + + const accounts = useWatch({ + control, + name: 'accounts' + }) ?? [] + + const [isConfigureAccountsModalOpen, setIsConfigureAccountsModalOpen] = useState(false) + + + + return
      +
      + + +
      + + + + + + {_("Account")} + {_("Debit")} + {_("Credit")} + + + + {accounts.length === 0 && ( + + +
      + {_("No accounts configured")} + +
      +
      +
      + )} + {accounts.map((account, index) => ( + + {account.account} + {index === accounts.length - 1 ? + + + {_("This is auto computed to balance the journal entry.")} + + + {_("Based on the above entries, the balance amount (debit or credit) will be set for the last row to balance the journal entry.")} + + + : <> + + + } + + ))} +
      +
      + + setIsConfigureAccountsModalOpen(false)} /> +
      +} + +const AmountFormulaRenderer = ({ value }: { value?: string }) => { + + // If it's a string and cannot be a number, then show it as a formula + + if (isNaN(Number(value))) { + + let calculatedValue = ""; + + try { + calculatedValue = window.eval(`const transaction_amount = 200; ${value}`); + } catch (error: unknown) { + console.error(error); + calculatedValue = "Error"; + } + + const isComputationValid = !isNaN(Number(calculatedValue)) && calculatedValue !== undefined && calculatedValue !== null; + + return + + {value} + + +

      + {isComputationValid ? _("This is a formula based value.") : _("This is not a valid formula. Check the variable used in the formula.")} +

      + {_("Example: If the transaction amount is 200, then this will be calculated as {} = {}", [value ?? "", calculatedValue])} +

      +
      +
      + } + + return {value} +} + +const ConfigureAccountsModal = ({ open, onClose }: { open: boolean, onClose: () => void }) => { + + + return + + + + +} + +const ConfigureAccountsModalContent = () => { + + const { control, getValues, setValue } = useFormContext() + + const { call } = useContext(FrappeContext) as FrappeConfig + + // const costCenterMapRef = useRef>({}) + + const partyMapRef = useRef>({}) + + const onPartyChange = (value: string, index: number) => { + // Get the account for the party type + if (value) { + if (partyMapRef.current[value]) { + setValue(`accounts.${index}.account`, partyMapRef.current[value]) + } else { + call.get('erpnext.accounts.party.get_party_account', { + party: value, + party_type: getValues(`accounts.${index}.party_type`), + company: company + }).then((result: { message: string }) => { + setValue(`accounts.${index}.account`, result.message) + partyMapRef.current[value] = result.message + }) + } + } else { + setValue(`accounts.${index}.account`, '') + } + } + + const transaction_type = useWatch({ + name: 'transaction_type', + control, + }) + + const { fields, append, remove } = useFieldArray({ + control, + name: 'accounts' + }) + + + const [selectedRows, setSelectedRows] = useState([]) + + const onSelectRow = useCallback((index: number) => { + setSelectedRows(prev => { + if (prev.includes(index)) { + return prev.filter(i => i !== index) + } + return [...prev, index] + }) + }, []) + + const onSelectAll = useCallback(() => { + setSelectedRows(prev => { + if (prev.length === fields.length) { + return [] + } + return [...fields.map((_, index) => index)] + }) + }, [fields]) + + const onAdd = () => { + append({ + party_type: '', + party: '', + account: '', + debit: '', + credit: '', + user_remark: '' + } as BankTransactionRuleAccounts, { + focusName: `accounts.${fields.length}.account` + }) + } + + const onRemove = useCallback(() => { + remove(selectedRows) + setSelectedRows([]) + }, [remove, selectedRows]) + + const isWithdrawal = transaction_type === 'Withdrawal' + + const company = useWatch({ + name: 'company', + control, + }) + + return <> + + {_("Configure Accounts for Bank Entry")} + {_("Add all accounts that you want to split the transaction into.")} + +
      + + + + 0 && selectedRows.length === fields.length} + onCheckedChange={onSelectAll} /> + {_("Party")} + {_("Account")} * + {/* {_("Cost Center")} */} + {_("Remarks")} + {_("Debit")} + {_("Credit")} + + + + + + + + + + + + Bank GL Account + + + + + + + + {transaction_type === "Withdrawal" || transaction_type === "Any" ? _("Will be auto-populated") : ""} + + + + + {transaction_type === "Deposit" || transaction_type === "Any" ? _("Will be auto-populated") : ""} + + + + {fields.map((field, index) => ( + + + onSelectRow(index)} + // Make this accessible to screen readers + aria-label={_("Select row {0}", [String(index + 1)])} + /> + + + +
      + + +
      + +
      + + { + // onAccountChange(event.target.value, index) + // } + }} + buttonClassName="min-w-64" + isRequired + hideLabel + /> + + {/* + + */} + + + + + + + + + +
      + ))} +
      +
      +
      +
      +
      + +
      + {selectedRows.length > 0 &&
      + +
      } +
      +
      +
      + +
      + +
      +

      {_("Help")}

      + + {(_("You can set up the rule to split the transaction across multiple accounts."))} +
      {_("You can also add credit or debit values to pre-fill - these support both static values (like 200) or formulas (like transaction_amount * 0.25).")} +
      +
      + {_("Example")}: +
      + + transaction_amount * 0.25 + +
      + + {_("In this case, the amount will be calculated as 25% of the transaction amount. If the transaction amount is 200, then this will be calculated as 200 * 0.25 = 50.")} + +
      +
      + + +
      + +} + + +const PartyRowField = ({ index, onChange }: { index: number, onChange: (value: string, index: number) => void }) => { + + const { control } = useFormContext() + + const party_type = useWatch({ + control, + name: `accounts.${index}.party_type` + }) + + if (!party_type) { + return + } + + return { + onChange(event.target.value, index) + }, + }} + hideLabel + buttonClassName="rounded-s-none border-s-0 min-w-64" + doctype={party_type} + + /> +} diff --git a/banking/src/components/features/BankReconciliation/SelectedTransactionDetails.tsx b/banking/src/components/features/BankReconciliation/SelectedTransactionDetails.tsx new file mode 100644 index 00000000000..53ffba910a5 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/SelectedTransactionDetails.tsx @@ -0,0 +1,73 @@ +import { useMemo } from 'react' +import { ArrowDownRight, ArrowUpRight, Calendar } from 'lucide-react' +import { formatCurrency } from '@/lib/numbers' +import { formatDate } from '@/lib/date' +import { UnreconciledTransaction, useGetBankAccounts } from './utils' +import { getCompanyCurrency } from '@/lib/company' +import { Card, CardContent } from '@/components/ui/card' +import { cn } from '@/lib/utils' +import _ from '@/lib/translate' +import BankLogo from '@/components/common/BankLogo' + +type Props = { + transaction: UnreconciledTransaction, + showAccount?: boolean, + account?: string +} + +const SelectedTransactionDetails = ({ transaction, showAccount = false, account }: Props) => { + + const isWithdrawal = transaction.withdrawal && transaction.withdrawal > 0 + + const { banks } = useGetBankAccounts() + + const bank = useMemo(() => { + if (transaction.bank_account) { + return banks?.find((bank) => bank.name === transaction.bank_account) + } + return null + }, [transaction.bank_account, banks]) + + const amount = transaction.withdrawal ? transaction.withdrawal : transaction.deposit + + const currency = transaction.currency || getCompanyCurrency(transaction.company ?? '') + + return ( + + +
      +
      +
      +
      + + {transaction.bank_account} +
      +
      + + {formatDate(transaction.date, 'Do MMM YYYY')} +
      +
      +
      +
      + {isWithdrawal ? : } + {isWithdrawal ? _('Spent') : _('Received')} +
      + {formatCurrency(amount, currency)} + {transaction.unallocated_amount && transaction.unallocated_amount !== amount ? {_("Unallocated")}: {formatCurrency(transaction.unallocated_amount)} : null} +
      +
      +
      + {transaction.description} + {transaction.reference_number ? {_("Ref")}: {transaction.reference_number} : null} + {showAccount && account ? {_("GL Account")}: {account} : null} +
      + +
      +
      +
      + ) +} + +export default SelectedTransactionDetails \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/SelectedTransactionsTable.tsx b/banking/src/components/features/BankReconciliation/SelectedTransactionsTable.tsx new file mode 100644 index 00000000000..6334305c080 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/SelectedTransactionsTable.tsx @@ -0,0 +1,47 @@ +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import _ from '@/lib/translate' +import { useAtomValue } from 'jotai' +import { bankRecSelectedTransactionAtom, selectedBankAccountAtom } from './bankRecAtoms' +import { formatDate } from '@/lib/date' +import { formatCurrency } from '@/lib/numbers' +import { ArrowDownRight, ArrowUpRight } from 'lucide-react' + +const SelectedTransactionsTable = () => { + + const selectedBankAccount = useAtomValue(selectedBankAccountAtom) + + const transactions = useAtomValue(bankRecSelectedTransactionAtom(selectedBankAccount?.name ?? '')) + return ( + + + + + {_("Date")} + + + {_("Description")} + + + {_("Amount")} + + + + + {transactions.map((transaction) => ( + + {formatDate(transaction.date)} + {transaction.description} + + {transaction.withdrawal && transaction.withdrawal > 0 ? : } + + {formatCurrency(transaction.unallocated_amount, transaction.currency ?? '')} + + + + ))} + +
      + ) +} + +export default SelectedTransactionsTable \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/TransferModal.tsx b/banking/src/components/features/BankReconciliation/TransferModal.tsx new file mode 100644 index 00000000000..fb824dbc6f7 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/TransferModal.tsx @@ -0,0 +1,555 @@ +import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import { bankRecSelectedTransactionAtom, bankRecTransferModalAtom, bankRecUnreconcileModalAtom, SelectedBank, selectedBankAccountAtom } from './bankRecAtoms' +import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogClose, DialogTitle, DialogDescription } from '@/components/ui/dialog' +import _ from '@/lib/translate' +import { UnreconciledTransaction, useGetBankAccounts, useGetRuleForTransaction, useRefreshUnreconciledTransactions, useUpdateActionLog } from './utils' +import { Button } from '@/components/ui/button' +import SelectedTransactionDetails from './SelectedTransactionDetails' +import { PaymentEntry } from '@/types/Accounts/PaymentEntry' +import { useForm, useFormContext, useWatch } from 'react-hook-form' +import { FrappeConfig, FrappeContext, useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk' +import { toast } from 'sonner' +import ErrorBanner from '@/components/ui/error-banner' +import { H4 } from '@/components/ui/typography' +import { cn } from '@/lib/utils' +import { ArrowRight, Banknote, BadgeCheck, Calendar, ArrowUpRight, ArrowDownRight, CheckIcon, CheckCircle, ArrowLeft } from 'lucide-react' +import { Separator } from '@/components/ui/separator' +import { Form } from '@/components/ui/form' +import { AccountFormField, DataField, DateField, SmallTextField } from '@/components/ui/form-elements' +import SelectedTransactionsTable from './SelectedTransactionsTable' +import { useCurrentCompany } from '@/hooks/useCurrentCompany' +import { formatDate } from '@/lib/date' +import { useContext, useMemo, useState } from 'react' +import { formatCurrency } from '@/lib/numbers' +import { Label } from '@/components/ui/label' +import { FileDropzone } from '@/components/ui/file-dropzone' +import FileUploadBanner from '@/components/common/FileUploadBanner' +import { BankTransaction } from '@/types/Accounts/BankTransaction' +import { useHotkeys } from 'react-hotkeys-hook' +import { useDirection } from '@/components/ui/direction' +import BankLogo from '@/components/common/BankLogo' + +const TransferModal = () => { + + const [isOpen, setIsOpen] = useAtom(bankRecTransferModalAtom) + + return ( + + + + {_("Transfer")} + + {_("Record an internal transfer to another bank/credit card/cash account.")} + + + + + + ) +} + +const TransferModalContent = () => { + + const selectedBankAccount = useAtomValue(selectedBankAccountAtom) + + const selectedTransaction = useAtomValue(bankRecSelectedTransactionAtom(selectedBankAccount?.name ?? '')) + + if (!selectedTransaction || !selectedBankAccount || selectedTransaction.length === 0) { + return
      + {_("No transaction selected")} +
      + } + + if (selectedTransaction.length === 1) { + return + } + + return + +} + +const BulkInternalTransferForm = ({ transactions }: { transactions: UnreconciledTransaction[] }) => { + + const form = useForm<{ + bank_account: string + }>() + + const setIsOpen = useSetAtom(bankRecTransferModalAtom) + + const { call: createPaymentEntry, loading, error } = useFrappePostCall<{ message: { transaction: BankTransaction, payment_entry: PaymentEntry }[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_bulk_internal_transfer') + + const onReconcile = useRefreshUnreconciledTransactions() + const addToActionLog = useUpdateActionLog() + + const onSubmit = (data: { bank_account: string }) => { + + createPaymentEntry({ + bank_transaction_names: transactions.map((transaction) => transaction.name), + bank_account: data.bank_account + }).then(({ message }) => { + addToActionLog({ + type: 'transfer', + timestamp: (new Date()).getTime(), + isBulk: true, + items: message.map((item) => ({ + bankTransaction: item.transaction, + voucher: { + reference_doctype: "Payment Entry", + reference_name: item.payment_entry.name, + posting_date: item.payment_entry.posting_date, + doc: item.payment_entry, + } + })), + bulkCommonData: { + bank_account: data.bank_account, + } + }) + toast.success(_("Transfer Recorded"), { + duration: 4000, + closeButton: true, + }) + onReconcile(transactions[transactions.length - 1]) + setIsOpen(false) + }) + + } + + const onAccountChange = (account: string) => { + form.setValue('bank_account', account) + } + + const selectedAccount = useWatch({ control: form.control, name: 'bank_account' }) + + const currentCompany = useCurrentCompany() + + const company = transactions && transactions.length > 0 ? transactions[0].company : (currentCompany ?? '') + + console.log("This is here", transactions) + + return
      + +
      + + {error && } + + + + + + + + + + + +
      +
      + + +} + +interface InternalTransferFormFields extends PaymentEntry { + mirror_transaction_name?: string +} + +const InternalTransferForm = ({ selectedBankAccount, selectedTransaction }: { selectedBankAccount: SelectedBank, selectedTransaction: UnreconciledTransaction }) => { + + + const setIsOpen = useSetAtom(bankRecTransferModalAtom) + + const onClose = () => { + setIsOpen(false) + } + + const { data: rule } = useGetRuleForTransaction(selectedTransaction) + + const isWithdrawal = (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) ? true : false + + const form = useForm({ + defaultValues: { + payment_type: 'Internal Transfer', + company: selectedTransaction?.company, + // If the transaction is a withdrawal, set the paid from to the selected bank account + paid_from: isWithdrawal ? selectedBankAccount.account : (rule?.account ?? ''), + // If the transaction is a deposit, set the paid to to the selected bank account + paid_to: !isWithdrawal ? selectedBankAccount.account : (rule?.account ?? ''), + // Set the amount to the amount of the selected transaction + paid_amount: selectedTransaction.unallocated_amount, + received_amount: selectedTransaction.unallocated_amount, + reference_date: selectedTransaction.date, + posting_date: selectedTransaction.date, + reference_no: (selectedTransaction.reference_number || selectedTransaction.description || '').slice(0, 140), + } + }) + + const onReconcile = useRefreshUnreconciledTransactions() + + const { call: createPaymentEntry, loading, error, isCompleted } = useFrappePostCall<{ message: { transaction: BankTransaction, payment_entry: PaymentEntry } }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_internal_transfer') + + const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom) + const addToActionLog = useUpdateActionLog() + + const { file: frappeFile } = useContext(FrappeContext) as FrappeConfig + + const [isUploading, setIsUploading] = useState(false) + const [uploadProgress, setUploadProgress] = useState(0) + + const [files, setFiles] = useState([]) + + const onSubmit = (data: InternalTransferFormFields) => { + + createPaymentEntry({ + bank_transaction_name: selectedTransaction.name, + ...data, + custom_remarks: data.remarks ? true : false, + // Pass this to reconcile both at the same time + mirror_transaction_name: data.mirror_transaction_name + }).then(async ({ message }) => { + addToActionLog({ + type: 'transfer', + timestamp: (new Date()).getTime(), + isBulk: false, + items: [ + { + bankTransaction: message.transaction, + voucher: { + reference_doctype: "Payment Entry", + reference_name: message.payment_entry.name, + reference_no: message.payment_entry.reference_no, + reference_date: message.payment_entry.reference_date, + posting_date: message.payment_entry.posting_date, + doc: message.payment_entry, + } + } + ] + }) + toast.success(_("Transfer Recorded"), { + duration: 4000, + closeButton: true, + action: { + label: _("Undo"), + onClick: () => setBankRecUnreconcileModalAtom(selectedTransaction.name) + }, + actionButtonStyle: { + backgroundColor: "rgb(0, 138, 46)" + } + }) + + if (files.length > 0) { + setIsUploading(true) + + const uploadPromises = files.map(f => { + return frappeFile.uploadFile(f, { + isPrivate: true, + doctype: "Payment Entry", + docname: message.payment_entry.name, + }, (_bytesUploaded, _totalBytes, progress) => { + + setUploadProgress((currentProgress) => { + //If there are multiple files, we need to add the progress to the current progress + return currentProgress + ((progress?.progress ?? 0) / files.length) + }) + + }) + }) + + return Promise.all(uploadPromises).then(() => { + setUploadProgress(0) + setIsUploading(false) + }) + } else { + return Promise.resolve() + } + }).then(() => { + setUploadProgress(0) + setIsUploading(false) + onReconcile(selectedTransaction) + onClose() + }) + } + + + useHotkeys('meta+s', () => { + form.handleSubmit(onSubmit)() + }, { + enabled: true, + preventDefault: true, + enableOnFormTags: true + }) + + const onAccountChange = (account: string, is_mirror: boolean = false) => { + //If the transaction is a withdrawal, set the paid to to the selected account - since this is the account where the money is deposited into + if (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) { + form.setValue('paid_to', account) + } else { + form.setValue('paid_from', account) + } + + if (!is_mirror) { + // Reset the mirror transaction name + form.setValue('mirror_transaction_name', '') + } + } + + const selectedAccount = useWatch({ control: form.control, name: (selectedTransaction.deposit && selectedTransaction.deposit > 0) ? 'paid_from' : 'paid_to' }) + + const direction = useDirection() + + if (isUploading && isCompleted) { + return + } + + return
      + +
      + {error && } +
      + + +
      +
      + + +
      + +
      +
      + +
      +

      {isWithdrawal ? _('Transferred to') : _('Transferred from')}

      + + +
      +
      +
      +
      + account.name !== selectedBankAccount.account} + isRequired + /> +
      + +
      + {direction === 'ltr' ? : } +
      +
      + account.name !== selectedBankAccount.account} + /> +
      +
      +
      + +
      +
      + + + +
      + + +
      +
      +
      + + + + + + +
      +
      + +} + + +const BankOrCashPicker = ({ bankAccount, onAccountChange, selectedAccount, company }: { selectedAccount: string, bankAccount: string, onAccountChange: (account: string) => void, company: string }) => { + + const { banks } = useGetBankAccounts(undefined, (bank) => bank.name !== bankAccount) + + return
      + {banks.map((bank) => ( +
      onAccountChange(bank.account ?? '')} + > + +
      + {bank.account_name} {bank.bank_account_no && ({bank.bank_account_no})} + {bank.account} +
      +
      + ))} + +
      + +} + +const CashPicker = ({ company, selectedAccount, setSelectedAccount }: { company: string, selectedAccount: string, setSelectedAccount: (account: string) => void }) => { + + const { data } = useFrappeGetCall('frappe.client.get_value', { + doctype: 'Company', + filters: company, + fieldname: 'default_cash_account' + }, undefined, { + revalidateOnFocus: false, + revalidateIfStale: false, + }) + + const account = data?.message?.default_cash_account + + if (account) { + return
      setSelectedAccount(account ?? '')} + > +
      + +
      +
      + Cash + {data?.message?.default_cash_account} +
      +
      + } + + return null +} + + +const RecommendedTransferAccount = ({ transaction, onAccountChange }: { transaction: UnreconciledTransaction, onAccountChange: (account: string, is_mirror: boolean) => void }) => { + + const { setValue, watch } = useFormContext() + + const mirrorTransactionName = watch('mirror_transaction_name') + const paid_from = watch('paid_from') + const paid_to = watch('paid_to') + + const { data } = useFrappeGetCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.search_for_transfer_transaction', { + transaction_id: transaction.name + }, undefined, { + revalidateOnFocus: false, + revalidateIfStale: false, + }) + + // Get bank accounts to find the logo + const { banks } = useGetBankAccounts() + + const bank = useMemo(() => { + if (data?.message?.bank_account && banks) { + return banks.find(bank => bank.name === data.message.bank_account) + } + return null + }, [data?.message?.bank_account, banks]) + + const selectTransaction = () => { + if (data?.message) { + setValue('mirror_transaction_name', data.message.name) + onAccountChange(data.message.account, true) + } + } + + if (data?.message) { + + const isWithdrawal = data.message.withdrawal && data.message.withdrawal > 0 + + const amount = isWithdrawal ? data.message.withdrawal : data.message.deposit + const currency = data.message.currency + + const isAccountSelected = isWithdrawal ? paid_from === data.message.account : paid_to === data.message.account + + const isSuggested = mirrorTransactionName === data?.message?.name && isAccountSelected + + return (
      +
      +
      +
      +
      + + {_("Suggested Transfer to {0}", [data.message.account])} +
      +
      + {_("The system found a mirror transaction ({0}) in another account with the same amount and date.", [data.message.name])} + {_("Accepting the suggestion will reconcile both transactions.")} +
      + +
      +
      + + {formatDate(data.message.date, 'Do MMM YYYY')} +
      + {data.message.description} +
      +
      +
      +
      +
      + +
      +
      +
      + {isWithdrawal ? : } + {isWithdrawal ? _('Transferred Out') : _('Received')} +
      +
      + {formatCurrency(amount, currency)} +
      + +
      +
      +
      +
      + ) + } + + return null +} + +export default TransferModal \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/bankRecAtoms.ts b/banking/src/components/features/BankReconciliation/bankRecAtoms.ts new file mode 100644 index 00000000000..4f6e51e51b1 --- /dev/null +++ b/banking/src/components/features/BankReconciliation/bankRecAtoms.ts @@ -0,0 +1,83 @@ +import { BankAccount } from "@/types/Accounts/BankAccount"; +import { getDatesForTimePeriod } from "@/lib/date"; +import { atom } from "jotai"; +import { atomWithStorage, createJSONStorage } from "jotai/utils"; +import { atomFamily } from 'jotai-family' +import { UnreconciledTransaction } from "./utils"; +import { BankTransaction } from "@/types/Accounts/BankTransaction"; +import { PaymentEntry } from "@/types/Accounts/PaymentEntry"; +import { JournalEntry } from "@/types/Accounts/JournalEntry"; + +export interface SelectedBank extends Pick { + logo?: string, + logoDark?: string, + darkModeInvert?: boolean, + logoClassName?: string, + account_currency?: string +} +export const selectedBankAccountAtom = atom(null) + +export const bankRecDateAtom = atomWithStorage<{ fromDate: string, toDate: string }>("bank-rec-date", { + fromDate: getDatesForTimePeriod('This Month').fromDate, + toDate: getDatesForTimePeriod('This Month').toDate +}) + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const bankRecClosingBalanceAtom = atomFamily((_id: string) => { + return atom<{ value: number, stringValue: string | number | undefined }>({ + value: 0, + stringValue: '0.00' + }) +}) + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const bankRecSelectedTransactionAtom = atomFamily((_id: string) => { + return atom([]) +}) + +/** Action Modals */ +export const bankRecTransferModalAtom = atom(false) +export const bankRecRecordPaymentModalAtom = atom(false) +export const bankRecRecordJournalEntryModalAtom = atom(false) + +export const bankRecUnreconcileModalAtom = atom('') + +export const bankRecMatchFilters = atomWithStorage('bank-rec-match-filters', ['payment_entry', 'journal_entry']) + +export const bankRecSearchText = atom('') +export const bankRecAmountFilter = atom<{ value: number, stringValue?: string | number }>({ + value: 0, + stringValue: '0.00' +}) +export const bankRecTransactionTypeFilter = atom('All') + +export interface ActionLog { + type: 'match' | 'payment' | 'transfer' | 'bank_entry' + isBulk: boolean + timestamp: number, + items: ActionLogItem[], + bulkCommonData?: { + party_type?: string, + party?: string, + account?: string, + bank_account?: string, + } +} + +export interface ActionLogItem { + bankTransaction: BankTransaction, + voucher: { + reference_doctype: string, + reference_name: string, + reference_no?: string, + reference_date?: string, + posting_date: string, + doc?: PaymentEntry | JournalEntry + }, +} + +const actionLogStorage = createJSONStorage(() => sessionStorage) + +export const bankRecActionLog = atomWithStorage('bank-rec-action-log', [], actionLogStorage, { + getOnInit: true, +}) \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/logos.ts b/banking/src/components/features/BankReconciliation/logos.ts new file mode 100644 index 00000000000..8212b72c33f --- /dev/null +++ b/banking/src/components/features/BankReconciliation/logos.ts @@ -0,0 +1,397 @@ +export const BANK_LOGOS: { keywords: string[], logo: string, locale?: string[], logoDark?: string, darkModeInvert?: boolean, logoClassName?: string }[] = [ + // United States + International + { + keywords: ['American Express', 'Amex'], + logo: 'Amex.svg', + locale: ['Global', 'United States'] + }, + { + keywords: ['Bank of America', 'BOA'], + logo: 'Bank_of_America.png', + darkModeInvert: true, + locale: ['United States'] + }, + { + keywords: ['Barclays'], + logo: 'Barclays.svg', + locale: ['Global', 'United Kingdom'], + logoClassName: 'h-12', + }, + { + keywords: ['BNP Paribas'], + logo: 'BNP_Paribas.svg', + logoDark: 'BNP_Paribas-Dark.svg', + locale: ['Global', 'France'], + logoClassName: 'max-w-24' + }, + { + keywords: ['Bank of New York Mellon', 'BNY Mellon', 'BNY'], + logo: 'BNY_Mellon.svg', + locale: ['Global', 'United States'], + logoDark: 'BNY_Mellon-Dark.svg', + }, + { + keywords: ['Capital One'], + logo: 'Capital_One.png', + locale: ['United States'], + darkModeInvert: true + }, + { + keywords: ['Charles Schwab', 'Schwab'], + logo: 'Charles_Schwab.svg', + locale: ['United States'], + logoClassName: 'h-7' + }, + { + keywords: ['Chase'], + logo: 'chase.svg', + locale: ['Global', 'United States'], + logoDark: 'chase-Dark.svg', + }, + { + keywords: ['Citi', 'Citibank', 'Citi Group', 'Citi Financial Services'], + logo: 'Citi.svg', + locale: ['Global', 'United States'] + }, + { + keywords: ['Deutsche Bank'], + logo: 'Deutsche_Bank.svg', + locale: ['Global', 'Germany'], + darkModeInvert: true, + }, + { + keywords: ['Goldman Sachs'], + logo: 'Goldman_Sachs.svg', + locale: ['Global', 'United States'], + darkModeInvert: true, + }, + { + keywords: ['HSBC'], + logo: 'HSBC.svg', + locale: ['Global', 'United Kingdom'], + logoDark: 'HSBC-dark.svg', + }, + { + keywords: ['JPMorgan Chase', 'JPMorgan', 'JP Morgan', 'JP Morgan Chase', 'JPMorgan Chase & Co', 'JPM', 'JPMC'], + logo: 'jpmc.svg', + locale: ['Global', 'United States'], + darkModeInvert: true, + }, + { + keywords: ['Morgan Stanley'], + logo: 'Morgan_Stanley.png', + locale: ['Global', 'United States'], + darkModeInvert: true, + }, + { + keywords: ['PNC', 'PNC Financial Services Group', 'PNC Financial Services', 'Pittsburgh National Corporation'], + logo: 'PNC.png', + locale: ['United States'] + }, + { + keywords: ['Santander'], + logo: 'Santander.svg', + locale: ['Global'] + }, + { + keywords: ['TD Bank', 'Toronto Dominion Bank'], + logo: 'Toronto_Dominion_Bank.png', + locale: ['Canada'] + }, + { + keywords: ['Truist'], + logo: 'Truist.svg', + locale: ['United States'], + darkModeInvert: true, + logoClassName: 'h-8' + }, + { + keywords: ['UBS'], + logo: 'UBS.svg', + locale: ['Global', 'Switzerland'], + logoDark: 'UBS-dark.svg', + }, + { + keywords: ['US Bank', 'USBank', 'U.S. Bank', 'U.S. Bancorp'], + logo: 'USBank.svg', + locale: ['United States'], + logoDark: 'USBank-dark.svg', + }, + { + keywords: ['Wells Fargo', 'Wells Fargo'], + logo: 'Wells_Fargo.svg', + locale: ['United States'], + logoClassName: 'h-7' + }, + { + keywords: ['OakStar', 'Oakstar', 'Oakstar'], + logo: 'Oakstar.png', + logoDark: 'Oakstar-dark.webp', + locale: ['United States'], + logoClassName: 'h-7' + }, + { + keywords: ['PlainsCapital', 'Plains Capital'], + logo: 'PlainsCapitalBank.png', + locale: ['United States'], + logoClassName: 'h-7' + }, + { + keywords: ["Standard Chartered"], + logo: 'Standard_Chartered.png', + logoDark: 'Standard_Chartered-dark.png', + locale: ['Global'], + }, + // India + { + keywords: ['HDFC Bank', 'HDFC'], + logo: 'HDFC.svg', + locale: ['India'], + }, + { + keywords: ['ICICI Bank', 'ICICI'], + logo: 'ICICI.svg', + logoDark: 'ICICI-dark.svg', + locale: ['India'], + }, + { + keywords: ['SBI', 'State Bank of India'], + logo: 'State_Bank_of_India.svg', + logoDark: 'State_bank_of_India-Dark.svg', + locale: ['India'], + logoClassName: 'h-4.5' + }, + { + keywords: ['Punjab National Bank', 'PNB'], + logo: 'Punjab_National_Bank.svg', + locale: ['India'] + }, + { + keywords: ['Union Bank of India', 'Union Bank'], + logo: 'Union_Bank_of_India.svg', + locale: ['India'] + }, + { + keywords: ['Yes Bank', 'Yes'], + logo: 'Yes_Bank.svg', + locale: ['India'], + logoDark: 'Yes_Bank-dark.svg', + }, + { + keywords: ['RBL Bank', 'RBL'], + logo: 'RBL_Bank.svg', + locale: ['India'], + logoDark: 'RBL_Bank-dark.svg', + }, + { + keywords: ['Axis Bank', 'Axis'], + logo: 'Axis_Bank.svg', + locale: ['India'], + darkModeInvert: true + }, + { + keywords: ['Bank of Baroda', 'BOB'], + logo: 'Bank_of_Baroda.svg', + locale: ['India', 'Kenya'], + logoClassName: 'h-7' + }, + { + keywords: ['Bank of India', 'BOI'], + logo: 'Bank_of_India.png', + locale: ['India'], + logoClassName: 'h-7' + }, + { + keywords: ['Bank of Maharashtra', 'BOM'], + logo: 'Bank_of_Maharashtra.png', + locale: ['India'], + logoClassName: 'min-w-24' + }, + { + keywords: ['Kotak Mahindra Bank', 'Kotak'], + logo: 'Kotak_Mahindra.svg', + locale: ['India'] + }, + { + keywords: ['IndusInd Bank', 'IndusInd'], + logo: 'IndusInd_Bank.svg', + locale: ['India'], + darkModeInvert: true, + }, + { + keywords: ['IDBI Bank', 'IDBI'], + logo: 'IDBI_Bank.svg', + locale: ['India'] + }, + { + keywords: ['IDFC First Bank', 'IDFC First'], + logo: 'IDFC_First_Bank.svg', + locale: ['India'] + }, + { + keywords: ['Federal Bank'], + logo: 'Federal_Bank.png', + logoDark: 'Federal_Bank-dark.png', + locale: ['India'] + }, + { + keywords: ['Fi Bank'], + logo: 'Fi_Bank.svg', + locale: ['India'] + }, + { + keywords: ['RazorpayX', 'Razorpay'], + logo: 'Razorpay.svg', + logoDark: 'Razorpay-dark.svg', + locale: ['India'] + }, + { + keywords: ['Revolut'], + logo: 'Revolut.png', + locale: ['Global'], + darkModeInvert: true + }, + { + keywords: ['Starling Bank'], + logo: 'Starling_Bank.png', + logoDark: 'Starling_Bank-dark.png', + locale: ['Global', 'UK'], + logoClassName: 'h-10' + }, + // Australia and New Zealand + { + keywords: ["Commonwealth Bank", "CBA"], + logo: "Commonwealth_Bank.svg", + locale: ['Australia', 'New Zealand'], + }, + { + keywords: ["Airwallex"], + logo: "Airwallex.png", + logoDark: "Airwallex-dark.png", + locale: ['Global'] + }, + { + keywords: ["Judo Bank"], + logo: "Judo_Bank.svg", + logoDark: "Judo_Bank-dark.svg", + locale: ['Australia', 'New Zealand'] + }, + { + keywords: ["Alpha"], // This might conflict with Alpha Bank in Greece + logo: "Alpha_Bank.svg", + darkModeInvert: true, + logoClassName: 'h-4.5', + locale: ['Australia', 'New Zealand'] + }, + { + keywords: ["Australian Tax Office", "Australian Taxation Office"], + logo: "Australian_Tax_Office.png", + darkModeInvert: true, + locale: ['Australia'] + }, + { + keywords: ["Westpac"], + logo: "Westpac.svg", + locale: ['Australia'] + }, + { + keywords: ["ANZ", "ANZ Bank", "Australia and New Zealand Banking Group"], + logo: "ANZ.png", + locale: ['Australia', 'New Zealand'] + }, + { + keywords: ["Macquarie Group", "Macquarie Bank"], + logo: "Macquarie.svg", + darkModeInvert: true, + locale: ['Australia'] + }, + // Nicaragua + { + keywords: ["Banco Atlantida", "Banco Atlántida"], + logo: "Banco_Atlantida.png", + locale: ['Nicaragua'] + }, + { + keywords: ["Banco de Finanzas"], + logo: "Banco_de_Finanzas.svg", + locale: ['Nicaragua'], + logoClassName: 'h-4.5' + }, + { + keywords: ["Avanz"], + logo: "Avanz.svg", + logoDark: "Avanz-dark.svg", + locale: ['Nicaragua'], + logoClassName: 'h-7' + }, + { + keywords: ["Ficohsa"], + logo: "Ficohsa.svg", + locale: ['Nicaragua'] + }, + { + keywords: ["BAC", "BAC Credomatic"], + logo: "BAC_Credomatic.svg", + locale: ['Nicaragua'], + logoClassName: 'h-4.5' + }, + { + keywords: ["Banco Lafise"], + logo: "Banco_Lafise.png", + darkModeInvert: true, + locale: ['Nicaragua'] + }, + // German + { + keywords: ["Sparkasse"], + logo: "Sparkasse.png", + locale: ['Germany'] + }, + { + keywords: ["Volksbank", "Raiffeisenbank", "VR-Bank"], + logo: "Volksbanken_Raiffeisenbanken.svg", + locale: ['Germany'], + logoClassName: 'min-w-32' + }, + // Kenya + { + keywords: ["KCB Bank", "KCB"], + logo: "KCB_Bank_Kenya.png", + locale: ['Kenya'] + }, + { + keywords: ["Equity Bank"], + logo: "Equity_Bank.png", + logoDark: "Equity_Bank-dark.png", + locale: ['Kenya'], + }, + { + keywords: ["I&M"], + logo: "I&M.png", + locale: ['Kenya'] + }, + { + keywords: ["ABSA"], + logo: "ABSA.png", + locale: ['Kenya'], + darkModeInvert: true, + logoClassName: 'h-7' + }, + { + keywords: ["Stanbic"], + logo: "Stanbic.png", + locale: ['Kenya'], + logoClassName: 'h-7' + }, + { + keywords: ["DTB", "Diamond Trust Bank"], + logo: "Diamond_Trust_Bank.png", + locale: ['Kenya'] + }, + { + keywords: ["Prime Bank"], + logo: "Prime_Bank.png", + locale: ['Kenya'], + logoClassName: 'max-w-28' + } +] \ No newline at end of file diff --git a/banking/src/components/features/BankReconciliation/utils.ts b/banking/src/components/features/BankReconciliation/utils.ts new file mode 100644 index 00000000000..833fb8afd4c --- /dev/null +++ b/banking/src/components/features/BankReconciliation/utils.ts @@ -0,0 +1,457 @@ +import { ActionLog, bankRecActionLog, bankRecAmountFilter, bankRecDateAtom, bankRecMatchFilters, bankRecSearchText, bankRecSelectedTransactionAtom, bankRecTransactionTypeFilter, bankRecUnreconcileModalAtom, SelectedBank, selectedBankAccountAtom } from './bankRecAtoms' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import { useMemo } from 'react' +import { SWRConfiguration, useFrappeGetCall, useFrappeGetDoc, useFrappePostCall, useSWRConfig } from 'frappe-react-sdk' +import { BankTransaction } from '@/types/Accounts/BankTransaction' +import { BankAccount } from '@/types/Accounts/BankAccount' +import dayjs from 'dayjs' +import { toast } from 'sonner' +import { BANK_LOGOS } from './logos' +import { getErrorMessage } from '@/lib/frappe' +import { useCurrentCompany } from '@/hooks/useCurrentCompany' +import _ from '@/lib/translate' +import { BankTransactionRule } from '@/types/Accounts/BankTransactionRule' +import { useRef } from 'react' +import type { DebouncedState } from 'usehooks-ts' +import { useDebounceCallback } from 'usehooks-ts' +import Fuse from 'fuse.js' + +export const useGetAccountOpeningBalance = () => { + + const companyID = useCurrentCompany() + const bankAccount = useAtomValue(selectedBankAccountAtom) + + const dates = useAtomValue(bankRecDateAtom) + + const args = useMemo(() => { + + return { + bank_account: bankAccount?.name, + company: companyID, + till_date: dayjs(dates.fromDate).subtract(1, 'days').format('YYYY-MM-DD'), + } + + }, [companyID, bankAccount?.name, dates.fromDate]) + + return useFrappeGetCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance', args, undefined, { + revalidateOnFocus: false + }) +} + +export const useGetAccountClosingBalance = () => { + + const companyID = useCurrentCompany() + const bankAccount = useAtomValue(selectedBankAccountAtom) + + const dates = useAtomValue(bankRecDateAtom) + + const args = useMemo(() => { + + return { + bank_account: bankAccount?.name, + company: companyID, + till_date: dates.toDate, + } + + }, [companyID, bankAccount?.name, dates.toDate]) + + return useFrappeGetCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance', args, + `bank-reconciliation-account-closing-balance-${bankAccount?.name}-${dates.toDate}`, + { + revalidateOnFocus: false + } + ) + +} + +/** + * Hook to fetch the closing balance set in the database for the given bank and date + */ +export const useGetAccountClosingBalanceAsPerStatement = (swrConfig: SWRConfiguration = {}) => { + + const dates = useAtomValue(bankRecDateAtom) + const bankAccount = useAtomValue(selectedBankAccountAtom) + + return useFrappeGetCall<{ message: { balance: number, date?: string } }>("erpnext.accounts.doctype.bank_account.bank_account.get_closing_balance_as_per_statement", { + bank_account: bankAccount?.name, + date: dates.toDate + }, `bank-reconciliation-account-closing-balance-as-per-statement-${bankAccount?.name}-${dates.toDate}`, { + revalidateOnFocus: false, + ...swrConfig + }) +} + +export type UnreconciledTransaction = Pick + + +export const useGetUnreconciledTransactions = () => { + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + return useFrappeGetCall<{ message: UnreconciledTransaction[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions', { + bank_account: bankAccount?.name, + from_date: dates.fromDate, + to_date: dates.toDate + }, bankAccount ? `bank-reconciliation-unreconciled-transactions-${bankAccount?.name}-${dates.fromDate}-${dates.toDate}` : null, { + revalidateOnFocus: false, + revalidateIfStale: false + }) +} + +export interface LinkedPayment { + rank: number, + doctype: string, + name: string, + paid_amount: number, + reference_no: string, + reference_date: string, + posting_date: string, + party_type?: string, + party?: string, + currency: string +} + +export const useGetBankTransactions = () => { + const bankAccount = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + return useFrappeGetCall<{ message: BankTransaction[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions', { + bank_account: bankAccount?.name, + from_date: dates.fromDate, + to_date: dates.toDate, + all_transactions: true + }, bankAccount ? `bank-reconciliation-bank-transactions-${bankAccount?.name}-${dates.fromDate}-${dates.toDate}` : null) +} + + +export const useGetVouchersForTransaction = (transaction: UnreconciledTransaction) => { + + const dates = useAtomValue(bankRecDateAtom) + + const matchFilters = useAtomValue(bankRecMatchFilters) + + return useFrappeGetCall<{ message: LinkedPayment[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_linked_payments', { + bank_transaction_name: transaction.name, + document_types: matchFilters ?? ['payment_entry', 'journal_entry'], + from_date: dates.fromDate, + to_date: dates.toDate, + filter_by_reference_date: 0 + }, `bank-reconciliation-vouchers-${transaction.name}-${dates.fromDate}-${dates.toDate}-${matchFilters.join(',')}`, { + revalidateOnFocus: false + }) +} + +/** + * Common hook to refresh the unreconciled transactions list after a transaction is reconciled + * @returns function to call to refresh the unreconciled transactions list AFTER the operation is done + */ +export const useRefreshUnreconciledTransactions = () => { + + const selectedBank = useAtomValue(selectedBankAccountAtom) + const dates = useAtomValue(bankRecDateAtom) + const matchFilters = useAtomValue(bankRecMatchFilters) + const setSelectedTransaction = useSetAtom(bankRecSelectedTransactionAtom(selectedBank?.name || '')) + + const { mutate } = useSWRConfig() + + const searchString = useAtomValue(bankRecSearchText) + const typeFilter = useAtomValue(bankRecTransactionTypeFilter) + const amountFilter = useAtomValue(bankRecAmountFilter) + + const { data: unreconciledTransactions } = useGetUnreconciledTransactions() + + /** + * This function should be called after a transaction is reconciled + * It will get the next unreconciled transaction and select it + * And then refresh the balance + unreconciled transactions list + */ + const onReconcileTransaction = (transaction: UnreconciledTransaction, updatedTransaction?: BankTransaction) => { + + // If the updated transaction has an unallocated amount of 0, then we need to select the next unreconciled transaction + if (updatedTransaction && updatedTransaction?.unallocated_amount !== 0) { + mutate(`bank-reconciliation-unreconciled-transactions-${selectedBank?.name}-${dates.fromDate}-${dates.toDate}`) + mutate(`bank-reconciliation-account-closing-balance-${selectedBank?.name}-${dates.toDate}`) + // Update the matching vouchers for the selected transaction + mutate(`bank-reconciliation-vouchers-${transaction.name}-${dates.fromDate}-${dates.toDate}-${matchFilters.join(',')}`) + return + } + + // From unreconciled transactions list, first apply the filters based on the search criteria and other filters + + const searchIndex = unreconciledTransactions ? new Fuse(unreconciledTransactions.message, { + keys: ['description', 'reference_number'], + threshold: 0.5, + includeScore: true + }) : null + + const results = getSearchResults(searchIndex, searchString, typeFilter, amountFilter.value, unreconciledTransactions?.message) + + const currentIndex = results.findIndex(t => t.name === transaction.name) + let nextTransaction = null + + if (currentIndex !== -1) { + // Check if there is a next transaction + if (currentIndex < (results.length || 0) - 1) { + nextTransaction = results[currentIndex + 1] + } + } + + // We need to select the next unreconciled transaction for a better UX + mutate(`bank-reconciliation-unreconciled-transactions-${selectedBank?.name}-${dates.fromDate}-${dates.toDate}`) + .then(res => { + if (nextTransaction) { + // Check if next transaction is there in the response + const nextTransactionObj = res?.message.find((t: UnreconciledTransaction) => t.name === nextTransaction.name) + if (nextTransactionObj) { + setSelectedTransaction([nextTransactionObj]) + } else { + // If the next transaction is not there in the response, we need to clear the selection + setSelectedTransaction([]) + } + } else { + // If there is no next transaction, we need to clear the selection + setSelectedTransaction([]) + } + }) + mutate(`bank-reconciliation-account-closing-balance-${selectedBank?.name}-${dates.toDate}`) + } + + return onReconcileTransaction + +} + +export const useReconcileTransaction = () => { + + const { call, loading } = useFrappePostCall<{ message: BankTransaction }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.reconcile_vouchers') + + const onReconcileTransaction = useRefreshUnreconciledTransactions() + + const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom) + + const addToActionLog = useUpdateActionLog() + + const reconcileTransaction = (transaction: UnreconciledTransaction, voucher: LinkedPayment) => { + + call({ + bank_transaction_name: transaction.name, + vouchers: JSON.stringify([{ + "payment_doctype": voucher.doctype, + "payment_name": voucher.name, + "amount": voucher.paid_amount + }]) + }).then((res) => { + addToActionLog({ + type: 'match', + timestamp: (new Date()).getTime(), + isBulk: false, + items: [ + { + bankTransaction: res.message, + voucher: { + reference_doctype: voucher.doctype, + reference_name: voucher.name, + reference_no: voucher.reference_no, + reference_date: voucher.reference_date, + posting_date: voucher.posting_date, + } + } + ] + }) + onReconcileTransaction(transaction, res.message) + toast.success(_("Reconciled"), { + duration: 4000, + closeButton: true, + action: { + label: _("Undo"), + onClick: () => setBankRecUnreconcileModalAtom(transaction.name) + }, + actionButtonStyle: { + backgroundColor: "rgb(0, 138, 46)" + } + }) + }).catch((error) => { + console.error(error) + toast.error(_("Error"), { + duration: 5000, + description: getErrorMessage(error) + }) + }) + } + + return { reconcileTransaction, loading } + +} + +interface BankAccountWithCurrency extends Pick { + account_currency?: string +} + +type BankLogoEntry = (typeof BANK_LOGOS)[number] + +/** Prefer the longest keyword match so short tokens (e.g. "anz" in "finanzas") do not beat full bank names. */ +function findBankLogoForName(bankName: string | undefined | null): BankLogoEntry | undefined { + if (!bankName) return undefined + const haystack = bankName.toLowerCase() + let best: BankLogoEntry | undefined + let bestKeywordLen = 0 + for (const entry of BANK_LOGOS) { + for (const keyword of entry.keywords) { + const needle = keyword.toLowerCase() + if (needle.length === 0) continue + if (haystack.includes(needle) && needle.length > bestKeywordLen) { + bestKeywordLen = needle.length + best = entry + } + } + } + return best +} + +export const useGetBankAccounts = (onSuccess?: (data?: Omit[]) => void, filterFn?: (bank: SelectedBank) => boolean) => { + + const company = useCurrentCompany() + + const { data, isLoading, error } = useFrappeGetCall<{ message: BankAccountWithCurrency[] }>('erpnext.accounts.doctype.bank_account.bank_account.get_list', { + company: company + }, undefined, { + revalidateOnFocus: false, + revalidateIfStale: false, + onSuccess: (data) => { + onSuccess?.(data?.message) + } + }) + + const banks = useMemo(() => { + // Match the bank account to the logo + const banksWithLogos = data?.message.map((bank) => { + const logo = findBankLogoForName(bank.bank) + return { + ...bank, + logo: logo?.logo, + logoDark: logo?.logoDark, + darkModeInvert: logo?.darkModeInvert, + logoClassName: logo?.logoClassName + } + }) ?? [] + + if (filterFn) { + return banksWithLogos.filter(filterFn) + } + + return banksWithLogos + }, [data, filterFn]) + + return { + banks, + isLoading, + error + } + +} + +export const useIsTransactionWithdrawal = (transaction: UnreconciledTransaction) => { + return useMemo(() => { + const isWithdrawal = transaction.withdrawal && transaction.withdrawal > 0 + const isDeposit = transaction.deposit && transaction.deposit > 0 + + return { + amount: isWithdrawal ? transaction.withdrawal : transaction.deposit, + isWithdrawal, + isDeposit + } + }, [transaction]) +} + +export const useGetRuleForTransaction = (transaction: UnreconciledTransaction) => { + + return useFrappeGetDoc('Bank Transaction Rule', transaction.matched_transaction_rule, + transaction.matched_transaction_rule ? undefined : null, { + revalidateOnFocus: false, + revalidateIfStale: false + } + ) +} + +/** Hook to handle the search input while maintaining debouncing and global state. */ +export function useTransactionSearch(): [string, DebouncedState<(value: string) => void>] { + const delay = 500 + const unwrappedInitialValue = '' + const eq = (left: string, right: string) => left === right + const [debouncedValue, setDebouncedValue] = useAtom(bankRecSearchText) + const previousValueRef = useRef(unwrappedInitialValue) + + const updateDebouncedValue = useDebounceCallback( + setDebouncedValue, + delay, + ) + + // Update the debounced value if the initial value changes + if (!eq(previousValueRef.current as string, unwrappedInitialValue)) { + updateDebouncedValue(unwrappedInitialValue) + previousValueRef.current = unwrappedInitialValue + } + + return [debouncedValue, updateDebouncedValue] +} + +/** Utility function to get the search results based on the search index, search string, type filter, amount filter and unreconciled transactions */ +export const getSearchResults = ( + /** Fuse index of the unreconciled transactions */ + searchIndex: Fuse | null, + /** Search string */ + search: string, + /** Type filter */ + typeFilter: string, + /** Amount filter */ + amountFilter: number, + /** Unreconciled transactions */ + unreconciledTransactions?: UnreconciledTransaction[]) => { + + let r = [] + if (!searchIndex || !search) { + r = unreconciledTransactions ?? [] + } else { + r = searchIndex.search(search).map((result) => result.item) + } + + if (typeFilter !== 'All') { + r = r.filter((transaction) => { + if (typeFilter === 'Debits') { + return transaction.withdrawal && transaction.withdrawal > 0 + } + if (typeFilter === 'Credits') { + return transaction.deposit && transaction.deposit > 0 + } + }) + } + + if (amountFilter > 0) { + r = r.filter((transaction) => { + if (transaction.withdrawal && transaction.withdrawal > 0) { + return transaction.withdrawal === amountFilter + } + if (transaction.deposit && transaction.deposit > 0) { + return transaction.deposit === amountFilter + } + return false + }) + } + + return r +} + +export const useUpdateActionLog = () => { + + const setActionLog = useSetAtom(bankRecActionLog) + + const addToActionLog = (action: ActionLog) => { + // Store at max 100 actions + setActionLog((prev) => { + const newActions = [action, ...prev] + if (newActions.length > 100) { + return newActions.slice(0, 100) + } + return newActions + }) + } + + return addToActionLog +} \ No newline at end of file diff --git a/banking/src/components/features/BankStatementImporter/CSV/CSVImport.tsx b/banking/src/components/features/BankStatementImporter/CSV/CSVImport.tsx new file mode 100644 index 00000000000..23edf987404 --- /dev/null +++ b/banking/src/components/features/BankStatementImporter/CSV/CSVImport.tsx @@ -0,0 +1,22 @@ +import CSVRawDataPreview from './CSVRawDataPreview' +import StatementDetails from './StatementDetails' +import _ from '@/lib/translate' +import { GetStatementDetailsResponse } from '../import_utils' + +const CSVImport = ({ data }: { data: { message: GetStatementDetailsResponse } }) => { + + + + return ( +
      +
      + +
      +
      + +
      +
      + ) +} + +export default CSVImport \ No newline at end of file diff --git a/banking/src/components/features/BankStatementImporter/CSV/CSVRawDataPreview.tsx b/banking/src/components/features/BankStatementImporter/CSV/CSVRawDataPreview.tsx new file mode 100644 index 00000000000..31a00a90694 --- /dev/null +++ b/banking/src/components/features/BankStatementImporter/CSV/CSVRawDataPreview.tsx @@ -0,0 +1,151 @@ +import { Table, TableBody, TableCell, TableHead, TableRow } from "@/components/ui/table" +import { cn } from "@/lib/utils" +import { ArrowDownRightIcon, ArrowUpDownIcon, ArrowUpRightIcon, BanknoteIcon, CalendarIcon, DollarSignIcon, FileTextIcon, ListIcon, ReceiptIcon } from "lucide-react" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import _ from "@/lib/translate" +import { GetStatementDetailsResponse } from "../import_utils" +import { useMemo } from "react" +import { BankStatementImportLogColumnMap } from "@/types/Accounts/BankStatementImportLogColumnMap" + + +const CSVRawDataPreview = ({ data }: { data: GetStatementDetailsResponse }) => { + + const column_mapping: Record = useMemo(() => { + + const col_map: Record = {} + + data.doc.column_mapping?.forEach(col => { + if (col.maps_to && col.maps_to !== "Do not import") { + col_map[col.maps_to] = col.index; + } + }) + + return col_map + + }, [data]) + + const validColumns = Object.values(column_mapping) + + // Reverse the column mapping to get a map of column index to variable name + const columnIndexMap: Record = Object.fromEntries(Object.entries(column_mapping).map(([variable, columnIndex]) => [columnIndex, variable as StandardColumnTypes])) + + // Loop over the contents of the CSV file and show a preview - highlight the header row and the transaction rows + return ( + + + {data.raw_data.map((row, index) => { + + const isHeaderRow = index === data.doc.detected_header_index; + const isTransactionRow = index >= (data.doc.detected_transaction_starting_index ?? 0) && index <= (data.doc.detected_transaction_ending_index ?? 0); + + return + {isHeaderRow ? + {index + 1} + : + + {index + 1} + + } + {row.map((cell, cellIndex) => { + + const isValidColumn = validColumns.includes(cellIndex); + const columnType = columnIndexMap[cellIndex]; + const isAmountColumn = ["Amount", "Withdrawal", "Deposit", "Balance"].includes(columnType); + + if (isHeaderRow) { + return +
      + {columnType && + + + + + {_(columnType)} + + + } + {cell} +
      +
      + } else { + return +
      + {cell} +
      +
      + } + } + + )} +
      + })} +
      +
      + ) +} + +type StandardColumnTypes = BankStatementImportLogColumnMap['maps_to']; + +const ColumnHeaderIcon = ({ columnType }: { columnType?: StandardColumnTypes }) => { + if (!columnType) { + return null + } + + if (columnType === 'Amount') { + return + } + + if (columnType === 'Withdrawal') { + return + } + + if (columnType === 'Deposit') { + return + } + + if (columnType === 'Balance') { + return + } + + if (columnType === 'Date') { + return + } + + if (columnType === 'Description') { + return + } + + if (columnType === 'Reference') { + return + } + + if (columnType === 'Transaction Type') { + return + } + + if (columnType === 'Debit/Credit') { + return + } + + return null +} + +export default CSVRawDataPreview \ No newline at end of file diff --git a/banking/src/components/features/BankStatementImporter/CSV/StatementDetails.tsx b/banking/src/components/features/BankStatementImporter/CSV/StatementDetails.tsx new file mode 100644 index 00000000000..74f40eb7e33 --- /dev/null +++ b/banking/src/components/features/BankStatementImporter/CSV/StatementDetails.tsx @@ -0,0 +1,351 @@ +import _ from '@/lib/translate' +import { GetStatementDetailsResponse } from '../import_utils' +import { flt, formatCurrency } from '@/lib/numbers' +import { formatDate } from '@/lib/date' +import { bankRecDateAtom } from '../../BankReconciliation/bankRecAtoms' +import { AlertCircleIcon, ChevronLeftIcon, ChevronRightIcon, ExternalLinkIcon, InfoIcon, Loader2Icon } from 'lucide-react' +import { H2, H3, Paragraph } from '@/components/ui/typography' +import { FileTypeIcon } from '@/components/ui/file-dropzone' +import { getFileExtension } from '@/lib/file' +import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { Separator } from '@/components/ui/separator' +import { Button } from '@/components/ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { useFrappeEventListener, useFrappePostCall } from 'frappe-react-sdk' +import { toast } from 'sonner' +import ErrorBanner from '@/components/ui/error-banner' +import { Link, useNavigate } from 'react-router-dom' +import { useMemo, useState } from 'react' +import { Progress } from '@/components/ui/progress' +import { useSetAtom } from 'jotai' +import { useDirection } from '@/components/ui/direction' +import BankLogo from '@/components/common/BankLogo' +import { useGetBankAccounts } from '../../BankReconciliation/utils' +import { BankStatementImportLog } from '@/types/Accounts/BankStatementImportLog' +import { Badge } from '@/components/ui/badge' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' + +const parseDateFormat = (dateFormat: string) => { + + const charMap = { + "%d": "DD", + "%m": "MM", + "%Y": "YYYY", + "%y": "YY", + "%b": "MMM", + "%B": "MMMM", + } + + let label = dateFormat + + Object.keys(charMap).forEach((char) => { + label = label.replace(char, charMap[char as keyof typeof charMap]) + }) + + return dateFormat + +} + +type Props = { + data: GetStatementDetailsResponse, +} + +const StatementDetails = ({ data }: Props) => { + const dateFormat = parseDateFormat(data.date_format) + + const { call, loading, error } = useFrappePostCall<{ docs: BankStatementImportLog[] }>('run_doc_method') + + const navigate = useNavigate() + + const setDates = useSetAtom(bankRecDateAtom) + + const direction = useDirection() + + const onImport = () => { + + call({ + docs: data.doc, + method: 'insert_transactions' + }).then((response) => { + const doc = response.docs ? response.docs[0] : undefined + if (doc && doc.start_date && doc.end_date) { + setDates({ + fromDate: doc.start_date, + toDate: doc.end_date, + }) + } + toast.success(_("Bank statement imported.")) + navigate(`/`) + }).catch(() => { + toast.error(_("There was an error while importing the bank statement.")) + }) + + } + + const [progress, setProgress] = useState(0) + + useFrappeEventListener("bank-rec-statement-import-progress", (event) => { + setProgress(event.progress) + }) + + const file_name = data.doc.file.split("/").pop() ?? "" + + const { banks } = useGetBankAccounts() + + const bank = useMemo(() => { + + return banks?.find((bank) => bank.name === data.doc.bank_account) + + }, [data.doc.bank_account, banks]) + + return ( +
      +
      +
      + + {data.doc.status === 'Completed' ? {_("Completed")} : + + } +
      +
      +
      +

      {_("Statement Details")}

      + + {_("We've auto-detected the details of the statement file.")} +
      + + {_("Please review the details below and click the 'Import' button to proceed.")} + +
      +
      +
      + + {progress > 0 &&
      + {_("Importing {0} transactions", [progress.toString()])} + +
      } + + {error && } + + + + + {_("Bank Account")} + +
      + + {bank?.account_name} + {bank?.account} +
      +
      +
      + + {_("Statement File")} + +
      + + {file_name} +
      +
      +
      + + {_("Transaction Dates")} + {_("{0} to {1}", [formatDate(data.doc.start_date, "Do MMMM YYYY"), formatDate(data.doc.end_date, "Do MMMM YYYY")])} + + + {_("Number of Transactions")} + {data.doc.number_of_transactions} + + + {_("Total Debits")} + {formatCurrency(flt(data.doc.total_debits, 2), data.currency)} ({data.doc.total_debit_transactions} {data.doc.total_debit_transactions === 1 ? _("transaction") : _("transactions")}) + + + {_("Total Credits")} + {formatCurrency(flt(data.doc.total_credits, 2), data.currency)} ({data.doc.total_credit_transactions} {data.doc.total_credit_transactions === 1 ? _("transaction") : _("transactions")}) + + + {_("Closing Balance as of {}", [formatDate(data.doc.end_date, "Do MMMM YYYY")])} + {formatCurrency(flt(data.doc.closing_balance, 2), data.currency)} + + + +
      + {_("Detected Amount Format")} + + + {_("The amount format detected in the statement file. This is used to parse the deposit and withdrawal values from each row.")} + + +
      +
      + {data.doc.detected_amount_format} +
      + + +
      + {_("Detected Date Format")} + + + + {_("The date format detected in the statement file. This is used to parse the date values.")} + + +
      +
      + + {dateFormat || data.date_format} (e.g.{" "} + {formatDate(new Date(), dateFormat || "YYYY-MM-DD")}) + +
      +
      +
      +
      + + {data.doc.status === "Not Started" ? <> + + + + + +
      +
      +

      {_("Preview Transactions")}

      + {data.final_transactions?.length === 1 ? ( + {_("We've found 1 transaction in the statement file that will be imported into the system. Please review the details below and click the 'Import' button to proceed.")} + ) : ( + {_("{0} transactions will be imported into the system. Please review the details below and click the 'Import' button to proceed.", [data.final_transactions?.length?.toString() || "0"])} + )} +
      +
      + + {_("Transactions to be imported into the system")} + + + # + {_("Date")} + {_("Description")} + {_("Ref.")} + {_("Withdrawal")} + {_("Deposit")} + + + + {data.final_transactions?.map((transaction, index) => ( + + {index + 1} + {formatDate(transaction.date)} + {transaction.description} + {transaction.reference} + {formatCurrency(transaction.withdrawal, data.currency)} + {formatCurrency(transaction.deposit, data.currency)} + + ))} + +
      +
      +
      + : null} +
      + + ) +} + +const ConflictingTransactions = ({ transactions }: { transactions: GetStatementDetailsResponse["conflicting_transactions"] }) => { + + if (transactions.length === 0) { + return null + } + + return <> + + + {_("Conflicting Transactions")} + + {transactions.length === 1 ? _("We've found 1 existing transaction in the system that conflicts with the transactions in the statement file. Are you sure you want to proceed with the import?") + : _("We've found {0} existing transactions in the system that conflict with the transactions in the statement file. Are you sure you want to proceed with the import?", [transactions.length.toString()])} + +
      + + + + + + + {_("Conflicting Transactions")} + + {transactions.length === 1 ? _("We've found 1 existing transaction in the system that conflicts with the transactions in the statement file. Are you sure you want to proceed with the import?") + : _("We've found {0} existing transactions in the system that conflict with the transactions in the statement file. Are you sure you want to proceed with the import?", [transactions.length.toString()])} + + + +
      + + {_("Existing transactions in the system belonging to the same bank account and date range")} + + + {_("Date")} + {_("Description")} + {_("Ref.")} + {_("Withdrawal")} + {_("Deposit")} + + + + + {transactions.map((transaction) => ( + + {formatDate(transaction.date)} + {transaction.description} + {transaction.reference_number ? transaction.reference_number : "-"} + {formatCurrency(transaction.withdrawal, transaction.currency)} + {formatCurrency(transaction.deposit, transaction.currency)} + + + + + + + {_("Open {0} in a new tab", [transaction.name])} + + + + + + ))} + +
      +
      + + + + + +
      + +
      +
      +
      +
      + +} + +export default StatementDetails \ No newline at end of file diff --git a/banking/src/components/features/BankStatementImporter/import_utils.ts b/banking/src/components/features/BankStatementImporter/import_utils.ts new file mode 100644 index 00000000000..1f918977751 --- /dev/null +++ b/banking/src/components/features/BankStatementImporter/import_utils.ts @@ -0,0 +1,42 @@ +import { BankStatementImportLog } from "@/types/Accounts/BankStatementImportLog" +import { useFrappeGetCall } from "frappe-react-sdk" + + +export interface GetStatementDetailsResponse { + doc: BankStatementImportLog, + conflicting_transactions: Array<{ + name: string, + date: string, + withdrawal: number, + deposit: number, + description: string, + reference_number: string, + currency: string, + }>, + final_transactions: Array<{ + date: string, + withdrawal: number, + deposit: number, + description: string, + reference: string, + transaction_type?: string, + debit_credit?: string, + included_fee?: number, + excluded_fee?: number, + party_name?: string, + party_account_number?: string, + party_iban?: string, + }>, + date_format: string, + raw_data: Array>, + currency: string, +} + +export const useGetStatementDetails = (id: string) => { + return useFrappeGetCall<{ message: GetStatementDetailsResponse }>("erpnext.accounts.doctype.bank_statement_import_log.bank_statement_import_log.get_statement_details", { + statement_import_id: id, + }, undefined, { + revalidateOnFocus: false + }) + +} \ No newline at end of file diff --git a/banking/src/components/features/Settings/KeyboardShortcuts.tsx b/banking/src/components/features/Settings/KeyboardShortcuts.tsx new file mode 100644 index 00000000000..435a0ac2ab0 --- /dev/null +++ b/banking/src/components/features/Settings/KeyboardShortcuts.tsx @@ -0,0 +1,115 @@ +import { Badge } from '@/components/ui/badge' +import { Kbd, KbdGroup } from '@/components/ui/kbd' +import { KeyboardMetaKeyIcon } from '@/components/ui/keyboard-keys' +import { SettingsPanelDescription, SettingsPanelTitle, SettingsPanelHeader, SettingsPanelContent } from '@/components/ui/settings-dialog' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import _ from '@/lib/translate' +import { ArrowRightLeftIcon, HistoryIcon, LandmarkIcon, ReceiptIcon, SaveIcon, SettingsIcon, ZapIcon } from 'lucide-react' + +const Shortcuts = [ + { + shortcut: B, + action: { + icon: , + label: _("Bank Entry"), + description: _("Record a bank journal entry for expenses, income or split transactions") + } + }, + { + shortcut: P, + action: { + icon: , + label: _("Record Payment"), + description: _("Record a payment against a customer or supplier") + } + }, + { + shortcut: I, + action: { + icon: , + label: _("Transfer"), + description: _("Record a transfer between two bank accounts") + } + }, + { + shortcut: R, + action: { + icon: , + label: _("Accept Matching Rule"), + description: _("Accept the rule for the selected transaction") + } + }, + { + shortcut: S, + action: { + icon: , + label: _("Save"), + description: _("Save the currently opened form") + } + }, + { + shortcut: Z, + action: { + icon: , + label: _("Reconciliation History"), + description: _("View all reconciliation actions taken in this session") + } + }, + { + shortcut: G, + action: { + icon: , + label: _("Settings"), + description: _("Open the settings dialog") + } + } +] + +const KeyboardShortcuts = () => { + return ( + <> + + {_("Keyboard Shortcuts")} + {_("Get around the system quickly with keyboard shortcuts")} + + +
      +

      + {_("Transaction actions work when one or more unreconciled transactions are selected.")} +
      + {_("To select more than one transaction at a time, press and hold the shift key.")} +

      + + + + {_("Shortcut")} + {_("Action")} + {_("Description")} + + + + {Shortcuts.map((shortcut) => ( + + + {shortcut.shortcut} + + + + {shortcut.action.icon} + {shortcut.action.label} + + + +

      {shortcut.action.description}

      +
      +
      + ))} +
      +
      +
      +
      + + ) +} + +export default KeyboardShortcuts \ No newline at end of file diff --git a/banking/src/components/features/Settings/MatchingRules.tsx b/banking/src/components/features/Settings/MatchingRules.tsx new file mode 100644 index 00000000000..5e42856b199 --- /dev/null +++ b/banking/src/components/features/Settings/MatchingRules.tsx @@ -0,0 +1,46 @@ +import { Button } from '@/components/ui/button' +import { SettingsPanelTitle, SettingsPanelHeader, SettingsPanelDescription, SettingsPanelContent } from '@/components/ui/settings-dialog' +import _ from '@/lib/translate' +import { PlusIcon } from 'lucide-react' +import { useState } from 'react' +import RuleList, { RunRulesButton } from './Rules/RuleList' +import CreateNewRule from '../BankReconciliation/Rules/CreateNewRule' +import EditRule from '../BankReconciliation/Rules/EditRule' + +const MatchingRules = () => { + + const [selectedRule, setSelectedRule] = useState(null) + const [isNewRule, setIsNewRule] = useState(false) + + + if (isNewRule) { + return setIsNewRule(false)} /> + } + + if (selectedRule) { + return setSelectedRule(null)} ruleID={selectedRule} /> + } + + return ( + <> + + + + + } + > + {_("Transaction Matching Rules")} + + + {_("Set up rules to automatically classify transactions. Drag and drop rules to reorder their priority.")} + + + + + + + ) +} +export default MatchingRules diff --git a/banking/src/components/features/Settings/Preferences.tsx b/banking/src/components/features/Settings/Preferences.tsx new file mode 100644 index 00000000000..b182485a7ec --- /dev/null +++ b/banking/src/components/features/Settings/Preferences.tsx @@ -0,0 +1,261 @@ +import ErrorBanner from "@/components/ui/error-banner" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" +import { SettingsPanelDescription, SettingsPanelHeader, SettingsPanelTitle, SettingsPanelContent } from "@/components/ui/settings-dialog" +import { Switch } from "@/components/ui/switch" +import { useTheme } from "@/components/ui/theme-provider" +import _ from "@/lib/translate" +import { AccountsSettings } from "@/types/Accounts/AccountsSettings" +import { useFrappeGetDoc, useFrappeUpdateDoc } from "frappe-react-sdk" +import { toast } from "sonner" + + +export const Preferences = () => { + + + const { data: accountsSettings, mutate, error: fetchError, isLoading } = useFrappeGetDoc("Accounts Settings", "Accounts Settings", undefined, { + revalidateOnFocus: false + }) + + const { updateDoc, error } = useFrappeUpdateDoc() + + const onUpdate = (field: keyof AccountsSettings, value: any) => { + mutate(updateDoc("Accounts Settings", "Accounts Settings", { + [field]: value + }), { + optimisticData: { + ...accountsSettings as AccountsSettings, + [field]: value + }, + revalidate: false, + }).then(() => { + toast.success(_("Preferences updated"), { + dismissible: true, + duration: 500, + }) + }) + } + + return <> + + + {_("Preferences")} + {_("Configure settings for the banking module")} + + + +
      + {fetchError && } + {error && } + +
      + + + +
      +
      + +

      + {_("For example, if set to 4, the system will try to find matching transfer transactions in other banks 4 days before and after the transaction date. This is because transactions can clear on different days on different bank accounts.")} +

      +
      +
      + +
      +
      + + + +
      +
      + +

      + {_("This will automatically run transaction matching rules on unreconciled transactions every hour.")} +

      +
      +
      + onUpdate("automatically_run_rules_on_unreconciled_transactions", checked ? 1 : 0)} + /> +
      +
      + + + +
      +
      + +

      + {_("The system will attempt to automatically match a party to a bank transaction based on account number or IBAN.")} + +

      +
      +
      + onUpdate("enable_party_matching", checked ? 1 : 0)} + /> +
      +
      + + + +
      +
      + +

      + {_("If a party cannot be matched by account number or IBAN, the system will try fuzzy matching using the party name and transaction description.")} + +

      +
      +
      + onUpdate("enable_fuzzy_matching", checked ? 1 : 0)} + /> +
      +
      + +
      + + + + {/* */} + +
      +
      + +} + + +const ThemeSwitcher = () => { + + const { theme, setTheme } = useTheme() + + const themeCards: Array<{ value: "Light" | "Dark" | "Automatic", label: string }> = [ + { + value: "Light", + label: _("Light"), + }, + { + value: "Dark", + label: _("Dark"), + }, + { + value: "Automatic", + label: _("System"), + }, + ] + + return
      +
      + +

      + {_("Switch between light, dark, or system theme")} +

      +
      +
      + {themeCards.map((option) => { + const selected = theme === option.value + + return ( + + ) + })} +
      +
      + +} + +const ThemePreviewWindow = ({ theme, roundedClass }: { theme: "light" | "dark", roundedClass: string }) => { + const isLight = theme === "light" + const frameClass = isLight ? "bg-white border-gray-100" : "bg-gray-900 border-gray-800" + const subtleSurfaceClass = isLight ? "bg-gray-50" : "bg-gray-800" + const mutedLineClass = isLight ? "bg-gray-200" : "bg-gray-700" + const mutedLineStrongClass = isLight ? "bg-gray-300" : "bg-gray-600" + const dividerClass = isLight ? "border-gray-100" : "border-gray-800" + const cardClass = isLight ? "bg-white border-gray-200" : "bg-gray-900 border-gray-700" + + return
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + {/*
      */} +
      +
      +
      +
      +
      +
      +
      +
      +} \ No newline at end of file diff --git a/banking/src/components/features/Settings/Rules/RuleList.tsx b/banking/src/components/features/Settings/Rules/RuleList.tsx new file mode 100644 index 00000000000..a05b8735235 --- /dev/null +++ b/banking/src/components/features/Settings/Rules/RuleList.tsx @@ -0,0 +1,314 @@ +import { Button } from "@/components/ui/button" +import ErrorBanner from "@/components/ui/error-banner" +import { Skeleton } from "@/components/ui/skeleton" +import { Badge } from "@/components/ui/badge" +import _ from "@/lib/translate" +import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule" +import { FrappeConfig, FrappeContext, useFrappeGetCall, useFrappeGetDocList, useFrappePostCall } from "frappe-react-sdk" +import { ArrowDownRight, ArrowDownUp, ArrowUpRight, MoreVertical, Trash2, GripVertical, Play, RefreshCw, ZapIcon, CalendarSyncIcon } from "lucide-react" +import { useContext, useState } from "react" +import { toast } from "sonner" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, DropdownMenuCheckboxItem } from "@/components/ui/dropdown-menu" +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from '@dnd-kit/core' +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' +import { + useSortable, +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty" +import { cn } from "@/lib/utils" + +const useGetRuleList = () => { + return useFrappeGetDocList("Bank Transaction Rule", { + fields: ["name", "rule_name", "rule_description", "transaction_type", "priority"], + orderBy: { + field: 'priority', + order: 'asc' + }, + limit: 100 + }) +} + +export const RunRulesButton = () => { + + const { data } = useGetRuleList() + + const { call: runRuleEvaluation, loading: isRunningRules } = useFrappePostCall('erpnext.accounts.doctype.bank_transaction_rule.bank_transaction_rule.run_rule_evaluation') + + const handleRunRules = async (forceEvaluate: boolean = false) => { + try { + await runRuleEvaluation({ + force_evaluate: forceEvaluate + }) + toast.success(forceEvaluate ? _("Rules evaluation started") : _("Rules evaluation completed")) + } catch (error) { + toast.error(_("Failed to run rules evaluation")) + console.error("Error running rules evaluation:", error) + } + } + + if (!data || data.length === 0) { + return null + } + + return + + + + + handleRunRules(false)} disabled={isRunningRules} title={_("Run rules on unreconciled transactions that haven't been evaluated yet")}> + + {_("Run on new transactions")} + + handleRunRules(true)} disabled={isRunningRules} title={_("Force re-evaluate all unreconciled transactions, even if they were previously evaluated")}> + + {_("Force evaluate all")} + + + + + +} + +const AutoRunRuleItem = () => { + + const { db } = useContext(FrappeContext) as FrappeConfig + + const { data: accountsSetting, mutate: setAutomaticallyRunRulesOnUnreconciledTransactions } = useFrappeGetCall("frappe.client.get_single_value", { + "doctype": "Accounts Settings", + "field": "automatically_run_rules_on_unreconciled_transactions" + }) + + const automaticallyRunRulesOnUnreconciledTransactions = accountsSetting?.message ? true : false + + const onAutoClassifyTransactions = (checked: boolean) => { + toast.promise(db.setValue("Accounts Settings", "Accounts Settings", "automatically_run_rules_on_unreconciled_transactions", checked ? 1 : 0).then(() => { + setAutomaticallyRunRulesOnUnreconciledTransactions({ + message: { + automatically_run_rules_on_unreconciled_transactions: checked ? 1 : 0, + } + }, { + revalidate: false + }) + }), { + loading: _("Updating..."), + success: checked ? _("Scheduled job enabled. Transactions will be auto classified.") : _("Scheduled job disabled. Transactions will not be auto classified."), + error: _("Failed to update auto classify transactions settings") + }) + } + + + return + + {_("Run rules automatically")} + +} + + + +const RuleList = ({ setSelectedRule }: { setSelectedRule: (rule: string) => void }) => { + + const { data, error, isLoading, mutate } = useGetRuleList() + + const { db } = useContext(FrappeContext) as FrappeConfig + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ) + + const onDeleteRule = (ruleID: string) => { + toast.promise(db.deleteDoc("Bank Transaction Rule", ruleID).then(() => { + mutate() + }), { + loading: _("Deleting rule..."), + success: _("Rule deleted."), + error: _("Failed to delete rule.") + }) + } + + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event + + if (active.id !== over?.id && data) { + const oldIndex = data.findIndex((rule) => rule.name === active.id) + const newIndex = data.findIndex((rule) => rule.name === over?.id) + + const newData = arrayMove(data, oldIndex, newIndex) + + // Update priorities based on new order + const updatePromises = newData.map((rule, index) => { + const newPriority = index + 1 + if (rule.priority !== newPriority) { + return db.setValue("Bank Transaction Rule", rule.name, "priority", newPriority) + } + return Promise.resolve() + }) + + try { + await Promise.all(updatePromises) + toast.success(_("Rule priorities updated")) + mutate() // Refresh the data + } catch (error) { + toast.error(_("Failed to update rule priorities")) + console.error("Error updating priorities:", error) + } + } + } + + return ( + <> +
      + {isLoading &&
      + + + + + +
      } + + {error && } + + {data && data.length === 0 && + + + + + {_("No rules setup yet")} + {_("Configure rules to save time when reconciling transactions.")} + + + } + + {data && data.length > 0 && ( + + rule.name)} + strategy={verticalListSortingStrategy} + > +
        + {data?.map((rule) => ( + + ))} +
      +
      +
      + )} +
      + + ) +} +const SortableRuleItem = ({ + rule, + setSelectedRule, + onDeleteRule +}: { + rule: BankTransactionRule + setSelectedRule: (rule: string) => void + onDeleteRule: (ruleID: string) => void +}) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: rule.name }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + } + + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + + return ( +
    • +
      +
      +
      + +
      + + {rule.priority} + +
      +
      + +
      + {rule.transaction_type === "Any" ? : rule.transaction_type === "Withdrawal" ? : } +
      +
      + + {rule.rule_description} + +
      +
      + +
      + + + + + + onDeleteRule(rule.name)}> + + {_("Delete")} + + + +
      +
      +
    • + ) +} + +export default RuleList diff --git a/banking/src/components/features/Settings/Settings.tsx b/banking/src/components/features/Settings/Settings.tsx new file mode 100644 index 00000000000..623fceea4ac --- /dev/null +++ b/banking/src/components/features/Settings/Settings.tsx @@ -0,0 +1,95 @@ +import { Button } from '@/components/ui/button' +import { Dialog, DialogTrigger } from '@/components/ui/dialog' +import { + SettingsDialog, + SettingsPanel, + SettingsPanels, + SettingsTabGroup, + SettingsTabItem, + SettingsTabs, +} from '@/components/ui/settings-dialog' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import _ from '@/lib/translate' +import { KeyboardIcon, SettingsIcon, SlidersVerticalIcon, ZapIcon } from 'lucide-react' +import { useState } from 'react' +import { Preferences } from './Preferences' +import MatchingRules from './MatchingRules' +import KeyboardShortcuts from './KeyboardShortcuts' +import { useHotkeys } from 'react-hotkeys-hook' + +const Settings = () => { + + const [isOpen, setIsOpen] = useState(false) + + useHotkeys('shift+meta+g', () => { + setIsOpen(x => !x) + }, { + enabled: true, + preventDefault: true, + enableOnFormTags: false + }) + + return ( + + + + + + + + + {_("Settings")} + + + setIsOpen(false)}> + + + } + label={_("Preferences")} + value="preferences" + /> + } + label={_("Matching Rules")} + value="rules" + /> + {/* } + label={_("Bank Accounts")} + value="bank-accounts" + /> + } + label={_("Masters")} + value="masters" + /> */} + } + label={_("Keyboard Shortcuts")} + value="keyboard-shortcuts" + /> + + + + + + + + + + + + + + + + + + + ) +} + +export default Settings diff --git a/banking/src/components/ui/alert-dialog.tsx b/banking/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000000..cbab3099bb3 --- /dev/null +++ b/banking/src/components/ui/alert-dialog.tsx @@ -0,0 +1,196 @@ +import * as React from "react" +import { AlertDialog as AlertDialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
      + ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
      + ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
      + ) +} + +function AlertDialogAction({ + className, + variant = "solid", + size = "md", + theme = "red", + ...props +}: React.ComponentProps & + Pick, "variant" | "size" | "theme">) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + variant = "outline", + size = "md", + theme = "gray", + ...props +}: React.ComponentProps & + Pick, "variant" | "size" | "theme">) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +} diff --git a/banking/src/components/ui/alert.tsx b/banking/src/components/ui/alert.tsx new file mode 100644 index 00000000000..556f064a2b3 --- /dev/null +++ b/banking/src/components/ui/alert.tsx @@ -0,0 +1,104 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3.5 text-base grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-1 [&>svg]:text-current", + { + variants: { + variant: { + subtle: "bg-surface-white", + outline: "border border-outline-gray-3", + }, + theme: { + gray: "text-ink-gray-8", + blue: "text-ink-blue-3", + green: "text-ink-green-3", + red: "text-ink-red-3", + amber: "text-ink-amber-3", + } + }, + compoundVariants: [ + // Subtle alerts + { + theme: "gray", + variant: "subtle", + className: "bg-surface-gray-2 border-outline-gray-1" + }, + { + theme: "blue", + variant: "subtle", + className: "bg-surface-blue-2 border-surface-blue-2" + }, + { + theme: "green", + variant: "subtle", + className: "bg-surface-green-2 border-surface-green-2" + }, + { + theme: "red", + variant: "subtle", + className: "bg-surface-red-2 border-surface-red-2" + }, + { + theme: "amber", + variant: "subtle", + className: "bg-surface-amber-2 border-surface-amber-2" + } + ], + defaultVariants: { + variant: "subtle", + theme: "gray", + }, + } +) + +export type AlertProps = React.ComponentProps<"div"> & VariantProps + +function Alert({ + className, + variant, + theme, + ...props +}: AlertProps) { + return ( +
      + ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
      + ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
      + ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/banking/src/components/ui/badge.tsx b/banking/src/components/ui/badge.tsx new file mode 100644 index 00000000000..099588f0b02 --- /dev/null +++ b/banking/src/components/ui/badge.tsx @@ -0,0 +1,188 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center select-none rounded-full whitespace-nowrap gap-1 w-fit shrink-0 [&>svg]:pointer-events-none transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + solid: "", + subtle: "", + outline: "bg-transparent border", + ghost: "bg-transparent", + }, + size: { + sm: 'h-4 text-xs px-1.5 [&>svg]:size-2.5', + md: 'h-5 text-xs px-1.5 [&>svg]:size-3', + lg: 'h-6 text-sm px-2 [&>svg]:size-3', + }, + theme: { + gray: "", + blue: "", + green: "", + red: "", + orange: "", + violet: "", + } + }, + compoundVariants: [ + // Solid badges + { + variant: "solid", + theme: "gray", + className: "text-ink-white bg-surface-gray-7 [a&]:hover:bg-surface-gray-8" + }, + { + variant: "solid", + theme: "blue", + className: "text-ink-blue-1 bg-surface-blue-5 [a&]:hover:bg-surface-blue-6" + }, + { + variant: "solid", + theme: "green", + className: "text-ink-green-1 bg-surface-green-5 [a&]:hover:bg-surface-green-6" + }, + { + variant: "solid", + theme: "orange", + className: "text-ink-amber-1 bg-surface-amber-5 [a&]:hover:bg-surface-amber-6" + }, + { + variant: "solid", + theme: "red", + className: "text-ink-red-1 bg-surface-red-5 [a&]:hover:bg-surface-red-6" + }, + { + variant: "solid", + theme: "violet", + className: "text-ink-violet-1 bg-surface-violet-5 [a&]:hover:bg-surface-violet-6" + }, + // Subtle badge + { + variant: "subtle", + theme: "gray", + className: "text-ink-gray-6 bg-surface-gray-2 [a&]:hover:bg-surface-gray-3" + }, + { + variant: "subtle", + theme: "blue", + className: "text-ink-blue-4 bg-surface-blue-2 [a&]:hover:bg-surface-blue-3" + }, + { + variant: "subtle", + theme: "green", + className: "text-ink-green-4 bg-surface-green-2 [a&]:hover:bg-surface-green-3" + }, + { + variant: "subtle", + theme: "orange", + className: "text-ink-amber-4 bg-surface-amber-2 [a&]:hover:bg-surface-amber-3" + }, + { + variant: "subtle", + theme: "red", + className: "text-ink-red-4 bg-surface-red-2 [a&]:hover:bg-surface-red-3" + }, + { + variant: "subtle", + theme: "violet", + className: "text-ink-violet-4 bg-surface-violet-2 [a&]:hover:bg-surface-violet-3" + }, + // Outline badge + { + variant: "outline", + theme: "gray", + className: "text-ink-gray-6 border-outline-gray-2 [a&]:hover:bg-surface-gray-2" + }, + { + variant: "outline", + theme: "blue", + className: "text-ink-blue-4 border-outline-blue-2 [a&]:hover:bg-surface-blue-2" + }, + { + variant: "outline", + theme: "green", + className: "text-ink-green-4 border-outline-green-2 [a&]:hover:bg-surface-green-2" + }, + { + variant: "outline", + theme: "orange", + className: "text-ink-amber-4 border-outline-amber-2 [a&]:hover:bg-surface-amber-2" + }, + { + variant: "outline", + theme: "red", + className: "text-ink-red-4 border-outline-red-2 [a&]:hover:bg-surface-red-2" + }, + { + variant: "outline", + theme: "violet", + className: "text-ink-violet-4 border-outline-violet-2 [a&]:hover:bg-surface-violet-2" + }, + // Ghost badge + { + variant: "ghost", + theme: "gray", + className: "text-ink-gray-6" + }, + { + variant: "ghost", + theme: "blue", + className: "text-ink-blue-4" + }, + { + variant: "ghost", + theme: "green", + className: "text-ink-green-4" + }, + { + variant: "ghost", + theme: "orange", + className: "text-ink-amber-4" + }, + { + variant: "ghost", + theme: "red", + className: "text-ink-red-4" + }, + { + variant: "ghost", + theme: "violet", + className: "text-ink-violet-4" + } + ], + defaultVariants: { + variant: "subtle", + size: "md", + theme: "gray", + }, + } +) + +function Badge({ + className, + variant = "subtle", + size = "md", + theme = "gray", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/banking/src/components/ui/breadcrumb.tsx b/banking/src/components/ui/breadcrumb.tsx new file mode 100644 index 00000000000..73dfac94c6f --- /dev/null +++ b/banking/src/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { MoreHorizontal } from "lucide-react" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return