diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index ace34e052cd..f91d29a6b68 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -122,13 +122,10 @@ frappe.ui.form.on('Payment Entry', { frm.set_query('payment_term', 'references', function(frm, cdt, cdn) { const child = locals[cdt][cdn]; if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) { - let payment_term_list = frappe.get_list('Payment Schedule', {'parent': child.reference_name}); - - payment_term_list = payment_term_list.map(pt => pt.payment_term); - return { + query: "erpnext.controllers.queries.get_payment_terms_for_references", filters: { - 'name': ['in', payment_term_list] + 'reference': child.reference_name } } } diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 957403bf895..83736bd68e9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -164,6 +164,20 @@ class PaymentEntry(AccountsController): if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount): frappe.throw(fail_message.format(d.idx)) + def term_based_allocation_enabled_for_reference( + self, reference_doctype: str, reference_name: str + ) -> bool: + if ( + reference_doctype + and reference_doctype in ["Sales Invoice", "Sales Order", "Purchase Order", "Purchase Invoice"] + and reference_name + ): + if template := frappe.db.get_value(reference_doctype, reference_name, "payment_terms_template"): + return frappe.db.get_value( + "Payment Terms Template", template, "allocate_payment_based_on_payment_terms" + ) + return False + def validate_allocated_amount_with_latest_data(self): latest_references = get_outstanding_reference_documents( { @@ -184,10 +198,23 @@ class PaymentEntry(AccountsController): d = frappe._dict(d) latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d - for d in self.get("references"): - latest = (latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()).get( - d.payment_term - ) + for idx, d in enumerate(self.get("references"), start=1): + latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict() + + # If term based allocation is enabled, throw + if ( + d.payment_term is None or d.payment_term == "" + ) and self.term_based_allocation_enabled_for_reference( + d.reference_doctype, d.reference_name + ): + frappe.throw( + _( + "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" + ).format(frappe.bold(d.reference_name), frappe.bold(idx)) + ) + + # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key + latest = latest.get(d.payment_term) or latest.get(None) # The reference has already been fully paid if not latest: @@ -1510,6 +1537,9 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company): "invoice_amount": flt(d.invoice_amount), "outstanding_amount": flt(d.outstanding_amount), "payment_term_outstanding": payment_term_outstanding, + "allocated_amount": payment_term_outstanding + if payment_term_outstanding + else d.outstanding_amount, "payment_amount": payment_term.payment_amount, "payment_term": payment_term.payment_term, } diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index ae2625b6539..785b8a180b1 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1061,6 +1061,101 @@ class TestPaymentEntry(FrappeTestCase): } self.assertDictEqual(ref_details, expected_response) + @change_settings( + "Accounts Settings", + { + "unlink_payment_on_cancellation_of_invoice": 1, + "delete_linked_ledger_entries": 1, + "allow_multi_currency_invoices_against_single_party_account": 1, + }, + ) + def test_overallocation_validation_on_payment_terms(self): + """ + Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown. + + """ + customer = create_customer() + create_payment_terms_template() + + # Validate allocation on base/company currency + si1 = create_sales_invoice(do_not_save=1, qty=1, rate=200) + si1.payment_terms_template = "Test Receivable Template" + si1.save().submit() + + si1.reload() + pe = get_payment_entry(si1.doctype, si1.name).save() + # Allocated amount should be according to the payment schedule + for idx, schedule in enumerate(si1.payment_schedule): + with self.subTest(idx=idx): + self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount)) + pe.save() + + # Overallocation validation should trigger + pe.paid_amount = 400 + pe.references[0].allocated_amount = 200 + pe.references[1].allocated_amount = 200 + self.assertRaises(frappe.ValidationError, pe.save) + pe.delete() + si1.cancel() + si1.delete() + + # Validate allocation on foreign currency + si2 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=80, + do_not_save=1, + ) + si2.payment_terms_template = "Test Receivable Template" + si2.save().submit() + + si2.reload() + pe = get_payment_entry(si2.doctype, si2.name).save() + # Allocated amount should be according to the payment schedule + for idx, schedule in enumerate(si2.payment_schedule): + with self.subTest(idx=idx): + self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount)) + pe.save() + + # Overallocation validation should trigger + pe.paid_amount = 200 + pe.references[0].allocated_amount = 100 + pe.references[1].allocated_amount = 100 + self.assertRaises(frappe.ValidationError, pe.save) + pe.delete() + si2.cancel() + si2.delete() + + # Validate allocation in base/company currency on a foreign currency document + # when invoice is made is foreign currency, but posted to base/company currency debtors account + si3 = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=80, + do_not_save=1, + ) + si3.payment_terms_template = "Test Receivable Template" + si3.save().submit() + + si3.reload() + pe = get_payment_entry(si3.doctype, si3.name).save() + # Allocated amount should be equal to payment term outstanding + self.assertEqual(len(pe.references), 2) + for idx, ref in enumerate(pe.references): + with self.subTest(idx=idx): + self.assertEqual(ref.payment_term_outstanding, ref.allocated_amount) + pe.save() + + # Overallocation validation should trigger + pe.paid_amount = 16000 + pe.references[0].allocated_amount = 8000 + pe.references[1].allocated_amount = 8000 + self.assertRaises(frappe.ValidationError, pe.save) + pe.delete() + si3.cancel() + si3.delete() + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") @@ -1150,3 +1245,17 @@ def create_payment_terms_template_with_discount( def create_payment_term(name): if not frappe.db.exists("Payment Term", name): frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert() + + +def create_customer(name="_Test Customer 2 USD", currency="USD"): + customer = None + if frappe.db.exists("Customer", name): + customer = name + else: + customer = frappe.new_doc("Customer") + customer.customer_name = name + customer.default_currency = currency + customer.type = "Individual" + customer.save() + customer = customer.name + return customer diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index ab0baf6273b..6cd601f663d 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -138,7 +138,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency): def get_ar_filters(doc, entry): return { "report_date": doc.posting_date if doc.posting_date else None, - "customer_name": entry.customer, + "customer": entry.customer, "payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None, "sales_partner": doc.sales_partner if doc.sales_partner else None, "sales_person": doc.sales_person if doc.sales_person else None, diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html index 07e1896292d..259526f8c43 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html @@ -10,16 +10,12 @@

{{ _(report.report_name) }}

- {% if (filters.customer_name) %} - {{ filters.customer_name }} - {% else %} - {{ filters.customer ~ filters.supplier }} - {% endif %} + {{ filters.customer }}

- {% if (filters.tax_id) %} - {{ _("Tax Id: ") }}{{ filters.tax_id }} - {% endif %} + {% if (filters.tax_id) %} + {{ _("Tax Id: ") }}{{ filters.tax_id }} + {% endif %}
{{ _(filters.ageing_based_on) }} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 66cf87761d9..df3db37bc65 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -887,6 +887,8 @@ frappe.ui.form.on('Sales Invoice', { frm.events.append_time_log(frm, timesheet, 1.0); } }); + frm.refresh_field("timesheets"); + frm.trigger("calculate_timesheet_totals"); }, async get_exchange_rate(frm, from_currency, to_currency) { @@ -926,9 +928,6 @@ frappe.ui.form.on('Sales Invoice', { row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate); row.timesheet_detail = time_log.name; row.project_name = time_log.project_name; - - frm.refresh_field("timesheets"); - frm.trigger("calculate_timesheet_totals"); }, calculate_timesheet_totals: function(frm) { diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f9fb6232ac9..856631ee657 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1900,16 +1900,22 @@ class TestSalesInvoice(unittest.TestCase): si = self.create_si_to_test_tax_breakup() - itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si) + itemised_tax_data = get_itemised_tax_breakup_data(si) - expected_itemised_tax = { - "_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}}, - "_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}}, - } - expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0} + expected_itemised_tax = [ + { + "item": "_Test Item", + "taxable_amount": 10000.0, + "Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}, + }, + { + "item": "_Test Item 2", + "taxable_amount": 5000.0, + "Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}, + }, + ] - self.assertEqual(itemised_tax, expected_itemised_tax) - self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount) + self.assertEqual(itemised_tax_data, expected_itemised_tax) frappe.flags.country = None diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index cfda48e5c75..f25280f007c 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -603,7 +603,8 @@ "in_list_view": 1, "label": "Batch No", "options": "Batch", - "print_hide": 1 + "print_hide": 1, + "search_index": 1 }, { "fieldname": "col_break5", @@ -890,7 +891,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-10-17 12:51:44.825398", + "modified": "2023-07-25 11:58:10.723833", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 7583a60d63e..56d33b758b7 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -33,6 +33,7 @@ import erpnext from erpnext import get_company_currency from erpnext.accounts.utils import get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen +from erpnext.utilities.regional import temporary_flag PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"} SALES_TRANSACTION_TYPES = { @@ -261,9 +262,8 @@ def set_address_details( ) if doctype in TRANSACTION_TYPES: - # required to set correct region - frappe.flags.company = company - get_regional_address_details(party_details, doctype, company) + with temporary_flag("company", company): + get_regional_address_details(party_details, doctype, company) return party_address, shipping_address diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js index 6caebd34a2f..27b29baa40a 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -16,9 +16,30 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "fieldname": "based_on", "label": __("Based On"), "fieldtype": "Select", - "options": ["Cost Center", "Project"], + "options": ["Cost Center", "Project", "Accounting Dimension"], "default": "Cost Center", - "reqd": 1 + "reqd": 1, + "on_change": function(query_report){ + let based_on = query_report.get_values().based_on; + if(based_on!='Accounting Dimension'){ + frappe.query_report.set_filter_value({ + accounting_dimension: '' + }); + } + } + }, + { + "fieldname": "accounting_dimension", + "label": __("Accounting Dimension"), + "fieldtype": "Link", + "options": "Accounting Dimension", + "get_query": () =>{ + return { + filters: { + "disabled": 0 + } + } + } }, { "fieldname": "fiscal_year", diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index 183e279fe5d..3d6e9b5428c 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.utils import cstr, flt +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.report.financial_statements import ( filter_accounts, filter_out_zero_value_rows, @@ -16,10 +17,12 @@ value_fields = ("income", "expense", "gross_profit_loss") def execute(filters=None): - if not filters.get("based_on"): - filters["based_on"] = "Cost Center" + if filters.get("based_on") == "Accounting Dimension" and not filters.get("accounting_dimension"): + frappe.throw(_("Select Accounting Dimension.")) - based_on = filters.based_on.replace(" ", "_").lower() + based_on = ( + filters.based_on if filters.based_on != "Accounting Dimension" else filters.accounting_dimension + ) validate_filters(filters) accounts = get_accounts_data(based_on, filters.get("company")) data = get_data(accounts, filters, based_on) @@ -28,14 +31,14 @@ def execute(filters=None): def get_accounts_data(based_on, company): - if based_on == "cost_center": + if based_on == "Cost Center": return frappe.db.sql( """select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt from `tabCost Center` where company=%s order by name""", company, as_dict=True, ) - elif based_on == "project": + elif based_on == "Project": return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name") else: filters = {} @@ -56,11 +59,17 @@ def get_data(accounts, filters, based_on): gl_entries_by_account = {} + accounting_dimensions = get_dimensions(with_cost_center_and_project=True)[0] + fieldname = "" + for dimension in accounting_dimensions: + if dimension["document_type"] == based_on: + fieldname = dimension["fieldname"] + set_gl_entries_by_account( filters.get("company"), filters.get("from_date"), filters.get("to_date"), - based_on, + fieldname, gl_entries_by_account, ignore_closing_entries=not flt(filters.get("with_period_closing_entry")), ) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 02260c6da3a..c6247ea0da3 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1413,6 +1413,8 @@ def create_new_asset_after_split(asset, split_qty): ) new_asset.gross_purchase_amount = new_gross_purchase_amount + if asset.purchase_receipt_amount: + new_asset.purchase_receipt_amount = new_gross_purchase_amount new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation new_asset.asset_quantity = split_qty new_asset.split_from = asset.name diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 22055dcb736..b85f7194f98 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -62,20 +62,21 @@ class AssetMovement(Document): frappe.throw(_("Source and Target Location cannot be same")) if self.purpose == "Receipt": - if not (d.source_location or d.from_employee) and not (d.target_location or d.to_employee): + if not (d.source_location) and not (d.target_location or d.to_employee): frappe.throw( _("Target Location or To Employee is required while receiving Asset {0}").format(d.asset) ) - elif d.from_employee and not d.target_location: - frappe.throw( - _("Target Location is required while receiving Asset {0} from an employee").format(d.asset) - ) - elif d.to_employee and d.target_location: - frappe.throw( - _( - "Asset {0} cannot be received at a location and given to an employee in a single movement" - ).format(d.asset) - ) + elif d.source_location: + if d.from_employee and not d.target_location: + frappe.throw( + _("Target Location is required while receiving Asset {0} from an employee").format(d.asset) + ) + elif d.to_employee and d.target_location: + frappe.throw( + _( + "Asset {0} cannot be received at a location and given to an employee in a single movement" + ).format(d.asset) + ) def validate_employee(self): for d in self.assets: diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index 6911f94bbbb..94c77ea517c 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -54,12 +54,12 @@ def get_conditions(filters): conditions["cost_center"] = filters.get("cost_center") if status: - # In Store assets are those that are not sold or scrapped + # In Store assets are those that are not sold or scrapped or capitalized or decapitalized operand = "not in" if status not in "In Location": operand = "in" - conditions["status"] = (operand, ["Sold", "Scrapped"]) + conditions["status"] = (operand, ["Sold", "Scrapped", "Capitalized", "Decapitalized"]) return conditions @@ -71,36 +71,6 @@ def get_data(filters): pr_supplier_map = get_purchase_receipt_supplier_map() pi_supplier_map = get_purchase_invoice_supplier_map() - group_by = frappe.scrub(filters.get("group_by")) - - if group_by == "asset_category": - fields = ["asset_category", "gross_purchase_amount", "opening_accumulated_depreciation"] - assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields, group_by=group_by) - - elif group_by == "location": - fields = ["location", "gross_purchase_amount", "opening_accumulated_depreciation"] - assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields, group_by=group_by) - - else: - fields = [ - "name as asset_id", - "asset_name", - "status", - "department", - "company", - "cost_center", - "calculate_depreciation", - "purchase_receipt", - "asset_category", - "purchase_date", - "gross_purchase_amount", - "location", - "available_for_use_date", - "purchase_invoice", - "opening_accumulated_depreciation", - ] - assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) - assets_linked_to_fb = get_assets_linked_to_fb(filters) company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") @@ -114,6 +84,31 @@ def get_data(filters): depreciation_amount_map = get_asset_depreciation_amount_map(filters, finance_book) + group_by = frappe.scrub(filters.get("group_by")) + + if group_by in ("asset_category", "location"): + data = get_group_by_data(group_by, conditions, assets_linked_to_fb, depreciation_amount_map) + return data + + fields = [ + "name as asset_id", + "asset_name", + "status", + "department", + "company", + "cost_center", + "calculate_depreciation", + "purchase_receipt", + "asset_category", + "purchase_date", + "gross_purchase_amount", + "location", + "available_for_use_date", + "purchase_invoice", + "opening_accumulated_depreciation", + ] + assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) + for asset in assets_record: if ( assets_linked_to_fb @@ -136,7 +131,7 @@ def get_data(filters): or pi_supplier_map.get(asset.purchase_invoice), "gross_purchase_amount": asset.gross_purchase_amount, "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, - "depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map), + "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, "available_for_use_date": asset.available_for_use_date, "location": asset.location, "asset_category": asset.asset_category, @@ -230,12 +225,11 @@ def get_assets_linked_to_fb(filters): return assets_linked_to_fb -def get_depreciation_amount_of_asset(asset, depreciation_amount_map): - return depreciation_amount_map.get(asset.asset_id) or 0.0 - - def get_asset_depreciation_amount_map(filters, finance_book): - date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date + start_date = ( + filters.from_date if filters.filter_based_on == "Date Range" else filters.year_start_date + ) + end_date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date asset = frappe.qb.DocType("Asset") gle = frappe.qb.DocType("GL Entry") @@ -256,25 +250,77 @@ def get_asset_depreciation_amount_map(filters, finance_book): ) .where(gle.debit != 0) .where(gle.is_cancelled == 0) + .where(company.name == filters.company) .where(asset.docstatus == 1) - .groupby(asset.name) ) + if filters.only_existing_assets: + query = query.where(asset.is_existing_asset == 1) + if filters.asset_category: + query = query.where(asset.asset_category == filters.asset_category) + if filters.cost_center: + query = query.where(asset.cost_center == filters.cost_center) + if filters.status: + if filters.status == "In Location": + query = query.where(asset.status.notin(["Sold", "Scrapped", "Capitalized", "Decapitalized"])) + else: + query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized", "Decapitalized"])) if finance_book: query = query.where( (gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull()) ) else: query = query.where((gle.finance_book.isin([""])) | (gle.finance_book.isnull())) - if filters.filter_based_on in ("Date Range", "Fiscal Year"): - query = query.where(gle.posting_date <= date) + query = query.where(gle.posting_date >= start_date) + query = query.where(gle.posting_date <= end_date) + + query = query.groupby(asset.name) asset_depr_amount_map = query.run() return dict(asset_depr_amount_map) +def get_group_by_data(group_by, conditions, assets_linked_to_fb, depreciation_amount_map): + fields = [ + group_by, + "name", + "gross_purchase_amount", + "opening_accumulated_depreciation", + "calculate_depreciation", + ] + assets = frappe.db.get_all("Asset", filters=conditions, fields=fields) + + data = [] + + for a in assets: + if assets_linked_to_fb and a.calculate_depreciation and a.name not in assets_linked_to_fb: + continue + + a["depreciated_amount"] = depreciation_amount_map.get(a["name"], 0.0) + a["asset_value"] = ( + a["gross_purchase_amount"] - a["opening_accumulated_depreciation"] - a["depreciated_amount"] + ) + + del a["name"] + del a["calculate_depreciation"] + + idx = ([i for i, d in enumerate(data) if a[group_by] == d[group_by]] or [None])[0] + if idx is None: + data.append(a) + else: + for field in ( + "gross_purchase_amount", + "opening_accumulated_depreciation", + "depreciated_amount", + "asset_value", + ): + data[idx][field] = data[idx][field] + a[field] + + return data + + def get_purchase_receipt_supplier_map(): return frappe._dict( frappe.db.sql( @@ -313,35 +359,35 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": frappe.scrub(filters.get("group_by")), "options": filters.get("group_by"), - "width": 120, + "width": 216, }, { "label": _("Gross Purchase Amount"), "fieldname": "gross_purchase_amount", "fieldtype": "Currency", "options": "company:currency", - "width": 100, + "width": 250, }, { "label": _("Opening Accumulated Depreciation"), "fieldname": "opening_accumulated_depreciation", "fieldtype": "Currency", "options": "company:currency", - "width": 90, + "width": 250, }, { "label": _("Depreciated Amount"), "fieldname": "depreciated_amount", "fieldtype": "Currency", "options": "company:currency", - "width": 100, + "width": 250, }, { "label": _("Asset Value"), "fieldname": "asset_value", "fieldtype": "Currency", "options": "company:currency", - "width": 100, + "width": 250, }, ] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a1eba4ae0c3..b70ea36dcde 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -55,6 +55,7 @@ from erpnext.stock.get_item_details import ( get_item_tax_map, get_item_warehouse, ) +from erpnext.utilities.regional import temporary_flag from erpnext.utilities.transaction_base import TransactionBase @@ -758,7 +759,9 @@ class AccountsController(TransactionBase): } ) - update_gl_dict_with_regional_fields(self, gl_dict) + with temporary_flag("company", self.company): + update_gl_dict_with_regional_fields(self, gl_dict) + accounting_dimensions = get_accounting_dimensions() dimension_dict = frappe._dict() diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 80bc3eef745..36225e3dd5e 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -823,3 +823,18 @@ def get_fields(doctype, fields=None): fields.insert(1, meta.title_field.strip()) return unique(fields) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len, filters) -> list: + terms = [] + if filters: + terms = frappe.db.get_all( + "Payment Schedule", + filters={"parent": filters.get("reference")}, + fields=["payment_term"], + limit=page_len, + as_list=1, + ) + return terms diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 4661c5ca7e8..62d4c538682 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -18,6 +18,7 @@ from erpnext.controllers.accounts_controller import ( validate_taxes_and_charges, ) from erpnext.stock.get_item_details import _get_item_tax_template +from erpnext.utilities.regional import temporary_flag class calculate_taxes_and_totals(object): @@ -942,7 +943,6 @@ class calculate_taxes_and_totals(object): def get_itemised_tax_breakup_html(doc): if not doc.taxes: return - frappe.flags.company = doc.company # get headers tax_accounts = [] @@ -952,22 +952,17 @@ def get_itemised_tax_breakup_html(doc): if tax.description not in tax_accounts: tax_accounts.append(tax.description) - headers = get_itemised_tax_breakup_header(doc.doctype + " Item", tax_accounts) - - # get tax breakup data - itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(doc) - - get_rounded_tax_amount(itemised_tax, doc.precision("tax_amount", "taxes")) - - update_itemised_tax_data(doc) - frappe.flags.company = None + with temporary_flag("company", doc.company): + headers = get_itemised_tax_breakup_header(doc.doctype + " Item", tax_accounts) + itemised_tax_data = get_itemised_tax_breakup_data(doc) + get_rounded_tax_amount(itemised_tax_data, doc.precision("tax_amount", "taxes")) + update_itemised_tax_data(doc) return frappe.render_template( "templates/includes/itemised_tax_breakup.html", dict( headers=headers, - itemised_tax=itemised_tax, - itemised_taxable_amount=itemised_taxable_amount, + itemised_tax_data=itemised_tax_data, tax_accounts=tax_accounts, doc=doc, ), @@ -977,10 +972,8 @@ def get_itemised_tax_breakup_html(doc): @frappe.whitelist() def get_round_off_applicable_accounts(company, account_list): # required to set correct region - frappe.flags.company = company - account_list = get_regional_round_off_accounts(company, account_list) - - return account_list + with temporary_flag("company", company): + return get_regional_round_off_accounts(company, account_list) @erpnext.allow_regional @@ -1005,7 +998,15 @@ def get_itemised_tax_breakup_data(doc): itemised_taxable_amount = get_itemised_taxable_amount(doc.items) - return itemised_tax, itemised_taxable_amount + itemised_tax_data = [] + for item_code, taxes in itemised_tax.items(): + itemised_tax_data.append( + frappe._dict( + {"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code), **taxes} + ) + ) + + return itemised_tax_data def get_itemised_tax(taxes, with_tax_account=False): @@ -1050,9 +1051,10 @@ def get_itemised_taxable_amount(items): def get_rounded_tax_amount(itemised_tax, precision): # Rounding based on tax_amount precision - for taxes in itemised_tax.values(): - for tax_account in taxes: - taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) + for taxes in itemised_tax: + for row in taxes.values(): + if isinstance(row, dict) and isinstance(row["tax_amount"], float): + row["tax_amount"] = flt(row["tax_amount"], precision) class init_landed_taxes_and_totals(object): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index cfbb54ab2d0..b6b6e2e5ad6 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -969,6 +969,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1); } + apply_discount_on_item(doc, cdt, cdn, field) { + var item = frappe.get_doc(cdt, cdn); + if(!item.price_list_rate) { + item[field] = 0.0; + } else { + this.price_list_rate(doc, cdt, cdn); + } + this.set_gross_profit(item); + } + shipping_rule() { var me = this; if(this.frm.doc.shipping_rule) { @@ -1639,6 +1649,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe () => { if(args.items.length) { me._set_values_for_item_list(r.message.children); + $.each(r.message.children || [], function(i, d) { + me.apply_discount_on_item(d, d.doctype, d.name, 'discount_percentage'); + }); } }, () => { me.in_apply_price_list = false; } diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index e3de49c57d8..80a6b7712fc 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -145,16 +145,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran this.apply_discount_on_item(doc, cdt, cdn, 'discount_amount'); } - apply_discount_on_item(doc, cdt, cdn, field) { - var item = frappe.get_doc(cdt, cdn); - if(!item.price_list_rate) { - item[field] = 0.0; - } else { - this.price_list_rate(doc, cdt, cdn); - } - this.set_gross_profit(item); - } - commission_rate() { this.calculate_commission(); } diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index b97e42c2468..edfb269da9a 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -515,7 +515,8 @@ "oldfieldname": "batch_no", "oldfieldtype": "Link", "options": "Batch", - "print_hide": 1 + "print_hide": 1, + "search_index": 1 }, { "allow_on_submit": 1, @@ -867,7 +868,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-01 21:05:14.175640", + "modified": "2023-07-25 11:58:28.101919", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", @@ -877,4 +878,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index ae39470d1dc..ffec57ca1df 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -181,7 +181,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nSubmitted\nStopped\nCancelled\nPending\nPartially Ordered\nOrdered\nIssued\nTransferred\nReceived", + "options": "\nDraft\nSubmitted\nStopped\nCancelled\nPending\nPartially Ordered\nPartially Received\nOrdered\nIssued\nTransferred\nReceived", "print_hide": 1, "print_width": "100px", "read_only": 1, @@ -356,7 +356,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2023-05-07 20:17:29.108095", + "modified": "2023-07-25 17:19:31.662662", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index b887f795640..883cdd19e7c 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -79,7 +79,8 @@ "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", - "options": "Batch" + "options": "Batch", + "search_index": 1 }, { "fieldname": "column_break_2", @@ -192,7 +193,7 @@ ], "istable": 1, "links": [], - "modified": "2023-06-16 14:05:51.719959", + "modified": "2023-07-25 11:56:23.361867", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", @@ -203,4 +204,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 5e61f0f196a..9f3074a24ca 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -256,7 +256,11 @@ def repost(doc): message += "
" + "Traceback:
" + traceback frappe.db.set_value(doc.doctype, doc.name, "error_log", message) - if not isinstance(e, RecoverableErrors): + outgoing_email_account = frappe.get_cached_value( + "Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name" + ) + + if outgoing_email_account and not isinstance(e, RecoverableErrors): notify_error_to_stock_managers(doc, message) doc.set_status("Failed") finally: diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index 2f65eaa358d..a04309ad48e 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -168,7 +168,8 @@ "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", - "options": "Batch" + "options": "Batch", + "search_index": 1 }, { "default": "0", @@ -189,7 +190,7 @@ ], "istable": 1, "links": [], - "modified": "2023-05-09 18:42:19.224916", + "modified": "2023-07-25 11:58:44.992419", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", diff --git a/erpnext/templates/includes/itemised_tax_breakup.html b/erpnext/templates/includes/itemised_tax_breakup.html index fbc80de7d0d..7b507bd1fdb 100644 --- a/erpnext/templates/includes/itemised_tax_breakup.html +++ b/erpnext/templates/includes/itemised_tax_breakup.html @@ -12,14 +12,14 @@ - {% for item, taxes in itemised_tax.items() %} + {% for taxes in itemised_tax_data %} - {{ item }} + {{ taxes.item }} {% if doc.get('is_return') %} - {{ frappe.utils.fmt_money((itemised_taxable_amount.get(item, 0))|abs, None, doc.currency) }} + {{ frappe.utils.fmt_money(taxes.taxable_amount|abs, None, doc.currency) }} {% else %} - {{ frappe.utils.fmt_money(itemised_taxable_amount.get(item, 0), None, doc.currency) }} + {{ frappe.utils.fmt_money(taxes.taxable_amount, None, doc.currency) }} {% endif %} {% for tax_account in tax_accounts %} diff --git a/erpnext/utilities/regional.py b/erpnext/utilities/regional.py new file mode 100644 index 00000000000..858976f8557 --- /dev/null +++ b/erpnext/utilities/regional.py @@ -0,0 +1,13 @@ +from contextlib import contextmanager + +import frappe + + +@contextmanager +def temporary_flag(flag_name, value): + flags = frappe.local.flags + flags[flag_name] = value + try: + yield + finally: + flags.pop(flag_name, None)