diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37bb37e1d24..93b17323681 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: 18 + node-version: 20 - name: Setup dependencies run: | npm install @semantic-release/git @semantic-release/exec --no-save diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py index 5a6bb6976f9..adf59254435 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py @@ -76,6 +76,7 @@ class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase): "deposit": 100, "bank_account": self.bank_account, "reference_number": "123", + "currency": "INR", } ) .save() diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index c38d27355fc..fef3b569ed2 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -49,6 +49,24 @@ class BankTransaction(Document): def validate(self): self.validate_duplicate_references() + self.validate_currency() + + def validate_currency(self): + """ + Bank Transaction should be on the same currency as the Bank Account. + """ + if self.currency and self.bank_account: + account = frappe.get_cached_value("Bank Account", self.bank_account, "account") + account_currency = frappe.get_cached_value("Account", account, "account_currency") + + if self.currency != account_currency: + frappe.throw( + _( + "Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}" + ).format( + frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency) + ) + ) def set_status(self): if self.docstatus == 2: diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 347cae05b72..adab54b3756 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -126,7 +126,7 @@ "fieldname": "rate", "fieldtype": "Float", "in_list_view": 1, - "label": "Rate", + "label": "Tax Rate", "oldfieldname": "rate", "oldfieldtype": "Currency" }, @@ -230,7 +230,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-05 20:04:36.618240", + "modified": "2024-01-14 10:04:36.618240", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", @@ -239,4 +239,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 72e574c299a..ce56a7b0a26 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -16,6 +16,7 @@ from frappe.utils.data import ( date_diff, flt, get_last_day, + get_link_to_form, getdate, nowdate, ) @@ -317,6 +318,37 @@ class Subscription(Document): if self.is_new(): self.set_subscription_status() + self.validate_party_billing_currency() + + def validate_party_billing_currency(self): + """ + Subscription should be of the same currency as the Party's default billing currency or company default. + """ + if self.party: + party_billing_currency = frappe.get_cached_value( + self.party_type, self.party, "default_currency" + ) or frappe.get_cached_value("Company", self.company, "default_currency") + + plans = [x.plan for x in self.plans] + subscription_plan_currencies = frappe.db.get_all( + "Subscription Plan", filters={"name": ("in", plans)}, fields=["name", "currency"] + ) + unsupported_plans = [] + for x in subscription_plan_currencies: + if x.currency != party_billing_currency: + unsupported_plans.append("{0}".format(get_link_to_form("Subscription Plan", x.name))) + + if unsupported_plans: + unsupported_plans = [ + _( + "Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0}" + ).format(frappe.bold(party_billing_currency)) + ] + unsupported_plans + + frappe.throw( + unsupported_plans, frappe.ValidationError, "Unsupported Subscription Plans", as_list=True + ) + def validate_trial_period(self) -> None: """ Runs sanity checks on trial period dates for the `Subscription` diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 785fd04b82e..a46642ad500 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -460,11 +460,13 @@ class TestSubscription(FrappeTestCase): self.assertEqual(len(subscription.invoices), 1) def test_multi_currency_subscription(self): + party = "_Test Subscription Customer" + frappe.db.set_value("Customer", party, "default_currency", "USD") subscription = create_subscription( start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period", - plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}], - party="_Test Subscription Customer", + plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}], + party=party, ) subscription.process() @@ -528,13 +530,21 @@ class TestSubscription(FrappeTestCase): def make_plans(): - create_plan(plan_name="_Test Plan Name", cost=900) - create_plan(plan_name="_Test Plan Name 2", cost=1999) + create_plan(plan_name="_Test Plan Name", cost=900, currency="INR") + create_plan(plan_name="_Test Plan Name 2", cost=1999, currency="INR") create_plan( - plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14 + plan_name="_Test Plan Name 3", + cost=1999, + billing_interval="Day", + billing_interval_count=14, + currency="INR", ) create_plan( - plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3 + plan_name="_Test Plan Name 4", + cost=20000, + billing_interval="Month", + billing_interval_count=3, + currency="INR", ) create_plan( plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD" diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json index 563df79eec7..bc1f579cf07 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json @@ -41,7 +41,8 @@ "fieldname": "currency", "fieldtype": "Link", "label": "Currency", - "options": "Currency" + "options": "Currency", + "reqd": 1 }, { "fieldname": "column_break_3", @@ -148,10 +149,11 @@ } ], "links": [], - "modified": "2021-12-10 15:24:15.794477", + "modified": "2024-01-14 17:59:34.687977", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Plan", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -193,5 +195,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index 118d2547808..cdfa3e56d9f 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -24,7 +24,7 @@ class SubscriptionPlan(Document): billing_interval_count: DF.Int cost: DF.Currency cost_center: DF.Link | None - currency: DF.Link | None + currency: DF.Link item: DF.Link payment_gateway: DF.Link | None plan_name: DF.Data diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 9c356bf28ea..d6a4755d6ae 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -84,10 +84,6 @@ function get_filters() { options: budget_against_options, default: "Cost Center", reqd: 1, - get_data: function() { - console.log(this.options); - return ["Emacs", "Rocks"]; - }, on_change: function() { frappe.query_report.set_filter_value("budget_against_filter", []); frappe.query_report.refresh(); diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index a2c0f86f6c5..f4a01759ddc 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -376,6 +376,10 @@ class PartyLedgerSummaryReport(object): if not income_or_expense_accounts: # prevent empty 'in' condition income_or_expense_accounts.append("") + else: + # escape '%' in account name + # ignoring frappe.db.escape as it replaces single quotes with double quotes + income_or_expense_accounts = [x.replace("%", "%%") for x in income_or_expense_accounts] accounts_query = ( qb.from_(gl) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index ff6cd9f4b25..110ec758fc1 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -448,6 +448,10 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): for gle in gl_entries: group_by_value = gle.get(group_by) gle.voucher_type = _(gle.voucher_type) + gle.voucher_subtype = _(gle.voucher_subtype) + gle.against_voucher_type = _(gle.against_voucher_type) + gle.remarks = _(gle.remarks) + gle.party_type = _(gle.party_type) if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries): if not group_by_voucher_consolidated: diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js index 39fb3ca5ee3..06f426b71e6 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js @@ -59,7 +59,21 @@ frappe.query_reports["Item-wise Sales Register"] = { "fieldname": "group_by", "fieldtype": "Select", "options": ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"] - } + }, + { + "fieldname": "income_account", + "label": __("Income Account"), + "fieldtype": "Link", + "options": "Account", + get_query: () => { + let company = frappe.query_report.get_filter_value('company'); + return { + filters: { + 'company': company, + } + }; + } + }, ], "formatter": function(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index ce22d7566c1..56ae41a5e19 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -83,9 +83,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions= "company": d.company, "sales_order": d.sales_order, "delivery_note": d.delivery_note, - "income_account": d.unrealized_profit_loss_account - if d.is_internal_customer == 1 - else d.income_account, + "income_account": get_income_account(d), "cost_center": d.cost_center, "stock_qty": d.stock_qty, "stock_uom": d.stock_uom, @@ -150,6 +148,15 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions= return columns, data, None, None, None, skip_total_row +def get_income_account(row): + if row.enable_deferred_revenue: + return row.deferred_revenue_account + elif row.is_internal_customer == 1: + return row.unrealized_profit_loss_account + else: + return row.income_account + + def get_columns(additional_table_columns, filters): columns = [] @@ -358,6 +365,13 @@ def get_conditions(filters, additional_conditions=None): if filters.get("item_group"): conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s""" + if filters.get("income_account"): + conditions += """ + and (ifnull(`tabSales Invoice Item`.income_account, '') = %(income_account)s + or ifnull(`tabSales Invoice Item`.deferred_revenue_account, '') = %(income_account)s + or ifnull(`tabSales Invoice`.unrealized_profit_loss_account, '') = %(income_account)s) + """ + if not filters.get("group_by"): conditions += ( "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc" @@ -399,6 +413,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None): `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group, `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center, + `tabSales Invoice Item`.enable_deferred_revenue, `tabSales Invoice Item`.deferred_revenue_account, `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail, diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 2650753275c..8ebdcc58752 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -421,23 +421,14 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): meta = frappe.get_meta(doctype, cached=True) searchfields = meta.get_search_fields() - query = get_batches_from_stock_ledger_entries(searchfields, txt, filters) - bundle_query = get_batches_from_serial_and_batch_bundle(searchfields, txt, filters) - - data = ( - frappe.qb.from_((query) + (bundle_query)) - .select("batch_no", "qty", "manufacturing_date", "expiry_date") - .offset(start) - .limit(page_len) + batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len) + batches.extend( + get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len) ) - for field in searchfields: - data = data.select(field) + filtered_batches = get_filterd_batches(batches) - data = data.run() - data = get_filterd_batches(data) - - return data + return filtered_batches def get_filterd_batches(data): @@ -457,7 +448,7 @@ def get_filterd_batches(data): return filterd_batch -def get_batches_from_stock_ledger_entries(searchfields, txt, filters): +def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, page_len=100): stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") batch_table = frappe.qb.DocType("Batch") @@ -479,6 +470,8 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters): & (stock_ledger_entry.batch_no.isnotnull()) ) .groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse) + .offset(start) + .limit(page_len) ) query = query.select( @@ -493,16 +486,16 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters): query = query.select(batch_table[field]) if txt: - txt_condition = batch_table.name.like(txt) + txt_condition = batch_table.name.like("%{0}%".format(txt)) for field in searchfields + ["name"]: - txt_condition |= batch_table[field].like(txt) + txt_condition |= batch_table[field].like("%{0}%".format(txt)) query = query.where(txt_condition) - return query + return query.run(as_list=1) or [] -def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters): +def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0, page_len=100): bundle = frappe.qb.DocType("Serial and Batch Entry") stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") batch_table = frappe.qb.DocType("Batch") @@ -527,6 +520,8 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters): & (stock_ledger_entry.serial_and_batch_bundle.isnotnull()) ) .groupby(bundle.batch_no, bundle.warehouse) + .offset(start) + .limit(page_len) ) bundle_query = bundle_query.select( @@ -541,13 +536,13 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters): bundle_query = bundle_query.select(batch_table[field]) if txt: - txt_condition = batch_table.name.like(txt) + txt_condition = batch_table.name.like("%{0}%".format(txt)) for field in searchfields + ["name"]: - txt_condition |= batch_table[field].like(txt) + txt_condition |= batch_table[field].like("%{0}%".format(txt)) bundle_query = bundle_query.where(txt_condition) - return bundle_query + return bundle_query.run(as_list=1) @frappe.whitelist() diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 598167b3370..de46271e474 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -21,7 +21,7 @@ $.extend(erpnext, { }, toggle_naming_series: function() { - if(cur_frm.fields_dict.naming_series) { + if(cur_frm && cur_frm.fields_dict.naming_series) { cur_frm.toggle_display("naming_series", cur_frm.doc.__islocal?true:false); } }, diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.js b/erpnext/selling/page/sales_funnel/sales_funnel.js index e3d0a55c3a0..a0a1222eb3f 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.js +++ b/erpnext/selling/page/sales_funnel/sales_funnel.js @@ -126,9 +126,9 @@ erpnext.SalesFunnel = class SalesFunnel { if (me.options.chart == 'sales_funnel'){ me.render_funnel(); } else if (me.options.chart == 'opp_by_lead_source'){ - me.render_chart("Sales Opportunities by Source"); + me.render_chart(__("Sales Opportunities by Source")); } else if (me.options.chart == 'sales_pipeline'){ - me.render_chart("Sales Pipeline by Stage"); + me.render_chart(__("Sales Pipeline by Stage")); } } diff --git a/erpnext/setup/doctype/vehicle/vehicle.json b/erpnext/setup/doctype/vehicle/vehicle.json index b19d45924fb..bf1a8c184d3 100644 --- a/erpnext/setup/doctype/vehicle/vehicle.json +++ b/erpnext/setup/doctype/vehicle/vehicle.json @@ -57,7 +57,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Make", + "label": "Manufacturer", "length": 0, "no_copy": 0, "permlevel": 0, diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js index 61927f51a82..c175a4ad648 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -1,7 +1,7 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, - title: 'Warehouse Capacity Summary', + title: __('Warehouse Capacity Summary'), single_column: true }); page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'refresh'); diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html index 1183ad4496e..1883004de95 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html @@ -1,19 +1,19 @@