diff --git a/.github/workflows/release_notes.yml b/.github/workflows/release_notes.yml new file mode 100644 index 000000000000..e765a66f691f --- /dev/null +++ b/.github/workflows/release_notes.yml @@ -0,0 +1,38 @@ +# This action: +# +# 1. Generates release notes using github API. +# 2. Strips unnecessary info like chore/style etc from notes. +# 3. Updates release info. + +# This action needs to be maintained on all branches that do releases. + +name: 'Release Notes' + +on: + workflow_dispatch: + inputs: + tag_name: + description: 'Tag of release like v13.0.0' + required: true + type: string + release: + types: [released] + +permissions: + contents: read + +jobs: + regen-notes: + name: 'Regenerate release notes' + runs-on: ubuntu-latest + + steps: + - name: Update notes + run: | + NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' ) + RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id') + gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES" + + env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }} diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 9b4db49d084e..2ce1125456ec 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -7,11 +7,9 @@ on: - '**.css' - '**.md' - '**.html' - push: - branches: [ develop ] - paths-ignore: - - '**.js' - - '**.md' + schedule: + # Run everday at midnight UTC / 5:30 IST + - cron: "0 0 * * *" workflow_dispatch: inputs: user: diff --git a/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json b/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json index 8631d3dc2a34..4883106227b6 100644 --- a/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json +++ b/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json @@ -4,18 +4,19 @@ "creation": "2020-07-17 11:25:34.593061", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}", "idx": 0, "is_public": 1, "is_standard": 1, - "modified": "2020-07-22 12:24:49.144210", + "modified": "2023-07-19 13:13:13.307073", "modified_by": "Administrator", "module": "Accounts", "name": "Budget Variance", "number_of_groups": 0, "owner": "Administrator", "report_name": "Budget Variance Report", + "roles": [], "timeseries": 0, "type": "Bar", "use_report_chart": 1, diff --git a/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json b/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json index 3fa995bbe158..25caa44769b9 100644 --- a/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json +++ b/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json @@ -4,18 +4,19 @@ "creation": "2020-07-17 11:25:34.448572", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}", "idx": 0, "is_public": 1, "is_standard": 1, - "modified": "2020-07-22 12:33:48.888943", + "modified": "2023-07-19 13:08:56.470390", "modified_by": "Administrator", "module": "Accounts", "name": "Profit and Loss", "number_of_groups": 0, "owner": "Administrator", "report_name": "Profit and Loss Statement", + "roles": [], "timeseries": 0, "type": "Bar", "use_report_chart": 1, diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py index 9540084e09f3..e75af7047f19 100644 --- a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py +++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py @@ -14,10 +14,8 @@ class AccountClosingBalance(Document): pass -def make_closing_entries(closing_entries, voucher_name): +def make_closing_entries(closing_entries, voucher_name, company, closing_date): accounting_dimensions = get_accounting_dimensions() - company = closing_entries[0].get("company") - closing_date = closing_entries[0].get("closing_date") previous_closing_entries = get_previous_closing_entries( company, closing_date, accounting_dimensions diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 81ff6a52db10..15c84d462f17 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -271,6 +271,12 @@ def get_dimensions(with_cost_center_and_project=False): as_dict=1, ) + if isinstance(with_cost_center_and_project, str): + if with_cost_center_and_project.lower().strip() == "true": + with_cost_center_and_project = True + else: + with_cost_center_and_project = False + if with_cost_center_and_project: dimension_filters.extend( [ diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.js b/erpnext/accounts/doctype/accounting_period/accounting_period.js index e3d805a16815..f17b6f9c6954 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.js +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.js @@ -20,5 +20,11 @@ frappe.ui.form.on('Accounting Period', { } }); } + + frm.set_query("document_type", "closed_documents", () => { + return { + query: "erpnext.controllers.queries.get_doctypes_for_closing", + } + }); } }); diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py index 80c9715e8e1b..d5f37a68067f 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py @@ -11,6 +11,10 @@ class OverlapError(frappe.ValidationError): pass +class ClosedAccountingPeriod(frappe.ValidationError): + pass + + class AccountingPeriod(Document): def validate(self): self.validate_overlap() @@ -65,3 +69,42 @@ def bootstrap_doctypes_for_closing(self): "closed_documents", {"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed}, ) + + +def validate_accounting_period_on_doc_save(doc, method=None): + if doc.doctype == "Bank Clearance": + return + elif doc.doctype == "Asset": + if doc.is_existing_asset: + return + else: + date = doc.available_for_use_date + elif doc.doctype == "Asset Repair": + date = doc.completion_date + else: + date = doc.posting_date + + ap = frappe.qb.DocType("Accounting Period") + cd = frappe.qb.DocType("Closed Document") + + accounting_period = ( + frappe.qb.from_(ap) + .from_(cd) + .select(ap.name) + .where( + (ap.name == cd.parent) + & (ap.company == doc.company) + & (cd.closed == 1) + & (cd.document_type == doc.doctype) + & (date >= ap.start_date) + & (date <= ap.end_date) + ) + ).run(as_dict=1) + + if accounting_period: + frappe.throw( + _("You cannot create a {0} within the closed Accounting Period {1}").format( + doc.doctype, frappe.bold(accounting_period[0]["name"]) + ), + ClosedAccountingPeriod, + ) diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py index 85025d190f5c..41d94797ad62 100644 --- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py @@ -6,9 +6,11 @@ import frappe from frappe.utils import add_months, nowdate -from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError +from erpnext.accounts.doctype.accounting_period.accounting_period import ( + ClosedAccountingPeriod, + OverlapError, +) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.accounts.general_ledger import ClosedAccountingPeriod test_dependencies = ["Item"] @@ -33,9 +35,9 @@ def test_accounting_period(self): ap1.save() doc = create_sales_invoice( - do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC" + do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC" ) - self.assertRaises(ClosedAccountingPeriod, doc.submit) + self.assertRaises(ClosedAccountingPeriod, doc.save) def tearDown(self): for d in frappe.get_all("Accounting Period"): diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 35d606ba3ae3..6667193a54c3 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -8,9 +8,6 @@ frappe.ui.form.on('Bank', { }, refresh: function(frm) { add_fields_to_mapping_table(frm); - - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Bank' }; - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); if (frm.doc.__islocal) { diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 9909c6c2ab0f..1ac909e74510 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -1,13 +1,14 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.ui.form.on("Dunning", { setup: function (frm) { - frm.set_query("sales_invoice", () => { + frm.set_query("sales_invoice", "overdue_payments", () => { return { filters: { docstatus: 1, company: frm.doc.company, + customer: frm.doc.customer, outstanding_amount: [">", 0], status: "Overdue" }, @@ -22,14 +23,24 @@ frappe.ui.form.on("Dunning", { } }; }); + frm.set_query("cost_center", () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query("contact_person", erpnext.queries.contact_query); + frm.set_query("customer_address", erpnext.queries.address_query); + frm.set_query("company_address", erpnext.queries.company_address_query); + + // cannot add rows manually, only via button "Fetch Overdue Payments" + frm.set_df_property("overdue_payments", "cannot_add_rows", true); }, refresh: function (frm) { frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1); - frm.set_df_property( - "sales_invoice", - "read_only", - frm.doc.__islocal ? 0 : 1 - ); if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") { frm.add_custom_button(__("Resolve"), () => { frm.set_value("status", "Resolved"); @@ -40,42 +51,111 @@ frappe.ui.form.on("Dunning", { __("Payment"), function () { frm.events.make_payment_entry(frm); - },__("Create") + }, __("Create") ); frm.page.set_inner_btn_group_as_primary(__("Create")); } - if(frm.doc.docstatus > 0) { - frm.add_custom_button(__('Ledger'), function() { - frappe.route_options = { - "voucher_no": frm.doc.name, - "from_date": frm.doc.posting_date, - "to_date": frm.doc.posting_date, - "company": frm.doc.company, - "show_cancelled_entries": frm.doc.docstatus === 2 - }; - frappe.set_route("query-report", "General Ledger"); - }, __('View')); + if (frm.doc.docstatus === 0) { + frm.add_custom_button(__("Fetch Overdue Payments"), () => { + erpnext.utils.map_current_doc({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", + source_doctype: "Sales Invoice", + date_field: "due_date", + target: frm, + setters: { + customer: frm.doc.customer || undefined, + }, + get_query_filters: { + docstatus: 1, + status: "Overdue", + company: frm.doc.company + }, + allow_child_item_selection: true, + child_fieldname: "payment_schedule", + child_columns: ["due_date", "outstanding"], + }); + }); } + + frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' }; + + frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer)); }, - overdue_days: function (frm) { - frappe.db.get_value( - "Dunning Type", - { - start_day: ["<", frm.doc.overdue_days], - end_day: [">=", frm.doc.overdue_days], - }, - "dunning_type", - (r) => { - if (r) { - frm.set_value("dunning_type", r.dunning_type); - } else { - frm.set_value("dunning_type", ""); - frm.set_value("rate_of_interest", ""); - frm.set_value("dunning_fee", ""); + // When multiple companies are set up. in case company name is changed set default company address + company: function (frm) { + if (frm.doc.company) { + frappe.call({ + method: "erpnext.setup.doctype.company.company.get_default_company_address", + args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" }, + debounce: 2000, + callback: function (r) { + frm.set_value("company_address", r && r.message || ""); + } + }); + + if (frm.fields_dict.currency) { + const company_currency = erpnext.get_currency(frm.doc.company); + + if (!frm.doc.currency) { + frm.set_value("currency", company_currency); + } + + if (frm.doc.currency == company_currency) { + frm.set_value("conversion_rate", 1.0); } } - ); + + const company_doc = frappe.get_doc(":Company", frm.doc.company); + if (company_doc.default_letter_head) { + if (frm.fields_dict.letter_head) { + frm.set_value("letter_head", company_doc.default_letter_head); + } + } + } + }, + currency: function (frm) { + // this.set_dynamic_labels(); + const company_currency = erpnext.get_currency(frm.doc.company); + // Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc + if (frm.doc.currency && frm.doc.currency !== company_currency) { + frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + transaction_date: frm.doc.posting_date, + from_currency: frm.doc.currency, + to_currency: company_currency, + args: "for_selling" + }, + freeze: true, + freeze_message: __("Fetching exchange rates ..."), + callback: function(r) { + const exchange_rate = flt(r.message); + if (exchange_rate != frm.doc.conversion_rate) { + frm.set_value("conversion_rate", exchange_rate); + } + } + }); + } else { + frm.trigger("conversion_rate"); + } + }, + customer: (frm) => { + erpnext.utils.get_party_details(frm); + }, + conversion_rate: function (frm) { + if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) { + frm.set_value("conversion_rate", 1.0); + } + + // Make read only if Accounts Settings doesn't allow stale rates + frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1); + }, + customer_address: function (frm) { + erpnext.utils.get_address_display(frm, "customer_address"); + }, + company_address: function (frm) { + erpnext.utils.get_address_display(frm, "company_address"); }, dunning_type: function (frm) { frm.trigger("get_dunning_letter_text"); @@ -87,7 +167,7 @@ frappe.ui.form.on("Dunning", { if (frm.doc.dunning_type) { frappe.call({ method: - "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text", + "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text", args: { dunning_type: frm.doc.dunning_type, language: frm.doc.language, @@ -106,49 +186,62 @@ frappe.ui.form.on("Dunning", { }); } }, - due_date: function (frm) { - frm.trigger("calculate_overdue_days"); - }, posting_date: function (frm) { frm.trigger("calculate_overdue_days"); }, rate_of_interest: function (frm) { - frm.trigger("calculate_interest_and_amount"); - }, - outstanding_amount: function (frm) { - frm.trigger("calculate_interest_and_amount"); - }, - interest_amount: function (frm) { - frm.trigger("calculate_interest_and_amount"); + frm.trigger("calculate_interest"); }, dunning_fee: function (frm) { - frm.trigger("calculate_interest_and_amount"); + frm.trigger("calculate_totals"); }, - sales_invoice: function (frm) { - frm.trigger("calculate_overdue_days"); + overdue_payments_add: function (frm) { + frm.trigger("calculate_totals"); + }, + overdue_payments_remove: function (frm) { + frm.trigger("calculate_totals"); }, calculate_overdue_days: function (frm) { - if (frm.doc.posting_date && frm.doc.due_date) { - const overdue_days = moment(frm.doc.posting_date).diff( - frm.doc.due_date, - "days" - ); - frm.set_value("overdue_days", overdue_days); - } + frm.doc.overdue_payments.forEach((row) => { + if (frm.doc.posting_date && row.due_date) { + const overdue_days = moment(frm.doc.posting_date).diff( + row.due_date, + "days" + ); + frappe.model.set_value(row.doctype, row.name, "overdue_days", overdue_days); + } + }); }, - calculate_interest_and_amount: function (frm) { - const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100; - const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount')); - const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount')); - const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total')); - frm.set_value("interest_amount", interest_amount); - frm.set_value("dunning_amount", dunning_amount); - frm.set_value("grand_total", grand_total); + calculate_interest: function (frm) { + frm.doc.overdue_payments.forEach((row) => { + const interest_per_day = frm.doc.rate_of_interest / 100 / 365; + const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row)); + frappe.model.set_value(row.doctype, row.name, "interest", interest); + }); + }, + calculate_totals: function (frm) { + const total_interest = frm.doc.overdue_payments + .reduce((prev, cur) => prev + cur.interest, 0); + const total_outstanding = frm.doc.overdue_payments + .reduce((prev, cur) => prev + cur.outstanding, 0); + const dunning_amount = total_interest + frm.doc.dunning_fee; + const base_dunning_amount = dunning_amount * frm.doc.conversion_rate; + const grand_total = total_outstanding + dunning_amount; + + function setWithPrecison(field, value) { + frm.set_value(field, flt(value, precision(field))); + } + + setWithPrecison("total_outstanding", total_outstanding); + setWithPrecison("total_interest", total_interest); + setWithPrecison("dunning_amount", dunning_amount); + setWithPrecison("base_dunning_amount", base_dunning_amount); + setWithPrecison("grand_total", grand_total); }, make_payment_entry: function (frm) { return frappe.call({ method: - "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry", + "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry", args: { dt: frm.doc.doctype, dn: frm.doc.name, @@ -160,3 +253,9 @@ frappe.ui.form.on("Dunning", { }); }, }); + +frappe.ui.form.on("Overdue Payment", { + interest: function (frm) { + frm.trigger("calculate_totals"); + } +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 2a32b99f4283..b7e8aeaaafd6 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -2,49 +2,60 @@ "actions": [], "allow_events_in_timeline": 1, "autoname": "naming_series:", + "beta": 1, "creation": "2019-07-05 16:34:31.013238", "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "title", "naming_series", - "sales_invoice", "customer", "customer_name", - "outstanding_amount", - "currency", - "conversion_rate", "column_break_3", "company", "posting_date", "posting_time", - "due_date", - "overdue_days", + "status", + "section_break_9", + "currency", + "column_break_11", + "conversion_rate", "address_and_contact_section", + "customer_address", "address_display", + "contact_person", "contact_display", + "column_break_16", + "company_address", + "company_address_display", "contact_mobile", "contact_email", - "column_break_18", - "company_address_display", "section_break_6", "dunning_type", - "dunning_fee", "column_break_8", "rate_of_interest", - "interest_amount", "section_break_12", + "overdue_payments", + "section_break_28", + "total_interest", + "dunning_fee", + "column_break_17", "dunning_amount", + "base_dunning_amount", + "section_break_32", + "spacer", + "column_break_33", + "total_outstanding", "grand_total", - "income_account", - "column_break_17", - "status", - "printing_setting_section", + "printing_settings_section", "language", "body_text", "column_break_22", "letter_head", "closing_text", + "accounting_details_section", + "income_account", + "column_break_48", + "cost_center", "amended_from" ], "fields": [ @@ -60,32 +71,17 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", - "options": "DUNN-.MM.-.YY.-" - }, - { - "fieldname": "sales_invoice", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Sales Invoice", - "options": "Sales Invoice", - "reqd": 1 + "options": "DUNN-.MM.-.YY.-", + "print_hide": 1 }, { - "fetch_from": "sales_invoice.customer_name", + "fetch_from": "customer.customer_name", "fieldname": "customer_name", "fieldtype": "Data", "in_list_view": 1, "label": "Customer Name", "read_only": 1 }, - { - "fetch_from": "sales_invoice.outstanding_amount", - "fieldname": "outstanding_amount", - "fieldtype": "Currency", - "label": "Outstanding Amount", - "read_only": 1 - }, { "fieldname": "column_break_3", "fieldtype": "Column Break" @@ -94,13 +90,8 @@ "default": "Today", "fieldname": "posting_date", "fieldtype": "Date", - "label": "Date" - }, - { - "fieldname": "overdue_days", - "fieldtype": "Int", - "label": "Overdue Days", - "read_only": 1 + "label": "Date", + "reqd": 1 }, { "fieldname": "section_break_6", @@ -112,16 +103,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Dunning Type", - "options": "Dunning Type", - "reqd": 1 - }, - { - "default": "0", - "fieldname": "interest_amount", - "fieldtype": "Currency", - "label": "Interest Amount", - "precision": "2", - "read_only": 1 + "options": "Dunning Type" }, { "fieldname": "column_break_8", @@ -134,6 +116,7 @@ "fieldname": "dunning_fee", "fieldtype": "Currency", "label": "Dunning Fee", + "options": "currency", "precision": "2" }, { @@ -144,36 +127,24 @@ "fieldname": "column_break_17", "fieldtype": "Column Break" }, - { - "fieldname": "printing_setting_section", - "fieldtype": "Section Break", - "label": "Printing Setting" - }, { "fieldname": "language", "fieldtype": "Link", "label": "Print Language", - "options": "Language" + "options": "Language", + "print_hide": 1 }, { "fieldname": "letter_head", "fieldtype": "Link", "label": "Letter Head", - "options": "Letter Head" + "options": "Letter Head", + "print_hide": 1 }, { "fieldname": "column_break_22", "fieldtype": "Column Break" }, - { - "fetch_from": "sales_invoice.currency", - "fieldname": "currency", - "fieldtype": "Link", - "hidden": 1, - "label": "Currency", - "options": "Currency", - "read_only": 1 - }, { "fieldname": "amended_from", "fieldtype": "Link", @@ -183,14 +154,6 @@ "print_hide": 1, "read_only": 1 }, - { - "allow_on_submit": 1, - "default": "{customer_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title" - }, { "fieldname": "body_text", "fieldtype": "Text Editor", @@ -201,13 +164,6 @@ "fieldtype": "Text Editor", "label": "Closing Text" }, - { - "fetch_from": "sales_invoice.due_date", - "fieldname": "due_date", - "fieldtype": "Date", - "label": "Due Date", - "read_only": 1 - }, { "fieldname": "posting_time", "fieldtype": "Time", @@ -222,26 +178,24 @@ "label": "Rate of Interest (%) Yearly" }, { + "collapsible": 1, "fieldname": "address_and_contact_section", "fieldtype": "Section Break", "label": "Address and Contact" }, { - "fetch_from": "sales_invoice.address_display", "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", "read_only": 1 }, { - "fetch_from": "sales_invoice.contact_display", "fieldname": "contact_display", "fieldtype": "Small Text", "label": "Contact", "read_only": 1 }, { - "fetch_from": "sales_invoice.contact_mobile", "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", @@ -249,18 +203,12 @@ "read_only": 1 }, { - "fieldname": "column_break_18", - "fieldtype": "Column Break" - }, - { - "fetch_from": "sales_invoice.company_address_display", "fieldname": "company_address_display", "fieldtype": "Small Text", - "label": "Company Address", + "label": "Company Address Display", "read_only": 1 }, { - "fetch_from": "sales_invoice.contact_email", "fieldname": "contact_email", "fieldtype": "Data", "label": "Contact Email", @@ -268,18 +216,18 @@ "read_only": 1 }, { - "fetch_from": "sales_invoice.customer", "fieldname": "customer", "fieldtype": "Link", "label": "Customer", "options": "Customer", - "read_only": 1 + "reqd": 1 }, { "default": "0", "fieldname": "grand_total", "fieldtype": "Currency", "label": "Grand Total", + "options": "currency", "precision": "2", "read_only": 1 }, @@ -290,33 +238,150 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", - "options": "Draft\nResolved\nUnresolved\nCancelled" + "options": "Draft\nResolved\nUnresolved\nCancelled", + "read_only": 1 + }, + { + "description": "For dunning fee and interest", + "fetch_from": "dunning_type.income_account", + "fieldname": "income_account", + "fieldtype": "Link", + "label": "Income Account", + "options": "Account", + "print_hide": 1 + }, + { + "fieldname": "overdue_payments", + "fieldtype": "Table", + "label": "Overdue Payments", + "options": "Overdue Payment" + }, + { + "fieldname": "section_break_28", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "total_interest", + "fieldtype": "Currency", + "label": "Total Interest", + "options": "currency", + "precision": "2", + "read_only": 1 + }, + { + "fieldname": "total_outstanding", + "fieldtype": "Currency", + "label": "Total Outstanding", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "customer_address", + "fieldtype": "Link", + "label": "Customer Address", + "options": "Address", + "print_hide": 1 }, { + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Contact Person", + "options": "Contact", + "print_hide": 1 + }, + { + "default": "0", "fieldname": "dunning_amount", "fieldtype": "Currency", - "hidden": 1, "label": "Dunning Amount", + "options": "currency", "read_only": 1 }, { - "fieldname": "income_account", + "collapsible": 1, + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fetch_from": "dunning_type.cost_center", + "fieldname": "cost_center", "fieldtype": "Link", - "label": "Income Account", - "options": "Account" + "label": "Cost Center", + "options": "Cost Center", + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "printing_settings_section", + "fieldtype": "Section Break", + "label": "Printing Settings" + }, + { + "fieldname": "section_break_32", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" + }, + { + "fieldname": "spacer", + "fieldtype": "Data", + "hidden": 1, + "label": "Spacer", + "print_hide": 1, + "read_only": 1, + "report_hide": 1 + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_address", + "fieldtype": "Link", + "label": "Company Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Currency" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" }, { - "fetch_from": "sales_invoice.conversion_rate", "fieldname": "conversion_rate", "fieldtype": "Float", - "hidden": 1, - "label": "Conversion Rate", + "label": "Conversion Rate" + }, + { + "default": "0", + "fieldname": "base_dunning_amount", + "fieldtype": "Currency", + "label": "Dunning Amount (Company Currency)", + "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fieldname": "column_break_48", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2023-06-03 16:24:01.677026", + "modified": "2023-06-15 15:46:53.865712", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning", diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index b4df0a5270c8..9d0d36b970a7 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -1,131 +1,150 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +""" +# Accounting +1. Payment of outstanding invoices with dunning amount + - Debit full amount to bank + - Credit invoiced amount to receivables + - Credit dunning amount to interest and similar revenue + + -> Resolves dunning automatically +""" import json import frappe -from frappe.utils import cint, flt, getdate +from frappe import _ +from frappe.contacts.doctype.address.address import get_address_display +from frappe.utils import getdate -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( - get_accounting_dimensions, -) -from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries from erpnext.controllers.accounts_controller import AccountsController class Dunning(AccountsController): def validate(self): - self.validate_overdue_days() - self.validate_amount() - if not self.income_account: - self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account") - - def validate_overdue_days(self): - self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 - - def validate_amount(self): - amounts = calculate_interest_and_amount( - self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days - ) - if self.interest_amount != amounts.get("interest_amount"): - self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount")) - if self.dunning_amount != amounts.get("dunning_amount"): - self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount")) - if self.grand_total != amounts.get("grand_total"): - self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total")) - - def on_submit(self): - self.make_gl_entries() - - def on_cancel(self): - if self.dunning_amount: - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") - make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - - def make_gl_entries(self): - if not self.dunning_amount: - return - gl_entries = [] - invoice_fields = [ - "project", - "cost_center", - "debit_to", - "party_account_currency", - "conversion_rate", - "cost_center", - ] - inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1) - - accounting_dimensions = get_accounting_dimensions() - invoice_fields.extend(accounting_dimensions) - - dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate) - default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center") - - gl_entries.append( - self.get_gl_dict( - { - "account": inv.debit_to, - "party_type": "Customer", - "party": self.customer, - "due_date": self.due_date, - "against": self.income_account, - "debit": dunning_in_company_currency, - "debit_in_account_currency": self.dunning_amount, - "against_voucher": self.name, - "against_voucher_type": "Dunning", - "cost_center": inv.cost_center or default_cost_center, - "project": inv.project, - }, - inv.party_account_currency, - item=inv, - ) + self.validate_same_currency() + self.validate_overdue_payments() + self.validate_totals() + self.set_party_details() + self.set_dunning_level() + + def validate_same_currency(self): + """ + Throw an error if invoice currency differs from dunning currency. + """ + for row in self.overdue_payments: + invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency") + if invoice_currency != self.currency: + frappe.throw( + _( + "The currency of invoice {} ({}) is different from the currency of this dunning ({})." + ).format(row.sales_invoice, invoice_currency, self.currency) + ) + + def validate_overdue_payments(self): + daily_interest = self.rate_of_interest / 100 / 365 + + for row in self.overdue_payments: + row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0 + row.interest = row.outstanding * daily_interest * row.overdue_days + + def validate_totals(self): + self.total_outstanding = sum(row.outstanding for row in self.overdue_payments) + self.total_interest = sum(row.interest for row in self.overdue_payments) + self.dunning_amount = self.total_interest + self.dunning_fee + self.base_dunning_amount = self.dunning_amount * self.conversion_rate + self.grand_total = self.total_outstanding + self.dunning_amount + + def set_party_details(self): + from erpnext.accounts.party import _get_party_details + + party_details = _get_party_details( + self.customer, + ignore_permissions=self.flags.ignore_permissions, + doctype=self.doctype, + company=self.company, + posting_date=self.get("posting_date"), + fetch_payment_terms_template=False, + party_address=self.customer_address, + company_address=self.get("company_address"), ) - gl_entries.append( - self.get_gl_dict( - { - "account": self.income_account, - "against": self.customer, - "credit": dunning_in_company_currency, - "cost_center": inv.cost_center or default_cost_center, - "credit_in_account_currency": self.dunning_amount, - "project": inv.project, + for field in [ + "customer_address", + "address_display", + "company_address", + "contact_person", + "contact_display", + "contact_mobile", + ]: + self.set(field, party_details.get(field)) + + self.set("company_address_display", get_address_display(self.company_address)) + + def set_dunning_level(self): + for row in self.overdue_payments: + past_dunnings = frappe.get_all( + "Overdue Payment", + filters={ + "payment_schedule": row.payment_schedule, + "parent": ("!=", row.parent), + "docstatus": 1, }, - item=inv, ) - ) - make_gl_entries( - gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False - ) + row.dunning_level = len(past_dunnings) + 1 def resolve_dunning(doc, state): + """ + Check if all payments have been made and resolve dunning, if yes. Called + when a Payment Entry is submitted. + """ for reference in doc.references: - if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: - dunnings = frappe.get_list( - "Dunning", - filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")}, - ignore_permissions=True, - ) + # Consider partial and full payments: + # Submitting full payment: outstanding_amount will be 0 + # Submitting 1st partial payment: outstanding_amount will be the pending installment + # Cancelling full payment: outstanding_amount will revert to total amount + # Cancelling last partial payment: outstanding_amount will revert to pending amount + submit_condition = reference.outstanding_amount < reference.total_amount + cancel_condition = reference.outstanding_amount <= reference.total_amount + + if reference.reference_doctype == "Sales Invoice" and ( + submit_condition if doc.docstatus == 1 else cancel_condition + ): + state = "Resolved" if doc.docstatus == 2 else "Unresolved" + dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state) for dunning in dunnings: - frappe.db.set_value("Dunning", dunning.name, "status", "Resolved") - - -def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): - interest_amount = 0 - grand_total = flt(outstanding_amount) + flt(dunning_fee) - if rate_of_interest: - interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 - interest_amount = (interest_per_year * cint(overdue_days)) / 365 - grand_total += flt(interest_amount) - dunning_amount = flt(interest_amount) + flt(dunning_fee) - return { - "interest_amount": interest_amount, - "grand_total": grand_total, - "dunning_amount": dunning_amount, - } + resolve = True + dunning = frappe.get_doc("Dunning", dunning.get("name")) + for overdue_payment in dunning.overdue_payments: + outstanding_inv = frappe.get_value( + "Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount" + ) + outstanding_ps = frappe.get_value( + "Payment Schedule", overdue_payment.payment_schedule, "outstanding" + ) + resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True + + dunning.status = "Resolved" if resolve else "Unresolved" + dunning.save() + + +def get_linked_dunnings_as_per_state(sales_invoice, state): + dunning = frappe.qb.DocType("Dunning") + overdue_payment = frappe.qb.DocType("Overdue Payment") + + return ( + frappe.qb.from_(dunning) + .join(overdue_payment) + .on(overdue_payment.parent == dunning.name) + .select(dunning.name) + .where( + (dunning.status == state) + & (dunning.docstatus != 2) + & (overdue_payment.sales_invoice == sales_invoice) + ) + ).run(as_dict=True) @frappe.whitelist() diff --git a/erpnext/accounts/doctype/dunning/dunning_dashboard.py b/erpnext/accounts/doctype/dunning/dunning_dashboard.py deleted file mode 100644 index d1d403141041..000000000000 --- a/erpnext/accounts/doctype/dunning/dunning_dashboard.py +++ /dev/null @@ -1,12 +0,0 @@ -from frappe import _ - - -def get_data(): - return { - "fieldname": "dunning", - "non_standard_fieldnames": { - "Journal Entry": "reference_name", - "Payment Entry": "reference_name", - }, - "transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}], - } diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index e1fd1e984f5b..b29ace275f45 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -1,162 +1,197 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt - -import unittest - import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, nowdate, today -from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount +from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( unlink_payment_on_cancel_of_invoice, ) +from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( + create_dunning as create_dunning_from_sales_invoice, +) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( create_sales_invoice_against_cost_center, ) +test_dependencies = ["Company", "Cost Center"] -class TestDunning(unittest.TestCase): + +class TestDunning(FrappeTestCase): @classmethod - def setUpClass(self): - create_dunning_type() - create_dunning_type_with_zero_interest_rate() + def setUpClass(cls): + super().setUpClass() + create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1) + create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0) unlink_payment_on_cancel_of_invoice() @classmethod - def tearDownClass(self): + def tearDownClass(cls): unlink_payment_on_cancel_of_invoice(0) + super().tearDownClass() - def test_dunning(self): - dunning = create_dunning() - amounts = calculate_interest_and_amount( - dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days - ) - self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44) - self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44) - self.assertEqual(round(amounts.get("grand_total"), 2), 120.44) - - def test_dunning_with_zero_interest_rate(self): - dunning = create_dunning_with_zero_interest_rate() - amounts = calculate_interest_and_amount( - dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days - ) - self.assertEqual(round(amounts.get("interest_amount"), 2), 0) - self.assertEqual(round(amounts.get("dunning_amount"), 2), 20) - self.assertEqual(round(amounts.get("grand_total"), 2), 120) + def test_dunning_without_fees(self): + dunning = create_dunning(overdue_days=20) - def test_gl_entries(self): - dunning = create_dunning() - dunning.submit() - gl_entries = frappe.db.sql( - """select account, debit, credit - from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s - order by account asc""", - dunning.name, - as_dict=1, - ) - self.assertTrue(gl_entries) - expected_values = dict( - (d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]] - ) - for gle in gl_entries: - self.assertEqual(expected_values[gle.account][0], gle.account) - self.assertEqual(expected_values[gle.account][1], gle.debit) - self.assertEqual(expected_values[gle.account][2], gle.credit) + self.assertEqual(round(dunning.total_outstanding, 2), 100.00) + self.assertEqual(round(dunning.total_interest, 2), 0.00) + self.assertEqual(round(dunning.dunning_fee, 2), 0.00) + self.assertEqual(round(dunning.dunning_amount, 2), 0.00) + self.assertEqual(round(dunning.grand_total, 2), 100.00) + + def test_dunning_with_fees_and_interest(self): + dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC") - def test_payment_entry(self): - dunning = create_dunning() + self.assertEqual(round(dunning.total_outstanding, 2), 100.00) + self.assertEqual(round(dunning.total_interest, 2), 0.41) + self.assertEqual(round(dunning.dunning_fee, 2), 10.00) + self.assertEqual(round(dunning.dunning_amount, 2), 10.41) + self.assertEqual(round(dunning.grand_total, 2), 110.41) + + def test_dunning_with_payment_entry(self): + dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC") dunning.submit() pe = get_payment_entry("Dunning", dunning.name) pe.reference_no = "1" pe.reference_date = nowdate() - pe.paid_from_account_currency = dunning.currency - pe.paid_to_account_currency = dunning.currency - pe.source_exchange_rate = 1 - pe.target_exchange_rate = 1 pe.insert() pe.submit() - si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice) - self.assertEqual(si_doc.outstanding_amount, 0) + for overdue_payment in dunning.overdue_payments: + outstanding_amount = frappe.get_value( + "Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount" + ) + self.assertEqual(outstanding_amount, 0) + + dunning.reload() + self.assertEqual(dunning.status, "Resolved") + + def test_dunning_and_payment_against_partially_due_invoice(self): + """ + Create SI with first installment overdue. Check impact of Dunning and Payment Entry. + """ + create_payment_terms_template_for_dunning() + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=add_days(today(), -1 * 6), + qty=1, + rate=100, + do_not_submit=True, + ) + sales_invoice.payment_terms_template = "_Test 50-50 for Dunning" + sales_invoice.submit() + dunning = create_dunning_from_sales_invoice(sales_invoice.name) -def create_dunning(): - posting_date = add_days(today(), -20) - due_date = add_days(today(), -15) - sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, due_date=due_date, status="Overdue" - ) - dunning_type = frappe.get_doc("Dunning Type", "First Notice") - dunning = frappe.new_doc("Dunning") - dunning.sales_invoice = sales_invoice.name - dunning.customer_name = sales_invoice.customer_name - dunning.outstanding_amount = sales_invoice.outstanding_amount - dunning.debit_to = sales_invoice.debit_to - dunning.currency = sales_invoice.currency - dunning.company = sales_invoice.company - dunning.posting_date = nowdate() - dunning.due_date = sales_invoice.due_date - dunning.dunning_type = "First Notice" - dunning.rate_of_interest = dunning_type.rate_of_interest - dunning.dunning_fee = dunning_type.dunning_fee - dunning.save() - return dunning - - -def create_dunning_with_zero_interest_rate(): - posting_date = add_days(today(), -20) - due_date = add_days(today(), -15) + self.assertEqual(len(dunning.overdue_payments), 1) + self.assertEqual(dunning.overdue_payments[0].payment_term, "_Test Payment Term 1 for Dunning") + + dunning.submit() + pe = get_payment_entry("Dunning", dunning.name) + pe.reference_no, pe.reference_date = "2", nowdate() + pe.insert() + pe.submit() + sales_invoice.load_from_db() + dunning.load_from_db() + + self.assertEqual(sales_invoice.status, "Partly Paid") + self.assertEqual(sales_invoice.payment_schedule[0].outstanding, 0) + self.assertEqual(dunning.status, "Resolved") + + # Test impact on cancellation of PE + pe.cancel() + sales_invoice.reload() + dunning.reload() + + self.assertEqual(sales_invoice.status, "Overdue") + self.assertEqual(dunning.status, "Unresolved") + + +def create_dunning(overdue_days, dunning_type_name=None): + posting_date = add_days(today(), -1 * overdue_days) sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, due_date=due_date, status="Overdue" + posting_date=posting_date, qty=1, rate=100 ) - dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest") - dunning = frappe.new_doc("Dunning") - dunning.sales_invoice = sales_invoice.name - dunning.customer_name = sales_invoice.customer_name - dunning.outstanding_amount = sales_invoice.outstanding_amount - dunning.debit_to = sales_invoice.debit_to - dunning.currency = sales_invoice.currency - dunning.company = sales_invoice.company - dunning.posting_date = nowdate() - dunning.due_date = sales_invoice.due_date - dunning.dunning_type = "First Notice with 0% Rate of Interest" - dunning.rate_of_interest = dunning_type.rate_of_interest - dunning.dunning_fee = dunning_type.dunning_fee - dunning.save() - return dunning - - -def create_dunning_type(): + dunning = create_dunning_from_sales_invoice(sales_invoice.name) + + if dunning_type_name: + dunning_type = frappe.get_doc("Dunning Type", dunning_type_name) + dunning.dunning_type = dunning_type.name + dunning.rate_of_interest = dunning_type.rate_of_interest + dunning.dunning_fee = dunning_type.dunning_fee + dunning.income_account = dunning_type.income_account + dunning.cost_center = dunning_type.cost_center + + return dunning.save() + + +def create_dunning_type(title, fee, interest, is_default): + company = "_Test Company" + if frappe.db.exists("Dunning Type", f"{title} - _TC"): + return + dunning_type = frappe.new_doc("Dunning Type") - dunning_type.dunning_type = "First Notice" - dunning_type.start_day = 10 - dunning_type.end_day = 20 - dunning_type.dunning_fee = 20 - dunning_type.rate_of_interest = 8 + dunning_type.dunning_type = title + dunning_type.company = company + dunning_type.is_default = is_default + dunning_type.dunning_fee = fee + dunning_type.rate_of_interest = interest + dunning_type.income_account = get_income_account(company) + dunning_type.cost_center = get_default_cost_center(company) dunning_type.append( "dunning_letter_text", { "language": "en", - "body_text": "We have still not received payment for our invoice ", + "body_text": "We have still not received payment for our invoice", "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", }, ) - dunning_type.save() + dunning_type.insert() + + +def get_income_account(company): + return ( + frappe.get_value("Company", company, "default_income_account") + or frappe.get_all( + "Account", + filters={"is_group": 0, "company": company}, + or_filters={ + "report_type": "Profit and Loss", + "account_type": ("in", ("Income Account", "Temporary")), + }, + limit=1, + pluck="name", + )[0] + ) -def create_dunning_type_with_zero_interest_rate(): - dunning_type = frappe.new_doc("Dunning Type") - dunning_type.dunning_type = "First Notice with 0% Rate of Interest" - dunning_type.start_day = 10 - dunning_type.end_day = 20 - dunning_type.dunning_fee = 20 - dunning_type.rate_of_interest = 0 - dunning_type.append( - "dunning_letter_text", - { - "language": "en", - "body_text": "We have still not received payment for our invoice ", - "closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.", - }, - ) - dunning_type.save() +def create_payment_terms_template_for_dunning(): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term + + create_payment_term("_Test Payment Term 1 for Dunning") + create_payment_term("_Test Payment Term 2 for Dunning") + + if not frappe.db.exists("Payment Terms Template", "_Test 50-50 for Dunning"): + frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test 50-50 for Dunning", + "allocate_payment_based_on_payment_terms": 1, + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "payment_term": "_Test Payment Term 1 for Dunning", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 5, + }, + { + "doctype": "Payment Terms Template Detail", + "payment_term": "_Test Payment Term 2 for Dunning", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 10, + }, + ], + } + ).insert() diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.js b/erpnext/accounts/doctype/dunning_type/dunning_type.js index 54156b488dd7..b2c08c1c7f25 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.js +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.js @@ -1,8 +1,24 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Dunning Type', { - // refresh: function(frm) { - - // } +frappe.ui.form.on("Dunning Type", { + setup: function (frm) { + frm.set_query("income_account", () => { + return { + filters: { + root_type: "Income", + is_group: 0, + company: frm.doc.company, + }, + }; + }); + frm.set_query("cost_center", () => { + return { + filters: { + is_group: 0, + company: frm.doc.company, + }, + }; + }); + }, }); diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.json b/erpnext/accounts/doctype/dunning_type/dunning_type.json index da4366447244..5e39769735ef 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.json +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json @@ -1,23 +1,26 @@ { "actions": [], "allow_rename": 1, - "autoname": "field:dunning_type", + "beta": 1, "creation": "2019-12-04 04:59:08.003664", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "dunning_type", - "overdue_interval_section", - "start_day", - "column_break_4", - "end_day", + "is_default", + "column_break_3", + "company", "section_break_6", "dunning_fee", "column_break_8", "rate_of_interest", "text_block_section", - "dunning_letter_text" + "dunning_letter_text", + "section_break_9", + "income_account", + "column_break_13", + "cost_center" ], "fields": [ { @@ -45,10 +48,6 @@ "fieldtype": "Table", "options": "Dunning Letter Text" }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, { "fieldname": "section_break_6", "fieldtype": "Section Break" @@ -58,32 +57,61 @@ "fieldtype": "Column Break" }, { - "fieldname": "overdue_interval_section", + "fieldname": "rate_of_interest", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Rate of Interest (%) Yearly" + }, + { + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" + }, + { + "fieldname": "section_break_9", "fieldtype": "Section Break", - "label": "Overdue Interval" + "label": "Accounting Details" }, { - "fieldname": "start_day", - "fieldtype": "Int", - "label": "Start Day" + "fieldname": "income_account", + "fieldtype": "Link", + "label": "Income Account", + "options": "Account" }, { - "fieldname": "end_day", - "fieldtype": "Int", - "label": "End Day" + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" }, { - "fieldname": "rate_of_interest", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Rate of Interest (%) Yearly" + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + } + ], + "links": [ + { + "link_doctype": "Dunning", + "link_fieldname": "dunning_type" } ], - "links": [], - "modified": "2020-07-15 17:14:17.835074", + "modified": "2021-11-13 00:25:35.659283", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning Type", + "naming_rule": "By script", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.py b/erpnext/accounts/doctype/dunning_type/dunning_type.py index 1b9bb9c0325f..226e159a3bba 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.py +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.py @@ -2,9 +2,11 @@ # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document class DunningType(Document): - pass + def autoname(self): + company_abbr = frappe.get_value("Company", self.company, "abbr") + self.name = f"{self.dunning_type} - {company_abbr}" diff --git a/erpnext/accounts/doctype/dunning_type/test_records.json b/erpnext/accounts/doctype/dunning_type/test_records.json new file mode 100644 index 000000000000..7f28aab873c6 --- /dev/null +++ b/erpnext/accounts/doctype/dunning_type/test_records.json @@ -0,0 +1,36 @@ +[ + { + "doctype": "Dunning Type", + "dunning_type": "_Test First Notice", + "company": "_Test Company", + "is_default": 1, + "dunning_fee": 0.0, + "rate_of_interest": 0.0, + "dunning_letter_text": [ + { + "language": "en", + "body_text": "We have still not received payment for our invoice", + "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees." + } + ], + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC" + }, + { + "doctype": "Dunning Type", + "dunning_type": "_Test Second Notice", + "company": "_Test Company", + "is_default": 0, + "dunning_fee": 10.0, + "rate_of_interest": 10.0, + "dunning_letter_text": [ + { + "language": "en", + "body_text": "We have still not received payment for our invoice", + "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees." + } + ], + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC" + } +] diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index c52ea24f25a4..3b5698b118a7 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -258,8 +258,8 @@ def calculate_new_account_balance(company, posting_date, account_details): new_balance_in_base_currency = 0 new_balance_in_account_currency = 0 - current_exchange_rate = calculate_exchange_rate_using_last_gle( - company, d.account, d.party_type, d.party + current_exchange_rate = ( + calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party) or 0.0 ) gain_loss = new_balance_in_account_currency - ( diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js index bc77dac1cdd9..508b2eaf2a48 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js @@ -8,17 +8,6 @@ frappe.ui.form.on('Fiscal Year', { frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)); } }, - refresh: function (frm) { - if (!frm.doc.__islocal && (frm.doc.name != frappe.sys_defaults.fiscal_year)) { - frm.add_custom_button(__("Set as Default"), () => frm.events.set_as_default(frm)); - frm.set_intro(__("To set this Fiscal Year as Default, click on 'Set as Default'")); - } else { - frm.set_intro(""); - } - }, - set_as_default: function(frm) { - return frm.call('set_as_default'); - }, year_start_date: function(frm) { if (!frm.doc.is_short_year) { let year_end_date = diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index 9d1b99b29b1f..0dfe569ec9af 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -4,28 +4,12 @@ import frappe from dateutil.relativedelta import relativedelta -from frappe import _, msgprint +from frappe import _ from frappe.model.document import Document from frappe.utils import add_days, add_years, cstr, getdate class FiscalYear(Document): - @frappe.whitelist() - def set_as_default(self): - frappe.db.set_single_value("Global Defaults", "current_fiscal_year", self.name) - global_defaults = frappe.get_doc("Global Defaults") - global_defaults.check_permission("write") - global_defaults.on_update() - - # clear cache - frappe.clear_cache() - - msgprint( - _( - "{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect." - ).format(self.name) - ) - def validate(self): self.validate_dates() self.validate_overlap() @@ -68,13 +52,6 @@ def on_update(self): frappe.cache().delete_value("fiscal_years") def on_trash(self): - global_defaults = frappe.get_doc("Global Defaults") - if global_defaults.current_fiscal_year == self.name: - frappe.throw( - _( - "You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings" - ).format(self.name) - ) frappe.cache().delete_value("fiscal_years") def validate_overlap(self): diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json index b42d712d88a0..87f0ad104831 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json @@ -35,6 +35,7 @@ { "fieldname": "company", "fieldtype": "Link", + "in_filter": 1, "in_list_view": 1, "label": "Company", "options": "Company", @@ -56,7 +57,7 @@ } ], "links": [], - "modified": "2022-01-18 21:11:23.105589", + "modified": "2023-07-09 18:11:23.105589", "modified_by": "Administrator", "module": "Accounts", "name": "Item Tax Template", @@ -102,4 +103,4 @@ "states": [], "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 83312dbd229b..ea4a2d4b19d1 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -408,6 +408,15 @@ def validate_party(self): d.idx, d.account ) ) + elif ( + d.party_type + and frappe.db.get_value("Party Type", d.party_type, "account_type") != account_type + ): + frappe.throw( + _("Row {0}: Account {1} and Party Type {2} have different account types").format( + d.idx, d.account, d.party_type + ) + ) def check_credit_limit(self): customers = list( diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py b/erpnext/accounts/doctype/overdue_payment/__init__.py similarity index 100% rename from erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py rename to erpnext/accounts/doctype/overdue_payment/__init__.py diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json new file mode 100644 index 000000000000..99e16469d047 --- /dev/null +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json @@ -0,0 +1,170 @@ +{ + "actions": [], + "creation": "2021-09-15 18:34:27.172906", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_invoice", + "payment_schedule", + "dunning_level", + "payment_term", + "section_break_15", + "description", + "section_break_4", + "due_date", + "overdue_days", + "mode_of_payment", + "column_break_5", + "invoice_portion", + "section_break_16", + "payment_amount", + "outstanding", + "paid_amount", + "discounted_amount", + "interest" + ], + "fields": [ + { + "columns": 2, + "fieldname": "payment_term", + "fieldtype": "Link", + "label": "Payment Term", + "options": "Payment Term", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_15", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "columns": 2, + "fetch_from": "payment_term.description", + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description", + "read_only": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "columns": 2, + "fieldname": "due_date", + "fieldtype": "Date", + "label": "Due Date", + "read_only": 1 + }, + { + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "invoice_portion", + "fieldtype": "Percent", + "label": "Invoice Portion", + "read_only": 1 + }, + { + "columns": 2, + "fieldname": "payment_amount", + "fieldtype": "Currency", + "label": "Payment Amount", + "options": "currency", + "read_only": 1 + }, + { + "fetch_from": "payment_amount", + "fieldname": "outstanding", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Outstanding", + "options": "currency", + "read_only": 1 + }, + { + "depends_on": "paid_amount", + "fieldname": "paid_amount", + "fieldtype": "Currency", + "label": "Paid Amount", + "options": "currency" + }, + { + "default": "0", + "depends_on": "discounted_amount", + "fieldname": "discounted_amount", + "fieldtype": "Currency", + "label": "Discounted Amount", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "sales_invoice", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Invoice", + "options": "Sales Invoice", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "payment_schedule", + "fieldtype": "Data", + "label": "Payment Schedule", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "overdue_days", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Overdue Days", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "dunning_level", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Dunning Level", + "read_only": 1 + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "fieldname": "interest", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Interest", + "options": "currency", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-23 13:48:27.898830", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Overdue Payment", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.py b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py new file mode 100644 index 000000000000..6a543ad46749 --- /dev/null +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class OverduePayment(Document): + pass diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 65ed4669b1ef..7542babe9218 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -226,10 +226,12 @@ def validate_allocated_amount_with_latest_data(self): latest_lookup = {} for d in latest_references: d = frappe._dict(d) - latest_lookup.update({(d.voucher_type, d.voucher_no): 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)) + latest = (latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()).get( + d.payment_term + ) # The reference has already been fully paid if not latest: @@ -237,10 +239,9 @@ def validate_allocated_amount_with_latest_data(self): _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) ) # The reference has already been partly paid - elif ( - latest.outstanding_amount < latest.invoice_amount - and flt(d.outstanding_amount, d.precision("outstanding_amount")) != latest.outstanding_amount - ): + elif latest.outstanding_amount < latest.invoice_amount and flt( + d.outstanding_amount, d.precision("outstanding_amount") + ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")): frappe.throw( _( "{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." @@ -252,6 +253,18 @@ def validate_allocated_amount_with_latest_data(self): if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): frappe.throw(fail_message.format(d.idx)) + if d.payment_term and ( + (flt(d.allocated_amount)) > 0 + and flt(d.allocated_amount) > flt(latest.payment_term_outstanding) + ): + frappe.throw( + _( + "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}" + ).format( + d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term + ) + ) + # Check for negative outstanding invoices as well if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): frappe.throw(fail_message.format(d.idx)) @@ -1433,6 +1446,9 @@ def get_outstanding_reference_documents(args, validate=False): if args.get("party_type") == "Member": return + if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"): + args["get_outstanding_invoices"] = True + ple = qb.DocType("Payment Ledger Entry") common_filter = [] accounting_dimensions_filter = [] @@ -1498,7 +1514,9 @@ def get_outstanding_reference_documents(args, validate=False): accounting_dimensions=accounting_dimensions_filter, ) - outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) + outstanding_invoices = split_invoices_based_on_payment_terms( + outstanding_invoices, args.get("company") + ) for d in outstanding_invoices: d["exchange_rate"] = 1 @@ -1558,8 +1576,27 @@ def get_outstanding_reference_documents(args, validate=False): return data -def split_invoices_based_on_payment_terms(outstanding_invoices): +def split_invoices_based_on_payment_terms(outstanding_invoices, company): invoice_ref_based_on_payment_terms = {} + + company_currency = ( + frappe.db.get_value("Company", company, "default_currency") if company else None + ) + exc_rates = frappe._dict() + for doctype in ["Sales Invoice", "Purchase Invoice"]: + invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] + for x in frappe.db.get_all( + doctype, + filters={"name": ["in", invoices]}, + fields=["name", "currency", "conversion_rate", "party_account_currency"], + ): + exc_rates[x.name] = frappe._dict( + conversion_rate=x.conversion_rate, + currency=x.currency, + party_account_currency=x.party_account_currency, + company_currency=company_currency, + ) + for idx, d in enumerate(outstanding_invoices): if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]: payment_term_template = frappe.db.get_value( @@ -1576,6 +1613,14 @@ def split_invoices_based_on_payment_terms(outstanding_invoices): for payment_term in payment_schedule: if payment_term.outstanding > 0.1: + doc_details = exc_rates.get(payment_term.parent, None) + is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and ( + doc_details.party_account_currency != doc_details.company_currency + ) + payment_term_outstanding = flt(payment_term.outstanding) + if not is_multi_currency_acc: + payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding) + invoice_ref_based_on_payment_terms.setdefault(idx, []) invoice_ref_based_on_payment_terms[idx].append( frappe._dict( @@ -1587,6 +1632,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices): "posting_date": d.posting_date, "invoice_amount": flt(d.invoice_amount), "outstanding_amount": flt(d.outstanding_amount), + "payment_term_outstanding": payment_term_outstanding, "payment_amount": payment_term.payment_amount, "payment_term": payment_term.payment_term, "account": d.account, @@ -1627,60 +1673,59 @@ def get_orders_to_be_billed( cost_center=None, filters=None, ): + voucher_type = None if party_type == "Customer": voucher_type = "Sales Order" elif party_type == "Supplier": voucher_type = "Purchase Order" - elif party_type == "Employee": - voucher_type = None + + if not voucher_type: + return [] # Add cost center condition - if voucher_type: - doc = frappe.get_doc({"doctype": voucher_type}) - condition = "" - if doc and hasattr(doc, "cost_center") and doc.cost_center: - condition = " and cost_center='%s'" % cost_center - - orders = [] - if voucher_type: - if party_account_currency == company_currency: - grand_total_field = "base_grand_total" - rounded_total_field = "base_rounded_total" - else: - grand_total_field = "grand_total" - rounded_total_field = "rounded_total" - - orders = frappe.db.sql( - """ - select - name as voucher_no, - if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, - (if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount, - transaction_date as posting_date - from - `tab{voucher_type}` - where - {party_type} = %s - and docstatus = 1 - and company = %s - and ifnull(status, "") != "Closed" - and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid - and abs(100 - per_billed) > 0.01 - {condition} - order by - transaction_date, name - """.format( - **{ - "rounded_total_field": rounded_total_field, - "grand_total_field": grand_total_field, - "voucher_type": voucher_type, - "party_type": scrub(party_type), - "condition": condition, - } - ), - (party, company), - as_dict=True, - ) + doc = frappe.get_doc({"doctype": voucher_type}) + condition = "" + if doc and hasattr(doc, "cost_center") and doc.cost_center: + condition = " and cost_center='%s'" % cost_center + + if party_account_currency == company_currency: + grand_total_field = "base_grand_total" + rounded_total_field = "base_rounded_total" + else: + grand_total_field = "grand_total" + rounded_total_field = "rounded_total" + + orders = frappe.db.sql( + """ + select + name as voucher_no, + if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, + (if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount, + transaction_date as posting_date + from + `tab{voucher_type}` + where + {party_type} = %s + and docstatus = 1 + and company = %s + and ifnull(status, "") != "Closed" + and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid + and abs(100 - per_billed) > 0.01 + {condition} + order by + transaction_date, name + """.format( + **{ + "rounded_total_field": rounded_total_field, + "grand_total_field": grand_total_field, + "voucher_type": voucher_type, + "party_type": scrub(party_type), + "condition": condition, + } + ), + (party, company), + as_dict=True, + ) order_list = [] for d in orders: @@ -1713,6 +1758,8 @@ def get_negative_outstanding_invoices( cost_center=None, condition=None, ): + if party_type not in ["Customer", "Supplier"]: + return [] voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" account = "debit_to" if voucher_type == "Sales Invoice" else "credit_to" supplier_condition = "" @@ -2007,28 +2054,27 @@ def get_payment_entry( pe.append("references", reference) else: if dt == "Dunning": + for overdue_payment in doc.overdue_payments: + pe.append( + "references", + { + "reference_doctype": "Sales Invoice", + "reference_name": overdue_payment.sales_invoice, + "payment_term": overdue_payment.payment_term, + "due_date": overdue_payment.due_date, + "total_amount": overdue_payment.outstanding, + "outstanding_amount": overdue_payment.outstanding, + "allocated_amount": overdue_payment.outstanding, + }, + ) + pe.append( - "references", - { - "reference_doctype": "Sales Invoice", - "reference_name": doc.get("sales_invoice"), - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - "total_amount": doc.get("outstanding_amount"), - "outstanding_amount": doc.get("outstanding_amount"), - "allocated_amount": doc.get("outstanding_amount"), - }, - ) - pe.append( - "references", + "deductions", { - "reference_doctype": dt, - "reference_name": dn, - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - "total_amount": doc.get("dunning_amount"), - "outstanding_amount": doc.get("dunning_amount"), - "allocated_amount": doc.get("dunning_amount"), + "account": doc.income_account, + "cost_center": doc.cost_center, + "amount": -1 * doc.dunning_amount, + "description": _("Interest and/or dunning fee"), }, ) else: @@ -2122,8 +2168,10 @@ def set_party_account_currency(dt, party_account, doc): def set_payment_type(dt, doc): if ( - dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0) - ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): + (dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0)) + or (dt == "Purchase Invoice" and doc.outstanding_amount < 0) + or dt == "Dunning" + ): payment_type = "Receive" else: payment_type = "Pay" @@ -2368,6 +2416,7 @@ def get_reference_as_per_payment_terms( "due_date": doc.get("due_date"), "total_amount": grand_total, "outstanding_amount": outstanding_amount, + "payment_term_outstanding": payment_term_outstanding, "payment_term": payment_term.payment_term, "allocated_amount": payment_term_outstanding, } diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 70cc4b3d347f..c6e93f3f7a29 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 @@ def test_details_update_on_reference_table(self): } 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/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 641f4528c534..49472484ef44 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -126,21 +126,22 @@ def check_if_previous_year_closed(self): def make_gl_entries(self, get_opening_entries=False): gl_entries = self.get_gl_entries() closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries) - if gl_entries: - if len(gl_entries) > 5000: - frappe.enqueue( - process_gl_entries, - gl_entries=gl_entries, - closing_entries=closing_entries, - voucher_name=self.name, - queue="long", - ) - frappe.msgprint( - _("The GL Entries will be processed in the background, it can take a few minutes."), - alert=True, - ) - else: - process_gl_entries(gl_entries, closing_entries, voucher_name=self.name) + if len(gl_entries) > 5000: + frappe.enqueue( + process_gl_entries, + gl_entries=gl_entries, + closing_entries=closing_entries, + voucher_name=self.name, + company=self.company, + closing_date=self.posting_date, + queue="long", + ) + frappe.msgprint( + _("The GL Entries will be processed in the background, it can take a few minutes."), + alert=True, + ) + else: + process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date) def get_grouped_gl_entries(self, get_opening_entries=False): closing_entries = [] @@ -321,24 +322,22 @@ def get_balances_based_on_dimensions( return query.run(as_dict=1) -def process_gl_entries(gl_entries, closing_entries, voucher_name=None): +def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date): from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( make_closing_entries, ) from erpnext.accounts.general_ledger import make_gl_entries try: - make_gl_entries(gl_entries, merge_entries=False) - make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name) - frappe.db.set_value( - "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed" - ) + if gl_entries: + make_gl_entries(gl_entries, merge_entries=False) + + make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date) + frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed") except Exception as e: frappe.db.rollback() frappe.log_error(e) - frappe.db.set_value( - "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed" - ) + frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed") def make_reverse_gl_entries(voucher_type, voucher_no): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8753ebc3baf0..4ec103c9f207 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -142,9 +142,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e cur_frm.events.create_invoice_discounting(cur_frm); }, __('Create')); - if (doc.due_date < frappe.datetime.get_today()) { - cur_frm.add_custom_button(__('Dunning'), function() { - cur_frm.events.create_dunning(cur_frm); + const payment_is_overdue = doc.payment_schedule.map( + row => Date.parse(row.due_date) < Date.now() + ).reduce( + (prev, current) => prev || current + ); + + if (payment_is_overdue) { + this.frm.add_custom_button(__('Dunning'), () => { + this.frm.events.create_dunning(this.frm); }, __('Create')); } } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7ab1c893971d..b3212b5a7b3d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2516,55 +2516,49 @@ def get_mode_of_payment_info(mode_of_payment, company): @frappe.whitelist() -def create_dunning(source_name, target_doc=None): +def create_dunning(source_name, target_doc=None, ignore_permissions=False): from frappe.model.mapper import get_mapped_doc - from erpnext.accounts.doctype.dunning.dunning import ( - calculate_interest_and_amount, - get_dunning_letter_text, - ) + def postprocess_dunning(source, target): + from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text - def set_missing_values(source, target): - target.sales_invoice = source_name - target.outstanding_amount = source.outstanding_amount - overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days - target.overdue_days = overdue_days - if frappe.db.exists( - "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]} - ): - dunning_type = frappe.get_doc( - "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]} - ) + dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1, "company": source.company}) + if dunning_type: + dunning_type = frappe.get_doc("Dunning Type", dunning_type) target.dunning_type = dunning_type.name target.rate_of_interest = dunning_type.rate_of_interest target.dunning_fee = dunning_type.dunning_fee - letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict()) + target.income_account = dunning_type.income_account + target.cost_center = dunning_type.cost_center + letter_text = get_dunning_letter_text( + dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language + ) + if letter_text: target.body_text = letter_text.get("body_text") target.closing_text = letter_text.get("closing_text") target.language = letter_text.get("language") - amounts = calculate_interest_and_amount( - target.outstanding_amount, - target.rate_of_interest, - target.dunning_fee, - target.overdue_days, - ) - target.interest_amount = amounts.get("interest_amount") - target.dunning_amount = amounts.get("dunning_amount") - target.grand_total = amounts.get("grand_total") - doclist = get_mapped_doc( - "Sales Invoice", - source_name, - { + target.validate() + + return get_mapped_doc( + from_doctype="Sales Invoice", + from_docname=source_name, + target_doc=target_doc, + table_maps={ "Sales Invoice": { "doctype": "Dunning", - } + "field_map": {"customer_address": "customer_address", "parent": "sales_invoice"}, + }, + "Payment Schedule": { + "doctype": "Overdue Payment", + "field_map": {"name": "payment_schedule", "parent": "sales_invoice"}, + "condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(), + }, }, - target_doc, - set_missing_values, + postprocess=postprocess_dunning, + ignore_permissions=ignore_permissions, ) - return doclist def check_if_return_invoice_linked_with_payment_entry(self): diff --git a/erpnext/accounts/doctype/shareholder/shareholder.js b/erpnext/accounts/doctype/shareholder/shareholder.js index c6f101e7f31d..544d417a0e5d 100644 --- a/erpnext/accounts/doctype/shareholder/shareholder.js +++ b/erpnext/accounts/doctype/shareholder/shareholder.js @@ -3,8 +3,6 @@ frappe.ui.form.on('Shareholder', { refresh: function(frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Shareholder' }; - frm.toggle_display(['contact_html'], !frm.doc.__islocal); if (frm.doc.__islocal) { diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index f1dad875fa79..e9dc5fc0ccca 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -13,14 +13,11 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) +from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.utils import create_payment_ledger_entry -class ClosedAccountingPeriod(frappe.ValidationError): - pass - - def make_gl_entries( gl_map, cancel=False, diff --git a/erpnext/accounts/print_format/dunning_letter/dunning_letter.json b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json index a7eac70b65fc..c48e1cf35b42 100644 --- a/erpnext/accounts/print_format/dunning_letter/dunning_letter.json +++ b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json @@ -1,4 +1,5 @@ { + "absolute_value": 0, "align_labels_right": 0, "creation": "2019-12-11 04:37:14.012805", "css": ".print-format th {\n background-color: transparent !important;\n border-bottom: 1px solid !important;\n border-top: none !important;\n}\n.print-format .ql-editor {\n padding-left: 0px;\n padding-right: 0px;\n}\n\n.print-format table {\n margin-bottom: 0px;\n }\n.print-format .table-data tr:last-child { \n border-bottom: 1px solid !important;\n}\n\n.print-format .table-inner tr:last-child {\n border-bottom:none !important;\n}\n.print-format .table-inner {\n margin: 0px 0px;\n}\n\n.print-format .table-data ul li { \n color:#787878 !important;\n}\n\n.no-top-border {\n border-top:none !important;\n}\n\n.table-inner td {\n padding-left: 0px !important; \n padding-top: 1px !important;\n padding-bottom: 1px !important;\n color:#787878 !important;\n}\n\n.total {\n background-color: lightgrey !important;\n padding-top: 4px !important;\n padding-bottom: 4px !important;\n}\n", @@ -9,10 +10,10 @@ "docstatus": 0, "doctype": "Print Format", "font": "Arial", - "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"{{doc.customer_name}}
\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"
\\n
{{_(doc.dunning_type)}}
\\n
{{ doc.name }}
\\n
\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldname\": \"sales_invoice\", \"print_hide\": 0, \"label\": \"Sales Invoice\"}, {\"fieldname\": \"due_date\", \"print_hide\": 0, \"label\": \"Due Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n \\n \\n \\n\\t \\n \\n \\n \\n \\n \\n {%if doc.rate_of_interest > 0%}\\n \\n \\n \\n \\n {% endif %}\\n {%if doc.dunning_fee > 0%}\\n \\n \\n \\n \\n {% endif %}\\n \\n
{{_(\\\"Description\\\")}}{{_(\\\"Amount\\\")}}
\\n {{_(\\\"Outstanding Amount\\\")}}\\n \\n {{doc.get_formatted(\\\"outstanding_amount\\\")}}\\n
\\n {{_(\\\"Interest \\\")}} {{doc.rate_of_interest}}% p.a. ({{doc.overdue_days}} {{_(\\\"days\\\")}})\\n \\n {{doc.get_formatted(\\\"interest_amount\\\")}}\\n
\\n {{_(\\\"Dunning Fee\\\")}}\\n \\n {{doc.get_formatted(\\\"dunning_fee\\\")}}\\n
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n
\\n\\t\\t
\\n\\t\\t\\t{{_(\\\"Grand Total\\\")}}
\\n\\t\\t
\\n\\t\\t\\t{{doc.get_formatted(\\\"grand_total\\\")}}\\n\\t\\t
\\n
\\n\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]", + "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"{{doc.customer_name}}
\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"
\\n
{{_(doc.dunning_type)}}
\\n
{{ doc.name }}
\\n
\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"overdue_payments\", \"print_hide\": 0, \"label\": \"Overdue Payments\", \"visible_columns\": [{\"fieldname\": \"sales_invoice\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"dunning_level\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"due_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"overdue_days\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"invoice_portion\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"outstanding\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"interest\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_outstanding\", \"print_hide\": 0, \"label\": \"Total Outstanding\"}, {\"fieldname\": \"dunning_fee\", \"print_hide\": 0, \"label\": \"Dunning Fee\"}, {\"fieldname\": \"total_interest\", \"print_hide\": 0, \"label\": \"Total Interest\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]", "idx": 0, "line_breaks": 0, - "modified": "2020-07-14 18:25:44.348207", + "modified": "2021-09-30 10:22:02.603871", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning Letter", diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index dd965a9813e9..d58fd95a8403 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -49,7 +49,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Start Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, on_change: () => { frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) { @@ -65,7 +65,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("End Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, on_change: () => { frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) { @@ -139,7 +139,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { return value; }, onload: function() { - let fiscal_year = frappe.defaults.get_user_default("fiscal_year") + let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js index 0056b9e8f564..96e0c844ca58 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js @@ -48,7 +48,7 @@ function get_filters() { "label": __("Start Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1 }, { @@ -56,7 +56,7 @@ function get_filters() { "label": __("End Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1 }, { @@ -100,7 +100,7 @@ frappe.query_reports["Deferred Revenue and Expense"] = { return default_formatter(value, row, column, data); }, onload: function(report){ - let fiscal_year = frappe.defaults.get_user_default("fiscal_year"); + let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index 3e11643776ea..cad5325c6e98 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -4,9 +4,10 @@ import frappe from frappe import _, qb from frappe.query_builder import Column, functions -from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded +from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, getdate, rounded from erpnext.accounts.report.financial_statements import get_period_list +from erpnext.accounts.utils import get_fiscal_year class Deferred_Item(object): @@ -226,7 +227,7 @@ def __init__(self, filters=None): # If no filters are provided, get user defaults if not filters: - fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date=getdate())) self.filters = frappe._dict( { "company": frappe.defaults.get_user_default("Company"), diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index 023ff225eea4..c84b843f1fd8 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -10,6 +10,7 @@ from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import ( Deferred_Revenue_and_Expense_Report, ) +from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.stock.doctype.item.test_item import create_item @@ -116,7 +117,7 @@ def test_deferred_revenue(self): pda.submit() # execute report - fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { "company": frappe.defaults.get_user_default("Company"), @@ -209,7 +210,7 @@ def test_deferred_expense(self): pda.submit() # execute report - fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { "company": frappe.defaults.get_user_default("Company"), @@ -297,7 +298,7 @@ def test_zero_months(self): pda.submit() # execute report - fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { "company": frappe.defaults.get_user_default("Company"), diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js index ea05a35b259c..9d416db4fdd8 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js @@ -18,7 +18,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "on_change": function(query_report) { var fiscal_year = query_report.get_values().fiscal_year; diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index f3a892ba43cc..db9609debe61 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -416,6 +416,7 @@ def set_gl_entries_by_account( filters, gl_entries_by_account, ignore_closing_entries=False, + ignore_opening_entries=False, ): """Returns a dict like { "account": [gl entries], ... }""" gl_entries = [] @@ -426,7 +427,6 @@ def set_gl_entries_by_account( pluck="name", ) - ignore_opening_entries = False if accounts_list: # For balance sheet if not from_date: diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js index 8dc5ab36dd96..92cf36ebc528 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js @@ -12,14 +12,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { erpnext.financial_statements); frappe.query_reports["Gross and Net Profit Report"]["filters"].push( - { - "fieldname": "project", - "label": __("Project"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Project', txt); - } - }, { "fieldname": "accumulated_values", "label": __("Accumulated Values"), diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js index 298d83894c69..e794f270c2bc 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js @@ -9,16 +9,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { erpnext.utils.add_dimensions('Profit and Loss Statement', 10); frappe.query_reports["Profit and Loss Statement"]["filters"].push( - { - "fieldname": "project", - "label": __("Project"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Project', txt, { - company: frappe.query_report.get_filter_value("company") - }); - }, - }, { "fieldname": "include_default_book_entries", "label": __("Include Default Book Entries"), diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js index 889ede5a8241..6caebd34a2fa 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -25,7 +25,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "on_change": function(query_report) { var fiscal_year = query_report.get_values().fiscal_year; diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 078b06519f18..e45c3adcb6dd 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -17,7 +17,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "on_change": function(query_report) { var fiscal_year = query_report.get_values().fiscal_year; diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index d51c4c4acba0..599c8a312a5d 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -117,6 +117,7 @@ def get_data(filters): filters, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry), + ignore_opening_entries=True, ) calculate_values(accounts, gl_entries_by_account, opening_balances) @@ -159,6 +160,8 @@ def get_rootwise_opening_balances(filters, report_type): accounting_dimensions, period_closing_voucher=last_period_closing_voucher[0].name, ) + + # Report getting generate from the mid of a fiscal year if getdate(last_period_closing_voucher[0].posting_date) < getdate( add_days(filters.from_date, -1) ): @@ -218,9 +221,18 @@ def get_opening_balance( ) else: if start_date: - opening_balance = opening_balance.where(closing_balance.posting_date >= start_date) + opening_balance = opening_balance.where( + (closing_balance.posting_date >= start_date) + & (closing_balance.posting_date < filters.from_date) + ) opening_balance = opening_balance.where(closing_balance.is_opening == "No") - opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date) + else: + opening_balance = opening_balance.where( + (closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes") + ) + + if doctype == "GL Entry": + opening_balance = opening_balance.where(closing_balance.is_cancelled == 0) if ( not filters.show_unclosed_fy_pl_balances diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js index 0e93035a35d9..0f7578cdc17a 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js @@ -16,7 +16,7 @@ frappe.query_reports["Trial Balance for Party"] = { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "on_change": function(query_report) { var fiscal_year = query_report.get_values().fiscal_year; diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 31473db675f9..e35466315143 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -850,7 +850,7 @@ def get_held_invoices(party_type, party): if party_type == "Supplier": held_invoices = frappe.db.sql( - "select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()", + "select name from `tabPurchase Invoice` where on_hold = 1 and release_date IS NOT NULL and release_date > CURDATE()", as_dict=1, ) held_invoices = set(d["name"] for d in held_invoices) @@ -1110,6 +1110,12 @@ def get_autoname_with_number(number_value, doc_title, company): return " - ".join(parts) +def parse_naming_series_variable(doc, variable): + if variable == "FY": + date = doc.get("posting_date") or doc.get("transaction_date") or getdate() + return get_fiscal_year(date=date, company=doc.get("company"))[0] + + @frappe.whitelist() def get_coa(doctype, parent, is_root, chart=None): from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import ( diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index f9c600731b3c..4ccc3f8013bf 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -63,7 +63,7 @@ frappe.ui.form.on('Asset Movement', { fieldnames_to_be_altered = { target_location: { read_only: 0, reqd: 1 }, source_location: { read_only: 1, reqd: 0 }, - from_employee: { read_only: 0, reqd: 1 }, + from_employee: { read_only: 0, reqd: 0 }, to_employee: { read_only: 1, reqd: 0 } }; } diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index b58ca10482b8..22055dcb736e 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -62,29 +62,20 @@ def validate_location(self): frappe.throw(_("Source and Target Location cannot be same")) if self.purpose == "Receipt": - # only when asset is bought and first entry is made - if not d.source_location and not (d.target_location or d.to_employee): + if not (d.source_location or d.from_employee) 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.source_location: - # when asset is received from an employee - if d.target_location and not d.from_employee: - frappe.throw( - _("From employee is required while receiving Asset {0} to a target location").format( - d.asset - ) - ) - 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) - ) - if d.to_employee and d.target_location: - frappe.throw( - _( - "Asset {0} cannot be received at a location and given to employee in a single movement" - ).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) + ) def validate_employee(self): for d in self.assets: diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index b788a32d6abd..48b17f58fb2d 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -82,7 +82,7 @@ frappe.query_reports["Fixed Asset Register"] = { "label": __("Start Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", }, { @@ -90,7 +90,7 @@ frappe.query_reports["Fixed Asset Register"] = { "label": __("End Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", }, { diff --git a/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json b/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json index 6452ed2139b3..751796bbbb55 100644 --- a/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json +++ b/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json @@ -5,18 +5,19 @@ "custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Item\"}", - "idx": 0, + "idx": 1, "is_public": 1, "is_standard": 1, - "modified": "2020-07-21 16:13:25.092287", + "modified": "2023-07-19 13:06:42.937941", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Trends", "number_of_groups": 0, "owner": "Administrator", "report_name": "Purchase Order Trends", + "roles": [], "timeseries": 0, "type": "Line", "use_report_chart": 1, diff --git a/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json b/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json index 6f7da8ea870a..f6b971753987 100644 --- a/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json +++ b/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json @@ -4,18 +4,19 @@ "creation": "2020-07-20 21:01:02.329519", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Supplier\"}", "idx": 0, "is_public": 1, "is_standard": 1, - "modified": "2020-07-22 12:43:40.829652", + "modified": "2023-07-19 13:07:41.753556", "modified_by": "Administrator", "module": "Buying", "name": "Top Suppliers", "number_of_groups": 0, "owner": "Administrator", "report_name": "Purchase Receipt Trends", + "roles": [], "timeseries": 0, "type": "Bar", "use_report_chart": 1, diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 5b95d0fde37c..372ca56b86b7 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -66,8 +66,6 @@ frappe.ui.form.on("Supplier", { }, refresh: function (frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Supplier' } - if (frappe.defaults.get_default("supp_master_name") != "Naming Series") { frm.toggle_display("naming_series", false); } else { diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.json b/erpnext/communication/doctype/communication_medium/communication_medium.json index 1e1fe3bf499f..b6b9c7e4347d 100644 --- a/erpnext/communication/doctype/communication_medium/communication_medium.json +++ b/erpnext/communication/doctype/communication_medium/communication_medium.json @@ -61,7 +61,7 @@ "fieldname": "communication_channel", "fieldtype": "Select", "label": "Communication Channel", - "options": "\nExotel" + "options": "" } ], "links": [], diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 3bb11282f1f8..d1dcd6a109de 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -822,6 +822,15 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_doctypes_for_closing(doctype, txt, searchfield, start, page_len, filters): + doctypes = frappe.get_hooks("period_closing_doctypes") + if txt: + doctypes = [d for d in doctypes if txt.lower() in d.lower()] + return [(d,) for d in set(doctypes)] + + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_tax_template(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 954668055e19..173e812dbd0b 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -669,7 +669,11 @@ def get_filters( if reference_voucher_detail_no: filters["voucher_detail_no"] = reference_voucher_detail_no - if item_row and item_row.get("warehouse"): + if ( + voucher_type in ["Purchase Receipt", "Purchase Invoice"] + and item_row + and item_row.get("warehouse") + ): filters["warehouse"] = item_row.get("warehouse") return filters diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 5137e030582e..caf4b6f18bc5 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -201,6 +201,12 @@ def get_gl_entries( warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"] expense_account = frappe.get_cached_value("Company", self.company, "default_expense_account") + if not expense_account: + frappe.throw( + _( + "Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer" + ).format(frappe.bold(self.company)) + ) gl_list.append( self.get_gl_dict( diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index b98a27ede8e8..9ac54183a211 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -30,11 +30,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller var me = this; let doc = this.frm.doc; erpnext.toggle_naming_series(); - frappe.dynamic_link = { - doc: doc, - fieldname: 'name', - doctype: 'Lead' - }; if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) { this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create")); diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js index 495ed291ae94..c1a7ff576c1a 100644 --- a/erpnext/crm/doctype/prospect/prospect.js +++ b/erpnext/crm/doctype/prospect/prospect.js @@ -3,8 +3,6 @@ frappe.ui.form.on('Prospect', { refresh (frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: frm.doctype }; - if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) { frm.add_custom_button(__("Customer"), function() { frappe.model.open_mapped_doc({ diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json deleted file mode 100644 index 0d42ca8c85d4..000000000000 --- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "actions": [], - "creation": "2019-05-21 07:41:53.536536", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "enabled", - "section_break_2", - "account_sid", - "api_key", - "api_token", - "section_break_6", - "map_custom_field_to_doctype", - "target_doctype" - ], - "fields": [ - { - "default": "0", - "fieldname": "enabled", - "fieldtype": "Check", - "label": "Enabled" - }, - { - "depends_on": "enabled", - "fieldname": "section_break_2", - "fieldtype": "Section Break", - "label": "Credentials" - }, - { - "fieldname": "account_sid", - "fieldtype": "Data", - "label": "Account SID" - }, - { - "fieldname": "api_token", - "fieldtype": "Data", - "label": "API Token" - }, - { - "fieldname": "api_key", - "fieldtype": "Data", - "label": "API Key" - }, - { - "depends_on": "enabled", - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "label": "Custom Field" - }, - { - "default": "0", - "fieldname": "map_custom_field_to_doctype", - "fieldtype": "Check", - "label": "Map Custom Field to DocType" - }, - { - "depends_on": "map_custom_field_to_doctype", - "fieldname": "target_doctype", - "fieldtype": "Link", - "label": "Target DocType", - "mandatory_depends_on": "map_custom_field_to_doctype", - "options": "DocType" - } - ], - "issingle": 1, - "links": [], - "modified": "2022-12-14 17:24:50.176107", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "Exotel Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "ASC", - "states": [], - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py deleted file mode 100644 index 4879cb56239d..000000000000 --- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -import requests -from frappe import _ -from frappe.model.document import Document - - -class ExotelSettings(Document): - def validate(self): - self.verify_credentials() - - def verify_credentials(self): - if self.enabled: - response = requests.get( - "https://api.exotel.com/v1/Accounts/{sid}".format(sid=self.account_sid), - auth=(self.api_key, self.api_token), - ) - if response.status_code != 200: - frappe.throw(_("Invalid credentials")) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py deleted file mode 100644 index 0d40667e32a6..000000000000 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ /dev/null @@ -1,151 +0,0 @@ -import frappe -import requests - -# api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call -# api/method/erpnext.erpnext_integrations.exotel_integration.handle_end_call -# api/method/erpnext.erpnext_integrations.exotel_integration.handle_missed_call - - -@frappe.whitelist(allow_guest=True) -def handle_incoming_call(**kwargs): - try: - exotel_settings = get_exotel_settings() - if not exotel_settings.enabled: - return - - call_payload = kwargs - status = call_payload.get("Status") - if status == "free": - return - - call_log = get_call_log(call_payload) - if not call_log: - create_call_log(call_payload) - else: - update_call_log(call_payload, call_log=call_log) - except Exception as e: - frappe.db.rollback() - exotel_settings.log_error("Error in Exotel incoming call") - frappe.db.commit() - - -@frappe.whitelist(allow_guest=True) -def handle_end_call(**kwargs): - update_call_log(kwargs, "Completed") - - -@frappe.whitelist(allow_guest=True) -def handle_missed_call(**kwargs): - status = "" - call_type = kwargs.get("CallType") - dial_call_status = kwargs.get("DialCallStatus") - - if call_type == "incomplete" and dial_call_status == "no-answer": - status = "No Answer" - elif call_type == "client-hangup" and dial_call_status == "canceled": - status = "Canceled" - elif call_type == "incomplete" and dial_call_status == "failed": - status = "Failed" - - update_call_log(kwargs, status) - - -def update_call_log(call_payload, status="Ringing", call_log=None): - call_log = call_log or get_call_log(call_payload) - - # for a new sid, call_log and get_call_log will be empty so create a new log - if not call_log: - call_log = create_call_log(call_payload) - if call_log: - call_log.status = status - call_log.to = call_payload.get("DialWhomNumber") - call_log.duration = call_payload.get("DialCallDuration") or 0 - call_log.recording_url = call_payload.get("RecordingUrl") - call_log.save(ignore_permissions=True) - frappe.db.commit() - return call_log - - -def get_call_log(call_payload): - call_log_id = call_payload.get("CallSid") - if frappe.db.exists("Call Log", call_log_id): - return frappe.get_doc("Call Log", call_log_id) - - -def map_custom_field(call_payload, call_log): - field_value = call_payload.get("CustomField") - - if not field_value: - return call_log - - settings = get_exotel_settings() - target_doctype = settings.target_doctype - mapping_enabled = settings.map_custom_field_to_doctype - - if not mapping_enabled or not target_doctype: - return call_log - - call_log.append("links", {"link_doctype": target_doctype, "link_name": field_value}) - - return call_log - - -def create_call_log(call_payload): - call_log = frappe.new_doc("Call Log") - call_log.id = call_payload.get("CallSid") - call_log.to = call_payload.get("DialWhomNumber") - call_log.medium = call_payload.get("To") - call_log.status = "Ringing" - setattr(call_log, "from", call_payload.get("CallFrom")) - map_custom_field(call_payload, call_log) - call_log.save(ignore_permissions=True) - frappe.db.commit() - return call_log - - -@frappe.whitelist() -def get_call_status(call_id): - endpoint = get_exotel_endpoint("Calls/{call_id}.json".format(call_id=call_id)) - response = requests.get(endpoint) - status = response.json().get("Call", {}).get("Status") - return status - - -@frappe.whitelist() -def make_a_call(from_number, to_number, caller_id, **kwargs): - endpoint = get_exotel_endpoint("Calls/connect.json?details=true") - response = requests.post( - endpoint, data={"From": from_number, "To": to_number, "CallerId": caller_id, **kwargs} - ) - - return response.json() - - -def get_exotel_settings(): - return frappe.get_single("Exotel Settings") - - -def whitelist_numbers(numbers, caller_id): - endpoint = get_exotel_endpoint("CustomerWhitelist") - response = requests.post( - endpoint, - data={ - "VirtualNumber": caller_id, - "Number": numbers, - }, - ) - - return response - - -def get_all_exophones(): - endpoint = get_exotel_endpoint("IncomingPhoneNumbers") - response = requests.post(endpoint) - return response - - -def get_exotel_endpoint(action): - settings = get_exotel_settings() - return "https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/{action}".format( - api_key=settings.api_key, api_token=settings.api_token, sid=settings.account_sid, action=action - ) diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json index ccc46b7a220e..5c4be6ffaa29 100644 --- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json +++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json @@ -230,17 +230,6 @@ "onboard": 0, "type": "Card Break" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Exotel Settings", - "link_count": 0, - "link_to": "Exotel Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -252,7 +241,7 @@ "type": "Link" } ], - "modified": "2023-05-24 14:47:25.984717", + "modified": "2023-05-24 14:47:26.984717", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "ERPNext Integrations", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index d02d318b2d89..d8b40e308c43 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -83,7 +83,7 @@ my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context" webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context" -calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "ToDo"] +calendars = ["Task", "Work Order", "Sales Order", "Holiday List", "ToDo"] website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner"] @@ -285,10 +285,34 @@ "Customer": "erpnext.controllers.queries.customer_query", } +period_closing_doctypes = [ + "Sales Invoice", + "Purchase Invoice", + "Journal Entry", + "Bank Clearance", + "Stock Entry", + "Dunning", + "Invoice Discounting", + "Payment Entry", + "Period Closing Voucher", + "Process Deferred Accounting", + "Asset", + "Asset Capitalization", + "Asset Repair", + "Delivery Note", + "Landed Cost Voucher", + "Purchase Receipt", + "Stock Reconciliation", + "Subcontracting Receipt", +] + doc_events = { "*": { "validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply", }, + tuple(period_closing_doctypes): { + "validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save", + }, "Stock Entry": { "on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", @@ -334,6 +358,7 @@ "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning", ], + "on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], "on_trash": "erpnext.regional.check_deletion_permission", }, "Address": { @@ -354,6 +379,11 @@ }, } +# function should expect the variable and doc as arguments +naming_series_variables = { + "FY": "erpnext.accounts.utils.parse_naming_series_variable", +} + # On cancel event Payment Entry will be exempted and all linked submittable doctype will get cancelled. # to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled. # if payment entry not in auto cancel exempted doctypes it will cancel payment entry. @@ -459,15 +489,6 @@ invoice_doctypes = ["Sales Invoice", "Purchase Invoice"] -period_closing_doctypes = [ - "Sales Invoice", - "Purchase Invoice", - "Journal Entry", - "Bank Clearance", - "Asset", - "Stock Entry", -] - bank_reconciliation_doctypes = [ "Payment Entry", "Journal Entry", @@ -611,3 +632,8 @@ additional_timeline_content = { "*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"] } + + +extend_bootinfo = [ + "erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes", +] diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 6dc1ff6a49f5..d8cc8f6d3953 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -621,7 +621,7 @@ def show_list_created_message(self, doctype, doc_list=None): def create_work_order(self, item): from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError - if item.get("qty") <= 0: + if flt(item.get("qty")) <= 0: return wo = frappe.new_doc("Work Order") @@ -697,10 +697,9 @@ def make_material_request(self): material_request.flags.ignore_permissions = 1 material_request.run_method("set_missing_values") + material_request.save() if self.get("submit_material_request"): material_request.submit() - else: - material_request.save() frappe.flags.mute_messages = False @@ -1540,7 +1539,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.required_bom_qty * IfNull(child.conversion_factor, 1.0))) + .select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0))) .where( (table.docstatus == 1) & (child.item_code == item_code) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index fcfba7fca563..f60dbfc3f55b 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -933,6 +933,54 @@ def test_resered_qty_for_production_plan_for_material_requests(self): self.assertEqual(after_qty, before_qty) + def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self): + from erpnext.stock.utils import get_or_make_bin + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + bom_item = make_item( + properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"} + ).name + + if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}): + doc = frappe.get_doc("Item", bom_item) + doc.append("uoms", {"uom": "Nos", "conversion_factor": 25}) + doc.save() + + make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC") + + bin_name = get_or_make_bin(bom_item, "_Test Warehouse - _TC") + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln = create_production_plan( + item_code=fg_item, planned_qty=100, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1" + ) + + for row in pln.mr_items: + self.assertEqual(row.uom, "Nos") + self.assertEqual(row.quantity, 4) + + reserved_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + self.assertEqual(reserved_qty - before_qty, 100.0) + + pln.submit_material_request = 1 + pln.make_work_order() + + for work_order in frappe.get_all( + "Work Order", + fields=["name"], + filters={"production_plan": pln.name}, + ): + wo_doc = frappe.get_doc("Work Order", work_order.name) + wo_doc.source_warehouse = "_Test Warehouse - _TC" + wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC" + wo_doc.fg_warehouse = "_Test Warehouse - _TC" + wo_doc.submit() + + reserved_qty_after_mr = flt( + frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan") + ) + self.assertEqual(reserved_qty_after_mr, before_qty) + def test_skip_available_qty_for_sub_assembly_items(self): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 79b1e798ede0..7c15bf9234bd 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1026,7 +1026,7 @@ def update_consumed_qty_for_required_items(self): consumed_qty = frappe.db.sql( """ SELECT - SUM(qty) + SUM(detail.qty) FROM `tabStock Entry` entry, `tabStock Entry Detail` detail diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js index 782ce8110a8f..a874f2248204 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -17,7 +17,7 @@ frappe.query_reports["Job Card Summary"] = { label: __("Fiscal Year"), fieldtype: "Link", options: "Fiscal Year", - default: frappe.defaults.get_user_default("fiscal_year"), + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), reqd: 1, on_change: function(query_report) { var fiscal_year = query_report.get_values().fiscal_year; diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b3b9bc60b79d..fb59a7de6290 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -317,7 +317,7 @@ erpnext.patches.v13_0.update_docs_link erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance -erpnext.patches.v14_0.update_closing_balances #17-05-2023 +erpnext.patches.v14_0.update_closing_balances #14-07-2023 execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger @@ -333,4 +333,7 @@ execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missin erpnext.patches.v14_0.cleanup_workspaces erpnext.patches.v15_0.remove_loan_management_module #2023-07-03 erpnext.patches.v14_0.set_report_in_process_SOA -erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users \ No newline at end of file +erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users +execute:frappe.defaults.clear_default("fiscal_year") +erpnext.patches.v15_0.remove_exotel_integration +erpnext.patches.v14_0.single_to_multi_dunning diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py new file mode 100644 index 000000000000..7a8e591798df --- /dev/null +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -0,0 +1,49 @@ +import frappe + +from erpnext.accounts.general_ledger import make_reverse_gl_entries + + +def execute(): + frappe.reload_doc("accounts", "doctype", "overdue_payment") + frappe.reload_doc("accounts", "doctype", "dunning") + + all_dunnings = frappe.get_all("Dunning", filters={"docstatus": ("!=", 2)}, pluck="name") + for dunning_name in all_dunnings: + dunning = frappe.get_doc("Dunning", dunning_name) + if not dunning.sales_invoice: + # nothing we can do + continue + + if dunning.overdue_payments: + # something's already here, doesn't need patching + continue + + payment_schedules = frappe.get_all( + "Payment Schedule", + filters={"parent": dunning.sales_invoice}, + fields=[ + "parent as sales_invoice", + "name as payment_schedule", + "payment_term", + "due_date", + "invoice_portion", + "payment_amount", + # at the time of creating this dunning, the full amount was outstanding + "payment_amount as outstanding", + "'0' as paid_amount", + "discounted_amount", + ], + ) + + dunning.extend("overdue_payments", payment_schedules) + dunning.validate() + + dunning.flags.ignore_validate_update_after_submit = True + dunning.save() + + if dunning.status != "Resolved": + # With the new logic, dunning amount gets recorded as additional income + # at time of payment. We don't want to record the dunning amount twice, + # so we reverse previous GL Entries that recorded the dunning amount at + # time of submission of the Dunning. + make_reverse_gl_entries(voucher_type="Dunning", voucher_no=dunning.name) diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py index d66467775c81..2c842814839c 100644 --- a/erpnext/patches/v14_0/update_closing_balances.py +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -13,56 +13,62 @@ def execute(): frappe.db.truncate("Account Closing Balance") - i = 0 - company_wise_order = {} - for pcv in frappe.db.get_all( - "Period Closing Voucher", - fields=["company", "posting_date", "name"], - filters={"docstatus": 1}, - order_by="posting_date", - ): + for company in frappe.get_all("Company", pluck="name"): + i = 0 + company_wise_order = {} + for pcv in frappe.db.get_all( + "Period Closing Voucher", + fields=["company", "posting_date", "name"], + filters={"docstatus": 1, "company": company}, + order_by="posting_date", + ): - company_wise_order.setdefault(pcv.company, []) - if pcv.posting_date not in company_wise_order[pcv.company]: - pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name) - pcv_doc.year_start_date = get_fiscal_year( - pcv.posting_date, pcv.fiscal_year, company=pcv.company - )[1] + company_wise_order.setdefault(pcv.company, []) + if pcv.posting_date not in company_wise_order[pcv.company]: + pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name) + pcv_doc.year_start_date = get_fiscal_year( + pcv.posting_date, pcv.fiscal_year, company=pcv.company + )[1] - # get gl entries against pcv - gl_entries = frappe.db.get_all( - "GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"] - ) - for entry in gl_entries: - entry["is_period_closing_voucher_entry"] = 1 - entry["closing_date"] = pcv_doc.posting_date - entry["period_closing_voucher"] = pcv_doc.name + # get gl entries against pcv + gl_entries = frappe.db.get_all( + "GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"] + ) + for entry in gl_entries: + entry["is_period_closing_voucher_entry"] = 1 + entry["closing_date"] = pcv_doc.posting_date + entry["period_closing_voucher"] = pcv_doc.name - # get all gl entries for the year - closing_entries = frappe.db.get_all( - "GL Entry", - filters={ - "is_cancelled": 0, - "voucher_no": ["!=", pcv.name], - "posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]], - "is_opening": "No", - }, - fields=["*"], - ) + closing_entries = [] - if i == 0: - # add opening entries only for the first pcv - closing_entries += frappe.db.get_all( - "GL Entry", - filters={"is_cancelled": 0, "is_opening": "Yes"}, - fields=["*"], - ) + if pcv.posting_date not in company_wise_order[pcv.company]: + # get all gl entries for the year + closing_entries = frappe.db.get_all( + "GL Entry", + filters={ + "is_cancelled": 0, + "voucher_no": ["!=", pcv.name], + "posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]], + "is_opening": "No", + "company": company, + }, + fields=["*"], + ) + + if i == 0: + # add opening entries only for the first pcv + closing_entries += frappe.db.get_all( + "GL Entry", + filters={"is_cancelled": 0, "is_opening": "Yes", "company": company}, + fields=["*"], + ) - for entry in closing_entries: - entry["closing_date"] = pcv_doc.posting_date - entry["period_closing_voucher"] = pcv_doc.name + for entry in closing_entries: + entry["closing_date"] = pcv_doc.posting_date + entry["period_closing_voucher"] = pcv_doc.name - make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name) - company_wise_order[pcv.company].append(pcv.posting_date) + entries = gl_entries + closing_entries - i += 1 + make_closing_entries(entries, pcv.name, pcv.company, pcv.posting_date) + company_wise_order[pcv.company].append(pcv.posting_date) + i += 1 diff --git a/erpnext/patches/v15_0/remove_exotel_integration.py b/erpnext/patches/v15_0/remove_exotel_integration.py new file mode 100644 index 000000000000..9b99fc6b073a --- /dev/null +++ b/erpnext/patches/v15_0/remove_exotel_integration.py @@ -0,0 +1,37 @@ +import click +import frappe +from frappe import _ +from frappe.desk.doctype.notification_log.notification_log import make_notification_logs +from frappe.utils.user import get_system_managers + +SETTINGS_DOCTYPE = "Exotel Settings" + + +def execute(): + if "exotel_integration" in frappe.get_installed_apps(): + return + + try: + exotel = frappe.get_doc(SETTINGS_DOCTYPE) + if exotel.enabled: + notify_existing_users() + + frappe.delete_doc("DocType", SETTINGS_DOCTYPE) + except Exception: + frappe.log_error("Failed to remove Exotel Integration.") + + +def notify_existing_users(): + click.secho( + "Exotel integration is moved to a separate app and will be removed from ERPNext in version-15.\n" + "Please install the app to continue using the integration: https://github.com/frappe/exotel_integration", + fg="yellow", + ) + + notification = { + "subject": _( + "WARNING: Exotel app has been separated from ERPNext, please install the app to continue using Exotel integration." + ), + "type": "Alert", + } + make_notification_logs(notification, get_system_managers(only_name=True)) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 543d0e979081..6410333f0cbf 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -358,12 +358,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } refresh() { + erpnext.toggle_naming_series(); erpnext.hide_company(); this.set_dynamic_labels(); this.setup_sms(); this.setup_quality_inspection(); this.validate_has_items(); + erpnext.utils.view_serial_batch_nos(this.frm); } scan_barcode() { diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index b0082bdb2817..959cf507d53d 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -56,7 +56,7 @@ erpnext.financial_statements = { // dropdown for links to other financial statements erpnext.financial_statements.filters = get_filters() - let fiscal_year = frappe.defaults.get_user_default("fiscal_year") + let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); @@ -137,7 +137,7 @@ function get_filters() { "label": __("Start Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'" }, @@ -146,7 +146,7 @@ function get_filters() { "label": __("End Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'" }, @@ -182,6 +182,16 @@ function get_filters() { company: frappe.query_report.get_filter_value("company") }); } + }, + { + "fieldname": "project", + "label": __("Project"), + "fieldtype": "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Project', txt, { + company: frappe.query_report.get_filter_value("company") + }); + }, } ] diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index a859a671b012..cc03eca95d82 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -113,6 +113,23 @@ $.extend(erpnext.utils, { } }, + view_serial_batch_nos: function(frm) { + let bundle_ids = frm.doc.items.filter(d => d.serial_and_batch_bundle); + + if (bundle_ids?.length) { + frm.add_custom_button(__('Serial / Batch Nos'), () => { + frappe.route_options = { + "voucher_no": frm.doc.name, + "voucher_type": frm.doc.doctype, + "from_date": frm.doc.posting_date || frm.doc.transaction_date, + "to_date": frm.doc.posting_date || frm.doc.transaction_date, + "company": frm.doc.company, + }; + frappe.set_route("query-report", "Serial and Batch Summary"); + }, __('View')); + } + }, + add_indicator_for_multicompany: function(frm, info) { frm.dashboard.stats_area.show(); frm.dashboard.stats_area_row.addClass('flex'); @@ -381,6 +398,27 @@ $.extend(erpnext.utils, { }); }); }); + }, + + get_fiscal_year: function(date) { + if(!date) { + date = frappe.datetime.get_today(); + } + + let fiscal_year = ''; + frappe.call({ + method: "erpnext.accounts.utils.get_fiscal_year", + args: { + date: date + }, + async: false, + callback: function(r) { + if (r.message) { + fiscal_year = r.message[0]; + } + } + }); + return fiscal_year; } }); @@ -632,7 +670,6 @@ erpnext.utils.update_child_items = function(opts) { fields.splice(3, 0, { fieldtype: 'Float', fieldname: "conversion_factor", - in_list_view: 1, label: __("Conversion Factor"), precision: get_precision('conversion_factor') }) @@ -640,6 +677,7 @@ erpnext.utils.update_child_items = function(opts) { new frappe.ui.Dialog({ title: __("Update Items"), + size: "extra-large", fields: [ { fieldname: "trans_items", @@ -854,95 +892,87 @@ $(document).on('app_ready', function() { // Show SLA dashboard $(document).on('app_ready', function() { - frappe.call({ - method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_sla_doctypes', - callback: function(r) { - if (!r.message) - return; - - $.each(r.message, function(_i, d) { - frappe.ui.form.on(d, { - onload: function(frm) { - if (!frm.doc.service_level_agreement) - return; - - frappe.call({ - method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters', - args: { - doctype: frm.doc.doctype, - name: frm.doc.service_level_agreement, - customer: frm.doc.customer - }, - callback: function (r) { - if (r && r.message) { - frm.set_query('priority', function() { - return { - filters: { - 'name': ['in', r.message.priority], - } - }; - }); - frm.set_query('service_level_agreement', function() { - return { - filters: { - 'name': ['in', r.message.service_level_agreements], - } - }; - }); - } - } - }); - }, + $.each(frappe.boot.service_level_agreement_doctypes, function(_i, d) { + frappe.ui.form.on(d, { + onload: function(frm) { + if (!frm.doc.service_level_agreement) + return; - refresh: function(frm) { - if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement - && ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) { - frappe.call({ - 'method': 'frappe.client.get', - args: { - doctype: 'Service Level Agreement', - name: frm.doc.service_level_agreement - }, - callback: function(data) { - let statuses = data.message.pause_sla_on; - const hold_statuses = []; - $.each(statuses, (_i, entry) => { - hold_statuses.push(entry.status); - }); - if (hold_statuses.includes(frm.doc.status)) { - frm.dashboard.clear_headline(); - let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])}; - frm.dashboard.set_headline_alert( - '
' + - '
' + - ''+ message.msg +' ' + - '
' + - '
' - ); - } else { - set_time_to_resolve_and_response(frm, data.message.apply_sla_for_resolution); + frappe.call({ + method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters', + args: { + doctype: frm.doc.doctype, + name: frm.doc.service_level_agreement, + customer: frm.doc.customer + }, + callback: function (r) { + if (r && r.message) { + frm.set_query('priority', function() { + return { + filters: { + 'name': ['in', r.message.priority], } - } + }; + }); + frm.set_query('service_level_agreement', function() { + return { + filters: { + 'name': ['in', r.message.service_level_agreements], + } + }; }); - } else if (frm.doc.service_level_agreement) { - frm.dashboard.clear_headline(); - - let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ? - {'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} : - {'indicator': 'red', 'msg': 'Service Level Agreement Failed'}; - - frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' - ); } - }, + } }); - }); - } + }, + + refresh: function(frm) { + if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement + && ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) { + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Service Level Agreement', + name: frm.doc.service_level_agreement + }, + callback: function(data) { + let statuses = data.message.pause_sla_on; + const hold_statuses = []; + $.each(statuses, (_i, entry) => { + hold_statuses.push(entry.status); + }); + if (hold_statuses.includes(frm.doc.status)) { + frm.dashboard.clear_headline(); + let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])}; + frm.dashboard.set_headline_alert( + '
' + + '
' + + ''+ message.msg +' ' + + '
' + + '
' + ); + } else { + set_time_to_resolve_and_response(frm, data.message.apply_sla_for_resolution); + } + } + }); + } else if (frm.doc.service_level_agreement) { + frm.dashboard.clear_headline(); + + let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ? + {'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} : + {'indicator': 'red', 'msg': 'Service Level Agreement Failed'}; + + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, + }); }); }); @@ -1011,4 +1041,4 @@ function attach_selector_button(inner_text, append_loction, context, grid_row) { $btn.on("click", function() { context.show_serial_batch_selector(grid_row.frm, grid_row.doc, "", "", true); }); -} +} \ No newline at end of file diff --git a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].js b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].js index a4c7640c813b..b85b58f636ab 100644 --- a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].js +++ b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].js @@ -16,7 +16,7 @@ frappe.query_reports["Fichier des Ecritures Comptables [FEC]"] = { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1 } ], diff --git a/erpnext/regional/report/irs_1099/irs_1099.js b/erpnext/regional/report/irs_1099/irs_1099.js index 070ff43f78cd..b3508e40a9f5 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.js +++ b/erpnext/regional/report/irs_1099/irs_1099.js @@ -17,7 +17,7 @@ frappe.query_reports["IRS 1099"] = { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "width": 80, }, diff --git a/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json b/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json index 914d915d9447..2f668a865d7d 100644 --- a/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json +++ b/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json @@ -5,18 +5,19 @@ "custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"period\":\"Monthly\",\"based_on\":\"Item\"}", - "idx": 0, + "idx": 1, "is_public": 1, "is_standard": 1, - "modified": "2020-07-22 16:24:45.726270", + "modified": "2023-07-19 13:09:45.341791", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Trends", "number_of_groups": 0, "owner": "Administrator", "report_name": "Sales Order Trends", + "roles": [], "timeseries": 0, "type": "Line", "use_report_chart": 1, diff --git a/erpnext/selling/dashboard_chart/top_customers/top_customers.json b/erpnext/selling/dashboard_chart/top_customers/top_customers.json index 59a2ba37ddf6..2972980967c0 100644 --- a/erpnext/selling/dashboard_chart/top_customers/top_customers.json +++ b/erpnext/selling/dashboard_chart/top_customers/top_customers.json @@ -5,18 +5,19 @@ "custom_options": "", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"period\":\"Yearly\",\"based_on\":\"Customer\"}", "idx": 0, "is_public": 1, "is_standard": 1, - "modified": "2020-07-22 17:03:10.320147", + "modified": "2023-07-19 13:14:20.151502", "modified_by": "Administrator", "module": "Selling", "name": "Top Customers", "number_of_groups": 0, "owner": "Administrator", "report_name": "Delivery Note Trends", + "roles": [], "timeseries": 0, "type": "Bar", "use_report_chart": 1, diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 540e767d323b..60f0941559a0 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -131,8 +131,6 @@ frappe.ui.form.on("Customer", { erpnext.toggle_naming_series(); } - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Customer'} - if(!frm.doc.__islocal) { frappe.contacts.render_address_and_contact(frm); diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 45100d7a64c1..796e2588ff2c 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1904,12 +1904,11 @@ def test_stock_reservation_against_sales_order(self) -> None: "voucher_no": so.name, "voucher_detail_no": item.name, }, - fields=["status", "reserved_qty", "delivered_qty"], + fields=["reserved_qty", "delivered_qty"], ) for sre_detail in sre_details: self.assertEqual(sre_detail.reserved_qty, sre_detail.delivered_qty) - self.assertEqual(sre_detail.status, "Delivered") def test_delivered_item_material_request(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.js b/erpnext/selling/report/address_and_contacts/address_and_contacts.js index ef87586f66e8..8aa14d1998e7 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.js +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.js @@ -13,7 +13,7 @@ frappe.query_reports["Address And Contacts"] = { "get_query": function() { return { "filters": { - "name": ["in","Customer,Supplier,Sales Partner"], + "name": ["in","Customer,Supplier,Sales Partner,Lead"], } } } diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index 9a1cfda8474e..4542bdff4397 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -130,6 +130,7 @@ def get_party_group(party_type): "Customer": "customer_group", "Supplier": "supplier_group", "Sales Partner": "partner_type", + "Lead": "status", } return group[party_type] diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 333538722eef..f4682c1b8066 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -81,8 +81,6 @@ frappe.ui.form.on("Company", { disbale_coa_fields(frm); frappe.contacts.render_address_and_contact(frm); - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'} - if (frappe.perm.has_perm("Cost Center", 0, 'read')) { frm.add_custom_button(__('Cost Centers'), function() { frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}); diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.json b/erpnext/setup/doctype/global_defaults/global_defaults.json index bafb97a5d801..823d2ba7d7bc 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.json +++ b/erpnext/setup/doctype/global_defaults/global_defaults.json @@ -1,352 +1,99 @@ { - "allow_copy": 1, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-05-02 17:53:24", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, + "actions": [], + "allow_copy": 1, + "creation": "2013-05-02 17:53:24", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "default_company", + "country", + "default_distance_unit", + "column_break_8", + "default_currency", + "hide_currency_symbol", + "disable_rounded_total", + "disable_in_words" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "default_company", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Company", + "options": "Company" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "current_fiscal_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Current Fiscal Year", - "length": 0, - "no_copy": 0, - "options": "Fiscal Year", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "country", + "fieldtype": "Link", + "label": "Country", + "options": "Country" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "country", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Country", - "length": 0, - "no_copy": 0, - "options": "Country", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "default_distance_unit", + "fieldtype": "Link", + "label": "Default Distance Unit", + "options": "UOM" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "default_distance_unit", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Distance Unit", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "INR", + "fieldname": "default_currency", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "Default Currency", + "options": "Currency", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "INR", - "fieldname": "default_currency", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default Currency", - "length": 0, - "no_copy": 0, - "options": "Currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "description": "Do not show any symbol like $ etc next to currencies.", + "fieldname": "hide_currency_symbol", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Hide Currency Symbol", + "options": "\nNo\nYes" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Do not show any symbol like $ etc next to currencies.", - "fieldname": "hide_currency_symbol", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Hide Currency Symbol", - "length": 0, - "no_copy": 0, - "options": "\nNo\nYes", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "description": "If disable, 'Rounded Total' field will not be visible in any transaction", + "fieldname": "disable_rounded_total", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Disable Rounded Total" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "If disable, 'Rounded Total' field will not be visible in any transaction", - "fieldname": "disable_rounded_total", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Disable Rounded Total", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "If disable, 'In Words' field will not be visible in any transaction", - "fieldname": "disable_in_words", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Disable In Words", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "default": "0", + "description": "If disable, 'In Words' field will not be visible in any transaction", + "fieldname": "disable_in_words", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Disable In Words" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-cog", - "idx": 1, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-10-15 03:08:19.886212", - "modified_by": "Administrator", - "module": "Setup", - "name": "Global Defaults", - "owner": "Administrator", + ], + "icon": "fa fa-cog", + "idx": 1, + "in_create": 1, + "issingle": 1, + "links": [], + "modified": "2023-07-01 19:45:00.323953", + "modified_by": "Administrator", + "module": "Setup", + "name": "Global Defaults", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "read": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 1, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index 16e94343a37d..fc80483e8eef 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -10,7 +10,6 @@ keydict = { # "key in defaults": "key in Global Defaults" - "fiscal_year": "current_fiscal_year", "company": "default_company", "currency": "default_currency", "country": "country", @@ -29,22 +28,6 @@ def on_update(self): for key in keydict: frappe.db.set_default(key, self.get(keydict[key], "")) - # update year start date and year end date from fiscal_year - if self.current_fiscal_year: - if fiscal_year := frappe.get_all( - "Fiscal Year", - filters={"name": self.current_fiscal_year}, - fields=["year_start_date", "year_end_date"], - limit=1, - order_by=None, - ): - ysd = fiscal_year[0].year_start_date or "" - yed = fiscal_year[0].year_end_date or "" - - if ysd and yed: - frappe.db.set_default("year_start_date", ysd.strftime("%Y-%m-%d")) - frappe.db.set_default("year_end_date", yed.strftime("%Y-%m-%d")) - # enable default currency if self.default_currency: frappe.db.set_value("Currency", self.default_currency, "enabled", 1) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.js b/erpnext/setup/doctype/holiday_list/holiday_list.js index ea033c7ed92f..90d9f1b6f509 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.js +++ b/erpnext/setup/doctype/holiday_list/holiday_list.js @@ -6,13 +6,41 @@ frappe.ui.form.on("Holiday List", { if (frm.doc.holidays) { frm.set_value("total_holidays", frm.doc.holidays.length); } + + frm.call("get_supported_countries").then(r => { + frm.subdivisions_by_country = r.message.subdivisions_by_country; + frm.fields_dict.country.set_data( + r.message.countries.sort((a, b) => a.label.localeCompare(b.label)) + ); + + if (frm.doc.country) { + frm.trigger("set_subdivisions"); + } + }); }, from_date: function(frm) { if (frm.doc.from_date && !frm.doc.to_date) { var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12); frm.set_value("to_date", frappe.datetime.add_days(a_year_from_start, -1)); } - } + }, + country: function(frm) { + frm.set_value("subdivision", ""); + + if (frm.doc.country) { + frm.trigger("set_subdivisions"); + } + }, + set_subdivisions: function(frm) { + const subdivisions = [...frm.subdivisions_by_country[frm.doc.country]]; + if (subdivisions && subdivisions.length > 0) { + frm.fields_dict.subdivision.set_data(subdivisions); + frm.set_df_property("subdivision", "hidden", 0); + } else { + frm.fields_dict.subdivision.set_data([]); + frm.set_df_property("subdivision", "hidden", 1); + } + }, }); frappe.tour["Holiday List"] = [ diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.json b/erpnext/setup/doctype/holiday_list/holiday_list.json index 4bbe6a6cb211..45671d181b31 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.json +++ b/erpnext/setup/doctype/holiday_list/holiday_list.json @@ -1,480 +1,166 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "field:holiday_list_name", - "beta": 0, "creation": "2013-01-10 16:34:14", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "holiday_list_name", + "from_date", + "to_date", + "column_break_4", + "total_holidays", + "add_weekly_holidays", + "weekly_off", + "get_weekly_off_dates", + "add_local_holidays", + "country", + "subdivision", + "get_local_holidays", + "holidays_section", + "holidays", + "clear_table", + "section_break_9", + "color" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "holiday_list_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Holiday List Name", - "length": 0, - "no_copy": 0, "oldfieldname": "holiday_list_name", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "from_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "From Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "to_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "To Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "total_holidays", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Total Holidays", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, + "depends_on": "eval: doc.from_date && doc.to_date", "fieldname": "add_weekly_holidays", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Add Weekly Holidays", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Add Weekly Holidays" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "weekly_off", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Weekly Off", - "length": 0, "no_copy": 1, "options": "\nSunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday", - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "report_hide": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "get_weekly_off_dates", "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Add to Holidays", - "length": 0, - "no_copy": 0, - "options": "get_weekly_off_dates", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "get_weekly_off_dates" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "holidays_section", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Holidays", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Holidays" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "holidays", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Holidays", - "length": 0, - "no_copy": 0, "oldfieldname": "holiday_list_details", "oldfieldtype": "Table", - "options": "Holiday", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Holiday" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "clear_table", "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Clear Table", - "length": 0, - "no_copy": 0, - "options": "clear_table", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "clear_table" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "color", "fieldtype": "Color", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Color", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "print_hide": 1 + }, + { + "fieldname": "country", + "fieldtype": "Autocomplete", + "label": "Country" + }, + { + "depends_on": "country", + "fieldname": "subdivision", + "fieldtype": "Autocomplete", + "label": "Subdivision" + }, + { + "collapsible": 1, + "depends_on": "eval: doc.from_date && doc.to_date", + "fieldname": "add_local_holidays", + "fieldtype": "Section Break", + "label": "Add Local Holidays" + }, + { + "fieldname": "get_local_holidays", + "fieldtype": "Button", + "label": "Add to Holidays", + "options": "get_local_holidays" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-calendar", "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-07-03 07:22:46.474096", + "links": [], + "modified": "2023-07-14 13:28:53.156421", "modified_by": "Administrator", "module": "Setup", "name": "Holiday List", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 84d0d3528715..2ef4e655b2d7 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -3,11 +3,15 @@ import json +from datetime import date import frappe +from babel import Locale from frappe import _, throw from frappe.model.document import Document -from frappe.utils import cint, formatdate, getdate, today +from frappe.utils import formatdate, getdate, today +from holidays import country_holidays +from holidays.utils import list_supported_countries class OverlapError(frappe.ValidationError): @@ -21,25 +25,66 @@ def validate(self): @frappe.whitelist() def get_weekly_off_dates(self): - self.validate_values() - date_list = self.get_weekly_off_date_list(self.from_date, self.to_date) - last_idx = max( - [cint(d.idx) for d in self.get("holidays")] - or [ - 0, - ] - ) - for i, d in enumerate(date_list): - ch = self.append("holidays", {}) - ch.description = _(self.weekly_off) - ch.holiday_date = d - ch.weekly_off = 1 - ch.idx = last_idx + i + 1 - - def validate_values(self): if not self.weekly_off: throw(_("Please select weekly off day")) + existing_holidays = self.get_holidays() + + for d in self.get_weekly_off_date_list(self.from_date, self.to_date): + if d in existing_holidays: + continue + + self.append("holidays", {"description": _(self.weekly_off), "holiday_date": d, "weekly_off": 1}) + + self.sort_holidays() + + @frappe.whitelist() + def get_supported_countries(self): + subdivisions_by_country = list_supported_countries() + countries = [ + {"value": country, "label": local_country_name(country)} + for country in subdivisions_by_country.keys() + ] + return { + "countries": countries, + "subdivisions_by_country": subdivisions_by_country, + } + + @frappe.whitelist() + def get_local_holidays(self): + if not self.country: + throw(_("Please select a country")) + + existing_holidays = self.get_holidays() + from_date = getdate(self.from_date) + to_date = getdate(self.to_date) + + for holiday_date, holiday_name in country_holidays( + self.country, + subdiv=self.subdivision, + years=[from_date.year, to_date.year], + language=frappe.local.lang, + ).items(): + if holiday_date in existing_holidays: + continue + + if holiday_date < from_date or holiday_date > to_date: + continue + + self.append( + "holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0} + ) + + self.sort_holidays() + + def sort_holidays(self): + self.holidays.sort(key=lambda x: getdate(x.holiday_date)) + for i in range(len(self.holidays)): + self.holidays[i].idx = i + 1 + + def get_holidays(self) -> list[date]: + return [getdate(holiday.holiday_date) for holiday in self.holidays] + def validate_days(self): if getdate(self.from_date) > getdate(self.to_date): throw(_("To Date cannot be before From Date")) @@ -120,3 +165,8 @@ def is_holiday(holiday_list, date=None): ) else: return False + + +def local_country_name(country_code: str) -> str: + """Return the localized country name for the given country code.""" + return Locale.parse(frappe.local.lang).territories.get(country_code, country_code) diff --git a/erpnext/setup/doctype/holiday_list/test_holiday_list.py b/erpnext/setup/doctype/holiday_list/test_holiday_list.py index d32cfe826504..23b08fd11709 100644 --- a/erpnext/setup/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/test_holiday_list.py @@ -3,7 +3,7 @@ import unittest from contextlib import contextmanager -from datetime import timedelta +from datetime import date, timedelta import frappe from frappe.utils import getdate @@ -23,6 +23,41 @@ def test_holiday_list(self): fetched_holiday_list = frappe.get_value("Holiday List", holiday_list.name) self.assertEqual(holiday_list.name, fetched_holiday_list) + def test_weekly_off(self): + holiday_list = frappe.new_doc("Holiday List") + holiday_list.from_date = "2023-01-01" + holiday_list.to_date = "2023-02-28" + holiday_list.weekly_off = "Sunday" + holiday_list.get_weekly_off_dates() + + holidays = [holiday.holiday_date for holiday in holiday_list.holidays] + + self.assertNotIn(date(2022, 12, 25), holidays) + self.assertIn(date(2023, 1, 1), holidays) + self.assertIn(date(2023, 1, 8), holidays) + self.assertIn(date(2023, 1, 15), holidays) + self.assertIn(date(2023, 1, 22), holidays) + self.assertIn(date(2023, 1, 29), holidays) + self.assertIn(date(2023, 2, 5), holidays) + self.assertIn(date(2023, 2, 12), holidays) + self.assertIn(date(2023, 2, 19), holidays) + self.assertIn(date(2023, 2, 26), holidays) + self.assertNotIn(date(2023, 3, 5), holidays) + + def test_local_holidays(self): + holiday_list = frappe.new_doc("Holiday List") + holiday_list.from_date = "2023-04-01" + holiday_list.to_date = "2023-04-30" + holiday_list.country = "DE" + holiday_list.subdivision = "SN" + holiday_list.get_local_holidays() + + holidays = [holiday.holiday_date for holiday in holiday_list.holidays] + self.assertNotIn(date(2023, 1, 1), holidays) + self.assertIn(date(2023, 4, 7), holidays) + self.assertIn(date(2023, 4, 10), holidays) + self.assertNotIn(date(2023, 5, 1), holidays) + def make_holiday_list( name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None diff --git a/erpnext/setup/doctype/sales_partner/sales_partner.js b/erpnext/setup/doctype/sales_partner/sales_partner.js index 5656d43e852f..f9e37705604b 100644 --- a/erpnext/setup/doctype/sales_partner/sales_partner.js +++ b/erpnext/setup/doctype/sales_partner/sales_partner.js @@ -3,8 +3,6 @@ frappe.ui.form.on('Sales Partner', { refresh: function(frm) { - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Sales Partner'} - if(frm.doc.__islocal){ hide_field(['address_html', 'contact_html', 'address_contacts']); frappe.contacts.clear_address_and_contact(frm); diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 8e61fe28728c..535c87d652c4 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -462,11 +462,9 @@ def install_defaults(args=None): # nosemgrep def set_global_defaults(args): global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") - current_fiscal_year = frappe.get_all("Fiscal Year")[0] global_defaults.update( { - "current_fiscal_year": current_fiscal_year.name, "default_currency": args.get("currency"), "default_company": args.get("company_name"), "country": args.get("country"), diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 8baae8a19c68..0ef3027bce3e 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -318,6 +318,37 @@ def test_sales_return_for_non_bundled_items_full(self): self.assertEqual(dn.per_returned, 100) self.assertEqual(dn.status, "Return Issued") + def test_delivery_note_return_valuation_on_different_warehuose(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") + item_code = "Test Return Valuation For DN" + make_item("Test Return Valuation For DN", {"is_stock_item": 1}) + return_warehouse = create_warehouse("Returned Test Warehouse", company=company) + + make_stock_entry(item_code=item_code, target="Stores - TCP1", qty=5, basic_rate=150) + + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=500, + warehouse="Stores - TCP1", + company=company, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) + + dn.submit() + self.assertEqual(dn.items[0].incoming_rate, 150) + + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return_dn = make_return_doc(dn.doctype, dn.name) + return_dn.items[0].warehouse = return_warehouse + return_dn.save().submit() + + self.assertEqual(return_dn.items[0].incoming_rate, 150) + def test_return_single_item_from_bundled_items(self): company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 34adbebc07cc..87c2a7ea691b 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -194,7 +194,8 @@ "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "label": "Disabled" + "label": "Disabled", + "search_index": 1 }, { "default": "0", @@ -911,7 +912,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2023-02-14 04:48:26.343620", + "modified": "2023-07-14 17:18:18.658942", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 93d799a3951e..ef4155e48ab2 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -773,7 +773,7 @@ def table_row(title, body): rows = "" for docname, attr_list in not_included.items(): - link = "{0}".format(frappe.bold(_(docname))) + link = f"{frappe.bold(docname)}" rows += table_row(link, body(attr_list)) error_description = _( diff --git a/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json b/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json index 6d02ea9db0c9..9699ecbb3db2 100644 --- a/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json +++ b/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json @@ -1,370 +1,90 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2015-05-19 05:12:30.344797", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Other", - "editable_grid": 1, + "actions": [], + "creation": "2015-05-19 05:12:30.344797", + "doctype": "DocType", + "document_type": "Other", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "variant_of", + "attribute", + "column_break_2", + "attribute_value", + "numeric_values", + "section_break_4", + "from_range", + "increment", + "column_break_8", + "to_range" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "variant_of", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Variant Of", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "variant_of", + "fieldtype": "Link", + "label": "Variant Of", + "options": "Item", + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "attribute", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Attribute", - "length": 0, - "no_copy": 0, - "options": "Item Attribute", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "attribute", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Attribute", + "options": "Item Attribute", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "attribute_value", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Attribute Value", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "attribute_value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Attribute Value" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "has_variants", - "fieldname": "numeric_values", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Numeric Values", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "depends_on": "has_variants", + "fieldname": "numeric_values", + "fieldtype": "Check", + "label": "Numeric Values" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "numeric_values", - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "numeric_values", + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "from_range", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "From Range", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_range", + "fieldtype": "Float", + "label": "From Range" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "increment", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Increment", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "increment", + "fieldtype": "Float", + "label": "Increment" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "to_range", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "To Range", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "to_range", + "fieldtype": "Float", + "label": "To Range" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-01-03 15:36:59.129006", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Variant Attribute", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-07-14 17:15:19.112119", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Variant Attribute", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/stock/doctype/manufacturer/manufacturer.js b/erpnext/stock/doctype/manufacturer/manufacturer.js index bb7e314e14ed..5b4990f08be3 100644 --- a/erpnext/stock/doctype/manufacturer/manufacturer.js +++ b/erpnext/stock/doctype/manufacturer/manufacturer.js @@ -3,7 +3,6 @@ frappe.ui.form.on('Manufacturer', { refresh: function(frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Manufacturer' }; if (frm.doc.__islocal) { hide_field(['address_html','contact_html']); frappe.contacts.clear_address_and_contact(frm); diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index ee247fd093bd..00b1b20f3f83 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -118,8 +118,8 @@ def set_title(self): self.title = _("{0} Request for {1}").format(_(self.material_request_type), items)[:100] def on_submit(self): - self.update_requested_qty() self.update_requested_qty_in_production_plan() + self.update_requested_qty() if self.material_request_type == "Purchase": self.validate_budget() @@ -178,8 +178,8 @@ def status_can_change(self, status): ) def on_cancel(self): - self.update_requested_qty() self.update_requested_qty_in_production_plan() + self.update_requested_qty() def get_mr_items_ordered_qty(self, mr_items): mr_items_ordered_qty = {} @@ -270,7 +270,13 @@ def update_requested_qty(self, mr_item_rows=None): item_wh_list.append([d.item_code, d.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, {"indented_qty": get_indented_qty(item_code, warehouse)}) + update_bin_qty( + item_code, + warehouse, + { + "indented_qty": get_indented_qty(item_code, warehouse), + }, + ) def update_requested_qty_in_production_plan(self): production_plans = [] diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ced894634f60..6134bfa1f235 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1965,6 +1965,32 @@ def test_purchase_receipt_with_backdated_landed_cost_voucher(self): ste5.reload() self.assertEqual(ste5.items[0].valuation_rate, 275.00) + ste6 = make_stock_entry( + purpose="Material Transfer", + posting_date=add_days(today(), -3), + source=warehouse1, + target=warehouse, + item_code=item_code, + qty=20, + company=pr.company, + ) + + ste6.reload() + self.assertEqual(ste6.items[0].valuation_rate, 275.00) + + ste7 = make_stock_entry( + purpose="Material Transfer", + posting_date=add_days(today(), -3), + source=warehouse, + target=warehouse1, + item_code=item_code, + qty=20, + company=pr.company, + ) + + ste7.reload() + self.assertEqual(ste7.items[0].valuation_rate, 275.00) + create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=2500 * -1) pr.reload() @@ -1985,6 +2011,12 @@ def test_purchase_receipt_with_backdated_landed_cost_voucher(self): ste5.reload() self.assertEqual(ste5.items[0].valuation_rate, valuation_rate) + ste6.reload() + self.assertEqual(ste6.items[0].valuation_rate, valuation_rate) + + ste7.reload() + self.assertEqual(ste7.items[0].valuation_rate, valuation_rate) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 6955c761e185..c5b96ff0fe71 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -193,7 +193,7 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Naming Series", - "options": "SBB-.####" + "options": "SABB-.########" }, { "default": "0", @@ -244,7 +244,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-04-10 20:02:42.964309", + "modified": "2023-07-16 10:53:04.045605", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 0c6d33bae21d..43bd7ac78cb5 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -889,13 +889,16 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals @frappe.whitelist() -def get_serial_batch_ledgers(item_code, docstatus=None, voucher_no=None, name=None): - filters = get_filters_for_bundle(item_code, docstatus=docstatus, voucher_no=voucher_no, name=name) +def get_serial_batch_ledgers(item_code=None, docstatus=None, voucher_no=None, name=None): + filters = get_filters_for_bundle( + item_code=item_code, docstatus=docstatus, voucher_no=voucher_no, name=name + ) return frappe.get_all( "Serial and Batch Bundle", fields=[ "`tabSerial and Batch Bundle`.`name`", + "`tabSerial and Batch Bundle`.`item_code`", "`tabSerial and Batch Entry`.`qty`", "`tabSerial and Batch Entry`.`warehouse`", "`tabSerial and Batch Entry`.`batch_no`", @@ -906,12 +909,14 @@ def get_serial_batch_ledgers(item_code, docstatus=None, voucher_no=None, name=No ) -def get_filters_for_bundle(item_code, docstatus=None, voucher_no=None, name=None): +def get_filters_for_bundle(item_code=None, docstatus=None, voucher_no=None, name=None): filters = [ - ["Serial and Batch Bundle", "item_code", "=", item_code], ["Serial and Batch Bundle", "is_cancelled", "=", 0], ] + if item_code: + filters.append(["Serial and Batch Bundle", "item_code", "=", item_code]) + if not docstatus: docstatus = [0, 1] diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 403e04ae60b5..3e83fafcad5a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -925,6 +925,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle this.toggle_related_fields(this.frm.doc); this.toggle_enable_bom(); this.show_stock_ledger(); + erpnext.utils.view_serial_batch_nos(this.frm); if (this.frm.doc.docstatus===1 && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { this.show_general_ledger(); } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d9b5503b5008..0059a3f43276 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -420,7 +420,7 @@ def validate_qty(self): transferred_materials = frappe.db.sql( """ select - sum(qty) as qty + sum(sed.qty) as qty from `tabStock Entry` se,`tabStock Entry Detail` sed where se.name = sed.parent and se.docstatus=1 and diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 0664c2929cc7..cb2adf1682a8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -337,6 +337,7 @@ erpnext.stock.StockReconciliation = class StockReconciliation extends erpnext.st refresh() { if(this.frm.doc.docstatus > 0) { this.show_stock_ledger(); + erpnext.utils.view_serial_batch_nos(this.frm); if (erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { this.show_general_ledger(); } diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 746a1cbaf17b..3819c0b24a14 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -83,12 +83,6 @@ frappe.ui.form.on("Warehouse", { } frm.toggle_enable(["is_group", "company"], false); - - frappe.dynamic_link = { - doc: frm.doc, - fieldname: "name", - doctype: "Warehouse", - }; }, }); diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js index a7d7149c3825..48a72a2bfe5a 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js @@ -9,13 +9,27 @@ frappe.query_reports["Batch Item Expiry Status"] = { "fieldtype": "Date", "width": "80", "default": frappe.sys_defaults.year_start_date, + "reqd": 1, }, { "fieldname":"to_date", "label": __("To Date"), "fieldtype": "Date", "width": "80", - "default": frappe.datetime.get_today() + "default": frappe.datetime.get_today(), + "reqd": 1, + }, + { + "fieldname":"item", + "label": __("Item"), + "fieldtype": "Link", + "options": "Item", + "width": "100", + "get_query": function () { + return { + filters: {"has_batch_no": 1} + } + } } ] } diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py index ef7d6e6816c1..5661e8b26096 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py @@ -4,113 +4,86 @@ import frappe from frappe import _ -from frappe.query_builder.functions import IfNull -from frappe.utils import cint, getdate +from frappe.query_builder.functions import Date def execute(filters=None): - if not filters: - filters = {} - - float_precision = cint(frappe.db.get_default("float_precision")) or 3 + validate_filters(filters) - columns = get_columns(filters) - item_map = get_item_details(filters) - iwb_map = get_item_warehouse_batch_map(filters, float_precision) - - data = [] - for item in sorted(iwb_map): - for wh in sorted(iwb_map[item]): - for batch in sorted(iwb_map[item][wh]): - qty_dict = iwb_map[item][wh][batch] - - data.append( - [ - item, - item_map[item]["item_name"], - item_map[item]["description"], - wh, - batch, - frappe.db.get_value("Batch", batch, "expiry_date"), - qty_dict.expiry_status, - ] - ) + columns = get_columns() + data = get_data(filters) return columns, data -def get_columns(filters): - """return columns based on filters""" - - columns = ( - [_("Item") + ":Link/Item:100"] - + [_("Item Name") + "::150"] - + [_("Description") + "::150"] - + [_("Warehouse") + ":Link/Warehouse:100"] - + [_("Batch") + ":Link/Batch:100"] - + [_("Expires On") + ":Date:90"] - + [_("Expiry (In Days)") + ":Int:120"] - ) - - return columns - +def validate_filters(filters): + if not filters: + frappe.throw(_("Please select the required filters")) -def get_stock_ledger_entries(filters): if not filters.get("from_date"): frappe.throw(_("'From Date' is required")) if not filters.get("to_date"): frappe.throw(_("'To Date' is required")) - sle = frappe.qb.DocType("Stock Ledger Entry") - query = ( - frappe.qb.from_(sle) - .select(sle.item_code, sle.batch_no, sle.warehouse, sle.posting_date, sle.actual_qty) - .where( - (sle.is_cancelled == 0) - & (sle.docstatus < 2) - & (IfNull(sle.batch_no, "") != "") - & (sle.posting_date <= filters["to_date"]) - ) - .orderby(sle.item_code, sle.warehouse) - ) - - return query.run(as_dict=True) +def get_columns(): + return ( + [_("Item") + ":Link/Item:150"] + + [_("Item Name") + "::150"] + + [_("Batch") + ":Link/Batch:150"] + + [_("Stock UOM") + ":Link/UOM:100"] + + [_("Quantity") + ":Float:100"] + + [_("Expires On") + ":Date:100"] + + [_("Expiry (In Days)") + ":Int:130"] + ) -def get_item_warehouse_batch_map(filters, float_precision): - sle = get_stock_ledger_entries(filters) - iwb_map = {} - from_date = getdate(filters["from_date"]) - to_date = getdate(filters["to_date"]) +def get_data(filters): + data = [] - for d in sle: - iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( - d.batch_no, frappe._dict({"expires_on": None, "expiry_status": None}) + for batch in get_batch_details(filters): + data.append( + [ + batch.item, + batch.item_name, + batch.name, + batch.stock_uom, + batch.batch_qty, + batch.expiry_date, + max((batch.expiry_date - frappe.utils.datetime.date.today()).days, 0) + if batch.expiry_date + else None, + ] ) - qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] - - expiry_date_unicode = frappe.db.get_value("Batch", d.batch_no, "expiry_date") - qty_dict.expires_on = expiry_date_unicode - - exp_date = frappe.utils.data.getdate(expiry_date_unicode) - qty_dict.expires_on = exp_date - - expires_in_days = (exp_date - frappe.utils.datetime.date.today()).days + return data - if expires_in_days > 0: - qty_dict.expiry_status = expires_in_days - else: - qty_dict.expiry_status = 0 - - return iwb_map +def get_batch_details(filters): + batch = frappe.qb.DocType("Batch") + query = ( + frappe.qb.from_(batch) + .select( + batch.name, + batch.creation, + batch.expiry_date, + batch.item, + batch.item_name, + batch.stock_uom, + batch.batch_qty, + ) + .where( + (batch.disabled == 0) + & (batch.batch_qty > 0) + & ( + (Date(batch.creation) >= filters["from_date"]) & (Date(batch.creation) <= filters["to_date"]) + ) + ) + .orderby(batch.creation) + ) -def get_item_details(filters): - item_map = {} - for d in (frappe.qb.from_("Item").select("name", "item_name", "description")).run(as_dict=True): - item_map.setdefault(d.name, d) + if filters.get("item"): + query = query.where(batch.item == filters["item"]) - return item_map + return query.run(as_dict=True) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index c07287437a67..e7d3e208d1bb 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -10,11 +10,18 @@ from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter +SLE_COUNT_LIMIT = 10_000 + def execute(filters=None): if not filters: filters = {} + sle_count = frappe.db.count("Stock Ledger Entry", {"is_cancelled": 0}) + + if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"): + frappe.throw(_("Please select either the Item or Warehouse filter to generate the report.")) + if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) diff --git a/erpnext/stock/report/serial_and_batch_summary/__init__.py b/erpnext/stock/report/serial_and_batch_summary/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js new file mode 100644 index 000000000000..10e5925ff425 --- /dev/null +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js @@ -0,0 +1,95 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Serial and Batch Summary"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today() + }, + { + "fieldname":"item_code", + "label": __("Item"), + "fieldtype": "Link", + "options": "Item", + }, + { + "fieldname":"warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + }, + { + "fieldname":"voucher_type", + "label": __("Voucher Type"), + "fieldtype": "Link", + "options": "DocType", + get_query: function() { + return { + query: "erpnext.stock.report.serial_and_batch_summary.serial_and_batch_summary.get_voucher_type", + } + } + }, + { + "fieldname":"voucher_no", + "label": __("Voucher No"), + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + let voucher_type = frappe.query_report.get_filter_value('voucher_type'); + if (!voucher_type) return; + + return frappe.db.get_link_options(voucher_type, txt); + }, + }, + { + "fieldname":"serial_no", + "label": __("Serial No"), + "fieldtype": "Link", + "options": "Serial No", + get_query: function() { + return { + query: "erpnext.stock.report.serial_and_batch_summary.serial_and_batch_summary.get_serial_nos", + filters: { + "item_code": frappe.query_report.get_filter_value('item_code'), + "voucher_type": frappe.query_report.get_filter_value('voucher_type'), + "voucher_no": frappe.query_report.get_filter_value('voucher_no'), + } + } + } + }, + { + "fieldname":"batch_no", + "label": __("Batch No"), + "fieldtype": "Link", + "options": "Batch", + get_query: function() { + return { + query: "erpnext.stock.report.serial_and_batch_summary.serial_and_batch_summary.get_batch_nos", + filters: { + "item_code": frappe.query_report.get_filter_value('item_code'), + "voucher_type": frappe.query_report.get_filter_value('voucher_type'), + "voucher_no": frappe.query_report.get_filter_value('voucher_no'), + } + } + } + } + ] +}; diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json new file mode 100644 index 000000000000..7511e3a1987d --- /dev/null +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json @@ -0,0 +1,38 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-07-13 16:53:27.735091", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2023-07-13 16:53:33.204591", + "modified_by": "Administrator", + "module": "Stock", + "name": "Serial and Batch Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Serial and Batch Bundle", + "report_name": "Serial and Batch Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Sales User" + }, + { + "role": "Purchase User" + }, + { + "role": "Stock User" + }, + { + "role": "Maintenance User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py new file mode 100644 index 000000000000..3ea5e8278dfa --- /dev/null +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py @@ -0,0 +1,245 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ + + +def execute(filters=None): + data = get_data(filters) + columns = get_columns(filters, data) + + return columns, data + + +def get_data(filters): + filter_conditions = get_filter_conditions(filters) + + return frappe.get_all( + "Serial and Batch Bundle", + fields=[ + "`tabSerial and Batch Bundle`.`voucher_type`", + "`tabSerial and Batch Bundle`.`posting_date`", + "`tabSerial and Batch Bundle`.`name`", + "`tabSerial and Batch Bundle`.`company`", + "`tabSerial and Batch Bundle`.`voucher_no`", + "`tabSerial and Batch Bundle`.`item_code`", + "`tabSerial and Batch Bundle`.`item_name`", + "`tabSerial and Batch Entry`.`serial_no`", + "`tabSerial and Batch Entry`.`batch_no`", + "`tabSerial and Batch Entry`.`warehouse`", + "`tabSerial and Batch Entry`.`incoming_rate`", + "`tabSerial and Batch Entry`.`stock_value_difference`", + "`tabSerial and Batch Entry`.`qty`", + ], + filters=filter_conditions, + order_by="posting_date", + ) + + +def get_filter_conditions(filters): + filter_conditions = [ + ["Serial and Batch Bundle", "docstatus", "=", 1], + ["Serial and Batch Bundle", "is_cancelled", "=", 0], + ] + + for field in ["voucher_type", "voucher_no", "item_code", "warehouse", "company"]: + if filters.get(field): + if field == "voucher_no": + filter_conditions.append(["Serial and Batch Bundle", field, "in", filters.get(field)]) + else: + filter_conditions.append(["Serial and Batch Bundle", field, "=", filters.get(field)]) + + if filters.get("from_date") and filters.get("to_date"): + filter_conditions.append( + [ + "Serial and Batch Bundle", + "posting_date", + "between", + [filters.get("from_date"), filters.get("to_date")], + ] + ) + + for field in ["serial_no", "batch_no"]: + if filters.get(field): + filter_conditions.append(["Serial and Batch Entry", field, "=", filters.get(field)]) + + return filter_conditions + + +def get_columns(filters, data): + columns = [ + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120, + }, + { + "label": _("Serial and Batch Bundle"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Serial and Batch Bundle", + "width": 110, + }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + ] + + item_details = {} + + item_codes = [] + if filters.get("voucher_type"): + item_codes = [d.item_code for d in data] + + if filters.get("item_code") or (item_codes and len(list(set(item_codes))) == 1): + item_details = frappe.get_cached_value( + "Item", + filters.get("item_code") or item_codes[0], + ["has_serial_no", "has_batch_no"], + as_dict=True, + ) + + if not filters.get("voucher_no"): + columns.extend( + [ + { + "label": _("Voucher Type"), + "fieldname": "voucher_type", + "fieldtype": "Link", + "options": "DocType", + "width": 120, + }, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 160, + }, + ] + ) + + if not filters.get("item_code"): + columns.extend( + [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + ] + ) + + if not filters.get("warehouse"): + columns.append( + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 120, + } + ) + + if not item_details or item_details.get("has_serial_no"): + columns.append( + {"label": _("Serial No"), "fieldname": "serial_no", "fieldtype": "Data", "width": 120} + ) + + if not item_details or item_details.get("has_batch_no"): + columns.extend( + [ + {"label": _("Batch No"), "fieldname": "batch_no", "fieldtype": "Data", "width": 120}, + {"label": _("Batch Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 120}, + ] + ) + + columns.extend( + [ + {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Float", "width": 120}, + { + "label": _("Change in Stock Value"), + "fieldname": "stock_value_difference", + "fieldtype": "Float", + "width": 120, + }, + ] + ) + + return columns + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_voucher_type(doctype, txt, searchfield, start, page_len, filters): + child_doctypes = frappe.get_all( + "DocField", + filters={"fieldname": "serial_and_batch_bundle"}, + fields=["distinct parent as parent"], + ) + + query_filters = {"options": ["in", [d.parent for d in child_doctypes]]} + if txt: + query_filters["parent"] = ["like", "%{}%".format(txt)] + + return frappe.get_all("DocField", filters=query_filters, fields=["distinct parent"], as_list=True) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_serial_nos(doctype, txt, searchfield, start, page_len, filters): + query_filters = {} + + if txt: + query_filters["serial_no"] = ["like", f"%{txt}%"] + + if filters.get("voucher_no"): + serial_batch_bundle = frappe.get_cached_value( + "Serial and Batch Bundle", + {"voucher_no": ("in", filters.get("voucher_no")), "docstatus": 1, "is_cancelled": 0}, + "name", + ) + + query_filters["parent"] = serial_batch_bundle + if not txt: + query_filters["serial_no"] = ("is", "set") + + return frappe.get_all( + "Serial and Batch Entry", filters=query_filters, fields=["serial_no"], as_list=True + ) + + else: + query_filters["item_code"] = filters.get("item_code") + return frappe.get_all("Serial No", filters=query_filters, as_list=True) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_batch_nos(doctype, txt, searchfield, start, page_len, filters): + query_filters = {} + + if txt: + query_filters["batch_no"] = ["like", f"%{txt}%"] + + if filters.get("voucher_no"): + serial_batch_bundle = frappe.get_cached_value( + "Serial and Batch Bundle", + {"voucher_no": ("in", filters.get("voucher_no")), "docstatus": 1, "is_cancelled": 0}, + "name", + ) + + query_filters["parent"] = serial_batch_bundle + if not txt: + query_filters["batch_no"] = ("is", "set") + + return frappe.get_all( + "Serial and Batch Entry", filters=query_filters, fields=["batch_no"], as_list=True + ) + + else: + query_filters["item"] = filters.get("item_code") + return frappe.get_all("Batch", filters=query_filters, as_list=True) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 7b1eae5545f7..5abb8e827f68 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -645,7 +645,7 @@ def get_dependent_entries_to_fix(self, entries_to_fix, sle): def update_distinct_item_warehouses(self, dependant_sle): key = (dependant_sle.item_code, dependant_sle.warehouse) - val = frappe._dict({"sle": dependant_sle}) + val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []}) if key not in self.distinct_item_warehouses: self.distinct_item_warehouses[key] = val @@ -654,13 +654,26 @@ def update_distinct_item_warehouses(self, dependant_sle): existing_sle_posting_date = ( self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") ) + + dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key) + if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): val.sle_changed = True self.distinct_item_warehouses[key] = val self.new_items_found = True - elif self.distinct_item_warehouses[key].get("reposting_status"): - self.distinct_item_warehouses[key] = val + elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos): + # Future dependent voucher needs to be repost to get the correct stock value + # If dependent voucher has not reposted, then add it to the list + dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no) self.new_items_found = True + val.dependent_voucher_detail_nos = dependent_voucher_detail_nos + self.distinct_item_warehouses[key] = val + + def get_dependent_voucher_detail_nos(self, key): + if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]: + self.distinct_item_warehouses[key].dependent_voucher_detail_nos = [] + + return self.distinct_item_warehouses[key].dependent_voucher_detail_nos def process_sle(self, sle): # previous sle data for this warehouse @@ -1370,6 +1383,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): "qty_after_transaction", "posting_date", "posting_time", + "voucher_detail_no", "timestamp(posting_date, posting_time) as timestamp", ], as_dict=1, diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 2a078c4395b4..6c9bc54f7e6d 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -21,6 +21,7 @@ time_diff_in_seconds, to_timedelta, ) +from frappe.utils.caching import redis_cache from frappe.utils.nestedset import get_ancestors_of from frappe.utils.safe_exec import get_safe_globals @@ -209,6 +210,10 @@ def after_insert(self): def on_update(self): set_documents_with_active_service_level_agreement() + def clear_cache(self): + get_sla_doctypes.clear_cache() + return super().clear_cache() + def create_docfields(self, meta, service_level_agreement_fields): last_index = len(meta.fields) @@ -990,6 +995,7 @@ def get_user_time(user, to_string=False): @frappe.whitelist() +@redis_cache() def get_sla_doctypes(): doctypes = [] data = frappe.get_all("Service Level Agreement", {"enabled": 1}, ["document_type"], distinct=1) @@ -998,3 +1004,7 @@ def get_sla_doctypes(): doctypes.append(entry.document_type) return doctypes + + +def add_sla_doctypes(bootinfo): + bootinfo.service_level_agreement_doctypes = get_sla_doctypes() diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 7725e71f19c4..1d6839c1e6e4 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -24,12 +24,10 @@ def before_insert(self): lead_number = self.get("from") if self.is_incoming_call() else self.get("to") lead_number = strip_number(lead_number) - contact = get_contact_with_phone_number(strip_number(lead_number)) - if contact: + if contact := get_contact_with_phone_number(strip_number(lead_number)): self.add_link(link_type="Contact", link_name=contact) - lead = get_lead_with_phone_number(lead_number) - if lead: + if lead := get_lead_with_phone_number(lead_number): self.add_link(link_type="Lead", link_name=lead) # Add Employee Name @@ -70,28 +68,30 @@ def add_link(self, link_type, link_name): self.append("links", {"link_doctype": link_type, "link_name": link_name}) def trigger_call_popup(self): - if self.is_incoming_call(): - scheduled_employees = get_scheduled_employees_for_popup(self.medium) - employees = get_employees_with_number(self.to) - employee_emails = [employee.get("user_id") for employee in employees] + if not self.is_incoming_call(): + return - # check if employees with matched number are scheduled to receive popup - emails = set(scheduled_employees).intersection(employee_emails) + scheduled_employees = get_scheduled_employees_for_popup(self.medium) + employees = get_employees_with_number(self.to) + employee_emails = [employee.get("user_id") for employee in employees] - if frappe.conf.developer_mode: - self.add_comment( - text=f""" + # check if employees with matched number are scheduled to receive popup + emails = set(scheduled_employees).intersection(employee_emails) + + if frappe.conf.developer_mode: + self.add_comment( + text=f""" Scheduled Employees: {scheduled_employees} Matching Employee: {employee_emails} Show Popup To: {emails} """ - ) + ) - if employee_emails and not emails: - self.add_comment(text=_("No employee was scheduled for call popup")) + if employee_emails and not emails: + self.add_comment(text=_("No employee was scheduled for call popup")) - for email in emails: - frappe.publish_realtime("show_call_popup", self, user=email) + for email in emails: + frappe.publish_realtime("show_call_popup", self, user=email) def update_received_by(self): if employees := get_employees_with_number(self.get("to")): @@ -154,8 +154,8 @@ def link_existing_conversations(doc, state): ELSE 0 END )=0 - """, - dict(phone_number="%{}".format(number), docname=doc.name, doctype=doc.doctype), + """, + dict(phone_number=f"%{number}", docname=doc.name, doctype=doc.doctype), ) for log in logs: @@ -175,7 +175,7 @@ def get_linked_call_logs(doctype, docname): filters={"parenttype": "Call Log", "link_doctype": doctype, "link_name": docname}, ) - logs = set([log.parent for log in logs]) + logs = {log.parent for log in logs} logs = frappe.get_all("Call Log", fields=["*"], filters={"name": ["in", logs]}) diff --git a/erpnext/templates/includes/itemised_tax_breakup.html b/erpnext/templates/includes/itemised_tax_breakup.html index 5652bb1ddddd..fbc80de7d0db 100644 --- a/erpnext/templates/includes/itemised_tax_breakup.html +++ b/erpnext/templates/includes/itemised_tax_breakup.html @@ -15,7 +15,7 @@ {% for item, taxes in itemised_tax.items() %} {{ item }} - + {% if doc.get('is_return') %} {{ frappe.utils.fmt_money((itemised_taxable_amount.get(item, 0))|abs, None, doc.currency) }} {% else %} @@ -25,7 +25,7 @@ {% for tax_account in tax_accounts %} {% set tax_details = taxes.get(tax_account) %} {% if tax_details %} - + {% if tax_details.tax_rate or not tax_details.tax_amount %} ({{ tax_details.tax_rate }}%) {% endif %} diff --git a/erpnext/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py deleted file mode 100644 index 3ad2575c23d8..000000000000 --- a/erpnext/tests/exotel_test_data.py +++ /dev/null @@ -1,122 +0,0 @@ -import frappe - -call_initiation_data = frappe._dict( - { - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "Created": "Wed, 23 Feb 2022 12:31:59", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-23 12:32:02", - "DialWhomNumber": "09999999999", - "Status": "busy", - "EventType": "Dial", - "AgentEmail": "test_employee_exotel@company.com", - } -) - -call_end_data = frappe._dict( - { - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Wed, 23 Feb 2022 12:31:59", - "DialCallDuration": "17", - "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3", - "StartTime": "2022-02-23 12:31:58", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "completed", - "CallType": "completed", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", - "CurrentTime": "2022-02-23 12:32:25", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "10", - "CallerId": "09999999980", - "CauseCode": "NORMAL_CLEARING", - "Cause": "16", - } - ], - } -) - -call_disconnected_data = frappe._dict( - { - "CallSid": "d96421addce69e24bdc7ce5880d1162l", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:58:12", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:58:12", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "canceled", - "CallType": "client-hangup", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:58:47", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], - } -) - -call_not_answered_data = frappe._dict( - { - "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:47:02", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:47:02", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "no-answer", - "CallType": "incomplete", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:47:40", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], - } -) diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py deleted file mode 100644 index 9b9141457141..000000000000 --- a/erpnext/tests/test_exotel.py +++ /dev/null @@ -1,68 +0,0 @@ -import frappe -from frappe.contacts.doctype.contact.test_contact import create_contact -from frappe.tests.test_api import FrappeAPITestCase - -from erpnext.setup.doctype.employee.test_employee import make_employee - - -class TestExotel(FrappeAPITestCase): - @classmethod - def setUpClass(cls): - cls.CURRENT_DB_CONNECTION = frappe.db - cls.test_employee_name = make_employee( - user="test_employee_exotel@company.com", cell_number="9999999999" - ) - frappe.db.set_single_value("Exotel Settings", "enabled", 1) - phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}] - create_contact(name="Test Contact", salutation="Mr", phones=phones) - frappe.db.commit() - - def test_for_successful_call(self): - from .exotel_test_data import call_end_data, call_initiation_data - - api_method = "handle_incoming_call" - end_call_api_method = "handle_end_call" - - self.emulate_api_call_from_exotel(api_method, call_initiation_data) - self.emulate_api_call_from_exotel(end_call_api_method, call_end_data) - call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid) - - self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom) - self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) - self.assertEqual(call_log.get("status"), "Completed") - - def test_for_disconnected_call(self): - from .exotel_test_data import call_disconnected_data - - api_method = "handle_missed_call" - self.emulate_api_call_from_exotel(api_method, call_disconnected_data) - call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid) - self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom) - self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) - self.assertEqual(call_log.get("status"), "Canceled") - - def test_for_call_not_answered(self): - from .exotel_test_data import call_not_answered_data - - api_method = "handle_missed_call" - self.emulate_api_call_from_exotel(api_method, call_not_answered_data) - call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid) - self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom) - self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) - self.assertEqual(call_log.get("status"), "No Answer") - - def emulate_api_call_from_exotel(self, api_method, data): - self.post( - f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}", - data=frappe.as_json(data), - content_type="application/json", - ) - # restart db connection to get latest data - frappe.connect() - - @classmethod - def tearDownClass(cls): - frappe.db = cls.CURRENT_DB_CONNECTION diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 5f0a8dc73541..8efa94df5a23 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1219,7 +1219,7 @@ High Sensitivity,Hohe Empfindlichkeit, Hold,Anhalten, Hold Invoice,Rechnung zurückhalten, Holiday,Urlaub, -Holiday List,Urlaubsübersicht, +Holiday List,Feiertagsliste, Hotel Rooms of type {0} are unavailable on {1},Hotelzimmer vom Typ {0} sind auf {1} nicht verfügbar, Hotels,Hotels, Hourly,Stündlich, @@ -3085,9 +3085,9 @@ Total Leaves,insgesamt Blätter, Total Order Considered,Geschätzte Summe der Bestellungen, Total Order Value,Gesamtbestellwert, Total Outgoing,Summe Auslieferungen, -Total Outstanding,Absolut aussergewöhnlich, -Total Outstanding Amount,Offener Gesamtbetrag, -Total Outstanding: {0},Gesamtsumme: {0}, +Total Outstanding,Summe ausstehende Beträge, +Total Outstanding Amount,Summe ausstehende Beträge, +Total Outstanding: {0},Summe ausstehende Beträge: {0}, Total Paid Amount,Summe gezahlte Beträge, Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total,Der gesamte Zahlungsbetrag im Zahlungsplan muss gleich Groß / Abgerundet sein, Total Payments,Gesamtzahlungen, @@ -3317,7 +3317,7 @@ Workflow,Workflow, Working,In Bearbeitung, Working Hours,Arbeitszeit, Workstation,Arbeitsplatz, -Workstation is closed on the following dates as per Holiday List: {0},Arbeitsplatz ist an folgenden Tagen gemäß der Urlaubsliste geschlossen: {0}, +Workstation is closed on the following dates as per Holiday List: {0},Arbeitsplatz ist an folgenden Tagen gemäß der Feiertagsliste geschlossen: {0}, Wrapping up,Aufwickeln, Wrong Password,Falsches Passwort, Year start date or end date is overlapping with {0}. To avoid please set company,"Jahresbeginn oder Enddatum überlappt mit {0}. Bitte ein Unternehmen wählen, um dies zu verhindern", @@ -3583,6 +3583,7 @@ Accounting Period overlaps with {0},Abrechnungszeitraum überschneidet sich mit Activity,Aktivität, Add / Manage Email Accounts.,Hinzufügen/Verwalten von E-Mail-Konten, Add Child,Unterpunkt hinzufügen, +Add Local Holidays,Lokale Feiertage hinzufügen, Add Multiple,Mehrere hinzufügen, Add Participants,Teilnehmer hinzufügen, Add to Featured Item,Zum empfohlenen Artikel hinzufügen, @@ -4046,6 +4047,7 @@ Stock Ledger ID,Bestandsbuch-ID, Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.,Der Bestandswert ({0}) und der Kontostand ({1}) sind für das Konto {2} und die verknüpften Lager nicht synchron., Stores - {0},Stores - {0}, Student with email {0} does not exist,Der Student mit der E-Mail-Adresse {0} existiert nicht, +Subdivision,Teilgebiet, Submit Review,Bewertung abschicken, Submitted,Gebucht, Supplier Addresses And Contacts,Lieferanten-Adressen und Kontaktdaten, @@ -4192,6 +4194,7 @@ Mode Of Payment,Zahlungsart, No students Found,Keine Schüler gefunden, Not in Stock,Nicht lagernd, Please select a Customer,Bitte wählen Sie einen Kunden aus, +Please select a country,Bitte wählen Sie ein Land aus, Printed On,Gedruckt auf, Received From,Erhalten von, Sales Person,Verkäufer, @@ -6497,7 +6500,7 @@ Reports to,Vorgesetzter, Attendance and Leave Details,Anwesenheits- und Urlaubsdetails, Leave Policy,Urlaubsrichtlinie, Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID), -Applicable Holiday List,Geltende Urlaubsliste, +Applicable Holiday List,Geltende Feiertagsliste, Default Shift,Standardverschiebung, Salary Details,Gehaltsdetails, Salary Mode,Gehaltsmodus, @@ -6662,12 +6665,12 @@ Unclaimed amount,Nicht beanspruchter Betrag, Expense Claim Detail,Auslage, Expense Date,Datum der Auslage, Expense Claim Type,Art der Auslagenabrechnung, -Holiday List Name,Urlaubslistenname, -Total Holidays,Insgesamt Feiertage, -Add Weekly Holidays,Wöchentliche Feiertage hinzufügen, +Holiday List Name,Name der Feiertagsliste, +Total Holidays,Insgesamt freie Tage, +Add Weekly Holidays,Wöchentlich freie Tage hinzufügen, Weekly Off,Wöchentlich frei, -Add to Holidays,Zu Feiertagen hinzufügen, -Holidays,Ferien, +Add to Holidays,Zu freien Tagen hinzufügen, +Holidays,Arbeitsfreie Tage, Clear Table,Tabelle leeren, HR Settings,Einstellungen zum Modul Personalwesen, Employee Settings,Mitarbeitereinstellungen, @@ -6777,7 +6780,7 @@ Transaction Name,Transaktionsname, Is Carry Forward,Ist Übertrag, Is Expired,Ist abgelaufen, Is Leave Without Pay,Ist unbezahlter Urlaub, -Holiday List for Optional Leave,Urlaubsliste für optionalen Urlaub, +Holiday List for Optional Leave,Feiertagsliste für optionalen Urlaub, Leave Allocations,Zuteilungen verlassen, Leave Policy Details,Urlaubsrichtliniendetails, Leave Policy Detail,Urlaubsrichtliniendetail, @@ -7646,7 +7649,7 @@ Legal Entity / Subsidiary with a separate Chart of Accounts belonging to the Org Change Abbreviation,Abkürzung ändern, Parent Company,Muttergesellschaft, Default Values,Standardwerte, -Default Holiday List,Standard-Urlaubsliste, +Default Holiday List,Standard Feiertagsliste, Default Selling Terms,Standardverkaufsbedingungen, Default Buying Terms,Standard-Einkaufsbedingungen, Create Chart Of Accounts Based On,"Kontenplan erstellen, basierend auf", @@ -8552,13 +8555,14 @@ If this is unchecked Journal Entries will be saved in a Draft state and will hav Enable Distributed Cost Center,Aktivieren Sie die verteilte Kostenstelle, Distributed Cost Center,Verteilte Kostenstelle, Dunning,Mahnung, +Dunning Level,Mahnstufe, DUNN-.MM.-.YY.-,DUNN-.MM .-. YY.-, Overdue Days,Überfällige Tage, Dunning Type,Mahnart, Dunning Fee,Mahngebühr, Dunning Amount,Mahnbetrag, -Resolved,Aufgelöst, -Unresolved,Ungelöst, +Resolved,Geklärt, +Unresolved,Ungeklärt, Printing Setting,Druckeinstellung, Body Text,Hauptteil, Closing Text,Text schließen, @@ -8738,7 +8742,7 @@ Company {0} already exists. Continuing will overwrite the Company and Chart of A Meta Data,Metadaten, Unresolve,Auflösen, Create Document,Dokument erstellen, -Mark as unresolved,Als ungelöst markieren, +Mark as unresolved,Als ungeklärt markieren, TaxJar Settings,TaxJar-Einstellungen, Sandbox Mode,Sandbox-Modus, Enable Tax Calculation,Steuerberechnung aktivieren, diff --git a/pyproject.toml b/pyproject.toml index 012ffb17a6d4..3e0dfb29b475 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "Unidecode~=1.3.6", "barcodenumber~=0.5.0", "rapidfuzz~=2.15.0", + "holidays~=0.28", # integration dependencies "gocardless-pro~=1.22.0",