diff --git a/.editorconfig b/.editorconfig index 24f122a8d43..e7d5cfeddcb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,13 @@ trim_trailing_whitespace = true charset = utf-8 # python, js indentation settings -[{*.py,*.js}] +[{*.py,*.js,*.vue,*.css,*.scss,*.html}] indent_style = tab indent_size = 4 +max_line_length = 110 + +# JSON files - mostly doctype schema files +[{*.json}] +insert_final_newline = false +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc index 276d6ff3725..4a5f87171e8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -156,6 +156,7 @@ "onScan": true, "html2canvas": true, "extend_cscript": true, - "localforage": true + "localforage": true, + "Plaid": true } } diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 3bc22af96ab..a770019bf70 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -28,4 +28,7 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a 494bd9ef78313436f0424b918f200dab8fc7c20b # bulk format python code with black -baec607ff5905b1c67531096a9cf50ec7ff00a5d \ No newline at end of file +baec607ff5905b1c67531096a9cf50ec7ff00a5d + +# ruff +4d34b1ead73baf4c5430a2ecbe44b9e8468d7626 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ccd712065dc..e6a7f85f81b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: 18 + node-version: 20 - name: Setup dependencies run: | diff --git a/.github/workflows/semantic-commits.yml b/.github/workflows/semantic-commits.yml index 1744bc33a9e..da3d564e66c 100644 --- a/.github/workflows/semantic-commits.yml +++ b/.github/workflows/semantic-commits.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 20 check-latest: true - name: Check commit titles diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d70977c07e2..1f670f2bec3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ fail_fast: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.3.0 hooks: - id: trailing-whitespace files: "erpnext.*" @@ -15,28 +15,65 @@ repos: args: ['--branch', 'develop'] - id: check-merge-conflict - id: check-ast + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements - - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 hooks: - - id: flake8 - additional_dependencies: [ - 'flake8-bugbear', - ] - args: ['--config', '.github/helper/.flake8_strict'] - exclude: ".*setup.py$" + - id: prettier + types_or: [javascript, vue, scss] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + erpnext/public/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.*| + erpnext/public/js/controllers/.*| + erpnext/templates/pages/order.js| + erpnext/templates/includes/.*| + .*/supplier_quotation.js| + .*/sales_taxes_and_charges_template.js| + .*/purchase_taxes_and_charges_template.js| + .*/subcontracting_order.js| + .*/landed_cost_voucher.js| + .*/payment_entry.js| + .*/loan_interest_accrual.js| + .*/loan_disbursement.js| + .*/loan_application.js| + .*/italy.js| + .*/sales_invoice.js| + .*/subcontracting_receipt.js| + .*/request_for_quotation.js| + .*/pos_profile.js| + .*/opportunity.js| + .*/quotation.js| + .*/sales_common.js| + .*/sales_order.js| + .*/pos_invoice.js| + .*/purchase_invoice.js| + .*/loan_repayment.js| + .*/material_request.js| + .*/purchase_receipt.js| + .*/delivery_note.js| + .*/loan.js| + .*/stock_entry.js| + .*/purchase_order.js| + .*/loan_write_off.js + )$ - - repo: https://github.com/adityahase/black - rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.0 hooks: - - id: black - additional_dependencies: ['click==8.0.4'] + - id: ruff + name: "Run ruff linter and apply fixes" + args: ["--fix"] - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - exclude: ".*setup.py$" + - id: ruff-format + name: "Format Python code" ci: diff --git a/commitlint.config.js b/commitlint.config.js index 8847564e53c..56702092214 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,25 +1,13 @@ module.exports = { - parserPreset: 'conventional-changelog-conventionalcommits', + parserPreset: "conventional-changelog-conventionalcommits", rules: { - 'subject-empty': [2, 'never'], - 'type-case': [2, 'always', 'lower-case'], - 'type-empty': [2, 'never'], - 'type-enum': [ + "subject-empty": [2, "never"], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [ 2, - 'always', - [ - 'build', - 'chore', - 'ci', - 'docs', - 'feat', - 'fix', - 'perf', - 'refactor', - 'revert', - 'style', - 'test', - ], + "always", + ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"], ], }, }; diff --git a/erpnext/__init__.py b/erpnext/__init__.py index ca5062d835e..0898ee7b4cd 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import inspect import frappe -__version__ = "14.52.1" +__version__ = "14.67.1" def get_default_company(user=None): @@ -13,7 +13,7 @@ def get_default_company(user=None): if not user: user = frappe.session.user - companies = get_user_default_as_list(user, "company") + companies = get_user_default_as_list("company", user) if companies: default_company = companies[0] else: @@ -36,10 +36,8 @@ def get_default_cost_center(company): if not frappe.flags.company_cost_center: frappe.flags.company_cost_center = {} - if not company in frappe.flags.company_cost_center: - frappe.flags.company_cost_center[company] = frappe.get_cached_value( - "Company", company, "cost_center" - ) + if company not in frappe.flags.company_cost_center: + frappe.flags.company_cost_center[company] = frappe.get_cached_value("Company", company, "cost_center") return frappe.flags.company_cost_center[company] @@ -47,7 +45,7 @@ def get_company_currency(company): """Returns the default company currency""" if not frappe.flags.company_currency: frappe.flags.company_currency = {} - if not company in frappe.flags.company_currency: + if company not in frappe.flags.company_currency: frappe.flags.company_currency[company] = frappe.db.get_value( "Company", company, "default_currency", cache=True ) @@ -81,7 +79,7 @@ def is_perpetual_inventory_enabled(company): if not hasattr(frappe.local, "enable_perpetual_inventory"): frappe.local.enable_perpetual_inventory = {} - if not company in frappe.local.enable_perpetual_inventory: + if company not in frappe.local.enable_perpetual_inventory: frappe.local.enable_perpetual_inventory[company] = ( frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0 ) @@ -96,7 +94,7 @@ def get_default_finance_book(company=None): if not hasattr(frappe.local, "default_finance_book"): frappe.local.default_finance_book = {} - if not company in frappe.local.default_finance_book: + if company not in frappe.local.default_finance_book: frappe.local.default_finance_book[company] = frappe.get_cached_value( "Company", company, "default_finance_book" ) @@ -108,7 +106,7 @@ def get_party_account_type(party_type): if not hasattr(frappe.local, "party_account_types"): frappe.local.party_account_types = {} - if not party_type in frappe.local.party_account_types: + if party_type not in frappe.local.party_account_types: frappe.local.party_account_types[party_type] = ( frappe.db.get_value("Party Type", party_type, "account_type") or "" ) diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py index 775a81fd25f..ef57a632506 100644 --- a/erpnext/accounts/custom/address.py +++ b/erpnext/accounts/custom/address.py @@ -11,14 +11,14 @@ class ERPNextAddress(Address): def validate(self): self.validate_reference() self.update_compnay_address() - super(ERPNextAddress, self).validate() + super().validate() def link_address(self): """Link address based on owner""" if self.is_your_company_address: return - return super(ERPNextAddress, self).link_address() + return super().link_address() def update_compnay_address(self): for link in self.get("links"): @@ -26,11 +26,11 @@ class ERPNextAddress(Address): self.is_your_company_address = 1 def validate_reference(self): - if self.is_your_company_address and not [ - row for row in self.links if row.link_doctype == "Company" - ]: + if self.is_your_company_address and not [row for row in self.links if row.link_doctype == "Company"]: frappe.throw( - _("Address needs to be linked to a Company. Please add a row for Company in the Links table."), + _( + "Address needs to be linked to a Company. Please add a row for Company in the Links table." + ), title=_("Company Not Linked"), ) diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.js b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.js index d8a83e53dc0..09846114ea2 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.js +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.js @@ -1,4 +1,4 @@ -frappe.provide('frappe.dashboards.chart_sources'); +frappe.provide("frappe.dashboards.chart_sources"); frappe.dashboards.chart_sources["Account Balance Timeline"] = { method: "erpnext.accounts.dashboard_chart_source.account_balance_timeline.account_balance_timeline.get", @@ -9,14 +9,14 @@ frappe.dashboards.chart_sources["Account Balance Timeline"] = { fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { fieldname: "account", label: __("Account"), fieldtype: "Link", options: "Account", - reqd: 1 + reqd: 1, }, - ] + ], }; diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index fefec0ee7b4..7e06c5b8c83 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -37,7 +37,7 @@ def get( filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) account = filters.get("account") - company = filters.get("company") + filters.get("company") if not account and chart_name: frappe.throw( @@ -83,7 +83,6 @@ def build_result(account, dates, gl_entries): # get balances in debit for entry in gl_entries: - # entry date is after the current pointer, so move the pointer forward while getdate(entry.posting_date) > result[date_index][0]: date_index += 1 @@ -133,8 +132,6 @@ def get_dates_from_timegrain(from_date, to_date, timegrain): dates = [get_period_ending(from_date, timegrain)] while getdate(dates[-1]) < getdate(to_date): - date = get_period_ending( - add_to_date(dates[-1], years=years, months=months, days=days), timegrain - ) + date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain) dates.append(date) return dates diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index d0940c7df21..9ffdf186f02 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -24,14 +24,10 @@ from erpnext.accounts.utils import get_account_currency def validate_service_stop_date(doc): """Validates service_stop_date for Purchase Invoice and Sales Invoice""" - enable_check = ( - "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" - ) + enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" old_stop_dates = {} - old_doc = frappe.db.get_all( - "{0} Item".format(doc.doctype), {"parent": doc.name}, ["name", "service_stop_date"] - ) + old_doc = frappe.db.get_all(f"{doc.doctype} Item", {"parent": doc.name}, ["name", "service_stop_date"]) for d in old_doc: old_stop_dates[d.name] = d.service_stop_date or "" @@ -62,16 +58,14 @@ def build_conditions(process_type, account, company): ) if account: - conditions += "AND %s='%s'" % (deferred_account, account) + conditions += f"AND {deferred_account}='{account}'" elif company: conditions += f"AND p.company = {frappe.db.escape(company)}" return conditions -def convert_deferred_expense_to_expense( - deferred_process, start_date=None, end_date=None, conditions="" -): +def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_date=None, conditions=""): # book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM if not start_date: @@ -81,16 +75,14 @@ def convert_deferred_expense_to_expense( # check for the purchase invoice for which GL entries has to be done invoices = frappe.db.sql_list( - """ + f""" select distinct item.parent from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p where item.service_start_date<=%s and item.service_end_date>=%s and item.enable_deferred_expense = 1 and item.parent=p.name and item.docstatus = 1 and ifnull(item.amount, 0) > 0 - {0} - """.format( - conditions - ), + {conditions} + """, (end_date, start_date), ) # nosec @@ -103,9 +95,7 @@ def convert_deferred_expense_to_expense( send_mail(deferred_process) -def convert_deferred_revenue_to_income( - deferred_process, start_date=None, end_date=None, conditions="" -): +def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_date=None, conditions=""): # book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM if not start_date: @@ -115,16 +105,14 @@ def convert_deferred_revenue_to_income( # check for the sales invoice for which GL entries has to be done invoices = frappe.db.sql_list( - """ + f""" select distinct item.parent from `tabSales Invoice Item` item, `tabSales Invoice` p where item.service_start_date<=%s and item.service_end_date>=%s and item.enable_deferred_revenue = 1 and item.parent=p.name and item.docstatus = 1 and ifnull(item.amount, 0) > 0 - {0} - """.format( - conditions - ), + {conditions} + """, (end_date, start_date), ) # nosec @@ -243,9 +231,7 @@ def calculate_monthly_amount( already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount( doc, item ) - base_amount = flt( - item.base_net_amount - already_booked_amount, item.precision("base_net_amount") - ) + base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount")) if account_currency == doc.company_currency: amount = base_amount else: @@ -265,17 +251,13 @@ def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, a if account_currency == doc.company_currency: amount = base_amount else: - amount = flt( - item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount") - ) + amount = flt(item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount")) else: already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount( doc, item ) - base_amount = flt( - item.base_net_amount - already_booked_amount, item.precision("base_net_amount") - ) + base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount")) if account_currency == doc.company_currency: amount = base_amount else: @@ -296,26 +278,22 @@ def get_already_booked_amount(doc, item): gl_entries_details = frappe.db.sql( """ - select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no + select sum({}) as total_credit, sum({}) as total_credit_in_account_currency, voucher_detail_no from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s and is_cancelled = 0 group by voucher_detail_no - """.format( - total_credit_debit, total_credit_debit_currency - ), + """.format(total_credit_debit, total_credit_debit_currency), (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True, ) journal_entry_details = frappe.db.sql( """ - SELECT sum(c.{0}) as total_credit, sum(c.{1}) as total_credit_in_account_currency, reference_detail_no + SELECT sum(c.{}) as total_credit, sum(c.{}) as total_credit_in_account_currency, reference_detail_no FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s and p.docstatus < 2 group by reference_detail_no - """.format( - total_credit_debit, total_credit_debit_currency - ), + """.format(total_credit_debit, total_credit_debit_currency), (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True, ) @@ -337,9 +315,7 @@ def get_already_booked_amount(doc, item): def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): - enable_check = ( - "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" - ) + enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto") @@ -440,9 +416,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): via_journal_entry = cint( frappe.db.get_singles_value("Accounts Settings", "book_deferred_entries_via_journal_entry") ) - submit_journal_entry = cint( - frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries") - ) + submit_journal_entry = cint(frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries")) book_deferred_entries_based_on = frappe.db.get_singles_value( "Accounts Settings", "book_deferred_entries_based_on" ) @@ -462,9 +436,7 @@ def process_deferred_accounting(posting_date=None): posting_date = today() if not cint( - frappe.db.get_singles_value( - "Accounts Settings", "automatically_process_deferred_accounting_entry" - ) + frappe.db.get_singles_value("Accounts Settings", "automatically_process_deferred_accounting_entry") ): return @@ -587,16 +559,13 @@ def book_revenue_via_journal_entry( deferred_process=None, submit="No", ): - if amount == 0: return journal_entry = frappe.new_doc("Journal Entry") journal_entry.posting_date = posting_date journal_entry.company = doc.company - journal_entry.voucher_type = ( - "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense" - ) + journal_entry.voucher_type = "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense" journal_entry.process_deferred_accounting = deferred_process debit_entry = { @@ -645,7 +614,6 @@ def book_revenue_via_journal_entry( def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr): - if doctype == "Sales Invoice": credit_account, debit_account = frappe.db.get_value( "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index 7d63b257faf..f26d1f82797 100644 --- a/erpnext/accounts/doctype/account/account.js +++ b/erpnext/accounts/doctype/account/account.js @@ -1,33 +1,32 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Account', { - setup: function(frm) { - frm.add_fetch('parent_account', 'report_type', 'report_type'); - frm.add_fetch('parent_account', 'root_type', 'root_type'); +frappe.ui.form.on("Account", { + setup: function (frm) { + frm.add_fetch("parent_account", "report_type", "report_type"); + frm.add_fetch("parent_account", "root_type", "root_type"); }, - onload: function(frm) { - frm.set_query('parent_account', function(doc) { + onload: function (frm) { + frm.set_query("parent_account", function (doc) { return { filters: { - "is_group": 1, - "company": doc.company - } + is_group: 1, + company: doc.company, + }, }; }); }, - refresh: function(frm) { - frm.toggle_display('account_name', frm.is_new()); + refresh: function (frm) { + frm.toggle_display("account_name", frm.is_new()); // hide fields if group - frm.toggle_display(['account_type', 'tax_rate'], cint(frm.doc.is_group) == 0); + frm.toggle_display(["account_type", "tax_rate"], cint(frm.doc.is_group) == 0); // disable fields - frm.toggle_enable(['is_group', 'company'], false); + frm.toggle_enable(["is_group", "company"], false); if (cint(frm.doc.is_group) == 0) { - frm.toggle_display('freeze_account', frm.doc.__onload - && frm.doc.__onload.can_freeze_account); + frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account); } // read-only for root accounts @@ -38,79 +37,101 @@ frappe.ui.form.on('Account', { } else { // credit days and type if customer or supplier frm.set_intro(null); - frm.trigger('account_type'); + frm.trigger("account_type"); // show / hide convert buttons - frm.trigger('add_toolbar_buttons'); + frm.trigger("add_toolbar_buttons"); } - if (frm.has_perm('write')) { - frm.add_custom_button(__('Merge Account'), function () { - frm.trigger("merge_account"); - }, __('Actions')); - frm.add_custom_button(__('Update Account Name / Number'), function () { - frm.trigger("update_account_number"); - }, __('Actions')); + if (frm.has_perm("write")) { + frm.add_custom_button( + __("Merge Account"), + function () { + frm.trigger("merge_account"); + }, + __("Actions") + ); + frm.add_custom_button( + __("Update Account Name / Number"), + function () { + frm.trigger("update_account_number"); + }, + __("Actions") + ); } } }, account_type: function (frm) { if (frm.doc.is_group == 0) { - frm.toggle_display(['tax_rate'], frm.doc.account_type == 'Tax'); - frm.toggle_display('warehouse', frm.doc.account_type == 'Stock'); + frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax"); + frm.toggle_display("warehouse", frm.doc.account_type == "Stock"); } }, - add_toolbar_buttons: function(frm) { - frm.add_custom_button(__('Chart of Accounts'), () => { - frappe.set_route("Tree", "Account"); - }, __('View')); + add_toolbar_buttons: function (frm) { + frm.add_custom_button( + __("Chart of Accounts"), + () => { + frappe.set_route("Tree", "Account"); + }, + __("View") + ); if (frm.doc.is_group == 1) { - frm.add_custom_button(__('Convert to Non-Group'), function () { - return frappe.call({ - doc: frm.doc, - method: 'convert_group_to_ledger', - callback: function() { - frm.refresh(); - } - }); - }, __('Actions')); + frm.add_custom_button( + __("Convert to Non-Group"), + function () { + return frappe.call({ + doc: frm.doc, + method: "convert_group_to_ledger", + callback: function () { + frm.refresh(); + }, + }); + }, + __("Actions") + ); + } else if (cint(frm.doc.is_group) == 0 && frappe.boot.user.can_read.indexOf("GL Entry") !== -1) { + frm.add_custom_button( + __("General Ledger"), + function () { + frappe.route_options = { + account: frm.doc.name, + from_date: frappe.sys_defaults.year_start_date, + to_date: frappe.sys_defaults.year_end_date, + company: frm.doc.company, + }; + frappe.set_route("query-report", "General Ledger"); + }, + __("View") + ); - } else if (cint(frm.doc.is_group) == 0 - && frappe.boot.user.can_read.indexOf("GL Entry") !== -1) { - frm.add_custom_button(__('General Ledger'), function () { - frappe.route_options = { - "account": frm.doc.name, - "from_date": frappe.sys_defaults.year_start_date, - "to_date": frappe.sys_defaults.year_end_date, - "company": frm.doc.company - }; - frappe.set_route("query-report", "General Ledger"); - }, __('View')); - - frm.add_custom_button(__('Convert to Group'), function () { - return frappe.call({ - doc: frm.doc, - method: 'convert_ledger_to_group', - callback: function() { - frm.refresh(); - } - }); - }, __('Actions')); + frm.add_custom_button( + __("Convert to Group"), + function () { + return frappe.call({ + doc: frm.doc, + method: "convert_ledger_to_group", + callback: function () { + frm.refresh(); + }, + }); + }, + __("Actions") + ); } }, - merge_account: function(frm) { + merge_account: function (frm) { var d = new frappe.ui.Dialog({ - title: __('Merge with Existing Account'), + title: __("Merge with Existing Account"), fields: [ { - "label" : "Name", - "fieldname": "name", - "fieldtype": "Data", - "reqd": 1, - "default": frm.doc.name - } + label: "Name", + fieldname: "name", + fieldtype: "Data", + reqd: 1, + default: frm.doc.name, + }, ], - primary_action: function() { + primary_action: function () { var data = d.get_values(); frappe.call({ method: "erpnext.accounts.doctype.account.account.merge_account", @@ -118,42 +139,45 @@ frappe.ui.form.on('Account', { old: frm.doc.name, new: data.name, }, - callback: function(r) { - if(!r.exc) { - if(r.message) { + callback: function (r) { + if (!r.exc) { + if (r.message) { frappe.set_route("Form", "Account", r.message); } d.hide(); } - } + }, }); }, - primary_action_label: __('Merge') + primary_action_label: __("Merge"), }); d.show(); }, - update_account_number: function(frm) { + update_account_number: function (frm) { var d = new frappe.ui.Dialog({ - title: __('Update Account Number / Name'), + title: __("Update Account Number / Name"), fields: [ { - "label": "Account Name", - "fieldname": "account_name", - "fieldtype": "Data", - "reqd": 1, - "default": frm.doc.account_name + label: "Account Name", + fieldname: "account_name", + fieldtype: "Data", + reqd: 1, + default: frm.doc.account_name, }, { - "label": "Account Number", - "fieldname": "account_number", - "fieldtype": "Data", - "default": frm.doc.account_number - } + label: "Account Number", + fieldname: "account_number", + fieldtype: "Data", + default: frm.doc.account_number, + }, ], - primary_action: function() { + primary_action: function () { var data = d.get_values(); - if(data.account_number === frm.doc.account_number && data.account_name === frm.doc.account_name) { + if ( + data.account_number === frm.doc.account_number && + data.account_name === frm.doc.account_name + ) { d.hide(); return; } @@ -163,11 +187,11 @@ frappe.ui.form.on('Account', { args: { account_number: data.account_number, account_name: data.account_name, - name: frm.doc.name + name: frm.doc.name, }, - callback: function(r) { - if(!r.exc) { - if(r.message) { + callback: function (r) { + if (!r.exc) { + if (r.message) { frappe.set_route("Form", "Account", r.message); } else { frm.set_value("account_number", data.account_number); @@ -175,11 +199,11 @@ frappe.ui.form.on('Account', { } d.hide(); } - } + }, }); }, - primary_action_label: __('Update') + primary_action_label: __("Update"), }); d.show(); - } + }, }); diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 22ddc2ffae3..8cbaef43180 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -29,7 +29,7 @@ class Account(NestedSet): if frappe.local.flags.ignore_update_nsm: return else: - super(Account, self).on_update() + super().on_update() def onload(self): frozen_accounts_modifier = frappe.db.get_value( @@ -58,6 +58,7 @@ class Account(NestedSet): self.validate_balance_must_be_debit_or_credit() self.validate_account_currency() self.validate_root_company_and_sync_account_to_children() + self.validate_receivable_payable_account_type() def validate_parent(self): """Fetch Parent Details and validate parent account""" @@ -86,9 +87,7 @@ class Account(NestedSet): def set_root_and_report_type(self): if self.parent_account: - par = frappe.db.get_value( - "Account", self.parent_account, ["report_type", "root_type"], as_dict=1 - ) + par = frappe.db.get_value("Account", self.parent_account, ["report_type", "root_type"], as_dict=1) if par.report_type: self.report_type = par.report_type @@ -114,6 +113,24 @@ class Account(NestedSet): "Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss" ) + def validate_receivable_payable_account_type(self): + doc_before_save = self.get_doc_before_save() + receivable_payable_types = ["Receivable", "Payable"] + if ( + doc_before_save + and doc_before_save.account_type in receivable_payable_types + and doc_before_save.account_type != self.account_type + ): + # check for ledger entries + if frappe.db.get_all("GL Entry", filters={"account": self.name, "is_cancelled": 0}, limit=1): + msg = _( + "There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report" + ).format( + frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type + ) + frappe.msgprint(msg) + self.add_comment("Comment", msg) + def validate_root_details(self): # does not exists parent if frappe.db.exists("Account", self.name): @@ -125,9 +142,7 @@ class Account(NestedSet): def validate_root_company_and_sync_account_to_children(self): # ignore validation while creating new compnay or while syncing to child companies - if ( - frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation - ): + if frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation: return ancestors = get_root_company(self.company) if ancestors: @@ -322,7 +337,7 @@ class Account(NestedSet): if self.check_gle_exists(): throw(_("Account with existing transaction can not be deleted")) - super(Account, self).on_trash(True) + super().on_trash(True) @frappe.whitelist() @@ -330,9 +345,8 @@ class Account(NestedSet): def get_parent_account(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( """select name from tabAccount - where is_group = 1 and docstatus != 2 and company = %s - and %s like %s order by name limit %s offset %s""" - % ("%s", searchfield, "%s", "%s", "%s"), + where is_group = 1 and docstatus != 2 and company = {} + and {} like {} order by name limit {} offset {}""".format("%s", searchfield, "%s", "%s", "%s"), (filters["company"], "%%%s%%" % txt, page_len, start), as_list=1, ) @@ -390,9 +404,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda if not account: return - old_acc_name, old_acc_number = frappe.db.get_value( - "Account", name, ["account_name", "account_number"] - ) + old_acc_name, old_acc_number = frappe.db.get_value("Account", name, ["account_name", "account_number"]) # check if account exists in parent company ancestors = get_ancestors_of("Company", account.company) @@ -500,7 +512,5 @@ def sync_update_account_number_in_child( if old_acc_number: filters["account_number"] = old_acc_number - for d in frappe.db.get_values( - "Account", filters=filters, fieldname=["company", "name"], as_dict=True - ): + for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True): update_account_number(d["name"], account_name, account_number, from_descendant=True) diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index d537adfcbfd..e0bfa6e902a 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -1,4 +1,4 @@ -frappe.provide("frappe.treeview_settings") +frappe.provide("frappe.treeview_settings"); frappe.treeview_settings["Account"] = { breadcrumb: "Accounts", @@ -7,12 +7,12 @@ frappe.treeview_settings["Account"] = { filters: [ { fieldname: "company", - fieldtype:"Select", + fieldtype: "Select", options: erpnext.utils.get_tree_options("company"), label: __("Company"), default: erpnext.utils.get_tree_default("company"), - on_change: function() { - var me = frappe.treeview_settings['Account'].treeview; + on_change: function () { + var me = frappe.treeview_settings["Account"].treeview; var company = me.page.fields_dict.company.get_value(); if (!company) { frappe.throw(__("Please set a Company")); @@ -22,30 +22,36 @@ frappe.treeview_settings["Account"] = { args: { company: company, }, - callback: function(r) { - if(r.message) { + callback: function (r) { + if (r.message) { let root_company = r.message.length ? r.message[0] : ""; me.page.fields_dict.root_company.set_value(root_company); - frappe.db.get_value("Company", {"name": company}, "allow_account_creation_against_child_company", (r) => { - frappe.flags.ignore_root_company_validation = r.allow_account_creation_against_child_company; - }); + frappe.db.get_value( + "Company", + { name: company }, + "allow_account_creation_against_child_company", + (r) => { + frappe.flags.ignore_root_company_validation = + r.allow_account_creation_against_child_company; + } + ); } - } + }, }); - } + }, }, { fieldname: "root_company", - fieldtype:"Data", + fieldtype: "Data", label: __("Root Company"), hidden: true, - disable_onchange: true - } + disable_onchange: true, + }, ], root_label: "Accounts", - get_tree_nodes: 'erpnext.accounts.utils.get_children', - on_get_node: function(nodes, deep=false) { + get_tree_nodes: "erpnext.accounts.utils.get_children", + on_get_node: function (nodes, deep = false) { if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return; let accounts = []; @@ -57,151 +63,231 @@ frappe.treeview_settings["Account"] = { } frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => { - if(value) { - + if (value) { const get_balances = frappe.call({ - method: 'erpnext.accounts.utils.get_account_balances', + method: "erpnext.accounts.utils.get_account_balances", args: { accounts: accounts, - company: cur_tree.args.company + company: cur_tree.args.company, }, }); - get_balances.then(r => { + get_balances.then((r) => { if (!r.message || r.message.length == 0) return; for (let account of r.message) { - const node = cur_tree.nodes && cur_tree.nodes[account.value]; if (!node || node.is_root) continue; // show Dr if positive since balance is calculated as debit - credit else show Cr const balance = account.balance_in_account_currency || account.balance; - const dr_or_cr = balance > 0 ? "Dr": "Cr"; + const dr_or_cr = balance > 0 ? "Dr" : "Cr"; const format = (value, currency) => format_currency(Math.abs(value), currency); - if (account.balance!==undefined) { - node.parent && node.parent.find('.balance-area').remove(); - $('' - + (account.balance_in_account_currency ? - (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") - + format(account.balance, account.company_currency) - + " " + dr_or_cr - + '').insertBefore(node.$ul); + if (account.balance !== undefined) { + node.parent && node.parent.find(".balance-area").remove(); + $( + '' + + (account.balance_in_account_currency + ? format( + account.balance_in_account_currency, + account.account_currency + ) + " / " + : "") + + format(account.balance, account.company_currency) + + " " + + dr_or_cr + + "" + ).insertBefore(node.$ul); } } }); } }); }, - add_tree_node: 'erpnext.accounts.utils.add_ac', - menu_items:[ + add_tree_node: "erpnext.accounts.utils.add_ac", + menu_items: [ { - label: __('New Company'), - action: function() { frappe.new_doc("Company", true) }, - condition: 'frappe.boot.user.can_create.indexOf("Company") !== -1' - } + label: __("New Company"), + action: function () { + frappe.new_doc("Company", true); + }, + condition: 'frappe.boot.user.can_create.indexOf("Company") !== -1', + }, ], fields: [ - {fieldtype:'Data', fieldname:'account_name', label:__('New Account Name'), reqd:true, - description: __("Name of new Account. Note: Please don't create accounts for Customers and Suppliers")}, - {fieldtype:'Data', fieldname:'account_number', label:__('Account Number'), - description: __("Number of new Account, it will be included in the account name as a prefix")}, - {fieldtype:'Check', fieldname:'is_group', label:__('Is Group'), - description: __('Further accounts can be made under Groups, but entries can be made against non-Groups')}, - {fieldtype:'Select', fieldname:'root_type', label:__('Root Type'), - options: ['Asset', 'Liability', 'Equity', 'Income', 'Expense'].join('\n'), - depends_on: 'eval:doc.is_group && !doc.parent_account'}, - {fieldtype:'Select', fieldname:'account_type', label:__('Account Type'), - options: frappe.get_meta("Account").fields.filter(d => d.fieldname=='account_type')[0].options, - description: __("Optional. This setting will be used to filter in various transactions.") + { + fieldtype: "Data", + fieldname: "account_name", + label: __("New Account Name"), + reqd: true, + description: __( + "Name of new Account. Note: Please don't create accounts for Customers and Suppliers" + ), + }, + { + fieldtype: "Data", + fieldname: "account_number", + label: __("Account Number"), + description: __("Number of new Account, it will be included in the account name as a prefix"), + }, + { + fieldtype: "Check", + fieldname: "is_group", + label: __("Is Group"), + description: __( + "Further accounts can be made under Groups, but entries can be made against non-Groups" + ), + }, + { + fieldtype: "Select", + fieldname: "root_type", + label: __("Root Type"), + options: ["Asset", "Liability", "Equity", "Income", "Expense"].join("\n"), + depends_on: "eval:doc.is_group && !doc.parent_account", + }, + { + fieldtype: "Select", + fieldname: "account_type", + label: __("Account Type"), + options: frappe.get_meta("Account").fields.filter((d) => d.fieldname == "account_type")[0] + .options, + description: __("Optional. This setting will be used to filter in various transactions."), + }, + { + fieldtype: "Float", + fieldname: "tax_rate", + label: __("Tax Rate"), + depends_on: 'eval:doc.is_group==0&&doc.account_type=="Tax"', + }, + { + fieldtype: "Link", + fieldname: "account_currency", + label: __("Currency"), + options: "Currency", + description: __("Optional. Sets company's default currency, if not specified."), }, - {fieldtype:'Float', fieldname:'tax_rate', label:__('Tax Rate'), - depends_on: 'eval:doc.is_group==0&&doc.account_type=="Tax"'}, - {fieldtype:'Link', fieldname:'account_currency', label:__('Currency'), options:"Currency", - description: __("Optional. Sets company's default currency, if not specified.")} ], - ignore_fields:["parent_account"], - onload: function(treeview) { - frappe.treeview_settings['Account'].treeview = {}; - $.extend(frappe.treeview_settings['Account'].treeview, treeview); + ignore_fields: ["parent_account"], + onload: function (treeview) { + frappe.treeview_settings["Account"].treeview = {}; + $.extend(frappe.treeview_settings["Account"].treeview, treeview); function get_company() { return treeview.page.fields_dict.company.get_value(); } // tools - treeview.page.add_inner_button(__("Chart of Cost Centers"), function() { - frappe.set_route('Tree', 'Cost Center', {company: get_company()}); - }, __('View')); + treeview.page.add_inner_button( + __("Chart of Cost Centers"), + function () { + frappe.set_route("Tree", "Cost Center", { company: get_company() }); + }, + __("View") + ); - treeview.page.add_inner_button(__("Opening Invoice Creation Tool"), function() { - frappe.set_route('Form', 'Opening Invoice Creation Tool', {company: get_company()}); - }, __('View')); + treeview.page.add_inner_button( + __("Opening Invoice Creation Tool"), + function () { + frappe.set_route("Form", "Opening Invoice Creation Tool", { company: get_company() }); + }, + __("View") + ); - treeview.page.add_inner_button(__("Period Closing Voucher"), function() { - frappe.set_route('List', 'Period Closing Voucher', {company: get_company()}); - }, __('View')); + treeview.page.add_inner_button( + __("Period Closing Voucher"), + function () { + frappe.set_route("List", "Period Closing Voucher", { company: get_company() }); + }, + __("View") + ); - - treeview.page.add_inner_button(__("Journal Entry"), function() { - frappe.new_doc('Journal Entry', {company: get_company()}); - }, __('Create')); - treeview.page.add_inner_button(__("Company"), function() { - frappe.new_doc('Company'); - }, __('Create')); + treeview.page.add_inner_button( + __("Journal Entry"), + function () { + frappe.new_doc("Journal Entry", { company: get_company() }); + }, + __("Create") + ); + treeview.page.add_inner_button( + __("Company"), + function () { + frappe.new_doc("Company"); + }, + __("Create") + ); // financial statements - for (let report of ['Trial Balance', 'General Ledger', 'Balance Sheet', - 'Profit and Loss Statement', 'Cash Flow Statement', 'Accounts Payable', 'Accounts Receivable']) { - treeview.page.add_inner_button(__(report), function() { - frappe.set_route('query-report', report, {company: get_company()}); - }, __('Financial Statements')); + for (let report of [ + "Trial Balance", + "General Ledger", + "Balance Sheet", + "Profit and Loss Statement", + "Cash Flow Statement", + "Accounts Payable", + "Accounts Receivable", + ]) { + treeview.page.add_inner_button( + __(report), + function () { + frappe.set_route("query-report", report, { company: get_company() }); + }, + __("Financial Statements") + ); } - }, - post_render: function(treeview) { - frappe.treeview_settings['Account'].treeview["tree"] = treeview.tree; - treeview.page.set_primary_action(__("New"), function() { - let root_company = treeview.page.fields_dict.root_company.get_value(); + post_render: function (treeview) { + frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree; + treeview.page.set_primary_action( + __("New"), + function () { + let root_company = treeview.page.fields_dict.root_company.get_value(); - if(root_company) { - frappe.throw(__("Please add the account to root level Company - {0}"), [root_company]); - } else { - treeview.new_node(); - } - }, "add"); + if (root_company) { + frappe.throw(__("Please add the account to root level Company - {0}"), [root_company]); + } else { + treeview.new_node(); + } + }, + "add" + ); }, toolbar: [ { - label:__("Add Child"), - condition: function(node) { - return frappe.boot.user.can_create.indexOf("Account") !== -1 - && (!frappe.treeview_settings['Account'].treeview.page.fields_dict.root_company.get_value() - || frappe.flags.ignore_root_company_validation) - && node.expandable && !node.hide_add; + label: __("Add Child"), + condition: function (node) { + return ( + frappe.boot.user.can_create.indexOf("Account") !== -1 && + (!frappe.treeview_settings[ + "Account" + ].treeview.page.fields_dict.root_company.get_value() || + frappe.flags.ignore_root_company_validation) && + node.expandable && + !node.hide_add + ); }, - click: function() { - var me = frappe.views.trees['Account']; + click: function () { + var me = frappe.views.trees["Account"]; me.new_node(); }, - btnClass: "hidden-xs" + btnClass: "hidden-xs", }, { - condition: function(node) { - return !node.root && frappe.boot.user.can_read.indexOf("GL Entry") !== -1 + condition: function (node) { + return !node.root && frappe.boot.user.can_read.indexOf("GL Entry") !== -1; }, label: __("View Ledger"), - click: function(node, btn) { + click: function (node, btn) { frappe.route_options = { - "account": node.label, - "from_date": frappe.sys_defaults.year_start_date, - "to_date": frappe.sys_defaults.year_end_date, - "company": frappe.treeview_settings['Account'].treeview.page.fields_dict.company.get_value() + account: node.label, + from_date: frappe.sys_defaults.year_start_date, + to_date: frappe.sys_defaults.year_end_date, + company: + frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(), }; frappe.set_route("query-report", "General Ledger"); }, - btnClass: "hidden-xs" - } + btnClass: "hidden-xs", + }, ], - extend_toolbar: true -} + extend_toolbar: true, +}; diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 216135a3975..ea5db9dccb7 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -31,7 +31,6 @@ def create_charts( "tax_rate", "account_currency", ]: - account_number = cstr(child.get("account_number")).strip() account_name, account_name_in_db = add_suffix_if_duplicate( account_name, account_number, accounts @@ -39,7 +38,9 @@ def create_charts( is_group = identify_is_group(child) report_type = ( - "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] else "Profit and Loss" + "Balance Sheet" + if root_type in ["Asset", "Liability", "Equity"] + else "Profit and Loss" ) account = frappe.get_doc( @@ -141,7 +142,7 @@ def get_chart(chart_template, existing_company=None): for fname in os.listdir(path): fname = frappe.as_unicode(fname) if fname.endswith(".json"): - with open(os.path.join(path, fname), "r") as f: + with open(os.path.join(path, fname)) as f: chart = f.read() if chart and json.loads(chart).get("name") == chart_template: return json.loads(chart).get("tree") @@ -173,7 +174,7 @@ def get_charts_for_country(country, with_standard=False): for fname in os.listdir(path): fname = frappe.as_unicode(fname) if (fname.startswith(country_code) or fname.startswith(country)) and fname.endswith(".json"): - with open(os.path.join(path, fname), "r") as f: + with open(os.path.join(path, fname)) as f: _get_chart_name(f.read()) # if more than one charts, returned then add the standard @@ -247,7 +248,13 @@ def validate_bank_account(coa, bank_account): def _get_account_names(account_master): for account_name, child in account_master.items(): - if account_name not in ["account_number", "account_type", "root_type", "is_group", "tax_rate"]: + if account_name not in [ + "account_number", + "account_type", + "root_type", + "is_group", + "tax_rate", + ]: accounts.append(account_name) _get_account_names(child) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py b/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py index 3f25ada8b35..070447863f4 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py @@ -26,7 +26,7 @@ def go(): default_account_types = get_default_account_types() country_dirs = [] - for basepath, folders, files in os.walk(path): + for basepath, _folders, _files in os.walk(path): basename = os.path.basename(basepath) if basename.startswith("l10n_"): country_dirs.append(basename) @@ -35,9 +35,7 @@ def go(): accounts, charts = {}, {} country_path = os.path.join(path, country_dir) manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read()) - data_files = ( - manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", []) - ) + data_files = manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", []) files_path = [os.path.join(country_path, d) for d in data_files] xml_roots = get_xml_roots(files_path) csv_content = get_csv_contents(files_path) @@ -90,10 +88,10 @@ def get_csv_contents(files_path): fname = os.path.basename(filepath) for file_type in ["account.account.template", "account.account.type", "account.chart.template"]: if fname.startswith(file_type) and fname.endswith(".csv"): - with open(filepath, "r") as csvfile: + with open(filepath) as csvfile: try: csv_content.setdefault(file_type, []).append(read_csv_content(csvfile.read())) - except Exception as e: + except Exception: continue return csv_content @@ -138,7 +136,7 @@ def get_account_types(root_list, csv_content, prefix=None): if csv_content and csv_content[0][0] == "id": for row in csv_content[1:]: - row_dict = dict(zip(csv_content[0], row)) + row_dict = dict(zip(csv_content[0], row, strict=False)) data = {} if row_dict.get("code") and account_type_map.get(row_dict["code"]): data["account_type"] = account_type_map[row_dict["code"]] @@ -150,7 +148,7 @@ def get_account_types(root_list, csv_content, prefix=None): def make_maps_for_xml(xml_roots, account_types, country_dir): """make maps for `charts` and `accounts`""" - for model, root_list in xml_roots.items(): + for _model, root_list in xml_roots.items(): for root in root_list: for node in root[0].findall("record"): if node.get("model") == "account.account.template": @@ -186,7 +184,7 @@ def make_maps_for_xml(xml_roots, account_types, country_dir): def make_maps_for_csv(csv_content, account_types, country_dir): for content in csv_content.get("account.account.template", []): for row in content[1:]: - data = dict(zip(content[0], row)) + data = dict(zip(content[0], row, strict=False)) account = { "name": data.get("name"), "parent_id": data.get("parent_id:id") or data.get("parent_id/id"), @@ -206,7 +204,7 @@ def make_maps_for_csv(csv_content, account_types, country_dir): for content in csv_content.get("account.chart.template", []): for row in content[1:]: if row: - data = dict(zip(content[0], row)) + data = dict(zip(content[0], row, strict=False)) charts.setdefault(data.get("id"), {}).update( { "account_root_id": data.get("account_root_id:id") or data.get("account_root_id/id"), @@ -241,7 +239,7 @@ def make_charts(): if not src.get("name") or not src.get("account_root_id"): continue - if not src["account_root_id"] in accounts: + if src["account_root_id"] not in accounts: continue filename = src["id"][5:] + "_" + chart_id @@ -255,14 +253,20 @@ def make_charts(): for key, val in chart["tree"].items(): if key in ["name", "parent_id"]: chart["tree"].pop(key) - if type(val) == dict: + if isinstance(val, dict): val["root_type"] = "" if chart: fpath = os.path.join( - "erpnext", "erpnext", "accounts", "doctype", "account", "chart_of_accounts", filename + ".json" + "erpnext", + "erpnext", + "accounts", + "doctype", + "account", + "chart_of_accounts", + filename + ".json", ) - with open(fpath, "r") as chartfile: + with open(fpath) as chartfile: old_content = chartfile.read() if not old_content or ( json.loads(old_content).get("is_active", "No") == "No" diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index 741d4283e2f..daf2e21d78c 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -53,8 +53,13 @@ }, "II. Forderungen und sonstige Vermögensgegenstände": { "is_group": 1, - "Ford. a. Lieferungen und Leistungen": { + "Forderungen aus Lieferungen und Leistungen mit Kontokorrent": { "account_number": "1400", + "account_type": "Receivable", + "is_group": 1 + }, + "Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": { + "account_number": "1410", "account_type": "Receivable" }, "Durchlaufende Posten": { @@ -180,8 +185,13 @@ }, "IV. Verbindlichkeiten aus Lieferungen und Leistungen": { "is_group": 1, - "Verbindlichkeiten aus Lieferungen u. Leistungen": { + "Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": { "account_number": "1600", + "account_type": "Payable", + "is_group": 1 + }, + "Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": { + "account_number": "1610", "account_type": "Payable" } }, diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json index 2bf55cfcd04..000ef80ee38 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json @@ -407,13 +407,10 @@ "Bewertungskorrektur zu Forderungen aus Lieferungen und Leistungen": { "account_number": "9960" }, - "Debitoren": { - "is_group": 1, - "account_number": "10000" - }, - "Forderungen aus Lieferungen und Leistungen": { + "Forderungen aus Lieferungen und Leistungen mit Kontokorrent": { "account_number": "1200", - "account_type": "Receivable" + "account_type": "Receivable", + "is_group": 1 }, "Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": { "account_number": "1210" @@ -1138,18 +1135,15 @@ "Bewertungskorrektur zu Verb. aus Lieferungen und Leistungen": { "account_number": "9964" }, - "Kreditoren": { - "account_number": "70000", + "Verb. aus Lieferungen und Leistungen mit Kontokorrent": { + "account_number": "3300", + "account_type": "Payable", "is_group": 1, - "Wareneingangs-­Verrechnungskonto" : { + "Wareneingangs-Verrechnungskonto" : { "account_number": "70001", "account_type": "Stock Received But Not Billed" } }, - "Verb. aus Lieferungen und Leistungen": { - "account_number": "3300", - "account_type": "Payable" - }, "Verb. aus Lieferungen und Leistungen ohne Kontokorrent": { "account_number": "3310" }, diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 30eebef7fba..49d3f66e912 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -260,28 +260,20 @@ class TestAccount(unittest.TestCase): acc.insert() self.assertTrue( - frappe.db.exists( - "Account", {"account_name": "Test Group Account", "company": "_Test Company 4"} - ) + frappe.db.exists("Account", {"account_name": "Test Group Account", "company": "_Test Company 4"}) ) self.assertTrue( - frappe.db.exists( - "Account", {"account_name": "Test Group Account", "company": "_Test Company 5"} - ) + frappe.db.exists("Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}) ) # Try renaming child company account acc_tc_5 = frappe.db.get_value( "Account", {"account_name": "Test Group Account", "company": "_Test Company 5"} ) - self.assertRaises( - frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account" - ) + self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account") # Rename child company account with allow_account_creation_against_child_company enabled - frappe.db.set_value( - "Company", "_Test Company 5", "allow_account_creation_against_child_company", 1 - ) + frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1) update_account_number(acc_tc_5, "Test Modified Account") self.assertTrue( @@ -290,9 +282,7 @@ class TestAccount(unittest.TestCase): ) ) - frappe.db.set_value( - "Company", "_Test Company 5", "allow_account_creation_against_child_company", 0 - ) + frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0) to_delete = [ "Test Group Account - _TC3", @@ -317,9 +307,7 @@ class TestAccount(unittest.TestCase): self.assertEqual(acc.account_currency, "INR") # Make a JV against this account - make_journal_entry( - "Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True - ) + make_journal_entry("Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True) acc.account_currency = "USD" self.assertRaises(frappe.ValidationError, acc.save) 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 d06bd833c8b..f6f5506efc0 100644 --- a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py +++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py @@ -17,16 +17,12 @@ class AccountClosingBalance(Document): def make_closing_entries(closing_entries, voucher_name, company, closing_date): accounting_dimensions = get_accounting_dimensions() - previous_closing_entries = get_previous_closing_entries( - company, closing_date, accounting_dimensions - ) + previous_closing_entries = get_previous_closing_entries(company, closing_date, accounting_dimensions) combined_entries = closing_entries + previous_closing_entries - merged_entries = aggregate_with_last_account_closing_balance( - combined_entries, accounting_dimensions - ) + merged_entries = aggregate_with_last_account_closing_balance(combined_entries, accounting_dimensions) - for key, value in merged_entries.items(): + for _key, value in merged_entries.items(): cle = frappe.new_doc("Account Closing Balance") cle.update(value) cle.update(value["dimensions"]) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js index 2f53f7b640d..cd883e5bf72 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js @@ -1,74 +1,86 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Accounting Dimension', { - refresh: function(frm) { - frm.set_query('document_type', () => { +frappe.ui.form.on("Accounting Dimension", { + refresh: function (frm) { + frm.set_query("document_type", () => { let invalid_doctypes = frappe.model.core_doctypes_list; - invalid_doctypes.push('Accounting Dimension', 'Project', - 'Cost Center', 'Accounting Dimension Detail', 'Company'); + invalid_doctypes.push( + "Accounting Dimension", + "Project", + "Cost Center", + "Accounting Dimension Detail", + "Company" + ); return { filters: { - name: ['not in', invalid_doctypes] - } + name: ["not in", invalid_doctypes], + }, }; }); - frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) { + frm.set_query("offsetting_account", "dimension_defaults", function (doc, cdt, cdn) { let d = locals[cdt][cdn]; return { filters: { company: d.company, root_type: ["in", ["Asset", "Liability"]], - is_group: 0 - } - } + is_group: 0, + }, + }; }); if (!frm.is_new()) { - frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () { + frm.add_custom_button(__("Show {0}", [frm.doc.document_type]), function () { frappe.set_route("List", frm.doc.document_type); }); let button = frm.doc.disabled ? "Enable" : "Disable"; - frm.add_custom_button(__(button), function() { - - frm.set_value('disabled', 1 - frm.doc.disabled); + frm.add_custom_button(__(button), function () { + frm.set_value("disabled", 1 - frm.doc.disabled); frappe.call({ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.disable_dimension", args: { - doc: frm.doc + doc: frm.doc, }, freeze: true, - callback: function(r) { + callback: function (r) { let message = frm.doc.disabled ? "Dimension Disabled" : "Dimension Enabled"; frm.save(); - frappe.show_alert({message:__(message), indicator:'green'}); - } + frappe.show_alert({ message: __(message), indicator: "green" }); + }, }); }); } }, - document_type: function(frm) { + document_type: function (frm) { + frm.set_value("label", frm.doc.document_type); + frm.set_value("fieldname", frappe.model.scrub(frm.doc.document_type)); - frm.set_value('label', frm.doc.document_type); - frm.set_value('fieldname', frappe.model.scrub(frm.doc.document_type)); - - frappe.db.get_value('Accounting Dimension', {'document_type': frm.doc.document_type}, 'document_type', (r) => { - if (r && r.document_type) { - frm.set_df_property('document_type', 'description', "Document type is already set as dimension"); + frappe.db.get_value( + "Accounting Dimension", + { document_type: frm.doc.document_type }, + "document_type", + (r) => { + if (r && r.document_type) { + frm.set_df_property( + "document_type", + "description", + "Document type is already set as dimension" + ); + } } - }); + ); }, }); -frappe.ui.form.on('Accounting Dimension Detail', { - dimension_defaults_add: function(frm, cdt, cdn) { +frappe.ui.form.on("Accounting Dimension Detail", { + dimension_defaults_add: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; row.reference_document = frm.doc.document_type; - } + }, }); diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 8afd313322e..edddcb54df0 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -17,7 +17,8 @@ class AccountingDimension(Document): self.set_fieldname_and_label() def validate(self): - if self.document_type in core_doctypes_list + ( + if self.document_type in ( + *core_doctypes_list, "Accounting Dimension", "Project", "Cost Center", @@ -25,13 +26,10 @@ class AccountingDimension(Document): "Company", "Account", ): - msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) frappe.throw(msg) - exists = frappe.db.get_value( - "Accounting Dimension", {"document_type": self.document_type}, ["name"] - ) + exists = frappe.db.get_value("Accounting Dimension", {"document_type": self.document_type}, ["name"]) if exists and self.is_new(): frappe.throw(_("Document Type already used as a dimension")) @@ -89,7 +87,6 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None): count = 0 for doctype in doclist: - if (doc_count + 1) % 2 == 0: insert_after_field = "dimension_col_break" else: @@ -123,7 +120,7 @@ def add_dimension_to_budget_doctype(df, doc): df.update( { "insert_after": "cost_center", - "depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type), + "depends_on": f"eval:doc.budget_against == '{doc.document_type}'", } ) @@ -157,19 +154,17 @@ def delete_accounting_dimension(doc): frappe.db.sql( """ DELETE FROM `tabCustom Field` - WHERE fieldname = %s - AND dt IN (%s)""" - % ("%s", ", ".join(["%s"] * len(doclist))), # nosec - tuple([doc.fieldname] + doclist), + WHERE fieldname = {} + AND dt IN ({})""".format("%s", ", ".join(["%s"] * len(doclist))), # nosec + tuple([doc.fieldname, *doclist]), ) frappe.db.sql( """ DELETE FROM `tabProperty Setter` - WHERE field_name = %s - AND doc_type IN (%s)""" - % ("%s", ", ".join(["%s"] * len(doclist))), # nosec - tuple([doc.fieldname] + doclist), + WHERE field_name = {} + AND doc_type IN ({})""".format("%s", ", ".join(["%s"] * len(doclist))), # nosec + tuple([doc.fieldname, *doclist]), ) budget_against_property = frappe.get_doc("Property Setter", "Budget-budget_against-options") @@ -218,7 +213,6 @@ def get_doctypes_with_dimensions(): def get_accounting_dimensions(as_list=True, filters=None): - if not filters: filters = {"disabled": 0} @@ -236,18 +230,19 @@ def get_accounting_dimensions(as_list=True, filters=None): def get_checks_for_pl_and_bs_accounts(): - dimensions = frappe.db.sql( - """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs - FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c - WHERE p.name = c.parent""", - as_dict=1, - ) + if frappe.flags.accounting_dimensions_details is None: + # nosemgrep + frappe.flags.accounting_dimensions_details = frappe.db.sql( + """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs + FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c + WHERE p.name = c.parent""", + as_dict=1, + ) - return dimensions + return frappe.flags.accounting_dimensions_details def get_dimension_with_children(doctype, dimensions): - if isinstance(dimensions, str): dimensions = [dimensions] @@ -255,9 +250,7 @@ def get_dimension_with_children(doctype, dimensions): for dimension in dimensions: lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"]) - children = frappe.get_all( - doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft" - ) + children = frappe.get_all(doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft") all_dimensions += [c.name for c in children] return all_dimensions diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 25ef2ea5c2c..2ceaffc4e25 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -78,6 +78,8 @@ class TestAccountingDimension(unittest.TestCase): def tearDown(self): disable_dimension() + frappe.flags.accounting_dimensions_details = None + frappe.flags.dimension_filter_map = None def create_dimension(): diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 8a6b021b8ad..7f655967b8e 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -1,10 +1,9 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Accounting Dimension Filter', { - refresh: function(frm, cdt, cdn) { - let help_content = - ` +frappe.ui.form.on("Accounting Dimension Filter", { + refresh: function (frm, cdt, cdn) { + let help_content = `

@@ -13,67 +12,70 @@ frappe.ui.form.on('Accounting Dimension Filter', {

`; - frm.set_df_property('dimension_filter_help', 'options', help_content); + frm.set_df_property("dimension_filter_help", "options", help_content); }, - onload: function(frm) { - frm.set_query('applicable_on_account', 'accounts', function() { + onload: function (frm) { + frm.set_query("applicable_on_account", "accounts", function () { return { filters: { - 'company': frm.doc.company - } + company: frm.doc.company, + }, }; }); - frappe.db.get_list('Accounting Dimension', - {fields: ['document_type']}).then((res) => { - let options = ['Cost Center', 'Project']; + frappe.db.get_list("Accounting Dimension", { fields: ["document_type"] }).then((res) => { + let options = ["Cost Center", "Project"]; res.forEach((dimension) => { options.push(dimension.document_type); }); - frm.set_df_property('accounting_dimension', 'options', options); + frm.set_df_property("accounting_dimension", "options", options); }); - frm.trigger('setup_filters'); + frm.trigger("setup_filters"); }, - setup_filters: function(frm) { + setup_filters: function (frm) { let filters = {}; if (frm.doc.accounting_dimension) { - frappe.model.with_doctype(frm.doc.accounting_dimension, function() { + frappe.model.with_doctype(frm.doc.accounting_dimension, function () { if (frappe.model.is_tree(frm.doc.accounting_dimension)) { - filters['is_group'] = 0; + filters["is_group"] = 0; } - if (frappe.meta.has_field(frm.doc.accounting_dimension, 'company')) { - filters['company'] = frm.doc.company; + if (frappe.meta.has_field(frm.doc.accounting_dimension, "company")) { + filters["company"] = frm.doc.company; } - frm.set_query('dimension_value', 'dimensions', function() { + frm.set_query("dimension_value", "dimensions", function () { return { - filters: filters + filters: filters, }; }); }); } }, - accounting_dimension: function(frm) { + accounting_dimension: function (frm) { frm.clear_table("dimensions"); let row = frm.add_child("dimensions"); row.accounting_dimension = frm.doc.accounting_dimension; - frm.fields_dict["dimensions"].grid.update_docfield_property("dimension_value", "label", frm.doc.accounting_dimension); + frm.fields_dict["dimensions"].grid.update_docfield_property( + "dimension_value", + "label", + frm.doc.accounting_dimension + ); frm.refresh_field("dimensions"); - frm.trigger('setup_filters'); + frm.trigger("setup_filters"); }, }); -frappe.ui.form.on('Allowed Dimension', { - dimensions_add: function(frm, cdt, cdn) { +frappe.ui.form.on("Allowed Dimension", { + dimensions_add: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; row.accounting_dimension = frm.doc.accounting_dimension; frm.refresh_field("dimensions"); - } + }, }); diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 80f736fa5bb..65262764294 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -38,37 +38,41 @@ class AccountingDimensionFilter(Document): def get_dimension_filter_map(): - filters = frappe.db.sql( - """ - SELECT - a.applicable_on_account, d.dimension_value, p.accounting_dimension, - p.allow_or_restrict, a.is_mandatory - FROM - `tabApplicable On Account` a, `tabAllowed Dimension` d, - `tabAccounting Dimension Filter` p - WHERE - p.name = a.parent - AND p.disabled = 0 - AND p.name = d.parent - """, - as_dict=1, - ) - - dimension_filter_map = {} - - for f in filters: - f.fieldname = scrub(f.accounting_dimension) - - build_map( - dimension_filter_map, - f.fieldname, - f.applicable_on_account, - f.dimension_value, - f.allow_or_restrict, - f.is_mandatory, + if not frappe.flags.get("dimension_filter_map"): + # nosemgrep + filters = frappe.db.sql( + """ + SELECT + a.applicable_on_account, d.dimension_value, p.accounting_dimension, + p.allow_or_restrict, a.is_mandatory + FROM + `tabApplicable On Account` a, `tabAllowed Dimension` d, + `tabAccounting Dimension Filter` p + WHERE + p.name = a.parent + AND p.disabled = 0 + AND p.name = d.parent + """, + as_dict=1, ) - return dimension_filter_map + dimension_filter_map = {} + + for f in filters: + f.fieldname = scrub(f.accounting_dimension) + + build_map( + dimension_filter_map, + f.fieldname, + f.applicable_on_account, + f.dimension_value, + f.allow_or_restrict, + f.is_mandatory, + ) + + frappe.flags.dimension_filter_map = dimension_filter_map + + return frappe.flags.dimension_filter_map def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory): diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index f13f2f9f279..50ca0626783 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -47,6 +47,8 @@ class TestAccountingDimensionFilter(unittest.TestCase): def tearDown(self): disable_dimension_filter() disable_dimension() + frappe.flags.accounting_dimensions_details = None + frappe.flags.dimension_filter_map = None for si in self.invoice_list: si.load_from_db() @@ -55,9 +57,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): def create_accounting_dimension_filter(): - if not frappe.db.get_value( - "Accounting Dimension Filter", {"accounting_dimension": "Cost Center"} - ): + if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}): frappe.get_doc( { "doctype": "Accounting Dimension Filter", diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.js b/erpnext/accounts/doctype/accounting_period/accounting_period.js index f17b6f9c695..441471f8c06 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.js +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.js @@ -1,30 +1,33 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Accounting Period', { - onload: function(frm) { - if(frm.doc.closed_documents.length === 0 || (frm.doc.closed_documents.length === 1 && frm.doc.closed_documents[0].document_type == undefined)) { +frappe.ui.form.on("Accounting Period", { + onload: function (frm) { + if ( + frm.doc.closed_documents.length === 0 || + (frm.doc.closed_documents.length === 1 && frm.doc.closed_documents[0].document_type == undefined) + ) { frappe.call({ method: "get_doctypes_for_closing", - doc:frm.doc, - callback: function(r) { - if(r.message) { + doc: frm.doc, + callback: function (r) { + if (r.message) { cur_frm.clear_table("closed_documents"); - r.message.forEach(function(element) { + r.message.forEach(function (element) { var c = frm.add_child("closed_documents"); c.document_type = element.document_type; c.closed = element.closed; }); refresh_field("closed_documents"); } - } + }, }); } 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 d5f37a68067..7bb36df3bea 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py @@ -67,7 +67,10 @@ class AccountingPeriod(Document): for doctype_for_closing in self.get_doctypes_for_closing(): self.append( "closed_documents", - {"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed}, + { + "document_type": doctype_for_closing.document_type, + "closed": doctype_for_closing.closed, + }, ) diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py index 41d94797ad6..16cae9683f9 100644 --- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py @@ -34,9 +34,7 @@ class TestAccountingPeriod(unittest.TestCase): ap1 = create_accounting_period(period_name="Test Accounting Period 2") ap1.save() - doc = create_sales_invoice( - do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC" - ) + doc = create_sales_invoice(do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC") self.assertRaises(ClosedAccountingPeriod, doc.save) def tearDown(self): diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js index 0627675de79..5b9a52e8f8b 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Accounts Settings', { - refresh: function(frm) { - - } +frappe.ui.form.on("Accounts Settings", { + refresh: function (frm) {}, }); diff --git a/erpnext/accounts/doctype/accounts_settings/regional/united_states.js b/erpnext/accounts/doctype/accounts_settings/regional/united_states.js index 3e38386481c..a522de9da75 100644 --- a/erpnext/accounts/doctype/accounts_settings/regional/united_states.js +++ b/erpnext/accounts/doctype/accounts_settings/regional/united_states.js @@ -1,8 +1,11 @@ - -frappe.ui.form.on('Accounts Settings', { - refresh: function(frm) { +frappe.ui.form.on("Accounts Settings", { + refresh: function (frm) { frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through"); - frm.set_df_property("frozen_accounts_modifier", "label", "Role Allowed to Close Books & Make Changes to Closed Periods"); + frm.set_df_property( + "frozen_accounts_modifier", + "label", + "Role Allowed to Close Books & Make Changes to Closed Periods" + ); frm.set_df_property("credit_controller", "label", "Credit Manager"); - } + }, }); diff --git a/erpnext/accounts/doctype/allowed_to_transact_with/allowed_to_transact_with.json b/erpnext/accounts/doctype/allowed_to_transact_with/allowed_to_transact_with.json index e3f2d59c065..234ffc8a870 100644 --- a/erpnext/accounts/doctype/allowed_to_transact_with/allowed_to_transact_with.json +++ b/erpnext/accounts/doctype/allowed_to_transact_with/allowed_to_transact_with.json @@ -11,6 +11,7 @@ { "fieldname": "company", "fieldtype": "Link", + "ignore_user_permissions": 1, "in_list_view": 1, "label": "Company", "options": "Company", @@ -19,7 +20,7 @@ ], "istable": 1, "links": [], - "modified": "2020-05-01 12:32:34.044911", + "modified": "2024-01-03 11:13:02.669632", "modified_by": "Administrator", "module": "Accounts", "name": "Allowed To Transact With", @@ -28,5 +29,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 35d606ba3ae..0e186af41ba 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -1,41 +1,39 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.provide('erpnext.integrations'); +frappe.provide("erpnext.integrations"); -frappe.ui.form.on('Bank', { - onload: function(frm) { +frappe.ui.form.on("Bank", { + onload: function (frm) { add_fields_to_mapping_table(frm); }, - refresh: function(frm) { + refresh: function (frm) { add_fields_to_mapping_table(frm); - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Bank' }; + frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: "Bank" }; - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); + frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal); if (frm.doc.__islocal) { - frm.set_df_property('address_and_contact', 'hidden', 1); + frm.set_df_property("address_and_contact", "hidden", 1); frappe.contacts.clear_address_and_contact(frm); - } - else { - frm.set_df_property('address_and_contact', 'hidden', 0); + } else { + frm.set_df_property("address_and_contact", "hidden", 0); frappe.contacts.render_address_and_contact(frm); } if (frm.doc.plaid_access_token) { - frm.add_custom_button(__('Refresh Plaid Link'), () => { + frm.add_custom_button(__("Refresh Plaid Link"), () => { new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token); }); } - } + }, }); - let add_fields_to_mapping_table = function (frm) { let options = []; - frappe.model.with_doctype("Bank Transaction", function() { + frappe.model.with_doctype("Bank Transaction", function () { let meta = frappe.get_meta("Bank Transaction"); - meta.fields.forEach(value => { + meta.fields.forEach((value) => { if (!["Section Break", "Column Break"].includes(value.fieldtype)) { options.push(value.fieldname); } @@ -43,30 +41,32 @@ let add_fields_to_mapping_table = function (frm) { }); frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property( - 'bank_transaction_field', 'options', options + "bank_transaction_field", + "options", + options ); }; erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { constructor(access_token) { this.access_token = access_token; - this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; + this.plaidUrl = "https://cdn.plaid.com/link/v2/stable/link-initialize.js"; this.init_config(); } async init_config() { - this.plaid_env = await frappe.db.get_single_value('Plaid Settings', 'plaid_env'); + this.plaid_env = await frappe.db.get_single_value("Plaid Settings", "plaid_env"); this.token = await this.get_link_token_for_update(); this.init_plaid(); } async get_link_token_for_update() { const token = frappe.xcall( - 'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update', + "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update", { access_token: this.access_token } - ) + ); if (!token) { - frappe.throw(__('Cannot retrieve link token for update. Check Error Log for more information')); + frappe.throw(__("Cannot retrieve link token for update. Check Error Log for more information")); } return token; } @@ -93,13 +93,13 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { resolve(); return; } - const el = document.createElement('script'); - el.type = 'text/javascript'; + const el = document.createElement("script"); + el.type = "text/javascript"; el.async = true; el.src = src; - el.addEventListener('load', resolve); - el.addEventListener('error', reject); - el.addEventListener('abort', reject); + el.addEventListener("load", resolve); + el.addEventListener("error", reject); + el.addEventListener("abort", reject); document.head.appendChild(el); }); } @@ -108,20 +108,29 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { me.linkHandler = Plaid.create({ env: me.plaid_env, token: me.token, - onSuccess: me.plaid_success + onSuccess: me.plaid_success, }); } onScriptError(error) { - frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information")); + frappe.msgprint( + __( + "There was an issue connecting to Plaid's authentication server. Check browser console for more information" + ) + ); console.log(error); } plaid_success(token, response) { - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', { - response: response, - }).then(() => { - frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); - }); + frappe + .xcall( + "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids", + { + response: response, + } + ) + .then(() => { + frappe.show_alert({ message: __("Plaid Link Updated"), indicator: "green" }); + }); } }; diff --git a/erpnext/accounts/doctype/bank_account/bank_account.js b/erpnext/accounts/doctype/bank_account/bank_account.js index 0598190b518..202f750fb50 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.js +++ b/erpnext/accounts/doctype/bank_account/bank_account.js @@ -1,45 +1,49 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Bank Account', { - setup: function(frm) { - frm.set_query("account", function() { +frappe.ui.form.on("Bank Account", { + setup: function (frm) { + frm.set_query("account", function () { return { filters: { - 'account_type': 'Bank', - 'company': frm.doc.company, - 'is_group': 0 - } + account_type: "Bank", + company: frm.doc.company, + is_group: 0, + }, }; }); - frm.set_query("party_type", function() { + frm.set_query("party_type", function () { return { query: "erpnext.setup.doctype.party_type.party_type.get_party_type", }; }); }, - refresh: function(frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Bank Account' } + refresh: function (frm) { + frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: "Bank Account" }; - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); + frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal); if (frm.doc.__islocal) { frappe.contacts.clear_address_and_contact(frm); - } - else { + } else { frappe.contacts.render_address_and_contact(frm); } if (frm.doc.integration_id) { - frm.add_custom_button(__("Unlink external integrations"), function() { - frappe.confirm(__("This action will unlink this account from any external service integrating ERPNext with your bank accounts. It cannot be undone. Are you certain ?"), function() { - frm.set_value("integration_id", ""); - }); + frm.add_custom_button(__("Unlink external integrations"), function () { + frappe.confirm( + __( + "This action will unlink this account from any external service integrating ERPNext with your bank accounts. It cannot be undone. Are you certain ?" + ), + function () { + frm.set_value("integration_id", ""); + } + ); }); } }, - is_company_account: function(frm) { - frm.set_df_property('account', 'reqd', frm.doc.is_company_account); - } + is_company_account: function (frm) { + frm.set_df_property("account", "reqd", frm.doc.is_company_account); + }, }); diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json index 41d79479ca5..b1d53dc1e70 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.json +++ b/erpnext/accounts/doctype/bank_account/bank_account.json @@ -13,6 +13,7 @@ "account_type", "account_subtype", "column_break_7", + "disabled", "is_default", "is_company_account", "company", @@ -199,10 +200,16 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Branch Code" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "links": [], - "modified": "2022-05-04 15:49:42.620630", + "modified": "2024-02-02 17:50:09.768835", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Account", diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index addcf62e5b6..28a4a41e28e 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -9,6 +9,7 @@ from frappe.contacts.address_and_contact import ( load_address_and_contact, ) from frappe.model.document import Document +from frappe.utils import comma_and, get_link_to_form class BankAccount(Document): @@ -25,6 +26,19 @@ class BankAccount(Document): def validate(self): self.validate_company() self.validate_iban() + self.validate_account() + + def validate_account(self): + if self.account: + if accounts := frappe.db.get_all( + "Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1 + ): + frappe.throw( + _("'{0}' account is already used by {1}. Use another account.").format( + frappe.bold(self.account), + frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])), + ) + ) def validate_company(self): if self.is_company_account and not self.company: diff --git a/erpnext/accounts/doctype/bank_account/test_bank_account.py b/erpnext/accounts/doctype/bank_account/test_bank_account.py index 8949524a561..0ec388d9e5c 100644 --- a/erpnext/accounts/doctype/bank_account/test_bank_account.py +++ b/erpnext/accounts/doctype/bank_account/test_bank_account.py @@ -37,11 +37,11 @@ class TestBankAccount(unittest.TestCase): try: bank_account.validate_iban() except ValidationError: - msg = "BankAccount.validate_iban() failed for valid IBAN {}".format(iban) + msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}" self.fail(msg=msg) for not_iban in invalid_ibans: bank_account.iban = not_iban - msg = "BankAccount.validate_iban() accepted invalid IBAN {}".format(not_iban) + msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}" with self.assertRaises(ValidationError, msg=msg): bank_account.validate_iban() diff --git a/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.js b/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.js index f0456651c8f..e8fe4e0b9bd 100644 --- a/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.js +++ b/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Bank Account Subtype', { - refresh: function() { - - } +frappe.ui.form.on("Bank Account Subtype", { + refresh: function () {}, }); diff --git a/erpnext/accounts/doctype/bank_account_type/bank_account_type.js b/erpnext/accounts/doctype/bank_account_type/bank_account_type.js index 4cfabe3d1d1..1285fe4866f 100644 --- a/erpnext/accounts/doctype/bank_account_type/bank_account_type.js +++ b/erpnext/accounts/doctype/bank_account_type/bank_account_type.js @@ -1,8 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Bank Account Type', { +frappe.ui.form.on("Bank Account Type", { // refresh: function(frm) { - // } }); diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js index 71f2dcca1b2..ddf7bc5dec6 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js @@ -2,80 +2,76 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Bank Clearance", { - setup: function(frm) { + setup: function (frm) { frm.add_fetch("account", "account_currency", "account_currency"); - frm.set_query("account", function() { + frm.set_query("account", function () { return { - "filters": { - "account_type": ["in",["Bank","Cash"]], - "is_group": 0, - } + filters: { + account_type: ["in", ["Bank", "Cash"]], + is_group: 0, + }, }; }); frm.set_query("bank_account", function () { return { filters: { - 'is_company_account': 1 + is_company_account: 1, }, }; }); }, - onload: function(frm) { - - let default_bank_account = frappe.defaults.get_user_default("Company")? - locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: ""; + onload: function (frm) { + let default_bank_account = frappe.defaults.get_user_default("Company") + ? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"] + : ""; frm.set_value("account", default_bank_account); - - frm.set_value("from_date", frappe.datetime.month_start()); frm.set_value("to_date", frappe.datetime.month_end()); }, - refresh: function(frm) { + refresh: function (frm) { frm.disable_save(); - frm.add_custom_button(__('Get Payment Entries'), () => - frm.trigger("get_payment_entries") - ); + frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries")); - frm.change_custom_button_type('Get Payment Entries', null, 'primary'); + frm.change_custom_button_type("Get Payment Entries", null, "primary"); }, - update_clearance_date: function(frm) { + update_clearance_date: function (frm) { return frappe.call({ method: "update_clearance_date", doc: frm.doc, - callback: function(r, rt) { + callback: function (r, rt) { frm.refresh_field("payment_entries"); frm.refresh_fields(); if (!frm.doc.payment_entries.length) { - frm.change_custom_button_type('Get Payment Entries', null, 'primary'); - frm.change_custom_button_type('Update Clearance Date', null, 'default'); + frm.change_custom_button_type("Get Payment Entries", null, "primary"); + frm.change_custom_button_type("Update Clearance Date", null, "default"); } - } + }, }); }, - get_payment_entries: function(frm) { + get_payment_entries: function (frm) { return frappe.call({ method: "get_payment_entries", doc: frm.doc, - callback: function(r, rt) { + callback: function (r, rt) { frm.refresh_field("payment_entries"); if (frm.doc.payment_entries.length) { - frm.add_custom_button(__('Update Clearance Date'), () => + frm.add_custom_button(__("Update Clearance Date"), () => frm.trigger("update_clearance_date") ); - frm.change_custom_button_type('Get Payment Entries', null, 'default'); - frm.change_custom_button_type('Update Clearance Date', null, 'primary'); + frm.change_custom_button_type("Get Payment Entries", null, "default"); + frm.change_custom_button_type("Update Clearance Date", null, "primary"); } - } + }, }); - } + }, }); diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 8ad0bd17b48..6445deba6c3 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -27,7 +27,7 @@ class BankClearance(Document): condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')" journal_entries = frappe.db.sql( - """ + f""" select "Journal Entry" as payment_document, t1.name as payment_entry, t1.cheque_no as cheque_number, t1.cheque_date, @@ -41,9 +41,7 @@ class BankClearance(Document): and ifnull(t1.is_opening, 'No') = 'No' {condition} group by t2.account, t1.name order by t1.posting_date ASC, t1.name DESC - """.format( - condition=condition - ), + """, {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1, ) @@ -52,7 +50,7 @@ class BankClearance(Document): condition += "and bank_account = %(bank_account)s" payment_entries = frappe.db.sql( - """ + f""" select "Payment Entry" as payment_document, name as payment_entry, reference_no as cheque_number, reference_date as cheque_date, @@ -67,9 +65,7 @@ class BankClearance(Document): {condition} order by posting_date ASC, name DESC - """.format( - condition=condition - ), + """, { "account": self.account, "from": self.from_date, @@ -132,11 +128,9 @@ class BankClearance(Document): query = query.where(loan_repayment.clearance_date.isnull()) if frappe.db.has_column("Loan Repayment", "repay_from_salary"): - query = query.where((loan_repayment.repay_from_salary == 0)) + query = query.where(loan_repayment.repay_from_salary == 0) - query = query.orderby(loan_repayment.posting_date).orderby( - loan_repayment.name, order=frappe.qb.desc - ) + query = query.orderby(loan_repayment.posting_date).orderby(loan_repayment.name, order=frappe.qb.desc) loan_repayments = query.run(as_dict=True) diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js index 99cc0a72fb3..060c4b5edaa 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js @@ -1,39 +1,39 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -cur_frm.add_fetch('bank_account','account','account'); -cur_frm.add_fetch('bank_account','bank_account_no','bank_account_no'); -cur_frm.add_fetch('bank_account','iban','iban'); -cur_frm.add_fetch('bank_account','branch_code','branch_code'); -cur_frm.add_fetch('bank','swift_number','swift_number'); +cur_frm.add_fetch("bank_account", "account", "account"); +cur_frm.add_fetch("bank_account", "bank_account_no", "bank_account_no"); +cur_frm.add_fetch("bank_account", "iban", "iban"); +cur_frm.add_fetch("bank_account", "branch_code", "branch_code"); +cur_frm.add_fetch("bank", "swift_number", "swift_number"); -frappe.ui.form.on('Bank Guarantee', { - setup: function(frm) { - frm.set_query("bank", function() { - return { - filters: { - company: frm.doc.company - } - }; - }); - frm.set_query("bank_account", function() { +frappe.ui.form.on("Bank Guarantee", { + setup: function (frm) { + frm.set_query("bank", function () { return { filters: { company: frm.doc.company, - bank: frm.doc.bank - } - } + }, + }; }); - frm.set_query("project", function() { + frm.set_query("bank_account", function () { return { filters: { - customer: frm.doc.customer - } + company: frm.doc.company, + bank: frm.doc.bank, + }, + }; + }); + frm.set_query("project", function () { + return { + filters: { + customer: frm.doc.customer, + }, }; }); }, - bg_type: function(frm) { + bg_type: function (frm) { if (frm.doc.bg_type == "Receiving") { frm.set_value("reference_doctype", "Sales Order"); } else if (frm.doc.bg_type == "Providing") { @@ -41,34 +41,33 @@ frappe.ui.form.on('Bank Guarantee', { } }, - reference_docname: function(frm) { + reference_docname: function (frm) { if (frm.doc.reference_docname && frm.doc.reference_doctype) { let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier"; frappe.call({ method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_voucher_details", args: { - "bank_guarantee_type": frm.doc.bg_type, - "reference_name": frm.doc.reference_docname + bank_guarantee_type: frm.doc.bg_type, + reference_name: frm.doc.reference_docname, }, - callback: function(r) { + callback: function (r) { if (r.message) { if (r.message[party_field]) frm.set_value(party_field, r.message[party_field]); if (r.message.project) frm.set_value("project", r.message.project); if (r.message.grand_total) frm.set_value("amount", r.message.grand_total); } - } + }, }); - } }, - start_date: function(frm) { + start_date: function (frm) { var end_date = frappe.datetime.add_days(cur_frm.doc.start_date, cur_frm.doc.validity - 1); cur_frm.set_value("end_date", end_date); }, - validity: function(frm) { + validity: function (frm) { var end_date = frappe.datetime.add_days(cur_frm.doc.start_date, cur_frm.doc.validity - 1); cur_frm.set_value("end_date", end_date); - } + }, }); diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js index d9cf1bb3b34..10c0781e1d3 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -8,21 +8,22 @@ frappe.ui.form.on("Bank Reconciliation Tool", { return { filters: { company: frm.doc.company, - 'is_company_account': 1 + is_company_account: 1, }, }; }); - let no_bank_transactions_text = - `
${__("No Matching Bank Transactions Found")}
` + let no_bank_transactions_text = `
${__( + "No Matching Bank Transactions Found" + )}
`; set_field_options("no_bank_transactions", no_bank_transactions_text); }, onload: function (frm) { // Set default filter dates - let today = frappe.datetime.get_today() + let today = frappe.datetime.get_today(); frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1); frm.doc.bank_statement_to_date = today; - frm.trigger('bank_account'); + frm.trigger("bank_account"); }, filter_by_reference_date: function (frm) { @@ -37,34 +38,27 @@ frappe.ui.form.on("Bank Reconciliation Tool", { refresh: function (frm) { frm.disable_save(); - frappe.require("bank-reconciliation-tool.bundle.js", () => - frm.trigger("make_reconciliation_tool") - ); + frappe.require("bank-reconciliation-tool.bundle.js", () => frm.trigger("make_reconciliation_tool")); frm.add_custom_button(__("Upload Bank Statement"), () => - frappe.call({ - method: - "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement", - args: { - dt: frm.doc.doctype, - dn: frm.doc.name, - company: frm.doc.company, - bank_account: frm.doc.bank_account, - }, - callback: function (r) { - if (!r.exc) { - var doc = frappe.model.sync(r.message); - frappe.set_route( - "Form", - doc[0].doctype, - doc[0].name - ); - } - }, - }) + frappe.call({ + method: "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement", + args: { + dt: frm.doc.doctype, + dn: frm.doc.name, + company: frm.doc.company, + bank_account: frm.doc.bank_account, + }, + callback: function (r) { + if (!r.exc) { + var doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + }, + }) ); - frm.add_custom_button(__('Auto Reconcile'), function() { + frm.add_custom_button(__("Auto Reconcile"), function () { frappe.call({ method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers", args: { @@ -75,33 +69,22 @@ frappe.ui.form.on("Bank Reconciliation Tool", { from_reference_date: frm.doc.from_reference_date, to_reference_date: frm.doc.to_reference_date, }, - }) + }); }); - frm.add_custom_button(__('Get Unreconciled Entries'), function() { + frm.add_custom_button(__("Get Unreconciled Entries"), function () { frm.trigger("make_reconciliation_tool"); }); - frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary'); - + frm.change_custom_button_type("Get Unreconciled Entries", null, "primary"); }, bank_account: function (frm) { - frappe.db.get_value( - "Bank Account", - frm.doc.bank_account, - "account", - (r) => { - frappe.db.get_value( - "Account", - r.account, - "account_currency", - (r) => { - frm.doc.account_currency = r.account_currency; - frm.trigger("render_chart"); - } - ); - } - ); + frappe.db.get_value("Bank Account", frm.doc.bank_account, "account", (r) => { + frappe.db.get_value("Account", r.account, "account_currency", (r) => { + frm.doc.account_currency = r.account_currency; + frm.trigger("render_chart"); + }); + }); frm.trigger("get_account_opening_balance"); }, @@ -120,11 +103,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { ) { frm.trigger("render_chart"); frm.trigger("render"); - frappe.utils.scroll_to( - frm.get_field("reconciliation_tool_cards").$wrapper, - true, - 30 - ); + frappe.utils.scroll_to(frm.get_field("reconciliation_tool_cards").$wrapper, true, 30); } }); } @@ -133,11 +112,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", { get_account_opening_balance(frm) { if (frm.doc.bank_account && frm.doc.bank_statement_from_date) { frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance", args: { bank_account: frm.doc.bank_account, - till_date: frm.doc.bank_statement_from_date, + till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1), }, callback: (response) => { frm.set_value("account_opening_balance", response.message); @@ -149,8 +127,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { get_cleared_balance(frm) { if (frm.doc.bank_account && frm.doc.bank_statement_to_date) { return frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance", args: { bank_account: frm.doc.bank_account, till_date: frm.doc.bank_statement_to_date, @@ -163,41 +140,30 @@ frappe.ui.form.on("Bank Reconciliation Tool", { }, render_chart(frm) { - frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager( - { - $reconciliation_tool_cards: frm.get_field( - "reconciliation_tool_cards" - ).$wrapper, - bank_statement_closing_balance: - frm.doc.bank_statement_closing_balance, - cleared_balance: frm.cleared_balance, - currency: frm.doc.account_currency, - } - ); + frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager({ + $reconciliation_tool_cards: frm.get_field("reconciliation_tool_cards").$wrapper, + bank_statement_closing_balance: frm.doc.bank_statement_closing_balance, + cleared_balance: frm.cleared_balance, + currency: frm.doc.account_currency, + }); }, render(frm) { if (frm.doc.bank_account) { - frm.bank_reconciliation_data_table_manager = new erpnext.accounts.bank_reconciliation.DataTableManager( - { + frm.bank_reconciliation_data_table_manager = + new erpnext.accounts.bank_reconciliation.DataTableManager({ company: frm.doc.company, bank_account: frm.doc.bank_account, - $reconciliation_tool_dt: frm.get_field( - "reconciliation_tool_dt" - ).$wrapper, - $no_bank_transactions: frm.get_field( - "no_bank_transactions" - ).$wrapper, + $reconciliation_tool_dt: frm.get_field("reconciliation_tool_dt").$wrapper, + $no_bank_transactions: frm.get_field("no_bank_transactions").$wrapper, bank_statement_from_date: frm.doc.bank_statement_from_date, bank_statement_to_date: frm.doc.bank_statement_to_date, filter_by_reference_date: frm.doc.filter_by_reference_date, from_reference_date: frm.doc.from_reference_date, to_reference_date: frm.doc.to_reference_date, - bank_statement_closing_balance: - frm.doc.bank_statement_closing_balance, + bank_statement_closing_balance: frm.doc.bank_statement_closing_balance, cards_manager: frm.cards_manager, - } - ); + }); } }, }); diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 6017c5a491d..2987bac677c 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -61,9 +61,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None): def get_account_balance(bank_account, till_date): # returns account balance till the specified date account = frappe.db.get_value("Bank Account", bank_account, "account") - filters = frappe._dict( - {"account": account, "report_date": till_date, "include_pos_transactions": 1} - ) + filters = frappe._dict({"account": account, "report_date": till_date, "include_pos_transactions": 1}) data = get_entries(filters) balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) @@ -76,10 +74,7 @@ def get_account_balance(bank_account, till_date): amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters) bank_bal = ( - flt(balance_as_per_system) - - flt(total_debit) - + flt(total_credit) - + amounts_not_reflected_in_system + flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system ) return bank_bal @@ -377,12 +372,13 @@ def auto_reconcile_vouchers( ) transaction = frappe.get_doc("Bank Transaction", transaction.name) account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") - matched_trans = 0 for voucher in vouchers: gl_entry = frappe.db.get_value( "GL Entry", dict( - account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"] + account=account, + voucher_type=voucher["payment_doctype"], + voucher_no=voucher["payment_name"], ), ["credit", "debit"], as_dict=1, @@ -731,7 +727,7 @@ def get_lr_matching_query(bank_account, exact_match, filters): ) if frappe.db.has_column("Loan Repayment", "repay_from_salary"): - query = query.where((loan_repayment.repay_from_salary == 0)) + query = query.where(loan_repayment.repay_from_salary == 0) if exact_match: query.where(loan_repayment.amount_paid == filters.get("amount")) @@ -764,7 +760,7 @@ def get_pe_matching_query( if cint(filter_by_reference_date): filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'" order_by = " reference_date" - if frappe.flags.auto_reconcile_vouchers == True: + if frappe.flags.auto_reconcile_vouchers is True: filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'" return f""" SELECT @@ -815,7 +811,7 @@ def get_je_matching_query( if cint(filter_by_reference_date): filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'" order_by = " je.cheque_date" - if frappe.flags.auto_reconcile_vouchers == True: + if frappe.flags.auto_reconcile_vouchers is True: filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'" return f""" SELECT diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js index db68dfad79e..c4b442070d2 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -17,11 +17,9 @@ frappe.ui.form.on("Bank Statement Import", { frm.import_in_progress = false; if (data_import !== frm.doc.name) return; frappe.model.clear_doc("Bank Statement Import", frm.doc.name); - frappe.model - .with_doc("Bank Statement Import", frm.doc.name) - .then(() => { - frm.refresh(); - }); + frappe.model.with_doc("Bank Statement Import", frm.doc.name).then(() => { + frm.refresh(); + }); }); frappe.realtime.on("data_import_progress", (data) => { frm.import_in_progress = true; @@ -48,20 +46,9 @@ frappe.ui.form.on("Bank Statement Import", { : __("Updating {0} of {1}, {2}", message_args); } if (data.skipping) { - message = __( - "Skipping {0} of {1}, {2}", - [ - data.current, - data.total, - eta_message, - ] - ); + message = __("Skipping {0} of {1}, {2}", [data.current, data.total, eta_message]); } - frm.dashboard.show_progress( - __("Import Progress"), - percent, - message - ); + frm.dashboard.show_progress(__("Import Progress"), percent, message); frm.page.set_indicator(__("In Progress"), "orange"); // hide progress when complete @@ -103,15 +90,12 @@ frappe.ui.form.on("Bank Statement Import", { frm.trigger("show_report_error_button"); if (frm.doc.status === "Partial Success") { - frm.add_custom_button(__("Export Errored Rows"), () => - frm.trigger("export_errored_rows") - ); + frm.add_custom_button(__("Export Errored Rows"), () => frm.trigger("export_errored_rows")); } if (frm.doc.status.includes("Success")) { - frm.add_custom_button( - __("Go to {0} List", [__(frm.doc.reference_doctype)]), - () => frappe.set_route("List", frm.doc.reference_doctype) + frm.add_custom_button(__("Go to {0} List", [__(frm.doc.reference_doctype)]), () => + frappe.set_route("List", frm.doc.reference_doctype) ); } }, @@ -128,13 +112,8 @@ frappe.ui.form.on("Bank Statement Import", { frm.disable_save(); if (frm.doc.status !== "Success") { if (!frm.is_new() && frm.has_import_file()) { - let label = - frm.doc.status === "Pending" - ? __("Start Import") - : __("Retry"); - frm.page.set_primary_action(label, () => - frm.events.start_import(frm) - ); + let label = frm.doc.status === "Pending" ? __("Start Import") : __("Retry"); + frm.page.set_primary_action(label, () => frm.events.start_import(frm)); } else { frm.page.set_primary_action(__("Save"), () => frm.save()); } @@ -176,24 +155,24 @@ frappe.ui.form.on("Bank Statement Import", { message = successful_records.length > 1 ? __( - "Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ) + "Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ) : __( - "Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ); + "Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ); } else { message = successful_records.length > 1 ? __( - "Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ) + "Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ) : __( - "Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ); + "Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ); } } frm.dashboard.set_headline(message); @@ -236,8 +215,7 @@ frappe.ui.form.on("Bank Statement Import", { }, download_template() { - let method = - "/api/method/frappe.core.doctype.data_import.data_import.download_template"; + let method = "/api/method/frappe.core.doctype.data_import.data_import.download_template"; open_url_post(method, { doctype: "Bank Transaction", @@ -250,7 +228,7 @@ frappe.ui.form.on("Bank Statement Import", { "description", "reference_number", "bank_account", - "currency" + "currency", ], }, }); @@ -321,10 +299,7 @@ frappe.ui.form.on("Bank Statement Import", { show_import_preview(frm, preview_data) { let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); - if ( - frm.import_preview && - frm.import_preview.doctype === frm.doc.reference_doctype - ) { + if (frm.import_preview && frm.import_preview.doctype === frm.doc.reference_doctype) { frm.import_preview.preview_data = preview_data; frm.import_preview.import_log = import_log; frm.import_preview.refresh(); @@ -340,19 +315,10 @@ frappe.ui.form.on("Bank Statement Import", { frm, events: { remap_column(changed_map) { - let template_options = JSON.parse( - frm.doc.template_options || "{}" - ); - template_options.column_to_field_map = - template_options.column_to_field_map || {}; - Object.assign( - template_options.column_to_field_map, - changed_map - ); - frm.set_value( - "template_options", - JSON.stringify(template_options) - ); + let template_options = JSON.parse(frm.doc.template_options || "{}"); + template_options.column_to_field_map = template_options.column_to_field_map || {}; + Object.assign(template_options.column_to_field_map, changed_map); + frm.set_value("template_options", JSON.stringify(template_options)); frm.save().then(() => frm.trigger("import_file")); }, }, @@ -386,8 +352,7 @@ frappe.ui.form.on("Bank Statement Import", { let other_warnings = []; for (let warning of warnings) { if (warning.row) { - warnings_by_row[warning.row] = - warnings_by_row[warning.row] || []; + warnings_by_row[warning.row] = warnings_by_row[warning.row] || []; warnings_by_row[warning.row].push(warning); } else { other_warnings.push(warning); @@ -402,9 +367,7 @@ frappe.ui.form.on("Bank Statement Import", { if (w.field) { let label = w.field.label + - (w.field.parent !== frm.doc.reference_doctype - ? ` (${w.field.parent})` - : ""); + (w.field.parent !== frm.doc.reference_doctype ? ` (${w.field.parent})` : ""); return `
  • ${label}: ${w.message}
  • `; } return `
  • ${w.message}
  • `; @@ -423,10 +386,9 @@ frappe.ui.form.on("Bank Statement Import", { .map((warning) => { let header = ""; if (warning.col) { - let column_number = `${__( - "Column {0}", - [warning.col] - )}`; + let column_number = `${__("Column {0}", [ + warning.col, + ])}`; let column_header = columns[warning.col].header_title; header = `${column_number} (${column_header})`; } @@ -465,36 +427,28 @@ frappe.ui.form.on("Bank Statement Import", { let html = ""; if (log.success) { if (frm.doc.import_type === "Insert New Records") { - html = __( - "Successfully imported {0}", [ - `${frappe.utils.get_form_link( - frm.doc.reference_doctype, - log.docname, - true - )}`, - ] - ); + html = __("Successfully imported {0}", [ + `${frappe.utils.get_form_link( + frm.doc.reference_doctype, + log.docname, + true + )}`, + ]); } else { - html = __( - "Successfully updated {0}", [ - `${frappe.utils.get_form_link( - frm.doc.reference_doctype, - log.docname, - true - )}`, - ] - ); + html = __("Successfully updated {0}", [ + `${frappe.utils.get_form_link( + frm.doc.reference_doctype, + log.docname, + true + )}`, + ]); } } else { let messages = log.messages .map(JSON.parse) .map((m) => { - let title = m.title - ? `${m.title}` - : ""; - let message = m.message - ? `
    ${m.message}
    ` - : ""; + let title = m.title ? `${m.title}` : ""; + let message = m.message ? `
    ${m.message}
    ` : ""; return title + message; }) .join(""); diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index 3f5c064f4b9..78a27c3b96d 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -21,7 +21,7 @@ INVALID_VALUES = ("", None) class BankStatementImport(DataImport): def __init__(self, *args, **kwargs): - super(BankStatementImport, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate(self): doc_before_save = self.get_doc_before_save() @@ -30,7 +30,6 @@ class BankStatementImport(DataImport): or (doc_before_save and doc_before_save.import_file != self.import_file) or (doc_before_save and doc_before_save.google_sheets_url != self.google_sheets_url) ): - template_options_dict = {} column_to_field_map = {} bank = frappe.get_doc("Bank", self.bank) @@ -45,7 +44,6 @@ class BankStatementImport(DataImport): self.validate_google_sheets_url() def start_import(self): - preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template( self.import_file, self.google_sheets_url ) @@ -102,7 +100,7 @@ def download_errored_template(data_import_name): def parse_data_from_template(raw_data): data = [] - for i, row in enumerate(raw_data): + for _i, row in enumerate(raw_data): if all(v in INVALID_VALUES for v in row): # empty row continue @@ -112,9 +110,7 @@ def parse_data_from_template(raw_data): return data -def start_import( - data_import, bank_account, import_file_path, google_sheets_url, bank, template_options -): +def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options): """This method runs in background job""" update_mapping_db(bank, template_options) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js index 6c754022e68..4ab65ff73aa 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js @@ -1,36 +1,34 @@ let imports_in_progress = []; -frappe.listview_settings['Bank Statement Import'] = { +frappe.listview_settings["Bank Statement Import"] = { onload(listview) { - frappe.realtime.on('data_import_progress', data => { + frappe.realtime.on("data_import_progress", (data) => { if (!imports_in_progress.includes(data.data_import)) { imports_in_progress.push(data.data_import); } }); - frappe.realtime.on('data_import_refresh', data => { - imports_in_progress = imports_in_progress.filter( - d => d !== data.data_import - ); + frappe.realtime.on("data_import_refresh", (data) => { + imports_in_progress = imports_in_progress.filter((d) => d !== data.data_import); listview.refresh(); }); }, - get_indicator: function(doc) { + get_indicator: function (doc) { var colors = { - 'Pending': 'orange', - 'Not Started': 'orange', - 'Partial Success': 'orange', - 'Success': 'green', - 'In Progress': 'orange', - 'Error': 'red' + Pending: "orange", + "Not Started": "orange", + "Partial Success": "orange", + Success: "green", + "In Progress": "orange", + Error: "red", }; let status = doc.status; if (imports_in_progress.includes(doc.name)) { - status = 'In Progress'; + status = "In Progress"; } - if (status == 'Pending') { - status = 'Not Started'; + if (status == "Pending") { + status = "Not Started"; } - return [__(status), colors[status], 'status,=,' + doc.status]; + return [__(status), colors[status], "status,=," + doc.status]; }, - hide_name_column: true + hide_name_column: true, }; diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index 04dab4c28a0..230407ba5a4 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -1,5 +1,3 @@ -from typing import Tuple, Union - import frappe from frappe.utils import flt from rapidfuzz import fuzz, process @@ -19,7 +17,7 @@ class AutoMatchParty: def get(self, key): return self.__dict__.get(key, None) - def match(self) -> Union[Tuple, None]: + def match(self) -> tuple | None: result = None result = AutoMatchbyAccountIBAN( bank_party_account_number=self.bank_party_account_number, @@ -50,7 +48,7 @@ class AutoMatchbyAccountIBAN: result = self.match_account_in_party() return result - def match_account_in_party(self) -> Union[Tuple, None]: + def match_account_in_party(self) -> tuple | None: """Check if there is a IBAN/Account No. match in Customer/Supplier/Employee""" result = None parties = get_parties_in_order(self.deposit) @@ -97,7 +95,7 @@ class AutoMatchbyPartyNameDescription: def get(self, key): return self.__dict__.get(key, None) - def match(self) -> Union[Tuple, None]: + def match(self) -> tuple | None: # fuzzy search by customer/supplier & employee if not (self.bank_party_name or self.description): return None @@ -105,7 +103,7 @@ class AutoMatchbyPartyNameDescription: result = self.match_party_name_desc_in_party() return result - def match_party_name_desc_in_party(self) -> Union[Tuple, None]: + def match_party_name_desc_in_party(self) -> tuple | None: """Fuzzy search party name and/or description against parties in the system""" result = None parties = get_parties_in_order(self.deposit) @@ -130,7 +128,7 @@ class AutoMatchbyPartyNameDescription: return result - def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]: + def fuzzy_search_and_return_result(self, party, names, field) -> tuple | None: skip = False result = process.extract( query=self.get(field), @@ -147,7 +145,7 @@ class AutoMatchbyPartyNameDescription: party_name, ), skip - def process_fuzzy_result(self, result: Union[list, None]): + def process_fuzzy_result(self, result: list | None): """ If there are multiple valid close matches return None as result may be faulty. Return the result only if one accurate match stands out. diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js index b3cc1cbb1be..d899d429178 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js @@ -3,7 +3,7 @@ frappe.ui.form.on("Bank Transaction", { onload(frm) { - frm.set_query("payment_document", "payment_entries", function() { + frm.set_query("payment_document", "payment_entries", function () { const payment_doctypes = frm.events.get_payment_doctypes(frm); return { filters: { @@ -23,7 +23,7 @@ frappe.ui.form.on("Bank Transaction", { set_bank_statement_filter(frm); }, - setup: function(frm) { + setup: function (frm) { frm.set_query("party_type", function () { return { filters: { @@ -33,16 +33,10 @@ frappe.ui.form.on("Bank Transaction", { }); }, - get_payment_doctypes: function() { + get_payment_doctypes: function () { // get payment doctypes from all the apps - return [ - "Payment Entry", - "Journal Entry", - "Sales Invoice", - "Purchase Invoice", - "Bank Transaction", - ]; - } + return ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice", "Bank Transaction"]; + }, }); frappe.ui.form.on("Bank Transaction Payments", { @@ -54,10 +48,11 @@ frappe.ui.form.on("Bank Transaction Payments", { const update_clearance_date = (frm, cdt, cdn) => { if (frm.doc.docstatus === 1) { frappe - .xcall( - "erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment", - { doctype: cdt, docname: cdn, bt_name: frm.doc.name } - ) + .xcall("erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment", { + doctype: cdt, + docname: cdn, + bt_name: frm.doc.name, + }) .then((e) => { if (e == "success") { frappe.show_alert({ diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 256bde5c719..49c5a9fe4ee 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -2,6 +2,7 @@ # For license information, please see license.txt import frappe +from frappe.model.docstatus import DocStatus from frappe.utils import flt from erpnext.controllers.status_updater import StatusUpdater @@ -40,9 +41,10 @@ class BankTransaction(StatusUpdater): else: allocated_amount = 0.0 - amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.db_set("allocated_amount", flt(allocated_amount)) - self.db_set("unallocated_amount", amount - flt(allocated_amount)) + unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - allocated_amount + + self.db_set("allocated_amount", flt(allocated_amount, self.precision("allocated_amount"))) + self.db_set("unallocated_amount", flt(unallocated_amount, self.precision("unallocated_amount"))) self.reload() self.set_status(update=True) @@ -68,7 +70,7 @@ class BankTransaction(StatusUpdater): "payment_entry": voucher["payment_name"], "allocated_amount": 0.0, # Temporary } - child = self.append("payment_entries", pe) + self.append("payment_entries", pe) added = True # runs on_update_after_submit @@ -184,9 +186,7 @@ def get_clearance_details(transaction, payment_entry): """ gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry) - bt_allocations = get_total_allocated_amount( - payment_entry.payment_document, payment_entry.payment_entry - ) + bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry) unallocated_amount = min( transaction.unallocated_amount, @@ -284,7 +284,6 @@ def get_total_allocated_amount(doctype, docname): def get_paid_amount(payment_entry, currency, gl_bank_account): if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: - paid_amount_field = "paid_amount" if payment_entry.payment_document == "Payment Entry": doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry) @@ -323,9 +322,7 @@ def get_paid_amount(payment_entry, currency, gl_bank_account): ) elif payment_entry.payment_document == "Loan Repayment": - return frappe.db.get_value( - payment_entry.payment_document, payment_entry.payment_entry, "amount_paid" - ) + return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid") elif payment_entry.payment_document == "Bank Transaction": dep, wth = frappe.db.get_value( @@ -335,9 +332,7 @@ def get_paid_amount(payment_entry, currency, gl_bank_account): else: frappe.throw( - "Please reconcile {0}: {1} manually".format( - payment_entry.payment_document, payment_entry.payment_entry - ) + f"Please reconcile {payment_entry.payment_document}: {payment_entry.payment_entry} manually" ) @@ -393,3 +388,21 @@ def unclear_reference_payment(doctype, docname, bt_name): bt = frappe.get_doc("Bank Transaction", bt_name) set_voucher_clearance(doctype, docname, None, bt) return docname + + +def remove_from_bank_transaction(doctype, docname): + """Remove a (cancelled) voucher from all Bank Transactions.""" + for bt_name in get_reconciled_bank_transactions(doctype, docname): + bt = frappe.get_doc("Bank Transaction", bt_name) + if bt.docstatus == DocStatus.cancelled(): + continue + + modified = False + + for pe in bt.payment_entries: + if pe.payment_document == doctype and pe.payment_entry == docname: + bt.remove(pe) + modified = True + + if modified: + bt.save() diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js index 2585ee9c923..9942c09bf47 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js @@ -1,15 +1,15 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.listview_settings['Bank Transaction'] = { +frappe.listview_settings["Bank Transaction"] = { add_fields: ["unallocated_amount"], - get_indicator: function(doc) { - if(doc.docstatus == 2) { + get_indicator: function (doc) { + if (doc.docstatus == 2) { return [__("Cancelled"), "red", "docstatus,=,2"]; - } else if(flt(doc.unallocated_amount)<=0) { + } else if (flt(doc.unallocated_amount) <= 0) { return [__("Reconciled"), "green", "unallocated_amount,=,0"]; - } else if(flt(doc.unallocated_amount)>0) { + } else if (flt(doc.unallocated_amount) > 0) { return [__("Unreconciled"), "orange", "unallocated_amount,>,0"]; } - } + }, }; diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py index 372c53d4992..2fec01ad397 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py @@ -18,12 +18,12 @@ def upload_bank_statement(): fcontent = frappe.local.uploaded_file fname = frappe.local.uploaded_filename - if frappe.safe_encode(fname).lower().endswith("csv".encode("utf-8")): + if frappe.safe_encode(fname).lower().endswith(b"csv"): from frappe.utils.csvutils import read_csv_content rows = read_csv_content(fcontent, False) - elif frappe.safe_encode(fname).lower().endswith("xlsx".encode("utf-8")): + elif frappe.safe_encode(fname).lower().endswith(b"xlsx"): from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file rows = read_xlsx_file_from_attached_file(fcontent=fcontent) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index f900e0775ce..71b901327d7 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -2,10 +2,10 @@ # See license.txt import json -import unittest import frappe from frappe import utils +from frappe.model.docstatus import DocStatus from frappe.tests.utils import FrappeTestCase from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( @@ -32,8 +32,16 @@ class TestBankTransaction(FrappeTestCase): frappe.db.delete(dt) make_pos_profile() - add_transactions() - add_vouchers() + + # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error + uniq_identifier = frappe.generate_hash(length=10) + gl_account = create_gl_account("_Test Bank " + uniq_identifier) + bank_account = create_bank_account( + gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier + ) + + add_transactions(bank_account=bank_account) + add_vouchers(gl_account=gl_account) # This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. def test_linked_payments(self): @@ -81,6 +89,29 @@ class TestBankTransaction(FrappeTestCase): clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") self.assertFalse(clearance_date) + def test_cancel_voucher(self): + bank_transaction = frappe.get_doc( + "Bank Transaction", + dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"), + ) + payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700)) + vouchers = json.dumps( + [ + { + "payment_doctype": "Payment Entry", + "payment_name": payment.name, + "amount": bank_transaction.unallocated_amount, + } + ] + ) + reconcile_vouchers(bank_transaction.name, vouchers) + payment.reload() + payment.cancel() + bank_transaction.reload() + self.assertEqual(bank_transaction.docstatus, DocStatus.submitted()) + self.assertEqual(bank_transaction.unallocated_amount, 1700) + self.assertEqual(bank_transaction.payment_entries, []) + # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount def test_debit_credit_output(self): bank_transaction = frappe.get_doc( @@ -190,7 +221,9 @@ class TestBankTransaction(FrappeTestCase): self.assertEqual(linked_payments[0][2], repayment_entry.name) -def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): +def create_bank_account( + bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account" +): try: frappe.get_doc( { @@ -202,21 +235,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): pass try: - frappe.get_doc( + bank_account = frappe.get_doc( { "doctype": "Bank Account", - "account_name": "Checking Account", + "account_name": bank_account_name, "bank": bank_name, - "account": account_name, + "account": gl_account, } ).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass + return bank_account.name -def add_transactions(): - create_bank_account() +def create_gl_account(gl_account_name="_Test Bank - _TC"): + gl_account = frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "parent_account": "Current Assets - _TC", + "account_type": "Bank", + "is_group": 0, + "account_name": gl_account_name, + } + ).insert() + return gl_account.name + + +def add_transactions(bank_account="_Test Bank - _TC"): doc = frappe.get_doc( { "doctype": "Bank Transaction", @@ -224,7 +271,7 @@ def add_transactions(): "date": "2018-10-23", "deposit": 1200, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -236,7 +283,7 @@ def add_transactions(): "date": "2018-10-23", "deposit": 1700, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -248,7 +295,7 @@ def add_transactions(): "date": "2018-10-26", "withdrawal": 690, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -260,7 +307,7 @@ def add_transactions(): "date": "2018-10-27", "deposit": 3900, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -272,13 +319,13 @@ def add_transactions(): "date": "2018-10-27", "withdrawal": 109080, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() -def add_vouchers(): +def add_vouchers(gl_account="_Test Bank - _TC"): try: frappe.get_doc( { @@ -294,7 +341,7 @@ def add_vouchers(): pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Conrad Oct 18" pe.reference_date = "2018-10-24" pe.insert() @@ -313,14 +360,14 @@ def add_vouchers(): pass pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Herr G Oct 18" pe.reference_date = "2018-10-24" pe.insert() pe.submit() pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Herr G Nov 18" pe.reference_date = "2018-11-01" pe.insert() @@ -351,10 +398,10 @@ def add_vouchers(): pass pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1) - pi.cash_bank_account = "_Test Bank - _TC" + pi.cash_bank_account = gl_account pi.insert() pi.submit() - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Poore Simon's Oct 18" pe.reference_date = "2018-10-28" pe.paid_amount = 690 @@ -363,7 +410,7 @@ def add_vouchers(): pe.submit() si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900) - pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account) pe.reference_no = "Poore Simon's Oct 18" pe.reference_date = "2018-10-28" pe.insert() @@ -383,19 +430,13 @@ def add_vouchers(): mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"}) - if not frappe.db.get_value( - "Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"} - ): - mode_of_payment.append( - "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"} - ) + if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}): + mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account}) mode_of_payment.save() si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1) si.is_pos = 1 - si.append( - "payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080} - ) + si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080}) si.insert() si.submit() diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/__init__.py b/erpnext/accounts/doctype/bisect_accounting_statements/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js new file mode 100644 index 00000000000..f3532e5be61 --- /dev/null +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -0,0 +1,100 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Bisect Accounting Statements", { + onload(frm) { + frm.trigger("render_heatmap"); + }, + refresh(frm) { + frm.add_custom_button(__("Bisect Left"), () => { + frm.trigger("bisect_left"); + }); + + frm.add_custom_button(__("Bisect Right"), () => { + frm.trigger("bisect_right"); + }); + + frm.add_custom_button(__("Up"), () => { + frm.trigger("move_up"); + }); + frm.add_custom_button(__("Build Tree"), () => { + frm.trigger("build_tree"); + }); + }, + render_heatmap(frm) { + let bisect_heatmap = frm.get_field("bisect_heatmap").$wrapper; + bisect_heatmap.addClass("bisect_heatmap_location"); + + // milliseconds in a day + let msiad = 24 * 60 * 60 * 1000; + let datapoints = {}; + let fr_dt = new Date(frm.doc.from_date).getTime(); + let to_dt = new Date(frm.doc.to_date).getTime(); + let bisect_start = new Date(frm.doc.current_from_date).getTime(); + let bisect_end = new Date(frm.doc.current_to_date).getTime(); + + for (let x = fr_dt; x <= to_dt; x += msiad) { + let epoch_in_seconds = x / 1000; + if (bisect_start <= x && x <= bisect_end) { + datapoints[epoch_in_seconds] = 1.0; + } else { + datapoints[epoch_in_seconds] = 0.0; + } + } + + new frappe.Chart(".bisect_heatmap_location", { + type: "heatmap", + data: { + dataPoints: datapoints, + start: new Date(frm.doc.from_date), + end: new Date(frm.doc.to_date), + }, + countLabel: "Bisecting", + discreteDomains: 1, + }); + }, + bisect_left(frm) { + frm.call({ + doc: frm.doc, + method: "bisect_left", + freeze: true, + freeze_message: __("Bisecting Left ..."), + callback: (r) => { + frm.trigger("render_heatmap"); + }, + }); + }, + bisect_right(frm) { + frm.call({ + doc: frm.doc, + freeze: true, + freeze_message: __("Bisecting Right ..."), + method: "bisect_right", + callback: (r) => { + frm.trigger("render_heatmap"); + }, + }); + }, + move_up(frm) { + frm.call({ + doc: frm.doc, + freeze: true, + freeze_message: __("Moving up in tree ..."), + method: "move_up", + callback: (r) => { + frm.trigger("render_heatmap"); + }, + }); + }, + build_tree(frm) { + frm.call({ + doc: frm.doc, + freeze: true, + freeze_message: __("Rebuilding BTree for period ..."), + method: "build_tree", + callback: (r) => { + frm.trigger("render_heatmap"); + }, + }); + }, +}); diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json new file mode 100644 index 00000000000..e129fa60c2c --- /dev/null +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -0,0 +1,194 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-09-15 21:28:28.054773", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_cvfg", + "company", + "column_break_hcam", + "from_date", + "column_break_qxbi", + "to_date", + "column_break_iwny", + "algorithm", + "section_break_8ph9", + "current_node", + "section_break_ngid", + "bisect_heatmap", + "section_break_hmsy", + "bisecting_from", + "current_from_date", + "column_break_uqyd", + "bisecting_to", + "current_to_date", + "section_break_hbyo", + "heading_cppb", + "p_l_summary", + "column_break_aivo", + "balance_sheet_summary", + "b_s_summary", + "column_break_gvwx", + "difference_heading", + "difference" + ], + "fields": [ + { + "fieldname": "column_break_qxbi", + "fieldtype": "Column Break" + }, + { + "fieldname": "from_date", + "fieldtype": "Datetime", + "label": "From Date" + }, + { + "fieldname": "to_date", + "fieldtype": "Datetime", + "label": "To Date" + }, + { + "default": "BFS", + "fieldname": "algorithm", + "fieldtype": "Select", + "label": "Algorithm", + "options": "BFS\nDFS" + }, + { + "fieldname": "column_break_iwny", + "fieldtype": "Column Break" + }, + { + "fieldname": "current_node", + "fieldtype": "Link", + "label": "Current Node", + "options": "Bisect Nodes" + }, + { + "fieldname": "section_break_hmsy", + "fieldtype": "Section Break" + }, + { + "fieldname": "current_from_date", + "fieldtype": "Datetime", + "read_only": 1 + }, + { + "fieldname": "current_to_date", + "fieldtype": "Datetime", + "read_only": 1 + }, + { + "fieldname": "column_break_uqyd", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_hbyo", + "fieldtype": "Section Break" + }, + { + "fieldname": "p_l_summary", + "fieldtype": "Float", + "read_only": 1 + }, + { + "fieldname": "b_s_summary", + "fieldtype": "Float", + "read_only": 1 + }, + { + "fieldname": "difference", + "fieldtype": "Float", + "read_only": 1 + }, + { + "fieldname": "column_break_aivo", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_gvwx", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "column_break_hcam", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_ngid", + "fieldtype": "Section Break" + }, + { + "fieldname": "section_break_8ph9", + "fieldtype": "Section Break", + "hidden": 1 + }, + { + "fieldname": "bisect_heatmap", + "fieldtype": "HTML", + "label": "Heatmap" + }, + { + "fieldname": "heading_cppb", + "fieldtype": "Heading", + "label": "Profit and Loss Summary" + }, + { + "fieldname": "balance_sheet_summary", + "fieldtype": "Heading", + "label": "Balance Sheet Summary" + }, + { + "fieldname": "difference_heading", + "fieldtype": "Heading", + "label": "Difference" + }, + { + "fieldname": "bisecting_from", + "fieldtype": "Heading", + "label": "Bisecting From" + }, + { + "fieldname": "bisecting_to", + "fieldtype": "Heading", + "label": "Bisecting To" + }, + { + "fieldname": "section_break_cvfg", + "fieldtype": "Section Break" + } + ], + "hide_toolbar": 1, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2023-12-01 16:49:54.073890", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Bisect Accounting Statements", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Administrator", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py new file mode 100644 index 00000000000..f2ed299592b --- /dev/null +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -0,0 +1,206 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import datetime +from collections import deque +from math import floor + +import frappe +from dateutil.relativedelta import relativedelta +from frappe import _ +from frappe.model.document import Document +from frappe.utils import getdate +from frappe.utils.data import guess_date_format + + +class BisectAccountingStatements(Document): + def validate(self): + self.validate_dates() + + def validate_dates(self): + if getdate(self.from_date) > getdate(self.to_date): + frappe.throw( + _("From Date: {0} cannot be greater than To date: {1}").format( + frappe.bold(self.from_date), frappe.bold(self.to_date) + ) + ) + + def bfs(self, from_date: datetime, to_date: datetime): + # Make Root node + node = frappe.new_doc("Bisect Nodes") + node.root = None + node.period_from_date = from_date + node.period_to_date = to_date + node.insert() + + period_queue = deque([node]) + while period_queue: + cur_node = period_queue.popleft() + delta = cur_node.period_to_date - cur_node.period_from_date + if delta.days == 0: + continue + else: + cur_floor = floor(delta.days / 2) + next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor) + left_node = frappe.new_doc("Bisect Nodes") + left_node.period_from_date = cur_node.period_from_date + left_node.period_to_date = next_to_date + left_node.root = cur_node.name + left_node.generated = False + left_node.insert() + cur_node.left_child = left_node.name + period_queue.append(left_node) + + next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1)) + right_node = frappe.new_doc("Bisect Nodes") + right_node.period_from_date = next_from_date + right_node.period_to_date = cur_node.period_to_date + right_node.root = cur_node.name + right_node.generated = False + right_node.insert() + cur_node.right_child = right_node.name + period_queue.append(right_node) + + cur_node.save() + + def dfs(self, from_date: datetime, to_date: datetime): + # Make Root node + node = frappe.new_doc("Bisect Nodes") + node.root = None + node.period_from_date = from_date + node.period_to_date = to_date + node.insert() + + period_stack = [node] + while period_stack: + cur_node = period_stack.pop() + delta = cur_node.period_to_date - cur_node.period_from_date + if delta.days == 0: + continue + else: + cur_floor = floor(delta.days / 2) + next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor) + left_node = frappe.new_doc("Bisect Nodes") + left_node.period_from_date = cur_node.period_from_date + left_node.period_to_date = next_to_date + left_node.root = cur_node.name + left_node.generated = False + left_node.insert() + cur_node.left_child = left_node.name + period_stack.append(left_node) + + next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1)) + right_node = frappe.new_doc("Bisect Nodes") + right_node.period_from_date = next_from_date + right_node.period_to_date = cur_node.period_to_date + right_node.root = cur_node.name + right_node.generated = False + right_node.insert() + cur_node.right_child = right_node.name + period_stack.append(right_node) + + cur_node.save() + + @frappe.whitelist() + def build_tree(self): + frappe.db.delete("Bisect Nodes") + + # Convert str to datetime format + dt_format = guess_date_format(self.from_date) + from_date = datetime.datetime.strptime(self.from_date, dt_format) + to_date = datetime.datetime.strptime(self.to_date, dt_format) + + if self.algorithm == "BFS": + self.bfs(from_date, to_date) + + if self.algorithm == "DFS": + self.dfs(from_date, to_date) + + # set root as current node + root = frappe.db.get_all("Bisect Nodes", filters={"root": ["is", "not set"]})[0] + self.get_report_summary() + self.current_node = root.name + self.current_from_date = self.from_date + self.current_to_date = self.to_date + self.save() + + def get_report_summary(self): + filters = { + "company": self.company, + "filter_based_on": "Date Range", + "period_start_date": self.current_from_date, + "period_end_date": self.current_to_date, + "periodicity": "Yearly", + } + pl_summary = frappe.get_doc("Report", "Profit and Loss Statement") + self.p_l_summary = pl_summary.execute_script_report(filters=filters)[5] + bs_summary = frappe.get_doc("Report", "Balance Sheet") + self.b_s_summary = bs_summary.execute_script_report(filters=filters)[5] + self.difference = abs(self.p_l_summary - self.b_s_summary) + + def update_node(self): + current_node = frappe.get_doc("Bisect Nodes", self.current_node) + current_node.balance_sheet_summary = self.b_s_summary + current_node.profit_loss_summary = self.p_l_summary + current_node.difference = self.difference + current_node.generated = True + current_node.save() + + def current_node_has_summary_info(self): + "Assertion method" + return frappe.db.get_value("Bisect Nodes", self.current_node, "generated") + + def fetch_summary_info_from_current_node(self): + current_node = frappe.get_doc("Bisect Nodes", self.current_node) + self.p_l_summary = current_node.balance_sheet_summary + self.b_s_summary = current_node.profit_loss_summary + self.difference = abs(self.p_l_summary - self.b_s_summary) + + def fetch_or_calculate(self): + if self.current_node_has_summary_info(): + self.fetch_summary_info_from_current_node() + else: + self.get_report_summary() + self.update_node() + + @frappe.whitelist() + def bisect_left(self): + if self.current_node is not None: + cur_node = frappe.get_doc("Bisect Nodes", self.current_node) + if cur_node.left_child is not None: + lft_node = frappe.get_doc("Bisect Nodes", cur_node.left_child) + self.current_node = cur_node.left_child + self.current_from_date = lft_node.period_from_date + self.current_to_date = lft_node.period_to_date + self.fetch_or_calculate() + self.save() + else: + frappe.msgprint(_("No more children on Left")) + + @frappe.whitelist() + def bisect_right(self): + if self.current_node is not None: + cur_node = frappe.get_doc("Bisect Nodes", self.current_node) + if cur_node.right_child is not None: + rgt_node = frappe.get_doc("Bisect Nodes", cur_node.right_child) + self.current_node = cur_node.right_child + self.current_from_date = rgt_node.period_from_date + self.current_to_date = rgt_node.period_to_date + self.fetch_or_calculate() + self.save() + else: + frappe.msgprint(_("No more children on Right")) + + @frappe.whitelist() + def move_up(self): + if self.current_node is not None: + cur_node = frappe.get_doc("Bisect Nodes", self.current_node) + if cur_node.root is not None: + root = frappe.get_doc("Bisect Nodes", cur_node.root) + self.current_node = cur_node.root + self.current_from_date = root.period_from_date + self.current_to_date = root.period_to_date + self.fetch_or_calculate() + self.save() + else: + frappe.msgprint(_("Reached Root")) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/test_bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/test_bisect_accounting_statements.py new file mode 100644 index 00000000000..56ecc94a18e --- /dev/null +++ b/erpnext/accounts/doctype/bisect_accounting_statements/test_bisect_accounting_statements.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBisectAccountingStatements(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/bisect_nodes/__init__.py b/erpnext/accounts/doctype/bisect_nodes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js new file mode 100644 index 00000000000..6dea25fc924 --- /dev/null +++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Bisect Nodes", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json new file mode 100644 index 00000000000..03fad261c3c --- /dev/null +++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json @@ -0,0 +1,97 @@ +{ + "actions": [], + "autoname": "autoincrement", + "creation": "2023-09-27 14:56:38.112462", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "root", + "left_child", + "right_child", + "period_from_date", + "period_to_date", + "difference", + "balance_sheet_summary", + "profit_loss_summary", + "generated" + ], + "fields": [ + { + "fieldname": "root", + "fieldtype": "Link", + "label": "Root", + "options": "Bisect Nodes" + }, + { + "fieldname": "left_child", + "fieldtype": "Link", + "label": "Left Child", + "options": "Bisect Nodes" + }, + { + "fieldname": "right_child", + "fieldtype": "Link", + "label": "Right Child", + "options": "Bisect Nodes" + }, + { + "fieldname": "period_from_date", + "fieldtype": "Datetime", + "label": "Period_from_date" + }, + { + "fieldname": "period_to_date", + "fieldtype": "Datetime", + "label": "Period To Date" + }, + { + "fieldname": "difference", + "fieldtype": "Float", + "label": "Difference" + }, + { + "fieldname": "balance_sheet_summary", + "fieldtype": "Float", + "label": "Balance Sheet Summary" + }, + { + "fieldname": "profit_loss_summary", + "fieldtype": "Float", + "label": "Profit and Loss Summary" + }, + { + "default": "0", + "fieldname": "generated", + "fieldtype": "Check", + "label": "Generated" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-12-01 17:46:12.437996", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Bisect Nodes", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py new file mode 100644 index 00000000000..f50776641d0 --- /dev/null +++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py @@ -0,0 +1,29 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BisectNodes(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + balance_sheet_summary: DF.Float + difference: DF.Float + generated: DF.Check + left_child: DF.Link | None + name: DF.Int | None + period_from_date: DF.Datetime | None + period_to_date: DF.Datetime | None + profit_loss_summary: DF.Float + right_child: DF.Link | None + root: DF.Link | None + # end: auto-generated types + + pass diff --git a/erpnext/accounts/doctype/bisect_nodes/test_bisect_nodes.py b/erpnext/accounts/doctype/bisect_nodes/test_bisect_nodes.py new file mode 100644 index 00000000000..5399df139f1 --- /dev/null +++ b/erpnext/accounts/doctype/bisect_nodes/test_bisect_nodes.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBisectNodes(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index e162e3222d3..6e874f7c08c 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -2,48 +2,48 @@ // For license information, please see license.txt frappe.provide("erpnext.accounts.dimensions"); -frappe.ui.form.on('Budget', { - onload: function(frm) { - frm.set_query("account", "accounts", function() { +frappe.ui.form.on("Budget", { + onload: function (frm) { + frm.set_query("account", "accounts", function () { return { filters: { company: frm.doc.company, report_type: "Profit and Loss", - is_group: 0 - } + is_group: 0, + }, }; }); - frm.set_query("monthly_distribution", function() { + frm.set_query("monthly_distribution", function () { return { filters: { - fiscal_year: frm.doc.fiscal_year - } + fiscal_year: frm.doc.fiscal_year, + }, }; }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, - refresh: function(frm) { - frm.trigger("toggle_reqd_fields") + refresh: function (frm) { + frm.trigger("toggle_reqd_fields"); }, - budget_against: function(frm) { - frm.trigger("set_null_value") - frm.trigger("toggle_reqd_fields") + budget_against: function (frm) { + frm.trigger("set_null_value"); + frm.trigger("toggle_reqd_fields"); }, - set_null_value: function(frm) { - if(frm.doc.budget_against == 'Cost Center') { - frm.set_value('project', null) + set_null_value: function (frm) { + if (frm.doc.budget_against == "Cost Center") { + frm.set_value("project", null); } else { - frm.set_value('cost_center', null) + frm.set_value("cost_center", null); } }, - toggle_reqd_fields: function(frm) { - frm.toggle_reqd("cost_center", frm.doc.budget_against=="Cost Center"); - frm.toggle_reqd("project", frm.doc.budget_against=="Project"); - } + toggle_reqd_fields: function (frm) { + frm.toggle_reqd("cost_center", frm.doc.budget_against == "Cost Center"); + frm.toggle_reqd("project", frm.doc.budget_against == "Project"); + }, }); diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 6cfd15d3ec8..22b8d64971c 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -40,10 +40,11 @@ class Budget(Document): select b.name, ba.account from `tabBudget` b, `tabBudget Account` ba where - ba.parent = b.name and b.docstatus < 2 and b.company = %s and %s=%s and - b.fiscal_year=%s and b.name != %s and ba.account in (%s) """ - % ("%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))), - (self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts), + ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and + b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format( + "%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts)) + ), + (self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)), as_dict=1, ) @@ -66,12 +67,14 @@ class Budget(Document): if account_details.is_group: frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account)) elif account_details.company != self.company: - frappe.throw(_("Account {0} does not belongs to company {1}").format(d.account, self.company)) + frappe.throw( + _("Account {0} does not belongs to company {1}").format(d.account, self.company) + ) elif account_details.report_type != "Profit and Loss": frappe.throw( - _("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format( - d.account - ) + _( + "Budget cannot be assigned against {0}, as it's not an Income or Expense account" + ).format(d.account) ) if d.account in account_list: @@ -109,6 +112,8 @@ class Budget(Document): def validate_expense_against_budget(args, expense_amount=0): args = frappe._dict(args) + if not frappe.get_all("Budget", limit=1): + return if args.get("company") and not args.fiscal_year: args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0] @@ -116,6 +121,9 @@ def validate_expense_against_budget(args, expense_amount=0): "Company", args.get("company"), "exception_budget_approver_role" ) + if not frappe.get_cached_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): # nosec + return + if not args.account: args.account = args.get("expense_account") @@ -142,32 +150,26 @@ def validate_expense_against_budget(args, expense_amount=0): if ( args.get(budget_against) and args.account - and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"}) + and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense") ): - doctype = dimension.get("document_type") if frappe.get_cached_value("DocType", doctype, "is_tree"): - lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"]) - condition = """and exists(select name from `tab%s` - where lft<=%s and rgt>=%s and name=b.%s)""" % ( - doctype, - lft, - rgt, - budget_against, - ) # nosec + lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"]) + condition = f"""and exists(select name from `tab{doctype}` + where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec args.is_tree = True else: - condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against))) + condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}" args.is_tree = False args.budget_against_field = budget_against args.budget_against_doctype = doctype budget_records = frappe.db.sql( - """ + f""" select - b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution, + b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution, ifnull(b.applicable_on_material_request, 0) as for_material_request, ifnull(applicable_on_purchase_order, 0) as for_purchase_order, ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses, @@ -180,9 +182,7 @@ def validate_expense_against_budget(args, expense_amount=0): b.name=ba.parent and b.fiscal_year=%s and ba.account=%s and b.docstatus=1 {condition} - """.format( - condition=condition, budget_against_field=budget_against - ), + """, (args.fiscal_year, args.account), as_dict=True, ) # nosec @@ -194,12 +194,18 @@ def validate_expense_against_budget(args, expense_amount=0): def validate_budget_records(args, budget_records, expense_amount): for budget in budget_records: if flt(budget.budget_amount): - amount = expense_amount or get_amount(args, budget) yearly_action, monthly_action = get_actions(args, budget) + args["for_material_request"] = budget.for_material_request + args["for_purchase_order"] = budget.for_purchase_order if yearly_action in ("Stop", "Warn"): compare_expense_with_budget( - args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount + args, + flt(budget.budget_amount), + _("Annual"), + yearly_action, + budget.budget_against, + expense_amount, ) if monthly_action in ["Stop", "Warn"]: @@ -210,18 +216,32 @@ def validate_budget_records(args, budget_records, expense_amount): args["month_end_date"] = get_last_day(args.posting_date) compare_expense_with_budget( - args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount + args, + budget_amount, + _("Accumulated Monthly"), + monthly_action, + budget.budget_against, + expense_amount, ) def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0): - actual_expense = get_actual_expense(args) - total_expense = actual_expense + amount + args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0 + if not amount: + args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args) + + if args.get("doctype") == "Material Request" and args.for_material_request: + amount = args.requested_amount + args.ordered_amount + + elif args.get("doctype") == "Purchase Order" and args.for_purchase_order: + amount = args.ordered_amount + + total_expense = args.actual_expense + amount if total_expense > budget_amount: - if actual_expense > budget_amount: + if args.actual_expense > budget_amount: error_tense = _("is already") - diff = actual_expense - budget_amount + diff = args.actual_expense - budget_amount else: error_tense = _("will be") diff = total_expense - budget_amount @@ -238,9 +258,10 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_ frappe.bold(fmt_money(diff, currency=currency)), ) - if ( - frappe.flags.exception_approver_role - and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user) + msg += get_expense_breakup(args, currency, budget_against) + + if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles( + frappe.session.user ): action = "Warn" @@ -250,6 +271,83 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_ frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded")) +def get_expense_breakup(args, currency, budget_against): + msg = "
    Total Expenses booked through -
      " + + common_filters = frappe._dict( + { + args.budget_against_field: budget_against, + "account": args.account, + "company": args.company, + } + ) + + msg += ( + "
    • " + + frappe.utils.get_link_to_report( + "General Ledger", + label="Actual Expenses", + filters=common_filters.copy().update( + { + "from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"), + "to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"), + "is_cancelled": 0, + } + ), + ) + + " - " + + frappe.bold(fmt_money(args.actual_expense, currency=currency)) + + "
    • " + ) + + msg += ( + "
    • " + + frappe.utils.get_link_to_report( + "Material Request", + label="Material Requests", + report_type="Report Builder", + doctype="Material Request", + filters=common_filters.copy().update( + { + "status": [["!=", "Stopped"]], + "docstatus": 1, + "material_request_type": "Purchase", + "schedule_date": [["fiscal year", "2023-2024"]], + "item_code": args.item_code, + "per_ordered": [["<", 100]], + } + ), + ) + + " - " + + frappe.bold(fmt_money(args.requested_amount, currency=currency)) + + "
    • " + ) + + msg += ( + "
    • " + + frappe.utils.get_link_to_report( + "Purchase Order", + label="Unbilled Orders", + report_type="Report Builder", + doctype="Purchase Order", + filters=common_filters.copy().update( + { + "status": [["!=", "Closed"]], + "docstatus": 1, + "transaction_date": [["fiscal year", "2023-2024"]], + "item_code": args.item_code, + "per_billed": [["<", 100]], + } + ), + ) + + " - " + + frappe.bold(fmt_money(args.ordered_amount, currency=currency)) + + "
    " + ) + + return msg + + def get_actions(args, budget): yearly_action = budget.action_if_annual_budget_exceeded monthly_action = budget.action_if_accumulated_monthly_budget_exceeded @@ -265,31 +363,15 @@ def get_actions(args, budget): return yearly_action, monthly_action -def get_amount(args, budget): - amount = 0 - - if args.get("doctype") == "Material Request" and budget.for_material_request: - amount = ( - get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args) - ) - - elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order: - amount = get_ordered_amount(args, budget) + get_actual_expense(args) - - return amount - - -def get_requested_amount(args, budget): +def get_requested_amount(args): item_code = args.get("item_code") - condition = get_other_condition(args, budget, "Material Request") + condition = get_other_condition(args, "Material Request") data = frappe.db.sql( """ select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and - child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {0} and - parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format( - condition - ), + child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {} and + parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition), item_code, as_list=1, ) @@ -297,17 +379,15 @@ def get_requested_amount(args, budget): return data[0][0] if data else 0 -def get_ordered_amount(args, budget): +def get_ordered_amount(args): item_code = args.get("item_code") - condition = get_other_condition(args, budget, "Purchase Order") + condition = get_other_condition(args, "Purchase Order") data = frappe.db.sql( - """ select ifnull(sum(child.amount - child.billed_amt), 0) as amount + f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount from `tabPurchase Order Item` child, `tabPurchase Order` parent where parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt - and parent.status != 'Closed' and {0}""".format( - condition - ), + and parent.status != 'Closed' and {condition}""", item_code, as_list=1, ) @@ -315,12 +395,12 @@ def get_ordered_amount(args, budget): return data[0][0] if data else 0 -def get_other_condition(args, budget, for_doc): +def get_other_condition(args, for_doc): condition = "expense_account = '%s'" % (args.expense_account) budget_against_field = args.get("budget_against_field") if budget_against_field and args.get(budget_against_field): - condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field)) + condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'" if args.get("fiscal_year"): date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date" @@ -328,12 +408,8 @@ def get_other_condition(args, budget, for_doc): "Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"] ) - condition += """ and parent.%s - between '%s' and '%s' """ % ( - date_field, - start_date, - end_date, - ) + condition += f""" and parent.{date_field} + between '{start_date}' and '{end_date}' """ return condition @@ -352,21 +428,17 @@ def get_actual_expense(args): args.update(lft_rgt) - condition2 = """and exists(select name from `tab{doctype}` + condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}` where lft>=%(lft)s and rgt<=%(rgt)s - and name=gle.{budget_against_field})""".format( - doctype=args.budget_against_doctype, budget_against_field=budget_against_field # nosec - ) + and name=gle.{budget_against_field})""" else: - condition2 = """and exists(select name from `tab{doctype}` - where name=gle.{budget_against} and - gle.{budget_against} = %({budget_against})s)""".format( - doctype=args.budget_against_doctype, budget_against=budget_against_field - ) + condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}` + where name=gle.{budget_against_field} and + gle.{budget_against_field} = %({budget_against_field})s)""" amount = flt( frappe.db.sql( - """ + f""" select sum(gle.debit) - sum(gle.credit) from `tabGL Entry` gle where @@ -377,9 +449,7 @@ def get_actual_expense(args): and gle.company=%(company)s and gle.docstatus=1 {condition2} - """.format( - condition1=condition1, condition2=condition2 - ), + """, (args), )[0][0] ) # nosec diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 11af9a29f6f..6d9a6f51468 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -41,9 +41,7 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Cost Center") - frappe.db.set_value( - "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" - ) + frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") jv = make_journal_entry( "_Test Account Cost for Goods Sold - _TC", @@ -63,9 +61,7 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Cost Center") - frappe.db.set_value( - "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" - ) + frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") jv = make_journal_entry( "_Test Account Cost for Goods Sold - _TC", @@ -97,9 +93,7 @@ class TestBudget(unittest.TestCase): ) fiscal_year = get_fiscal_year(nowdate())[0] - frappe.db.set_value( - "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" - ) + frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year) mr = frappe.get_doc( @@ -138,9 +132,7 @@ class TestBudget(unittest.TestCase): ) fiscal_year = get_fiscal_year(nowdate())[0] - frappe.db.set_value( - "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" - ) + frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year) po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True) @@ -158,9 +150,7 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Project") - frappe.db.set_value( - "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" - ) + frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") project = frappe.get_value("Project", {"project_name": "_Test Project"}) @@ -223,7 +213,7 @@ class TestBudget(unittest.TestCase): if month > 9: month = 9 - for i in range(month + 1): + for _i in range(month + 1): jv = make_journal_entry( "_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", @@ -237,9 +227,7 @@ class TestBudget(unittest.TestCase): frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name}) ) - frappe.db.set_value( - "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" - ) + frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") self.assertRaises(BudgetError, jv.cancel) @@ -255,7 +243,7 @@ class TestBudget(unittest.TestCase): month = 9 project = frappe.get_value("Project", {"project_name": "_Test Project"}) - for i in range(month + 1): + for _i in range(month + 1): jv = make_journal_entry( "_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", @@ -270,9 +258,7 @@ class TestBudget(unittest.TestCase): frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name}) ) - frappe.db.set_value( - "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" - ) + frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") self.assertRaises(BudgetError, jv.cancel) @@ -284,9 +270,7 @@ class TestBudget(unittest.TestCase): set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC") budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC") - frappe.db.set_value( - "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" - ) + frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") jv = make_journal_entry( "_Test Account Cost for Goods Sold - _TC", @@ -316,9 +300,7 @@ class TestBudget(unittest.TestCase): ).insert(ignore_permissions=True) budget = make_budget(budget_against="Cost Center", cost_center=cost_center) - frappe.db.set_value( - "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" - ) + frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") jv = make_journal_entry( "_Test Account Cost for Goods Sold - _TC", @@ -423,13 +405,11 @@ def make_budget(**args): fiscal_year = get_fiscal_year(nowdate())[0] if budget_against == "Project": - project_name = "{0}%".format("_Test Project/" + fiscal_year) + project_name = "{}%".format("_Test Project/" + fiscal_year) budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)}) else: - cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year) - budget_list = frappe.get_all( - "Budget", fields=["name"], filters={"name": ("like", cost_center_name)} - ) + cost_center_name = "{}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year) + budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", cost_center_name)}) for d in budget_list: frappe.db.sql("delete from `tabBudget` where name = %(name)s", d) frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d) @@ -451,24 +431,18 @@ def make_budget(**args): budget.action_if_annual_budget_exceeded = "Stop" budget.action_if_accumulated_monthly_budget_exceeded = "Ignore" budget.budget_against = budget_against - budget.append( - "accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000} - ) + budget.append("accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000}) if args.applicable_on_material_request: budget.applicable_on_material_request = 1 - budget.action_if_annual_budget_exceeded_on_mr = ( - args.action_if_annual_budget_exceeded_on_mr or "Warn" - ) + budget.action_if_annual_budget_exceeded_on_mr = args.action_if_annual_budget_exceeded_on_mr or "Warn" budget.action_if_accumulated_monthly_budget_exceeded_on_mr = ( args.action_if_accumulated_monthly_budget_exceeded_on_mr or "Warn" ) if args.applicable_on_purchase_order: budget.applicable_on_purchase_order = 1 - budget.action_if_annual_budget_exceeded_on_po = ( - args.action_if_annual_budget_exceeded_on_po or "Warn" - ) + budget.action_if_annual_budget_exceeded_on_po = args.action_if_annual_budget_exceeded_on_po or "Warn" budget.action_if_accumulated_monthly_budget_exceeded_on_po = ( args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn" ) diff --git a/erpnext/accounts/doctype/budget_account/budget_account.json b/erpnext/accounts/doctype/budget_account/budget_account.json index ead07614a7f..c7d872647f1 100644 --- a/erpnext/accounts/doctype/budget_account/budget_account.json +++ b/erpnext/accounts/doctype/budget_account/budget_account.json @@ -1,94 +1,42 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-05-16 11:54:09.286135", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2016-05-16 11:54:09.286135", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account", + "budget_amount" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "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, - "unique": 0 - }, + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Account", + "options": "Account", + "reqd": 1, + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "budget_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Budget Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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, - "unique": 0 + "fieldname": "budget_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Budget Amount", + "options": "Company:company:default_currency", + "reqd": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-01-02 17:02:53.339420", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Budget Account", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2024-03-04 15:43:27.016947", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Budget Account", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/cash_flow_mapper/cash_flow_mapper.js b/erpnext/accounts/doctype/cash_flow_mapper/cash_flow_mapper.js index 13d223ad407..5a1df905ce6 100644 --- a/erpnext/accounts/doctype/cash_flow_mapper/cash_flow_mapper.js +++ b/erpnext/accounts/doctype/cash_flow_mapper/cash_flow_mapper.js @@ -1,6 +1,4 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Cash Flow Mapper', { - -}); +frappe.ui.form.on("Cash Flow Mapper", {}); diff --git a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.js b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.js index 00c71657c5c..402dd7e2202 100644 --- a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.js +++ b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.js @@ -1,43 +1,45 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Cash Flow Mapping', { - refresh: function(frm) { +frappe.ui.form.on("Cash Flow Mapping", { + refresh: function (frm) { frm.events.disable_unchecked_fields(frm); }, - reset_check_fields: function(frm) { - frm.fields.filter(field => field.df.fieldtype === 'Check') - .map(field => frm.set_df_property(field.df.fieldname, 'read_only', 0)); + reset_check_fields: function (frm) { + frm.fields + .filter((field) => field.df.fieldtype === "Check") + .map((field) => frm.set_df_property(field.df.fieldname, "read_only", 0)); }, has_checked_field(frm) { - const val = frm.fields.filter(field => field.value === 1); + const val = frm.fields.filter((field) => field.value === 1); return val.length ? 1 : 0; }, - _disable_unchecked_fields: function(frm) { + _disable_unchecked_fields: function (frm) { // get value of clicked field - frm.fields.filter(field => field.value === 0) - .map(field => frm.set_df_property(field.df.fieldname, 'read_only', 1)); + frm.fields + .filter((field) => field.value === 0) + .map((field) => frm.set_df_property(field.df.fieldname, "read_only", 1)); }, - disable_unchecked_fields: function(frm) { + disable_unchecked_fields: function (frm) { frm.events.reset_check_fields(frm); const checked = frm.events.has_checked_field(frm); if (checked) { frm.events._disable_unchecked_fields(frm); } }, - is_working_capital: function(frm) { + is_working_capital: function (frm) { frm.events.disable_unchecked_fields(frm); }, - is_finance_cost: function(frm) { + is_finance_cost: function (frm) { frm.events.disable_unchecked_fields(frm); }, - is_income_tax_liability: function(frm) { + is_income_tax_liability: function (frm) { frm.events.disable_unchecked_fields(frm); }, - is_income_tax_expense: function(frm) { + is_income_tax_expense: function (frm) { frm.events.disable_unchecked_fields(frm); }, - is_finance_cost_adjustment: function(frm) { + is_finance_cost_adjustment: function (frm) { frm.events.disable_unchecked_fields(frm); - } + }, }); diff --git a/erpnext/accounts/doctype/cash_flow_mapping_template/cash_flow_mapping_template.js b/erpnext/accounts/doctype/cash_flow_mapping_template/cash_flow_mapping_template.js index 8611153cd8b..5b799b70131 100644 --- a/erpnext/accounts/doctype/cash_flow_mapping_template/cash_flow_mapping_template.js +++ b/erpnext/accounts/doctype/cash_flow_mapping_template/cash_flow_mapping_template.js @@ -1,6 +1,4 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Cash Flow Mapping Template', { - -}); +frappe.ui.form.on("Cash Flow Mapping Template", {}); diff --git a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.js b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.js index 2e5dce4fb57..b0e869fd608 100644 --- a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.js +++ b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.js @@ -1,6 +1,4 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Cash Flow Mapping Template Details', { - -}); +frappe.ui.form.on("Cash Flow Mapping Template Details", {}); diff --git a/erpnext/accounts/doctype/cashier_closing/cashier_closing.js b/erpnext/accounts/doctype/cashier_closing/cashier_closing.js index ce791e43acd..71664b7ada3 100644 --- a/erpnext/accounts/doctype/cashier_closing/cashier_closing.js +++ b/erpnext/accounts/doctype/cashier_closing/cashier_closing.js @@ -1,11 +1,10 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Cashier Closing', { - - setup: function(frm){ +frappe.ui.form.on("Cashier Closing", { + setup: function (frm) { if (frm.doc.user == "" || frm.doc.user == null) { frm.doc.user = frappe.session.user; } - } + }, }); diff --git a/erpnext/accounts/doctype/cashier_closing/cashier_closing.json b/erpnext/accounts/doctype/cashier_closing/cashier_closing.json index 1b38f0d36d7..051b44b5868 100644 --- a/erpnext/accounts/doctype/cashier_closing/cashier_closing.json +++ b/erpnext/accounts/doctype/cashier_closing/cashier_closing.json @@ -1,457 +1,152 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "naming_series:", - "beta": 0, "creation": "2018-06-18 16:51:49.994750", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "naming_series", + "user", + "date", + "from_time", + "time", + "expense", + "custody", + "returns", + "outstanding_amount", + "payments", + "net_amount", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "POS-CLO-", "fieldname": "naming_series", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, "in_global_search": 1, - "in_list_view": 0, "in_standard_filter": 1, "label": "Series", - "length": 0, - "no_copy": 0, "options": "POS-CLO-", - "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": 0, - "columns": 0, "fieldname": "user", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "User", - "length": 0, - "no_copy": 0, "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, "read_only": 1, - "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, "default": "Today", "fieldname": "date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Date", - "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": 0, - "columns": 0, "fieldname": "from_time", "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "From Time", - "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, - "default": "", "fieldname": "time", "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "To Time", - "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, "default": "0.00", "fieldname": "expense", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Expense", - "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": "Expense" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0.00", "fieldname": "custody", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Custody", - "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": "Custody" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0.00", "fieldname": "returns", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Returns", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "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, - "unique": 0 + "precision": "2" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0.00", "fieldname": "outstanding_amount", "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": "Outstanding Amount", - "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": 0, - "columns": 0, - "default": "0.0", "fieldname": "payments", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Payments", - "length": 0, - "no_copy": 0, - "options": "Cashier Closing Payments", - "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 + "options": "Cashier Closing Payments" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "net_amount", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Net Amount", - "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": 0, - "columns": 0, "fieldname": "amended_from", "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": "Amended From", - "length": 0, "no_copy": 1, "options": "Cashier Closing", - "permlevel": 0, "print_hide": 1, - "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 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-02-19 08:35:24.157327", + "links": [], + "modified": "2023-12-28 13:15:46.858427", "modified_by": "Administrator", "module": "Accounts", "name": "Cashier Closing", - "name_case": "", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, "submit": 1, "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": 1, - "track_seen": 0, - "track_views": 0 -} + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index 56fa6ce2f30..1d8bb853083 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -1,4 +1,4 @@ -frappe.ui.form.on('Chart of Accounts Importer', { +frappe.ui.form.on("Chart of Accounts Importer", { onload: function (frm) { frm.set_value("company", ""); frm.set_value("import_file", ""); @@ -8,31 +8,34 @@ frappe.ui.form.on('Chart of Accounts Importer', { frm.disable_save(); // make company mandatory - frm.set_df_property('company', 'reqd', frm.doc.company ? 0 : 1); - frm.set_df_property('import_file_section', 'hidden', frm.doc.company ? 0 : 1); + frm.set_df_property("company", "reqd", frm.doc.company ? 0 : 1); + frm.set_df_property("import_file_section", "hidden", frm.doc.company ? 0 : 1); if (frm.doc.import_file) { frappe.run_serially([ () => generate_tree_preview(frm), () => create_import_button(frm), - () => frm.set_df_property('chart_preview', 'hidden', 0) + () => frm.set_df_property("chart_preview", "hidden", 0), ]); } - frm.set_df_property('chart_preview', 'hidden', - $(frm.fields_dict['chart_tree'].wrapper).html()!="" ? 0 : 1); + frm.set_df_property( + "chart_preview", + "hidden", + $(frm.fields_dict["chart_tree"].wrapper).html() != "" ? 0 : 1 + ); }, - download_template: function(frm) { + download_template: function (frm) { var d = new frappe.ui.Dialog({ title: __("Download Template"), fields: [ { - label : "File Type", + label: "File Type", fieldname: "file_type", fieldtype: "Select", reqd: 1, - options: ["Excel", "CSV"] + options: ["Excel", "CSV"], }, { label: "Template Type", @@ -41,21 +44,27 @@ frappe.ui.form.on('Chart of Accounts Importer', { reqd: 1, options: ["Sample Template", "Blank Template"], change: () => { - let template_type = d.get_value('template_type'); + let template_type = d.get_value("template_type"); if (template_type === "Sample Template") { - d.set_df_property('template_type', 'description', + d.set_df_property( + "template_type", + "description", `The Sample Template contains all the required accounts pre filled in the template. - You can add more accounts or change existing accounts in the template as per your choice.`); + You can add more accounts or change existing accounts in the template as per your choice.` + ); } else { - d.set_df_property('template_type', 'description', + d.set_df_property( + "template_type", + "description", `The Blank Template contains just the account type and root type required to build the Chart - of Accounts. Please enter the account names and add more rows as per your requirement.`); + of Accounts. Please enter the account names and add more rows as per your requirement.` + ); } - } + }, }, { - label : "Company", + label: "Company", fieldname: "company", fieldtype: "Link", reqd: 1, @@ -63,25 +72,25 @@ frappe.ui.form.on('Chart of Accounts Importer', { default: frm.doc.company, }, ], - primary_action: function() { + primary_action: function () { let data = d.get_values(); if (!data.template_type) { - frappe.throw(__('Please select Template Type to download template')); + frappe.throw(__("Please select Template Type to download template")); } open_url_post( - '/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template', + "/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template", { file_type: data.file_type, template_type: data.template_type, - company: data.company + company: data.company, } ); d.hide(); }, - primary_action_label: __('Download') + primary_action_label: __("Download"), }); d.show(); }, @@ -89,7 +98,7 @@ frappe.ui.form.on('Chart of Accounts Importer', { import_file: function (frm) { if (!frm.doc.import_file) { frm.page.set_indicator(""); - $(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file + $(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper on removing file } }, @@ -99,89 +108,97 @@ frappe.ui.form.on('Chart of Accounts Importer', { frappe.call({ method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_company", args: { - company: frm.doc.company + company: frm.doc.company, }, - callback: function(r) { - if(r.message===false) { + callback: function (r) { + if (r.message === false) { frm.set_value("company", ""); - frappe.throw(__("Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions.")); + frappe.throw( + __( + "Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions." + ) + ); } else { frm.trigger("refresh"); } - } + }, }); } - } + }, }); -var create_import_button = function(frm) { - frm.page.set_primary_action(__("Import"), function () { +var create_import_button = function (frm) { + frm.page + .set_primary_action(__("Import"), function () { + return frappe.call({ + method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa", + args: { + file_name: frm.doc.import_file, + company: frm.doc.company, + }, + freeze: true, + freeze_message: __("Creating Accounts..."), + callback: function (r) { + if (!r.exc) { + clearInterval(frm.page["interval"]); + frm.page.set_indicator(__("Import Successful"), "blue"); + create_reset_button(frm); + } + }, + }); + }) + .addClass("btn btn-primary"); +}; + +var create_reset_button = function (frm) { + frm.page + .set_primary_action(__("Reset"), function () { + frm.page.clear_primary_action(); + delete frm.page["show_import_button"]; + frm.reload_doc(); + }) + .addClass("btn btn-primary"); +}; + +var validate_coa = function (frm) { + if (frm.doc.import_file) { + let parent = __("All Accounts"); return frappe.call({ - method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa", + method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa", args: { - file_name: frm.doc.import_file, - company: frm.doc.company - }, - freeze: true, - freeze_message: __("Creating Accounts..."), - callback: function(r) { - if (!r.exc) { - clearInterval(frm.page["interval"]); - frm.page.set_indicator(__('Import Successful'), 'blue'); - create_reset_button(frm); - } - } - }); - }).addClass('btn btn-primary'); -}; - -var create_reset_button = function(frm) { - frm.page.set_primary_action(__("Reset"), function () { - frm.page.clear_primary_action(); - delete frm.page["show_import_button"]; - frm.reload_doc(); - }).addClass('btn btn-primary'); -}; - -var validate_coa = function(frm) { - if (frm.doc.import_file) { - let parent = __('All Accounts'); - return frappe.call({ - 'method': 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa', - 'args': { file_name: frm.doc.import_file, parent: parent, - doctype: 'Chart of Accounts Importer', + doctype: "Chart of Accounts Importer", file_type: frm.doc.file_type, - for_validate: 1 + for_validate: 1, }, - callback: function(r) { - if (r.message['show_import_button']) { - frm.page['show_import_button'] = Boolean(r.message['show_import_button']); + callback: function (r) { + if (r.message["show_import_button"]) { + frm.page["show_import_button"] = Boolean(r.message["show_import_button"]); } - } + }, }); } }; -var generate_tree_preview = function(frm) { - let parent = __('All Accounts'); - $(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data +var generate_tree_preview = function (frm) { + let parent = __("All Accounts"); + $(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data // generate tree structure based on the csv data return new frappe.ui.Tree({ - parent: $(frm.fields_dict['chart_tree'].wrapper), + parent: $(frm.fields_dict["chart_tree"].wrapper), label: parent, expandable: true, - method: 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa', + method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa", args: { file_name: frm.doc.import_file, parent: parent, - doctype: 'Chart of Accounts Importer', - file_type: frm.doc.file_type + doctype: "Chart of Accounts Importer", + file_type: frm.doc.file_type, }, - onclick: function(node) { + onclick: function (node) { parent = node.value; - } + }, }); }; diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index ea73847555d..0846841e84c 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -26,9 +26,7 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import class ChartofAccountsImporter(Document): def validate(self): if self.import_file: - get_coa( - "Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1 - ) + get_coa("Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1) def validate_columns(data): @@ -104,7 +102,7 @@ def generate_data_from_csv(file_doc, as_dict=False): file_path = file_doc.get_full_path() data = [] - with open(file_path, "r") as in_file: + with open(file_path) as in_file: csv_reader = list(csv.reader(in_file)) headers = csv_reader[0] del csv_reader[0] # delete top row and headers row @@ -203,10 +201,10 @@ def build_forest(data): for row in data: account_name, parent_account, account_number, parent_account_number = row[0:4] if account_number: - account_name = "{} - {}".format(account_number, account_name) + account_name = f"{account_number} - {account_name}" if parent_account_number: parent_account_number = cstr(parent_account_number).strip() - parent_account = "{} - {}".format(parent_account_number, parent_account) + parent_account = f"{parent_account_number} - {parent_account}" if parent_account == account_name == child: return [parent_account] @@ -218,7 +216,7 @@ def build_forest(data): frappe.bold(parent_account) ) ) - return [child] + parent_account_list + return [child, *parent_account_list] charts_map, paths = {}, [] @@ -238,12 +236,12 @@ def build_forest(data): ) = i if not account_name: - error_messages.append("Row {0}: Please enter Account Name".format(line_no)) + error_messages.append(f"Row {line_no}: Please enter Account Name") name = account_name if account_number: account_number = cstr(account_number).strip() - account_name = "{} - {}".format(account_number, account_name) + account_name = f"{account_number} - {account_name}" charts_map[account_name] = {} charts_map[account_name]["account_name"] = name @@ -340,9 +338,9 @@ def get_template(template_type, company): def get_sample_template(writer, company): currency = frappe.db.get_value("Company", company, "default_currency") - with open(os.path.join(os.path.dirname(__file__), "coa_sample_template.csv"), "r") as f: + with open(os.path.join(os.path.dirname(__file__), "coa_sample_template.csv")) as f: for row in f: - row = row.strip().split(",") + [currency] + row = [*row.strip().split(","), currency] writer.writerow(row) return writer @@ -451,7 +449,7 @@ def unset_existing_data(company): "Purchase Taxes and Charges Template", ]: frappe.db.sql( - '''delete from `tab{0}` where `company`="%s"'''.format(doctype) % (company) # nosec + f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec ) diff --git a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js index d10c61858f1..17cb8d00e48 100644 --- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js +++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js @@ -3,18 +3,20 @@ frappe.provide("erpnext.cheque_print"); -frappe.ui.form.on('Cheque Print Template', { - refresh: function(frm) { - if(!frm.doc.__islocal) { - frm.add_custom_button(frm.doc.has_print_format?__("Update Print Format"):__("Create Print Format"), - function() { +frappe.ui.form.on("Cheque Print Template", { + refresh: function (frm) { + if (!frm.doc.__islocal) { + frm.add_custom_button( + frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"), + function () { erpnext.cheque_print.view_cheque_print(frm); - }).addClass("btn-primary"); + } + ).addClass("btn-primary"); - $(frm.fields_dict.cheque_print_preview.wrapper).empty() + $(frm.fields_dict.cheque_print_preview.wrapper).empty(); - - var template = '
    \ + var template = + '
    \
    - .print-format { + .print-format {{ padding: 0px; - } - @media screen { - .print-format { + }} + @media screen {{ + .print-format {{ padding: 0in; - } - } + }} + }} -
    -
    - +
    + - %(message_to_show)s + {message_to_show} - - {{ frappe.utils.formatdate(doc.reference_date) or '' }} + {{{{ frappe.utils.formatdate(doc.reference_date) or '' }}}} - - {{ doc.account_no or '' }} + {{{{ doc.account_no or '' }}}} - - {{doc.party_name}} + {{{{doc.party_name}}}} - - {{frappe.utils.money_in_words(doc.base_paid_amount or doc.base_received_amount)}} + + {{{{frappe.utils.money_in_words(doc.base_paid_amount or doc.base_received_amount)}}}} - - {{doc.get_formatted("base_paid_amount") or doc.get_formatted("base_received_amount")}} + {{{{doc.get_formatted("base_paid_amount") or doc.get_formatted("base_received_amount")}}}} - - {{doc.company}} + {{{{doc.company}}}}
    -
    """ % { - "starting_position_from_top_edge": doc.starting_position_from_top_edge +
    """.format( + starting_position_from_top_edge=doc.starting_position_from_top_edge if doc.cheque_size == "A4" else 0.0, - "cheque_width": doc.cheque_width, - "cheque_height": doc.cheque_height, - "acc_pay_dist_from_top_edge": doc.acc_pay_dist_from_top_edge, - "acc_pay_dist_from_left_edge": doc.acc_pay_dist_from_left_edge, - "message_to_show": doc.message_to_show if doc.message_to_show else _("Account Pay Only"), - "date_dist_from_top_edge": doc.date_dist_from_top_edge, - "date_dist_from_left_edge": doc.date_dist_from_left_edge, - "acc_no_dist_from_top_edge": doc.acc_no_dist_from_top_edge, - "acc_no_dist_from_left_edge": doc.acc_no_dist_from_left_edge, - "payer_name_from_top_edge": doc.payer_name_from_top_edge, - "payer_name_from_left_edge": doc.payer_name_from_left_edge, - "amt_in_words_from_top_edge": doc.amt_in_words_from_top_edge, - "amt_in_words_from_left_edge": doc.amt_in_words_from_left_edge, - "amt_in_word_width": doc.amt_in_word_width, - "amt_in_words_line_spacing": doc.amt_in_words_line_spacing, - "amt_in_figures_from_top_edge": doc.amt_in_figures_from_top_edge, - "amt_in_figures_from_left_edge": doc.amt_in_figures_from_left_edge, - "signatory_from_top_edge": doc.signatory_from_top_edge, - "signatory_from_left_edge": doc.signatory_from_left_edge, - } + cheque_width=doc.cheque_width, + cheque_height=doc.cheque_height, + acc_pay_dist_from_top_edge=doc.acc_pay_dist_from_top_edge, + acc_pay_dist_from_left_edge=doc.acc_pay_dist_from_left_edge, + message_to_show=doc.message_to_show if doc.message_to_show else _("Account Pay Only"), + date_dist_from_top_edge=doc.date_dist_from_top_edge, + date_dist_from_left_edge=doc.date_dist_from_left_edge, + acc_no_dist_from_top_edge=doc.acc_no_dist_from_top_edge, + acc_no_dist_from_left_edge=doc.acc_no_dist_from_left_edge, + payer_name_from_top_edge=doc.payer_name_from_top_edge, + payer_name_from_left_edge=doc.payer_name_from_left_edge, + amt_in_words_from_top_edge=doc.amt_in_words_from_top_edge, + amt_in_words_from_left_edge=doc.amt_in_words_from_left_edge, + amt_in_word_width=doc.amt_in_word_width, + amt_in_words_line_spacing=doc.amt_in_words_line_spacing, + amt_in_figures_from_top_edge=doc.amt_in_figures_from_top_edge, + amt_in_figures_from_left_edge=doc.amt_in_figures_from_left_edge, + signatory_from_top_edge=doc.signatory_from_top_edge, + signatory_from_left_edge=doc.signatory_from_left_edge, + ) cheque_print.save(ignore_permissions=True) diff --git a/erpnext/accounts/doctype/cost_center/cost_center.js b/erpnext/accounts/doctype/cost_center/cost_center.js index 632fab0197c..c2e4442a9f8 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.js +++ b/erpnext/accounts/doctype/cost_center/cost_center.js @@ -3,75 +3,80 @@ frappe.provide("erpnext.accounts"); - - -frappe.ui.form.on('Cost Center', { - onload: function(frm) { - frm.set_query("parent_cost_center", function() { +frappe.ui.form.on("Cost Center", { + onload: function (frm) { + frm.set_query("parent_cost_center", function () { return { filters: { company: frm.doc.company, - is_group: 1 - } - } + is_group: 1, + }, + }; }); }, - refresh: function(frm) { + refresh: function (frm) { if (!frm.is_new()) { - frm.add_custom_button(__('Update Cost Center Name / Number'), function () { + frm.add_custom_button(__("Update Cost Center Name / Number"), function () { frm.trigger("update_cost_center_number"); }); } - let intro_txt = ''; + let intro_txt = ""; let doc = frm.doc; - frm.toggle_display('cost_center_name', doc.__islocal); - frm.toggle_enable(['is_group', 'company'], doc.__islocal); + frm.toggle_display("cost_center_name", doc.__islocal); + frm.toggle_enable(["is_group", "company"], doc.__islocal); - if(!doc.__islocal && doc.is_group==1) { - intro_txt += __('Note: This Cost Center is a Group. Cannot make accounting entries against groups.'); + if (!doc.__islocal && doc.is_group == 1) { + intro_txt += __( + "Note: This Cost Center is a Group. Cannot make accounting entries against groups." + ); } frm.events.hide_unhide_group_ledger(frm); - frm.toggle_display('sb1', doc.is_group==0); + frm.toggle_display("sb1", doc.is_group == 0); frm.set_intro(intro_txt); - if(!frm.doc.__islocal) { - frm.add_custom_button(__('Chart of Cost Centers'), - function() { frappe.set_route("Tree", "Cost Center"); }); + if (!frm.doc.__islocal) { + frm.add_custom_button(__("Chart of Cost Centers"), function () { + frappe.set_route("Tree", "Cost Center"); + }); - frm.add_custom_button(__('Budget'), - function() { frappe.set_route("List", "Budget", {'cost_center': frm.doc.name}); }); + frm.add_custom_button(__("Budget"), function () { + frappe.set_route("List", "Budget", { cost_center: frm.doc.name }); + }); } }, - update_cost_center_number: function(frm) { + update_cost_center_number: function (frm) { var d = new frappe.ui.Dialog({ - title: __('Update Cost Center Name / Number'), + title: __("Update Cost Center Name / Number"), fields: [ { - "label": "Cost Center Name", - "fieldname": "cost_center_name", - "fieldtype": "Data", - "reqd": 1, - "default": frm.doc.cost_center_name + label: "Cost Center Name", + fieldname: "cost_center_name", + fieldtype: "Data", + reqd: 1, + default: frm.doc.cost_center_name, }, { - "label": "Cost Center Number", - "fieldname": "cost_center_number", - "fieldtype": "Data", - "default": frm.doc.cost_center_number + label: "Cost Center Number", + fieldname: "cost_center_number", + fieldtype: "Data", + default: frm.doc.cost_center_number, }, { - "label": __("Merge with existing"), - "fieldname": "merge", - "fieldtype": "Check", - "default": 0 - } + label: __("Merge with existing"), + fieldname: "merge", + fieldtype: "Check", + default: 0, + }, ], - primary_action: function() { + primary_action: function () { var data = d.get_values(); - if(data.cost_center_name === frm.doc.cost_center_name && data.cost_center_number === frm.doc.cost_center_number) { + if ( + data.cost_center_name === frm.doc.cost_center_name && + data.cost_center_number === frm.doc.cost_center_number + ) { d.hide(); return; } @@ -83,12 +88,12 @@ frappe.ui.form.on('Cost Center', { cost_center_name: data.cost_center_name, cost_center_number: cstr(data.cost_center_number), company: frm.doc.company, - merge: data.merge + merge: data.merge, }, - callback: function(r) { + callback: function (r) { frappe.dom.unfreeze(); - if(!r.exc) { - if(r.message) { + if (!r.exc) { + if (r.message) { frappe.set_route("Form", "Cost Center", r.message); } else { me.frm.set_value("cost_center_name", data.cost_center_name); @@ -96,44 +101,42 @@ frappe.ui.form.on('Cost Center', { } d.hide(); } - } + }, }); }, - primary_action_label: __('Update') + primary_action_label: __("Update"), }); d.show(); }, parent_cost_center(frm) { - if(!frm.doc.company) { - frappe.msgprint(__('Please enter company name first')); + if (!frm.doc.company) { + frappe.msgprint(__("Please enter company name first")); } }, hide_unhide_group_ledger(frm) { let doc = frm.doc; if (doc.is_group == 1) { - frm.add_custom_button(__('Convert to Non-Group'), - () => frm.events.convert_to_ledger(frm)); + frm.add_custom_button(__("Convert to Non-Group"), () => frm.events.convert_to_ledger(frm)); } else if (doc.is_group == 0) { - frm.add_custom_button(__('Convert to Group'), - () => frm.events.convert_to_group(frm)); + frm.add_custom_button(__("Convert to Group"), () => frm.events.convert_to_group(frm)); } }, convert_to_group(frm) { - frm.call('convert_ledger_to_group').then(r => { - if(r.message === 1) { + frm.call("convert_ledger_to_group").then((r) => { + if (r.message === 1) { frm.refresh(); } }); }, convert_to_ledger(frm) { - frm.call('convert_group_to_ledger').then(r => { - if(r.message === 1) { + frm.call("convert_group_to_ledger").then((r) => { + if (r.message === 1) { frm.refresh(); } }); - } + }, }); diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json index 7cbb290947e..135f5fdcb3e 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.json +++ b/erpnext/accounts/doctype/cost_center/cost_center.json @@ -125,7 +125,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2022-01-31 13:22:58.916273", + "modified": "2024-04-24 10:55:54.083042", "modified_by": "Administrator", "module": "Accounts", "name": "Cost Center", @@ -163,6 +163,15 @@ { "read": 1, "role": "Purchase User" + }, + { + "email": 1, + "export": 1, + "print": 1, + "report": 1, + "role": "Employee", + "select": 1, + "share": 1 } ], "search_fields": "parent_cost_center, is_group", diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index e8b34bbf034..ec6c17ad639 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -15,9 +15,7 @@ class CostCenter(NestedSet): def autoname(self): from erpnext.accounts.utils import get_autoname_with_number - self.name = get_autoname_with_number( - self.cost_center_number, self.cost_center_name, self.company - ) + self.name = get_autoname_with_number(self.cost_center_number, self.cost_center_name, self.company) def validate(self): self.validate_mandatory() @@ -90,14 +88,14 @@ class CostCenter(NestedSet): new_cost_center = get_name_with_abbr(newdn, self.company) # Validate properties before merging - super(CostCenter, self).before_rename(olddn, new_cost_center, merge, "is_group") + super().before_rename(olddn, new_cost_center, merge, "is_group") if not merge: new_cost_center = get_name_with_number(new_cost_center, self.cost_center_number) return new_cost_center def after_rename(self, olddn, newdn, merge=False): - super(CostCenter, self).after_rename(olddn, newdn, merge) + super().after_rename(olddn, newdn, merge) if not merge: new_cost_center = frappe.db.get_value( diff --git a/erpnext/accounts/doctype/cost_center/cost_center_tree.js b/erpnext/accounts/doctype/cost_center/cost_center_tree.js index 1d482c58f1a..3edeb8efb0b 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center_tree.js +++ b/erpnext/accounts/doctype/cost_center/cost_center_tree.js @@ -1,54 +1,84 @@ frappe.treeview_settings["Cost Center"] = { breadcrumb: "Accounts", get_tree_root: false, - filters: [{ - fieldname: "company", - fieldtype:"Select", - options: erpnext.utils.get_tree_options("company"), - label: __("Company"), - default: erpnext.utils.get_tree_default("company") - }], - root_label: "Cost Centers", - get_tree_nodes: 'erpnext.accounts.utils.get_children', - add_tree_node: 'erpnext.accounts.utils.add_cc', - menu_items:[ + filters: [ { - label: __('New Company'), - action: function() { frappe.new_doc("Company", true) }, - condition: 'frappe.boot.user.can_create.indexOf("Company") !== -1' - } + fieldname: "company", + fieldtype: "Select", + options: erpnext.utils.get_tree_options("company"), + label: __("Company"), + default: erpnext.utils.get_tree_default("company"), + }, ], - fields:[ - {fieldtype:'Data', fieldname:'cost_center_name', label:__('New Cost Center Name'), reqd:true}, - {fieldtype:'Check', fieldname:'is_group', label:__('Is Group'), - description:__('Further cost centers can be made under Groups but entries can be made against non-Groups')}, - {fieldtype:'Data', fieldname:'cost_center_number', label:__('Cost Center Number'), - description: __("Number of new Cost Center, it will be included in the cost center name as a prefix")} + root_label: "Cost Centers", + get_tree_nodes: "erpnext.accounts.utils.get_children", + add_tree_node: "erpnext.accounts.utils.add_cc", + menu_items: [ + { + label: __("New Company"), + action: function () { + frappe.new_doc("Company", true); + }, + condition: 'frappe.boot.user.can_create.indexOf("Company") !== -1', + }, ], - ignore_fields:["parent_cost_center"], - onload: function(treeview) { + fields: [ + { fieldtype: "Data", fieldname: "cost_center_name", label: __("New Cost Center Name"), reqd: true }, + { + fieldtype: "Check", + fieldname: "is_group", + label: __("Is Group"), + description: __( + "Further cost centers can be made under Groups but entries can be made against non-Groups" + ), + }, + { + fieldtype: "Data", + fieldname: "cost_center_number", + label: __("Cost Center Number"), + description: __( + "Number of new Cost Center, it will be included in the cost center name as a prefix" + ), + }, + ], + ignore_fields: ["parent_cost_center"], + onload: function (treeview) { function get_company() { return treeview.page.fields_dict.company.get_value(); } // tools - treeview.page.add_inner_button(__("Chart of Accounts"), function() { - frappe.set_route('Tree', 'Account', {company: get_company()}); - }, __('View')); + treeview.page.add_inner_button( + __("Chart of Accounts"), + function () { + frappe.set_route("Tree", "Account", { company: get_company() }); + }, + __("View") + ); // make - treeview.page.add_inner_button(__("Budget List"), function() { - frappe.set_route('List', 'Budget', {company: get_company()}); - }, __('Budget')); + treeview.page.add_inner_button( + __("Budget List"), + function () { + frappe.set_route("List", "Budget", { company: get_company() }); + }, + __("Budget") + ); - treeview.page.add_inner_button(__("Monthly Distribution"), function() { - frappe.set_route('List', 'Monthly Distribution', {company: get_company()}); - }, __('Budget')); + treeview.page.add_inner_button( + __("Monthly Distribution"), + function () { + frappe.set_route("List", "Monthly Distribution", { company: get_company() }); + }, + __("Budget") + ); - treeview.page.add_inner_button(__("Budget Variance Report"), function() { - frappe.set_route('query-report', 'Budget Variance Report', {company: get_company()}); - }, __('Budget')); - - } - -} + treeview.page.add_inner_button( + __("Budget Variance Report"), + function () { + frappe.set_route("query-report", "Budget Variance Report", { company: get_company() }); + }, + __("Budget") + ); + }, +}; diff --git a/erpnext/accounts/doctype/cost_center/test_cost_center.py b/erpnext/accounts/doctype/cost_center/test_cost_center.py index 2ec16092d4d..7d01918fb54 100644 --- a/erpnext/accounts/doctype/cost_center/test_cost_center.py +++ b/erpnext/accounts/doctype/cost_center/test_cost_center.py @@ -10,7 +10,6 @@ test_records = frappe.get_test_records("Cost Center") class TestCostCenter(unittest.TestCase): def test_cost_center_creation_against_child_node(self): - if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}): frappe.get_doc(test_records[1]).insert() diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js index ab0baab24a0..8adc5b21215 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js +++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js @@ -1,19 +1,24 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Cost Center Allocation', { - setup: function(frm) { - let filters = {"is_group": 0}; - if (frm.doc.company) { - $.extend(filters, { - "company": frm.doc.company - }); - } - - frm.set_query('main_cost_center', function() { +frappe.ui.form.on("Cost Center Allocation", { + setup: function (frm) { + frm.set_query("main_cost_center", function () { return { - filters: filters + filters: { + company: frm.doc.company, + is_group: 0, + }, }; }); - } + + frm.set_query("cost_center", "allocation_percentages", function () { + return { + filters: { + company: frm.doc.company, + is_group: 0, + }, + }; + }); + }, }); diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py index 54ffe21a152..c6cf232ceb8 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py +++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py @@ -29,7 +29,7 @@ class InvalidDateError(frappe.ValidationError): class CostCenterAllocation(Document): def __init__(self, *args, **kwargs): - super(CostCenterAllocation, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._skip_from_date_validation = False def validate(self): @@ -44,9 +44,7 @@ class CostCenterAllocation(Document): total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])]) if total_percentage != 100: - frappe.throw( - _("Total percentage against cost centers should be 100"), WrongPercentageAllocation - ) + frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation) def validate_from_date_based_on_existing_gle(self): # Check if GLE exists against the main cost center diff --git a/erpnext/accounts/doctype/coupon_code/coupon_code.js b/erpnext/accounts/doctype/coupon_code/coupon_code.js index da3a9f8132f..f8b5bdc35f5 100644 --- a/erpnext/accounts/doctype/coupon_code/coupon_code.js +++ b/erpnext/accounts/doctype/coupon_code/coupon_code.js @@ -1,44 +1,41 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Coupon Code', { - setup: function(frm) { - frm.set_query("pricing_rule", function() { +frappe.ui.form.on("Coupon Code", { + setup: function (frm) { + frm.set_query("pricing_rule", function () { return { - filters: [ - ["Pricing Rule","coupon_code_based", "=", "1"] - ] + filters: [["Pricing Rule", "coupon_code_based", "=", "1"]], }; }); }, - coupon_name:function(frm){ - if (frm.doc.__islocal===1) { + coupon_name: function (frm) { + if (frm.doc.__islocal === 1) { frm.trigger("make_coupon_code"); } }, - coupon_type:function(frm){ - if (frm.doc.__islocal===1) { + coupon_type: function (frm) { + if (frm.doc.__islocal === 1) { frm.trigger("make_coupon_code"); } }, - make_coupon_code: function(frm) { - var coupon_name=frm.doc.coupon_name; + make_coupon_code: function (frm) { + var coupon_name = frm.doc.coupon_name; var coupon_code; - if (frm.doc.coupon_type=='Gift Card') { - coupon_code=Math.random().toString(12).substring(2, 12).toUpperCase(); + if (frm.doc.coupon_type == "Gift Card") { + coupon_code = Math.random().toString(12).substring(2, 12).toUpperCase(); + } else if (frm.doc.coupon_type == "Promotional") { + coupon_name = coupon_name.replace(/\s/g, ""); + coupon_code = coupon_name.toUpperCase().slice(0, 8); } - else if(frm.doc.coupon_type=='Promotional'){ - coupon_name=coupon_name.replace(/\s/g,''); - coupon_code=coupon_name.toUpperCase().slice(0,8); - } - frm.doc.coupon_code=coupon_code; - frm.refresh_field('coupon_code'); + frm.doc.coupon_code = coupon_code; + frm.refresh_field("coupon_code"); }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.pricing_rule) { - frm.add_custom_button(__("Add/Edit Coupon Conditions"), function(){ + frm.add_custom_button(__("Add/Edit Coupon Conditions"), function () { frappe.set_route("Form", "Pricing Rule", frm.doc.pricing_rule); }); } - } + }, }); diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js index 6c40f2bec0d..ad68352c2a4 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js @@ -1,28 +1,41 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Currency Exchange Settings', { - service_provider: function(frm) { - if (frm.doc.service_provider == "exchangerate.host") { - let result = ['result']; - let params = { - date: '{transaction_date}', - from: '{from_currency}', - to: '{to_currency}' - }; - add_param(frm, "https://api.exchangerate.host/convert", params, result); - } else if (frm.doc.service_provider == "frankfurter.app") { - let result = ['rates', '{to_currency}']; - let params = { - base: '{from_currency}', - symbols: '{to_currency}' - }; - add_param(frm, "https://frankfurter.app/{transaction_date}", params, result); - } - } +frappe.ui.form.on("Currency Exchange Settings", { + service_provider: function (frm) { + frm.call({ + method: "erpnext.accounts.doctype.currency_exchange_settings.currency_exchange_settings.get_api_endpoint", + args: { + service_provider: frm.doc.service_provider, + use_http: frm.doc.use_http, + }, + callback: function (r) { + if (r && r.message) { + if (frm.doc.service_provider == "exchangerate.host") { + let result = ["result"]; + let params = { + date: "{transaction_date}", + from: "{from_currency}", + to: "{to_currency}", + }; + add_param(frm, r.message, params, result); + } else if (frm.doc.service_provider == "frankfurter.app") { + let result = ["rates", "{to_currency}"]; + let params = { + base: "{from_currency}", + symbols: "{to_currency}", + }; + add_param(frm, r.message, params, result); + } + } + }, + }); + }, + use_http: function (frm) { + frm.trigger("service_provider"); + }, }); - function add_param(frm, api, params, result) { var row; frm.clear_table("req_params"); @@ -30,13 +43,13 @@ function add_param(frm, api, params, result) { frm.doc.api_endpoint = api; - $.each(params, function(key, value) { + $.each(params, function (key, value) { row = frm.add_child("req_params"); row.key = key; row.value = value; }); - $.each(result, function(key, value) { + $.each(result, function (key, value) { row = frm.add_child("result_key"); row.key = value; }); diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index df232a5848c..bd90b8add80 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -9,6 +9,7 @@ "disabled", "service_provider", "api_endpoint", + "use_http", "access_key", "url", "column_break_3", @@ -91,12 +92,19 @@ "fieldname": "access_key", "fieldtype": "Data", "label": "Access Key" + }, + { + "default": "0", + "depends_on": "eval: doc.service_provider != \"Custom\"", + "fieldname": "use_http", + "fieldtype": "Check", + "label": "Use HTTP Protocol" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-04 15:30:25.333860", + "modified": "2024-03-18 08:32:26.895076", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py index 117d5ff21e8..39aff032b3d 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -18,7 +18,6 @@ class CurrencyExchangeSettings(Document): def set_parameters_and_result(self): if self.service_provider == "exchangerate.host": - if not self.access_key: frappe.throw( _("Access Key is required for Service Provider: {0}").format( @@ -29,7 +28,7 @@ class CurrencyExchangeSettings(Document): self.set("result_key", []) self.set("req_params", []) - self.api_endpoint = "https://api.exchangerate.host/convert" + self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http) self.append("result_key", {"key": "result"}) self.append("req_params", {"key": "access_key", "value": self.access_key}) self.append("req_params", {"key": "amount", "value": "1"}) @@ -40,7 +39,7 @@ class CurrencyExchangeSettings(Document): self.set("result_key", []) self.set("req_params", []) - self.api_endpoint = "https://frankfurter.app/{transaction_date}" + self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http) self.append("result_key", {"key": "rates"}) self.append("result_key", {"key": "{to_currency}"}) self.append("req_params", {"key": "base", "value": "{from_currency}"}) @@ -53,9 +52,7 @@ class CurrencyExchangeSettings(Document): transaction_date=nowdate(), to_currency="INR", from_currency="USD" ) - api_url = self.api_endpoint.format( - transaction_date=nowdate(), to_currency="INR", from_currency="USD" - ) + api_url = self.api_endpoint.format(transaction_date=nowdate(), to_currency="INR", from_currency="USD") try: response = requests.get(api_url, params=params) @@ -75,7 +72,23 @@ class CurrencyExchangeSettings(Document): ] except Exception: frappe.throw(_("Invalid result key. Response:") + " " + response.text) - if not isinstance(value, (int, float)): + if not isinstance(value, int | float): frappe.throw(_("Returned exchange rate is neither integer not float.")) self.url = response.url + + +@frappe.whitelist() +def get_api_endpoint(service_provider: str | None = None, use_http: bool = False): + if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]: + if service_provider == "exchangerate.host": + api = "api.exchangerate.host/convert" + elif service_provider == "frankfurter.app": + api = "frankfurter.app/{transaction_date}" + + protocol = "https://" + if use_http: + protocol = "http://" + + return protocol + api + return None diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 9909c6c2ab0..5ddee638dea 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -9,7 +9,7 @@ frappe.ui.form.on("Dunning", { docstatus: 1, company: frm.doc.company, outstanding_amount: [">", 0], - status: "Overdue" + status: "Overdue", }, }; }); @@ -18,18 +18,14 @@ frappe.ui.form.on("Dunning", { filters: { company: frm.doc.company, root_type: "Income", - is_group: 0 - } + is_group: 0, + }, }; }); }, 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 - ); + 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,22 +36,27 @@ 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( + __("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") + ); } }, overdue_days: function (frm) { @@ -86,8 +87,7 @@ frappe.ui.form.on("Dunning", { get_dunning_letter_text: function (frm) { if (frm.doc.dunning_type) { frappe.call({ - method: - "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text", + method: "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text", args: { dunning_type: frm.doc.dunning_type, language: frm.doc.language, @@ -129,26 +129,25 @@ frappe.ui.form.on("Dunning", { }, 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" - ); + const overdue_days = moment(frm.doc.posting_date).diff(frm.doc.due_date, "days"); frm.set_value("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')); + 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); }, make_payment_entry: function (frm) { return frappe.call({ - method: - "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry", + method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry", args: { dt: frm.doc.doctype, dn: frm.doc.name, diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.js b/erpnext/accounts/doctype/dunning_type/dunning_type.js index 54156b488dd..5fa4fd411db 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.js +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.js @@ -1,8 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Dunning Type', { +frappe.ui.form.on("Dunning Type", { // refresh: function(frm) { - // } }); diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js index 1ef5c837402..741039acae6 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js @@ -1,75 +1,79 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Exchange Rate Revaluation', { - setup: function(frm) { - frm.set_query("party_type", "accounts", function() { +frappe.ui.form.on("Exchange Rate Revaluation", { + setup: function (frm) { + frm.set_query("party_type", "accounts", function () { return { - "filters": { - "name": ["in", Object.keys(frappe.boot.party_account_types)], - } + filters: { + name: ["in", Object.keys(frappe.boot.party_account_types)], + }, }; }); - frm.set_query("account", "accounts", function(doc) { + frm.set_query("account", "accounts", function (doc) { return { - "filters": { - "company": doc.company - } + filters: { + company: doc.company, + }, }; }); }, - refresh: function(frm) { - if(frm.doc.docstatus==1) { + refresh: function (frm) { + if (frm.doc.docstatus == 1) { frappe.call({ - method: 'check_journal_entry_condition', + method: "check_journal_entry_condition", doc: frm.doc, - callback: function(r) { + callback: function (r) { if (r.message) { - frm.add_custom_button(__('Journal Entries'), function() { - return frm.events.make_jv(frm); - }, __('Create')); + frm.add_custom_button( + __("Journal Entries"), + function () { + return frm.events.make_jv(frm); + }, + __("Create") + ); } - } + }, }); } }, - validate_rounding_loss: function(frm) { + validate_rounding_loss: function (frm) { let allowance = frm.doc.rounding_loss_allowance; if (!(allowance >= 0 && allowance < 1)) { frappe.throw(__("Rounding Loss Allowance should be between 0 and 1")); } }, - rounding_loss_allowance: function(frm) { + rounding_loss_allowance: function (frm) { frm.events.validate_rounding_loss(frm); }, - validate: function(frm) { + validate: function (frm) { frm.events.validate_rounding_loss(frm); }, - get_entries: function(frm, account) { + get_entries: function (frm, account) { frappe.call({ method: "get_accounts_data", doc: cur_frm.doc, account: account, - callback: function(r){ + callback: function (r) { frappe.model.clear_table(frm.doc, "accounts"); - if(r.message) { + if (r.message) { r.message.forEach((d) => { - cur_frm.add_child("accounts",d); + cur_frm.add_child("accounts", d); }); frm.events.get_total_gain_loss(frm); refresh_field("accounts"); } - } + }, }); }, - get_total_gain_loss: function(frm) { - if(!(frm.doc.accounts && frm.doc.accounts.length)) return; + get_total_gain_loss: function (frm) { + if (!(frm.doc.accounts && frm.doc.accounts.length)) return; let total_gain_loss = 0; frm.doc.accounts.forEach((d) => { @@ -80,7 +84,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', { frm.refresh_fields(); }, - make_jv : function(frm) { + make_jv: function (frm) { let revaluation_journal = null; let zero_balance_journal = null; frappe.call({ @@ -88,66 +92,68 @@ frappe.ui.form.on('Exchange Rate Revaluation', { doc: frm.doc, freeze: true, freeze_message: "Making Journal Entries...", - callback: function(r){ + callback: function (r) { if (r.message) { let response = r.message; - if(response['revaluation_jv'] || response['zero_balance_jv']) { + if (response["revaluation_jv"] || response["zero_balance_jv"]) { frappe.msgprint(__("Journals have been created")); } } - } + }, }); - } + }, }); frappe.ui.form.on("Exchange Rate Revaluation Account", { - new_exchange_rate: function(frm, cdt, cdn) { + new_exchange_rate: function (frm, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); - row.new_balance_in_base_currency = flt(row.new_exchange_rate * flt(row.balance_in_account_currency), - precision("new_balance_in_base_currency", row)); + row.new_balance_in_base_currency = flt( + row.new_exchange_rate * flt(row.balance_in_account_currency), + precision("new_balance_in_base_currency", row) + ); row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency); refresh_field("accounts"); frm.events.get_total_gain_loss(frm); }, - account: function(frm, cdt, cdn) { + account: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; if (row.account) { get_account_details(frm, cdt, cdn); } }, - party: function(frm, cdt, cdn) { + party: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; if (row.party && row.account) { get_account_details(frm, cdt, cdn); } }, - accounts_remove: function(frm) { + accounts_remove: function (frm) { frm.events.get_total_gain_loss(frm); - } + }, }); -var get_account_details = function(frm, cdt, cdn) { +var get_account_details = function (frm, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); - if(!frm.doc.company || !frm.doc.posting_date) { + if (!frm.doc.company || !frm.doc.posting_date) { frappe.throw(__("Please select Company and Posting Date to getting entries")); } frappe.call({ method: "erpnext.accounts.doctype.exchange_rate_revaluation.exchange_rate_revaluation.get_account_details", - args:{ + args: { account: row.account, company: frm.doc.company, posting_date: frm.doc.posting_date, party_type: row.party_type, party: row.party, - rounding_loss_allowance: frm.doc.rounding_loss_allowance + rounding_loss_allowance: frm.doc.rounding_loss_allowance, }, - callback: function(r){ + callback: function (r) { $.extend(row, r.message); refresh_field("accounts"); frm.events.get_total_gain_loss(frm); - } + }, }); }; 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 977cfe94f8a..24a325b9164 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -246,7 +246,6 @@ class ExchangeRateRevaluation(Document): # Handle Accounts with '0' balance in Account/Base Currency for d in [x for x in account_details if x.zero_balance]: - if d.balance != 0: current_exchange_rate = new_exchange_rate = 0 @@ -259,7 +258,8 @@ class ExchangeRateRevaluation(Document): new_balance_in_account_currency = 0 current_exchange_rate = ( - calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party) or 0.0 + calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party) + or 0.0 ) gain_loss = new_balance_in_account_currency - ( @@ -313,9 +313,7 @@ class ExchangeRateRevaluation(Document): revaluation_jv = self.make_jv_for_revaluation() if revaluation_jv: - frappe.msgprint( - f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}" - ) + frappe.msgprint(f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}") return { "revaluation_jv": revaluation_jv.name if revaluation_jv else None, @@ -372,7 +370,8 @@ class ExchangeRateRevaluation(Document): journal_account.update( { dr_or_cr: flt( - abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") + abs(d.get("balance_in_account_currency")), + d.precision("balance_in_account_currency"), ), reverse_dr_or_cr: 0, "debit": 0, @@ -498,7 +497,9 @@ class ExchangeRateRevaluation(Document): abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") ), "cost_center": erpnext.get_default_cost_center(self.company), - "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")), + "exchange_rate": flt( + d.get("current_exchange_rate"), d.precision("current_exchange_rate") + ), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, } @@ -576,7 +577,7 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party): @frappe.whitelist() def get_account_details( - company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float = None + company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float | None = None ): if not (company and posting_date): frappe.throw(_("Company and Posting Date is mandatory")) @@ -589,7 +590,7 @@ def get_account_details( frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type)) account_details = {} - company_currency = erpnext.get_company_currency(company) + erpnext.get_company_currency(company) account_details = { "account_currency": account_currency, @@ -603,24 +604,22 @@ def get_account_details( rounding_loss_allowance=rounding_loss_allowance, ) - if account_balance and ( - account_balance[0].balance or account_balance[0].balance_in_account_currency - ): - account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance( + if account_balance and (account_balance[0].balance or account_balance[0].balance_in_account_currency): + if account_with_new_balance := ExchangeRateRevaluation.calculate_new_account_balance( company, posting_date, account_balance - ) - row = account_with_new_balance[0] - account_details.update( - { - "balance_in_base_currency": row["balance_in_base_currency"], - "balance_in_account_currency": row["balance_in_account_currency"], - "current_exchange_rate": row["current_exchange_rate"], - "new_exchange_rate": row["new_exchange_rate"], - "new_balance_in_base_currency": row["new_balance_in_base_currency"], - "new_balance_in_account_currency": row["new_balance_in_account_currency"], - "zero_balance": row["zero_balance"], - "gain_loss": row["gain_loss"], - } - ) + ): + row = account_with_new_balance[0] + account_details.update( + { + "balance_in_base_currency": row["balance_in_base_currency"], + "balance_in_account_currency": row["balance_in_account_currency"], + "current_exchange_rate": row["current_exchange_rate"], + "new_exchange_rate": row["new_exchange_rate"], + "new_balance_in_base_currency": row["new_balance_in_base_currency"], + "new_balance_in_account_currency": row["new_balance_in_account_currency"], + "zero_balance": row["zero_balance"], + "gain_loss": row["gain_loss"], + } + ) return account_details diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index ced04ced3fd..709be43e699 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -1,21 +1,14 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe -from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, today -from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry -from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry -from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.accounts.party import get_party_account from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -from erpnext.stock.doctype.item.test_item import create_item class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase): @@ -73,9 +66,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase): err.extend("accounts", accounts) row = err.accounts[0] row.new_exchange_rate = 85 - row.new_balance_in_base_currency = flt( - row.new_exchange_rate * flt(row.balance_in_account_currency) - ) + row.new_balance_in_base_currency = flt(row.new_exchange_rate * flt(row.balance_in_account_currency)) row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) err.set_total_gain_loss() err = err.save().submit() @@ -127,9 +118,9 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase): pe.save().submit() # Cancel the auto created gain/loss JE to simulate balance only in base currency - je = frappe.db.get_all( - "Journal Entry Account", filters={"reference_name": si.name}, pluck="parent" - )[0] + je = frappe.db.get_all("Journal Entry Account", filters={"reference_name": si.name}, pluck="parent")[ + 0 + ] frappe.get_doc("Journal Entry", je).cancel() err = frappe.new_doc("Exchange Rate Revaluation") @@ -235,9 +226,9 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase): self.assertEqual(flt(acc.debit, precision), 0.0) self.assertEqual(flt(acc.credit, precision), 0.0) - row = [x for x in je.accounts if x.account == self.debtors_usd][0] + row = next(x for x in je.accounts if x.account == self.debtors_usd) self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD - row = [x for x in je.accounts if x.account != self.debtors_usd][0] + row = next(x for x in je.accounts if x.account != self.debtors_usd) self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR # total_debit and total_credit will be 0.0, as JV is posting only to account currency fields @@ -294,5 +285,5 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase): "new_balance_in_account_currency": 100.0, } - for key, val in expected_data.items(): + for key, _val in expected_data.items(): self.assertEqual(expected_data.get(key), account_details.get(key)) diff --git a/erpnext/accounts/doctype/finance_book/finance_book.js b/erpnext/accounts/doctype/finance_book/finance_book.js index 71191bb0cfa..ebe1d246ef1 100644 --- a/erpnext/accounts/doctype/finance_book/finance_book.js +++ b/erpnext/accounts/doctype/finance_book/finance_book.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Finance Book', { - refresh: function(frm) { - - } +frappe.ui.form.on("Finance Book", { + refresh: function (frm) {}, }); diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js index 508b2eaf2a4..a44b52f08f8 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js @@ -1,17 +1,21 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Fiscal Year', { - onload: function(frm) { - if(frm.doc.__islocal) { - frm.set_value("year_start_date", - frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)); +frappe.ui.form.on("Fiscal Year", { + onload: function (frm) { + if (frm.doc.__islocal) { + frm.set_value( + "year_start_date", + frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1) + ); } }, - year_start_date: function(frm) { + year_start_date: function (frm) { if (!frm.doc.is_short_year) { - let year_end_date = - frappe.datetime.add_days(frappe.datetime.add_months(frm.doc.year_start_date, 12), -1); + let year_end_date = frappe.datetime.add_days( + frappe.datetime.add_months(frm.doc.year_start_date, 12), + -1 + ); frm.set_value("year_end_date", year_end_date); } }, diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json index 5ab91f2506c..ff4ac50850f 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json @@ -118,9 +118,17 @@ { "read": 1, "role": "Employee" + }, + { + "read": 1, + "role": "Accounts Manager" + }, + { + "read": 1, + "role": "Stock Manager" } ], "show_name_in_global_search": 1, "sort_field": "name", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index a945860a711..4acdd9a7e8e 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -98,9 +98,9 @@ class FiscalYear(Document): if overlap: frappe.throw( - _("Year start date or end date is overlapping with {0}. To avoid please set company").format( - existing.name - ), + _( + "Year start date or end date is overlapping with {0}. To avoid please set company" + ).format(existing.name), frappe.NameError, ) @@ -116,9 +116,9 @@ def check_duplicate_fiscal_year(doc): not frappe.flags.in_test ): frappe.throw( - _("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format( - fiscal_year - ) + _( + "Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}" + ).format(fiscal_year) ) diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py index 6e946f74660..82147f4f6ab 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py @@ -41,7 +41,7 @@ def test_record_generator(): ] start = 2012 - end = now_datetime().year + 5 + end = now_datetime().year + 25 for year in range(start, end): test_records.append( { diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.js b/erpnext/accounts/doctype/gl_entry/gl_entry.js index 4d2a5135187..7f81a2a4e02 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.js +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.js @@ -1,8 +1,8 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('GL Entry', { - refresh: function(frm) { - frm.page.btn_secondary.hide() - } +frappe.ui.form.on("GL Entry", { + refresh: function (frm) { + frm.page.btn_secondary.hide(); + }, }); diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json index e6d97a1fb26..eb99768b05e 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.json +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json @@ -138,8 +138,7 @@ "label": "Against Voucher Type", "oldfieldname": "against_voucher_type", "oldfieldtype": "Data", - "options": "DocType", - "search_index": 1 + "options": "DocType" }, { "fieldname": "against_voucher", @@ -158,8 +157,7 @@ "label": "Voucher Type", "oldfieldname": "voucher_type", "oldfieldtype": "Select", - "options": "DocType", - "search_index": 1 + "options": "DocType" }, { "fieldname": "voucher_no", @@ -176,7 +174,8 @@ "fieldname": "voucher_detail_no", "fieldtype": "Data", "label": "Voucher Detail No", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "project", @@ -258,7 +257,8 @@ "icon": "fa fa-list", "idx": 1, "in_create": 1, - "modified": "2020-04-07 16:22:33.766994", + "links": [], + "modified": "2024-03-19 18:30:49.613401", "modified_by": "Administrator", "module": "Accounts", "name": "GL Entry", @@ -290,5 +290,6 @@ "quick_entry": 1, "search_fields": "voucher_no,account,posting_date,against_voucher", "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 3a564825b55..6ca5e6377c5 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -13,16 +13,9 @@ import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) -from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( - get_dimension_filter_map, -) from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency from erpnext.accounts.utils import get_account_currency, get_fiscal_year -from erpnext.exceptions import ( - InvalidAccountCurrency, - InvalidAccountDimensionError, - MandatoryAccountDimensionError, -) +from erpnext.exceptions import InvalidAccountCurrency exclude_from_linked_with = True @@ -54,7 +47,6 @@ class GLEntry(Document): if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher": self.validate_account_details(adv_adj) self.validate_dimensions_for_pl_and_bs() - self.validate_allowed_dimensions() validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) @@ -71,13 +63,18 @@ class GLEntry(Document): ]: # Update outstanding amt on against voucher if ( - self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"] + self.against_voucher_type + in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"] and self.against_voucher and self.flags.update_outstanding == "Yes" and not frappe.flags.is_reverse_depr_entry ): update_outstanding_amt( - self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher + self.account, + self.party_type, + self.party, + self.against_voucher_type, + self.against_voucher, ) def check_mandatory(self): @@ -135,7 +132,7 @@ class GLEntry(Document): frappe.throw(msg, title=_("Missing Cost Center")) def validate_dimensions_for_pl_and_bs(self): - account_type = frappe.db.get_value("Account", self.account, "report_type") + account_type = frappe.get_cached_value("Account", self.account, "report_type") for dimension in get_checks_for_pl_and_bs_accounts(): if ( @@ -143,12 +140,13 @@ class GLEntry(Document): and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled + and not self.is_cancelled ): if not self.get(dimension.fieldname): frappe.throw( - _("Accounting Dimension {0} is required for 'Profit and Loss' account {1}.").format( - dimension.label, self.account - ) + _( + "Accounting Dimension {0} is required for 'Profit and Loss' account {1}." + ).format(dimension.label, self.account) ) if ( @@ -156,54 +154,19 @@ class GLEntry(Document): and self.company == dimension.company and dimension.mandatory_for_bs and not dimension.disabled + and not self.is_cancelled ): if not self.get(dimension.fieldname): frappe.throw( - _("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.").format( - dimension.label, self.account - ) + _( + "Accounting Dimension {0} is required for 'Balance Sheet' account {1}." + ).format(dimension.label, self.account) ) - def validate_allowed_dimensions(self): - dimension_filter_map = get_dimension_filter_map() - for key, value in dimension_filter_map.items(): - dimension = key[0] - account = key[1] - - if self.account == account: - if value["is_mandatory"] and not self.get(dimension): - frappe.throw( - _("{0} is mandatory for account {1}").format( - frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account) - ), - MandatoryAccountDimensionError, - ) - - if value["allow_or_restrict"] == "Allow": - if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]: - frappe.throw( - _("Invalid value {0} for {1} against account {2}").format( - frappe.bold(self.get(dimension)), - frappe.bold(frappe.unscrub(dimension)), - frappe.bold(self.account), - ), - InvalidAccountDimensionError, - ) - else: - if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]: - frappe.throw( - _("Invalid value {0} for {1} against account {2}").format( - frappe.bold(self.get(dimension)), - frappe.bold(frappe.unscrub(dimension)), - frappe.bold(self.account), - ), - InvalidAccountDimensionError, - ) - def check_pl_account(self): if ( self.is_opening == "Yes" - and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss" + and frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss" and not self.is_cancelled ): frappe.throw( @@ -245,9 +208,7 @@ class GLEntry(Document): if not self.cost_center: return - is_group, company = frappe.get_cached_value( - "Cost Center", self.cost_center, ["is_group", "company"] - ) + is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"]) if company != self.company: frappe.throw( @@ -296,7 +257,7 @@ class GLEntry(Document): def validate_balance_type(account, adv_adj=False): if not adv_adj and account: - balance_must_be = frappe.db.get_value("Account", account, "balance_must_be") + balance_must_be = frappe.get_cached_value("Account", account, "balance_must_be") if balance_must_be: balance = frappe.db.sql( """select sum(debit) - sum(credit) @@ -316,31 +277,27 @@ def update_outstanding_amt( account, party_type, party, against_voucher_type, against_voucher, on_cancel=False ): if party_type and party: - party_condition = " and party_type={0} and party={1}".format( + party_condition = " and party_type={} and party={}".format( frappe.db.escape(party_type), frappe.db.escape(party) ) else: party_condition = "" if against_voucher_type == "Sales Invoice": - party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to") - account_condition = "and account in ({0}, {1})".format( - frappe.db.escape(account), frappe.db.escape(party_account) - ) + party_account = frappe.get_cached_value(against_voucher_type, against_voucher, "debit_to") + account_condition = f"and account in ({frappe.db.escape(account)}, {frappe.db.escape(party_account)})" else: - account_condition = " and account = {0}".format(frappe.db.escape(account)) + account_condition = f" and account = {frappe.db.escape(account)}" # get final outstanding amt bal = flt( frappe.db.sql( - """ + f""" select sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where against_voucher_type=%s and against_voucher=%s and voucher_type != 'Invoice Discounting' - {0} {1}""".format( - party_condition, account_condition - ), + {party_condition} {account_condition}""", (against_voucher_type, against_voucher), )[0][0] or 0.0 @@ -351,12 +308,10 @@ def update_outstanding_amt( elif against_voucher_type == "Journal Entry": against_voucher_amount = flt( frappe.db.sql( - """ + f""" select sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s - and account = %s and (against_voucher is null or against_voucher='') {0}""".format( - party_condition - ), + and account = %s and (against_voucher is null or against_voucher='') {party_condition}""", (against_voucher, account), )[0][0] ) @@ -375,7 +330,9 @@ def update_outstanding_amt( # Validation : Outstanding can not be negative for JV if bal < 0 and not on_cancel: frappe.throw( - _("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal)) + _("Outstanding for {0} cannot be less than zero ({1})").format( + against_voucher, fmt_money(bal) + ) ) if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]: @@ -391,7 +348,7 @@ def update_outstanding_amt( def validate_frozen_account(account, adv_adj=None): frozen_account = frappe.get_cached_value("Account", account, "freeze_account") if frozen_account == "Yes" and not adv_adj: - frozen_accounts_modifier = frappe.db.get_value( + frozen_accounts_modifier = frappe.get_cached_value( "Accounts Settings", None, "frozen_accounts_modifier" ) @@ -448,7 +405,7 @@ def rename_temporarily_named_docs(doctype): set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc) newname = doc.name frappe.db.sql( - "UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype), + f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0 where name = %s", (newname, oldname), auto_commit=True, ) diff --git a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py index b188b09843a..3edfd67b005 100644 --- a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py @@ -14,9 +14,7 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ class TestGLEntry(unittest.TestCase): def test_round_off_entry(self): frappe.db.set_value("Company", "_Test Company", "round_off_account", "_Test Write Off - _TC") - frappe.db.set_value( - "Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC" - ) + frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC") jv = make_journal_entry( "_Test Account Cost for Goods Sold - _TC", @@ -73,7 +71,9 @@ class TestGLEntry(unittest.TestCase): ) self.assertTrue(all(entry.to_rename == 0 for entry in new_gl_entries)) - self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries))) + self.assertTrue( + all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries, strict=False)) + ) new_naming_series_current_value = frappe.db.sql( "SELECT current from tabSeries where name = %s", naming_series diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js index db4f7c423f9..974f037a81d 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js @@ -1,47 +1,49 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Invoice Discounting', { +frappe.ui.form.on("Invoice Discounting", { setup: (frm) => { frm.set_query("sales_invoice", "invoices", (doc) => { return { - "filters": { - "docstatus": 1, - "company": doc.company, - "outstanding_amount": [">", 0] + filters: { + docstatus: 1, + company: doc.company, + outstanding_amount: [">", 0], }, }; }); - frm.events.filter_accounts("bank_account", frm, [["account_type", "=", "Bank"]]); frm.events.filter_accounts("bank_charges_account", frm, [["root_type", "=", "Expense"]]); frm.events.filter_accounts("short_term_loan", frm, [["root_type", "=", "Liability"]]); - frm.events.filter_accounts("accounts_receivable_discounted", frm, [["account_type", "=", "Receivable"]]); + frm.events.filter_accounts("accounts_receivable_discounted", frm, [ + ["account_type", "=", "Receivable"], + ]); frm.events.filter_accounts("accounts_receivable_credit", frm, [["account_type", "=", "Receivable"]]); frm.events.filter_accounts("accounts_receivable_unpaid", frm, [["account_type", "=", "Receivable"]]); - }, filter_accounts: (fieldname, frm, addl_filters) => { let filters = [ ["company", "=", frm.doc.company], - ["is_group", "=", 0] + ["is_group", "=", 0], ]; - if(addl_filters){ - filters = $.merge(filters , addl_filters); + if (addl_filters) { + filters = $.merge(filters, addl_filters); } - frm.set_query(fieldname, () => { return { "filters": filters }; }); + frm.set_query(fieldname, () => { + return { filters: filters }; + }); }, - refresh_filters: (frm) =>{ - let invoice_accounts = Object.keys(frm.doc.invoices).map(function(key) { + refresh_filters: (frm) => { + let invoice_accounts = Object.keys(frm.doc.invoices).map(function (key) { return frm.doc.invoices[key].debit_to; }); let filters = [ ["account_type", "=", "Receivable"], - ["name", "not in", invoice_accounts] + ["name", "not in", invoice_accounts], ]; frm.events.filter_accounts("accounts_receivable_credit", frm, filters); frm.events.filter_accounts("accounts_receivable_discounted", frm, filters); @@ -52,19 +54,19 @@ frappe.ui.form.on('Invoice Discounting', { frm.events.show_general_ledger(frm); if (frm.doc.docstatus === 0) { - frm.add_custom_button(__('Get Invoices'), function() { + frm.add_custom_button(__("Get Invoices"), function () { frm.events.get_invoices(frm); }); } if (frm.doc.docstatus === 1 && frm.doc.status !== "Settled") { if (frm.doc.status == "Sanctioned") { - frm.add_custom_button(__('Disburse Loan'), function() { + frm.add_custom_button(__("Disburse Loan"), function () { frm.events.create_disbursement_entry(frm); }).addClass("btn-primary"); } if (frm.doc.status == "Disbursed") { - frm.add_custom_button(__('Close Loan'), function() { + frm.add_custom_button(__("Close Loan"), function () { frm.events.close_loan(frm); }).addClass("btn-primary"); } @@ -92,119 +94,121 @@ frappe.ui.form.on('Invoice Discounting', { calculate_total_amount: (frm) => { let total_amount = 0.0; - for (let row of (frm.doc.invoices || [])) { + for (let row of frm.doc.invoices || []) { total_amount += flt(row.outstanding_amount); } frm.set_value("total_amount", total_amount); }, get_invoices: (frm) => { var d = new frappe.ui.Dialog({ - title: __('Get Invoices based on Filters'), + title: __("Get Invoices based on Filters"), fields: [ { - "label": "Customer", - "fieldname": "customer", - "fieldtype": "Link", - "options": "Customer" + label: "Customer", + fieldname: "customer", + fieldtype: "Link", + options: "Customer", }, { - "label": "From Date", - "fieldname": "from_date", - "fieldtype": "Date" + label: "From Date", + fieldname: "from_date", + fieldtype: "Date", }, { - "label": "To Date", - "fieldname": "to_date", - "fieldtype": "Date" + label: "To Date", + fieldname: "to_date", + fieldtype: "Date", }, { - "fieldname": "col_break", - "fieldtype": "Column Break", + fieldname: "col_break", + fieldtype: "Column Break", }, { - "label": "Min Amount", - "fieldname": "min_amount", - "fieldtype": "Currency" + label: "Min Amount", + fieldname: "min_amount", + fieldtype: "Currency", }, { - "label": "Max Amount", - "fieldname": "max_amount", - "fieldtype": "Currency" - } + label: "Max Amount", + fieldname: "max_amount", + fieldtype: "Currency", + }, ], - primary_action: function() { + primary_action: function () { var data = d.get_values(); frappe.call({ method: "erpnext.accounts.doctype.invoice_discounting.invoice_discounting.get_invoices", args: { - filters: data + filters: data, }, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { d.hide(); - $.each(r.message, function(i, v) { - frm.doc.invoices = frm.doc.invoices.filter(row => row.sales_invoice); + $.each(r.message, function (i, v) { + frm.doc.invoices = frm.doc.invoices.filter((row) => row.sales_invoice); let row = frm.add_child("invoices"); $.extend(row, v); frm.events.refresh_filters(frm); }); refresh_field("invoices"); } - } + }, }); }, - primary_action_label: __('Get Invocies') + primary_action_label: __("Get Invocies"), }); d.show(); }, create_disbursement_entry: (frm) => { frappe.call({ - method:"create_disbursement_entry", + method: "create_disbursement_entry", doc: frm.doc, - callback: function(r) { - if(!r.exc){ + callback: function (r) { + if (!r.exc) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } - } + }, }); - }, close_loan: (frm) => { frappe.call({ - method:"close_loan", + method: "close_loan", doc: frm.doc, - callback: function(r) { - if(!r.exc){ + callback: function (r) { + if (!r.exc) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } - } + }, }); - }, show_general_ledger: (frm) => { - if(frm.doc.docstatus > 0) { - cur_frm.add_custom_button(__('Accounting Ledger'), function() { - frappe.route_options = { - voucher_no: frm.doc.name, - from_date: frm.doc.posting_date, - to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), - company: frm.doc.company, - group_by: "Group by Voucher (Consolidated)", - show_cancelled_entries: frm.doc.docstatus === 2 - }; - frappe.set_route("query-report", "General Ledger"); - }, __("View")); + if (frm.doc.docstatus > 0) { + cur_frm.add_custom_button( + __("Accounting Ledger"), + function () { + frappe.route_options = { + voucher_no: frm.doc.name, + from_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), + company: frm.doc.company, + group_by: "Group by Voucher (Consolidated)", + show_cancelled_entries: frm.doc.docstatus === 2, + }; + frappe.set_route("query-report", "General Ledger"); + }, + __("View") + ); } - } + }, }); -frappe.ui.form.on('Discounted Invoice', { +frappe.ui.form.on("Discounted Invoice", { sales_invoice: (frm) => { frm.events.calculate_total_amount(frm); frm.events.refresh_filters(frm); @@ -212,5 +216,5 @@ frappe.ui.form.on('Discounted Invoice', { invoices_remove: (frm) => { frm.events.calculate_total_amount(frm); frm.events.refresh_filters(frm); - } + }, }); diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 5bd4585a9a8..2322d68c000 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -55,9 +55,7 @@ class InvoiceDiscounting(AccountsController): frappe.throw( _( "Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}" - ).format( - record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice) - ) + ).format(record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)) ) def calculate_total_amount(self): @@ -77,7 +75,9 @@ class InvoiceDiscounting(AccountsController): self.status = status self.db_set("status", status) for d in self.invoices: - frappe.get_doc("Sales Invoice", d.sales_invoice).set_status(update=True, update_modified=False) + frappe.get_doc("Sales Invoice", d.sales_invoice).set_status( + update=True, update_modified=False + ) else: self.status = "Draft" if self.docstatus == 1: diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js index 4895efcd4cc..0b08e393de2 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js @@ -1,21 +1,16 @@ -frappe.listview_settings['Invoice Discounting'] = { +frappe.listview_settings["Invoice Discounting"] = { add_fields: ["status"], - get_indicator: function(doc) - { - if(doc.status == "Draft") { + get_indicator: function (doc) { + if (doc.status == "Draft") { return [__("Draft"), "red", "status,=,Draft"]; - } - else if(doc.status == "Sanctioned") { + } else if (doc.status == "Sanctioned") { return [__("Sanctioned"), "green", "status,=,Sanctioned"]; - } - else if(doc.status == "Disbursed") { + } else if (doc.status == "Disbursed") { return [__("Disbursed"), "blue", "status,=,Disbursed"]; - } - else if(doc.status == "Settled") { + } else if (doc.status == "Settled") { return [__("Settled"), "orange", "status,=,Settled"]; - } - else if(doc.status == "Canceled") { + } else if (doc.status == "Canceled") { return [__("Canceled"), "red", "status,=,Canceled"]; } - } + }, }; diff --git a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py index a85fdfcad7f..01050c058e8 100644 --- a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py @@ -75,8 +75,8 @@ class TestInvoiceDiscounting(unittest.TestCase): gle = get_gl_entries("Invoice Discounting", inv_disc.name) expected_gle = {inv.debit_to: [0.0, 200], self.ar_credit: [200, 0.0]} - for i, gle in enumerate(gle): - self.assertEqual([gle.debit, gle.credit], expected_gle.get(gle.account)) + for _i, gle_value in enumerate(gle): + self.assertEqual([gle_value.debit, gle_value.credit], expected_gle.get(gle_value.account)) def test_loan_on_submit(self): inv = create_sales_invoice(rate=300) @@ -92,9 +92,7 @@ class TestInvoiceDiscounting(unittest.TestCase): period=60, ) self.assertEqual(inv_disc.status, "Sanctioned") - self.assertEqual( - inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period) - ) + self.assertEqual(inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period)) def test_on_disbursed(self): inv = create_sales_invoice(rate=500) @@ -262,13 +260,9 @@ class TestInvoiceDiscounting(unittest.TestCase): je_on_payment.submit() self.assertEqual(je_on_payment.accounts[0].account, self.ar_discounted) - self.assertEqual( - je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount) - ) + self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)) self.assertEqual(je_on_payment.accounts[1].account, self.bank_account) - self.assertEqual( - je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount) - ) + self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)) inv.reload() self.assertEqual(inv.outstanding_amount, 0) @@ -304,13 +298,9 @@ class TestInvoiceDiscounting(unittest.TestCase): je_on_payment.submit() self.assertEqual(je_on_payment.accounts[0].account, self.ar_unpaid) - self.assertEqual( - je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount) - ) + self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)) self.assertEqual(je_on_payment.accounts[1].account, self.bank_account) - self.assertEqual( - je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount) - ) + self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)) inv.reload() self.assertEqual(inv.outstanding_amount, 0) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.js b/erpnext/accounts/doctype/item_tax_template/item_tax_template.js index e921a0d949d..b608ccd3568 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.js +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.js @@ -1,27 +1,49 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Item Tax Template', { - setup: function(frm) { - frm.set_query("tax_type", "taxes", function(doc) { +frappe.ui.form.on("Item Tax Template", { + setup: function (frm) { + frm.set_query("tax_type", "taxes", function (doc) { return { filters: [ - ['Account', 'company', '=', frm.doc.company], - ['Account', 'is_group', '=', 0], - ['Account', 'account_type', 'in', ['Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation']] - ] - } + ["Account", "company", "=", frm.doc.company], + ["Account", "is_group", "=", 0], + [ + "Account", + "account_type", + "in", + [ + "Tax", + "Chargeable", + "Income Account", + "Expense Account", + "Expenses Included In Valuation", + ], + ], + ], + }; }); }, company: function (frm) { - frm.set_query("tax_type", "taxes", function(doc) { + frm.set_query("tax_type", "taxes", function (doc) { return { filters: [ - ['Account', 'company', '=', frm.doc.company], - ['Account', 'is_group', '=', 0], - ['Account', 'account_type', 'in', ['Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation']] - ] - } + ["Account", "company", "=", frm.doc.company], + ["Account", "is_group", "=", 0], + [ + "Account", + "account_type", + "in", + [ + "Tax", + "Chargeable", + "Income Account", + "Expense Account", + "Expenses Included In Valuation", + ], + ], + ], + }; }); - } + }, }); diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py index 23f36ec6d8d..755ff490885 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py @@ -14,7 +14,7 @@ class ItemTaxTemplate(Document): def autoname(self): if self.company and self.title: abbr = frappe.get_cached_value("Company", self.company, "abbr") - self.name = "{0} - {1}".format(self.title, abbr) + self.name = f"{self.title} - {abbr}" def validate_tax_accounts(self): """Check whether Tax Rate is not entered twice for same Tax Type""" diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index c59643280e8..cda0adc7257 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -4,39 +4,57 @@ frappe.provide("erpnext.accounts"); frappe.provide("erpnext.journal_entry"); - frappe.ui.form.on("Journal Entry", { - setup: function(frm) { + setup: function (frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; + frm.ignore_doctypes_on_cancel_all = [ + "Sales Invoice", + "Purchase Invoice", + "Journal Entry", + "Repost Payment Ledger", + "Asset", + "Asset Movement", + "Repost Accounting Ledger", + "Unreconcile Payment", + "Unreconcile Payment Entries", + "Bank Transaction", + ]; }, - refresh: function(frm) { + refresh: function (frm) { erpnext.toggle_naming_series(); - 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": moment(frm.doc.modified).format('YYYY-MM-DD'), - "company": frm.doc.company, - "finance_book": frm.doc.finance_book, - "group_by": '', - "show_cancelled_entries": frm.doc.docstatus === 2 - }; - frappe.set_route("query-report", "General Ledger"); - }, __('View')); + 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: moment(frm.doc.modified).format("YYYY-MM-DD"), + company: frm.doc.company, + finance_book: frm.doc.finance_book, + group_by: "", + show_cancelled_entries: frm.doc.docstatus === 2, + }; + frappe.set_route("query-report", "General Ledger"); + }, + __("View") + ); } - if(frm.doc.docstatus==1) { - frm.add_custom_button(__('Reverse Journal Entry'), function() { - return erpnext.journal_entry.reverse_journal_entry(frm); - }, __('Actions')); + if (frm.doc.docstatus == 1) { + frm.add_custom_button( + __("Reverse Journal Entry"), + function () { + return erpnext.journal_entry.reverse_journal_entry(frm); + }, + __("Actions") + ); } if (frm.doc.__islocal) { - frm.add_custom_button(__('Quick Entry'), function() { + frm.add_custom_button(__("Quick Entry"), function () { return erpnext.journal_entry.quick_entry(frm); }); } @@ -44,52 +62,63 @@ frappe.ui.form.on("Journal Entry", { // hide /unhide fields based on currency erpnext.journal_entry.toggle_fields_based_on_currency(frm); - if ((frm.doc.voucher_type == "Inter Company Journal Entry") && (frm.doc.docstatus == 1) && (!frm.doc.inter_company_journal_entry_reference)) { - frm.add_custom_button(__("Create Inter Company Journal Entry"), - function() { + if ( + frm.doc.voucher_type == "Inter Company Journal Entry" && + frm.doc.docstatus == 1 && + !frm.doc.inter_company_journal_entry_reference + ) { + frm.add_custom_button( + __("Create Inter Company Journal Entry"), + function () { frm.trigger("make_inter_company_journal_entry"); - }, __('Make')); + }, + __("Make") + ); } erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm); }, - before_save: function(frm) { - if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) { - let payment_entry_references = frm.doc.accounts.filter(elem => (elem.reference_type == "Payment Entry")); + before_save: function (frm) { + if (frm.doc.docstatus == 0 && !frm.doc.is_system_generated) { + let payment_entry_references = frm.doc.accounts.filter( + (elem) => elem.reference_type == "Payment Entry" + ); if (payment_entry_references.length > 0) { - let rows = payment_entry_references.map(x => "#"+x.idx); - frappe.throw(__("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [frappe.utils.comma_and(rows)])); + let rows = payment_entry_references.map((x) => "#" + x.idx); + frappe.throw( + __("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [ + frappe.utils.comma_and(rows), + ]) + ); } } }, - make_inter_company_journal_entry: function(frm) { + make_inter_company_journal_entry: function (frm) { var d = new frappe.ui.Dialog({ title: __("Select Company"), fields: [ { - 'fieldname': 'company', - 'fieldtype': 'Link', - 'label': __('Company'), - 'options': 'Company', - "get_query": function () { + fieldname: "company", + fieldtype: "Link", + label: __("Company"), + options: "Company", + get_query: function () { return { - filters: [ - ["Company", "name", "!=", frm.doc.company] - ] + filters: [["Company", "name", "!=", frm.doc.company]], }; }, - 'reqd': 1 - } + reqd: 1, + }, ], }); - d.set_primary_action(__('Create'), function() { + d.set_primary_action(__("Create"), function () { d.hide(); var args = d.get_values(); frappe.call({ args: { - "name": frm.doc.name, - "voucher_type": frm.doc.voucher_type, - "company": args.company + name: frm.doc.name, + voucher_type: frm.doc.voucher_type, + company: args.company, }, method: "erpnext.accounts.doctype.journal_entry.journal_entry.make_inter_company_journal_entry", callback: function (r) { @@ -97,103 +126,106 @@ frappe.ui.form.on("Journal Entry", { var doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); } - } + }, }); }); d.show(); }, - multi_currency: function(frm) { + multi_currency: function (frm) { erpnext.journal_entry.toggle_fields_based_on_currency(frm); }, - posting_date: function(frm) { - if(!frm.doc.multi_currency || !frm.doc.posting_date) return; + posting_date: function (frm) { + if (!frm.doc.multi_currency || !frm.doc.posting_date) return; - $.each(frm.doc.accounts || [], function(i, row) { + $.each(frm.doc.accounts || [], function (i, row) { erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name); - }) + }); }, - company: function(frm) { + company: function (frm) { frappe.call({ method: "frappe.client.get_value", args: { doctype: "Company", - filters: {"name": frm.doc.company}, - fieldname: "cost_center" + filters: { name: frm.doc.company }, + fieldname: "cost_center", }, - callback: function(r){ - if(r.message){ - $.each(frm.doc.accounts || [], function(i, jvd) { + callback: function (r) { + if (r.message) { + $.each(frm.doc.accounts || [], function (i, jvd) { frappe.model.set_value(jvd.doctype, jvd.name, "cost_center", r.message.cost_center); }); } - } + }, }); erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, - voucher_type: function(frm){ + voucher_type: function (frm) { + if (!frm.doc.company) return null; - if(!frm.doc.company) return null; - - if((!(frm.doc.accounts || []).length) || ((frm.doc.accounts || []).length === 1 && !frm.doc.accounts[0].account)) { - if(in_list(["Bank Entry", "Cash Entry"], frm.doc.voucher_type)) { + if ( + !(frm.doc.accounts || []).length || + ((frm.doc.accounts || []).length === 1 && !frm.doc.accounts[0].account) + ) { + if (in_list(["Bank Entry", "Cash Entry"], frm.doc.voucher_type)) { return frappe.call({ type: "GET", method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account", args: { - "account_type": (frm.doc.voucher_type=="Bank Entry" ? - "Bank" : (frm.doc.voucher_type=="Cash Entry" ? "Cash" : null)), - "company": frm.doc.company + account_type: + frm.doc.voucher_type == "Bank Entry" + ? "Bank" + : frm.doc.voucher_type == "Cash Entry" + ? "Cash" + : null, + company: frm.doc.company, }, - callback: function(r) { - if(r.message) { + callback: function (r) { + if (r.message) { // If default company bank account not set - if(!$.isEmptyObject(r.message)){ + if (!$.isEmptyObject(r.message)) { update_jv_details(frm.doc, [r.message]); } } - } + }, }); } } }, - from_template: function(frm){ - if (frm.doc.from_template){ - frappe.db.get_doc("Journal Entry Template", frm.doc.from_template) - .then((doc) => { - frappe.model.clear_table(frm.doc, "accounts"); - frm.set_value({ - "company": doc.company, - "voucher_type": doc.voucher_type, - "naming_series": doc.naming_series, - "is_opening": doc.is_opening, - "multi_currency": doc.multi_currency - }) - update_jv_details(frm.doc, doc.accounts); + from_template: function (frm) { + if (frm.doc.from_template) { + frappe.db.get_doc("Journal Entry Template", frm.doc.from_template).then((doc) => { + frappe.model.clear_table(frm.doc, "accounts"); + frm.set_value({ + company: doc.company, + voucher_type: doc.voucher_type, + naming_series: doc.naming_series, + is_opening: doc.is_opening, + multi_currency: doc.multi_currency, }); + update_jv_details(frm.doc, doc.accounts); + }); } - } + }, }); -var update_jv_details = function(doc, r) { - $.each(r, function(i, d) { +var update_jv_details = function (doc, r) { + $.each(r, function (i, d) { var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts"); - frappe.model.set_value(row.doctype, row.name, "account", d.account) - frappe.model.set_value(row.doctype, row.name, "balance", d.balance) + frappe.model.set_value(row.doctype, row.name, "account", d.account); }); refresh_field("accounts"); -} +}; erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Controller { onload() { this.load_defaults(); this.setup_queries(); - this.setup_balance_formatter(); erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); } @@ -203,69 +235,67 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro load_defaults() { //this.frm.show_print_first = true; - if(this.frm.doc.__islocal && this.frm.doc.company) { + if (this.frm.doc.__islocal && this.frm.doc.company) { frappe.model.set_default_values(this.frm.doc); - $.each(this.frm.doc.accounts || [], function(i, jvd) { + $.each(this.frm.doc.accounts || [], function (i, jvd) { frappe.model.set_default_values(jvd); }); var posting_date = this.frm.doc.posting_date; - if(!this.frm.doc.amended_from) this.frm.set_value('posting_date', posting_date || frappe.datetime.get_today()); + if (!this.frm.doc.amended_from) + this.frm.set_value("posting_date", posting_date || frappe.datetime.get_today()); } } setup_queries() { var me = this; - me.frm.set_query("account", "accounts", function(doc, cdt, cdn) { + me.frm.set_query("account", "accounts", function (doc, cdt, cdn) { return erpnext.journal_entry.account_query(me.frm); }); - me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) { + me.frm.set_query("party_type", "accounts", function (doc, cdt, cdn) { const row = locals[cdt][cdn]; return { query: "erpnext.setup.doctype.party_type.party_type.get_party_type", filters: { - 'account': row.account - } - } + account: row.account, + }, + }; }); - me.frm.set_query("reference_name", "accounts", function(doc, cdt, cdn) { + me.frm.set_query("reference_name", "accounts", function (doc, cdt, cdn) { var jvd = frappe.get_doc(cdt, cdn); // journal entry - if(jvd.reference_type==="Journal Entry") { + if (jvd.reference_type === "Journal Entry") { frappe.model.validate_missing(jvd, "account"); return { query: "erpnext.accounts.doctype.journal_entry.journal_entry.get_against_jv", filters: { account: jvd.account, - party: jvd.party - } + party: jvd.party, + }, }; } var out = { - filters: [ - [jvd.reference_type, "docstatus", "=", 1] - ] + filters: [[jvd.reference_type, "docstatus", "=", 1]], }; - if(in_list(["Sales Invoice", "Purchase Invoice"], jvd.reference_type)) { + if (in_list(["Sales Invoice", "Purchase Invoice"], jvd.reference_type)) { out.filters.push([jvd.reference_type, "outstanding_amount", "!=", 0]); // Filter by cost center - if(jvd.cost_center) { + if (jvd.cost_center) { out.filters.push([jvd.reference_type, "cost_center", "in", ["", jvd.cost_center]]); } // account filter frappe.model.validate_missing(jvd, "account"); - var party_account_field = jvd.reference_type==="Sales Invoice" ? "debit_to": "credit_to"; + var party_account_field = jvd.reference_type === "Sales Invoice" ? "debit_to" : "credit_to"; out.filters.push([jvd.reference_type, party_account_field, "=", jvd.account]); - } - if(in_list(["Sales Order", "Purchase Order"], jvd.reference_type)) { + if (in_list(["Sales Order", "Purchase Order"], jvd.reference_type)) { // party_type and party mandatory frappe.model.validate_missing(jvd, "party_type"); frappe.model.validate_missing(jvd, "party"); @@ -273,11 +303,11 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro out.filters.push([jvd.reference_type, "per_billed", "<", 100]); } - if(jvd.party_type && jvd.party) { + if (jvd.party_type && jvd.party) { var party_field = ""; - if(jvd.reference_type.indexOf("Sales")===0) { + if (jvd.reference_type.indexOf("Sales") === 0) { var party_field = "customer"; - } else if (jvd.reference_type.indexOf("Purchase")===0) { + } else if (jvd.reference_type.indexOf("Purchase") === 0) { var party_field = "supplier"; } @@ -288,64 +318,49 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro return out; }); - - - } - - setup_balance_formatter() { - const formatter = function(value, df, options, doc) { - var currency = frappe.meta.get_field_currency(df, doc); - var dr_or_cr = value ? ('') : ""; - return "
    " - + ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency)) - + " " + dr_or_cr - + "
    "; - }; - this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter); - this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter); } reference_name(doc, cdt, cdn) { var d = frappe.get_doc(cdt, cdn); - if(d.reference_name) { - if (d.reference_type==="Purchase Invoice" && !flt(d.debit)) { - this.get_outstanding('Purchase Invoice', d.reference_name, doc.company, d); - } else if (d.reference_type==="Sales Invoice" && !flt(d.credit)) { - this.get_outstanding('Sales Invoice', d.reference_name, doc.company, d); - } else if (d.reference_type==="Journal Entry" && !flt(d.credit) && !flt(d.debit)) { - this.get_outstanding('Journal Entry', d.reference_name, doc.company, d); + if (d.reference_name) { + if (d.reference_type === "Purchase Invoice" && !flt(d.debit)) { + this.get_outstanding("Purchase Invoice", d.reference_name, doc.company, d); + } else if (d.reference_type === "Sales Invoice" && !flt(d.credit)) { + this.get_outstanding("Sales Invoice", d.reference_name, doc.company, d); + } else if (d.reference_type === "Journal Entry" && !flt(d.credit) && !flt(d.debit)) { + this.get_outstanding("Journal Entry", d.reference_name, doc.company, d); } } } get_outstanding(doctype, docname, company, child) { var args = { - "doctype": doctype, - "docname": docname, - "party": child.party, - "account": child.account, - "account_currency": child.account_currency, - "company": company - } + doctype: doctype, + docname: docname, + party: child.party, + account: child.account, + account_currency: child.account_currency, + company: company, + }; return frappe.call({ method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_outstanding", - args: { args: args}, - callback: function(r) { - if(r.message) { - $.each(r.message, function(field, value) { + args: { args: args }, + callback: function (r) { + if (r.message) { + $.each(r.message, function (field, value) { frappe.model.set_value(child.doctype, child.name, field, value); - }) + }); } - } + }, }); } accounts_add(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); - $.each(doc.accounts, function(i, d) { - if(d.account && d.party && d.party_type) { + $.each(doc.accounts, function (i, d) { + if (d.account && d.party && d.party_type) { row.account = d.account; row.party = d.party; row.party_type = d.party_type; @@ -353,8 +368,8 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro }); // set difference - if(doc.difference) { - if(doc.difference > 0) { + if (doc.difference) { + if (doc.difference > 0) { row.credit_in_account_currency = doc.difference; row.credit = doc.difference; } else { @@ -364,132 +379,144 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro } cur_frm.cscript.update_totals(doc); - erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'accounts'); + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, "accounts"); } - }; cur_frm.script_manager.make(erpnext.accounts.JournalEntry); -cur_frm.cscript.update_totals = function(doc) { - var td=0.0; var tc =0.0; +cur_frm.cscript.update_totals = function (doc) { + var td = 0.0; + var tc = 0.0; var accounts = doc.accounts || []; - for(var i in accounts) { + for (var i in accounts) { td += flt(accounts[i].debit, precision("debit", accounts[i])); tc += flt(accounts[i].credit, precision("credit", accounts[i])); } var doc = locals[doc.doctype][doc.name]; doc.total_debit = td; doc.total_credit = tc; - doc.difference = flt((td - tc), precision("difference")); - refresh_many(['total_debit','total_credit','difference']); -} + doc.difference = flt(td - tc, precision("difference")); + refresh_many(["total_debit", "total_credit", "difference"]); +}; -cur_frm.cscript.get_balance = function(doc,dt,dn) { +cur_frm.cscript.get_balance = function (doc, dt, dn) { cur_frm.cscript.update_totals(doc); - cur_frm.call('get_balance', null, () => { cur_frm.refresh(); }); -} + cur_frm.call("get_balance", null, () => { + cur_frm.refresh(); + }); +}; -cur_frm.cscript.validate = function(doc,cdt,cdn) { +cur_frm.cscript.validate = function (doc, cdt, cdn) { cur_frm.cscript.update_totals(doc); -} +}; frappe.ui.form.on("Journal Entry Account", { - party: function(frm, cdt, cdn) { + party: function (frm, cdt, cdn) { var d = frappe.get_doc(cdt, cdn); - if(!d.account && d.party_type && d.party) { - if(!frm.doc.company) frappe.throw(__("Please select Company")); + if (!d.account && d.party_type && d.party) { + if (!frm.doc.company) frappe.throw(__("Please select Company")); return frm.call({ - method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_party_account_and_balance", + method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_party_account_and_currency", child: d, args: { company: frm.doc.company, party_type: d.party_type, party: d.party, - cost_center: d.cost_center - } + }, }); } }, - cost_center: function(frm, dt, dn) { - erpnext.journal_entry.set_account_balance(frm, dt, dn); + cost_center: function (frm, dt, dn) { + // Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0' + if (frm.doc.voucher_type != "Exchange Gain Or Loss") { + erpnext.journal_entry.set_account_details(frm, dt, dn); + } }, - account: function(frm, dt, dn) { - erpnext.journal_entry.set_account_balance(frm, dt, dn); + account: function (frm, dt, dn) { + erpnext.journal_entry.set_account_details(frm, dt, dn); }, - debit_in_account_currency: function(frm, cdt, cdn) { + debit_in_account_currency: function (frm, cdt, cdn) { erpnext.journal_entry.set_exchange_rate(frm, cdt, cdn); }, - credit_in_account_currency: function(frm, cdt, cdn) { + credit_in_account_currency: function (frm, cdt, cdn) { erpnext.journal_entry.set_exchange_rate(frm, cdt, cdn); }, - debit: function(frm, dt, dn) { + debit: function (frm, dt, dn) { cur_frm.cscript.update_totals(frm.doc); }, - credit: function(frm, dt, dn) { + credit: function (frm, dt, dn) { cur_frm.cscript.update_totals(frm.doc); }, - exchange_rate: function(frm, cdt, cdn) { + exchange_rate: function (frm, cdt, cdn) { var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; var row = locals[cdt][cdn]; - if(row.account_currency == company_currency || !frm.doc.multi_currency) { + if (row.account_currency == company_currency || !frm.doc.multi_currency) { frappe.model.set_value(cdt, cdn, "exchange_rate", 1); } erpnext.journal_entry.set_debit_credit_in_company_currency(frm, cdt, cdn); - } -}) + }, +}); -frappe.ui.form.on("Journal Entry Account", "accounts_remove", function(frm) { +frappe.ui.form.on("Journal Entry Account", "accounts_remove", function (frm) { cur_frm.cscript.update_totals(frm.doc); }); $.extend(erpnext.journal_entry, { - toggle_fields_based_on_currency: function(frm) { + toggle_fields_based_on_currency: function (frm) { var fields = ["currency_section", "account_currency", "exchange_rate", "debit", "credit"]; var grid = frm.get_field("accounts").grid; - if(grid) grid.set_column_disp(fields, frm.doc.multi_currency); + if (grid) grid.set_column_disp(fields, frm.doc.multi_currency); // dynamic label var field_label_map = { - "debit_in_account_currency": "Debit", - "credit_in_account_currency": "Credit" + debit_in_account_currency: "Debit", + credit_in_account_currency: "Credit", }; $.each(field_label_map, function (fieldname, label) { frm.fields_dict.accounts.grid.update_docfield_property( fieldname, - 'label', - frm.doc.multi_currency ? (label + " in Account Currency") : label + "label", + frm.doc.multi_currency ? label + " in Account Currency" : label ); - }) + }); }, - set_debit_credit_in_company_currency: function(frm, cdt, cdn) { + set_debit_credit_in_company_currency: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; - frappe.model.set_value(cdt, cdn, "debit", - flt(flt(row.debit_in_account_currency)*row.exchange_rate, precision("debit", row))); + frappe.model.set_value( + cdt, + cdn, + "debit", + flt(flt(row.debit_in_account_currency) * row.exchange_rate, precision("debit", row)) + ); - frappe.model.set_value(cdt, cdn, "credit", - flt(flt(row.credit_in_account_currency)*row.exchange_rate, precision("credit", row))); + frappe.model.set_value( + cdt, + cdn, + "credit", + flt(flt(row.credit_in_account_currency) * row.exchange_rate, precision("credit", row)) + ); cur_frm.cscript.update_totals(frm.doc); }, - set_exchange_rate: function(frm, cdt, cdn) { + set_exchange_rate: function (frm, cdt, cdn) { var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; var row = locals[cdt][cdn]; - if(row.account_currency == company_currency || !frm.doc.multi_currency) { + if (row.account_currency == company_currency || !frm.doc.multi_currency) { row.exchange_rate = 1; erpnext.journal_entry.set_debit_credit_in_company_currency(frm, cdt, cdn); } else if (!row.exchange_rate || row.exchange_rate == 1 || row.account_type == "Bank") { @@ -504,50 +531,70 @@ $.extend(erpnext.journal_entry, { reference_name: cstr(row.reference_name), debit: flt(row.debit_in_account_currency), credit: flt(row.credit_in_account_currency), - exchange_rate: row.exchange_rate + exchange_rate: row.exchange_rate, }, - callback: function(r) { - if(r.message) { + callback: function (r) { + if (r.message) { row.exchange_rate = r.message; erpnext.journal_entry.set_debit_credit_in_company_currency(frm, cdt, cdn); } - } - }) + }, + }); } else { erpnext.journal_entry.set_debit_credit_in_company_currency(frm, cdt, cdn); } refresh_field("exchange_rate", cdn, "accounts"); }, - quick_entry: function(frm) { + quick_entry: function (frm) { var naming_series_options = frm.fields_dict.naming_series.df.options; - var naming_series_default = frm.fields_dict.naming_series.df.default || naming_series_options.split("\n")[0]; + var naming_series_default = + frm.fields_dict.naming_series.df.default || naming_series_options.split("\n")[0]; var dialog = new frappe.ui.Dialog({ title: __("Quick Journal Entry"), fields: [ - {fieldtype: "Currency", fieldname: "debit", label: __("Amount"), reqd: 1}, - {fieldtype: "Link", fieldname: "debit_account", label: __("Debit Account"), reqd: 1, + { fieldtype: "Currency", fieldname: "debit", label: __("Amount"), reqd: 1 }, + { + fieldtype: "Link", + fieldname: "debit_account", + label: __("Debit Account"), + reqd: 1, options: "Account", - get_query: function() { + get_query: function () { return erpnext.journal_entry.account_query(frm); - } + }, }, - {fieldtype: "Link", fieldname: "credit_account", label: __("Credit Account"), reqd: 1, + { + fieldtype: "Link", + fieldname: "credit_account", + label: __("Credit Account"), + reqd: 1, options: "Account", - get_query: function() { + get_query: function () { return erpnext.journal_entry.account_query(frm); - } + }, }, - {fieldtype: "Date", fieldname: "posting_date", label: __("Date"), reqd: 1, - default: frm.doc.posting_date}, - {fieldtype: "Small Text", fieldname: "user_remark", label: __("User Remark")}, - {fieldtype: "Select", fieldname: "naming_series", label: __("Series"), reqd: 1, - options: naming_series_options, default: naming_series_default}, - ] + { + fieldtype: "Date", + fieldname: "posting_date", + label: __("Date"), + reqd: 1, + default: frm.doc.posting_date, + }, + { fieldtype: "Small Text", fieldname: "user_remark", label: __("User Remark") }, + { + fieldtype: "Select", + fieldname: "naming_series", + label: __("Series"), + reqd: 1, + options: naming_series_options, + default: naming_series_default, + }, + ], }); - dialog.set_primary_action(__("Save"), function() { + dialog.set_primary_action(__("Save"), function () { var btn = this; var values = dialog.get_values(); @@ -564,11 +611,21 @@ $.extend(erpnext.journal_entry, { var debit_row = frm.fields_dict.accounts.grid.add_new_row(); frappe.model.set_value(debit_row.doctype, debit_row.name, "account", values.debit_account); - frappe.model.set_value(debit_row.doctype, debit_row.name, "debit_in_account_currency", values.debit); + frappe.model.set_value( + debit_row.doctype, + debit_row.name, + "debit_in_account_currency", + values.debit + ); var credit_row = frm.fields_dict.accounts.grid.add_new_row(); frappe.model.set_value(credit_row.doctype, credit_row.name, "account", values.credit_account); - frappe.model.set_value(credit_row.doctype, credit_row.name, "credit_in_account_currency", values.debit); + frappe.model.set_value( + credit_row.doctype, + credit_row.name, + "credit_in_account_currency", + values.debit + ); frm.save(); @@ -578,36 +635,36 @@ $.extend(erpnext.journal_entry, { dialog.show(); }, - account_query: function(frm) { + account_query: function (frm) { var filters = { company: frm.doc.company, - is_group: 0 + is_group: 0, }; - if(!frm.doc.multi_currency) { + if (!frm.doc.multi_currency) { $.extend(filters, { - account_currency: frappe.get_doc(":Company", frm.doc.company).default_currency + account_currency: frappe.get_doc(":Company", frm.doc.company).default_currency, }); } return { filters: filters }; }, - reverse_journal_entry: function() { + reverse_journal_entry: function () { frappe.model.open_mapped_doc({ method: "erpnext.accounts.doctype.journal_entry.journal_entry.make_reverse_journal_entry", - frm: cur_frm - }) + frm: cur_frm, + }); }, }); $.extend(erpnext.journal_entry, { - set_account_balance: function(frm, dt, dn) { + set_account_details: function (frm, dt, dn) { var d = locals[dt][dn]; - if(d.account) { - if(!frm.doc.company) frappe.throw(__("Please select Company first")); - if(!frm.doc.posting_date) frappe.throw(__("Please select Posting Date first")); + if (d.account) { + if (!frm.doc.company) frappe.throw(__("Please select Company first")); + if (!frm.doc.posting_date) frappe.throw(__("Please select Posting Date first")); return frappe.call({ - method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_account_balance_and_party_type", + method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_account_details_and_party_type", args: { account: d.account, date: frm.doc.posting_date, @@ -615,15 +672,14 @@ $.extend(erpnext.journal_entry, { debit: flt(d.debit_in_account_currency), credit: flt(d.credit_in_account_currency), exchange_rate: d.exchange_rate, - cost_center: d.cost_center }, - callback: function(r) { - if(r.message) { + callback: function (r) { + if (r.message) { $.extend(d, r.message); erpnext.journal_entry.set_debit_credit_in_company_currency(frm, dt, dn); - refresh_field('accounts'); + refresh_field("accounts"); } - } + }, }); } }, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 82a9c0f1524..b4fb13e42c3 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -33,7 +33,7 @@ class StockAccountInvalidTransaction(frappe.ValidationError): class JournalEntry(AccountsController): def __init__(self, *args, **kwargs): - super(JournalEntry, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def get_feed(self): return self.voucher_type @@ -68,7 +68,6 @@ class JournalEntry(AccountsController): self.set_print_format_fields() self.validate_credit_debit_note() self.validate_empty_accounts_table() - self.set_account_and_party_balance() self.validate_inter_company_accounts() self.validate_depr_entry_voucher_type() @@ -78,6 +77,20 @@ class JournalEntry(AccountsController): if not self.title: self.title = self.get_title() + def submit(self): + if len(self.accounts) > 100: + msgprint(_("The task has been enqueued as a background job."), alert=True) + self.queue_action("submit", timeout=4600) + else: + return self._submit() + + def cancel(self): + if len(self.accounts) > 100: + msgprint(_("The task has been enqueued as a background job."), alert=True) + self.queue_action("cancel", timeout=4600) + else: + return self._cancel() + def on_submit(self): self.validate_cheque_info() self.check_credit_limit() @@ -89,7 +102,7 @@ class JournalEntry(AccountsController): def on_cancel(self): # References for this Journal are removed on the `on_cancel` event in accounts_controller - super(JournalEntry, self).on_cancel() + super().on_cancel() self.ignore_linked_doctypes = ( "GL Entry", "Stock Ledger Entry", @@ -124,10 +137,7 @@ class JournalEntry(AccountsController): frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid() def validate_inter_company_accounts(self): - if ( - self.voucher_type == "Inter Company Journal Entry" - and self.inter_company_journal_entry_reference - ): + if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference) account_currency = frappe.get_cached_value("Company", self.company, "default_currency") previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency") @@ -273,10 +283,7 @@ class JournalEntry(AccountsController): asset.set_status() def update_inter_company_jv(self): - if ( - self.voucher_type == "Inter Company Journal Entry" - and self.inter_company_journal_entry_reference - ): + if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: frappe.db.set_value( "Journal Entry", self.inter_company_journal_entry_reference, @@ -304,17 +311,25 @@ class JournalEntry(AccountsController): if d.account == inv_disc_doc.short_term_loan and d.reference_name == inv_disc: if self.docstatus == 1: if d.credit > 0: - _validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Sanctioned", d.idx) + _validate_invoice_discounting_status( + inv_disc, inv_disc_doc.status, "Sanctioned", d.idx + ) status = "Disbursed" elif d.debit > 0: - _validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Disbursed", d.idx) + _validate_invoice_discounting_status( + inv_disc, inv_disc_doc.status, "Disbursed", d.idx + ) status = "Settled" else: if d.credit > 0: - _validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Disbursed", d.idx) + _validate_invoice_discounting_status( + inv_disc, inv_disc_doc.status, "Disbursed", d.idx + ) status = "Sanctioned" elif d.debit > 0: - _validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Settled", d.idx) + _validate_invoice_discounting_status( + inv_disc, inv_disc_doc.status, "Settled", d.idx + ) status = "Disbursed" break if status: @@ -371,10 +386,7 @@ class JournalEntry(AccountsController): ) def unlink_inter_company_jv(self): - if ( - self.voucher_type == "Inter Company Journal Entry" - and self.inter_company_journal_entry_reference - ): + if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: frappe.db.set_value( "Journal Entry", self.inter_company_journal_entry_reference, @@ -396,9 +408,9 @@ class JournalEntry(AccountsController): if account_type in ["Receivable", "Payable"]: if not (d.party_type and d.party): frappe.throw( - _("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format( - d.idx, d.account - ) + _( + "Row {0}: Party Type and Party is required for Receivable / Payable account {1}" + ).format(d.idx, d.account) ) elif ( d.party_type @@ -461,17 +473,30 @@ class JournalEntry(AccountsController): elif d.party_type == "Supplier" and flt(d.credit) > 0: frappe.throw(_("Row {0}: Advance against Supplier must be debit").format(d.idx)) + def system_generated_gain_loss(self): + return ( + self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency and self.is_system_generated + ) + def validate_against_jv(self): for d in self.get("accounts"): if d.reference_type == "Journal Entry": - account_root_type = frappe.db.get_value("Account", d.account, "root_type") - if account_root_type == "Asset" and flt(d.debit) > 0: + account_root_type = frappe.get_cached_value("Account", d.account, "root_type") + if ( + account_root_type == "Asset" + and flt(d.debit) > 0 + and not self.system_generated_gain_loss() + ): frappe.throw( _( "Row #{0}: For {1}, you can select reference document only if account gets credited" ).format(d.idx, d.account) ) - elif account_root_type == "Liability" and flt(d.credit) > 0: + elif ( + account_root_type == "Liability" + and flt(d.credit) > 0 + and not self.system_generated_gain_loss() + ): frappe.throw( _( "Row #{0}: For {1}, you can select reference document only if account gets debited" @@ -503,7 +528,7 @@ class JournalEntry(AccountsController): for jvd in against_entries: if flt(jvd[dr_or_cr]) > 0: valid = True - if not valid: + if not valid and not self.system_generated_gain_loss(): frappe.throw( _("Against Journal Entry {0} does not have any unmatched {1} entry").format( d.reference_name, dr_or_cr @@ -543,11 +568,13 @@ class JournalEntry(AccountsController): if d.reference_type == "Purchase Order" and flt(d.credit) > 0: frappe.throw( - _("Row {0}: Credit entry can not be linked with a {1}").format(d.idx, d.reference_type) + _("Row {0}: Credit entry can not be linked with a {1}").format( + d.idx, d.reference_type + ) ) # set totals - if not d.reference_name in self.reference_totals: + if d.reference_name not in self.reference_totals: self.reference_totals[d.reference_name] = 0.0 if self.voucher_type not in ("Deferred Revenue", "Deferred Expense"): @@ -565,7 +592,10 @@ class JournalEntry(AccountsController): # check if party and account match if d.reference_type in ("Sales Invoice", "Purchase Invoice"): - if self.voucher_type in ("Deferred Revenue", "Deferred Expense") and d.reference_detail_no: + if ( + self.voucher_type in ("Deferred Revenue", "Deferred Expense") + and d.reference_detail_no + ): debit_or_credit = "Debit" if d.debit else "Credit" party_account = get_deferred_booking_accounts( d.reference_type, d.reference_detail_no, debit_or_credit @@ -574,7 +604,8 @@ class JournalEntry(AccountsController): else: if d.reference_type == "Sales Invoice": party_account = ( - get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1] + get_party_account_based_on_invoice_discounting(d.reference_name) + or against_voucher[1] ) else: party_account = against_voucher[1] @@ -698,7 +729,9 @@ class JournalEntry(AccountsController): if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency): if self.difference: frappe.throw( - _("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference) + _("Total Debit must be equal to Total Credit. The difference is {0}").format( + self.difference + ) ) def set_total_debit_credit(self): @@ -762,7 +795,6 @@ class JournalEntry(AccountsController): and self.posting_date ) ): - ignore_exchange_rate = False if self.get("flags") and self.flags.get("ignore_exchange_rate"): ignore_exchange_rate = True @@ -1008,27 +1040,21 @@ class JournalEntry(AccountsController): self.validate_total_debit_and_credit() def get_values(self): - cond = ( - " and outstanding_amount <= {0}".format(self.write_off_amount) - if flt(self.write_off_amount) > 0 - else "" - ) + cond = f" and outstanding_amount <= {self.write_off_amount}" if flt(self.write_off_amount) > 0 else "" if self.write_off_based_on == "Accounts Receivable": return frappe.db.sql( """select name, debit_to as account, customer as party, outstanding_amount - from `tabSales Invoice` where docstatus = 1 and company = %s - and outstanding_amount > 0 %s""" - % ("%s", cond), + from `tabSales Invoice` where docstatus = 1 and company = {} + and outstanding_amount > 0 {}""".format("%s", cond), self.company, as_dict=True, ) elif self.write_off_based_on == "Accounts Payable": return frappe.db.sql( """select name, credit_to as account, supplier as party, outstanding_amount - from `tabPurchase Invoice` where docstatus = 1 and company = %s - and outstanding_amount > 0 %s""" - % ("%s", cond), + from `tabPurchase Invoice` where docstatus = 1 and company = {} + and outstanding_amount > 0 {}""".format("%s", cond), self.company, as_dict=True, ) @@ -1051,21 +1077,6 @@ class JournalEntry(AccountsController): if not self.get("accounts"): frappe.throw(_("Accounts table cannot be blank.")) - def set_account_and_party_balance(self): - account_balance = {} - party_balance = {} - for d in self.get("accounts"): - if d.account not in account_balance: - account_balance[d.account] = get_balance_on(account=d.account, date=self.posting_date) - - if (d.party_type, d.party) not in party_balance: - party_balance[(d.party_type, d.party)] = get_balance_on( - party_type=d.party_type, party=d.party, date=self.posting_date, company=self.company - ) - - d.account_balance = account_balance[d.account] - d.party_balance = party_balance[(d.party_type, d.party)] - @frappe.whitelist() def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None): @@ -1152,7 +1163,7 @@ def get_payment_entry_against_order( "amount_field_bank": amount_field_bank, "amount": amount, "debit_in_account_currency": debit_in_account_currency, - "remarks": "Advance Payment received against {0} {1}".format(dt, dn), + "remarks": f"Advance Payment received against {dt} {dn}", "is_advance": "Yes", "bank_account": bank_account, "journal_entry": journal_entry, @@ -1191,7 +1202,7 @@ def get_payment_entry_against_invoice( "amount_field_bank": amount_field_bank, "amount": amount if amount else abs(ref_doc.outstanding_amount), "debit_in_account_currency": debit_in_account_currency, - "remarks": "Payment received against {0} {1}. {2}".format(dt, dn, ref_doc.remarks), + "remarks": f"Payment received against {dt} {dn}. {ref_doc.remarks}", "is_advance": "No", "bank_account": bank_account, "journal_entry": journal_entry, @@ -1217,9 +1228,7 @@ def get_payment_entry(ref_doc, args): ) je = frappe.new_doc("Journal Entry") - je.update( - {"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")} - ) + je.update({"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")}) party_row = je.append( "accounts", @@ -1231,8 +1240,6 @@ def get_payment_entry(ref_doc, args): "account_type": frappe.db.get_value("Account", args.get("party_account"), "account_type"), "account_currency": args.get("party_account_currency") or get_account_currency(args.get("party_account")), - "balance": get_balance_on(args.get("party_account")), - "party_balance": get_balance_on(party=args.get("party"), party_type=args.get("party_type")), "exchange_rate": exchange_rate, args.get("amount_field_party"): args.get("amount"), "is_advance": args.get("is_advance"), @@ -1244,9 +1251,7 @@ def get_payment_entry(ref_doc, args): bank_row = je.append("accounts") # Make it bank_details - bank_account = get_default_bank_cash_account( - ref_doc.company, "Bank", account=args.get("bank_account") - ) + bank_account = get_default_bank_cash_account(ref_doc.company, "Bank", account=args.get("bank_account")) if bank_account: bank_row.update(bank_account) # Modified to include the posting date for which the exchange rate is required. @@ -1286,7 +1291,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters): return [] return frappe.db.sql( - """ + f""" SELECT jv.name, jv.posting_date, jv.user_remark FROM `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail WHERE jv_detail.parent = jv.name @@ -1297,16 +1302,14 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters): OR jv_detail.reference_type = '' ) AND jv.docstatus = 1 - AND jv.`{0}` LIKE %(txt)s + AND jv.`{searchfield}` LIKE %(txt)s ORDER BY jv.name DESC LIMIT %(limit)s offset %(offset)s - """.format( - searchfield - ), + """, dict( account=filters.get("account"), party=cstr(filters.get("party")), - txt="%{0}%".format(txt), + txt=f"%{txt}%", offset=start, limit=page_len, ), @@ -1328,19 +1331,15 @@ def get_outstanding(args): condition = " and party=%(party)s" if args.get("party") else "" against_jv_amount = frappe.db.sql( - """ + f""" select sum(debit_in_account_currency) - sum(credit_in_account_currency) - from `tabJournal Entry Account` where parent=%(docname)s and account=%(account)s {0} - and (reference_type is null or reference_type = '')""".format( - condition - ), + from `tabJournal Entry Account` where parent=%(docname)s and account=%(account)s {condition} + and (reference_type is null or reference_type = '')""", args, ) against_jv_amount = flt(against_jv_amount[0][0]) if against_jv_amount else 0 - amount_field = ( - "credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency" - ) + amount_field = "credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency" return {amount_field: abs(against_jv_amount)} elif args.get("doctype") in ("Sales Invoice", "Purchase Invoice"): party_type = "Customer" if args.get("doctype") == "Sales Invoice" else "Supplier" @@ -1353,9 +1352,7 @@ def get_outstanding(args): due_date = invoice.get("due_date") - exchange_rate = ( - invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1 - ) + exchange_rate = invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1 if args["doctype"] == "Sales Invoice": amount_field = ( @@ -1380,37 +1377,26 @@ def get_outstanding(args): @frappe.whitelist() -def get_party_account_and_balance(company, party_type, party, cost_center=None): +def get_party_account_and_currency(company, party_type, party): if not frappe.has_permission("Account"): frappe.msgprint(_("No Permission"), raise_exception=1) account = get_party_account(party_type, party, company) - account_balance = get_balance_on(account=account, cost_center=cost_center) - party_balance = get_balance_on( - party_type=party_type, party=party, company=company, cost_center=cost_center - ) - return { "account": account, - "balance": account_balance, - "party_balance": party_balance, - "account_currency": frappe.db.get_value("Account", account, "account_currency"), + "account_currency": frappe.get_cached_value("Account", account, "account_currency"), } @frappe.whitelist() -def get_account_balance_and_party_type( - account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None -): - """Returns dict of account balance and party type to be set in Journal Entry on selection of account.""" +def get_account_details_and_party_type(account, date, company, debit=None, credit=None, exchange_rate=None): + """Returns dict of account details and party type to be set in Journal Entry on selection of account.""" if not frappe.has_permission("Account"): frappe.msgprint(_("No Permission"), raise_exception=1) company_currency = erpnext.get_company_currency(company) - account_details = frappe.db.get_value( - "Account", account, ["account_type", "account_currency"], as_dict=1 - ) + account_details = frappe.db.get_value("Account", account, ["account_type", "account_currency"], as_dict=1) if not account_details: return @@ -1423,7 +1409,6 @@ def get_account_balance_and_party_type( party_type = "" grid_values = { - "balance": get_balance_on(account, date, cost_center=cost_center), "party_type": party_type, "account_type": account_details.account_type, "account_currency": account_details.account_currency or company_currency, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry_list.js b/erpnext/accounts/doctype/journal_entry/journal_entry_list.js index 48d6115e3df..9d6e87392e5 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry_list.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry_list.js @@ -1,12 +1,12 @@ -frappe.listview_settings['Journal Entry'] = { +frappe.listview_settings["Journal Entry"] = { add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark"], - get_indicator: function(doc) { - if(doc.docstatus==0) { - return [__("Draft", "red", "docstatus,=,0")] - } else if(doc.docstatus==2) { - return [__("Cancelled", "grey", "docstatus,=,2")] + get_indicator: function (doc) { + if (doc.docstatus == 0) { + return [__("Draft", "red", "docstatus,=,0")]; + } else if (doc.docstatus == 2) { + return [__("Cancelled", "grey", "docstatus,=,2")]; } else { - return [__(doc.voucher_type), "blue", "voucher_type,=," + doc.voucher_type] + return [__(doc.voucher_type), "blue", "voucher_type,=," + doc.voucher_type]; } - } + }, }; diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index e44ebc6afce..979f964b62a 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -69,10 +69,8 @@ class TestJournalEntry(unittest.TestCase): self.assertTrue( frappe.db.sql( - """select name from `tabJournal Entry Account` - where reference_type = %s and reference_name = %s and {0}=400""".format( - dr_or_cr - ), + f"""select name from `tabJournal Entry Account` + where reference_type = %s and reference_name = %s and {dr_or_cr}=400""", (submitted_voucher.doctype, submitted_voucher.name), ) ) @@ -84,9 +82,8 @@ class TestJournalEntry(unittest.TestCase): def advance_paid_testcase(self, base_jv, test_voucher, dr_or_cr): # Test advance paid field advance_paid = frappe.db.sql( - """select advance_paid from `tab%s` - where name=%s""" - % (test_voucher.doctype, "%s"), + """select advance_paid from `tab{}` + where name={}""".format(test_voucher.doctype, "%s"), (test_voucher.name), ) payment_against_order = base_jv.get("accounts")[0].get(dr_or_cr) @@ -159,9 +156,7 @@ class TestJournalEntry(unittest.TestCase): jv.cancel() def test_multi_currency(self): - jv = make_journal_entry( - "_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False - ) + jv = make_journal_entry("_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False) jv.get("accounts")[1].credit_in_account_currency = 5000 jv.submit() @@ -201,7 +196,7 @@ class TestJournalEntry(unittest.TestCase): "credit", "credit_in_account_currency", ): - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][field], gle[field]) # cancel @@ -263,7 +258,7 @@ class TestJournalEntry(unittest.TestCase): "credit", "credit_in_account_currency", ): - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][field], gle[field]) def test_disallow_change_in_account_currency_for_a_party(self): diff --git a/erpnext/accounts/doctype/journal_entry/test_records.json b/erpnext/accounts/doctype/journal_entry/test_records.json index 5077305cf22..73777d4008c 100644 --- a/erpnext/accounts/doctype/journal_entry/test_records.json +++ b/erpnext/accounts/doctype/journal_entry/test_records.json @@ -81,7 +81,6 @@ }, { "account": "Sales - _TC", - "cost_center": "_Test Cost Center - _TC", "credit_in_account_currency": 400.0, "debit_in_account_currency": 0.0, "doctype": "Journal Entry Account", diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 3132fe9b12b..94a050a9004 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -9,12 +9,10 @@ "field_order": [ "account", "account_type", - "balance", "col_break1", "bank_account", "party_type", "party", - "party_balance", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -63,17 +61,6 @@ "label": "Account Type", "print_hide": 1 }, - { - "fieldname": "balance", - "fieldtype": "Currency", - "label": "Account Balance", - "no_copy": 1, - "oldfieldname": "balance", - "oldfieldtype": "Data", - "options": "account_currency", - "print_hide": 1, - "read_only": 1 - }, { "default": ":Company", "description": "If Income or Expense", @@ -107,14 +94,6 @@ "label": "Party", "options": "party_type" }, - { - "fieldname": "party_balance", - "fieldtype": "Currency", - "label": "Party Balance", - "options": "account_currency", - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "currency_section", "fieldtype": "Section Break", diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js index 5ebdf61db2c..9a526af4eb4 100644 --- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js +++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js @@ -2,78 +2,82 @@ // For license information, please see license.txt frappe.ui.form.on("Journal Entry Template", { - onload: function(frm) { - if(frm.is_new()) { + onload: function (frm) { + if (frm.is_new()) { frappe.call({ type: "GET", method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series", - callback: function(r){ - if(r.message) { + callback: function (r) { + if (r.message) { frm.set_df_property("naming_series", "options", r.message.split("\n")); frm.set_value("naming_series", r.message.split("\n")[0]); frm.refresh_field("naming_series"); } - } + }, }); } }, - refresh: function(frm) { + refresh: function (frm) { frappe.model.set_default_values(frm.doc); - frm.set_query("account" ,"accounts", function(){ + frm.set_query("account", "accounts", function () { var filters = { company: frm.doc.company, - is_group: 0 + is_group: 0, }; - if(!frm.doc.multi_currency) { + if (!frm.doc.multi_currency) { $.extend(filters, { - account_currency: frappe.get_doc(":Company", frm.doc.company).default_currency + account_currency: frappe.get_doc(":Company", frm.doc.company).default_currency, }); } return { filters: filters }; }); }, - voucher_type: function(frm) { - var add_accounts = function(doc, r) { - $.each(r, function(i, d) { + voucher_type: function (frm) { + var add_accounts = function (doc, r) { + $.each(r, function (i, d) { var row = frappe.model.add_child(doc, "Journal Entry Template Account", "accounts"); row.account = d.account; }); refresh_field("accounts"); }; - if(!frm.doc.company) return; + if (!frm.doc.company) return; frm.trigger("clear_child"); - switch(frm.doc.voucher_type){ + switch (frm.doc.voucher_type) { case "Bank Entry": case "Cash Entry": frappe.call({ type: "GET", method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account", args: { - "account_type": (frm.doc.voucher_type=="Bank Entry" ? - "Bank" : (frm.doc.voucher_type=="Cash Entry" ? "Cash" : null)), - "company": frm.doc.company + account_type: + frm.doc.voucher_type == "Bank Entry" + ? "Bank" + : frm.doc.voucher_type == "Cash Entry" + ? "Cash" + : null, + company: frm.doc.company, }, - callback: function(r) { - if(r.message) { + callback: function (r) { + if (r.message) { // If default company bank account not set - if(!$.isEmptyObject(r.message)){ + if (!$.isEmptyObject(r.message)) { add_accounts(frm.doc, [r.message]); } } - } + }, }); break; default: frm.trigger("clear_child"); } }, - clear_child: function(frm){ + clear_child: function (frm) { frappe.model.clear_table(frm.doc, "accounts"); frm.refresh_field("accounts"); - } + }, }); diff --git a/erpnext/accounts/doctype/ledger_health/__init__.py b/erpnext/accounts/doctype/ledger_health/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/ledger_health/ledger_health.js b/erpnext/accounts/doctype/ledger_health/ledger_health.js new file mode 100644 index 00000000000..e207daef511 --- /dev/null +++ b/erpnext/accounts/doctype/ledger_health/ledger_health.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Ledger Health", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/ledger_health/ledger_health.json b/erpnext/accounts/doctype/ledger_health/ledger_health.json new file mode 100644 index 00000000000..fb2da3d2564 --- /dev/null +++ b/erpnext/accounts/doctype/ledger_health/ledger_health.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "autoname": "autoincrement", + "creation": "2024-03-26 17:01:47.443986", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_no", + "checked_on", + "debit_credit_mismatch", + "general_and_payment_ledger_mismatch" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Data", + "label": "Voucher Type" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Data", + "label": "Voucher No" + }, + { + "default": "0", + "fieldname": "debit_credit_mismatch", + "fieldtype": "Check", + "label": "Debit-Credit mismatch" + }, + { + "fieldname": "checked_on", + "fieldtype": "Datetime", + "label": "Checked On" + }, + { + "default": "0", + "fieldname": "general_and_payment_ledger_mismatch", + "fieldtype": "Check", + "label": "General and Payment Ledger mismatch" + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-04-09 11:16:07.044484", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Ledger Health", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/ledger_health/ledger_health.py b/erpnext/accounts/doctype/ledger_health/ledger_health.py new file mode 100644 index 00000000000..590ff80cc11 --- /dev/null +++ b/erpnext/accounts/doctype/ledger_health/ledger_health.py @@ -0,0 +1,25 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LedgerHealth(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + checked_on: DF.Datetime | None + debit_credit_mismatch: DF.Check + general_and_payment_ledger_mismatch: DF.Check + name: DF.Int | None + voucher_no: DF.Data | None + voucher_type: DF.Data | None + # end: auto-generated types + + pass diff --git a/erpnext/accounts/doctype/ledger_health/test_ledger_health.py b/erpnext/accounts/doctype/ledger_health/test_ledger_health.py new file mode 100644 index 00000000000..d35b39d9ea1 --- /dev/null +++ b/erpnext/accounts/doctype/ledger_health/test_ledger_health.py @@ -0,0 +1,109 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase +from frappe.utils import nowdate + +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.accounts.utils import run_ledger_health_checks + + +class TestLedgerHealth(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.configure_monitoring_tool() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def configure_monitoring_tool(self): + monitor_settings = frappe.get_doc("Ledger Health Monitor") + monitor_settings.enable_health_monitor = True + monitor_settings.enable_for_last_x_days = 60 + monitor_settings.debit_credit_mismatch = True + monitor_settings.general_and_payment_ledger_mismatch = True + exists = [x for x in monitor_settings.companies if x.company == self.company] + if not exists: + monitor_settings.append("companies", {"company": self.company}) + monitor_settings.save() + + def clear_old_entries(self): + super().clear_old_entries() + lh = qb.DocType("Ledger Health") + qb.from_(lh).delete().run() + + def create_journal(self): + je = frappe.new_doc("Journal Entry") + je.company = self.company + je.voucher_type = "Journal Entry" + je.posting_date = nowdate() + je.append( + "accounts", + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "debit_in_account_currency": 10000, + }, + ) + je.append("accounts", {"account": self.income_account, "credit_in_account_currency": 10000}) + je.save().submit() + self.je = je + + def test_debit_credit_mismatch(self): + self.create_journal() + + # manually cause debit-credit mismatch + gle = frappe.db.get_all( + "GL Entry", filters={"voucher_no": self.je.name, "account": self.income_account} + )[0] + frappe.db.set_value("GL Entry", gle.name, "credit", 8000) + + run_ledger_health_checks() + expected = { + "voucher_type": self.je.doctype, + "voucher_no": self.je.name, + "debit_credit_mismatch": True, + "general_and_payment_ledger_mismatch": False, + } + actual = frappe.db.get_all( + "Ledger Health", + fields=[ + "voucher_type", + "voucher_no", + "debit_credit_mismatch", + "general_and_payment_ledger_mismatch", + ], + ) + self.assertEqual(len(actual), 1) + self.assertEqual(expected, actual[0]) + + def test_gl_and_pl_mismatch(self): + self.create_journal() + + # manually cause GL and PL discrepancy + ple = frappe.db.get_all("Payment Ledger Entry", filters={"voucher_no": self.je.name})[0] + frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", 11000) + + run_ledger_health_checks() + expected = { + "voucher_type": self.je.doctype, + "voucher_no": self.je.name, + "debit_credit_mismatch": False, + "general_and_payment_ledger_mismatch": True, + } + actual = frappe.db.get_all( + "Ledger Health", + fields=[ + "voucher_type", + "voucher_no", + "debit_credit_mismatch", + "general_and_payment_ledger_mismatch", + ], + ) + self.assertEqual(len(actual), 1) + self.assertEqual(expected, actual[0]) diff --git a/erpnext/accounts/doctype/ledger_health_monitor/__init__.py b/erpnext/accounts/doctype/ledger_health_monitor/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.js b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.js new file mode 100644 index 00000000000..cf112760f8c --- /dev/null +++ b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Ledger Health Monitor", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.json b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.json new file mode 100644 index 00000000000..6e688333e3f --- /dev/null +++ b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.json @@ -0,0 +1,104 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-03-27 09:38:07.427997", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enable_health_monitor", + "monitor_section", + "monitor_for_last_x_days", + "debit_credit_mismatch", + "general_and_payment_ledger_mismatch", + "section_break_xdsp", + "companies" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable_health_monitor", + "fieldtype": "Check", + "label": "Enable Health Monitor" + }, + { + "fieldname": "monitor_section", + "fieldtype": "Section Break", + "label": "Configuration" + }, + { + "default": "0", + "fieldname": "debit_credit_mismatch", + "fieldtype": "Check", + "label": "Debit-Credit Mismatch" + }, + { + "default": "0", + "fieldname": "general_and_payment_ledger_mismatch", + "fieldtype": "Check", + "label": "Discrepancy between General and Payment Ledger" + }, + { + "default": "60", + "fieldname": "monitor_for_last_x_days", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Monitor for Last 'X' days", + "reqd": 1 + }, + { + "fieldname": "section_break_xdsp", + "fieldtype": "Section Break", + "label": "Companies" + }, + { + "fieldname": "companies", + "fieldtype": "Table", + "options": "Ledger Health Monitor Company" + } + ], + "hide_toolbar": 1, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2024-03-27 10:14:16.511681", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Ledger Health Monitor", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.py b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.py new file mode 100644 index 00000000000..9f7c569d6d1 --- /dev/null +++ b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.py @@ -0,0 +1,28 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LedgerHealthMonitor(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.ledger_health_monitor_company.ledger_health_monitor_company import ( + LedgerHealthMonitorCompany, + ) + + companies: DF.Table[LedgerHealthMonitorCompany] + debit_credit_mismatch: DF.Check + enable_health_monitor: DF.Check + general_and_payment_ledger_mismatch: DF.Check + monitor_for_last_x_days: DF.Int + # end: auto-generated types + + pass diff --git a/erpnext/accounts/doctype/ledger_health_monitor/test_ledger_health_monitor.py b/erpnext/accounts/doctype/ledger_health_monitor/test_ledger_health_monitor.py new file mode 100644 index 00000000000..e0ba4435b82 --- /dev/null +++ b/erpnext/accounts/doctype/ledger_health_monitor/test_ledger_health_monitor.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLedgerHealthMonitor(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/ledger_health_monitor_company/__init__.py b/erpnext/accounts/doctype/ledger_health_monitor_company/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.json b/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.json new file mode 100644 index 00000000000..87fa3e32801 --- /dev/null +++ b/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-03-27 10:04:45.727054", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-03-27 10:06:22.806155", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Ledger Health Monitor Company", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.py b/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.py new file mode 100644 index 00000000000..5890410090d --- /dev/null +++ b/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.py @@ -0,0 +1,23 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LedgerHealthMonitorCompany(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + company: DF.Link | None + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + # end: auto-generated types + + pass diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.js b/erpnext/accounts/doctype/ledger_merge/ledger_merge.js index b2db98dbd03..6faae84447b 100644 --- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.js +++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.js @@ -1,9 +1,9 @@ // Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Ledger Merge', { - setup: function(frm) { - frappe.realtime.on('ledger_merge_refresh', ({ ledger_merge }) => { +frappe.ui.form.on("Ledger Merge", { + setup: function (frm) { + frappe.realtime.on("ledger_merge_refresh", ({ ledger_merge }) => { if (ledger_merge !== frm.doc.name) return; frappe.model.clear_doc(frm.doc.doctype, frm.doc.name); frappe.model.with_doc(frm.doc.doctype, frm.doc.name).then(() => { @@ -11,29 +11,29 @@ frappe.ui.form.on('Ledger Merge', { }); }); - frappe.realtime.on('ledger_merge_progress', data => { + frappe.realtime.on("ledger_merge_progress", (data) => { if (data.ledger_merge !== frm.doc.name) return; - let message = __('Merging {0} of {1}', [data.current, data.total]); + let message = __("Merging {0} of {1}", [data.current, data.total]); let percent = Math.floor((data.current * 100) / data.total); - frm.dashboard.show_progress(__('Merge Progress'), percent, message); - frm.page.set_indicator(__('In Progress'), 'orange'); + frm.dashboard.show_progress(__("Merge Progress"), percent, message); + frm.page.set_indicator(__("In Progress"), "orange"); }); - frm.set_query("account", function(doc) { - if (!doc.company) frappe.throw(__('Please set Company')); - if (!doc.root_type) frappe.throw(__('Please set Root Type')); + frm.set_query("account", function (doc) { + if (!doc.company) frappe.throw(__("Please set Company")); + if (!doc.root_type) frappe.throw(__("Please set Root Type")); return { filters: { root_type: doc.root_type, - company: doc.company - } + company: doc.company, + }, }; }); - frm.set_query('account', 'merge_accounts', function(doc) { - if (!doc.company) frappe.throw(__('Please set Company')); - if (!doc.root_type) frappe.throw(__('Please set Root Type')); - if (!doc.account) frappe.throw(__('Please set Account')); + frm.set_query("account", "merge_accounts", function (doc) { + if (!doc.company) frappe.throw(__("Please set Company")); + if (!doc.root_type) frappe.throw(__("Please set Root Type")); + if (!doc.account) frappe.throw(__("Please set Account")); let acc = [doc.account]; frm.doc.merge_accounts.forEach((row) => { acc.push(row.account); @@ -43,86 +43,86 @@ frappe.ui.form.on('Ledger Merge', { is_group: doc.is_group, root_type: doc.root_type, name: ["not in", acc], - company: doc.company - } + company: doc.company, + }, }; }); }, - refresh: function(frm) { + refresh: function (frm) { frm.page.hide_icon_group(); - frm.trigger('set_merge_status'); - frm.trigger('update_primary_action'); + frm.trigger("set_merge_status"); + frm.trigger("update_primary_action"); }, - after_save: function(frm) { + after_save: function (frm) { setTimeout(() => { - frm.trigger('update_primary_action'); + frm.trigger("update_primary_action"); }, 500); }, - update_primary_action: function(frm) { + update_primary_action: function (frm) { if (frm.is_dirty()) { frm.enable_save(); return; } frm.disable_save(); - if (frm.doc.status !== 'Success') { + if (frm.doc.status !== "Success") { if (!frm.is_new()) { - let label = frm.doc.status === 'Pending' ? __('Start Merge') : __('Retry'); + let label = frm.doc.status === "Pending" ? __("Start Merge") : __("Retry"); frm.page.set_primary_action(label, () => frm.events.start_merge(frm)); } else { - frm.page.set_primary_action(__('Save'), () => frm.save()); + frm.page.set_primary_action(__("Save"), () => frm.save()); } } }, - start_merge: function(frm) { + start_merge: function (frm) { frm.call({ - method: 'form_start_merge', + method: "form_start_merge", args: { docname: frm.doc.name }, - btn: frm.page.btn_primary - }).then(r => { + btn: frm.page.btn_primary, + }).then((r) => { if (r.message === true) { frm.disable_save(); } }); }, - set_merge_status: function(frm) { + set_merge_status: function (frm) { if (frm.doc.status == "Pending") return; let successful_records = 0; frm.doc.merge_accounts.forEach((row) => { if (row.merged) successful_records += 1; }); let message_args = [successful_records, frm.doc.merge_accounts.length]; - frm.dashboard.set_headline(__('Successfully merged {0} out of {1}.', message_args)); + frm.dashboard.set_headline(__("Successfully merged {0} out of {1}.", message_args)); }, - root_type: function(frm) { - frm.set_value('account', ''); - frm.set_value('merge_accounts', []); + root_type: function (frm) { + frm.set_value("account", ""); + frm.set_value("merge_accounts", []); }, - company: function(frm) { - frm.set_value('account', ''); - frm.set_value('merge_accounts', []); - } + company: function (frm) { + frm.set_value("account", ""); + frm.set_value("merge_accounts", []); + }, }); -frappe.ui.form.on('Ledger Merge Accounts', { - merge_accounts_add: function(frm) { - frm.trigger('update_primary_action'); +frappe.ui.form.on("Ledger Merge Accounts", { + merge_accounts_add: function (frm) { + frm.trigger("update_primary_action"); }, - merge_accounts_remove: function(frm) { - frm.trigger('update_primary_action'); + merge_accounts_remove: function (frm) { + frm.trigger("update_primary_action"); }, - account: function(frm, cdt, cdn) { + account: function (frm, cdt, cdn) { let row = frappe.get_doc(cdt, cdn); row.account_name = row.account; - frm.refresh_field('merge_accounts'); - frm.trigger('update_primary_action'); - } + frm.refresh_field("merge_accounts"); + frm.trigger("update_primary_action"); + }, }); diff --git a/erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py index 992ce9ede5d..dccd73c62a9 100644 --- a/erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py +++ b/erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py @@ -83,7 +83,10 @@ class TestLedgerMerge(unittest.TestCase): "account": "Indirect Income - _TC", "merge_accounts": [ {"account": "Indirect Test Income - _TC", "account_name": "Indirect Test Income"}, - {"account": "Administrative Test Income - _TC", "account_name": "Administrative Test Income"}, + { + "account": "Administrative Test Income - _TC", + "account_name": "Administrative Test Income", + }, ], } ).insert(ignore_permissions=True) diff --git a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.js b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.js index d7dc7f3f3c7..d522f6d8af1 100644 --- a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.js +++ b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Loyalty Point Entry', { - refresh: function(frm) { - - } +frappe.ui.form.on("Loyalty Point Entry", { + refresh: function (frm) {}, }); diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js index 6951b2a2b32..4c29be3b556 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js @@ -3,30 +3,37 @@ frappe.provide("erpnext.accounts.dimensions"); -frappe.ui.form.on('Loyalty Program', { - setup: function(frm) { - var help_content = - ` +frappe.ui.form.on("Loyalty Program", { + setup: function (frm) { + var help_content = `
    {% } %} {% if(!filters.show_future_payments) { %} - + {% } %} {% if(!filters.show_future_payments) { %} @@ -158,7 +156,7 @@ {% } %} {% } else { %} - + @@ -187,7 +185,7 @@ {% if(!filters.show_future_payments) { %} {% } else { %} diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index f0ac3d0ffdb..3d0a2b78e5e 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -2,114 +2,114 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["General Ledger"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book" + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "reqd": 1, - "width": "60px" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, + width: "60px", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1, - "width": "60px" + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + width: "60px", }, { - "fieldname":"account", - "label": __("Account"), - "fieldtype": "MultiSelectList", - "options": "Account", - get_data: function(txt) { - return frappe.db.get_link_options('Account', txt, { - company: frappe.query_report.get_filter_value("company") + fieldname: "account", + label: __("Account"), + fieldtype: "MultiSelectList", + options: "Account", + get_data: function (txt) { + return frappe.db.get_link_options("Account", txt, { + company: frappe.query_report.get_filter_value("company"), }); - } + }, }, { - "fieldname":"voucher_no", - "label": __("Voucher No"), - "fieldtype": "Data", - on_change: function() { - frappe.query_report.set_filter_value('group_by', "Group by Voucher (Consolidated)"); - } + fieldname: "voucher_no", + label: __("Voucher No"), + fieldtype: "Data", + on_change: function () { + frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)"); + }, }, { - "fieldtype": "Break", + fieldtype: "Break", }, { - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Autocomplete", + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Autocomplete", options: Object.keys(frappe.boot.party_account_types), - on_change: function() { - frappe.query_report.set_filter_value('party', ""); - } + on_change: function () { + frappe.query_report.set_filter_value("party", ""); + }, }, { - "fieldname":"party", - "label": __("Party"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { + fieldname: "party", + label: __("Party"), + fieldtype: "MultiSelectList", + get_data: function (txt) { if (!frappe.query_report.filters) return; - let party_type = frappe.query_report.get_filter_value('party_type'); + let party_type = frappe.query_report.get_filter_value("party_type"); if (!party_type) return; return frappe.db.get_link_options(party_type, txt); }, - on_change: function() { - var party_type = frappe.query_report.get_filter_value('party_type'); - var parties = frappe.query_report.get_filter_value('party'); + on_change: function () { + var party_type = frappe.query_report.get_filter_value("party_type"); + var parties = frappe.query_report.get_filter_value("party"); - if(!party_type || parties.length === 0 || parties.length > 1) { - frappe.query_report.set_filter_value('party_name', ""); - frappe.query_report.set_filter_value('tax_id', ""); + if (!party_type || parties.length === 0 || parties.length > 1) { + frappe.query_report.set_filter_value("party_name", ""); + frappe.query_report.set_filter_value("tax_id", ""); return; } else { var party = parties[0]; var fieldname = erpnext.utils.get_party_name(party_type) || "name"; - frappe.db.get_value(party_type, party, fieldname, function(value) { - frappe.query_report.set_filter_value('party_name', value[fieldname]); + frappe.db.get_value(party_type, party, fieldname, function (value) { + frappe.query_report.set_filter_value("party_name", value[fieldname]); }); if (party_type === "Customer" || party_type === "Supplier") { - frappe.db.get_value(party_type, party, "tax_id", function(value) { - frappe.query_report.set_filter_value('tax_id', value["tax_id"]); + frappe.db.get_value(party_type, party, "tax_id", function (value) { + frappe.query_report.set_filter_value("tax_id", value["tax_id"]); }); } } - } + }, }, { - "fieldname":"party_name", - "label": __("Party Name"), - "fieldtype": "Data", - "hidden": 1 + fieldname: "party_name", + label: __("Party Name"), + fieldtype: "Data", + hidden: 1, }, { - "fieldname":"group_by", - "label": __("Group by"), - "fieldtype": "Select", - "options": [ + fieldname: "group_by", + label: __("Group by"), + fieldtype: "Select", + options: [ "", { label: __("Group by Voucher"), @@ -128,74 +128,78 @@ frappe.query_reports["General Ledger"] = { value: "Group by Party", }, ], - "default": "Group by Voucher (Consolidated)" + default: "Group by Voucher (Consolidated)", }, { - "fieldname":"tax_id", - "label": __("Tax Id"), - "fieldtype": "Data", - "hidden": 1 + fieldname: "tax_id", + label: __("Tax Id"), + fieldtype: "Data", + hidden: 1, }, { - "fieldname": "presentation_currency", - "label": __("Currency"), - "fieldtype": "Select", - "options": erpnext.get_presentation_currency_list() + fieldname: "presentation_currency", + label: __("Currency"), + fieldtype: "Select", + options: erpnext.get_presentation_currency_list(), }, { - "fieldname":"cost_center", - "label": __("Cost Center"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Cost Center', txt, { - company: frappe.query_report.get_filter_value("company") + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + 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") + 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_dimensions", - "label": __("Consider Accounting Dimensions"), - "fieldtype": "Check", - "default": 1 + fieldname: "include_dimensions", + label: __("Consider Accounting Dimensions"), + fieldtype: "Check", + default: 1, }, { - "fieldname": "show_opening_entries", - "label": __("Show Opening Entries"), - "fieldtype": "Check" + fieldname: "show_opening_entries", + label: __("Show Opening Entries"), + fieldtype: "Check", }, { - "fieldname": "include_default_book_entries", - "label": __("Include Default FB Entries"), - "fieldtype": "Check", - "default": 1 + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, }, { - "fieldname": "show_cancelled_entries", - "label": __("Show Cancelled Entries"), - "fieldtype": "Check" + fieldname: "show_cancelled_entries", + label: __("Show Cancelled Entries"), + fieldtype: "Check", }, { - "fieldname": "show_net_values_in_party_account", - "label": __("Show Net Values in Party Account"), - "fieldtype": "Check" + fieldname: "show_net_values_in_party_account", + label: __("Show Net Values in Party Account"), + fieldtype: "Check", }, { - "fieldname": "show_remarks", - "label": __("Show Remarks"), - "fieldtype": "Check" - } + fieldname: "show_remarks", + label: __("Show Remarks"), + fieldtype: "Check", + }, + { + fieldname: "ignore_err", + label: __("Ignore Exchange Rate Revaluation Journals"), + fieldtype: "Check", + }, + ], +}; - ] -} - -erpnext.utils.add_dimensions('General Ledger', 15) +erpnext.utils.add_dimensions("General Ledger", 15); diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 95397452b01..f70aec743d2 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -102,9 +102,7 @@ def validate_party(filters): def set_account_currency(filters): if filters.get("account") or (filters.get("party") and len(filters.party) == 1): - filters["company_currency"] = frappe.get_cached_value( - "Company", filters.company, "default_currency" - ) + filters["company_currency"] = frappe.get_cached_value("Company", filters.company, "default_currency") account_currency = None if filters.get("account"): @@ -164,9 +162,7 @@ def get_gl_entries(filters, accounting_dimensions): credit_in_account_currency """ if filters.get("show_remarks"): - if remarks_length := frappe.db.get_single_value( - "Accounts Settings", "general_ledger_remarks_length" - ): + if remarks_length := frappe.db.get_single_value("Accounts Settings", "general_ledger_remarks_length"): select_fields += f",substr(remarks, 1, {remarks_length}) as 'remarks'" else: select_fields += """,remarks""" @@ -182,16 +178,14 @@ def get_gl_entries(filters, accounting_dimensions): order_by_statement = "order by account, posting_date, creation" if filters.get("include_default_book_entries"): - filters["company_fb"] = frappe.db.get_value( - "Company", filters.get("company"), "default_finance_book" - ) + filters["company_fb"] = frappe.db.get_value("Company", filters.get("company"), "default_finance_book") dimension_fields = "" if accounting_dimensions: dimension_fields = ", ".join(accounting_dimensions) + "," gl_entries = frappe.db.sql( - """ + f""" select name as gl_entry, posting_date, account, party_type, party, voucher_type, voucher_no, {dimension_fields} @@ -199,14 +193,9 @@ def get_gl_entries(filters, accounting_dimensions): against_voucher_type, against_voucher, account_currency, against, is_opening, creation {select_fields} from `tabGL Entry` - where company=%(company)s {conditions} + where company=%(company)s {get_conditions(filters)} {order_by_statement} - """.format( - dimension_fields=dimension_fields, - select_fields=select_fields, - conditions=get_conditions(filters), - order_by_statement=order_by_statement, - ), + """, filters, as_dict=1, ) @@ -231,6 +220,19 @@ def get_conditions(filters): if filters.get("voucher_no"): conditions.append("voucher_no=%(voucher_no)s") + if filters.get("ignore_err"): + err_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "company": filters.get("company"), + "docstatus": 1, + "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), + }, + as_list=True, + ) + if err_journals: + filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) + if filters.get("voucher_no_not_in"): conditions.append("voucher_no not in %(voucher_no_not_in)s") @@ -260,7 +262,9 @@ def get_conditions(filters): if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr( filters.get("company_fb") ): - frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'")) + frappe.throw( + _("To use a different finance book, please uncheck 'Include Default FB Entries'") + ) else: conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") else: @@ -292,9 +296,9 @@ def get_conditions(filters): filters[dimension.fieldname] = get_dimension_with_children( dimension.document_type, filters.get(dimension.fieldname) ) - conditions.append("{0} in %({0})s".format(dimension.fieldname)) + conditions.append(f"{dimension.fieldname} in %({dimension.fieldname})s") else: - conditions.append("{0} in %({0})s".format(dimension.fieldname)) + conditions.append(f"{dimension.fieldname} in %({dimension.fieldname})s") return "and {}".format(" and ".join(conditions)) if conditions else "" @@ -326,7 +330,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension data.append(totals.opening) if filters.get("group_by") != "Group by Voucher (Consolidated)": - for acc, acc_dict in gle_map.items(): + for _acc, acc_dict in gle_map.items(): # acc if acc_dict.entries: # opening @@ -358,7 +362,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension def get_totals_dict(): def _get_debit_credit_dict(label): return _dict( - account="'{0}'".format(label), + account=f"'{label}'", debit=0.0, credit=0.0, debit_in_account_currency=0.0, @@ -407,9 +411,10 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): data[key].debit_in_account_currency += gle.debit_in_account_currency data[key].credit_in_account_currency += gle.credit_in_account_currency - if filters.get("show_net_values_in_party_account") and account_type_map.get( - data[key].account - ) in ("Receivable", "Payable"): + if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in ( + "Receivable", + "Payable", + ): net_value = data[key].debit - data[key].credit net_value_in_account_currency = ( data[key].debit_in_account_currency - data[key].credit_in_account_currency @@ -472,7 +477,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): else: update_value_in_dict(consolidated_gle, key, gle) - for key, value in consolidated_gle.items(): + for value in consolidated_gle.values(): update_value_in_dict(totals, "total", value) update_value_in_dict(totals, "closing", value) entries.append(value) @@ -482,21 +487,19 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): def get_account_type_map(company): account_type_map = frappe._dict( - frappe.get_all( - "Account", fields=["name", "account_type"], filters={"company": company}, as_list=1 - ) + frappe.get_all("Account", fields=["name", "account_type"], filters={"company": company}, as_list=1) ) return account_type_map def get_result_as_list(data, filters): - balance, balance_in_account_currency = 0, 0 + balance, _balance_in_account_currency = 0, 0 inv_details = get_supplier_invoice_details() for d in data: if not d.get("posting_date"): - balance, balance_in_account_currency = 0, 0 + balance, _balance_in_account_currency = 0, 0 balance = get_balance(d, balance, "debit", "credit") d["balance"] = balance diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index a8c362e78c1..33b356feb33 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import today +from frappe.utils import flt, today from erpnext.accounts.report.general_ledger.general_ledger import execute @@ -148,3 +148,103 @@ class TestGeneralLedger(FrappeTestCase): self.assertEqual(data[2]["credit"], 900) self.assertEqual(data[3]["debit"], 100) self.assertEqual(data[3]["credit"], 100) + + def test_ignore_exchange_rate_journals_filter(self): + # create a new account with USD currency + account_name = "Test Debtors USD" + company = "_Test Company" + account = frappe.get_doc( + { + "account_name": account_name, + "is_group": 0, + "company": company, + "root_type": "Asset", + "report_type": "Balance Sheet", + "account_currency": "USD", + "parent_account": "Accounts Receivable - _TC", + "account_type": "Receivable", + "doctype": "Account", + } + ) + account.insert(ignore_if_duplicate=True) + # create a JV to debit 1000 USD at 75 exchange rate + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = company + jv.multi_currency = 1 + jv.cost_center = "_Test Cost Center - _TC" + jv.set( + "accounts", + [ + { + "account": account.name, + "party_type": "Customer", + "party": "_Test Customer USD", + "debit_in_account_currency": 1000, + "credit_in_account_currency": 0, + "exchange_rate": 75, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "Cash - _TC", + "debit_in_account_currency": 0, + "credit_in_account_currency": 75000, + "cost_center": "_Test Cost Center - _TC", + }, + ], + ) + jv.save() + jv.submit() + + revaluation = frappe.new_doc("Exchange Rate Revaluation") + revaluation.posting_date = today() + revaluation.company = company + accounts = revaluation.get_accounts_data() + revaluation.extend("accounts", accounts) + row = revaluation.accounts[0] + row.new_exchange_rate = 83 + row.new_balance_in_base_currency = flt(row.new_exchange_rate * flt(row.balance_in_account_currency)) + row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) + revaluation.set_total_gain_loss() + revaluation = revaluation.save().submit() + + # post journal entry for Revaluation doc + frappe.db.set_value( + "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" + ) + revaluation_jv = revaluation.make_jv_for_revaluation() + revaluation_jv.cost_center = "_Test Cost Center - _TC" + for acc in revaluation_jv.get("accounts"): + acc.cost_center = "_Test Cost Center - _TC" + revaluation_jv.save() + revaluation_jv.submit() + + # With ignore_err enabled + columns, data = execute( + frappe._dict( + { + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + "ignore_err": True, + } + ) + ) + self.assertNotIn(revaluation_jv.name, set([x.voucher_no for x in data])) + + # Without ignore_err enabled + columns, data = execute( + frappe._dict( + { + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + "ignore_err": False, + } + ) + ) + self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data])) 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 92cf36ebc52..3610d1721ea 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 @@ -3,19 +3,14 @@ /* eslint-disable */ frappe.query_reports["Gross and Net Profit Report"] = { - "filters": [ + filters: [], +}; +frappe.require("assets/erpnext/js/financial_statements.js", function () { + frappe.query_reports["Gross and Net Profit Report"] = $.extend({}, erpnext.financial_statements); - ] -} -frappe.require("assets/erpnext/js/financial_statements.js", function() { - frappe.query_reports["Gross and Net Profit Report"] = $.extend({}, - erpnext.financial_statements); - - frappe.query_reports["Gross and Net Profit Report"]["filters"].push( - { - "fieldname": "accumulated_values", - "label": __("Accumulated Values"), - "fieldtype": "Check" - } - ); + frappe.query_reports["Gross and Net Profit Report"]["filters"].push({ + fieldname: "accumulated_values", + label: __("Accumulated Values"), + fieldtype: "Check", + }); }); diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index f0ca405401d..b90e2e44bf3 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -49,9 +49,7 @@ def execute(filters=None): total=False, ) - columns = get_columns( - filters.periodicity, period_list, filters.accumulated_values, filters.company - ) + columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) gross_income = get_revenue(income, period_list) gross_expense = get_revenue(expense, period_list) @@ -119,9 +117,7 @@ def execute(filters=None): def get_revenue(data, period_list, include_in_gross=1): - revenue = [ - item for item in data if item["include_in_gross"] == include_in_gross or item["is_group"] == 1 - ] + revenue = [item for item in data if item["include_in_gross"] == include_in_gross or item["is_group"] == 1] data_to_be_removed = True while data_to_be_removed: @@ -134,7 +130,7 @@ def get_revenue(data, period_list, include_in_gross=1): def remove_parent_with_no_child(data): data_to_be_removed = False - for parent in data: + for parent in list(data): if "is_group" in parent and parent.get("is_group") == 1: have_child = False for child in data: diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 8e05cf86061..64a9838f54b 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -2,60 +2,61 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Gross Profit"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], - "reqd": 1 + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + reqd: 1, }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], - "reqd": 1 + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + reqd: 1, }, { - "fieldname": "sales_invoice", - "label": __("Sales Invoice"), - "fieldtype": "Link", - "options": "Sales Invoice" + fieldname: "sales_invoice", + label: __("Sales Invoice"), + fieldtype: "Link", + options: "Sales Invoice", }, { - "fieldname": "group_by", - "label": __("Group By"), - "fieldtype": "Select", - "options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term", - "default": "Invoice" + fieldname: "group_by", + label: __("Group By"), + fieldtype: "Select", + options: + "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term", + default: "Invoice", }, { - "fieldname": "item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "options": "Item Group" + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", }, { - "fieldname": "sales_person", - "label": __("Sales Person"), - "fieldtype": "Link", - "options": "Sales Person" + fieldname: "sales_person", + label: __("Sales Person"), + fieldtype: "Link", + options: "Sales Person", }, ], - "tree": true, - "name_field": "parent", - "parent_field": "parent_invoice", - "initial_depth": 3, - "formatter": function(value, row, column, data, default_formatter) { + tree: true, + name_field: "parent", + parent_field: "parent_invoice", + initial_depth: 3, + formatter: function (value, row, column, data, default_formatter) { if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) { column._options = "Sales Invoice"; } else { @@ -71,4 +72,4 @@ frappe.query_reports["Gross Profit"] = { return value; }, -} +}; diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index de3d57d095a..f4d4772901c 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -158,9 +158,7 @@ def execute(filters=None): return columns, data -def get_data_when_grouped_by_invoice( - columns, gross_profit_data, filters, group_wise_columns, data -): +def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data): column_names = get_column_names() # to display item as Item Code: Item Name @@ -395,7 +393,7 @@ def get_column_names(): ) -class GrossProfitGenerator(object): +class GrossProfitGenerator: def __init__(self, filters=None): self.sle = {} self.data = [] @@ -496,10 +494,11 @@ class GrossProfitGenerator(object): def get_average_rate_based_on_group_by(self): for key in list(self.grouped): if self.filters.get("group_by") == "Invoice": - for i, row in enumerate(self.grouped[key]): + for row in self.grouped[key]: if row.indent == 1.0: if ( - row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent] + row.parent in self.returned_invoices + and row.item_code in self.returned_invoices[row.parent] ): returned_item_rows = self.returned_invoices[row.parent][row.item_code] for returned_item_row in returned_item_rows: @@ -512,7 +511,9 @@ class GrossProfitGenerator(object): row.qty = 0 returned_item_row.qty += row.qty row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) - row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) + row.buying_amount = flt( + flt(row.qty) * flt(row.buying_rate), self.currency_precision + ) if flt(row.qty) or row.base_amount: row = self.set_average_rate(row) self.grouped_data.append(row) @@ -567,9 +568,7 @@ class GrossProfitGenerator(object): new_row.buying_rate = ( flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 ) - new_row.base_rate = ( - flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 - ) + new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 return new_row def set_average_gross_profit(self, new_row): @@ -656,33 +655,33 @@ class GrossProfitGenerator(object): elif self.delivery_notes.get((row.parent, row.item_code), None): # check if Invoice has delivery notes dn = self.delivery_notes.get((row.parent, row.item_code)) - parenttype, parent, item_row, warehouse = ( + parenttype, parent, item_row, dn_warehouse = ( "Delivery Note", dn["delivery_note"], dn["item_row"], dn["warehouse"], ) - my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) + my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse) return self.calculate_buying_amount_from_sle( row, my_sle, parenttype, parent, item_row, item_code ) elif row.sales_order and row.so_detail: incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code) if incoming_amount: - return incoming_amount + return flt(row.qty) * incoming_amount else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) return flt(row.qty) * self.get_average_buying_rate(row, item_code) def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code): - from frappe.query_builder.functions import Sum + from frappe.query_builder.functions import Avg delivery_note_item = frappe.qb.DocType("Delivery Note Item") query = ( frappe.qb.from_(delivery_note_item) - .select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty)) + .select(Avg(delivery_note_item.incoming_rate)) .where(delivery_note_item.docstatus == 1) .where(delivery_note_item.item_code == item_code) .where(delivery_note_item.against_sales_order == sales_order) @@ -695,7 +694,7 @@ class GrossProfitGenerator(object): def get_average_buying_rate(self, row, item_code): args = row - if not item_code in self.average_buying_rate: + if item_code not in self.average_buying_rate: args.update( { "voucher_type": row.parenttype, @@ -748,7 +747,7 @@ class GrossProfitGenerator(object): conditions += " and (is_return = 0 or (is_return=1 and return_against is null))" if self.filters.item_group: - conditions += " and {0}".format(get_item_group_condition(self.filters.item_group)) + conditions += f" and {get_item_group_condition(self.filters.item_group)}" if self.filters.sales_person: conditions += """ @@ -767,12 +766,10 @@ class GrossProfitGenerator(object): if self.filters.group_by == "Payment Term": payment_term_cols = """,if(`tabSales Invoice`.is_return = 1, - '{0}', - coalesce(schedule.payment_term, '{1}')) as payment_term, + '{}', + coalesce(schedule.payment_term, '{}')) as payment_term, schedule.invoice_portion, - schedule.payment_amount """.format( - _("Sales Return"), _("No Terms") - ) + schedule.payment_amount """.format(_("Sales Return"), _("No Terms")) payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and `tabSales Invoice`.is_return = 0 """ else: @@ -939,9 +936,7 @@ class GrossProfitGenerator(object): ) def get_bundle_item_details(self, item_code): - return frappe.db.get_value( - "Item", item_code, ["item_name", "description", "item_group", "brand"] - ) + return frappe.db.get_value("Item", item_code, ["item_name", "description", "item_group", "brand"]) def get_stock_ledger_entries(self, item_code, warehouse): if item_code and warehouse: @@ -965,7 +960,7 @@ class GrossProfitGenerator(object): & (sle.is_cancelled == 0) ) .orderby(sle.item_code) - .orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc) + .orderby(sle.warehouse, sle.posting_datetime, sle.creation, order=Order.desc) .run(as_dict=True) ) diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 82fe1a0ba12..741ea46a516 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -1,7 +1,7 @@ import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, flt, nowdate +from frappe.utils import flt, nowdate from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -86,11 +86,14 @@ class TestGrossProfit(FrappeTestCase): self.customer = customer.name def create_sales_invoice( - self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False ): """ Helper function to populate default values in sales invoice """ + if posting_date is None: + posting_date = nowdate() + sinv = create_sales_invoice( qty=qty, rate=rate, @@ -115,11 +118,14 @@ class TestGrossProfit(FrappeTestCase): return sinv def create_delivery_note( - self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + self, item=None, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False ): """ Helper function to populate default values in Delivery Note """ + if posting_date is None: + posting_date = nowdate() + dnote = create_delivery_note( company=self.company, customer=self.customer, @@ -460,3 +466,95 @@ class TestGrossProfit(FrappeTestCase): } gp_entry = [x for x in data if x.parent_invoice == sinv.name] self.assertDictContainsSubset(expected_entry, gp_entry[0]) + + def test_different_rates_in_si_and_dn(self): + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + """ + Test gp calculation when invoice and delivery note differ in qty and aren't connected + SO -- INV + | + DN + """ + se = make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=3, + basic_rate=700, + do_not_submit=True, + ) + item = se.items[0] + se.append( + "items", + { + "item_code": item.item_code, + "s_warehouse": item.s_warehouse, + "t_warehouse": item.t_warehouse, + "qty": 10, + "basic_rate": 700, + "conversion_factor": item.conversion_factor or 1.0, + "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "cost_center": item.cost_center, + "expense_account": item.expense_account, + }, + ) + se = se.save().submit() + + so = make_sales_order( + customer=self.customer, + company=self.company, + warehouse=self.warehouse, + item=self.item, + rate=800, + qty=10, + do_not_save=False, + do_not_submit=False, + ) + + from erpnext.selling.doctype.sales_order.sales_order import ( + make_delivery_note, + make_sales_invoice, + ) + + dn1 = make_delivery_note(so.name) + dn1.items[0].qty = 4 + dn1.items[0].rate = 800 + dn1.save().submit() + + dn2 = make_delivery_note(so.name) + dn2.items[0].qty = 6 + dn2.items[0].rate = 800 + dn2.save().submit() + + sinv = make_sales_invoice(so.name) + sinv.items[0].qty = 4 + sinv.items[0].rate = 800 + sinv.save().submit() + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + + columns, data = execute(filters=filters) + expected_entry = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": 4.0, + "avg._selling_rate": 800.0, + "valuation_rate": 700.0, + "selling_amount": 3200.0, + "buying_amount": 2800.0, + "gross_profit": 400.0, + "gross_profit_%": 12.5, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + self.assertDictContainsSubset(expected_entry, gp_entry[0]) diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js index 7908c07a0ad..6aa1943c453 100644 --- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js +++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js @@ -3,7 +3,7 @@ /* eslint-disable */ frappe.query_reports["Inactive Sales Items"] = { - "filters": [ + filters: [ { fieldname: "territory", label: __("Territory"), @@ -15,27 +15,27 @@ frappe.query_reports["Inactive Sales Items"] = { fieldname: "item", label: __("Item"), fieldtype: "Link", - options: "Item" + options: "Item", }, { fieldname: "item_group", label: __("Item Group"), fieldtype: "Link", - options: "Item Group" + options: "Item Group", }, { fieldname: "based_on", label: __("Based On"), fieldtype: "Select", options: "Sales Order\nSales Invoice", - default: "Sales Order" + default: "Sales Order", }, { fieldname: "days", label: __("Days Since Last order"), fieldtype: "Select", options: [30, 60, 90], - default: 30 + default: 30, }, - ] + ], }; diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py index 230b18c293f..df3fc48f9e1 100644 --- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py +++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py @@ -92,7 +92,6 @@ def get_data(filters): def get_sales_details(filters): - data = [] item_details_map = {} date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date" @@ -116,7 +115,6 @@ def get_sales_details(filters): def get_territories(filters): - filter_dict = {} if filters.get("territory"): filter_dict.update({"name": filters["territory"]}) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js index b709ab9b57d..19ce9ffc607 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js @@ -2,59 +2,58 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Item-wise Purchase Register"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "reqd": 1, + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1, + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname": "item_code", - "label": __("Item"), - "fieldtype": "Link", - "options": "Item", + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", }, { - "fieldname":"supplier", - "label": __("Supplier"), - "fieldtype": "Link", - "options": "Supplier" + fieldname: "supplier", + label: __("Supplier"), + fieldtype: "Link", + options: "Supplier", }, { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"mode_of_payment", - "label": __("Mode of Payment"), - "fieldtype": "Link", - "options": "Mode of Payment" + fieldname: "mode_of_payment", + label: __("Mode of Payment"), + fieldtype: "Link", + options: "Mode of Payment", }, { - "label": __("Group By"), - "fieldname": "group_by", - "fieldtype": "Select", - "options": ["Supplier", "Item Group", "Item", "Invoice"] - } + label: __("Group By"), + fieldname: "group_by", + fieldtype: "Select", + options: ["Supplier", "Item Group", "Item", "Invoice"], + }, ], - "formatter": function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (data && data.bold) { value = value.bold(); - } return value; - } -} + }, +}; diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index ce1a62d0065..2f87b5be836 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -140,7 +140,6 @@ def _execute(filters=None, additional_table_columns=None): def get_columns(additional_table_columns, filters): - columns = [] if filters.get("group_by") != ("Item"): @@ -194,7 +193,12 @@ def get_columns(additional_table_columns, filters): "options": "Supplier", "width": 120, }, - {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 120}, + { + "label": _("Supplier Name"), + "fieldname": "supplier_name", + "fieldtype": "Data", + "width": 120, + }, ] ) @@ -309,7 +313,8 @@ def get_conditions(filters): def get_items(filters, additional_query_columns): conditions = get_conditions(filters) - + if additional_query_columns: + additional_query_columns = "," + ",".join(additional_query_columns) return frappe.db.sql( """ select @@ -318,20 +323,19 @@ def get_items(filters, additional_query_columns): `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `tabPurchase Invoice`.unrealized_profit_loss_account, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, - `tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group, + `tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` + ,`tabPurchase Invoice Item`.`item_group` as pi_item_group, `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, `tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`, - `tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {0} + `tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {} from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem` where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and `tabItem`.name = `tabPurchase Invoice Item`.`item_code` and - `tabPurchase Invoice`.docstatus = 1 {1} - """.format( - additional_query_columns, conditions - ), + `tabPurchase Invoice`.docstatus = 1 {} + """.format(additional_query_columns, conditions), filters, as_dict=1, ) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js index 39fb3ca5ee3..7ed2ebd89ab 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js @@ -2,71 +2,70 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Item-wise Sales Register"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "reqd": 1, + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1, + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname": "customer", - "label": __("Customer"), - "fieldtype": "Link", - "options": "Customer" + fieldname: "customer", + label: __("Customer"), + fieldtype: "Link", + options: "Customer", }, { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname": "mode_of_payment", - "label": __("Mode of Payment"), - "fieldtype": "Link", - "options": "Mode of Payment" + fieldname: "mode_of_payment", + label: __("Mode of Payment"), + fieldtype: "Link", + options: "Mode of Payment", }, { - "fieldname": "warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse" + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", }, { - "fieldname": "brand", - "label": __("Brand"), - "fieldtype": "Link", - "options": "Brand" + fieldname: "brand", + label: __("Brand"), + fieldtype: "Link", + options: "Brand", }, { - "fieldname": "item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "options": "Item Group" + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", }, { - "label": __("Group By"), - "fieldname": "group_by", - "fieldtype": "Select", - "options": ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"] - } + label: __("Group By"), + fieldname: "group_by", + fieldtype: "Select", + options: ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"], + }, ], - "formatter": function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (data && data.bold) { value = value.bold(); - } return value; - } -} + }, +}; diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 19bb449cd94..c7819731930 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -217,7 +217,12 @@ def get_columns(additional_table_columns, filters): "options": "Customer", "width": 120, }, - {"label": _("Customer Name"), "fieldname": "customer_name", "fieldtype": "Data", "width": 120}, + { + "label": _("Customer Name"), + "fieldname": "customer_name", + "fieldtype": "Data", + "width": 120, + }, ] ) @@ -350,7 +355,13 @@ def get_conditions(filters, additional_conditions=None): and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)""" if filters.get("warehouse"): - conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s""" + if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"): + lft, rgt = frappe.db.get_all( + "Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True + )[0] + conditions += f"and ifnull(`tabSales Invoice Item`.warehouse, '') in (select name from `tabWarehouse` where lft > {lft} and rgt < {rgt}) " + else: + conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s""" if filters.get("brand"): conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s""" @@ -359,9 +370,7 @@ def get_conditions(filters, additional_conditions=None): conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s""" if not filters.get("group_by"): - conditions += ( - "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc" - ) + conditions += "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc" else: conditions += get_group_by_conditions(filters, "Sales Invoice") @@ -370,18 +379,19 @@ def get_conditions(filters, additional_conditions=None): def get_group_by_conditions(filters, doctype): if filters.get("group_by") == "Invoice": - return "ORDER BY `tab{0} Item`.parent desc".format(doctype) + return f"ORDER BY `tab{doctype} Item`.parent desc" elif filters.get("group_by") == "Item": - return "ORDER BY `tab{0} Item`.`item_code`".format(doctype) + return f"ORDER BY `tab{doctype} Item`.`item_code`" elif filters.get("group_by") == "Item Group": - return "ORDER BY `tab{0} Item`.{1}".format(doctype, frappe.scrub(filters.get("group_by"))) + return "ORDER BY `tab{} Item`.{}".format(doctype, frappe.scrub(filters.get("group_by"))) elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"): - return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by"))) + return "ORDER BY `tab{}`.{}".format(doctype, frappe.scrub(filters.get("group_by"))) def get_items(filters, additional_query_columns, additional_conditions=None): conditions = get_conditions(filters, additional_conditions) - + if additional_query_columns: + additional_query_columns = "," + ",".join(additional_query_columns) return frappe.db.sql( """ select @@ -401,14 +411,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None): `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail, - `tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0} + `tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {} from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem` where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and `tabItem`.name = `tabSales Invoice Item`.`item_code` and - `tabSales Invoice`.docstatus = 1 {1} - """.format( - additional_query_columns, conditions - ), + `tabSales Invoice`.docstatus = 1 {} + """.format(additional_query_columns, conditions), filters, as_dict=1, ) # nosec @@ -438,20 +446,15 @@ def get_delivery_notes_against_sales_order(item_list): def get_grand_total(filters, doctype): - return frappe.db.sql( - """ SELECT - SUM(`tab{0}`.base_grand_total) - FROM `tab{0}` - WHERE `tab{0}`.docstatus = 1 + f""" SELECT + SUM(`tab{doctype}`.base_grand_total) + FROM `tab{doctype}` + WHERE `tab{doctype}`.docstatus = 1 and posting_date between %s and %s - """.format( - doctype - ), + """, (filters.get("from_date"), filters.get("to_date")), - )[0][ - 0 - ] # nosec + )[0][0] # nosec def get_tax_accounts( @@ -470,9 +473,7 @@ def get_tax_accounts( add_deduct_tax = "charge_type" tax_amount_precision = ( - get_field_precision( - frappe.get_meta(tax_doctype).get_field("tax_amount"), currency=company_currency - ) + get_field_precision(frappe.get_meta(tax_doctype).get_field("tax_amount"), currency=company_currency) or 2 ) @@ -482,11 +483,13 @@ def get_tax_accounts( conditions = "" if doctype == "Purchase Invoice": - conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0" + conditions = ( + " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0" + ) add_deduct_tax = "add_deduct_tax" tax_details = frappe.db.sql( - """ + f""" select name, parent, description, item_wise_tax_detail, account_head, charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount @@ -497,11 +500,9 @@ def get_tax_accounts( and parent in (%s) %s order by description - """.format( - add_deduct_tax=add_deduct_tax - ) + """ % (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions), - tuple([doctype] + list(invoice_item_row)), + tuple([doctype, *list(invoice_item_row)]), ) account_doctype = frappe.qb.DocType("Account") @@ -509,13 +510,13 @@ def get_tax_accounts( query = ( frappe.qb.from_(account_doctype) .select(account_doctype.name) - .where((account_doctype.account_type == "Tax")) + .where(account_doctype.account_type == "Tax") ) tax_accounts = query.run() for ( - name, + _name, parent, description, item_wise_tax_detail, @@ -576,7 +577,9 @@ def get_tax_accounts( itemised_tax.setdefault(d.name, {})[description] = frappe._dict( { "tax_rate": "NA", - "tax_amount": flt((tax_amount * d.base_net_amount) / d.base_net_total, tax_amount_precision), + "tax_amount": flt( + (tax_amount * d.base_net_amount) / d.base_net_total, tax_amount_precision + ), } ) diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.js b/erpnext/accounts/report/payment_ledger/payment_ledger.js index a5a4108f1df..e0ea7522b12 100644 --- a/erpnext/accounts/report/payment_ledger/payment_ledger.js +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.js @@ -5,92 +5,89 @@ function get_filters() { let filters = [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"period_start_date", - "label": __("Start Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + fieldname: "period_start_date", + label: __("Start Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - "fieldname":"period_end_date", - "label": __("End Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.get_today() + fieldname: "period_end_date", + label: __("End Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.get_today(), }, { - "fieldname":"account", - "label": __("Account"), - "fieldtype": "MultiSelectList", - "options": "Account", - get_data: function(txt) { - return frappe.db.get_link_options('Account', txt, { - company: frappe.query_report.get_filter_value("company") + fieldname: "account", + label: __("Account"), + fieldtype: "MultiSelectList", + options: "Account", + get_data: function (txt) { + return frappe.db.get_link_options("Account", txt, { + company: frappe.query_report.get_filter_value("company"), }); - } + }, }, { - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - "default": "", - on_change: function() { - frappe.query_report.set_filter_value('party', ""); - } + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Link", + options: "Party Type", + default: "", + on_change: function () { + frappe.query_report.set_filter_value("party", ""); + }, }, { - "fieldname":"party", - "label": __("Party"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { + fieldname: "party", + label: __("Party"), + fieldtype: "MultiSelectList", + get_data: function (txt) { if (!frappe.query_report.filters) return; - let party_type = frappe.query_report.get_filter_value('party_type'); + let party_type = frappe.query_report.get_filter_value("party_type"); if (!party_type) return; return frappe.db.get_link_options(party_type, txt); }, }, { - "fieldname":"voucher_no", - "label": __("Voucher No"), - "fieldtype": "Data", - "width": 100, + fieldname: "voucher_no", + label: __("Voucher No"), + fieldtype: "Data", + width: 100, }, { - "fieldname":"against_voucher_no", - "label": __("Against Voucher No"), - "fieldtype": "Data", - "width": 100, + fieldname: "against_voucher_no", + label: __("Against Voucher No"), + fieldtype: "Data", + width: 100, }, { - "fieldname":"include_account_currency", - "label": __("Include Account Currency"), - "fieldtype": "Check", - "width": 100, + fieldname: "include_account_currency", + label: __("Include Account Currency"), + fieldtype: "Check", + width: 100, }, { - "fieldname":"group_party", - "label": __("Group by Party"), - "fieldtype": "Check", - "width": 100, + fieldname: "group_party", + label: __("Group by Party"), + fieldtype: "Check", + width: 100, }, - - - - ] + ]; return filters; } frappe.query_reports["Payment Ledger"] = { - "filters": get_filters() + filters: get_filters(), }; diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.py b/erpnext/accounts/report/payment_ledger/payment_ledger.py index 8875d2743fe..9852c6e7ab9 100644 --- a/erpnext/accounts/report/payment_ledger/payment_ledger.py +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.py @@ -8,7 +8,7 @@ from frappe import _, qb from frappe.query_builder import Criterion -class PaymentLedger(object): +class PaymentLedger: def __init__(self, filters=None): self.filters = filters self.columns, self.data = [], [] diff --git a/erpnext/accounts/report/payment_ledger/test_payment_ledger.py b/erpnext/accounts/report/payment_ledger/test_payment_ledger.py index 5ae9b87cde9..c982f3166e7 100644 --- a/erpnext/accounts/report/payment_ledger/test_payment_ledger.py +++ b/erpnext/accounts/report/payment_ledger/test_payment_ledger.py @@ -1,5 +1,3 @@ -import unittest - import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase @@ -57,7 +55,7 @@ class TestPaymentLedger(FrappeTestCase): income_account=self.income_account, warehouse=self.warehouse, ) - pe = get_payment_entry(sinv.doctype, sinv.name).save().submit() + get_payment_entry(sinv.doctype, sinv.name).save().submit() filters = frappe._dict({"company": self.company}) columns, data = execute(filters=filters) diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.js b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.js index 5ec2c9880c3..528246cd370 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.js +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.js @@ -2,14 +2,14 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Payment Period Based On Invoice Date"] = { - "filters": [ + filters: [ { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", reqd: 1, - default: frappe.defaults.get_user_default("Company") + default: frappe.defaults.get_user_default("Company"), }, { fieldname: "from_date", @@ -18,41 +18,41 @@ frappe.query_reports["Payment Period Based On Invoice Date"] = { default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.datetime.get_today() + default: frappe.datetime.get_today(), }, { - fieldname:"payment_type", + fieldname: "payment_type", label: __("Payment Type"), fieldtype: "Select", options: __("Incoming") + "\n" + __("Outgoing"), - default: __("Incoming") + default: __("Incoming"), }, { - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Link", - "options": "DocType", - "get_query": function() { + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Link", + options: "DocType", + get_query: function () { return { - filters: {"name": ["in", ["Customer", "Supplier"]]} - } - } + filters: { name: ["in", ["Customer", "Supplier"]] }, + }; + }, }, { - "fieldname":"party", - "label": __("Party"), - "fieldtype": "Dynamic Link", - "get_options": function() { - var party_type = frappe.query_report.get_filter_value('party_type'); - var party = frappe.query_report.get_filter_value('party'); - if(party && !party_type) { + fieldname: "party", + label: __("Party"), + fieldtype: "Dynamic Link", + get_options: function () { + var party_type = frappe.query_report.get_filter_value("party_type"); + var party = frappe.query_report.get_filter_value("party"); + if (party && !party_type) { frappe.throw(__("Please select Party Type first")); } return party_type; - } + }, }, - ] -} + ], +}; diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index 3f178f4715c..834eb5f519c 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -163,10 +163,8 @@ def get_entries(filters): """select voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher from `tabGL Entry` - where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') {0} - """.format( - get_conditions(filters) - ), + where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {} + """.format(get_conditions(filters)), filters, as_dict=1, ) @@ -175,7 +173,7 @@ def get_entries(filters): def get_invoice_posting_date_map(filters): invoice_details = {} dt = "Sales Invoice" if filters.get("payment_type") == _("Incoming") else "Purchase Invoice" - for t in frappe.db.sql("select name, posting_date, due_date from `tab{0}`".format(dt), as_dict=1): + for t in frappe.db.sql(f"select name, posting_date, due_date from `tab{dt}`", as_dict=1): invoice_details[t.name] = t return invoice_details diff --git a/erpnext/accounts/report/pos_register/pos_register.js b/erpnext/accounts/report/pos_register/pos_register.js index b8d48d92de0..1200d3e79f2 100644 --- a/erpnext/accounts/report/pos_register/pos_register.js +++ b/erpnext/accounts/report/pos_register/pos_register.js @@ -3,74 +3,73 @@ /* eslint-disable */ frappe.query_reports["POS Register"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "reqd": 1, - "width": "60px" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, + width: "60px", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1, - "width": "60px" + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + width: "60px", }, { - "fieldname":"pos_profile", - "label": __("POS Profile"), - "fieldtype": "Link", - "options": "POS Profile" + fieldname: "pos_profile", + label: __("POS Profile"), + fieldtype: "Link", + options: "POS Profile", }, { - "fieldname":"cashier", - "label": __("Cashier"), - "fieldtype": "Link", - "options": "User" + fieldname: "cashier", + label: __("Cashier"), + fieldtype: "Link", + options: "User", }, { - "fieldname":"customer", - "label": __("Customer"), - "fieldtype": "Link", - "options": "Customer" + fieldname: "customer", + label: __("Customer"), + fieldtype: "Link", + options: "Customer", }, { - "fieldname":"mode_of_payment", - "label": __("Payment Method"), - "fieldtype": "Link", - "options": "Mode of Payment" + fieldname: "mode_of_payment", + label: __("Payment Method"), + fieldtype: "Link", + options: "Mode of Payment", }, { - "fieldname":"group_by", - "label": __("Group by"), - "fieldtype": "Select", - "options": ["", "POS Profile", "Cashier", "Payment Method", "Customer"], - "default": "POS Profile" + fieldname: "group_by", + label: __("Group by"), + fieldtype: "Select", + options: ["", "POS Profile", "Cashier", "Payment Method", "Customer"], + default: "POS Profile", }, { - "fieldname":"is_return", - "label": __("Is Return"), - "fieldtype": "Check" + fieldname: "is_return", + label: __("Is Return"), + fieldtype: "Check", }, ], - "formatter": function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (data && data.bold) { value = value.bold(); - } return value; - } + }, }; diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py index 488bb9957c9..2248f6eca65 100644 --- a/erpnext/accounts/report/pos_register/pos_register.py +++ b/erpnext/accounts/report/pos_register/pos_register.py @@ -50,19 +50,22 @@ def get_pos_entries(filters, group_by_field): order_by = "p.posting_date" select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", "" if group_by_field == "mode_of_payment": - select_mop_field = ", sip.mode_of_payment, sip.base_amount - IF(sip.type='Cash', p.change_amount, 0) as paid_amount" + select_mop_field = ( + ", sip.mode_of_payment, sip.base_amount - IF(sip.type='Cash', p.change_amount, 0) as paid_amount" + ) from_sales_invoice_payment = ", `tabSales Invoice Payment` sip" group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount - IF(sip.type='Cash', p.change_amount, 0), 0) != 0 AND" order_by += ", sip.mode_of_payment" elif group_by_field: - order_by += ", p.{}".format(group_by_field) + order_by += f", p.{group_by_field}" select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount " + # nosemgrep return frappe.db.sql( - """ + f""" SELECT - p.posting_date, p.name as pos_invoice, p.pos_profile, + p.posting_date, p.name as pos_invoice, p.pos_profile, p.company, p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field} FROM `tabPOS Invoice` p {from_sales_invoice_payment} @@ -72,13 +75,7 @@ def get_pos_entries(filters, group_by_field): {conditions} ORDER BY {order_by} - """.format( - select_mop_field=select_mop_field, - from_sales_invoice_payment=from_sales_invoice_payment, - group_by_mop_condition=group_by_mop_condition, - conditions=conditions, - order_by=order_by, - ), + """, filters, as_dict=1, ) @@ -131,9 +128,7 @@ def validate_filters(filters): def get_conditions(filters): - conditions = ( - "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s" - ) + conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s" if filters.get("pos_profile"): conditions += " AND pos_profile = %(pos_profile)s" @@ -207,14 +202,14 @@ def get_columns(filters): "label": _("Grand Total"), "fieldname": "grand_total", "fieldtype": "Currency", - "options": "company:currency", + "options": "Company:company:default_currency", "width": 120, }, { "label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", - "options": "company:currency", + "options": "Company:company:default_currency", "width": 120, }, { @@ -224,6 +219,13 @@ def get_columns(filters): "width": 150, }, {"label": _("Is Return"), "fieldname": "is_return", "fieldtype": "Data", "width": 80}, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120, + }, ] return columns 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 e794f270c2b..521625bbcd4 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 @@ -1,19 +1,35 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +frappe.require("assets/erpnext/js/financial_statements.js", function () { + frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financial_statements); -frappe.require("assets/erpnext/js/financial_statements.js", function() { - frappe.query_reports["Profit and Loss Statement"] = $.extend({}, - erpnext.financial_statements); + erpnext.utils.add_dimensions("Profit and Loss Statement", 10); - erpnext.utils.add_dimensions('Profit and Loss Statement', 10); + frappe.query_reports["Profit and Loss Statement"]["filters"].push({ + fieldname: "selected_view", + label: __("Select View"), + fieldtype: "Select", + options: [ + { value: "Report", label: __("Report View") }, + { value: "Growth", label: __("Growth View") }, + { value: "Margin", label: __("Margin View") }, + ], + default: "Report", + reqd: 1, + }); - frappe.query_reports["Profit and Loss Statement"]["filters"].push( - { - "fieldname": "include_default_book_entries", - "label": __("Include Default Book Entries"), - "fieldtype": "Check", - "default": 1 - } - ); + frappe.query_reports["Profit and Loss Statement"]["filters"].push({ + fieldname: "include_default_book_entries", + label: __("Include Default Book Entries"), + fieldtype: "Check", + default: 1, + }); +}); + +frappe.query_reports["Profit and Loss Statement"]["filters"].push({ + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, }); diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 66353358a06..4571c4b26a0 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -57,20 +57,18 @@ def execute(filters=None): if net_profit_loss: data.append(net_profit_loss) - columns = get_columns( - filters.periodicity, period_list, filters.accumulated_values, filters.company - ) + columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) chart = get_chart_data(filters, columns, income, expense, net_profit_loss) currency = filters.presentation_currency or frappe.get_cached_value( "Company", filters.company, "default_currency" ) - report_summary = get_report_summary( + report_summary, primitive_summary = get_report_summary( period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters ) - return columns, data, None, chart, report_summary + return columns, data, None, chart, report_summary, primitive_summary def get_report_summary( @@ -112,7 +110,7 @@ def get_report_summary( "datatype": "Currency", "currency": currency, }, - ] + ], net_profit def get_net_profit_loss(income, expense, period_list, company, currency=None, consolidated=False): diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js index b3b32c08c4c..816adfaf916 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -1,90 +1,92 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.require("assets/erpnext/js/financial_statements.js", function() { +frappe.require("assets/erpnext/js/financial_statements.js", function () { frappe.query_reports["Profitability Analysis"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname": "based_on", - "label": __("Based On"), - "fieldtype": "Select", - "options": ["Cost Center", "Project", "Accounting Dimension"], - "default": "Cost Center", - "reqd": 1, - "on_change": function(query_report){ + fieldname: "based_on", + label: __("Based On"), + fieldtype: "Select", + options: ["Cost Center", "Project", "Accounting Dimension"], + default: "Cost Center", + reqd: 1, + on_change: function (query_report) { let based_on = query_report.get_values().based_on; - if(based_on!='Accounting Dimension'){ + if (based_on != "Accounting Dimension") { frappe.query_report.set_filter_value({ - accounting_dimension: '' + accounting_dimension: "", }); } - } + }, }, { - "fieldname": "accounting_dimension", - "label": __("Accounting Dimension"), - "fieldtype": "Link", - "options": "Accounting Dimension", + fieldname: "accounting_dimension", + label: __("Accounting Dimension"), + fieldtype: "Link", + options: "Accounting Dimension", }, { - "fieldname": "fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1, - "on_change": function(query_report) { + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "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; if (!fiscal_year) { return; } - frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); frappe.query_report.set_filter_value({ from_date: fy.year_start_date, - to_date: fy.year_end_date + to_date: fy.year_end_date, }); }); - } + }, }, { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], }, { - "fieldname": "show_zero_values", - "label": __("Show zero values"), - "fieldtype": "Check" - } + fieldname: "show_zero_values", + label: __("Show zero values"), + fieldtype: "Check", + }, ], - "formatter": function(value, row, column, data, default_formatter) { - if (column.fieldname=="account") { + formatter: function (value, row, column, data, default_formatter) { + if (column.fieldname == "account") { value = data.account_name; column.link_onclick = - "frappe.query_reports['Profitability Analysis'].open_profit_and_loss_statement(" + JSON.stringify(data) + ")"; + "frappe.query_reports['Profitability Analysis'].open_profit_and_loss_statement(" + + JSON.stringify(data) + + ")"; column.is_tree = true; } value = default_formatter(value, row, column, data); - if (!data.parent_account && data.based_on != 'project') { + if (!data.parent_account && data.based_on != "project") { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); if (data.warn_if_negative && data[column.fieldname] < 0) { @@ -96,31 +98,26 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { return value; }, - "open_profit_and_loss_statement": function(data) { + open_profit_and_loss_statement: function (data) { if (!data.account) return; frappe.route_options = { - "company": frappe.query_report.get_filter_value('company'), - "from_fiscal_year": data.fiscal_year, - "to_fiscal_year": data.fiscal_year + company: frappe.query_report.get_filter_value("company"), + from_fiscal_year: data.fiscal_year, + to_fiscal_year: data.fiscal_year, }; - if(data.based_on == 'cost_center'){ - frappe.route_options["cost_center"] = data.account + if (data.based_on == "Cost Center") { + frappe.route_options["cost_center"] = data.account; } else { - frappe.route_options["project"] = data.account + frappe.route_options["project"] = data.account; } frappe.set_route("query-report", "Profit and Loss Statement"); }, - "tree": true, - "name_field": "account", - "parent_field": "parent_account", - "initial_depth": 3 - } - - erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Profitability Analysis"].filters[1].options.push(dimension["document_type"]); - }); - + tree: true, + name_field: "account", + parent_field: "parent_account", + initial_depth: 3, + }; }); diff --git a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js index feab96f2652..e3f90f29982 100644 --- a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js +++ b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js @@ -1,8 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.require("assets/erpnext/js/purchase_trends_filters.js", function() { +frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () { frappe.query_reports["Purchase Invoice Trends"] = { - filters: erpnext.get_purchase_trends_filters() - } + filters: erpnext.get_purchase_trends_filters(), + }; }); diff --git a/erpnext/accounts/report/purchase_register/purchase_register.js b/erpnext/accounts/report/purchase_register/purchase_register.js index aaf76c42997..6366f368552 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.js +++ b/erpnext/accounts/report/purchase_register/purchase_register.js @@ -2,58 +2,64 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Purchase Register"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "width": "80" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + width: "80", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, { - "fieldname":"supplier", - "label": __("Supplier"), - "fieldtype": "Link", - "options": "Supplier" + fieldname: "supplier", + label: __("Supplier"), + fieldtype: "Link", + options: "Supplier", }, { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"mode_of_payment", - "label": __("Mode of Payment"), - "fieldtype": "Link", - "options": "Mode of Payment" + fieldname: "mode_of_payment", + label: __("Mode of Payment"), + fieldtype: "Link", + options: "Mode of Payment", }, { - "fieldname":"cost_center", - "label": __("Cost Center"), - "fieldtype": "Link", - "options": "Cost Center" + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "Link", + options: "Cost Center", }, { - "fieldname":"warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse" + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", }, { - "fieldname":"item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "options": "Item Group" - } - ] -} + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", + }, + { + fieldname: "include_payments", + label: __("Show Ledger View"), + fieldtype: "Check", + default: 0, + }, + ], +}; -erpnext.utils.add_dimensions('Purchase Register', 7); +erpnext.utils.add_dimensions("Purchase Register", 7); diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 69827aca694..92d91a4cc46 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -4,13 +4,22 @@ import frappe from frappe import _, msgprint -from frappe.utils import flt +from frappe.query_builder.custom import ConstantColumn +from frappe.utils import flt, getdate +from pypika import Order -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( - get_accounting_dimensions, - get_dimension_with_children, +from erpnext.accounts.party import get_party_account +from erpnext.accounts.report.utils import ( + apply_common_conditions, + get_advance_taxes_and_charges, + get_journal_entries, + get_opening_row, + get_party_details, + get_payment_entries, + get_query_columns, + get_taxes_query, + get_values_for_columns, ) -from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns def execute(filters=None): @@ -21,9 +30,15 @@ def _execute(filters=None, additional_table_columns=None): if not filters: filters = {} + include_payments = filters.get("include_payments") + if filters.get("include_payments") and not filters.get("supplier"): + frappe.throw(_("Please select a supplier for fetching payments.")) invoice_list = get_invoices(filters, get_query_columns(additional_table_columns)) + if filters.get("include_payments"): + invoice_list += get_payments(filters) + columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns( - invoice_list, additional_table_columns + invoice_list, additional_table_columns, include_payments ) if not invoice_list: @@ -33,14 +48,28 @@ def _execute(filters=None, additional_table_columns=None): invoice_expense_map = get_invoice_expense_map(invoice_list) internal_invoice_map = get_internal_invoice_map(invoice_list) invoice_expense_map, invoice_tax_map = get_invoice_tax_map( - invoice_list, invoice_expense_map, expense_accounts + invoice_list, invoice_expense_map, expense_accounts, include_payments ) invoice_po_pr_map = get_invoice_po_pr_map(invoice_list) suppliers = list(set(d.supplier for d in invoice_list)) - supplier_details = get_supplier_details(suppliers) + supplier_details = get_party_details("Supplier", suppliers) company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") + res = [] + if include_payments: + opening_row = get_opening_row( + "Supplier", filters.supplier, getdate(filters.from_date), filters.company + )[0] + res.append( + { + "payable_account": opening_row.account, + "debit": flt(opening_row.debit), + "credit": flt(opening_row.credit), + "balance": flt(opening_row.balance), + } + ) + data = [] for inv in invoice_list: # invoice details @@ -48,24 +77,25 @@ def _execute(filters=None, additional_table_columns=None): purchase_receipt = list(set(invoice_po_pr_map.get(inv.name, {}).get("purchase_receipt", []))) project = list(set(invoice_po_pr_map.get(inv.name, {}).get("project", []))) - row = [ - inv.name, - inv.posting_date, - inv.supplier, - inv.supplier_name, - *get_values_for_columns(additional_table_columns, inv).values(), - supplier_details.get(inv.supplier), # supplier_group - inv.tax_id, - inv.credit_to, - inv.mode_of_payment, - ", ".join(project), - inv.bill_no, - inv.bill_date, - inv.remarks, - ", ".join(purchase_order), - ", ".join(purchase_receipt), - company_currency, - ] + row = { + "voucher_type": inv.doctype, + "voucher_no": inv.name, + "posting_date": inv.posting_date, + "supplier_id": inv.supplier, + "supplier_name": inv.supplier_name, + **get_values_for_columns(additional_table_columns, inv), + "supplier_group": supplier_details.get(inv.supplier).get("supplier_group"), # supplier_group + "tax_id": supplier_details.get(inv.supplier).get("tax_id"), + "payable_account": inv.credit_to, + "mode_of_payment": inv.mode_of_payment, + "project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project, + "bill_no": inv.bill_no, + "bill_date": inv.bill_date, + "remarks": inv.remarks, + "purchase_order": ", ".join(purchase_order), + "purchase_receipt": ", ".join(purchase_receipt), + "currency": company_currency, + } # map expense values base_net_total = 0 @@ -75,14 +105,16 @@ def _execute(filters=None, additional_table_columns=None): else: expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc)) base_net_total += expense_amount - row.append(expense_amount) + row.update({frappe.scrub(expense_acc): expense_amount}) # Add amount in unrealized account for account in unrealized_profit_loss_accounts: - row.append(flt(internal_invoice_map.get((inv.name, account)))) + row.update( + {frappe.scrub(account + "_unrealized"): flt(internal_invoice_map.get((inv.name, account)))} + ) # net total - row.append(base_net_total or inv.base_net_total) + row.update({"net_total": base_net_total or inv.base_net_total}) # tax account total_tax = 0 @@ -90,45 +122,190 @@ def _execute(filters=None, additional_table_columns=None): if tax_acc not in expense_accounts: tax_amount = flt(invoice_tax_map.get(inv.name, {}).get(tax_acc)) total_tax += tax_amount - row.append(tax_amount) + row.update({frappe.scrub(tax_acc): tax_amount}) # total tax, grand total, rounded total & outstanding amount - row += [total_tax, inv.base_grand_total, flt(inv.base_grand_total, 0), inv.outstanding_amount] + row.update( + { + "total_tax": total_tax, + "grand_total": inv.base_grand_total, + "rounded_total": inv.base_rounded_total, + "outstanding_amount": inv.outstanding_amount, + } + ) + + if inv.doctype == "Purchase Invoice": + row.update({"debit": inv.base_grand_total, "credit": 0.0}) + else: + row.update({"debit": 0.0, "credit": inv.base_grand_total}) data.append(row) - return columns, data + res += sorted(data, key=lambda x: x["posting_date"]) + + if include_payments: + running_balance = flt(opening_row.balance) + for row in range(1, len(res)): + running_balance += res[row]["debit"] - res[row]["credit"] + res[row].update({"balance": running_balance}) + + return columns, res, None, None, None, include_payments -def get_columns(invoice_list, additional_table_columns): +def get_columns(invoice_list, additional_table_columns, include_payments=False): """return columns based on filters""" columns = [ - _("Invoice") + ":Link/Purchase Invoice:120", - _("Posting Date") + ":Date:80", - _("Supplier Id") + "::120", - _("Supplier Name") + "::120", + { + "label": _("Voucher Type"), + "fieldname": "voucher_type", + "width": 120, + }, + { + "label": _("Voucher"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 120, + }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 80}, + { + "label": _("Supplier"), + "fieldname": "supplier_id", + "fieldtype": "Link", + "options": "Supplier", + "width": 120, + }, + {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 120}, ] - if additional_table_columns: + if additional_table_columns and not include_payments: columns += additional_table_columns - columns += [ - _("Supplier Group") + ":Link/Supplier Group:120", - _("Tax Id") + "::80", - _("Payable Account") + ":Link/Account:120", - _("Mode of Payment") + ":Link/Mode of Payment:80", - _("Project") + ":Link/Project:80", - _("Bill No") + "::120", - _("Bill Date") + ":Date:80", - _("Remarks") + "::150", - _("Purchase Order") + ":Link/Purchase Order:100", - _("Purchase Receipt") + ":Link/Purchase Receipt:100", - {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80}, - ] + if not include_payments: + columns += [ + { + "label": _("Supplier Group"), + "fieldname": "supplier_group", + "fieldtype": "Link", + "options": "Supplier Group", + "width": 120, + }, + {"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 80}, + { + "label": _("Payable Account"), + "fieldname": "payable_account", + "fieldtype": "Link", + "options": "Account", + "width": 100, + }, + { + "label": _("Mode Of Payment"), + "fieldname": "mode_of_payment", + "fieldtype": "Data", + "width": 120, + }, + { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 80, + }, + {"label": _("Bill No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 120}, + {"label": _("Bill Date"), "fieldname": "bill_date", "fieldtype": "Date", "width": 80}, + { + "label": _("Purchase Order"), + "fieldname": "purchase_order", + "fieldtype": "Link", + "options": "Purchase Order", + "width": 100, + }, + { + "label": _("Purchase Receipt"), + "fieldname": "purchase_receipt", + "fieldtype": "Link", + "options": "Purchase Receipt", + "width": 100, + }, + {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80}, + ] + else: + columns += [ + { + "fieldname": "payable_account", + "label": _("Payable Account"), + "fieldtype": "Link", + "options": "Account", + "width": 120, + }, + {"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 120}, + {"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 120}, + {"fieldname": "balance", "label": _("Balance"), "fieldtype": "Currency", "width": 120}, + ] + account_columns, accounts = get_account_columns(invoice_list, include_payments) + + columns = ( + columns + + account_columns[0] + + account_columns[1] + + [ + { + "label": _("Net Total"), + "fieldname": "net_total", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ] + + account_columns[2] + + [ + { + "label": _("Total Tax"), + "fieldname": "total_tax", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ] + ) + + if not include_payments: + columns += [ + { + "label": _("Grand Total"), + "fieldname": "grand_total", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Rounded Total"), + "fieldname": "rounded_total", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Outstanding Amount"), + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + ] + columns += [{"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Data", "width": 120}] + return columns, accounts[0], accounts[2], accounts[1] + + +def get_account_columns(invoice_list, include_payments): expense_accounts = [] tax_accounts = [] unrealized_profit_loss_accounts = [] + expense_columns = [] + tax_columns = [] + unrealized_profit_loss_account_columns = [] + if invoice_list: expense_accounts = frappe.db.sql_list( """select distinct expense_account @@ -139,15 +316,14 @@ def get_columns(invoice_list, additional_table_columns): tuple([inv.name for inv in invoice_list]), ) - tax_accounts = frappe.db.sql_list( - """select distinct account_head - from `tabPurchase Taxes and Charges` where parenttype = 'Purchase Invoice' - and docstatus = 1 and (account_head is not null and account_head != '') - and category in ('Total', 'Valuation and Total') - and parent in (%s) order by account_head""" - % ", ".join(["%s"] * len(invoice_list)), - tuple(inv.name for inv in invoice_list), - ) + purchase_taxes_query = get_taxes_query(invoice_list, "Purchase Taxes and Charges", "Purchase Invoice") + purchase_tax_accounts = purchase_taxes_query.run(as_dict=True, pluck="account_head") + tax_accounts = purchase_tax_accounts + + if include_payments: + advance_taxes_query = get_taxes_query(invoice_list, "Advance Taxes and Charges", "Payment Entry") + advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head") + tax_accounts = set(tax_accounts + advance_tax_accounts) unrealized_profit_loss_accounts = frappe.db.sql_list( """SELECT distinct unrealized_profit_loss_account @@ -158,108 +334,109 @@ def get_columns(invoice_list, additional_table_columns): tuple(inv.name for inv in invoice_list), ) - expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts] - unrealized_profit_loss_account_columns = [ - (account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts - ] - tax_columns = [ - (account + ":Currency/currency:120") - for account in tax_accounts - if account not in expense_accounts - ] + for account in expense_accounts: + expense_columns.append( + { + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) - columns = ( - columns - + expense_columns - + unrealized_profit_loss_account_columns - + [_("Net Total") + ":Currency/currency:120"] - + tax_columns - + [ - _("Total Tax") + ":Currency/currency:120", - _("Grand Total") + ":Currency/currency:120", - _("Rounded Total") + ":Currency/currency:120", - _("Outstanding Amount") + ":Currency/currency:120", - ] - ) + for account in tax_accounts: + if account not in expense_accounts: + tax_columns.append( + { + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) - return columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts + for account in unrealized_profit_loss_accounts: + unrealized_profit_loss_account_columns.append( + { + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) + columns = [expense_columns, unrealized_profit_loss_account_columns, tax_columns] + accounts = [expense_accounts, unrealized_profit_loss_accounts, tax_accounts] -def get_conditions(filters): - conditions = "" - - if filters.get("company"): - conditions += " and company=%(company)s" - if filters.get("supplier"): - conditions += " and supplier = %(supplier)s" - - if filters.get("from_date"): - conditions += " and posting_date>=%(from_date)s" - if filters.get("to_date"): - conditions += " and posting_date<=%(to_date)s" - - if filters.get("mode_of_payment"): - conditions += " and ifnull(mode_of_payment, '') = %(mode_of_payment)s" - - if filters.get("cost_center"): - conditions += """ and exists(select name from `tabPurchase Invoice Item` - where parent=`tabPurchase Invoice`.name - and ifnull(`tabPurchase Invoice Item`.cost_center, '') = %(cost_center)s)""" - - if filters.get("warehouse"): - conditions += """ and exists(select name from `tabPurchase Invoice Item` - where parent=`tabPurchase Invoice`.name - and ifnull(`tabPurchase Invoice Item`.warehouse, '') = %(warehouse)s)""" - - if filters.get("item_group"): - conditions += """ and exists(select name from `tabPurchase Invoice Item` - where parent=`tabPurchase Invoice`.name - and ifnull(`tabPurchase Invoice Item`.item_group, '') = %(item_group)s)""" - - accounting_dimensions = get_accounting_dimensions(as_list=False) - - if accounting_dimensions: - common_condition = """ - and exists(select name from `tabPurchase Invoice Item` - where parent=`tabPurchase Invoice`.name - """ - for dimension in accounting_dimensions: - if filters.get(dimension.fieldname): - if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): - filters[dimension.fieldname] = get_dimension_with_children( - dimension.document_type, filters.get(dimension.fieldname) - ) - - conditions += ( - common_condition - + "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname) - ) - else: - conditions += ( - common_condition - + "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname) - ) - - return conditions + return columns, accounts def get_invoices(filters, additional_query_columns): - conditions = get_conditions(filters) - return frappe.db.sql( - """ - select - name, posting_date, credit_to, supplier, supplier_name, tax_id, bill_no, bill_date, - remarks, base_net_total, base_grand_total, outstanding_amount, - mode_of_payment {0} - from `tabPurchase Invoice` - where docstatus = 1 {1} - order by posting_date desc, name desc""".format( - additional_query_columns, conditions - ), - filters, - as_dict=1, + pi = frappe.qb.DocType("Purchase Invoice") + query = ( + frappe.qb.from_(pi) + .select( + ConstantColumn("Purchase Invoice").as_("doctype"), + pi.name, + pi.posting_date, + pi.credit_to, + pi.supplier, + pi.supplier_name, + pi.tax_id, + pi.bill_no, + pi.bill_date, + pi.remarks, + pi.base_net_total, + pi.base_grand_total, + pi.base_rounded_total, + pi.outstanding_amount, + pi.mode_of_payment, + ) + .where(pi.docstatus == 1) + .orderby(pi.posting_date, pi.name, order=Order.desc) ) + if additional_query_columns: + for col in additional_query_columns: + query = query.select(col) + + if filters.get("supplier"): + query = query.where(pi.supplier == filters.supplier) + + query = get_conditions(filters, query, "Purchase Invoice") + + query = apply_common_conditions( + filters, query, doctype="Purchase Invoice", child_doctype="Purchase Invoice Item" + ) + + invoices = query.run(as_dict=True) + return invoices + + +def get_conditions(filters, query, doctype): + parent_doc = frappe.qb.DocType(doctype) + + if filters.get("mode_of_payment"): + query = query.where(parent_doc.mode_of_payment == filters.mode_of_payment) + + return query + + +def get_payments(filters): + args = frappe._dict( + account="credit_to", + account_fieldname="paid_to", + party="supplier", + party_name="supplier_name", + party_account=[get_party_account("Supplier", filters.supplier, filters.company)], + ) + payment_entries = get_payment_entries(filters, args) + journal_entries = get_journal_entries(filters, args) + return payment_entries + journal_entries + def get_invoice_expense_map(invoice_list): expense_details = frappe.db.sql( @@ -300,7 +477,7 @@ def get_internal_invoice_map(invoice_list): return internal_invoice_map -def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): +def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, include_payments=False): tax_details = frappe.db.sql( """ select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount) @@ -315,6 +492,9 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): as_dict=1, ) + if include_payments: + tax_details += get_advance_taxes_and_charges(invoice_list) + invoice_tax_map = {} for d in tax_details: if d.account_head in expense_accounts: @@ -362,9 +542,7 @@ def get_invoice_po_pr_map(invoice_list): invoice_po_pr_map.setdefault(d.parent, frappe._dict()).setdefault("purchase_receipt", pr_list) if d.project: - invoice_po_pr_map.setdefault(d.parent, frappe._dict()).setdefault("project", []).append( - d.project - ) + invoice_po_pr_map.setdefault(d.parent, frappe._dict()).setdefault("project", []).append(d.project) return invoice_po_pr_map @@ -382,17 +560,3 @@ def get_account_details(invoice_list): account_map[acc.name] = acc.parent_account return account_map - - -def get_supplier_details(suppliers): - supplier_details = {} - for supp in frappe.db.sql( - """select name, supplier_group from `tabSupplier` - where name in (%s)""" - % ", ".join(["%s"] * len(suppliers)), - tuple(suppliers), - as_dict=1, - ): - supplier_details.setdefault(supp.name, supp.supplier_group) - - return supplier_details diff --git a/erpnext/accounts/report/purchase_register/test_purchase_register.py b/erpnext/accounts/report/purchase_register/test_purchase_register.py new file mode 100644 index 00000000000..a7a5c07152b --- /dev/null +++ b/erpnext/accounts/report/purchase_register/test_purchase_register.py @@ -0,0 +1,126 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_months, today + +from erpnext.accounts.report.purchase_register.purchase_register import execute + + +class TestPurchaseRegister(FrappeTestCase): + def test_purchase_register(self): + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'") + frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'") + + filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today()) + + pi = make_purchase_invoice() + + report_results = execute(filters) + first_row = frappe._dict(report_results[1][0]) + self.assertEqual(first_row.voucher_type, "Purchase Invoice") + self.assertEqual(first_row.voucher_no, pi.name) + self.assertEqual(first_row.payable_account, "Creditors - _TC6") + self.assertEqual(first_row.net_total, 1000) + self.assertEqual(first_row.total_tax, 100) + self.assertEqual(first_row.grand_total, 1100) + + def test_purchase_register_ledger_view(self): + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'") + frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'") + + filters = frappe._dict( + company="_Test Company 6", + from_date=add_months(today(), -1), + to_date=today(), + include_payments=True, + supplier="_Test Supplier", + ) + + make_purchase_invoice() + pe = make_payment_entry() + + report_results = execute(filters) + first_row = frappe._dict(report_results[1][2]) + self.assertEqual(first_row.voucher_type, "Payment Entry") + self.assertEqual(first_row.voucher_no, pe.name) + self.assertEqual(first_row.payable_account, "Creditors - _TC6") + self.assertEqual(first_row.debit, 0) + self.assertEqual(first_row.credit, 600) + self.assertEqual(first_row.balance, 500) + + +def make_purchase_invoice(): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + create_account( + account_name="GST", + account_type="Tax", + parent_account="Duties and Taxes - _TC6", + company="_Test Company 6", + account_currency="INR", + ) + create_warehouse(warehouse_name="_Test Warehouse - _TC6", company="_Test Company 6") + create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company 6") + pi = create_purchase_invoice_with_taxes() + pi.submit() + return pi + + +def create_purchase_invoice_with_taxes(): + return frappe.get_doc( + { + "doctype": "Purchase Invoice", + "posting_date": today(), + "supplier": "_Test Supplier", + "company": "_Test Company 6", + "cost_center": "_Test Cost Center - _TC6", + "taxes_and_charges": "", + "currency": "INR", + "credit_to": "Creditors - _TC6", + "items": [ + { + "doctype": "Purchase Invoice Item", + "cost_center": "_Test Cost Center - _TC6", + "item_code": "_Test Item", + "qty": 1, + "rate": 1000, + "expense_account": "Stock Received But Not Billed - _TC6", + } + ], + "taxes": [ + { + "account_head": "GST - _TC6", + "cost_center": "_Test Cost Center - _TC6", + "add_deduct_tax": "Add", + "category": "Valuation and Total", + "charge_type": "Actual", + "description": "Shipping Charges", + "doctype": "Purchase Taxes and Charges", + "parentfield": "taxes", + "rate": 100, + "tax_amount": 100.0, + } + ], + } + ) + + +def make_payment_entry(): + frappe.set_user("Administrator") + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry + + return create_payment_entry( + company="_Test Company 6", + party_type="Supplier", + party="_Test Supplier", + payment_type="Pay", + paid_from="Cash - _TC6", + paid_to="Creditors - _TC6", + paid_amount=600, + save=1, + submit=1, + ) diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.js b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.js index 0ee3f577171..ad97f270dd3 100644 --- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.js +++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.js @@ -2,7 +2,5 @@ // For license information, please see license.txt frappe.query_reports["Received Items To Be Billed"] = { - "filters": [ - - ] -} + filters: [], +}; diff --git a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js index e3d43a7de1e..292d827b163 100644 --- a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js +++ b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js @@ -1,8 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.require("assets/erpnext/js/sales_trends_filters.js", function() { +frappe.require("assets/erpnext/js/sales_trends_filters.js", function () { frappe.query_reports["Sales Invoice Trends"] = { - filters: erpnext.get_sales_trends_filters() - } + filters: erpnext.get_sales_trends_filters(), + }; }); diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js index 44e20e83c50..0ec24ab0a2a 100644 --- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js +++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js @@ -1,45 +1,45 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt frappe.query_reports["Sales Payment Summary"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1, - "width": "80" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + width: "80", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.get_today(), }, { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"owner", - "label": __("Owner"), - "fieldtype": "Link", - "options": "User", - "defaults": user + fieldname: "owner", + label: __("Owner"), + fieldtype: "Link", + options: "User", + defaults: user, }, { - "fieldname":"is_pos", - "label": __("Show only POS"), - "fieldtype": "Check" + fieldname: "is_pos", + label: __("Show only POS"), + fieldtype: "Check", }, { - "fieldname":"payment_detail", - "label": __("Show Payment Details"), - "fieldtype": "Check" + fieldname: "payment_detail", + label: __("Show Payment Details"), + fieldtype: "Check", }, - ] + ], }; diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py index 057721479e3..7c2bf7ee18c 100644 --- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py +++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py @@ -144,9 +144,9 @@ def get_pos_invoice_data(filters): "ON (" "t1.parent = a.name and t1.base_total = a.base_total) " "WHERE a.docstatus = 1" - " AND {conditions} " + f" AND {conditions} " "GROUP BY " - "owner, posting_date, warehouse".format(conditions=conditions), + "owner, posting_date, warehouse", filters, as_dict=1, ) @@ -156,7 +156,7 @@ def get_pos_invoice_data(filters): def get_sales_invoice_data(filters): conditions = get_conditions(filters) return frappe.db.sql( - """ + f""" select a.posting_date, a.owner, sum(a.net_total) as "net_total", @@ -168,9 +168,7 @@ def get_sales_invoice_data(filters): and {conditions} group by a.owner, a.posting_date - """.format( - conditions=conditions - ), + """, filters, as_dict=1, ) @@ -182,7 +180,7 @@ def get_mode_of_payments(filters): invoice_list_names = ",".join("'" + invoice["name"] + "'" for invoice in invoice_list) if invoice_list: inv_mop = frappe.db.sql( - """select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment + f"""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment from `tabSales Invoice` a, `tabSales Invoice Payment` b where a.name = b.parent and a.docstatus = 1 @@ -202,9 +200,7 @@ def get_mode_of_payments(filters): and a.docstatus = 1 and b.reference_type = 'Sales Invoice' and b.reference_name in ({invoice_list_names}) - """.format( - invoice_list_names=invoice_list_names - ), + """, as_dict=1, ) for d in inv_mop: @@ -215,11 +211,9 @@ def get_mode_of_payments(filters): def get_invoices(filters): conditions = get_conditions(filters) return frappe.db.sql( - """select a.name + f"""select a.name from `tabSales Invoice` a - where a.docstatus = 1 and {conditions}""".format( - conditions=conditions - ), + where a.docstatus = 1 and {conditions}""", filters, as_dict=1, ) @@ -231,7 +225,7 @@ def get_mode_of_payment_details(filters): invoice_list_names = ",".join("'" + invoice["name"] + "'" for invoice in invoice_list) if invoice_list: inv_mop_detail = frappe.db.sql( - """ + f""" select t.owner, t.posting_date, t.mode_of_payment, @@ -264,23 +258,19 @@ def get_mode_of_payment_details(filters): group by a.owner, a.posting_date, mode_of_payment ) t group by t.owner, t.posting_date, t.mode_of_payment - """.format( - invoice_list_names=invoice_list_names - ), + """, as_dict=1, ) inv_change_amount = frappe.db.sql( - """select a.owner, a.posting_date, + f"""select a.owner, a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment, sum(a.base_change_amount) as change_amount from `tabSales Invoice` a, `tabSales Invoice Payment` b where a.name = b.parent and a.name in ({invoice_list_names}) and b.type = 'Cash' and a.base_change_amount > 0 - group by a.owner, a.posting_date, mode_of_payment""".format( - invoice_list_names=invoice_list_names - ), + group by a.owner, a.posting_date, mode_of_payment""", as_dict=1, ) diff --git a/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py index 3ad0ff2ce26..3be96c6de93 100644 --- a/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py +++ b/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py @@ -33,7 +33,7 @@ class TestSalesPaymentSummary(unittest.TestCase): def test_get_mode_of_payments(self): filters = get_filters() - for dummy in range(2): + for _dummy in range(2): si = create_sales_invoice_record() si.insert() si.submit() @@ -53,8 +53,8 @@ class TestSalesPaymentSummary(unittest.TestCase): pe.submit() mop = get_mode_of_payments(filters) - self.assertTrue("Credit Card" in list(mop.values())[0]) - self.assertTrue("Cash" in list(mop.values())[0]) + self.assertTrue("Credit Card" in next(iter(mop.values()))) + self.assertTrue("Cash" in next(iter(mop.values()))) # Cancel all Cash payment entry and check if this mode of payment is still fetched. payment_entries = frappe.get_all( @@ -67,13 +67,13 @@ class TestSalesPaymentSummary(unittest.TestCase): pe.cancel() mop = get_mode_of_payments(filters) - self.assertTrue("Credit Card" in list(mop.values())[0]) - self.assertTrue("Cash" not in list(mop.values())[0]) + self.assertTrue("Credit Card" in next(iter(mop.values()))) + self.assertTrue("Cash" not in next(iter(mop.values()))) def test_get_mode_of_payments_details(self): filters = get_filters() - for dummy in range(2): + for _dummy in range(2): si = create_sales_invoice_record() si.insert() si.submit() @@ -94,7 +94,7 @@ class TestSalesPaymentSummary(unittest.TestCase): mopd = get_mode_of_payment_details(filters) - mopd_values = list(mopd.values())[0] + mopd_values = next(iter(mopd.values())) for mopd_value in mopd_values: if mopd_value[0] == "Credit Card": cc_init_amount = mopd_value[1] @@ -110,7 +110,7 @@ class TestSalesPaymentSummary(unittest.TestCase): pe.cancel() mopd = get_mode_of_payment_details(filters) - mopd_values = list(mopd.values())[0] + mopd_values = next(iter(mopd.values())) for mopd_value in mopd_values: if mopd_value[0] == "Credit Card": cc_final_amount = mopd_value[1] diff --git a/erpnext/accounts/report/sales_register/sales_register.js b/erpnext/accounts/report/sales_register/sales_register.js index 2c9b01bbaa3..b7f82e2367b 100644 --- a/erpnext/accounts/report/sales_register/sales_register.js +++ b/erpnext/accounts/report/sales_register/sales_register.js @@ -2,70 +2,76 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Sales Register"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "width": "80" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + width: "80", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, { - "fieldname":"customer", - "label": __("Customer"), - "fieldtype": "Link", - "options": "Customer" + fieldname: "customer", + label: __("Customer"), + fieldtype: "Link", + options: "Customer", }, { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"mode_of_payment", - "label": __("Mode of Payment"), - "fieldtype": "Link", - "options": "Mode of Payment" + fieldname: "mode_of_payment", + label: __("Mode of Payment"), + fieldtype: "Link", + options: "Mode of Payment", }, { - "fieldname":"owner", - "label": __("Owner"), - "fieldtype": "Link", - "options": "User" + fieldname: "owner", + label: __("Owner"), + fieldtype: "Link", + options: "User", }, { - "fieldname":"cost_center", - "label": __("Cost Center"), - "fieldtype": "Link", - "options": "Cost Center" + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "Link", + options: "Cost Center", }, { - "fieldname":"warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse" + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", }, { - "fieldname":"brand", - "label": __("Brand"), - "fieldtype": "Link", - "options": "Brand" + fieldname: "brand", + label: __("Brand"), + fieldtype: "Link", + options: "Brand", }, { - "fieldname":"item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "options": "Item Group" - } - ] -} + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", + }, + { + fieldname: "include_payments", + label: __("Show Ledger View"), + fieldtype: "Check", + default: 0, + }, + ], +}; -erpnext.utils.add_dimensions('Sales Register', 7); +erpnext.utils.add_dimensions("Sales Register", 7); diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 291c7d976e4..3e7ac1a818d 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -5,13 +5,22 @@ import frappe from frappe import _, msgprint from frappe.model.meta import get_field_precision -from frappe.utils import flt +from frappe.query_builder.custom import ConstantColumn +from frappe.utils import flt, getdate +from pypika import Order -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( - get_accounting_dimensions, - get_dimension_with_children, +from erpnext.accounts.party import get_party_account +from erpnext.accounts.report.utils import ( + apply_common_conditions, + get_advance_taxes_and_charges, + get_journal_entries, + get_opening_row, + get_party_details, + get_payment_entries, + get_query_columns, + get_taxes_query, + get_values_for_columns, ) -from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns def execute(filters=None): @@ -22,9 +31,15 @@ def _execute(filters, additional_table_columns=None): if not filters: filters = frappe._dict({}) + include_payments = filters.get("include_payments") + if filters.get("include_payments") and not filters.get("customer"): + frappe.throw(_("Please select a customer for fetching payments.")) invoice_list = get_invoices(filters, get_query_columns(additional_table_columns)) - columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns( - invoice_list, additional_table_columns + if filters.get("include_payments"): + invoice_list += get_payments(filters) + + columns, income_accounts, unrealized_profit_loss_accounts, tax_accounts = get_columns( + invoice_list, additional_table_columns, include_payments ) if not invoice_list: @@ -34,13 +49,29 @@ def _execute(filters, additional_table_columns=None): invoice_income_map = get_invoice_income_map(invoice_list) internal_invoice_map = get_internal_invoice_map(invoice_list) invoice_income_map, invoice_tax_map = get_invoice_tax_map( - invoice_list, invoice_income_map, income_accounts + invoice_list, invoice_income_map, income_accounts, include_payments ) # Cost Center & Warehouse Map invoice_cc_wh_map = get_invoice_cc_wh_map(invoice_list) invoice_so_dn_map = get_invoice_so_dn_map(invoice_list) company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") mode_of_payments = get_mode_of_payments([inv.name for inv in invoice_list]) + customers = list(set(d.customer for d in invoice_list)) + customer_details = get_party_details("Customer", customers) + + res = [] + if include_payments: + opening_row = get_opening_row( + "Customer", filters.customer, getdate(filters.from_date), filters.company + )[0] + res.append( + { + "receivable_account": opening_row.account, + "debit": flt(opening_row.debit), + "credit": flt(opening_row.credit), + "balance": flt(opening_row.balance), + } + ) data = [] for inv in invoice_list: @@ -51,14 +82,15 @@ def _execute(filters, additional_table_columns=None): warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", []))) row = { - "invoice": inv.name, + "voucher_type": inv.doctype, + "voucher_no": inv.name, "posting_date": inv.posting_date, "customer": inv.customer, "customer_name": inv.customer_name, **get_values_for_columns(additional_table_columns, inv), - "customer_group": inv.get("customer_group"), - "territory": inv.get("territory"), - "tax_id": inv.get("tax_id"), + "customer_group": customer_details.get(inv.customer).get("customer_group"), + "territory": customer_details.get(inv.customer).get("territory"), + "tax_id": customer_details.get(inv.customer).get("tax_id"), "receivable_account": inv.debit_to, "mode_of_payment": ", ".join(mode_of_payments.get(inv.name, [])), "project": inv.project, @@ -66,7 +98,7 @@ def _execute(filters, additional_table_columns=None): "remarks": inv.remarks, "sales_order": ", ".join(sales_order), "delivery_note": ", ".join(delivery_note), - "cost_center": ", ".join(cost_center), + "cost_center": ", ".join(cost_center) if inv.doctype == "Sales Invoice" else inv.cost_center, "warehouse": ", ".join(warehouse), "currency": company_currency, } @@ -97,7 +129,8 @@ def _execute(filters, additional_table_columns=None): if tax_acc not in income_accounts: tax_amount_precision = ( get_field_precision( - frappe.get_meta("Sales Taxes and Charges").get_field("tax_amount"), currency=company_currency + frappe.get_meta("Sales Taxes and Charges").get_field("tax_amount"), + currency=company_currency, ) or 2 ) @@ -116,19 +149,36 @@ def _execute(filters, additional_table_columns=None): } ) + if inv.doctype == "Sales Invoice": + row.update({"debit": inv.base_grand_total, "credit": 0.0}) + else: + row.update({"debit": 0.0, "credit": inv.base_grand_total}) data.append(row) - return columns, data + res += sorted(data, key=lambda x: x["posting_date"]) + + if include_payments: + running_balance = flt(opening_row.balance) + for row in range(1, len(res)): + running_balance += res[row]["debit"] - res[row]["credit"] + res[row].update({"balance": running_balance}) + + return columns, res, None, None, None, include_payments -def get_columns(invoice_list, additional_table_columns): +def get_columns(invoice_list, additional_table_columns, include_payments=False): """return columns based on filters""" columns = [ { - "label": _("Invoice"), - "fieldname": "invoice", - "fieldtype": "Link", - "options": "Sales Invoice", + "label": _("Voucher Type"), + "fieldname": "voucher_type", + "width": 120, + }, + { + "label": _("Voucher"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", "width": 120, }, {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 80}, @@ -142,83 +192,156 @@ def get_columns(invoice_list, additional_table_columns): {"label": _("Customer Name"), "fieldname": "customer_name", "fieldtype": "Data", "width": 120}, ] - if additional_table_columns: + if additional_table_columns and not include_payments: columns += additional_table_columns - columns += [ + if not include_payments: + columns += [ + { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "Customer Group", + "width": 120, + }, + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 80, + }, + {"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 80}, + { + "label": _("Receivable Account"), + "fieldname": "receivable_account", + "fieldtype": "Link", + "options": "Account", + "width": 100, + }, + { + "label": _("Mode Of Payment"), + "fieldname": "mode_of_payment", + "fieldtype": "Data", + "width": 120, + }, + { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 80, + }, + {"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 100}, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 100, + }, + { + "label": _("Delivery Note"), + "fieldname": "delivery_note", + "fieldtype": "Link", + "options": "Delivery Note", + "width": 100, + }, + { + "label": _("Cost Center"), + "fieldname": "cost_center", + "fieldtype": "Link", + "options": "Cost Center", + "width": 100, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, + {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80}, + ] + else: + columns += [ + { + "fieldname": "receivable_account", + "label": _("Receivable Account"), + "fieldtype": "Link", + "options": "Account", + "width": 120, + }, + {"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 120}, + {"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 120}, + {"fieldname": "balance", "label": _("Balance"), "fieldtype": "Currency", "width": 120}, + ] + + account_columns, accounts = get_account_columns(invoice_list, include_payments) + + net_total_column = [ { - "label": _("Customer Group"), - "fieldname": "customer_group", - "fieldtype": "Link", - "options": "Customer Group", + "label": _("Net Total"), + "fieldname": "net_total", + "fieldtype": "Currency", + "options": "currency", "width": 120, - }, - { - "label": _("Territory"), - "fieldname": "territory", - "fieldtype": "Link", - "options": "Territory", - "width": 80, - }, - {"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 120}, - { - "label": _("Receivable Account"), - "fieldname": "receivable_account", - "fieldtype": "Link", - "options": "Account", - "width": 80, - }, - { - "label": _("Mode Of Payment"), - "fieldname": "mode_of_payment", - "fieldtype": "Data", - "width": 120, - }, - { - "label": _("Project"), - "fieldname": "project", - "fieldtype": "Link", - "options": "Project", - "width": 80, - }, - {"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 150}, - {"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Data", "width": 150}, - { - "label": _("Sales Order"), - "fieldname": "sales_order", - "fieldtype": "Link", - "options": "Sales Order", - "width": 100, - }, - { - "label": _("Delivery Note"), - "fieldname": "delivery_note", - "fieldtype": "Link", - "options": "Delivery Note", - "width": 100, - }, - { - "label": _("Cost Center"), - "fieldname": "cost_center", - "fieldtype": "Link", - "options": "Cost Center", - "width": 100, - }, - { - "label": _("Warehouse"), - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 100, - }, - {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80}, + } ] + total_columns = [ + { + "label": _("Tax Total"), + "fieldname": "tax_total", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ] + if not include_payments: + total_columns += [ + { + "label": _("Grand Total"), + "fieldname": "grand_total", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Rounded Total"), + "fieldname": "rounded_total", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Outstanding Amount"), + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + ] + + columns = ( + columns + + account_columns[0] + + account_columns[2] + + net_total_column + + account_columns[1] + + total_columns + ) + columns += [{"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Data", "width": 150}] + return columns, accounts[0], accounts[1], accounts[2] + + +def get_account_columns(invoice_list, include_payments): income_accounts = [] tax_accounts = [] + unrealized_profit_loss_accounts = [] + income_columns = [] tax_columns = [] - unrealized_profit_loss_accounts = [] unrealized_profit_loss_account_columns = [] if invoice_list: @@ -230,14 +353,14 @@ def get_columns(invoice_list, additional_table_columns): tuple(inv.name for inv in invoice_list), ) - tax_accounts = frappe.db.sql_list( - """select distinct account_head - from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice' - and docstatus = 1 and base_tax_amount_after_discount_amount != 0 - and parent in (%s) order by account_head""" - % ", ".join(["%s"] * len(invoice_list)), - tuple(inv.name for inv in invoice_list), - ) + sales_taxes_query = get_taxes_query(invoice_list, "Sales Taxes and Charges", "Sales Invoice") + sales_tax_accounts = sales_taxes_query.run(as_dict=True, pluck="account_head") + tax_accounts = sales_tax_accounts + + if include_payments: + advance_taxes_query = get_taxes_query(invoice_list, "Advance Taxes and Charges", "Payment Entry") + advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head") + tax_accounts = set(tax_accounts + advance_tax_accounts) unrealized_profit_loss_accounts = frappe.db.sql_list( """SELECT distinct unrealized_profit_loss_account @@ -283,134 +406,82 @@ def get_columns(invoice_list, additional_table_columns): } ) - net_total_column = [ - { - "label": _("Net Total"), - "fieldname": "net_total", - "fieldtype": "Currency", - "options": "currency", - "width": 120, - } - ] + columns = [income_columns, unrealized_profit_loss_account_columns, tax_columns] + accounts = [income_accounts, unrealized_profit_loss_accounts, tax_accounts] - total_columns = [ - { - "label": _("Tax Total"), - "fieldname": "tax_total", - "fieldtype": "Currency", - "options": "currency", - "width": 120, - }, - { - "label": _("Grand Total"), - "fieldname": "grand_total", - "fieldtype": "Currency", - "options": "currency", - "width": 120, - }, - { - "label": _("Rounded Total"), - "fieldname": "rounded_total", - "fieldtype": "Currency", - "options": "currency", - "width": 120, - }, - { - "label": _("Outstanding Amount"), - "fieldname": "outstanding_amount", - "fieldtype": "Currency", - "options": "currency", - "width": 120, - }, - ] - - columns = ( - columns - + income_columns - + unrealized_profit_loss_account_columns - + net_total_column - + tax_columns - + total_columns - ) - - return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts - - -def get_conditions(filters): - conditions = "" - - accounting_dimensions = get_accounting_dimensions(as_list=False) or [] - accounting_dimensions_list = [d.fieldname for d in accounting_dimensions] - - if filters.get("company"): - conditions += " and company=%(company)s" - - if filters.get("customer") and "customer" not in accounting_dimensions_list: - conditions += " and customer = %(customer)s" - - if filters.get("from_date"): - conditions += " and posting_date >= %(from_date)s" - if filters.get("to_date"): - conditions += " and posting_date <= %(to_date)s" - - if filters.get("owner"): - conditions += " and owner = %(owner)s" - - def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str: - if not filters.get(field) or field in accounting_dimensions_list: - return "" - return f""" and exists(select name from `tab{table}` - where parent=`tabSales Invoice`.name - and ifnull(`tab{table}`.{field}, '') = %({field})s)""" - - conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment") - conditions += get_sales_invoice_item_field_condition("cost_center") - conditions += get_sales_invoice_item_field_condition("warehouse") - conditions += get_sales_invoice_item_field_condition("brand") - conditions += get_sales_invoice_item_field_condition("item_group") - - if accounting_dimensions: - common_condition = """ - and exists(select name from `tabSales Invoice Item` - where parent=`tabSales Invoice`.name - """ - for dimension in accounting_dimensions: - if filters.get(dimension.fieldname): - if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): - filters[dimension.fieldname] = get_dimension_with_children( - dimension.document_type, filters.get(dimension.fieldname) - ) - - conditions += ( - common_condition - + "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname) - ) - else: - conditions += ( - common_condition - + "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname) - ) - - return conditions + return columns, accounts def get_invoices(filters, additional_query_columns): - conditions = get_conditions(filters) - return frappe.db.sql( - """ - select name, posting_date, debit_to, project, customer, - customer_name, owner, remarks, territory, tax_id, customer_group, - base_net_total, base_grand_total, base_rounded_total, outstanding_amount, - is_internal_customer, represents_company, company {0} - from `tabSales Invoice` - where docstatus = 1 {1} - order by posting_date desc, name desc""".format( - additional_query_columns, conditions - ), - filters, - as_dict=1, + si = frappe.qb.DocType("Sales Invoice") + query = ( + frappe.qb.from_(si) + .select( + ConstantColumn("Sales Invoice").as_("doctype"), + si.name, + si.posting_date, + si.debit_to, + si.project, + si.customer, + si.customer_name, + si.owner, + si.remarks, + si.territory, + si.tax_id, + si.customer_group, + si.base_net_total, + si.base_grand_total, + si.base_rounded_total, + si.outstanding_amount, + si.is_internal_customer, + si.represents_company, + si.company, + ) + .where(si.docstatus == 1) + .orderby(si.posting_date, si.name, order=Order.desc) ) + if additional_query_columns: + for col in additional_query_columns: + query = query.select(col) + + if filters.get("customer"): + query = query.where(si.customer == filters.customer) + + query = get_conditions(filters, query, "Sales Invoice") + query = apply_common_conditions( + filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item" + ) + + invoices = query.run(as_dict=True) + return invoices + + +def get_conditions(filters, query, doctype): + parent_doc = frappe.qb.DocType(doctype) + if filters.get("owner"): + query = query.where(parent_doc.owner == filters.owner) + + if filters.get("mode_of_payment"): + payment_doc = frappe.qb.DocType("Sales Invoice Payment") + query = query.inner_join(payment_doc).on(parent_doc.name == payment_doc.parent) + query = query.where(payment_doc.mode_of_payment == filters.mode_of_payment).distinct() + + return query + + +def get_payments(filters): + args = frappe._dict( + account="debit_to", + account_fieldname="paid_from", + party="customer", + party_name="customer_name", + party_account=[get_party_account("Customer", filters.customer, filters.company)], + ) + payment_entries = get_payment_entries(filters, args) + journal_entries = get_journal_entries(filters, args) + return payment_entries + journal_entries + def get_invoice_income_map(invoice_list): income_details = frappe.db.sql( @@ -447,7 +518,7 @@ def get_internal_invoice_map(invoice_list): return internal_invoice_map -def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts): +def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, include_payments=False): tax_details = frappe.db.sql( """select parent, account_head, sum(base_tax_amount_after_discount_amount) as tax_amount @@ -457,6 +528,9 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts): as_dict=1, ) + if include_payments: + tax_details += get_advance_taxes_and_charges(invoice_list) + invoice_tax_map = {} for d in tax_details: if d.account_head in income_accounts: diff --git a/erpnext/accounts/report/share_balance/share_balance.js b/erpnext/accounts/report/share_balance/share_balance.js index 6db5bdd299e..bcfc2e43f17 100644 --- a/erpnext/accounts/report/share_balance/share_balance.js +++ b/erpnext/accounts/report/share_balance/share_balance.js @@ -4,19 +4,19 @@ /* eslint-disable */ frappe.query_reports["Share Balance"] = { - "filters": [ + filters: [ { - "fieldname":"date", - "label": __("Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1 + fieldname: "date", + label: __("Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"shareholder", - "label": __("Shareholder"), - "fieldtype": "Link", - "options": "Shareholder" - } - ] -} + fieldname: "shareholder", + label: __("Shareholder"), + fieldtype: "Link", + options: "Shareholder", + }, + ], +}; diff --git a/erpnext/accounts/report/share_balance/share_balance.py b/erpnext/accounts/report/share_balance/share_balance.py index d02f53b0d2b..1d02a996b76 100644 --- a/erpnext/accounts/report/share_balance/share_balance.py +++ b/erpnext/accounts/report/share_balance/share_balance.py @@ -15,7 +15,7 @@ def execute(filters=None): columns = get_columns(filters) - date = filters.get("date") + filters.get("date") data = [] diff --git a/erpnext/accounts/report/share_ledger/share_ledger.js b/erpnext/accounts/report/share_ledger/share_ledger.js index 6d1c44a6d01..e5e6289a8d3 100644 --- a/erpnext/accounts/report/share_ledger/share_ledger.js +++ b/erpnext/accounts/report/share_ledger/share_ledger.js @@ -4,19 +4,19 @@ /* eslint-disable */ frappe.query_reports["Share Ledger"] = { - "filters": [ + filters: [ { - "fieldname":"date", - "label": __("Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1 + fieldname: "date", + label: __("Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"shareholder", - "label": __("Shareholder"), - "fieldtype": "Link", - "options": "Shareholder" - } - ] + fieldname: "shareholder", + label: __("Shareholder"), + fieldtype: "Link", + options: "Shareholder", + }, + ], }; diff --git a/erpnext/accounts/report/share_ledger/share_ledger.py b/erpnext/accounts/report/share_ledger/share_ledger.py index 629528e5cc7..ed65687a2ed 100644 --- a/erpnext/accounts/report/share_ledger/share_ledger.py +++ b/erpnext/accounts/report/share_ledger/share_ledger.py @@ -26,9 +26,9 @@ def execute(filters=None): for transfer in transfers: if transfer.transfer_type == "Transfer": if transfer.from_shareholder == filters.get("shareholder"): - transfer.transfer_type += " to {}".format(transfer.to_shareholder) + transfer.transfer_type += f" to {transfer.to_shareholder}" else: - transfer.transfer_type += " from {}".format(transfer.from_shareholder) + transfer.transfer_type += f" from {transfer.from_shareholder}" row = [ filters.get("shareholder"), transfer.date, @@ -66,13 +66,11 @@ def get_all_transfers(date, shareholder): # if company: # condition = 'AND company = %(company)s ' return frappe.db.sql( - """SELECT * FROM `tabShare Transfer` + f"""SELECT * FROM `tabShare Transfer` WHERE ((DATE(date) <= %(date)s AND from_shareholder = %(shareholder)s {condition}) OR (DATE(date) <= %(date)s AND to_shareholder = %(shareholder)s {condition})) AND docstatus = 1 - ORDER BY date""".format( - condition=condition - ), + ORDER BY date""", {"date": date, "shareholder": shareholder}, as_dict=1, ) diff --git a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js index 5dc4c3d1c15..1244a1b0168 100644 --- a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js +++ b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js @@ -3,77 +3,77 @@ /* eslint-disable */ frappe.query_reports["Supplier Ledger Summary"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + 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), - "reqd": 1, - "width": "60px" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, + width: "60px", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1, - "width": "60px" + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + width: "60px", }, { - "fieldname":"finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book" + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname":"party", - "label": __("Supplier"), - "fieldtype": "Link", - "options": "Supplier", + fieldname: "party", + label: __("Supplier"), + fieldtype: "Link", + options: "Supplier", on_change: () => { - var party = frappe.query_report.get_filter_value('party'); + var party = frappe.query_report.get_filter_value("party"); if (party) { - frappe.db.get_value('Supplier', party, ["tax_id", "supplier_name"], function(value) { - frappe.query_report.set_filter_value('tax_id', value["tax_id"]); - frappe.query_report.set_filter_value('supplier_name', value["supplier_name"]); + frappe.db.get_value("Supplier", party, ["tax_id", "supplier_name"], function (value) { + frappe.query_report.set_filter_value("tax_id", value["tax_id"]); + frappe.query_report.set_filter_value("supplier_name", value["supplier_name"]); }); } else { - frappe.query_report.set_filter_value('tax_id', ""); - frappe.query_report.set_filter_value('supplier_name', ""); + frappe.query_report.set_filter_value("tax_id", ""); + frappe.query_report.set_filter_value("supplier_name", ""); } - } + }, }, { - "fieldname":"supplier_group", - "label": __("Supplier Group"), - "fieldtype": "Link", - "options": "Supplier Group" + fieldname: "supplier_group", + label: __("Supplier Group"), + fieldtype: "Link", + options: "Supplier Group", }, { - "fieldname":"payment_terms_template", - "label": __("Payment Terms Template"), - "fieldtype": "Link", - "options": "Payment Terms Template" + fieldname: "payment_terms_template", + label: __("Payment Terms Template"), + fieldtype: "Link", + options: "Payment Terms Template", }, { - "fieldname":"tax_id", - "label": __("Tax Id"), - "fieldtype": "Data", - "hidden": 1 + fieldname: "tax_id", + label: __("Tax Id"), + fieldtype: "Data", + hidden: 1, }, { - "fieldname":"supplier_name", - "label": __("Supplier Name"), - "fieldtype": "Data", - "hidden": 1 - } - ] + fieldname: "supplier_name", + label: __("Supplier Name"), + fieldtype: "Data", + hidden: 1, + }, + ], }; diff --git a/erpnext/accounts/report/tax_detail/tax_detail.js b/erpnext/accounts/report/tax_detail/tax_detail.js index ed6fac4e605..d0e0970daa0 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.js +++ b/erpnext/accounts/report/tax_detail/tax_detail.js @@ -3,7 +3,7 @@ // Contributed by Case Solved and sponsored by Nulight Studios /* eslint-disable */ -frappe.provide('frappe.query_reports'); +frappe.provide("frappe.query_reports"); frappe.query_reports["Tax Detail"] = { filters: [ @@ -13,7 +13,7 @@ frappe.query_reports["Tax Detail"] = { fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("company"), - reqd: 1 + reqd: 1, }, { fieldname: "from_date", @@ -21,7 +21,7 @@ frappe.query_reports["Tax Detail"] = { fieldtype: "Date", default: frappe.datetime.month_start(frappe.datetime.get_today()), reqd: 1, - width: "60px" + width: "60px", }, { fieldname: "to_date", @@ -29,7 +29,7 @@ frappe.query_reports["Tax Detail"] = { fieldtype: "Date", default: frappe.datetime.month_end(frappe.datetime.get_today()), reqd: 1, - width: "60px" + width: "60px", }, { fieldname: "report_name", @@ -37,7 +37,7 @@ frappe.query_reports["Tax Detail"] = { fieldtype: "Read Only", default: frappe.query_report.report_name, hidden: 1, - reqd: 1 + reqd: 1, }, { fieldname: "mode", @@ -45,23 +45,25 @@ frappe.query_reports["Tax Detail"] = { fieldtype: "Read Only", default: "edit", hidden: 1, - reqd: 1 - } + reqd: 1, + }, ], onload: function onload(report) { // Remove Add Column and Save from menu report.page.add_inner_button(__("New Report"), () => new_report(), __("Custom Report")); report.page.add_inner_button(__("Load Report"), () => load_report(), __("Custom Report")); hide_filters(report); - } + }, }; function hide_filters(report) { - report.page.page_form[0].querySelectorAll('.form-group.frappe-control').forEach(function setHidden(field) { - if (field.dataset.fieldtype == "Read Only") { - field.classList.add("hidden"); - } - }); + report.page.page_form[0] + .querySelectorAll(".form-group.frappe-control") + .forEach(function setHidden(field) { + if (field.dataset.fieldtype == "Read Only") { + field.classList.add("hidden"); + } + }); } erpnext.TaxDetail = class TaxDetail { @@ -74,20 +76,20 @@ erpnext.TaxDetail = class TaxDetail { this.qr = frappe.query_report; this.super = { refresh_report: this.qr.refresh_report, - show_footer_message: this.qr.show_footer_message - } + show_footer_message: this.qr.show_footer_message, + }; this.qr.refresh_report = () => this.refresh_report(); this.qr.show_footer_message = () => this.show_footer_message(); } show_footer_message() { // The last thing to run after datatable_render in refresh() this.super.show_footer_message.apply(this.qr); - if (this.qr.report_name !== 'Tax Detail') { + if (this.qr.report_name !== "Tax Detail") { this.show_help(); if (this.loading) { - this.set_section(''); + this.set_section(""); } else { - this.reload_component(''); + this.reload_component(""); } } this.loading = false; @@ -98,16 +100,18 @@ erpnext.TaxDetail = class TaxDetail { // already run within frappe.run_serially this.loading = true; this.super.refresh_report.apply(this.qr); - if (this.qr.report_name !== 'Tax Detail') { - frappe.call({ - method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports', - args: {name: this.qr.report_name} - }).then((r) => { - const data = JSON.parse(r.message[this.qr.report_name]['json']); - this.create_controls(); - this.sections = data.sections || {}; - this.controls['show_detail'].set_input(data.show_detail); - }); + if (this.qr.report_name !== "Tax Detail") { + frappe + .call({ + method: "erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports", + args: { name: this.qr.report_name }, + }) + .then((r) => { + const data = JSON.parse(r.message[this.qr.report_name]["json"]); + this.create_controls(); + this.sections = data.sections || {}; + this.controls["show_detail"].set_input(data.show_detail); + }); } } load_report() { @@ -118,14 +122,14 @@ erpnext.TaxDetail = class TaxDetail { get_menu_items() { // Replace Save action let new_items = []; - const save = __('Save'); + const save = __("Save"); for (let item of this.qr.menu_items) { if (item.label === save) { new_items.push({ label: save, action: () => this.save_report(), - standard: false + standard: false, }); } else { new_items.push(item); @@ -135,27 +139,29 @@ erpnext.TaxDetail = class TaxDetail { } save_report() { this.check_datatable(); - if (this.qr.report_name !== 'Tax Detail') { - frappe.call({ - method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report', - args: { - reference_report: 'Tax Detail', - report_name: this.qr.report_name, - data: { - columns: this.qr.get_visible_columns(), - sections: this.sections, - show_detail: this.controls['show_detail'].get_input_value() - } - }, - freeze: true - }).then((r) => { - this.set_section(''); - }); + if (this.qr.report_name !== "Tax Detail") { + frappe + .call({ + method: "erpnext.accounts.report.tax_detail.tax_detail.save_custom_report", + args: { + reference_report: "Tax Detail", + report_name: this.qr.report_name, + data: { + columns: this.qr.get_visible_columns(), + sections: this.sections, + show_detail: this.controls["show_detail"].get_input_value(), + }, + }, + freeze: true, + }) + .then((r) => { + this.set_section(""); + }); } } check_datatable() { if (!this.qr.datatable) { - frappe.throw(__('Please change the date range to load data first')); + frappe.throw(__("Please change the date range to load data first")); } } set_section(name) { @@ -164,60 +170,62 @@ erpnext.TaxDetail = class TaxDetail { this.sections[name] = {}; } let options = Object.keys(this.sections); - options.unshift(''); - this.controls['section_name'].$wrapper.find("select").empty().add_options(options); - const org_mode = this.qr.get_filter_value('mode'); + options.unshift(""); + this.controls["section_name"].$wrapper.find("select").empty().add_options(options); + const org_mode = this.qr.get_filter_value("mode"); let refresh = false; if (name) { - this.controls['section_name'].set_input(name); - this.qr.set_filter_value('mode', 'edit'); - if (org_mode === 'run') { + this.controls["section_name"].set_input(name); + this.qr.set_filter_value("mode", "edit"); + if (org_mode === "run") { refresh = true; } } else { - this.controls['section_name'].set_input(''); - this.qr.set_filter_value('mode', 'run'); - if (org_mode === 'edit') { + this.controls["section_name"].set_input(""); + this.qr.set_filter_value("mode", "run"); + if (org_mode === "edit") { refresh = true; } } if (refresh) { this.qr.refresh(); } - this.reload_component(''); + this.reload_component(""); } reload_component(component_name) { - const section_name = this.controls['section_name'].get_input_value(); + const section_name = this.controls["section_name"].get_input_value(); if (section_name) { const section = this.sections[section_name]; const component_names = Object.keys(section); - component_names.unshift(''); - this.controls['component'].$wrapper.find("select").empty().add_options(component_names); - this.controls['component'].set_input(component_name); + component_names.unshift(""); + this.controls["component"].$wrapper.find("select").empty().add_options(component_names); + this.controls["component"].set_input(component_name); if (component_name) { - this.controls['component_type'].set_input(section[component_name].type); + this.controls["component_type"].set_input(section[component_name].type); } } else { - this.controls['component'].$wrapper.find("select").empty(); - this.controls['component'].set_input(''); + this.controls["component"].$wrapper.find("select").empty(); + this.controls["component"].set_input(""); } this.set_table_filters(); } set_table_filters() { let filters = {}; - const section_name = this.controls['section_name'].get_input_value(); - const component_name = this.controls['component'].get_input_value(); + const section_name = this.controls["section_name"].get_input_value(); + const component_name = this.controls["component"].get_input_value(); if (section_name && component_name) { const component_type = this.sections[section_name][component_name].type; - if (component_type === 'filter') { - filters = this.sections[section_name][component_name]['filters']; + if (component_type === "filter") { + filters = this.sections[section_name][component_name]["filters"]; } } this.setAppliedFilters(filters); } setAppliedFilters(filters) { if (this.qr.datatable) { - Array.from(this.qr.datatable.header.querySelectorAll('.dt-filter')).map(function setFilters(input) { + Array.from(this.qr.datatable.header.querySelectorAll(".dt-filter")).map(function setFilters( + input + ) { let idx = input.dataset.colIndex; if (filters[idx]) { input.value = filters[idx]; @@ -229,82 +237,89 @@ erpnext.TaxDetail = class TaxDetail { } } delete(name, type) { - if (type === 'section') { + if (type === "section") { delete this.sections[name]; - const new_section = Object.keys(this.sections)[0] || ''; + const new_section = Object.keys(this.sections)[0] || ""; this.set_section(new_section); } - if (type === 'component') { - const cur_section = this.controls['section_name'].get_input_value(); + if (type === "component") { + const cur_section = this.controls["section_name"].get_input_value(); delete this.sections[cur_section][name]; - this.reload_component(''); + this.reload_component(""); } } create_controls() { let controls = {}; // SELECT in data.js - controls['section_name'] = this.qr.page.add_field({ - label: __('Section'), - fieldtype: 'Select', - fieldname: 'section_name', + controls["section_name"] = this.qr.page.add_field({ + label: __("Section"), + fieldtype: "Select", + fieldname: "section_name", change: (e) => { - this.set_section(this.controls['section_name'].get_input_value()); - } + this.set_section(this.controls["section_name"].get_input_value()); + }, }); // BUTTON in button.js - controls['new_section'] = this.qr.page.add_field({ - label: __('New Section'), - fieldtype: 'Button', - fieldname: 'new_section', + controls["new_section"] = this.qr.page.add_field({ + label: __("New Section"), + fieldtype: "Button", + fieldname: "new_section", click: () => { - frappe.prompt({ - label: __('Section Name'), - fieldname: 'name', - fieldtype: 'Data' - }, (values) => { - this.set_section(values.name); - }); - } + frappe.prompt( + { + label: __("Section Name"), + fieldname: "name", + fieldtype: "Data", + }, + (values) => { + this.set_section(values.name); + } + ); + }, }); - controls['delete_section'] = this.qr.page.add_field({ - label: __('Delete Section'), - fieldtype: 'Button', - fieldname: 'delete_section', + controls["delete_section"] = this.qr.page.add_field({ + label: __("Delete Section"), + fieldtype: "Button", + fieldname: "delete_section", click: () => { - let cur_section = this.controls['section_name'].get_input_value(); + let cur_section = this.controls["section_name"].get_input_value(); if (cur_section) { - frappe.confirm(__('Are you sure you want to delete section') + ' ' + cur_section + '?', - () => {this.delete(cur_section, 'section')}); + frappe.confirm( + __("Are you sure you want to delete section") + " " + cur_section + "?", + () => { + this.delete(cur_section, "section"); + } + ); } - } + }, }); - controls['component'] = this.qr.page.add_field({ - label: __('Component'), - fieldtype: 'Select', - fieldname: 'component', + controls["component"] = this.qr.page.add_field({ + label: __("Component"), + fieldtype: "Select", + fieldname: "component", change: (e) => { - this.reload_component(this.controls['component'].get_input_value()); - } + this.reload_component(this.controls["component"].get_input_value()); + }, }); - controls['component_type'] = this.qr.page.add_field({ - label: __('Component Type'), - fieldtype: 'Select', - fieldname: 'component_type', - default: 'filter', + controls["component_type"] = this.qr.page.add_field({ + label: __("Component Type"), + fieldtype: "Select", + fieldname: "component_type", + default: "filter", options: [ - {label: __('Filtered Row Subtotal'), value: 'filter'}, - {label: __('Section Subtotal'), value: 'section'} - ] + { label: __("Filtered Row Subtotal"), value: "filter" }, + { label: __("Section Subtotal"), value: "section" }, + ], }); - controls['add_component'] = this.qr.page.add_field({ - label: __('Add Component'), - fieldtype: 'Button', - fieldname: 'add_component', + controls["add_component"] = this.qr.page.add_field({ + label: __("Add Component"), + fieldtype: "Button", + fieldname: "add_component", click: () => { this.check_datatable(); - let section_name = this.controls['section_name'].get_input_value(); + let section_name = this.controls["section_name"].get_input_value(); if (section_name) { - const component_type = this.controls['component_type'].get_input_value(); + const component_type = this.controls["component_type"].get_input_value(); let idx = 0; const names = Object.keys(this.sections[section_name]); if (names.length > 0) { @@ -312,118 +327,135 @@ erpnext.TaxDetail = class TaxDetail { idx = Math.max(...idxs) + 1; } const filters = this.qr.datatable.columnmanager.getAppliedFilters(); - if (component_type === 'filter') { - const name = 'Filter' + idx.toString(); + if (component_type === "filter") { + const name = "Filter" + idx.toString(); let data = { type: component_type, - filters: filters - } + filters: filters, + }; this.sections[section_name][name] = data; this.reload_component(name); - } else if (component_type === 'section') { + } else if (component_type === "section") { if (filters && Object.keys(filters).length !== 0) { frappe.show_alert({ - message: __('Column filters ignored'), - indicator: 'yellow' + message: __("Column filters ignored"), + indicator: "yellow", }); } let data = { - type: component_type - } - frappe.prompt({ - label: __('Section'), - fieldname: 'section', - fieldtype: 'Select', - options: Object.keys(this.sections) - }, (values) => { - this.sections[section_name][values.section] = data; - this.reload_component(values.section); - }); + type: component_type, + }; + frappe.prompt( + { + label: __("Section"), + fieldname: "section", + fieldtype: "Select", + options: Object.keys(this.sections), + }, + (values) => { + this.sections[section_name][values.section] = data; + this.reload_component(values.section); + } + ); } else { - frappe.throw(__('Please select the Component Type first')); + frappe.throw(__("Please select the Component Type first")); } } else { - frappe.throw(__('Please select the Section first')); + frappe.throw(__("Please select the Section first")); } - } + }, }); - controls['delete_component'] = this.qr.page.add_field({ - label: __('Delete Component'), - fieldtype: 'Button', - fieldname: 'delete_component', + controls["delete_component"] = this.qr.page.add_field({ + label: __("Delete Component"), + fieldtype: "Button", + fieldname: "delete_component", click: () => { - const component = this.controls['component'].get_input_value(); + const component = this.controls["component"].get_input_value(); if (component) { - frappe.confirm(__('Are you sure you want to delete component') + ' ' + component + '?', - () => {this.delete(component, 'component')}); + frappe.confirm( + __("Are you sure you want to delete component") + " " + component + "?", + () => { + this.delete(component, "component"); + } + ); } - } + }, }); - controls['save'] = this.qr.page.add_field({ - label: __('Save & Run'), - fieldtype: 'Button', - fieldname: 'save', + controls["save"] = this.qr.page.add_field({ + label: __("Save & Run"), + fieldtype: "Button", + fieldname: "save", click: () => { this.save_report(); - } + }, }); - controls['show_detail'] = this.qr.page.add_field({ - label: __('Show Detail'), - fieldtype: 'Check', - fieldname: 'show_detail', - default: 1 + controls["show_detail"] = this.qr.page.add_field({ + label: __("Show Detail"), + fieldtype: "Check", + fieldname: "show_detail", + default: 1, }); this.controls = controls; } show_help() { - const help = __('Your custom report is built from General Ledger Entries within the date range. You can add multiple sections to the report using the New Section button. Each component added to a section adds a subset of the data into the specified section. Beware of duplicated data rows. The Filtered Row component type saves the datatable column filters to specify the added data. The Section component type refers to the data in a previously defined section, but it cannot refer to its parent section. The Amount column is summed to give the section subtotal. Use the Show Detail box to see the data rows included in each section in the final report. Once finished, hit Save & Run. Report contributed by'); - this.qr.$report_footer.append('
    ' + __('Help') + `: ${help} Case Solved
    `); + const help = __( + "Your custom report is built from General Ledger Entries within the date range. You can add multiple sections to the report using the New Section button. Each component added to a section adds a subset of the data into the specified section. Beware of duplicated data rows. The Filtered Row component type saves the datatable column filters to specify the added data. The Section component type refers to the data in a previously defined section, but it cannot refer to its parent section. The Amount column is summed to give the section subtotal. Use the Show Detail box to see the data rows included in each section in the final report. Once finished, hit Save & Run. Report contributed by" + ); + this.qr.$report_footer.append( + '
    ' + + __("Help") + + `: ${help} Case Solved
    ` + ); } -} +}; if (!window.taxdetail) { window.taxdetail = new erpnext.TaxDetail(); } function get_reports(cb) { - frappe.call({ - method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports', - freeze: true - }).then((r) => { - cb(r.message); - }) + frappe + .call({ + method: "erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports", + freeze: true, + }) + .then((r) => { + cb(r.message); + }); } function new_report() { const dialog = new frappe.ui.Dialog({ - title: __('New Report'), + title: __("New Report"), fields: [ { - fieldname: 'report_name', - label: __('Report Name'), - fieldtype: 'Data', - default: 'VAT Return' - } + fieldname: "report_name", + label: __("Report Name"), + fieldtype: "Data", + default: "VAT Return", + }, ], - primary_action_label: __('Create'), + primary_action_label: __("Create"), primary_action: function new_report_pa(values) { - frappe.call({ - method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report', - args: { - reference_report: 'Tax Detail', - report_name: values.report_name, - data: { - columns: [], - sections: {}, - show_detail: 1 - } - }, - freeze: true - }).then((r) => { - frappe.set_route('query-report', values.report_name); - }); + frappe + .call({ + method: "erpnext.accounts.report.tax_detail.tax_detail.save_custom_report", + args: { + reference_report: "Tax Detail", + report_name: values.report_name, + data: { + columns: [], + sections: {}, + show_detail: 1, + }, + }, + freeze: true, + }) + .then((r) => { + frappe.set_route("query-report", values.report_name); + }); dialog.hide(); - } + }, }); dialog.show(); } @@ -431,20 +463,20 @@ function new_report() { function load_report() { get_reports(function load_report_cb(reports) { const dialog = new frappe.ui.Dialog({ - title: __('Load Report'), + title: __("Load Report"), fields: [ { - fieldname: 'report_name', - label: __('Report Name'), - fieldtype: 'Select', - options: Object.keys(reports) - } + fieldname: "report_name", + label: __("Report Name"), + fieldtype: "Select", + options: Object.keys(reports), + }, ], - primary_action_label: __('Load'), + primary_action_label: __("Load"), primary_action: function load_report_pa(values) { dialog.hide(); - frappe.set_route('query-report', values.report_name); - } + frappe.set_route("query-report", values.report_name); + }, }); dialog.show(); }); diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index ba733c2d185..47b92dd13b0 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -34,7 +34,7 @@ def execute(filters=None): fieldstr = get_fieldstr(fieldlist) gl_entries = frappe.db.sql( - """ + f""" select {fieldstr} from `tabGL Entry` ge inner join `tabAccount` a on @@ -52,9 +52,7 @@ def execute(filters=None): ge.posting_date>=%(from_date)s and ge.posting_date<=%(to_date)s order by ge.posting_date, ge.voucher_no - """.format( - fieldstr=fieldstr - ), + """, filters, as_dict=1, ) @@ -94,7 +92,9 @@ def run_report(report_name, data): report[section_name]["subtotal"] += row["amount"] if component["type"] == "section": if component_name == section_name: - frappe.throw(_("A report component cannot refer to its parent section") + ": " + section_name) + frappe.throw( + _("A report component cannot refer to its parent section") + ": " + section_name + ) try: report[section_name]["rows"] += report[component_name]["rows"] report[section_name]["subtotal"] += report[component_name]["subtotal"] diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py index 869f2450218..55ae32a2c6a 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.py +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py @@ -21,7 +21,7 @@ class TestTaxDetail(unittest.TestCase): from erpnext.accounts.utils import FiscalYearError, get_fiscal_year datapath, _ = os.path.splitext(os.path.realpath(__file__)) - with open(datapath + ".json", "r") as fp: + with open(datapath + ".json") as fp: docs = json.load(fp) now = getdate() @@ -45,8 +45,9 @@ class TestTaxDetail(unittest.TestCase): "year": "_Test Fiscal", "year_end_date": get_year_ending(now), "year_start_date": get_year_start(now), - } - ] + docs + }, + *docs, + ] docs = [ { @@ -56,8 +57,9 @@ class TestTaxDetail(unittest.TestCase): "default_currency": "GBP", "doctype": "Company", "name": "_T", - } - ] + docs + }, + *docs, + ] for doc in docs: try: @@ -109,9 +111,14 @@ class TestTaxDetail(unittest.TestCase): "Box5": {"Box3": {"type": "section"}, "Box4": {"type": "section"}}, "Box6": {"Filter0": {"type": "filter", "filters": {"3": "!=Tax", "4": "Sales"}}}, "Box7": {"Filter0": {"type": "filter", "filters": {"2": "Expense", "3": "!=Tax"}}}, - "Box8": {"Filter0": {"type": "filter", "filters": {"3": "!=Tax", "4": "Sales", "12": "EU"}}}, + "Box8": { + "Filter0": {"type": "filter", "filters": {"3": "!=Tax", "4": "Sales", "12": "EU"}} + }, "Box9": { - "Filter0": {"type": "filter", "filters": {"2": "Expense", "3": "!=Tax", "12": "EU"}} + "Filter0": { + "type": "filter", + "filters": {"2": "Expense", "3": "!=Tax", "12": "EU"}, + } }, }, "show_detail": 1, diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js index c42028b61f5..054f085e3b7 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js @@ -3,60 +3,60 @@ /* eslint-disable */ frappe.query_reports["TDS Computation Summary"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_default('company') + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_default("company"), }, { - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Select", - "options": ["Supplier", "Customer"], - "reqd": 1, - "default": "Supplier", - "on_change": function(){ + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Select", + options: ["Supplier", "Customer"], + reqd: 1, + default: "Supplier", + on_change: function () { frappe.query_report.set_filter_value("party", ""); - } + }, }, { - "fieldname":"party", - "label": __("Party"), - "fieldtype": "Dynamic Link", - "get_options": function() { - var party_type = frappe.query_report.get_filter_value('party_type'); - var party = frappe.query_report.get_filter_value('party'); - if(party && !party_type) { + fieldname: "party", + label: __("Party"), + fieldtype: "Dynamic Link", + get_options: function () { + var party_type = frappe.query_report.get_filter_value("party_type"); + var party = frappe.query_report.get_filter_value("party"); + if (party && !party_type) { frappe.throw(__("Please select Party Type first")); } return party_type; }, - "get_query": function() { + get_query: function () { return { - "filters": { - "tax_withholding_category": ["!=",""], - } - } + filters: { + tax_withholding_category: ["!=", ""], + }, + }; }, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "reqd": 1, - "width": "60px" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, + width: "60px", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1, - "width": "60px" - } - ] -} + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + width: "60px", + }, + ], +}; diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index 82f97f18941..d876d6d14d2 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -67,13 +67,13 @@ def group_by_party_and_category(data, filters): }, ) - party_category_wise_map.get((row.get("party"), row.get("section_code")))[ - "total_amount" - ] += row.get("total_amount", 0.0) + party_category_wise_map.get((row.get("party"), row.get("section_code")))["total_amount"] += row.get( + "total_amount", 0.0 + ) - party_category_wise_map.get((row.get("party"), row.get("section_code")))[ - "tax_amount" - ] += row.get("tax_amount", 0.0) + party_category_wise_map.get((row.get("party"), row.get("section_code")))["tax_amount"] += row.get( + "tax_amount", 0.0 + ) final_result = get_final_result(party_category_wise_map) @@ -82,7 +82,7 @@ def group_by_party_and_category(data, filters): def get_final_result(party_category_wise_map): out = [] - for key, value in party_category_wise_map.items(): + for _key, value in party_category_wise_map.items(): out.append(value) return out diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js index 6585ea0a293..cf7e6669596 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js @@ -3,60 +3,60 @@ /* eslint-disable */ frappe.query_reports["TDS Payable Monthly"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_default('company') + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_default("company"), }, { - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Select", - "options": ["Supplier", "Customer"], - "reqd": 1, - "default": "Supplier", - "on_change": function(){ + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Select", + options: ["Supplier", "Customer"], + reqd: 1, + default: "Supplier", + on_change: function () { frappe.query_report.set_filter_value("party", ""); - } + }, }, { - "fieldname":"party", - "label": __("Party"), - "fieldtype": "Dynamic Link", - "get_options": function() { - var party_type = frappe.query_report.get_filter_value('party_type'); - var party = frappe.query_report.get_filter_value('party'); - if(party && !party_type) { + fieldname: "party", + label: __("Party"), + fieldtype: "Dynamic Link", + get_options: function () { + var party_type = frappe.query_report.get_filter_value("party_type"); + var party = frappe.query_report.get_filter_value("party"); + if (party && !party_type) { frappe.throw(__("Please select Party Type first")); } return party_type; }, - "get_query": function() { + get_query: function () { return { - "filters": { - "tax_withholding_category": ["!=",""], - } - } + filters: { + tax_withholding_category: ["!=", ""], + }, + }; }, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "reqd": 1, - "width": "60px" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, + width: "60px", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1, - "width": "60px" - } - ] -} + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + width: "60px", + }, + ], +}; diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index f6c7bd3db70..fc5e8452e1d 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -37,21 +37,17 @@ def validate_filters(filters): frappe.throw(_("From Date must be before To Date")) -def get_result( - filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map -): +def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map): party_map = get_party_pan_map(filters.get("party_type")) tax_rate_map = get_tax_rate_map(filters) gle_map = get_gle_map(tds_docs) out = [] for name, details in gle_map.items(): - tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 - bill_no, bill_date = "", "" - tax_withholding_category = tax_category_map.get(name) - rate = tax_rate_map.get(tax_withholding_category) - for entry in details: + tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 + tax_withholding_category, rate = None, None + bill_no, bill_date = "", "" party = entry.party or entry.against posting_date = entry.posting_date voucher_type = entry.voucher_type @@ -61,13 +57,18 @@ def get_result( if party_list: party = party_list[0] - if not tax_withholding_category: - tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") - rate = tax_rate_map.get(tax_withholding_category) - - if entry.account in tds_accounts: + if entry.account in tds_accounts.keys(): tax_amount += entry.credit - entry.debit + # infer tax withholding category from the account if it's the single account for this category + tax_withholding_category = tds_accounts.get(entry.account) + # or else the consolidated value from the voucher document + if not tax_withholding_category: + tax_withholding_category = tax_category_map.get(name) + # or else from the party default + if not tax_withholding_category: + tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") + rate = tax_rate_map.get(tax_withholding_category) if net_total_map.get(name): if voucher_type == "Journal Entry" and tax_amount and rate: # back calcalute total amount from rate and tax_amount @@ -80,41 +81,41 @@ def get_result( else: total_amount += entry.credit - if tax_amount: - if party_map.get(party, {}).get("party_type") == "Supplier": - party_name = "supplier_name" - party_type = "supplier_type" - else: - party_name = "customer_name" - party_type = "customer_type" + if tax_amount: + if party_map.get(party, {}).get("party_type") == "Supplier": + party_name = "supplier_name" + party_type = "supplier_type" + else: + party_name = "customer_name" + party_type = "customer_type" - row = { - "pan" - if frappe.db.has_column(filters.party_type, "pan") - else "tax_id": party_map.get(party, {}).get("pan"), - "party": party_map.get(party, {}).get("name"), - } - - if filters.naming_series == "Naming Series": - row.update({"party_name": party_map.get(party, {}).get(party_name)}) - - row.update( - { - "section_code": tax_withholding_category or "", - "entity_type": party_map.get(party, {}).get(party_type), - "rate": rate, - "total_amount": total_amount, - "grand_total": grand_total, - "base_total": base_total, - "tax_amount": tax_amount, - "transaction_date": posting_date, - "transaction_type": voucher_type, - "ref_no": name, - "supplier_invoice_no": bill_no, - "supplier_invoice_date": bill_date, + row = { + "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id": party_map.get( + party, {} + ).get("pan"), + "party": party_map.get(party, {}).get("name"), } - ) - out.append(row) + + if filters.naming_series == "Naming Series": + row.update({"party_name": party_map.get(party, {}).get(party_name)}) + + row.update( + { + "section_code": tax_withholding_category or "", + "entity_type": party_map.get(party, {}).get(party_type), + "rate": rate, + "total_amount": total_amount, + "grand_total": grand_total, + "base_total": base_total, + "tax_amount": tax_amount, + "transaction_date": posting_date, + "transaction_type": voucher_type, + "ref_no": name, + "supplier_invoice_no": bill_no, + "supplier_invoice_date": bill_date, + } + ) + out.append(row) out.sort(key=lambda x: x["section_code"]) @@ -154,7 +155,7 @@ def get_gle_map(documents): ) for d in gle: - if not d.voucher_no in gle_map: + if d.voucher_no not in gle_map: gle_map[d.voucher_no] = [d] else: gle_map[d.voucher_no].append(d) @@ -239,7 +240,7 @@ def get_columns(filters): "width": 120, }, { - "label": _("Tax Amount"), + "label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"), "fieldname": "tax_amount", "fieldtype": "Float", "width": 120, @@ -278,15 +279,24 @@ def get_tds_docs(filters): journal_entries = [] tax_category_map = frappe._dict() net_total_map = frappe._dict() - or_filters = frappe._dict() + frappe._dict() journal_entry_party_map = frappe._dict() bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name") - tds_accounts = frappe.get_all( - "Tax Withholding Account", {"company": filters.get("company")}, pluck="account" + _tds_accounts = frappe.get_all( + "Tax Withholding Account", + {"company": filters.get("company")}, + ["account", "parent"], ) + tds_accounts = {} + for tds_acc in _tds_accounts: + # if it turns out not to be the only tax withholding category, then don't include in the map + if tds_acc["account"] in tds_accounts: + tds_accounts[tds_acc["account"]] = None + else: + tds_accounts[tds_acc["account"]] = tds_acc["parent"] - tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True) + tds_docs = get_tds_docs_query(filters, bank_accounts, list(tds_accounts.keys())).run(as_dict=True) for d in tds_docs: if d.voucher_type == "Purchase Invoice": @@ -332,7 +342,7 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts): query = ( frappe.qb.from_(gle) .select("voucher_no", "voucher_type", "against", "party") - .where((gle.is_cancelled == 0)) + .where(gle.is_cancelled == 0) ) if filters.get("from_date"): @@ -345,21 +355,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts): if filters.get("party"): party = [filters.get("party")] - query = query.where( - ((gle.account.isin(tds_accounts) & gle.against.isin(party))) - | ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))) - | gle.party.isin(party) + jv_condition = gle.against.isin(party) | ( + (gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")) ) else: party = frappe.get_all(filters.get("party_type"), pluck="name") - query = query.where( - ((gle.account.isin(tds_accounts) & gle.against.isin(party))) - | ( - (gle.voucher_type == "Journal Entry") - & ((gle.party_type == filters.get("party_type")) | (gle.party_type == "")) - ) - | gle.party.isin(party) + jv_condition = gle.against.isin(party) | ( + (gle.voucher_type == "Journal Entry") + & ((gle.party_type == filters.get("party_type")) | (gle.party_type == "")) ) + query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party)) return query @@ -399,7 +404,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): "paid_amount_after_tax", "base_paid_amount", ], - "Journal Entry": ["tax_withholding_category", "total_amount"], + "Journal Entry": ["total_amount"], } entries = frappe.get_all( diff --git a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py index 89ecef1904c..742d2f37c45 100644 --- a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py @@ -5,7 +5,6 @@ import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import today -from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -22,18 +21,42 @@ class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase): self.create_company() self.clear_old_entries() create_tax_accounts() - create_tcs_category() def test_tax_withholding_for_customers(self): + create_tax_category(cumulative_threshold=300) + frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS") si = create_sales_invoice(rate=1000) pe = create_tcs_payment_entry() + jv = create_tcs_journal_entry() + filters = frappe._dict( company="_Test Company", party_type="Customer", from_date=today(), to_date=today() ) result = execute(filters)[1] expected_values = [ + # Check for JV totals using back calculation logic + [jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0], [pe.name, "TCS", 0.075, 2550, 0.53, 2550.53], - [si.name, "TCS", 0.075, 1000, 0.53, 1000.53], + [si.name, "TCS", 0.075, 1000.0, 0.53, 1000.53], + ] + self.check_expected_values(result, expected_values) + + def test_single_account_for_multiple_categories(self): + create_tax_category("TDS - 1", rate=10, account="TDS - _TC") + inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_1.tax_withholding_category = "TDS - 1" + inv_1.submit() + + create_tax_category("TDS - 2", rate=20, account="TDS - _TC") + inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_2.tax_withholding_category = "TDS - 2" + inv_2.submit() + result = execute( + frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today()) + )[1] + expected_values = [ + [inv_1.name, "TDS - 1", 10, 5000, 500, 5500], + [inv_2.name, "TDS - 2", 20, 5000, 1000, 6000], ] self.check_expected_values(result, expected_values) @@ -41,12 +64,15 @@ class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase): for i in range(len(result)): voucher = frappe._dict(result[i]) voucher_expected_values = expected_values[i] - self.assertEqual(voucher.ref_no, voucher_expected_values[0]) - self.assertEqual(voucher.section_code, voucher_expected_values[1]) - self.assertEqual(voucher.rate, voucher_expected_values[2]) - self.assertEqual(voucher.base_total, voucher_expected_values[3]) - self.assertEqual(voucher.tax_amount, voucher_expected_values[4]) - self.assertEqual(voucher.grand_total, voucher_expected_values[5]) + voucher_actual_values = ( + voucher.ref_no, + voucher.section_code, + voucher.rate, + voucher.base_total, + voucher.tax_amount, + voucher.grand_total, + ) + self.assertSequenceEqual(voucher_actual_values, voucher_expected_values) def tearDown(self): self.clear_old_entries() @@ -67,24 +93,20 @@ def create_tax_accounts(): ).insert(ignore_if_duplicate=True) -def create_tcs_category(): +def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0): fiscal_year = get_fiscal_year(today(), company="_Test Company") from_date = fiscal_year[1] to_date = fiscal_year[2] - tax_category = create_tax_withholding_category( - category_name="TCS", - rate=0.075, + create_tax_withholding_category( + category_name=category, + rate=rate, from_date=from_date, to_date=to_date, - account="TCS - _TC", - cumulative_threshold=300, + account=account, + cumulative_threshold=cumulative_threshold, ) - customer = frappe.get_doc("Customer", "_Test Customer") - customer.tax_withholding_category = "TCS" - customer.save() - def create_tcs_payment_entry(): payment_entry = create_payment_entry( @@ -109,3 +131,32 @@ def create_tcs_payment_entry(): ) payment_entry.submit() return payment_entry + + +def create_tcs_journal_entry(): + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = "_Test Company" + jv.set( + "accounts", + [ + { + "account": "Debtors - _TC", + "party_type": "Customer", + "party": "_Test Customer", + "credit_in_account_currency": 10000, + }, + { + "account": "Debtors - _TC", + "party_type": "Customer", + "party": "_Test Customer", + "debit_in_account_currency": 9992.5, + }, + { + "account": "TCS - _TC", + "debit_in_account_currency": 7.5, + }, + ], + ) + jv.insert() + return jv.submit() diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index ee1d5e3dba0..7337fd477e7 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -1,118 +1,118 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.require("assets/erpnext/js/financial_statements.js", function() { +frappe.require("assets/erpnext/js/financial_statements.js", function () { frappe.query_reports["Trial Balance"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname": "fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1, - "on_change": function(query_report) { + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "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; if (!fiscal_year) { return; } - frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); frappe.query_report.set_filter_value({ from_date: fy.year_start_date, - to_date: fy.year_end_date + to_date: fy.year_end_date, }); }); - } + }, }, { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], }, { - "fieldname": "cost_center", - "label": __("Cost Center"), - "fieldtype": "Link", - "options": "Cost Center", - "get_query": function() { - var company = frappe.query_report.get_filter_value('company'); + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "Link", + options: "Cost Center", + get_query: function () { + var company = frappe.query_report.get_filter_value("company"); return { - "doctype": "Cost Center", - "filters": { - "company": company, - } - } - } + doctype: "Cost Center", + filters: { + company: company, + }, + }; + }, }, { - "fieldname": "project", - "label": __("Project"), - "fieldtype": "Link", - "options": "Project" + fieldname: "project", + label: __("Project"), + fieldtype: "Link", + options: "Project", }, { - "fieldname": "finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book", + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname": "presentation_currency", - "label": __("Currency"), - "fieldtype": "Select", - "options": erpnext.get_presentation_currency_list() + fieldname: "presentation_currency", + label: __("Currency"), + fieldtype: "Select", + options: erpnext.get_presentation_currency_list(), }, { - "fieldname": "with_period_closing_entry", - "label": __("Period Closing Entry"), - "fieldtype": "Check", - "default": 1 + fieldname: "with_period_closing_entry", + label: __("Period Closing Entry"), + fieldtype: "Check", + default: 1, }, { - "fieldname": "show_zero_values", - "label": __("Show zero values"), - "fieldtype": "Check" + fieldname: "show_zero_values", + label: __("Show zero values"), + fieldtype: "Check", }, { - "fieldname": "show_unclosed_fy_pl_balances", - "label": __("Show unclosed fiscal year's P&L balances"), - "fieldtype": "Check" + fieldname: "show_unclosed_fy_pl_balances", + label: __("Show unclosed fiscal year's P&L balances"), + fieldtype: "Check", }, { - "fieldname": "include_default_book_entries", - "label": __("Include Default FB Entries"), - "fieldtype": "Check", - "default": 1 + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, }, { - "fieldname": "show_net_values", - "label": __("Show net values in opening and closing columns"), - "fieldtype": "Check", - "default": 1 - } + fieldname: "show_net_values", + label: __("Show net values in opening and closing columns"), + fieldtype: "Check", + default: 1, + }, ], - "formatter": erpnext.financial_statements.formatter, - "tree": true, - "name_field": "account", - "parent_field": "parent_account", - "initial_depth": 3 - } + formatter: erpnext.financial_statements.formatter, + tree: true, + name_field: "account", + parent_field: "parent_account", + initial_depth: 3, + }; - erpnext.utils.add_dimensions('Trial Balance', 6); + erpnext.utils.add_dimensions("Trial Balance", 6); }); diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 8b7f0bbc006..e65f479f056 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -120,9 +120,7 @@ def get_data(filters): ignore_opening_entries=True, ) - calculate_values( - accounts, gl_entries_by_account, opening_balances, filters.get("show_net_values") - ) + calculate_values(accounts, gl_entries_by_account, opening_balances, filters.get("show_net_values")) accumulate_values_into_parents(accounts, accounts_by_name) data = prepare_data(accounts, filters, parent_children_map, company_currency) @@ -170,9 +168,7 @@ def get_rootwise_opening_balances(filters, report_type): ) # 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) - ): + if getdate(last_period_closing_voucher[0].posting_date) < getdate(add_days(filters.from_date, -1)): start_date = add_days(last_period_closing_voucher[0].posting_date, 1) gle += get_opening_balance( "GL Entry", filters, report_type, accounting_dimensions, start_date=start_date @@ -253,9 +249,7 @@ def get_opening_balance( if doctype == "Account Closing Balance": opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0) else: - opening_balance = opening_balance.where( - closing_balance.voucher_type != "Period Closing Voucher" - ) + opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher") if filters.cost_center: lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) @@ -388,7 +382,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency): "to_date": filters.to_date, "currency": company_currency, "account_name": ( - "{} - {}".format(d.account_number, d.account_name) if d.account_number else d.account_name + f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name ), } 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 33c644adcb9..50578d314e3 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 @@ -2,88 +2,88 @@ // For license information, please see license.txt frappe.query_reports["Trial Balance for Party"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname": "fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1, - "on_change": function(query_report) { + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "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; if (!fiscal_year) { return; } - frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); frappe.query_report.set_filter_value({ from_date: fy.year_start_date, - to_date: fy.year_end_date + to_date: fy.year_end_date, }); }); - } + }, }, { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], }, { - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - "default": "Customer", - "reqd": 1 + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Link", + options: "Party Type", + default: "Customer", + reqd: 1, }, { - "fieldname":"party", - "label": __("Party"), - "fieldtype": "Dynamic Link", - "get_options": function() { - var party_type = frappe.query_report.get_filter_value('party_type'); - var party = frappe.query_report.get_filter_value('party'); - if(party && !party_type) { + fieldname: "party", + label: __("Party"), + fieldtype: "Dynamic Link", + get_options: function () { + var party_type = frappe.query_report.get_filter_value("party_type"); + var party = frappe.query_report.get_filter_value("party"); + if (party && !party_type) { frappe.throw(__("Please select Party Type first")); } return party_type; - } + }, }, { - "fieldname": "account", - "label": __("Account"), - "fieldtype": "Link", - "options": "Account", - "get_query": function() { - var company = frappe.query_report.get_filter_value('company'); + fieldname: "account", + label: __("Account"), + fieldtype: "Link", + options: "Account", + get_query: function () { + var company = frappe.query_report.get_filter_value("company"); return { - "doctype": "Account", - "filters": { - "company": company, - } - } - } + doctype: "Account", + filters: { + company: company, + }, + }; + }, }, { - "fieldname": "show_zero_values", - "label": __("Show zero values"), - "fieldtype": "Check" - } - ] -} + fieldname: "show_zero_values", + label: __("Show zero values"), + fieldtype: "Check", + }, + ], +}; diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index ee223484d47..dd1a12514e2 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -22,7 +22,7 @@ def execute(filters=None): def get_data(filters, show_party_name): if filters.get("party_type") in ("Customer", "Supplier", "Employee", "Member"): - party_name_field = "{0}_name".format(frappe.scrub(filters.get("party_type"))) + party_name_field = "{}_name".format(frappe.scrub(filters.get("party_type"))) elif filters.get("party_type") == "Shareholder": party_name_field = "title" else: @@ -65,9 +65,7 @@ def get_data(filters, show_party_name): row.update({"debit": debit, "credit": credit}) # closing - closing_debit, closing_credit = toggle_debit_credit( - opening_debit + debit, opening_credit + credit - ) + closing_debit, closing_credit = toggle_debit_credit(opening_debit + debit, opening_credit + credit) row.update({"closing_debit": closing_debit, "closing_credit": closing_credit}) # totals @@ -92,13 +90,12 @@ def get_data(filters, show_party_name): def get_opening_balances(filters): - account_filter = "" if filters.get("account"): account_filter = "and account = %s" % (frappe.db.escape(filters.get("account"))) gle = frappe.db.sql( - """ + f""" select party, sum(debit) as opening_debit, sum(credit) as opening_credit from `tabGL Entry` where company=%(company)s @@ -106,9 +103,7 @@ def get_opening_balances(filters): and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) {account_filter} - group by party""".format( - account_filter=account_filter - ), + group by party""", { "company": filters.company, "from_date": filters.from_date, @@ -127,13 +122,12 @@ def get_opening_balances(filters): def get_balances_within_period(filters): - account_filter = "" if filters.get("account"): account_filter = "and account = %s" % (frappe.db.escape(filters.get("account"))) gle = frappe.db.sql( - """ + f""" select party, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` where company=%(company)s @@ -142,9 +136,7 @@ def get_balances_within_period(filters): and posting_date >= %(from_date)s and posting_date <= %(to_date)s and ifnull(is_opening, 'No') = 'No' {account_filter} - group by party""".format( - account_filter=account_filter - ), + group by party""", { "company": filters.company, "from_date": filters.from_date, diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 7ea1fac1056..3fa62b583a1 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -1,8 +1,16 @@ import frappe +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import Sum from frappe.utils import flt, formatdate, get_datetime_str, get_table_name +from pypika import Order from erpnext import get_company_currency, get_default_company +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, + get_dimension_with_children, +) from erpnext.accounts.doctype.fiscal_year.fiscal_year import get_from_and_to_date +from erpnext.accounts.party import get_party_account from erpnext.setup.utils import get_exchange_rate __exchange_rates = {} @@ -70,10 +78,10 @@ def get_rate_as_at(date, from_currency, to_currency): :return: Retrieved exchange rate """ - rate = __exchange_rates.get("{0}-{1}@{2}".format(from_currency, to_currency, date)) + rate = __exchange_rates.get(f"{from_currency}-{to_currency}@{date}") if not rate: rate = get_exchange_rate(from_currency, to_currency, date) or 1 - __exchange_rates["{0}-{1}@{2}".format(from_currency, to_currency, date)] = rate + __exchange_rates[f"{from_currency}-{to_currency}@{date}"] = rate return rate @@ -128,9 +136,7 @@ def get_appropriate_company(filters): @frappe.whitelist() -def get_invoiced_item_gross_margin( - sales_invoice=None, item_code=None, company=None, with_item_data=False -): +def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=None, with_item_data=False): from erpnext.accounts.report.gross_profit.gross_profit import GrossProfitGenerator sales_invoice = sales_invoice or frappe.form_dict.get("sales_invoice") @@ -165,7 +171,7 @@ def get_query_columns(report_columns): else: columns.append(fieldname) - return ", " + ", ".join(columns) + return columns def get_values_for_columns(report_columns, report_row): @@ -179,3 +185,206 @@ def get_values_for_columns(report_columns, report_row): values[fieldname] = report_row.get(fieldname) return values + + +def get_party_details(party_type, party_list): + party_details = {} + party = frappe.qb.DocType(party_type) + query = frappe.qb.from_(party).select(party.name, party.tax_id).where(party.name.isin(party_list)) + if party_type == "Supplier": + query = query.select(party.supplier_group) + else: + query = query.select(party.customer_group, party.territory) + + party_detail_list = query.run(as_dict=True) + for party_dict in party_detail_list: + party_details[party_dict.name] = party_dict + return party_details + + +def get_taxes_query(invoice_list, doctype, parenttype): + taxes = frappe.qb.DocType(doctype) + + query = ( + frappe.qb.from_(taxes) + .select(taxes.account_head) + .distinct() + .where( + (taxes.parenttype == parenttype) + & (taxes.docstatus == 1) + & (taxes.account_head.isnotnull()) + & (taxes.parent.isin([inv.name for inv in invoice_list])) + ) + .orderby(taxes.account_head) + ) + + if doctype == "Purchase Taxes and Charges": + return query.where(taxes.category.isin(["Total", "Valuation and Total"])) + elif doctype == "Sales Taxes and Charges": + return query + return query.where(taxes.charge_type.isin(["On Paid Amount", "Actual"])) + + +def get_journal_entries(filters, args): + je = frappe.qb.DocType("Journal Entry") + journal_account = frappe.qb.DocType("Journal Entry Account") + query = ( + frappe.qb.from_(je) + .inner_join(journal_account) + .on(je.name == journal_account.parent) + .select( + je.voucher_type.as_("doctype"), + je.name, + je.posting_date, + journal_account.account.as_(args.account), + journal_account.party.as_(args.party), + journal_account.party.as_(args.party_name), + je.bill_no, + je.bill_date, + je.remark.as_("remarks"), + je.total_amount.as_("base_net_total"), + je.total_amount.as_("base_grand_total"), + je.mode_of_payment, + journal_account.project, + ) + .where( + (je.voucher_type == "Journal Entry") + & (je.docstatus == 1) + & (journal_account.party == filters.get(args.party)) + & (journal_account.account.isin(args.party_account)) + ) + .orderby(je.posting_date, je.name, order=Order.desc) + ) + query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True) + + journal_entries = query.run(as_dict=True) + return journal_entries + + +def get_payment_entries(filters, args): + pe = frappe.qb.DocType("Payment Entry") + query = ( + frappe.qb.from_(pe) + .select( + ConstantColumn("Payment Entry").as_("doctype"), + pe.name, + pe.posting_date, + pe[args.account_fieldname].as_(args.account), + pe.party.as_(args.party), + pe.party_name.as_(args.party_name), + pe.remarks, + pe.paid_amount.as_("base_net_total"), + pe.paid_amount_after_tax.as_("base_grand_total"), + pe.mode_of_payment, + pe.project, + pe.cost_center, + ) + .where( + (pe.docstatus == 1) + & (pe.party == filters.get(args.party)) + & (pe[args.account_fieldname].isin(args.party_account)) + ) + .orderby(pe.posting_date, pe.name, order=Order.desc) + ) + query = apply_common_conditions(filters, query, doctype="Payment Entry", payments=True) + payment_entries = query.run(as_dict=True) + return payment_entries + + +def apply_common_conditions(filters, query, doctype, child_doctype=None, payments=False): + parent_doc = frappe.qb.DocType(doctype) + if child_doctype: + child_doc = frappe.qb.DocType(child_doctype) + + join_required = False + + if filters.get("company"): + query = query.where(parent_doc.company == filters.company) + if filters.get("from_date"): + query = query.where(parent_doc.posting_date >= filters.from_date) + if filters.get("to_date"): + query = query.where(parent_doc.posting_date <= filters.to_date) + + if payments: + if filters.get("cost_center"): + query = query.where(parent_doc.cost_center == filters.cost_center) + else: + if filters.get("cost_center"): + query = query.where(child_doc.cost_center == filters.cost_center) + join_required = True + if filters.get("warehouse"): + query = query.where(child_doc.warehouse == filters.warehouse) + join_required = True + if filters.get("item_group"): + query = query.where(child_doc.item_group == filters.item_group) + join_required = True + + if not payments: + if filters.get("brand"): + query = query.where(child_doc.brand == filters.brand) + join_required = True + + if join_required: + query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent) + query = query.distinct() + + if parent_doc.get_table_name() != "tabJournal Entry": + query = filter_invoices_based_on_dimensions(filters, query, parent_doc) + + return query + + +def get_advance_taxes_and_charges(invoice_list): + adv_taxes = frappe.qb.DocType("Advance Taxes and Charges") + return ( + frappe.qb.from_(adv_taxes) + .select( + adv_taxes.parent, + adv_taxes.account_head, + ( + frappe.qb.terms.Case() + .when(adv_taxes.add_deduct_tax == "Add", Sum(adv_taxes.base_tax_amount)) + .else_(Sum(adv_taxes.base_tax_amount) * -1) + ).as_("tax_amount"), + ) + .where( + (adv_taxes.parent.isin([inv.name for inv in invoice_list])) + & (adv_taxes.charge_type.isin(["On Paid Amount", "Actual"])) + & (adv_taxes.base_tax_amount != 0) + ) + .groupby(adv_taxes.parent, adv_taxes.account_head, adv_taxes.add_deduct_tax) + ).run(as_dict=True) + + +def filter_invoices_based_on_dimensions(filters, query, parent_doc): + accounting_dimensions = get_accounting_dimensions(as_list=False) + if accounting_dimensions: + for dimension in accounting_dimensions: + if filters.get(dimension.fieldname): + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) + fieldname = dimension.fieldname + query = query.where(parent_doc[fieldname].isin(filters[fieldname])) + return query + + +def get_opening_row(party_type, party, from_date, company): + party_account = get_party_account(party_type, party, company) + gle = frappe.qb.DocType("GL Entry") + return ( + frappe.qb.from_(gle) + .select( + ConstantColumn("Opening").as_("account"), + Sum(gle.debit).as_("debit"), + Sum(gle.credit).as_("credit"), + (Sum(gle.debit) - Sum(gle.credit)).as_("balance"), + ) + .where( + (gle.account == party_account) + & (gle.party == party) + & (gle.posting_date < from_date) + & (gle.is_cancelled == 0) + ) + ).run(as_dict=True) diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js index 0c148f85fb8..37c506aea2f 100644 --- a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js +++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js @@ -3,26 +3,26 @@ /* eslint-disable */ frappe.query_reports["Voucher-wise Balance"] = { - "filters": [ - { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company" - }, - { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "width": "60px" - }, - { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "width": "60px" - }, - ] + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + width: "60px", + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + width: "60px", + }, + ], }; diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py index bd9e9fccadc..1918165c6db 100644 --- a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py +++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py @@ -43,9 +43,7 @@ def get_data(filters): gle = frappe.qb.DocType("GL Entry") query = ( frappe.qb.from_(gle) - .select( - gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit") - ) + .select(gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")) .where(gle.is_cancelled == 0) .groupby(gle.voucher_no) ) diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py index 3f06c30adb6..9496a1aa664 100644 --- a/erpnext/accounts/test/test_reports.py +++ b/erpnext/accounts/test/test_reports.py @@ -1,5 +1,4 @@ import unittest -from typing import List, Tuple from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report @@ -12,7 +11,7 @@ DEFAULT_FILTERS = { } -REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ +REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}), ("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}), ("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}), diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 3d5e5fc4ec7..59cbc11794f 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -19,9 +19,13 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestUtils(unittest.TestCase): @classmethod def setUpClass(cls): - super(TestUtils, cls).setUpClass() + super().setUpClass() make_test_objects("Address", ADDRESS_RECORDS) + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + def test_get_party_shipping_address(self): address = get_party_shipping_address("Customer", "_Test Customer 1") self.assertEqual(address, "_Test Billing Address 2 Title-Billing") @@ -31,7 +35,6 @@ class TestUtils(unittest.TestCase): self.assertEqual(address, "_Test Shipping Address 2 Title-Shipping") def test_get_voucher_wise_gl_entry(self): - pr = make_purchase_receipt( item_code="_Test Item", posting_date="2021-02-01", @@ -125,6 +128,34 @@ class TestUtils(unittest.TestCase): self.assertEqual(len(payment_entry.references), 1) self.assertEqual(payment_entry.difference_amount, 0) + def test_naming_series_variable_parsing(self): + """ + Tests parsing utility used by Naming Series Variable hook for FY + """ + from frappe.custom.doctype.property_setter.property_setter import make_property_setter + from frappe.utils import nowdate + + from erpnext.accounts.utils import get_fiscal_year + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + # Configure Supplier Naming in Buying Settings + frappe.db.set_default("supp_master_name", "Auto Name") + + # Configure Autoname in Supplier DocType + make_property_setter("Supplier", None, "naming_rule", "Expression", "Data", for_doctype="Doctype") + make_property_setter("Supplier", None, "autoname", "SUP-.FY.-.#####", "Data", for_doctype="Doctype") + + fiscal_year = get_fiscal_year(nowdate())[0] + + # Create Supplier + supplier = create_supplier() + + # Check Naming Series in generated Supplier ID + doc_name = supplier.name.split("-") + self.assertEqual(len(doc_name), 3) + self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year)) + frappe.db.set_default("supp_master_name", "Supplier Name") + ADDRESS_RECORDS = [ { @@ -134,9 +165,7 @@ ADDRESS_RECORDS = [ "address_title": "_Test Billing Address Title", "city": "Lagos", "country": "Nigeria", - "links": [ - {"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"} - ], + "links": [{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}], }, { "doctype": "Address", @@ -145,9 +174,7 @@ ADDRESS_RECORDS = [ "address_title": "_Test Shipping Address 1 Title", "city": "Lagos", "country": "Nigeria", - "links": [ - {"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"} - ], + "links": [{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}], }, { "doctype": "Address", @@ -157,9 +184,7 @@ ADDRESS_RECORDS = [ "city": "Lagos", "country": "Nigeria", "is_shipping_address": "1", - "links": [ - {"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"} - ], + "links": [{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}], }, { "doctype": "Address", @@ -169,8 +194,6 @@ ADDRESS_RECORDS = [ "city": "Lagos", "country": "Nigeria", "is_shipping_address": "1", - "links": [ - {"link_doctype": "Customer", "link_name": "_Test Customer 1", "doctype": "Dynamic Link"} - ], + "links": [{"link_doctype": "Customer", "link_name": "_Test Customer 1", "doctype": "Dynamic Link"}], }, ] diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 52b0c34673a..d966074b2ae 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -3,21 +3,23 @@ from json import loads -from typing import TYPE_CHECKING, List, Optional, Tuple +from typing import TYPE_CHECKING, Optional import frappe import frappe.defaults from frappe import _, qb, throw from frappe.model.meta import get_field_precision from frappe.query_builder import AliasedQuery, Criterion, Table -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Round, Sum from frappe.query_builder.utils import DocType from frappe.utils import ( + add_days, cint, create_batch, cstr, flt, formatdate, + get_datetime, get_number_format_info, getdate, now, @@ -29,7 +31,7 @@ from pypika.terms import ExistsCriterion import erpnext # imported to enable erpnext.accounts.utils.get_account_currency -from erpnext.accounts.doctype.account.account import get_account_currency # noqa +from erpnext.accounts.doctype.account.account import get_account_currency from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.stock import get_warehouse_account_map from erpnext.stock.utils import get_stock_value_on @@ -78,9 +80,7 @@ def get_fiscal_years( FY = DocType("Fiscal Year") query = ( - frappe.qb.from_(FY) - .select(FY.name, FY.year_start_date, FY.year_end_date) - .where(FY.disabled == 0) + frappe.qb.from_(FY).select(FY.name, FY.year_start_date, FY.year_end_date).where(FY.disabled == 0) ) if fiscal_year: @@ -127,9 +127,7 @@ def get_fiscal_years( else: return ((fy.name, fy.year_start_date, fy.year_end_date),) - error_msg = _("""{0} {1} is not in any active Fiscal Year""").format( - label, formatdate(transaction_date) - ) + error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date)) if company: error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company)) @@ -205,12 +203,12 @@ def get_balance_on( acc = frappe.get_doc("Account", account) try: - year_start_date = get_fiscal_year(date, company=company, verbose=0)[1] + get_fiscal_year(date, company=company, verbose=0)[1] except FiscalYearError: if getdate(date) > getdate(nowdate()): # if fiscal year not found and the date is greater than today # get fiscal year for today's date and its corresponding year start date - year_start_date = get_fiscal_year(nowdate(), verbose=1)[1] + get_fiscal_year(nowdate(), verbose=1)[1] else: # this indicates that it is a date older than any existing fiscal year. # hence, assuming balance as 0.0 @@ -225,29 +223,26 @@ def get_balance_on( cc = frappe.get_doc("Cost Center", cost_center) if cc.is_group: cond.append( - """ exists ( + f""" exists ( select 1 from `tabCost Center` cc where cc.name = gle.cost_center - and cc.lft >= %s and cc.rgt <= %s + and cc.lft >= {cc.lft} and cc.rgt <= {cc.rgt} )""" - % (cc.lft, cc.rgt) ) else: - cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),)) + cond.append(f"""gle.cost_center = {frappe.db.escape(cost_center, percent=False)} """) if account: - if not (frappe.flags.ignore_account_permission or ignore_account_permission): acc.check_permission("read") # different filter for group and ledger - improved performance if acc.is_group: cond.append( - """exists ( + f"""exists ( select name from `tabAccount` ac where ac.name = gle.account - and ac.lft >= %s and ac.rgt <= %s + and ac.lft >= {acc.lft} and ac.rgt <= {acc.rgt} )""" - % (acc.lft, acc.rgt) ) # If group and currency same as company, @@ -255,12 +250,11 @@ def get_balance_on( if acc.account_currency == frappe.get_cached_value("Company", acc.company, "default_currency"): in_account_currency = False else: - cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),)) + cond.append(f"""gle.account = {frappe.db.escape(account, percent=False)} """) if party_type and party: cond.append( - """gle.party_type = %s and gle.party = %s """ - % (frappe.db.escape(party_type), frappe.db.escape(party, percent=False)) + f"""gle.party_type = {frappe.db.escape(party_type)} and gle.party = {frappe.db.escape(party, percent=False)} """ ) if company: @@ -273,11 +267,9 @@ def get_balance_on( select_field = "sum(debit) - sum(credit)" bal = frappe.db.sql( """ - SELECT {0} + SELECT {} FROM `tabGL Entry` gle - WHERE {1}""".format( - select_field, " and ".join(cond) - ) + WHERE {}""".format(select_field, " and ".join(cond)) )[0][0] # if bal is None, return 0 @@ -312,30 +304,25 @@ def get_count_on(account, fieldname, date): # for pl accounts, get balance within a fiscal year if acc.report_type == "Profit and Loss": - cond.append( - "posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date - ) + cond.append("posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date) # different filter for group and ledger - improved performance if acc.is_group: cond.append( - """exists ( + f"""exists ( select name from `tabAccount` ac where ac.name = gle.account - and ac.lft >= %s and ac.rgt <= %s + and ac.lft >= {acc.lft} and ac.rgt <= {acc.rgt} )""" - % (acc.lft, acc.rgt) ) else: - cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),)) + cond.append(f"""gle.account = {frappe.db.escape(account, percent=False)} """) entries = frappe.db.sql( """ SELECT name, posting_date, account, party_type, party,debit,credit, voucher_type, voucher_no, against_voucher_type, against_voucher FROM `tabGL Entry` gle - WHERE {0}""".format( - " and ".join(cond) - ), + WHERE {}""".format(" and ".join(cond)), as_dict=True, ) @@ -358,13 +345,11 @@ def get_count_on(account, fieldname, date): or (gle.against_voucher == gle.voucher_no and gle.get(dr_or_cr) > 0) ): payment_amount = frappe.db.sql( - """ - SELECT {0} + f""" + SELECT {select_fields} FROM `tabGL Entry` gle WHERE docstatus < 2 and posting_date <= %(date)s and against_voucher = %(voucher_no)s - and party = %(party)s and name != %(name)s""".format( - select_fields - ), + and party = %(party)s and name != %(name)s""", {"date": date, "voucher_no": gle.voucher_no, "party": gle.party, "name": gle.name}, )[0][0] @@ -419,7 +404,7 @@ def add_cc(args=None): args = make_tree_args(**args) if args.parent_cost_center == args.company: - args.parent_cost_center = "{0} - {1}".format( + args.parent_cost_center = "{} - {}".format( args.parent_cost_center, frappe.get_cached_value("Company", args.company, "abbr") ) @@ -434,7 +419,19 @@ def add_cc(args=None): return cc.name -def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep +def _build_dimensions_dict_for_exc_gain_loss( + entry: dict | object = None, active_dimensions: list | None = None +): + dimensions_dict = frappe._dict() + if entry and active_dimensions: + for dim in active_dimensions: + dimensions_dict[dim.fieldname] = entry.get(dim.fieldname) + return dimensions_dict + + +def reconcile_against_document( + args, skip_ref_details_update_for_pe=False, active_dimensions=None +): # nosemgrep """ Cancel PE or JV, Update against document, split if required and resubmit """ @@ -459,6 +456,8 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n check_if_advance_entry_modified(entry) validate_allocated_amount(entry) + dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions) + # update ref in advance entry if voucher_type == "Journal Entry": referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False) @@ -466,10 +465,14 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n # amount and account in args # referenced_row is used to deduplicate gain/loss journal entry.update({"referenced_row": referenced_row}) - doc.make_exchange_gain_loss_journal([entry]) + doc.make_exchange_gain_loss_journal([entry], dimensions_dict) else: - update_reference_in_payment_entry( - entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe + referenced_row = update_reference_in_payment_entry( + entry, + doc, + do_not_save=True, + skip_ref_details_update_for_pe=skip_ref_details_update_for_pe, + dimensions_dict=dimensions_dict, ) doc.save(ignore_permissions=True) @@ -481,7 +484,11 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n # Only update outstanding for newly linked vouchers for entry in entries: update_voucher_outstanding( - entry.against_voucher_type, entry.against_voucher, entry.account, entry.party_type, entry.party + entry.against_voucher_type, + entry.against_voucher, + entry.account, + entry.party_type, + entry.party, ) frappe.flags.ignore_party_validation = False @@ -505,9 +512,7 @@ def check_if_advance_entry_modified(args): and t2.party_type = %(party_type)s and t2.party = %(party)s and (t2.reference_type is null or t2.reference_type in ('', 'Sales Order', 'Purchase Order')) and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s - and t1.docstatus=1 """.format( - dr_or_cr=args.get("dr_or_cr") - ), + and t1.docstatus=1 """.format(dr_or_cr=args.get("dr_or_cr")), args, ) else: @@ -522,25 +527,26 @@ def check_if_advance_entry_modified(args): where t1.name = t2.parent and t1.docstatus = 1 and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s - and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{0} = %(account)s + and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{} = %(account)s and t2.reference_doctype in ('', 'Sales Order', 'Purchase Order') and t2.allocated_amount = %(unreconciled_amount)s - """.format( - party_account_field - ), + """.format(party_account_field), args, ) else: - ret = frappe.db.sql( - """select name from `tabPayment Entry` - where - name = %(voucher_no)s and docstatus = 1 - and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s - and round(unallocated_amount, {1}) = round(%(unreconciled_amount)s, {1}) - """.format( - party_account_field, precision - ), - args, + pe = qb.DocType("Payment Entry") + ret = ( + qb.from_(pe) + .select(pe.name) + .where( + (pe.name == args.voucher_no) + & (pe.docstatus == 1) + & (pe.party_type == args.party_type) + & (pe.party == args.party) + & (pe[party_account_field] == args.account) + & (Round(pe.unallocated_amount, precision) == Round(args.unreconciled_amount, precision)) + ) + .run() ) if not ret: @@ -548,9 +554,7 @@ def check_if_advance_entry_modified(args): def validate_allocated_amount(args): - precision = args.get("precision") or frappe.db.get_single_value( - "System Settings", "currency_precision" - ) + precision = args.get("precision") or frappe.db.get_single_value("System Settings", "currency_precision") if args.get("allocated_amount") < 0: throw(_("Allocated amount cannot be negative")) elif flt(args.get("allocated_amount"), precision) > flt(args.get("unadjusted_amount"), precision): @@ -618,7 +622,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): def update_reference_in_payment_entry( - d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False + d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False, dimensions_dict=None ): reference_details = { "reference_doctype": d.against_voucher_type, @@ -630,6 +634,8 @@ def update_reference_in_payment_entry( if d.difference_amount is not None else payment_entry.get_exchange_rate(), "exchange_gain_loss": d.difference_amount, + "account": d.account, + "dimensions": d.dimensions, } if d.voucher_detail_no: @@ -659,10 +665,23 @@ def update_reference_in_payment_entry( payment_entry.setup_party_account_field() payment_entry.set_missing_values() if not skip_ref_details_update_for_pe: - payment_entry.set_missing_ref_details() + reference_exchange_details = frappe._dict() + if d.against_voucher_type == "Journal Entry" and d.exchange_rate: + reference_exchange_details.update( + { + "reference_doctype": d.against_voucher_type, + "reference_name": d.against_voucher, + "exchange_rate": d.exchange_rate, + } + ) + payment_entry.set_missing_ref_details( + update_ref_details_only_for=[(d.against_voucher_type, d.against_voucher)], + reference_exchange_details=reference_exchange_details, + ) payment_entry.set_amounts() + payment_entry.make_exchange_gain_loss_journal( - frappe._dict({"difference_posting_date": d.difference_posting_date}) + frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict ) if not do_not_save: @@ -670,7 +689,7 @@ def update_reference_in_payment_entry( def cancel_exchange_gain_loss_journal( - parent_doc: dict | object, referenced_dt: str = None, referenced_dn: str = None + parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None ) -> None: """ Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. @@ -713,7 +732,7 @@ def cancel_exchange_gain_loss_journal( def update_accounting_ledgers_after_reference_removal( - ref_type: str = None, ref_no: str = None, payment_name: str = None + ref_type: str | None = None, ref_no: str | None = None, payment_name: str | None = None ): # General Ledger gle = qb.DocType("GL Entry") @@ -739,9 +758,7 @@ def update_accounting_ledgers_after_reference_removal( .set(ple.modified, now()) .set(ple.modified_by, frappe.session.user) .where( - (ple.against_voucher_type == ref_type) - & (ple.against_voucher_no == ref_no) - & (ple.delinked == 0) + (ple.against_voucher_type == ref_type) & (ple.against_voucher_no == ref_no) & (ple.delinked == 0) ) ) @@ -758,7 +775,7 @@ def remove_ref_from_advance_section(ref_doc: object = None): qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run() -def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str = None): +def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str | None = None): remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name) remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name) update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name) @@ -766,7 +783,7 @@ def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: st def remove_ref_doc_link_from_jv( - ref_type: str = None, ref_no: str = None, payment_name: str = None + ref_type: str | None = None, ref_no: str | None = None, payment_name: str | None = None ): jea = qb.DocType("Journal Entry Account") @@ -806,7 +823,7 @@ def convert_to_list(result): def remove_ref_doc_link_from_pe( - ref_type: str = None, ref_no: str = None, payment_name: str = None + ref_type: str | None = None, ref_no: str | None = None, payment_name: str | None = None ): per = qb.DocType("Payment Entry Reference") pay = qb.DocType("Payment Entry") @@ -814,9 +831,7 @@ def remove_ref_doc_link_from_pe( linked_pe = ( qb.from_(per) .select(per.parent) - .where( - (per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2)) - ) + .where((per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2))) .run(as_list=1) ) linked_pe = convert_to_list(linked_pe) @@ -829,9 +844,7 @@ def remove_ref_doc_link_from_pe( .set(per.allocated_amount, 0) .set(per.modified, now()) .set(per.modified_by, frappe.session.user) - .where( - (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) - ) + .where(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) ) if payment_name: @@ -845,7 +858,7 @@ def remove_ref_doc_link_from_pe( pe_doc.set_amounts() pe_doc.clear_unallocated_reference_document_rows() pe_doc.validate_payment_type_with_outstanding() - except Exception as e: + except Exception: msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name) msg += "
    " msg += _("Please cancel payment entry manually first") @@ -855,9 +868,7 @@ def remove_ref_doc_link_from_pe( pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount ).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set( pay.modified_by, frappe.session.user - ).where( - pay.name == pe - ).run() + ).where(pay.name == pe).run() frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) @@ -891,9 +902,10 @@ def fix_total_debit_credit(): dr_or_cr = d.voucher_type == "Sales Invoice" and "credit" or "debit" frappe.db.sql( - """update `tabGL Entry` set %s = %s + %s - where voucher_type = %s and voucher_no = %s and %s > 0 limit 1""" - % (dr_or_cr, dr_or_cr, "%s", "%s", "%s", dr_or_cr), + """update `tabGL Entry` set {} = {} + {} + where voucher_type = {} and voucher_no = {} and {} > 0 limit 1""".format( + dr_or_cr, dr_or_cr, "%s", "%s", "%s", dr_or_cr + ), (d.diff, d.voucher_type, d.voucher_no), ) @@ -918,20 +930,22 @@ def get_stock_rbnb_difference(posting_date, company): """ select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor) from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr - where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s - and pr.posting_date <= %s and pr_item.item_code in (%s)""" - % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), - tuple([company, posting_date] + stock_items), + where pr.name = pr_item.parent and pr.docstatus=1 and pr.company={} + and pr.posting_date <= {} and pr_item.item_code in ({})""".format( + "%s", "%s", ", ".join(["%s"] * len(stock_items)) + ), + tuple([company, posting_date, *stock_items]), )[0][0] pi_valuation_amount = frappe.db.sql( """ select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor) from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi - where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s - and pi.posting_date <= %s and pi_item.item_code in (%s)""" - % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), - tuple([company, posting_date] + stock_items), + where pi.name = pi_item.parent and pi.docstatus=1 and pi.company={} + and pi.posting_date <= {} and pi_item.item_code in ({})""".format( + "%s", "%s", ", ".join(["%s"] * len(stock_items)) + ), + tuple([company, posting_date, *stock_items]), )[0][0] # Balance should be @@ -976,15 +990,12 @@ def get_outstanding_invoices( limit=None, # passed by reconciliation tool voucher_no=None, # filter passed by reconciliation tool ): - ple = qb.DocType("Payment Ledger Entry") outstanding_invoices = [] precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2 if account: - root_type, account_type = frappe.get_cached_value( - "Account", account, ["root_type", "account_type"] - ) + root_type, account_type = frappe.get_cached_value("Account", account, ["root_type", "account_type"]) party_account_type = "Receivable" if root_type == "Asset" else "Payable" party_account_type = account_type or party_account_type else: @@ -1038,15 +1049,11 @@ def get_outstanding_invoices( ) ) - outstanding_invoices = sorted( - outstanding_invoices, key=lambda k: k["due_date"] or getdate(nowdate()) - ) + outstanding_invoices = sorted(outstanding_invoices, key=lambda k: k["due_date"] or getdate(nowdate())) return outstanding_invoices -def get_account_name( - account_type=None, root_type=None, is_group=None, account_currency=None, company=None -): +def get_account_name(account_type=None, root_type=None, is_group=None, account_currency=None, company=None): """return account based on matching conditions""" return frappe.db.get_value( "Account", @@ -1075,7 +1082,7 @@ def get_children(doctype, parent, company, is_root=False): fields = ["name as value", "is_group as expandable"] filters = [["docstatus", "<", 2]] - filters.append(['ifnull(`{0}`,"")'.format(parent_fieldname), "=", "" if is_root else parent]) + filters.append([f'ifnull(`{parent_fieldname}`,"")', "=", "" if is_root else parent]) if is_root: fields += ["root_type", "report_type", "account_currency"] if doctype == "Account" else [] @@ -1095,7 +1102,6 @@ def get_children(doctype, parent, company, is_root=False): @frappe.whitelist() def get_account_balances(accounts, company): - if isinstance(accounts, str): accounts = loads(accounts) @@ -1106,9 +1112,7 @@ def get_account_balances(accounts, company): for account in accounts: account["company_currency"] = company_currency - account["balance"] = flt( - get_balance_on(account["value"], in_account_currency=False, company=company) - ) + account["balance"] = flt(get_balance_on(account["value"], in_account_currency=False, company=company)) if account["account_currency"] and account["account_currency"] != company_currency: account["balance_in_account_currency"] = flt(get_balance_on(account["value"], company=company)) @@ -1222,8 +1226,13 @@ def get_autoname_with_number(number_value, doc_title, company): 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] + if doc: + date = doc.get("posting_date") or doc.get("transaction_date") or getdate() + company = doc.get("company") + else: + date = getdate() + company = None + return get_fiscal_year(date=date, company=company)[0] @frappe.whitelist() @@ -1253,20 +1262,17 @@ def update_gl_entries_after( warehouse_account=None, company=None, ): - stock_vouchers = get_future_stock_vouchers( - posting_date, posting_time, for_warehouses, for_items, company - ) + stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items, company) repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company, warehouse_account) def repost_gle_for_stock_vouchers( - stock_vouchers: List[Tuple[str, str]], + stock_vouchers: list[tuple[str, str]], posting_date: str, - company: Optional[str] = None, + company: str | None = None, warehouse_account=None, repost_doc: Optional["RepostItemValuation"] = None, ): - from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative if not stock_vouchers: @@ -1312,16 +1318,12 @@ def repost_gle_for_stock_vouchers( def _delete_pl_entries(voucher_type, voucher_no): ple = qb.DocType("Payment Ledger Entry") - qb.from_(ple).delete().where( - (ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no) - ).run() + qb.from_(ple).delete().where((ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no)).run() def _delete_gl_entries(voucher_type, voucher_no): gle = qb.DocType("GL Entry") - qb.from_(gle).delete().where( - (gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no) - ).run() + qb.from_(gle).delete().where((gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no)).run() def _delete_accounting_ledger_entries(voucher_type, voucher_no): @@ -1332,9 +1334,7 @@ def _delete_accounting_ledger_entries(voucher_type, voucher_no): _delete_pl_entries(voucher_type, voucher_no) -def sort_stock_vouchers_by_posting_date( - stock_vouchers: List[Tuple[str, str]] -) -> List[Tuple[str, str]]: +def sort_stock_vouchers_by_posting_date(stock_vouchers: list[tuple[str, str]]) -> list[tuple[str, str]]: sle = frappe.qb.DocType("Stock Ledger Entry") voucher_nos = [v[1] for v in stock_vouchers] @@ -1343,8 +1343,7 @@ def sort_stock_vouchers_by_posting_date( .select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation) .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos))) .groupby(sle.voucher_type, sle.voucher_no) - .orderby(sle.posting_date) - .orderby(sle.posting_time) + .orderby(sle.posting_datetime) .orderby(sle.creation) ).run(as_dict=True) sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] @@ -1356,10 +1355,7 @@ def sort_stock_vouchers_by_posting_date( return sorted_vouchers -def get_future_stock_vouchers( - posting_date, posting_time, for_warehouses=None, for_items=None, company=None -): - +def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None): values = [] condition = "" if for_items: @@ -1375,16 +1371,14 @@ def get_future_stock_vouchers( values.append(company) future_stock_vouchers = frappe.db.sql( - """select distinct sle.voucher_type, sle.voucher_no + f"""select distinct sle.voucher_type, sle.voucher_no from `tabStock Ledger Entry` sle where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) and is_cancelled = 0 {condition} - order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format( - condition=condition - ), - tuple([posting_date, posting_time] + values), + order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""", + tuple([posting_date, posting_time, *values]), as_dict=True, ) @@ -1411,9 +1405,8 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): select name, account, credit, debit, cost_center, project, voucher_type, voucher_no from `tabGL Entry` where - posting_date >= %s and voucher_no in (%s)""" - % ("%s", ", ".join(["%s"] * len(voucher_nos))), - tuple([posting_date] + voucher_nos), + posting_date >= {} and voucher_no in ({})""".format("%s", ", ".join(["%s"] * len(voucher_nos))), + tuple([posting_date, *voucher_nos]), as_dict=1, ) @@ -1452,16 +1445,16 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): def get_stock_accounts(company, voucher_type=None, voucher_no=None): stock_accounts = [ d.name - for d in frappe.db.get_all( - "Account", {"account_type": "Stock", "company": company, "is_group": 0} - ) + for d in frappe.db.get_all("Account", {"account_type": "Stock", "company": company, "is_group": 0}) ] if voucher_type and voucher_no: if voucher_type == "Journal Entry": stock_accounts = [ d.account for d in frappe.db.get_all( - "Journal Entry Account", {"parent": voucher_no, "account": ["in", stock_accounts]}, "account" + "Journal Entry Account", + {"parent": voucher_no, "account": ["in", stock_accounts]}, + "account", ) ] @@ -1470,7 +1463,11 @@ def get_stock_accounts(company, voucher_type=None, voucher_no=None): d.account for d in frappe.db.get_all( "GL Entry", - {"voucher_type": voucher_type, "voucher_no": voucher_no, "account": ["in", stock_accounts]}, + { + "voucher_type": voucher_type, + "voucher_no": voucher_no, + "account": ["in", stock_accounts], + }, "account", ) ] @@ -1501,9 +1498,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None) def get_journal_entry(account, stock_adjustment_account, amount): - db_or_cr_warehouse_account = ( - "credit_in_account_currency" if amount < 0 else "debit_in_account_currency" - ) + db_or_cr_warehouse_account = "credit_in_account_currency" if amount < 0 else "debit_in_account_currency" db_or_cr_stock_adjustment_account = ( "debit_in_account_currency" if amount < 0 else "credit_in_account_currency" ) @@ -1524,7 +1519,7 @@ def check_and_delete_linked_reports(report): frappe.delete_doc("Desktop Icon", icon) -def create_err_and_its_journals(companies: list = None) -> None: +def create_err_and_its_journals(companies: list | None = None) -> None: if companies: for company in companies: err = frappe.new_doc("Exchange Rate Revaluation") @@ -1581,9 +1576,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0): accounts_with_types = ( qb.from_(account) .select(account.name, account.account_type) - .where( - (account.account_type.isin(["Receivable", "Payable"]) & (account.company.isin(companies))) - ) + .where(account.account_type.isin(["Receivable", "Payable"]) & (account.company.isin(companies))) .run(as_dict=True) ) receivable_or_payable_accounts = [y.name for y in accounts_with_types] @@ -1650,7 +1643,6 @@ def create_payment_ledger_entry( ple_map = get_payment_ledger_entries(gl_entries, cancel=cancel) for entry in ple_map: - ple = frappe.get_doc(entry) if cancel: @@ -1728,7 +1720,7 @@ def delink_original_entry(pl_entry, partial_cancel=False): query.run() -class QueryPaymentLedger(object): +class QueryPaymentLedger: """ Helper Class for Querying Payment Ledger Entry """ @@ -1896,7 +1888,8 @@ class QueryPaymentLedger(object): Table("outstanding").amount_in_account_currency.as_("outstanding_in_account_currency"), (Table("vouchers").amount - Table("outstanding").amount).as_("paid_amount"), ( - Table("vouchers").amount_in_account_currency - Table("outstanding").amount_in_account_currency + Table("vouchers").amount_in_account_currency + - Table("outstanding").amount_in_account_currency ).as_("paid_amount_in_account_currency"), Table("vouchers").due_date, Table("vouchers").currency, @@ -1986,6 +1979,7 @@ def create_gain_loss_journal( ref2_dn, ref2_detail_no, cost_center, + dimensions, ) -> str: journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" @@ -2019,7 +2013,8 @@ def create_gain_loss_journal( dr_or_cr + "_in_account_currency": 0, } ) - + if dimensions: + journal_account.update(dimensions) journal_entry.append("accounts", journal_account) journal_account = frappe._dict( @@ -2035,9 +2030,51 @@ def create_gain_loss_journal( reverse_dr_or_cr: abs(exc_gain_loss), } ) - + if dimensions: + journal_account.update(dimensions) journal_entry.append("accounts", journal_account) journal_entry.save() journal_entry.submit() return journal_entry.name + + +def run_ledger_health_checks(): + health_monitor_settings = frappe.get_doc("Ledger Health Monitor") + if health_monitor_settings.enable_health_monitor: + period_end = getdate() + period_start = add_days(period_end, -abs(health_monitor_settings.monitor_for_last_x_days)) + + run_date = get_datetime() + + # Debit-Credit mismatch report + if health_monitor_settings.debit_credit_mismatch: + for x in health_monitor_settings.companies: + filters = {"company": x.company, "from_date": period_start, "to_date": period_end} + voucher_wise = frappe.get_doc("Report", "Voucher-wise Balance") + res = voucher_wise.execute_script_report(filters=filters) + for x in res[1]: + doc = frappe.new_doc("Ledger Health") + doc.voucher_type = x.voucher_type + doc.voucher_no = x.voucher_no + doc.debit_credit_mismatch = True + doc.checked_on = run_date + doc.save() + + # General Ledger and Payment Ledger discrepancy + if health_monitor_settings.general_and_payment_ledger_mismatch: + for x in health_monitor_settings.companies: + filters = { + "company": x.company, + "period_start_date": period_start, + "period_end_date": period_end, + } + gl_pl_comparison = frappe.get_doc("Report", "General and Payment Ledger Comparison") + res = gl_pl_comparison.execute_script_report(filters=filters) + for x in res[1]: + doc = frappe.new_doc("Ledger Health") + doc.voucher_type = x.voucher_type + doc.voucher_no = x.voucher_no + doc.general_and_payment_ledger_mismatch = True + doc.checked_on = run_date + doc.save() diff --git a/erpnext/assets/dashboard_fixtures.py b/erpnext/assets/dashboard_fixtures.py index fc9ba386a38..3b1d14440cf 100644 --- a/erpnext/assets/dashboard_fixtures.py +++ b/erpnext/assets/dashboard_fixtures.py @@ -12,7 +12,6 @@ from erpnext.buying.dashboard_fixtures import get_company_for_dashboards def get_data(): - fiscal_year = _get_fiscal_year(nowdate()) if not fiscal_year: @@ -168,9 +167,7 @@ def get_number_cards(fiscal_year, year_start_date, year_end_date): "is_public": 1, "show_percentage_stats": 1, "stats_time_interval": "Monthly", - "filters_json": json.dumps( - [["Asset", "creation", "between", [year_start_date, year_end_date]]] - ), + "filters_json": json.dumps([["Asset", "creation", "between", [year_start_date, year_end_date]]]), "doctype": "Number Card", }, { diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 37fbd184fe1..49c0b24e4cb 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -4,135 +4,174 @@ frappe.provide("erpnext.asset"); frappe.provide("erpnext.accounts.dimensions"); -frappe.ui.form.on('Asset', { - onload: function(frm) { - frm.set_query("item_code", function() { +frappe.ui.form.on("Asset", { + onload: function (frm) { + frm.set_query("item_code", function () { return { - "filters": { - "is_fixed_asset": 1, - "is_stock_item": 0 - } + filters: { + is_fixed_asset: 1, + is_stock_item: 0, + }, }; }); - frm.set_query("warehouse", function() { + frm.set_query("warehouse", function () { return { - "filters": { - "company": frm.doc.company, - "is_group": 0 - } + filters: { + company: frm.doc.company, + is_group: 0, + }, }; }); - frm.set_query("department", function() { + frm.set_query("department", function () { return { - "filters": { - "company": frm.doc.company, - } + filters: { + company: frm.doc.company, + }, }; }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, - company: function(frm) { + company: function (frm) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, - setup: function(frm) { + setup: function (frm) { frm.make_methods = { - 'Asset Movement': () => { + "Asset Movement": () => { frappe.call({ - method: "erpnext.assets.doctype.asset.asset.make_asset_movement", - freeze: true, - args:{ - "assets": [{ name: cur_frm.doc.name }] - }, - callback: function (r) { - if (r.message) { - var doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name); - } - } - }); + method: "erpnext.assets.doctype.asset.asset.make_asset_movement", + freeze: true, + args: { + assets: [{ name: cur_frm.doc.name }], + }, + callback: function (r) { + if (r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }, + }); }, - } + }; frm.set_query("purchase_receipt", (doc) => { return { query: "erpnext.controllers.queries.get_purchase_receipts", - filters: { item_code: doc.item_code } - } + filters: { item_code: doc.item_code }, + }; }); frm.set_query("purchase_invoice", (doc) => { return { query: "erpnext.controllers.queries.get_purchase_invoices", - filters: { item_code: doc.item_code } - } + filters: { item_code: doc.item_code }, + }; }); }, - refresh: function(frm) { + refresh: function (frm) { frappe.ui.form.trigger("Asset", "is_existing_asset"); frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); frm.events.make_schedules_editable(frm); - if (frm.doc.docstatus==1) { + if (frm.doc.docstatus == 1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { - frm.add_custom_button(__("Transfer Asset"), function() { - erpnext.asset.transfer_asset(frm); - }, __("Manage")); + frm.add_custom_button( + __("Transfer Asset"), + function () { + erpnext.asset.transfer_asset(frm); + }, + __("Manage") + ); - frm.add_custom_button(__("Scrap Asset"), function() { - erpnext.asset.scrap_asset(frm); - }, __("Manage")); + frm.add_custom_button( + __("Scrap Asset"), + function () { + erpnext.asset.scrap_asset(frm); + }, + __("Manage") + ); - frm.add_custom_button(__("Sell Asset"), function() { - frm.trigger("make_sales_invoice"); - }, __("Manage")); - - } else if (frm.doc.status=='Scrapped') { - frm.add_custom_button(__("Restore Asset"), function() { - erpnext.asset.restore_asset(frm); - }, __("Manage")); + frm.add_custom_button( + __("Sell Asset"), + function () { + frm.trigger("make_sales_invoice"); + }, + __("Manage") + ); + } else if (frm.doc.status == "Scrapped") { + frm.add_custom_button( + __("Restore Asset"), + function () { + erpnext.asset.restore_asset(frm); + }, + __("Manage") + ); } if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { - frm.add_custom_button(__("Maintain Asset"), function() { - frm.trigger("create_asset_maintenance"); - }, __("Manage")); + frm.add_custom_button( + __("Maintain Asset"), + function () { + frm.trigger("create_asset_maintenance"); + }, + __("Manage") + ); } - frm.add_custom_button(__("Repair Asset"), function() { - frm.trigger("create_asset_repair"); - }, __("Manage")); + frm.add_custom_button( + __("Repair Asset"), + function () { + frm.trigger("create_asset_repair"); + }, + __("Manage") + ); - frm.add_custom_button(__("Split Asset"), function() { - frm.trigger("split_asset"); - }, __("Manage")); + frm.add_custom_button( + __("Split Asset"), + function () { + frm.trigger("split_asset"); + }, + __("Manage") + ); - if (frm.doc.status != 'Fully Depreciated') { - frm.add_custom_button(__("Adjust Asset Value"), function() { - frm.trigger("create_asset_value_adjustment"); - }, __("Manage")); + if (frm.doc.status != "Fully Depreciated") { + frm.add_custom_button( + __("Adjust Asset Value"), + function () { + frm.trigger("create_asset_value_adjustment"); + }, + __("Manage") + ); } if (!frm.doc.calculate_depreciation) { - frm.add_custom_button(__("Create Depreciation Entry"), function() { - frm.trigger("make_journal_entry"); - }, __("Manage")); + frm.add_custom_button( + __("Create Depreciation Entry"), + function () { + frm.trigger("make_journal_entry"); + }, + __("Manage") + ); } if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button(__("View General Ledger"), function() { - frappe.route_options = { - "voucher_no": frm.doc.name, - "from_date": frm.doc.available_for_use_date, - "to_date": frm.doc.available_for_use_date, - "company": frm.doc.company - }; - frappe.set_route("query-report", "General Ledger"); - }, __("Manage")); + frm.add_custom_button( + __("View General Ledger"), + function () { + frappe.route_options = { + voucher_no: frm.doc.name, + from_date: frm.doc.available_for_use_date, + to_date: frm.doc.available_for_use_date, + company: frm.doc.company, + }; + frappe.set_route("query-report", "General Ledger"); + }, + __("Manage") + ); } if (frm.doc.depr_entry_posting_status === "Failed") { @@ -148,10 +187,10 @@ frappe.ui.form.on('Asset', { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); if (frm.doc.is_composite_asset && !frm.doc.capitalized_in) { - $('.primary-action').prop('hidden', true); - $('.form-message').text('Capitalize this asset to confirm'); + $(".primary-action").prop("hidden", true); + $(".form-message").text("Capitalize this asset to confirm"); - frm.add_custom_button(__("Capitalize Asset"), function() { + frm.add_custom_button(__("Capitalize Asset"), function () { frm.trigger("create_asset_capitalization"); }); } @@ -171,70 +210,74 @@ frappe.ui.form.on('Asset', { frm.dashboard.set_headline_alert(alert); }, - toggle_reference_doc: function(frm) { + toggle_reference_doc: function (frm) { if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) { - frm.set_df_property('purchase_invoice', 'read_only', 1); - frm.set_df_property('purchase_receipt', 'read_only', 1); - } - else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) { - frm.toggle_reqd('purchase_receipt', 0); - frm.toggle_reqd('purchase_invoice', 0); - } - else if (frm.doc.purchase_receipt) { + frm.set_df_property("purchase_invoice", "read_only", 1); + frm.set_df_property("purchase_receipt", "read_only", 1); + } else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) { + frm.toggle_reqd("purchase_receipt", 0); + frm.toggle_reqd("purchase_invoice", 0); + } else if (frm.doc.purchase_receipt) { // if purchase receipt link is set then set PI disabled - frm.toggle_reqd('purchase_invoice', 0); - frm.set_df_property('purchase_invoice', 'read_only', 1); - } - else if (frm.doc.purchase_invoice) { + frm.toggle_reqd("purchase_invoice", 0); + frm.set_df_property("purchase_invoice", "read_only", 1); + } else if (frm.doc.purchase_invoice) { // if purchase invoice link is set then set PR disabled - frm.toggle_reqd('purchase_receipt', 0); - frm.set_df_property('purchase_receipt', 'read_only', 1); - } - else { - frm.toggle_reqd('purchase_receipt', 1); - frm.set_df_property('purchase_receipt', 'read_only', 0); - frm.toggle_reqd('purchase_invoice', 1); - frm.set_df_property('purchase_invoice', 'read_only', 0); + frm.toggle_reqd("purchase_receipt", 0); + frm.set_df_property("purchase_receipt", "read_only", 1); + } else { + frm.toggle_reqd("purchase_receipt", 1); + frm.set_df_property("purchase_receipt", "read_only", 0); + frm.toggle_reqd("purchase_invoice", 1); + frm.set_df_property("purchase_invoice", "read_only", 0); } }, - make_journal_entry: function(frm) { + make_journal_entry: function (frm) { frappe.call({ method: "erpnext.assets.doctype.asset.asset.make_journal_entry", args: { - asset_name: frm.doc.name + asset_name: frm.doc.name, }, - callback: function(r) { + callback: function (r) { if (r.message) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } - } - }) + }, + }); }, - setup_chart: async function(frm) { - if(frm.doc.finance_books.length > 1) { - return + setup_chart: async function (frm) { + if (frm.doc.finance_books.length > 1) { + return; } - var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: 'Date' })]; + var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: "Date" })]; var asset_values = [frm.doc.gross_purchase_amount]; - if(frm.doc.calculate_depreciation) { - if(frm.doc.opening_accumulated_depreciation) { + if (frm.doc.calculate_depreciation) { + if (frm.doc.opening_accumulated_depreciation) { var depreciation_date = frappe.datetime.add_months( frm.doc.finance_books[0].depreciation_start_date, -1 * frm.doc.finance_books[0].frequency_of_depreciation ); - x_intervals.push(frappe.format(depreciation_date, { fieldtype: 'Date' })); - asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount'))); + x_intervals.push(frappe.format(depreciation_date, { fieldtype: "Date" })); + asset_values.push( + flt( + frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, + precision("gross_purchase_amount") + ) + ); } - $.each(frm.doc.schedules || [], function(i, v) { - x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' })); - var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount')); - if(v.journal_entry) { + $.each(frm.doc.schedules || [], function (i, v) { + x_intervals.push(frappe.format(v.schedule_date, { fieldtype: "Date" })); + var asset_value = flt( + frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, + precision("gross_purchase_amount") + ); + if (v.journal_entry) { asset_values.push(asset_value); } else { if (in_list(["Scrapped", "Sold"], frm.doc.status)) { @@ -245,25 +288,32 @@ frappe.ui.form.on('Asset', { } }); } else { - if(frm.doc.opening_accumulated_depreciation) { - x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' })); - asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount'))); + if (frm.doc.opening_accumulated_depreciation) { + x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: "Date" })); + asset_values.push( + flt( + frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, + precision("gross_purchase_amount") + ) + ); } - let depr_entries = (await frappe.call({ - method: "get_manual_depreciation_entries", - doc: frm.doc, - })).message; + let depr_entries = ( + await frappe.call({ + method: "get_manual_depreciation_entries", + doc: frm.doc, + }) + ).message; - $.each(depr_entries || [], function(i, v) { - x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' })); - let last_asset_value = asset_values[asset_values.length - 1] - asset_values.push(flt(last_asset_value - v.value, precision('gross_purchase_amount'))); + $.each(depr_entries || [], function (i, v) { + x_intervals.push(frappe.format(v.posting_date, { fieldtype: "Date" })); + let last_asset_value = asset_values[asset_values.length - 1]; + asset_values.push(flt(last_asset_value - v.value, precision("gross_purchase_amount"))); }); } - if(in_list(["Scrapped", "Sold"], frm.doc.status)) { - x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: 'Date' })); + if (in_list(["Scrapped", "Sold"], frm.doc.status)) { + x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: "Date" })); asset_values.push(0); } @@ -271,62 +321,65 @@ frappe.ui.form.on('Asset', { title: "Asset Value", data: { labels: x_intervals, - datasets: [{ - color: 'green', - values: asset_values, - formatted: asset_values.map(d => d?.toFixed(2)) - }] + datasets: [ + { + color: "green", + values: asset_values, + formatted: asset_values.map((d) => d?.toFixed(2)), + }, + ], }, - type: 'line' + type: "line", }); }, - - item_code: function(frm) { - if(frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) { - frm.trigger('set_finance_book'); + item_code: function (frm) { + if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) { + frm.trigger("set_finance_book"); } else { - frm.set_value('finance_books', []); + frm.set_value("finance_books", []); } }, - set_finance_book: function(frm) { + set_finance_book: function (frm) { frappe.call({ method: "erpnext.assets.doctype.asset.asset.get_item_details", args: { item_code: frm.doc.item_code, asset_category: frm.doc.asset_category, - gross_purchase_amount: frm.doc.gross_purchase_amount + gross_purchase_amount: frm.doc.gross_purchase_amount, }, - callback: function(r, rt) { - if(r.message) { - frm.set_value('finance_books', r.message); + callback: function (r, rt) { + if (r.message) { + frm.set_value("finance_books", r.message); } - } - }) + }, + }); }, - is_existing_asset: function(frm) { + is_existing_asset: function (frm) { frm.trigger("toggle_reference_doc"); }, - is_composite_asset: function(frm) { - if(frm.doc.is_composite_asset) { - frm.set_value('gross_purchase_amount', 0); - frm.set_df_property('gross_purchase_amount', 'read_only', 1); + is_composite_asset: function (frm) { + if (frm.doc.is_composite_asset) { + frm.set_value("gross_purchase_amount", 0); + frm.set_df_property("gross_purchase_amount", "read_only", 1); } else { - frm.set_df_property('gross_purchase_amount', 'read_only', 0); + frm.set_df_property("gross_purchase_amount", "read_only", 0); } frm.trigger("toggle_reference_doc"); }, - make_schedules_editable: function(frm) { - if (frm.doc.finance_books.length) { - var is_manual_hence_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 - ? true : false; - var is_shift_hence_editable = frm.doc.finance_books.filter(d => d.shift_based).length > 0 - ? true : false; + make_schedules_editable: function (frm) { + if (frm.doc.finance_books && frm.doc.finance_books.length) { + var is_manual_hence_editable = + frm.doc.finance_books.filter((d) => d.depreciation_method == "Manual").length > 0 + ? true + : false; + var is_shift_hence_editable = + frm.doc.finance_books.filter((d) => d.shift_based).length > 0 ? true : false; frm.toggle_enable("schedules", is_manual_hence_editable || is_shift_hence_editable); frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_manual_hence_editable); @@ -335,95 +388,95 @@ frappe.ui.form.on('Asset', { } }, - make_sales_invoice: function(frm) { + make_sales_invoice: function (frm) { frappe.call({ args: { - "asset": frm.doc.name, - "item_code": frm.doc.item_code, - "company": frm.doc.company, - "serial_no": frm.doc.serial_no + asset: frm.doc.name, + item_code: frm.doc.item_code, + company: frm.doc.company, + serial_no: frm.doc.serial_no, }, method: "erpnext.assets.doctype.asset.asset.make_sales_invoice", - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } - }) + }, + }); }, - create_asset_maintenance: function(frm) { + create_asset_maintenance: function (frm) { frappe.call({ args: { - "asset": frm.doc.name, - "item_code": frm.doc.item_code, - "item_name": frm.doc.item_name, - "asset_category": frm.doc.asset_category, - "company": frm.doc.company + asset: frm.doc.name, + item_code: frm.doc.item_code, + item_name: frm.doc.item_name, + asset_category: frm.doc.asset_category, + company: frm.doc.company, }, method: "erpnext.assets.doctype.asset.asset.create_asset_maintenance", - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } - }) + }, + }); }, - create_asset_repair: function(frm) { + create_asset_repair: function (frm) { frappe.call({ args: { - "asset": frm.doc.name, - "asset_name": frm.doc.asset_name + asset: frm.doc.name, + asset_name: frm.doc.asset_name, }, method: "erpnext.assets.doctype.asset.asset.create_asset_repair", - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } + }, }); }, - create_asset_capitalization: function(frm) { + create_asset_capitalization: function (frm) { frappe.call({ args: { - "asset": frm.doc.name, + asset: frm.doc.name, }, method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization", - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } + }, }); }, - split_asset: function(frm) { - const title = __('Split Asset'); + split_asset: function (frm) { + const title = __("Split Asset"); const fields = [ { - fieldname: 'split_qty', - fieldtype: 'Int', - label: __('Split Qty'), - reqd: 1 - } + fieldname: "split_qty", + fieldtype: "Int", + label: __("Split Qty"), + reqd: 1, + }, ]; let dialog = new frappe.ui.Dialog({ title: title, - fields: fields + fields: fields, }); - dialog.set_primary_action(__('Split'), function() { + dialog.set_primary_action(__("Split"), function () { const dialog_data = dialog.get_values(); frappe.call({ args: { - "asset_name": frm.doc.name, - "split_qty": cint(dialog_data.split_qty) + asset_name: frm.doc.name, + split_qty: cint(dialog_data.split_qty), }, method: "erpnext.assets.doctype.asset.asset.split_asset", - callback: function(r) { + callback: function (r) { let doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } + }, }); dialog.hide(); @@ -432,23 +485,23 @@ frappe.ui.form.on('Asset', { dialog.show(); }, - create_asset_value_adjustment: function(frm) { + create_asset_value_adjustment: function (frm) { frappe.call({ args: { - "asset": frm.doc.name, - "asset_category": frm.doc.asset_category, - "company": frm.doc.company + asset: frm.doc.name, + asset_category: frm.doc.asset_category, + company: frm.doc.company, }, method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment", freeze: 1, - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } - }) + }, + }); }, - calculate_depreciation: function(frm) { + calculate_depreciation: function (frm) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) { frm.trigger("set_finance_book"); @@ -457,237 +510,293 @@ frappe.ui.form.on('Asset', { } }, - gross_purchase_amount: function(frm) { + gross_purchase_amount: function (frm) { if (frm.doc.finance_books) { - frm.doc.finance_books.forEach(d => { + frm.doc.finance_books.forEach((d) => { frm.events.set_depreciation_rate(frm, d); - }) + }); } }, purchase_receipt: (frm) => { - frm.trigger('toggle_reference_doc'); + frm.trigger("toggle_reference_doc"); if (frm.doc.purchase_receipt) { if (frm.doc.item_code) { - frappe.db.get_doc('Purchase Receipt', frm.doc.purchase_receipt).then(pr_doc => { - frm.events.set_values_from_purchase_doc(frm, 'Purchase Receipt', pr_doc) + frappe.db.get_doc("Purchase Receipt", frm.doc.purchase_receipt).then((pr_doc) => { + frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt", pr_doc); }); } else { - frm.set_value('purchase_receipt', ''); + frm.set_value("purchase_receipt", ""); frappe.msgprint({ - title: __('Not Allowed'), - message: __("Please select Item Code first") + title: __("Not Allowed"), + message: __("Please select Item Code first"), }); } } }, purchase_invoice: (frm) => { - frm.trigger('toggle_reference_doc'); + frm.trigger("toggle_reference_doc"); if (frm.doc.purchase_invoice) { if (frm.doc.item_code) { - frappe.db.get_doc('Purchase Invoice', frm.doc.purchase_invoice).then(pi_doc => { - frm.events.set_values_from_purchase_doc(frm, 'Purchase Invoice', pi_doc) + frappe.db.get_doc("Purchase Invoice", frm.doc.purchase_invoice).then((pi_doc) => { + frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice", pi_doc); }); } else { - frm.set_value('purchase_invoice', ''); + frm.set_value("purchase_invoice", ""); frappe.msgprint({ - title: __('Not Allowed'), - message: __("Please select Item Code first") + title: __("Not Allowed"), + message: __("Please select Item Code first"), }); } } }, - set_values_from_purchase_doc: function(frm, doctype, purchase_doc) { - frm.set_value('company', purchase_doc.company); + set_values_from_purchase_doc: function (frm, doctype, purchase_doc) { + frm.set_value("company", purchase_doc.company); if (purchase_doc.bill_date) { - frm.set_value('purchase_date', purchase_doc.bill_date); + frm.set_value("purchase_date", purchase_doc.bill_date); } else { - frm.set_value('purchase_date', purchase_doc.posting_date); + frm.set_value("purchase_date", purchase_doc.posting_date); } if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) { - frm.set_value('available_for_use_date', frm.doc.purchase_date); + frm.set_value("available_for_use_date", frm.doc.purchase_date); } - const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code); + const item = purchase_doc.items.find((item) => item.item_code === frm.doc.item_code); if (!item) { - doctype_field = frappe.scrub(doctype) - frm.set_value(doctype_field, ''); + doctype_field = frappe.scrub(doctype); + frm.set_value(doctype_field, ""); frappe.msgprint({ - title: __('Invalid {0}', [__(doctype)]), - message: __('The selected {0} does not contain the selected Asset Item.', [__(doctype)]), - indicator: 'red' + title: __("Invalid {0}", [__(doctype)]), + message: __("The selected {0} does not contain the selected Asset Item.", [__(doctype)]), + indicator: "red", }); } - frm.set_value('gross_purchase_amount', item.base_net_rate + item.item_tax_amount); - frm.set_value('purchase_receipt_amount', item.base_net_rate + item.item_tax_amount); - item.asset_location && frm.set_value('location', item.asset_location); - frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center); + frappe.db.get_value("Item", item.item_code, "is_grouped_asset", (r) => { + var asset_quantity = r.is_grouped_asset ? item.qty : 1; + var purchase_amount = flt( + item.valuation_rate * asset_quantity, + precision("gross_purchase_amount") + ); + + frm.set_value("gross_purchase_amount", purchase_amount); + frm.set_value("purchase_receipt_amount", purchase_amount); + frm.set_value("asset_quantity", asset_quantity); + frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center); + if (item.asset_location) { + frm.set_value("location", item.asset_location); + } + }); }, - set_depreciation_rate: function(frm, row) { - if (row.total_number_of_depreciations && row.frequency_of_depreciation - && row.expected_value_after_useful_life) { + set_depreciation_rate: function (frm, row) { + if ( + row.total_number_of_depreciations && + row.frequency_of_depreciation && + row.expected_value_after_useful_life + ) { frappe.call({ method: "get_depreciation_rate", doc: frm.doc, args: row, - callback: function(r) { + callback: function (r) { if (r.message) { frappe.flags.dont_change_rate = true; - frappe.model.set_value(row.doctype, row.name, - "rate_of_depreciation", flt(r.message, precision("rate_of_depreciation", row))); + frappe.model.set_value( + row.doctype, + row.name, + "rate_of_depreciation", + flt(r.message, precision("rate_of_depreciation", row)) + ); } - } + }, }); } }, - set_salvage_value_percentage_or_expected_value_after_useful_life: function(frm, row, salvage_value_percentage_changed, expected_value_after_useful_life_changed) { + set_salvage_value_percentage_or_expected_value_after_useful_life: function ( + frm, + row, + salvage_value_percentage_changed, + expected_value_after_useful_life_changed + ) { if (expected_value_after_useful_life_changed) { frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true; - const new_salvage_value_percentage = flt((row.expected_value_after_useful_life * 100) / frm.doc.gross_purchase_amount, precision("salvage_value_percentage", row)); - frappe.model.set_value(row.doctype, row.name, "salvage_value_percentage", new_salvage_value_percentage); + const new_salvage_value_percentage = flt( + (row.expected_value_after_useful_life * 100) / frm.doc.gross_purchase_amount, + precision("salvage_value_percentage", row) + ); + frappe.model.set_value( + row.doctype, + row.name, + "salvage_value_percentage", + new_salvage_value_percentage + ); frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false; } else if (salvage_value_percentage_changed) { frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true; - const new_expected_value_after_useful_life = flt(frm.doc.gross_purchase_amount * (row.salvage_value_percentage / 100), precision('gross_purchase_amount')); - frappe.model.set_value(row.doctype, row.name, "expected_value_after_useful_life", new_expected_value_after_useful_life); + const new_expected_value_after_useful_life = flt( + frm.doc.gross_purchase_amount * (row.salvage_value_percentage / 100), + precision("gross_purchase_amount") + ); + frappe.model.set_value( + row.doctype, + row.name, + "expected_value_after_useful_life", + new_expected_value_after_useful_life + ); frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false; } }, }); -frappe.ui.form.on('Asset Finance Book', { - depreciation_method: function(frm, cdt, cdn) { +frappe.ui.form.on("Asset Finance Book", { + depreciation_method: function (frm, cdt, cdn) { const row = locals[cdt][cdn]; frm.events.set_depreciation_rate(frm, row); frm.events.make_schedules_editable(frm); }, - expected_value_after_useful_life: function(frm, cdt, cdn) { + expected_value_after_useful_life: function (frm, cdt, cdn) { const row = locals[cdt][cdn]; if (!frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life) { - frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life(frm, row, false, true); + frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life( + frm, + row, + false, + true + ); } frm.events.set_depreciation_rate(frm, row); }, - salvage_value_percentage: function(frm, cdt, cdn) { + salvage_value_percentage: function (frm, cdt, cdn) { const row = locals[cdt][cdn]; if (!frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life) { - frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life(frm, row, true, false); + frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life( + frm, + row, + true, + false + ); } }, - frequency_of_depreciation: function(frm, cdt, cdn) { + frequency_of_depreciation: function (frm, cdt, cdn) { const row = locals[cdt][cdn]; frm.events.set_depreciation_rate(frm, row); }, - total_number_of_depreciations: function(frm, cdt, cdn) { + total_number_of_depreciations: function (frm, cdt, cdn) { const row = locals[cdt][cdn]; frm.events.set_depreciation_rate(frm, row); }, - rate_of_depreciation: function(frm, cdt, cdn) { - if(!frappe.flags.dont_change_rate) { + rate_of_depreciation: function (frm, cdt, cdn) { + if (!frappe.flags.dont_change_rate) { frappe.model.set_value(cdt, cdn, "expected_value_after_useful_life", 0); } frappe.flags.dont_change_rate = false; }, - depreciation_start_date: function(frm, cdt, cdn) { + depreciation_start_date: function (frm, cdt, cdn) { const book = locals[cdt][cdn]; - if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) { + if ( + frm.doc.available_for_use_date && + book.depreciation_start_date == frm.doc.available_for_use_date + ) { frappe.msgprint(__("Depreciation Posting Date should not be equal to Available for Use Date.")); book.depreciation_start_date = ""; frm.refresh_field("finance_books"); } - } + }, }); -frappe.ui.form.on('Depreciation Schedule', { - make_depreciation_entry: function(frm, cdt, cdn) { +frappe.ui.form.on("Depreciation Schedule", { + make_depreciation_entry: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; if (!row.journal_entry) { frappe.call({ method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry", args: { - "asset_name": frm.doc.name, - "date": row.schedule_date + asset_name: frm.doc.name, + date: row.schedule_date, }, - callback: function(r) { + callback: function (r) { frappe.model.sync(r.message); frm.refresh(); - } - }) + }, + }); } }, - depreciation_amount: function(frm, cdt, cdn) { + depreciation_amount: function (frm, cdt, cdn) { erpnext.asset.set_accumulated_depreciation(frm, locals[cdt][cdn].finance_book_id); - } - + }, }); -erpnext.asset.set_accumulated_depreciation = function(frm, finance_book_id) { +erpnext.asset.set_accumulated_depreciation = function (frm, finance_book_id) { var depreciation_method = frm.doc.finance_books[Number(finance_book_id) - 1].depreciation_method; - if(depreciation_method != "Manual") return; + if (depreciation_method != "Manual") return; var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); - $.each(frm.doc.schedules || [], function(i, row) { + $.each(frm.doc.schedules || [], function (i, row) { if (row.finance_book_id === finance_book_id) { - accumulated_depreciation += flt(row.depreciation_amount); - frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation); - }; - }) + accumulated_depreciation += flt(row.depreciation_amount); + frappe.model.set_value( + row.doctype, + row.name, + "accumulated_depreciation_amount", + accumulated_depreciation + ); + } + }); }; -erpnext.asset.scrap_asset = function(frm) { +erpnext.asset.scrap_asset = function (frm) { frappe.confirm(__("Do you really want to scrap this asset?"), function () { frappe.call({ args: { - "asset_name": frm.doc.name + asset_name: frm.doc.name, }, method: "erpnext.assets.doctype.asset.depreciation.scrap_asset", - callback: function(r) { + callback: function (r) { cur_frm.reload_doc(); - } - }) - }) + }, + }); + }); }; -erpnext.asset.restore_asset = function(frm) { +erpnext.asset.restore_asset = function (frm) { frappe.confirm(__("Do you really want to restore this scrapped asset?"), function () { frappe.call({ args: { - "asset_name": frm.doc.name + asset_name: frm.doc.name, }, method: "erpnext.assets.doctype.asset.depreciation.restore_asset", - callback: function(r) { + callback: function (r) { cur_frm.reload_doc(); - } - }) - }) + }, + }); + }); }; -erpnext.asset.transfer_asset = function() { +erpnext.asset.transfer_asset = function () { frappe.call({ method: "erpnext.assets.doctype.asset.asset.make_asset_movement", freeze: true, - args:{ - "assets": [{ name: cur_frm.doc.name }], - "purpose": "Transfer" + args: { + assets: [{ name: cur_frm.doc.name }], + purpose: "Transfer", }, callback: function (r) { if (r.message) { var doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); } - } + }, }); }; diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 3b0d5d5e7fb..d8b8bf18a15 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -35,6 +35,8 @@ "purchase_receipt", "purchase_invoice", "available_for_use_date", + "total_asset_cost", + "additional_asset_cost", "column_break_23", "gross_purchase_amount", "asset_quantity", @@ -200,9 +202,8 @@ "fieldname": "purchase_date", "fieldtype": "Date", "label": "Purchase Date", - "read_only": 1, - "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", - "reqd": 1 + "mandatory_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" }, { "fieldname": "disposal_date", @@ -225,15 +226,15 @@ "fieldname": "gross_purchase_amount", "fieldtype": "Currency", "label": "Gross Purchase Amount", + "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)", "options": "Company:company:default_currency", - "read_only_depends_on": "eval:!doc.is_existing_asset", - "reqd": 1 + "read_only_depends_on": "eval:!doc.is_existing_asset" }, { "fieldname": "available_for_use_date", "fieldtype": "Date", "label": "Available-for-use Date", - "reqd": 1 + "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)" }, { "default": "0", @@ -532,6 +533,22 @@ "label": "Capitalized In", "options": "Asset Capitalization", "read_only": 1 + }, + { + "depends_on": "eval:doc.docstatus > 0", + "fieldname": "total_asset_cost", + "fieldtype": "Currency", + "label": "Total Asset Cost", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "depends_on": "eval:doc.docstatus > 0", + "fieldname": "additional_asset_cost", + "fieldtype": "Currency", + "label": "Additional Asset Cost", + "options": "Company:company:default_currency", + "read_only": 1 } ], "idx": 72, @@ -565,7 +582,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2023-11-20 21:05:45.216899", + "modified": "2024-01-15 17:35:49.226603", "modified_by": "Administrator", "module": "Assets", "name": "Asset", @@ -609,4 +626,4 @@ "states": [], "title_field": "asset_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 92a349abb3e..fd00a495030 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -10,6 +10,7 @@ from frappe import _ from frappe.utils import ( add_days, add_months, + add_years, cint, date_diff, flt, @@ -23,6 +24,7 @@ from frappe.utils import ( import erpnext from erpnext.accounts.general_ledger import make_reverse_gl_entries +from erpnext.accounts.utils import get_fiscal_year from erpnext.assets.doctype.asset.depreciation import ( get_depreciation_accounts, get_disposal_account_and_cost_center, @@ -48,18 +50,22 @@ class Asset(AccountsController): if self.get("schedules"): self.validate_expected_value_after_useful_life() + self.total_asset_cost = self.gross_purchase_amount self.status = self.get_status() def on_submit(self): self.validate_in_use_date() self.set_status() self.make_asset_movement() + self.reload() if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() def on_cancel(self): self.validate_cancellation() self.cancel_movement_entries() + self.cancel_capitalization() + self.reload() self.delete_depreciation_entries() self.set_status() self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") @@ -79,9 +85,7 @@ class Asset(AccountsController): ) if self.is_existing_asset and self.purchase_invoice: - frappe.throw( - _("Purchase Invoice cannot be made against an existing asset {0}").format(self.name) - ) + frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) def prepare_depreciation_data( self, @@ -201,9 +205,9 @@ class Asset(AccountsController): for d in self.finance_books: if d.depreciation_start_date == self.available_for_use_date: frappe.throw( - _("Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.").format( - d.idx - ), + _( + "Row #{}: Depreciation Posting Date should not be equal to Available for Use Date." + ).format(d.idx), title=_("Incorrect Date"), ) @@ -212,9 +216,7 @@ class Asset(AccountsController): self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") if self.item_code and not self.get("finance_books"): - finance_books = get_item_details( - self.item_code, self.asset_category, self.gross_purchase_amount - ) + finance_books = get_item_details(self.item_code, self.asset_category, self.gross_purchase_amount) self.set("finance_books", finance_books) def validate_finance_books(self): @@ -246,7 +248,12 @@ class Asset(AccountsController): frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) if is_cwip_accounting_enabled(self.asset_category): - if not self.is_existing_asset and not (self.purchase_receipt or self.purchase_invoice): + if ( + not self.is_existing_asset + and not self.is_composite_asset + and not self.purchase_receipt + and not self.purchase_invoice + ): frappe.throw( _("Please create purchase receipt or purchase invoice for the item {0}").format( self.item_code @@ -259,7 +266,9 @@ class Asset(AccountsController): and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock") ): frappe.throw( - _("Update stock must be enable for the purchase invoice {0}").format(self.purchase_invoice) + _("Update stock must be enabled for the purchase invoice {0}").format( + self.purchase_invoice + ) ) if not self.calculate_depreciation: @@ -273,9 +282,7 @@ class Asset(AccountsController): if self.is_existing_asset: return - if self.available_for_use_date and getdate(self.available_for_use_date) < getdate( - self.purchase_date - ): + if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date): frappe.throw(_("Available-for-use Date should be after purchase date")) def validate_gross_and_purchase_amount(self): @@ -298,7 +305,7 @@ class Asset(AccountsController): posting_date, posting_time = frappe.db.get_value( reference_doctype, reference_docname, ["posting_date", "posting_time"] ) - transaction_date = get_datetime("{} {}".format(posting_date, posting_time)) + transaction_date = get_datetime(f"{posting_date} {posting_time}") assets = [ { "asset": self.name, @@ -336,9 +343,7 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() for finance_book in self.get("finance_books"): - self._make_depreciation_schedule( - finance_book, start, date_of_disposal, value_after_depreciation - ) + self._make_depreciation_schedule(finance_book, start, date_of_disposal, value_after_depreciation) if len(self.get("finance_books")) > 1 and any(start): self.sort_depreciation_schedule() @@ -374,14 +379,24 @@ class Asset(AccountsController): should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date) depreciation_amount = 0 - number_of_pending_depreciations = final_number_of_depreciations - start[finance_book.idx - 1] + yearly_opening_wdv = value_after_depreciation + current_fiscal_year_end_date = None for n in range(start[finance_book.idx - 1], final_number_of_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue + schedule_date = add_months( + finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation) + ) + if not current_fiscal_year_end_date: + current_fiscal_year_end_date = get_fiscal_year(finance_book.depreciation_start_date)[2] + elif getdate(schedule_date) > getdate(current_fiscal_year_end_date): + current_fiscal_year_end_date = add_years(current_fiscal_year_end_date, 1) + yearly_opening_wdv = value_after_depreciation + if n > 0 and len(self.get("schedules")) > n - 1: prev_depreciation_amount = self.get("schedules")[n - 1].depreciation_amount else: @@ -390,6 +405,7 @@ class Asset(AccountsController): depreciation_amount = get_depreciation_amount( self, value_after_depreciation, + yearly_opening_wdv, finance_book, n, prev_depreciation_amount, @@ -408,13 +424,14 @@ class Asset(AccountsController): schedule_date = get_last_day(schedule_date) # if asset is being sold - if date_of_disposal: + if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal): from_date = self.get_from_date_for_disposal(finance_book) depreciation_amount, days, months = self.get_pro_rata_amt( finance_book, depreciation_amount, from_date, date_of_disposal, + original_schedule_date=schedule_date, ) if depreciation_amount > 0: @@ -427,10 +444,7 @@ class Asset(AccountsController): n == 0 and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) and not self.opening_accumulated_depreciation - and get_updated_rate_of_depreciation_for_wdv_and_dd( - self, value_after_depreciation, finance_book, False - ) - == finance_book.rate_of_depreciation + and not self.flags.wdv_it_act_applied ): from_date = add_days( self.available_for_use_date, -1 @@ -447,7 +461,10 @@ class Asset(AccountsController): from_date = get_last_day( add_months( getdate(self.available_for_use_date), - ((self.number_of_depreciations_booked - 1) * finance_book.frequency_of_depreciation), + ( + (self.number_of_depreciations_booked - 1) + * finance_book.frequency_of_depreciation + ), ) ) else: @@ -469,7 +486,8 @@ class Asset(AccountsController): # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months( self.available_for_use_date, - (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation), + (n + self.number_of_depreciations_booked) + * cint(finance_book.frequency_of_depreciation), ) depreciation_amount_without_pro_rata = depreciation_amount @@ -490,17 +508,19 @@ class Asset(AccountsController): if not depreciation_amount: continue - value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount")) + value_after_depreciation = flt( + value_after_depreciation - flt(depreciation_amount), + self.precision("gross_purchase_amount"), + ) # Adjust depreciation amount in the last period based on the expected value after useful life - if finance_book.expected_value_after_useful_life and ( - ( - n == cint(final_number_of_depreciations) - 1 - and value_after_depreciation != finance_book.expected_value_after_useful_life + if ( + n == cint(final_number_of_depreciations) - 1 + and flt(value_after_depreciation) != flt(finance_book.expected_value_after_useful_life) + ) or flt(value_after_depreciation) < flt(finance_book.expected_value_after_useful_life): + depreciation_amount += flt(value_after_depreciation) - flt( + finance_book.expected_value_after_useful_life ) - or value_after_depreciation < finance_book.expected_value_after_useful_life - ): - depreciation_amount += value_after_depreciation - finance_book.expected_value_after_useful_life skip_row = True if flt(depreciation_amount, self.precision("gross_purchase_amount")) > 0: @@ -648,7 +668,8 @@ class Asset(AccountsController): if not row.depreciation_start_date: if not self.available_for_use_date: frappe.throw( - _("Row {0}: Depreciation Start Date is required").format(row.idx), title=_("Invalid Schedule") + _("Row {0}: Depreciation Start Date is required").format(row.idx), + title=_("Invalid Schedule"), ) row.depreciation_start_date = get_last_day(self.available_for_use_date) @@ -678,9 +699,7 @@ class Asset(AccountsController): title=_("Invalid Schedule"), ) - if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate( - self.purchase_date - ): + if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date): frappe.throw( _("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date").format( row.idx @@ -828,6 +847,16 @@ class Asset(AccountsController): movement = frappe.get_doc("Asset Movement", movement.get("name")) movement.cancel() + def cancel_capitalization(self): + asset_capitalization = frappe.db.get_value( + "Asset Capitalization", + {"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"}, + ) + + if asset_capitalization: + asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization) + asset_capitalization.cancel() + def delete_depreciation_entries(self): if self.calculate_depreciation: for d in self.get("schedules"): @@ -866,11 +895,14 @@ class Asset(AccountsController): if self.calculate_depreciation: idx = self.get_default_finance_book_idx() or 0 - expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life + expected_value_after_useful_life = self.finance_books[ + idx + ].expected_value_after_useful_life value_after_depreciation = self.finance_books[idx].value_after_depreciation if ( - flt(value_after_depreciation) <= expected_value_after_useful_life or self.is_fully_depreciated + flt(value_after_depreciation) <= expected_value_after_useful_life + or self.is_fully_depreciated ): status = "Fully Depreciated" elif flt(value_after_depreciation) < flt(self.gross_purchase_amount): @@ -978,9 +1010,10 @@ class Asset(AccountsController): fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account() if ( - purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate() + purchase_document + and self.purchase_receipt_amount + and getdate(self.available_for_use_date) <= getdate() ): - gl_entries.append( self.get_gl_dict( { @@ -1019,9 +1052,7 @@ class Asset(AccountsController): @frappe.whitelist() def get_manual_depreciation_entries(self): - (_, _, depreciation_expense_account) = get_depreciation_accounts( - self.asset_category, self.company - ) + (_, _, depreciation_expense_account) = get_depreciation_accounts(self.asset_category, self.company) gle = frappe.qb.DocType("GL Entry") @@ -1048,7 +1079,8 @@ class Asset(AccountsController): if args.get("depreciation_method") == "Double Declining Balance": return 200.0 / ( ( - flt(args.get("total_number_of_depreciations"), 2) * flt(args.get("frequency_of_depreciation")) + flt(args.get("total_number_of_depreciations"), 2) + * flt(args.get("frequency_of_depreciation")) ) / 12 ) @@ -1083,14 +1115,20 @@ class Asset(AccountsController): return flt((100 * (1 - depreciation_rate)), float_precision) def get_pro_rata_amt( - self, row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False + self, + row, + depreciation_amount, + from_date, + to_date, + has_wdv_or_dd_non_yearly_pro_rata=False, + original_schedule_date=None, ): days = date_diff(to_date, from_date) months = month_diff(to_date, from_date) if has_wdv_or_dd_non_yearly_pro_rata: - total_days = get_total_days(to_date, 12) + total_days = get_total_days(original_schedule_date or to_date, 12) else: - total_days = get_total_days(to_date, row.frequency_of_depreciation) + total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation) return (depreciation_amount * flt(days)) / flt(total_days), days, months @@ -1104,9 +1142,7 @@ def update_maintenance_status(): asset = frappe.get_doc("Asset", asset.name) if frappe.db.exists("Asset Repair", {"asset_name": asset.name, "repair_status": "Pending"}): asset.set_status("Out of Order") - elif frappe.db.exists( - "Asset Maintenance Task", {"parent": asset.name, "next_due_date": today()} - ): + elif frappe.db.exists("Asset Maintenance Task", {"parent": asset.name, "next_due_date": today()}): asset.set_status("In Maintenance") else: asset.set_status() @@ -1190,9 +1226,7 @@ def create_asset_capitalization(asset): @frappe.whitelist() def create_asset_value_adjustment(asset, asset_category, company): asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") - asset_value_adjustment.update( - {"asset": asset, "company": company, "asset_category": asset_category} - ) + asset_value_adjustment.update({"asset": asset, "company": company, "asset_category": asset_category}) return asset_value_adjustment @@ -1249,18 +1283,14 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non ) if not asset and not account: - account = get_asset_category_account( - account_name, asset_category=asset_category, company=company - ) + account = get_asset_category_account(account_name, asset_category=asset_category, company=company) if not account: account = frappe.get_cached_value("Company", company, account_name) if not account: if not asset_category: - frappe.throw( - _("Set {0} in company {1}").format(account_name.replace("_", " ").title(), company) - ) + frappe.throw(_("Set {0} in company {1}").format(account_name.replace("_", " ").title(), company)) else: frappe.throw( _("Set {0} in asset category {1} or company {2}").format( @@ -1289,7 +1319,7 @@ def make_journal_entry(asset_name): je.voucher_type = "Depreciation Entry" je.naming_series = depreciation_series je.company = asset.company - je.remark = "Depreciation Entry against asset {0}".format(asset_name) + je.remark = f"Depreciation Entry against asset {asset_name}" je.append( "accounts", @@ -1348,6 +1378,8 @@ def is_cwip_accounting_enabled(asset_category): @frappe.whitelist() def get_asset_value_after_depreciation(asset_name, finance_book=None): asset = frappe.get_doc("Asset", asset_name) + if not asset.calculate_depreciation: + return flt(asset.value_after_depreciation) return asset.get_value_after_depreciation(finance_book) @@ -1364,6 +1396,7 @@ def get_total_days(date, frequency): def get_depreciation_amount( asset, depreciable_value, + yearly_opening_wdv, fb_row, schedule_idx=0, prev_depreciation_amount=0, @@ -1377,29 +1410,18 @@ def get_depreciation_amount( asset, fb_row, schedule_idx, number_of_pending_depreciations ) else: - rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( - asset, depreciable_value, fb_row - ) return get_wdv_or_dd_depr_amount( + asset, + fb_row, depreciable_value, - rate_of_depreciation, - fb_row.frequency_of_depreciation, + yearly_opening_wdv, schedule_idx, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, ) -@erpnext.allow_regional -def get_updated_rate_of_depreciation_for_wdv_and_dd( - asset, depreciable_value, fb_row, show_msg=True -): - return fb_row.rate_of_depreciation - - -def get_straight_line_or_manual_depr_amount( - asset, row, schedule_idx, number_of_pending_depreciations -): +def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx, number_of_pending_depreciations): if row.shift_based: return get_shift_depr_amount(asset, row, schedule_idx) @@ -1416,39 +1438,44 @@ def get_straight_line_or_manual_depr_amount( # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value elif asset.flags.decrease_in_asset_value_due_to_value_adjustment: if row.daily_prorata_based: - daily_depr_amount = ( - flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) - ) / date_diff( - get_last_day( - add_months( - row.depreciation_start_date, - flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) - * row.frequency_of_depreciation, - ) - ), - add_days( + amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + total_days = ( + date_diff( get_last_day( add_months( row.depreciation_start_date, - flt( - row.total_number_of_depreciations - - asset.number_of_depreciations_booked - - number_of_pending_depreciations - - 1 - ) + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) * row.frequency_of_depreciation, ) ), - 1, - ), + add_days( + get_last_day( + add_months( + row.depreciation_start_date, + flt( + row.total_number_of_depreciations + - asset.number_of_depreciations_booked + - number_of_pending_depreciations + - 1 + ) + * row.frequency_of_depreciation, + ) + ), + 1, + ), + ) + + 1 ) + daily_depr_amount = amount / total_days to_date = get_last_day( add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) ) from_date = add_days( get_last_day( - add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation) + add_months( + row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + ) ), 1, ) @@ -1461,29 +1488,39 @@ def get_straight_line_or_manual_depr_amount( # if the Depreciation Schedule is being prepared for the first time else: if row.daily_prorata_based: - daily_depr_amount = ( + amount = ( flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - flt(row.expected_value_after_useful_life) - ) / date_diff( - get_last_day( - add_months( - row.depreciation_start_date, - flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) - * row.frequency_of_depreciation, - ) - ), - add_days( - get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), 1 - ), ) + total_days = ( + date_diff( + get_last_day( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) + * row.frequency_of_depreciation, + ) + ), + add_days( + get_last_day( + add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation) + ), + 1, + ), + ) + + 1 + ) + daily_depr_amount = amount / total_days to_date = get_last_day( add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) ) from_date = add_days( get_last_day( - add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation) + add_months( + row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + ) ), 1, ) @@ -1531,30 +1568,54 @@ def get_asset_shift_factors_map(): return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) +@erpnext.allow_regional def get_wdv_or_dd_depr_amount( + asset, + fb_row, depreciable_value, - rate_of_depreciation, - frequency_of_depreciation, + yearly_opening_wdv, schedule_idx, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, ): - if cint(frequency_of_depreciation) == 12: - return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) + return get_default_wdv_or_dd_depr_amount( + asset, + fb_row, + depreciable_value, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + ) + + +def get_default_wdv_or_dd_depr_amount( + asset, + fb_row, + depreciable_value, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, +): + if cint(fb_row.frequency_of_depreciation) == 12: + return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) else: if has_wdv_or_dd_non_yearly_pro_rata: if schedule_idx == 0: - return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) - elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1: + return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) + elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: return ( - flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) + flt(depreciable_value) + * flt(fb_row.frequency_of_depreciation) + * (flt(fb_row.rate_of_depreciation) / 1200) ) else: return prev_depreciation_amount else: - if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0: + if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: return ( - flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) + flt(depreciable_value) + * flt(fb_row.frequency_of_depreciation) + * (flt(fb_row.rate_of_depreciation) / 1200) ) else: return prev_depreciation_amount @@ -1619,9 +1680,7 @@ def update_existing_asset(asset, remaining_qty): processed_finance_books.append(int(term.finance_book_id)) depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity) - frappe.db.set_value( - "Depreciation Schedule", term.name, "depreciation_amount", depreciation_amount - ) + frappe.db.set_value("Depreciation Schedule", term.name, "depreciation_amount", depreciation_amount) accumulated_depreciation += depreciation_amount frappe.db.set_value( "Depreciation Schedule", term.name, "accumulated_depreciation_amount", accumulated_depreciation diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 5f53b005aaf..712958adcfc 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -1,56 +1,46 @@ -frappe.listview_settings['Asset'] = { - add_fields: ['status'], +frappe.listview_settings["Asset"] = { + add_fields: ["status"], get_indicator: function (doc) { if (doc.status === "Fully Depreciated") { return [__("Fully Depreciated"), "green", "status,=,Fully Depreciated"]; - } else if (doc.status === "Partially Depreciated") { return [__("Partially Depreciated"), "grey", "status,=,Partially Depreciated"]; - } else if (doc.status === "Sold") { return [__("Sold"), "green", "status,=,Sold"]; - } else if (["Capitalized", "Decapitalized"].includes(doc.status)) { return [__(doc.status), "grey", "status,=," + doc.status]; - } else if (doc.status === "Scrapped") { return [__("Scrapped"), "grey", "status,=,Scrapped"]; - } else if (doc.status === "In Maintenance") { return [__("In Maintenance"), "orange", "status,=,In Maintenance"]; - } else if (doc.status === "Out of Order") { return [__("Out of Order"), "grey", "status,=,Out of Order"]; - } else if (doc.status === "Issue") { return [__("Issue"), "orange", "status,=,Issue"]; - } else if (doc.status === "Receipt") { return [__("Receipt"), "green", "status,=,Receipt"]; - } else if (doc.status === "Submitted") { return [__("Submitted"), "blue", "status,=,Submitted"]; - } else if (doc.status === "Draft") { return [__("Draft"), "red", "status,=,Draft"]; } }, - onload: function(me) { - me.page.add_action_item(__("Make Asset Movement"), function() { + onload: function (me) { + me.page.add_action_item(__("Make Asset Movement"), function () { const assets = me.get_checked_items(); frappe.call({ method: "erpnext.assets.doctype.asset.asset.make_asset_movement", freeze: true, - args:{ - "assets": assets + args: { + assets: assets, }, callback: function (r) { if (r.message) { var doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); } - } + }, }); }); }, -} +}; diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 6f76955e12d..f7417cce76c 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -19,6 +19,7 @@ from frappe.utils import ( from frappe.utils.data import get_link_to_form from frappe.utils.user import get_users_with_role +import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) @@ -56,7 +57,10 @@ def post_depreciation_entries(date=None): ) not in credit_and_debit_accounts_for_asset_category_and_company: credit_and_debit_accounts_for_asset_category_and_company.update( { - (asset_category, asset_company): get_credit_and_debit_accounts_for_asset_category_and_company( + ( + asset_category, + asset_company, + ): get_credit_and_debit_accounts_for_asset_category_and_company( asset_category, asset_company ), } @@ -119,9 +123,7 @@ def get_acc_frozen_upto(): if not acc_frozen_upto: return - frozen_accounts_modifier = frappe.db.get_single_value( - "Accounts Settings", "frozen_accounts_modifier" - ) + frozen_accounts_modifier = frappe.db.get_single_value("Accounts Settings", "frozen_accounts_modifier") if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator": return getdate(acc_frozen_upto) @@ -209,9 +211,7 @@ def make_depreciation_entry( debit_account, accounting_dimensions, ) - frappe.db.commit() except Exception as e: - frappe.db.rollback() depreciation_posting_error = e asset.set_status() @@ -246,9 +246,7 @@ def _make_journal_entry_for_depreciation( je.posting_date = depr_schedule.schedule_date je.company = asset.company je.finance_book = depr_schedule.finance_book - je.remark = "Depreciation Entry against {0} worth {1}".format( - asset.name, depr_schedule.depreciation_amount - ) + je.remark = f"Depreciation Entry against {asset.name} worth {depr_schedule.depreciation_amount}" credit_entry = { "account": credit_account, @@ -330,11 +328,7 @@ def get_depreciation_accounts(asset_category, company): if not depreciation_expense_account: depreciation_expense_account = accounts[1] - if ( - not fixed_asset_account - or not accumulated_depreciation_account - or not depreciation_expense_account - ): + if not fixed_asset_account or not accumulated_depreciation_account or not depreciation_expense_account: frappe.throw( _("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format( asset_category, company @@ -412,25 +406,21 @@ def scrap_asset(asset_name): if asset.docstatus != 1: frappe.throw(_("Asset {0} must be submitted").format(asset.name)) elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized", "Decapitalized"): - frappe.throw( - _("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status) - ) + frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)) date = today() depreciate_asset(asset, date) asset.reload() - depreciation_series = frappe.get_cached_value( - "Company", asset.company, "series_for_depreciation_entry" - ) + depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry") je = frappe.new_doc("Journal Entry") je.voucher_type = "Journal Entry" je.naming_series = depreciation_series je.posting_date = date je.company = asset.company - je.remark = "Scrap Entry for asset {0}".format(asset_name) + je.remark = f"Scrap Entry for asset {asset_name}" for entry in get_gl_entries_on_asset_disposal(asset, date): entry.update({"reference_type": "Asset", "reference_name": asset_name}) @@ -473,6 +463,14 @@ def depreciate_asset(asset, date): make_depreciation_entry(asset.name, date) + asset.reload() + cancel_depreciation_entries(asset, date) + + +@erpnext.allow_regional +def cancel_depreciation_entries(asset, date): + pass + def reset_depreciation_schedule(asset, date): if not asset.calculate_depreciation: @@ -500,7 +498,7 @@ def modify_depreciation_schedule_for_asset_repairs(asset): def reverse_depreciation_entry_made_after_disposal(asset, date): - if not asset.calculate_depreciation: + if not asset.calculate_depreciation or not asset.get("schedules"): return row = -1 @@ -512,11 +510,10 @@ def reverse_depreciation_entry_made_after_disposal(asset, date): else: row += 1 - if schedule.schedule_date == date: + if schedule.schedule_date == date and schedule.journal_entry: if not disposal_was_made_on_original_schedule_date( asset, schedule, row, date ) or disposal_happens_in_the_future(date): - reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry.posting_date = nowdate() @@ -707,7 +704,6 @@ def get_asset_details(asset, finance_book=None): def get_profit_gl_entries( asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None ): - if not date: date = getdate() @@ -733,9 +729,7 @@ def get_disposal_account_and_cost_center(company): ) if not disposal_account: - frappe.throw( - _("Please set 'Gain/Loss Account on Asset Disposal' in Company {0}").format(company) - ) + frappe.throw(_("Please set 'Gain/Loss Account on Asset Disposal' in Company {0}").format(company)) if not depreciation_cost_center: frappe.throw(_("Please set 'Asset Depreciation Cost Center' in Company {0}").format(company)) @@ -748,7 +742,7 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_ if asset_doc.available_for_use_date > getdate(disposal_date): frappe.throw( - "Disposal date {0} cannot be before available for use date {1} of the asset.".format( + "Disposal date {} cannot be before available for use date {} of the asset.".format( disposal_date, asset_doc.available_for_use_date ) ) @@ -765,9 +759,7 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_ finance_book_id = fb.idx break - asset_schedules = [ - sch for sch in asset_doc.schedules if cint(sch.finance_book_id) == finance_book_id - ] + asset_schedules = [sch for sch in asset_doc.schedules if cint(sch.finance_book_id) == finance_book_id] accumulated_depr_amount = asset_schedules[-1].accumulated_depreciation_amount return flt( diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 21afd5df851..00a53aa80d2 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -102,9 +102,7 @@ class TestAsset(AssetSetup): self.assertRaises(frappe.ValidationError, asset.save) def test_purchase_asset(self): - pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" - ) + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") asset = frappe.get_doc("Asset", asset_name) @@ -209,7 +207,7 @@ class TestAsset(AssetSetup): asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, asset.precision("gross_purchase_amount"), ) - self.assertEquals(accumulated_depr_amount, 18000.0) + self.assertEqual(accumulated_depr_amount, 18000.0) scrap_asset(asset.name) asset.load_from_db() @@ -219,10 +217,14 @@ class TestAsset(AssetSetup): asset.precision("gross_purchase_amount"), ) pro_rata_amount, _, _ = asset.get_pro_rata_amt( - asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date + asset.finance_books[0], + 9000, + get_last_day(add_months(purchase_date, 1)), + date, + original_schedule_date=get_last_day(nowdate()), ) pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) - self.assertEquals( + self.assertEqual( accumulated_depr_amount, flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")), ) @@ -259,7 +261,7 @@ class TestAsset(AssetSetup): ) this_month_depr_amount = 9000.0 if is_last_day_of_the_month(date) else 0 - self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount) + self.assertEqual(accumulated_depr_amount, 18000.0 + this_month_depr_amount) def test_gle_made_by_asset_sale(self): date = nowdate() @@ -287,7 +289,11 @@ class TestAsset(AssetSetup): self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") pro_rata_amount, _, _ = asset.get_pro_rata_amt( - asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date + asset.finance_books[0], + 9000, + get_last_day(add_months(purchase_date, 1)), + date, + original_schedule_date=get_last_day(nowdate()), ) pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) @@ -349,7 +355,7 @@ class TestAsset(AssetSetup): self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") - expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]] + expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1737.7, 37737.7]] for i, schedule in enumerate(asset.schedules): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) @@ -360,7 +366,7 @@ class TestAsset(AssetSetup): expected_gle = ( ( "_Test Accumulated Depreciations - _TC", - 37742.47, + 37737.7, 0.0, ), ( @@ -371,7 +377,7 @@ class TestAsset(AssetSetup): ( "_Test Gain/Loss on Asset Disposal - _TC", 0.0, - 17742.47, + 17737.7, ), ("Debtors - _TC", 40000.0, 0.0), ) @@ -459,9 +465,7 @@ class TestAsset(AssetSetup): self.assertEqual(jv.accounts[3].reference_name, new_asset.name) def test_expense_head(self): - pr = make_purchase_receipt( - item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location" - ) + pr = make_purchase_receipt(item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location") doc = make_invoice(pr.name) self.assertEqual("Asset Received But Not Billed - _TC", doc.items[0].expense_account) @@ -571,9 +575,7 @@ class TestAsset(AssetSetup): self.assertFalse(gle) # case 1 -- PR with cwip disabled, Asset with cwip enabled - pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location" - ) + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location") frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name") @@ -585,9 +587,7 @@ class TestAsset(AssetSetup): self.assertFalse(gle) # case 2 -- PR with cwip enabled, Asset with cwip disabled - pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location" - ) + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location") frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name") asset_doc = frappe.get_doc("Asset", asset) @@ -691,18 +691,18 @@ class TestDepreciationMethods(AssetSetup): ) expected_schedules = [ - ["2023-01-31", 1021.98, 1021.98], - ["2023-02-28", 923.08, 1945.06], - ["2023-03-31", 1021.98, 2967.04], - ["2023-04-30", 989.01, 3956.05], - ["2023-05-31", 1021.98, 4978.03], - ["2023-06-30", 989.01, 5967.04], - ["2023-07-31", 1021.98, 6989.02], - ["2023-08-31", 1021.98, 8011.0], - ["2023-09-30", 989.01, 9000.01], - ["2023-10-31", 1021.98, 10021.99], - ["2023-11-30", 989.01, 11011.0], - ["2023-12-31", 989.0, 12000.0], + ["2023-01-31", 1019.18, 1019.18], + ["2023-02-28", 920.55, 1939.73], + ["2023-03-31", 1019.18, 2958.91], + ["2023-04-30", 986.3, 3945.21], + ["2023-05-31", 1019.18, 4964.39], + ["2023-06-30", 986.3, 5950.69], + ["2023-07-31", 1019.18, 6969.87], + ["2023-08-31", 1019.18, 7989.05], + ["2023-09-30", 986.3, 8975.35], + ["2023-10-31", 1019.18, 9994.53], + ["2023-11-30", 986.3, 10980.83], + ["2023-12-31", 1019.17, 12000.0], ] schedules = [ @@ -845,7 +845,7 @@ class TestDepreciationMethods(AssetSetup): ["2030-12-31", 28630.14, 28630.14], ["2031-12-31", 35684.93, 64315.07], ["2032-12-31", 17842.47, 82157.54], - ["2033-06-06", 5342.46, 87500.0], + ["2033-06-06", 5342.47, 87500.01], ] schedules = [ @@ -957,7 +957,7 @@ class TestDepreciationBasics(AssetSetup): }, ) - depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0]) + depreciation_amount = get_depreciation_amount(asset, 100000, 100000, asset.finance_books[0]) self.assertEqual(depreciation_amount, 30000) def test_make_depreciation_schedule(self): @@ -1199,8 +1199,7 @@ class TestDepreciationBasics(AssetSetup): je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) accounting_entries = [ - {"account": entry.account, "debit": entry.debit, "credit": entry.credit} - for entry in je.accounts + {"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts ] for entry in accounting_entries: @@ -1235,8 +1234,7 @@ class TestDepreciationBasics(AssetSetup): je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) accounting_entries = [ - {"account": entry.account, "debit": entry.debit, "credit": entry.credit} - for entry in je.accounts + {"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts ] for entry in accounting_entries: @@ -1460,13 +1458,13 @@ class TestDepreciationBasics(AssetSetup): asset.finance_books[0].expected_value_after_useful_life = 100 asset.save() asset.reload() - self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0) + self.assertEqual(asset.finance_books[0].value_after_depreciation, 98000.0) # changing expected_value_after_useful_life shouldn't affect value_after_depreciation asset.finance_books[0].expected_value_after_useful_life = 200 asset.save() asset.reload() - self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0) + self.assertEqual(asset.finance_books[0].value_after_depreciation, 98000.0) def test_asset_cost_center(self): asset = create_asset(is_existing_asset=1, do_not_save=1) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 304bdf26dee..6ac6289c837 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -3,10 +3,10 @@ frappe.provide("erpnext.assets"); - erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController { setup() { this.setup_posting_date_time_check(); + this.frm.ignore_doctypes_on_cancel_all = ["Asset Movement"]; } onload() { @@ -16,14 +16,17 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s refresh() { this.show_general_ledger(); - if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) { + if ( + (this.frm.doc.stock_items && this.frm.doc.stock_items.length) || + !this.frm.doc.target_is_fixed_asset + ) { this.show_stock_ledger(); } - if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { - this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); - this.get_target_asset_details(); - } + // if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { + // this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); + // this.get_target_asset_details(); + // } } setup_queries() { @@ -31,68 +34,77 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s me.setup_warehouse_query(); - me.frm.set_query("target_item_code", function() { + me.frm.set_query("target_item_code", function () { if (me.frm.doc.entry_type == "Capitalization") { - return erpnext.queries.item({"is_stock_item": 0, "is_fixed_asset": 1}); + return erpnext.queries.item({ is_stock_item: 0, is_fixed_asset: 1 }); } else { - return erpnext.queries.item({"is_stock_item": 1, "is_fixed_asset": 0}); + return erpnext.queries.item({ is_stock_item: 1, is_fixed_asset: 0 }); } }); - me.frm.set_query("target_asset", function() { + me.frm.set_query("target_asset", function () { return { - filters: {'is_composite_asset': 1, 'docstatus': 0 } - } + filters: { is_composite_asset: 1, docstatus: 0 }, + }; }); - me.frm.set_query("asset", "asset_items", function() { + me.frm.set_query("asset", "asset_items", function () { var filters = { - 'status': ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]], - 'docstatus': 1 + status: ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]], + docstatus: 1, }; if (me.frm.doc.target_asset) { - filters['name'] = ['!=', me.frm.doc.target_asset]; + filters["name"] = ["!=", me.frm.doc.target_asset]; } return { - filters: filters + filters: filters, }; }); - me.frm.set_query("item_code", "stock_items", function() { - return erpnext.queries.item({"is_stock_item": 1}); + me.frm.set_query("item_code", "stock_items", function () { + return erpnext.queries.item({ is_stock_item: 1 }); }); - me.frm.set_query("item_code", "service_items", function() { - return erpnext.queries.item({"is_stock_item": 0, "is_fixed_asset": 0}); + me.frm.set_query("item_code", "service_items", function () { + return erpnext.queries.item({ is_stock_item: 0, is_fixed_asset: 0 }); }); - me.frm.set_query('batch_no', 'stock_items', function(doc, cdt, cdn) { + me.frm.set_query("batch_no", "stock_items", function (doc, cdt, cdn) { var item = locals[cdt][cdn]; if (!item.item_code) { frappe.throw(__("Please enter Item Code to get Batch Number")); } else { var filters = { - 'item_code': item.item_code, - 'posting_date': me.frm.doc.posting_date || frappe.datetime.nowdate(), - 'warehouse': item.warehouse + item_code: item.item_code, + posting_date: me.frm.doc.posting_date || frappe.datetime.nowdate(), + warehouse: item.warehouse, }; return { query: "erpnext.controllers.queries.get_batch_no", - filters: filters + filters: filters, }; } }); - me.frm.set_query('expense_account', 'service_items', function() { + me.frm.set_query("expense_account", "service_items", function () { return { filters: { - "account_type": ['in', ["Tax", "Expense Account", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]], - "is_group": 0, - "company": me.frm.doc.company - } + account_type: [ + "in", + [ + "Tax", + "Expense Account", + "Income Account", + "Expenses Included In Valuation", + "Expenses Included In Asset Valuation", + ], + ], + is_group: 0, + company: me.frm.doc.company, + }, }; }); } @@ -102,7 +114,10 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } target_asset() { - if (this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { + if ( + this.frm.doc.target_asset && + this.frm.doc.capitalization_method === "Choose a WIP composite asset" + ) { this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); this.get_target_asset_details(); } @@ -119,17 +134,24 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s }, callback: function (r) { if (!r.exc && r.message) { - me.frm.clear_table("stock_items"); - - for (let item of r.message) { - me.frm.add_child("stock_items", item); + if (r.message[0] && r.message[0].length) { + me.frm.clear_table("stock_items"); + for (let item of r.message[0]) { + me.frm.add_child("stock_items", item); + } + refresh_field("stock_items"); + } + if (r.message[1] && r.message[1].length) { + me.frm.clear_table("asset_items"); + for (let item of r.message[1]) { + me.frm.add_child("asset_items", item); + } + me.frm.refresh_field("asset_items"); } - - refresh_field("stock_items"); me.calculate_totals(); } - } + }, }); } } @@ -161,7 +183,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s if (this.frm.doc.posting_date) { frappe.run_serially([ () => this.get_all_item_warehouse_details(), - () => this.get_all_asset_values() + () => this.get_all_asset_values(), ]); } } @@ -217,15 +239,15 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } stock_items_add(doc, cdt, cdn) { - erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'stock_items'); + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, "stock_items"); } asset_items_add(doc, cdt, cdn) { - erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'asset_items'); + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, "asset_items"); } serivce_items_add(doc, cdt, cdn) { - erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'service_items'); + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, "service_items"); } get_target_item_details() { @@ -243,7 +265,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s if (!r.exc) { me.frm.refresh_fields(); } - } + }, }); } } @@ -263,7 +285,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s if (!r.exc) { me.frm.refresh_fields(); } - } + }, }); } } @@ -285,13 +307,13 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s company: me.frm.doc.company, posting_date: me.frm.doc.posting_date, posting_time: me.frm.doc.posting_time, - } + }, }, callback: function (r) { if (!r.exc) { me.calculate_totals(); } - } + }, }); } } @@ -312,13 +334,13 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s finance_book: row.finance_book || me.frm.doc.finance_book, posting_date: me.frm.doc.posting_date, posting_time: me.frm.doc.posting_time, - } + }, }, callback: function (r) { if (!r.exc) { me.calculate_totals(); } - } + }, }); } } @@ -336,13 +358,13 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s qty: flt(row.qty), expense_account: row.expense_account, company: me.frm.doc.company, - } + }, }, callback: function (r) { if (!r.exc) { me.calculate_totals(); } - } + }, }); } } @@ -355,23 +377,23 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s child: item, args: { args: { - 'item_code': item.item_code, - 'warehouse': cstr(item.warehouse), - 'qty': flt(item.stock_qty), - 'serial_no': item.serial_no, - 'posting_date': me.frm.doc.posting_date, - 'posting_time': me.frm.doc.posting_time, - 'company': me.frm.doc.company, - 'voucher_type': me.frm.doc.doctype, - 'voucher_no': me.frm.doc.name, - 'allow_zero_valuation': 1 - } + item_code: item.item_code, + warehouse: cstr(item.warehouse), + qty: flt(item.stock_qty), + serial_no: item.serial_no, + posting_date: me.frm.doc.posting_date, + posting_time: me.frm.doc.posting_time, + company: me.frm.doc.company, + voucher_type: me.frm.doc.doctype, + voucher_no: me.frm.doc.name, + allow_zero_valuation: 1, + }, }, - callback: function(r) { + callback: function (r) { if (!r.exc) { me.calculate_totals(); } - } + }, }); } } @@ -381,11 +403,11 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s return me.frm.call({ method: "set_warehouse_details", doc: me.frm.doc, - callback: function(r) { + callback: function (r) { if (!r.exc) { me.calculate_totals(); } - } + }, }); } @@ -394,11 +416,11 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s return me.frm.call({ method: "set_asset_values", doc: me.frm.doc, - callback: function(r) { + callback: function (r) { if (!r.exc) { me.calculate_totals(); } - } + }, }); } @@ -410,33 +432,38 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s me.frm.doc.service_items_total = 0; $.each(me.frm.doc.stock_items || [], function (i, d) { - d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), precision('amount', d)); + d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), precision("amount", d)); me.frm.doc.stock_items_total += d.amount; }); $.each(me.frm.doc.asset_items || [], function (i, d) { - d.asset_value = flt(flt(d.asset_value), precision('asset_value', d)); + d.asset_value = flt(flt(d.asset_value), precision("asset_value", d)); me.frm.doc.asset_items_total += d.asset_value; }); $.each(me.frm.doc.service_items || [], function (i, d) { - d.amount = flt(flt(d.qty) * flt(d.rate), precision('amount', d)); + d.amount = flt(flt(d.qty) * flt(d.rate), precision("amount", d)); me.frm.doc.service_items_total += d.amount; }); - me.frm.doc.stock_items_total = flt(me.frm.doc.stock_items_total, precision('stock_items_total')); - me.frm.doc.asset_items_total = flt(me.frm.doc.asset_items_total, precision('asset_items_total')); - me.frm.doc.service_items_total = flt(me.frm.doc.service_items_total, precision('service_items_total')); + me.frm.doc.stock_items_total = flt(me.frm.doc.stock_items_total, precision("stock_items_total")); + me.frm.doc.asset_items_total = flt(me.frm.doc.asset_items_total, precision("asset_items_total")); + me.frm.doc.service_items_total = flt( + me.frm.doc.service_items_total, + precision("service_items_total") + ); - me.frm.doc.total_value = me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total; - me.frm.doc.total_value = flt(me.frm.doc.total_value, precision('total_value')); + me.frm.doc.total_value = + me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total; + me.frm.doc.total_value = flt(me.frm.doc.total_value, precision("total_value")); - me.frm.doc.target_qty = flt(me.frm.doc.target_qty, precision('target_qty')); - me.frm.doc.target_incoming_rate = me.frm.doc.target_qty ? me.frm.doc.total_value / flt(me.frm.doc.target_qty) + me.frm.doc.target_qty = flt(me.frm.doc.target_qty, precision("target_qty")); + me.frm.doc.target_incoming_rate = me.frm.doc.target_qty + ? me.frm.doc.total_value / flt(me.frm.doc.target_qty) : me.frm.doc.total_value; me.frm.refresh_fields(); } }; -cur_frm.cscript = new erpnext.assets.AssetCapitalization({frm: cur_frm}); +cur_frm.cscript = new erpnext.assets.AssetCapitalization({ frm: cur_frm }); diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 92cb85d1b7c..bfeeb75a642 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -72,11 +72,29 @@ class AssetCapitalization(StockController): self.update_target_asset() def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Repost Item Valuation", + "Asset", + "Asset Movement", + ) + self.cancel_target_asset() self.update_stock_ledger() self.make_gl_entries() self.restore_consumed_asset_items() + def on_trash(self): + frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None) + super().on_trash() + + def cancel_target_asset(self): + if self.entry_type == "Capitalization" and self.target_asset: + asset_doc = frappe.get_doc("Asset", self.target_asset) + asset_doc.db_set("capitalized_in", None) + if asset_doc.docstatus == 1: + asset_doc.cancel() + def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code @@ -161,7 +179,9 @@ class AssetCapitalization(StockController): if target_asset.item_code != self.target_item_code: frappe.throw( - _("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code) + _("Asset {0} does not belong to Item {1}").format( + self.target_asset, self.target_item_code + ) ) if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"): @@ -176,7 +196,9 @@ class AssetCapitalization(StockController): if target_asset.company != self.company: frappe.throw( - _("Target Asset {0} does not belong to company {1}").format(target_asset.name, self.company) + _("Target Asset {0} does not belong to company {1}").format( + target_asset.name, self.company + ) ) def validate_consumed_stock_item(self): @@ -206,13 +228,17 @@ class AssetCapitalization(StockController): if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): frappe.throw( - _("Row #{0}: Consumed Asset {1} cannot be {2}").format(d.idx, asset.name, asset.status) + _("Row #{0}: Consumed Asset {1} cannot be {2}").format( + d.idx, asset.name, asset.status + ) ) if asset.docstatus == 0: frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be Draft").format(d.idx, asset.name)) elif asset.docstatus == 2: - frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be cancelled").format(d.idx, asset.name)) + frappe.throw( + _("Row #{0}: Consumed Asset {1} cannot be cancelled").format(d.idx, asset.name) + ) if asset.company != self.company: frappe.throw( @@ -369,9 +395,7 @@ class AssetCapitalization(StockController): elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - def get_gl_entries( - self, warehouse_account=None, default_expense_account=None, default_cost_center=None - ): + def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): # Stock GL Entries gl_entries = [] @@ -385,15 +409,9 @@ class AssetCapitalization(StockController): target_account = self.get_target_account() target_against = set() - self.get_gl_entries_for_consumed_stock_items( - gl_entries, target_account, target_against, precision - ) - self.get_gl_entries_for_consumed_asset_items( - gl_entries, target_account, target_against, precision - ) - self.get_gl_entries_for_consumed_service_items( - gl_entries, target_account, target_against, precision - ) + self.get_gl_entries_for_consumed_stock_items(gl_entries, target_account, target_against, precision) + self.get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against, precision) + self.get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against, precision) self.get_gl_entries_for_target_item(gl_entries, target_against, precision) @@ -405,9 +423,7 @@ class AssetCapitalization(StockController): else: return self.warehouse_account[self.target_warehouse]["account"] - def get_gl_entries_for_consumed_stock_items( - self, gl_entries, target_account, target_against, precision - ): + def get_gl_entries_for_consumed_stock_items(self, gl_entries, target_account, target_against, precision): # Consumed Stock Items for item_row in self.stock_items: sle_list = self.sle_map.get(item_row.name) @@ -436,9 +452,7 @@ class AssetCapitalization(StockController): ) ) - def get_gl_entries_for_consumed_asset_items( - self, gl_entries, target_account, target_against, precision - ): + def get_gl_entries_for_consumed_asset_items(self, gl_entries, target_account, target_against, precision): # Consumed Assets for item in self.asset_items: asset = frappe.get_doc("Asset", item.asset) @@ -554,9 +568,9 @@ class AssetCapitalization(StockController): ) frappe.msgprint( - _( - "Asset {0} has been created. Please set the depreciation details if any and submit it." - ).format(get_link_to_form("Asset", asset_doc.name)) + _("Asset {0} has been created. Please set the depreciation details if any and submit it.").format( + get_link_to_form("Asset", asset_doc.name) + ) ) def update_target_asset(self): @@ -576,9 +590,9 @@ class AssetCapitalization(StockController): asset_doc.save() frappe.msgprint( - _( - "Asset {0} has been updated. Please set the depreciation details if any and submit it." - ).format(get_link_to_form("Asset", asset_doc.name)) + _("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format( + get_link_to_form("Asset", asset_doc.name) + ) ) def restore_consumed_asset_items(self): @@ -691,9 +705,7 @@ def get_consumed_stock_item_details(args): item_defaults = get_item_defaults(item.name, args.company) item_group_defaults = get_item_group_defaults(item.name, args.company) brand_defaults = get_brand_defaults(item.name, args.company) - out.cost_center = get_default_cost_center( - args, item_defaults, item_group_defaults, brand_defaults - ) + out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) if args.item_code and out.warehouse: incoming_rate_args = frappe._dict( @@ -779,10 +791,7 @@ def get_consumed_asset_details(args): item_defaults = get_item_defaults(item.name, args.company) item_group_defaults = get_item_group_defaults(item.name, args.company) brand_defaults = get_brand_defaults(item.name, args.company) - out.cost_center = get_default_cost_center( - args, item_defaults, item_group_defaults, brand_defaults - ) - + out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) return out @@ -809,9 +818,7 @@ def get_service_item_details(args): out.expense_account = get_default_expense_account( args, item_defaults, item_group_defaults, brand_defaults ) - out.cost_center = get_default_cost_center( - args, item_defaults, item_group_defaults, brand_defaults - ) + out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) return out @@ -830,10 +837,27 @@ def get_items_tagged_to_wip_composite_asset(asset): "qty", "valuation_rate", "amount", + "is_fixed_asset", + "parent", ] pr_items = frappe.get_all( - "Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields + "Purchase Receipt Item", filters={"wip_composite_asset": asset, "docstatus": 1}, fields=fields ) - return pr_items + stock_items = [] + asset_items = [] + for d in pr_items: + if not d.is_fixed_asset: + stock_items.append(frappe._dict(d)) + else: + asset_details = frappe.db.get_value( + "Asset", + {"item_code": d.item_code, "purchase_receipt": d.parent}, + ["name as asset", "asset_name"], + as_dict=1, + ) + d.update(asset_details) + asset_items.append(frappe._dict(d)) + + return stock_items, asset_items diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 59b65ec3fd0..128e2a7fe8b 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -346,9 +346,7 @@ class TestAssetCapitalization(unittest.TestCase): consumed_depreciation_schedule = [ d for d in consumed_asset.schedules if getdate(d.schedule_date) == getdate(capitalization_date) ] - self.assertTrue( - consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry - ) + self.assertTrue(consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry) self.assertEqual( consumed_depreciation_schedule[0].depreciation_amount, depreciation_before_disposal_amount ) @@ -371,15 +369,9 @@ class TestAssetCapitalization(unittest.TestCase): def create_asset_capitalization_data(): - create_item( - "Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0 - ) - create_item( - "Capitalization Source Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0 - ) - create_item( - "Capitalization Source Service Item", is_stock_item=0, is_fixed_asset=0, is_purchase_item=0 - ) + create_item("Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0) + create_item("Capitalization Source Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0) + create_item("Capitalization Source Service Item", is_stock_item=0, is_fixed_asset=0, is_purchase_item=0) def create_asset_capitalization(**args): diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js index 7dde14ea0e6..046b62f244e 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.js +++ b/erpnext/assets/doctype/asset_category/asset_category.js @@ -1,56 +1,55 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Asset Category', { - onload: function(frm) { - frm.add_fetch('company_name', 'accumulated_depreciation_account', 'accumulated_depreciation_account'); - frm.add_fetch('company_name', 'depreciation_expense_account', 'depreciation_expense_account'); +frappe.ui.form.on("Asset Category", { + onload: function (frm) { + frm.add_fetch("company_name", "accumulated_depreciation_account", "accumulated_depreciation_account"); + frm.add_fetch("company_name", "depreciation_expense_account", "depreciation_expense_account"); - frm.set_query('fixed_asset_account', 'accounts', function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; + frm.set_query("fixed_asset_account", "accounts", function (doc, cdt, cdn) { + var d = locals[cdt][cdn]; return { - "filters": { - "account_type": "Fixed Asset", - "root_type": "Asset", - "is_group": 0, - "company": d.company_name - } + filters: { + account_type: "Fixed Asset", + root_type: "Asset", + is_group: 0, + company: d.company_name, + }, }; }); - frm.set_query('accumulated_depreciation_account', 'accounts', function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; + frm.set_query("accumulated_depreciation_account", "accounts", function (doc, cdt, cdn) { + var d = locals[cdt][cdn]; return { - "filters": { - "account_type": "Accumulated Depreciation", - "is_group": 0, - "company": d.company_name - } + filters: { + account_type: "Accumulated Depreciation", + is_group: 0, + company: d.company_name, + }, }; }); - frm.set_query('depreciation_expense_account', 'accounts', function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; + frm.set_query("depreciation_expense_account", "accounts", function (doc, cdt, cdn) { + var d = locals[cdt][cdn]; return { - "filters": { - "account_type": "Depreciation", - "root_type": ["in", ["Expense", "Income"]], - "is_group": 0, - "company": d.company_name - } + filters: { + account_type: "Depreciation", + root_type: ["in", ["Expense", "Income"]], + is_group: 0, + company: d.company_name, + }, }; }); - frm.set_query('capital_work_in_progress_account', 'accounts', function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; + frm.set_query("capital_work_in_progress_account", "accounts", function (doc, cdt, cdn) { + var d = locals[cdt][cdn]; return { - "filters": { - "account_type": "Capital Work in Progress", - "is_group": 0, - "company": d.company_name - } + filters: { + account_type: "Capital Work in Progress", + is_group: 0, + company: d.company_name, + }, }; }); - - } + }, }); diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 8d351412ca8..1beb423ba25 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -38,7 +38,9 @@ class AssetCategory(Document): account_currency = frappe.get_value("Account", d.get(type_of_account), "account_currency") if account_currency != company_currency: invalid_accounts.append( - frappe._dict({"type": type_of_account, "idx": d.idx, "account": d.get(type_of_account)}) + frappe._dict( + {"type": type_of_account, "idx": d.idx, "account": d.get(type_of_account)} + ) ) for d in invalid_accounts: @@ -67,12 +69,12 @@ class AssetCategory(Document): if selected_key_type not in expected_key_types: frappe.throw( _( - "Row #{}: {} of {} should be {}. Please modify the account or select a different account." + "Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account." ).format( d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), - frappe.bold(expected_key_types), + frappe.bold(" or ".join(expected_key_types)), ), title=_("Invalid Account"), ) diff --git a/erpnext/assets/doctype/asset_category/test_asset_category.py b/erpnext/assets/doctype/asset_category/test_asset_category.py index 2c926565768..516e27e00fa 100644 --- a/erpnext/assets/doctype/asset_category/test_asset_category.py +++ b/erpnext/assets/doctype/asset_category/test_asset_category.py @@ -31,9 +31,7 @@ class TestAssetCategory(unittest.TestCase): pass def test_cwip_accounting(self): - company_cwip_acc = frappe.db.get_value( - "Company", "_Test Company", "capital_work_in_progress_account" - ) + frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account") frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "") asset_category = frappe.new_doc("Asset Category") diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index 172b81d98b0..9e5dd3e1a68 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -90,7 +90,6 @@ }, { "default": "0", - "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"", "fieldname": "daily_prorata_based", "fieldtype": "Check", "label": "Depreciate based on daily pro-rata" @@ -106,7 +105,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-29 03:53:03.591098", + "modified": "2023-12-29 08:49:39.876439", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 5c03b98873b..83dabab8935 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -1,49 +1,47 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Asset Maintenance', { +frappe.ui.form.on("Asset Maintenance", { setup: (frm) => { - frm.set_query("assign_to", "asset_maintenance_tasks", function(doc) { + frm.set_query("assign_to", "asset_maintenance_tasks", function (doc) { return { query: "erpnext.assets.doctype.asset_maintenance.asset_maintenance.get_team_members", filters: { - maintenance_team: doc.maintenance_team - } + maintenance_team: doc.maintenance_team, + }, }; }); - frm.set_indicator_formatter('maintenance_status', - function(doc) { - let indicator = 'blue'; - if (doc.maintenance_status == 'Overdue') { - indicator = 'orange'; - } - if (doc.maintenance_status == 'Cancelled') { - indicator = 'red'; - } - return indicator; + frm.set_indicator_formatter("maintenance_status", function (doc) { + let indicator = "blue"; + if (doc.maintenance_status == "Overdue") { + indicator = "orange"; } - ); + if (doc.maintenance_status == "Cancelled") { + indicator = "red"; + } + return indicator; + }); }, refresh: (frm) => { - if(!frm.is_new()) { - frm.trigger('make_dashboard'); + if (!frm.is_new()) { + frm.trigger("make_dashboard"); } }, make_dashboard: (frm) => { - if(!frm.is_new()) { + if (!frm.is_new()) { frappe.call({ - method: 'erpnext.assets.doctype.asset_maintenance.asset_maintenance.get_maintenance_log', - args: {asset_name: frm.doc.asset_name}, + method: "erpnext.assets.doctype.asset_maintenance.asset_maintenance.get_maintenance_log", + args: { asset_name: frm.doc.asset_name }, callback: (r) => { - if(!r.message) { + if (!r.message) { return; } - const section = frm.dashboard.add_section('', __("Maintenance Log")); - var rows = $('
    ').appendTo(section); + const section = frm.dashboard.add_section("", __("Maintenance Log")); + var rows = $("
    ").appendTo(section); // show - (r.message || []).forEach(function(d) { + (r.message || []).forEach(function (d) { $(`
    { get_next_due_date(frm, cdt, cdn); }, - periodicity: (frm, cdt, cdn) => { + periodicity: (frm, cdt, cdn) => { get_next_due_date(frm, cdt, cdn); }, - last_completion_date: (frm, cdt, cdn) => { + last_completion_date: (frm, cdt, cdn) => { get_next_due_date(frm, cdt, cdn); }, - end_date: (frm, cdt, cdn) => { + end_date: (frm, cdt, cdn) => { get_next_due_date(frm, cdt, cdn); - } + }, }); var get_next_due_date = function (frm, cdt, cdn) { var d = locals[cdt][cdn]; if (d.start_date && d.periodicity) { return frappe.call({ - method: 'erpnext.assets.doctype.asset_maintenance.asset_maintenance.calculate_next_due_date', + method: "erpnext.assets.doctype.asset_maintenance.asset_maintenance.calculate_next_due_date", args: { start_date: d.start_date, periodicity: d.periodicity, end_date: d.end_date, last_completion_date: d.last_completion_date, - next_due_date: d.next_due_date + next_due_date: d.next_due_date, }, - callback: function(r) { + callback: function (r) { if (r.message) { frappe.model.set_value(cdt, cdn, "next_due_date", r.message); - } - else { + } else { frappe.model.set_value(cdt, cdn, "next_due_date", ""); } - } + }, }); } }; diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 5c40072086e..7762e63d779 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -70,9 +70,7 @@ def calculate_next_due_date( if not start_date and not last_completion_date: start_date = frappe.utils.now() - if last_completion_date and ( - (start_date and last_completion_date > start_date) or not start_date - ): + if last_completion_date and ((start_date and last_completion_date > start_date) or not start_date): start_date = last_completion_date if periodicity == "Daily": next_due_date = add_days(start_date, 1) diff --git a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py index e40a5519eb2..dcc2f4d32e6 100644 --- a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py @@ -17,9 +17,7 @@ class TestAssetMaintenance(unittest.TestCase): create_maintenance_team() def test_create_asset_maintenance(self): - pr = make_purchase_receipt( - item_code="Photocopier", qty=1, rate=100000.0, location="Test Location" - ) + pr = make_purchase_receipt(item_code="Photocopier", qty=1, rate=100000.0, location="Test Location") asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") asset_doc = frappe.get_doc("Asset", asset_name) @@ -130,8 +128,7 @@ def create_maintenance_team(): def get_maintenance_team(user_list): return [ - {"team_member": user, "full_name": user, "maintenance_role": "Technician"} - for user in user_list[1:] + {"team_member": user, "full_name": user, "maintenance_role": "Technician"} for user in user_list[1:] ] diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.js b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.js index bcdc3acf0ac..47a2128c379 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.js +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.js @@ -1,15 +1,15 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Asset Maintenance Log', { +frappe.ui.form.on("Asset Maintenance Log", { asset_maintenance: (frm) => { - frm.set_query('task', function(doc) { + frm.set_query("task", function (doc) { return { query: "erpnext.assets.doctype.asset_maintenance_log.asset_maintenance_log.get_maintenance_tasks", filters: { - 'asset_maintenance': doc.asset_maintenance - } + asset_maintenance: doc.asset_maintenance, + }, }; }); - } + }, }); diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_calendar.js b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_calendar.js index c804b31e908..7b7b5085369 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_calendar.js +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_calendar.js @@ -3,20 +3,20 @@ frappe.views.calendar["Asset Maintenance Log"] = { field_map: { - "start": "due_date", - "end": "due_date", - "id": "name", - "title": "task", - "allDay": "allDay", - "progress": "progress" + start: "due_date", + end: "due_date", + id: "name", + title: "task", + allDay: "allDay", + progress: "progress", }, filters: [ { - "fieldtype": "Link", - "fieldname": "asset_name", - "options": "Asset Maintenance", - "label": __("Asset Maintenance") - } + fieldtype: "Link", + fieldname: "asset_name", + options: "Asset Maintenance", + label: __("Asset Maintenance"), + }, ], - get_events_method: "frappe.desk.calendar.get_events" + get_events_method: "frappe.desk.calendar.get_events", }; diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js index 23000e60eff..13f2444742d 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js @@ -1,15 +1,15 @@ -frappe.listview_settings['Asset Maintenance Log'] = { +frappe.listview_settings["Asset Maintenance Log"] = { add_fields: ["maintenance_status"], has_indicator_for_draft: 1, - get_indicator: function(doc) { - if (doc.maintenance_status=="Planned") { + get_indicator: function (doc) { + if (doc.maintenance_status == "Planned") { return [__(doc.maintenance_status), "orange", "status,=," + doc.maintenance_status]; - } else if (doc.maintenance_status=="Completed") { + } else if (doc.maintenance_status == "Completed") { return [__(doc.maintenance_status), "green", "status,=," + doc.maintenance_status]; - } else if (doc.maintenance_status=="Cancelled") { + } else if (doc.maintenance_status == "Cancelled") { return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status]; - } else if (doc.maintenance_status=="Overdue") { + } else if (doc.maintenance_status == "Overdue") { return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status]; } - } + }, }; diff --git a/erpnext/assets/doctype/asset_maintenance_team/asset_maintenance_team.js b/erpnext/assets/doctype/asset_maintenance_team/asset_maintenance_team.js index c94e3dbc3c0..85987651900 100644 --- a/erpnext/assets/doctype/asset_maintenance_team/asset_maintenance_team.js +++ b/erpnext/assets/doctype/asset_maintenance_team/asset_maintenance_team.js @@ -1,8 +1,6 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Asset Maintenance Team', { - refresh: function() { - - } +frappe.ui.form.on("Asset Maintenance Team", { + refresh: function () {}, }); diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index 4ccc3f8013b..e445c90f308 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -1,104 +1,107 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Asset Movement', { +frappe.ui.form.on("Asset Movement", { setup: (frm) => { frm.set_query("to_employee", "assets", (doc) => { return { filters: { - company: doc.company - } + company: doc.company, + }, }; - }) + }); frm.set_query("from_employee", "assets", (doc) => { return { filters: { - company: doc.company - } + company: doc.company, + }, }; - }) + }); frm.set_query("reference_name", (doc) => { return { filters: { company: doc.company, - docstatus: 1 - } + docstatus: 1, + }, }; - }) + }); frm.set_query("reference_doctype", () => { return { filters: { - name: ["in", ["Purchase Receipt", "Purchase Invoice"]] - } + name: ["in", ["Purchase Receipt", "Purchase Invoice"]], + }, }; }), - frm.set_query("asset", "assets", () => { - return { - filters: { - status: ["not in", ["Draft"]] - } - } - }) + frm.set_query("asset", "assets", () => { + return { + filters: { + status: ["not in", ["Draft"]], + }, + }; + }); }, onload: (frm) => { - frm.trigger('set_required_fields'); + frm.trigger("set_required_fields"); }, purpose: (frm) => { - frm.trigger('set_required_fields'); + frm.trigger("set_required_fields"); }, set_required_fields: (frm, cdt, cdn) => { let fieldnames_to_be_altered; - if (frm.doc.purpose === 'Transfer') { + if (frm.doc.purpose === "Transfer") { fieldnames_to_be_altered = { target_location: { read_only: 0, reqd: 1 }, source_location: { read_only: 1, reqd: 1 }, from_employee: { read_only: 1, reqd: 0 }, - to_employee: { read_only: 1, reqd: 0 } + to_employee: { read_only: 1, reqd: 0 }, }; - } - else if (frm.doc.purpose === 'Receipt') { + } else if (frm.doc.purpose === "Receipt") { fieldnames_to_be_altered = { target_location: { read_only: 0, reqd: 1 }, source_location: { read_only: 1, reqd: 0 }, from_employee: { read_only: 0, reqd: 0 }, - to_employee: { read_only: 1, reqd: 0 } + to_employee: { read_only: 1, reqd: 0 }, }; - } - else if (frm.doc.purpose === 'Issue') { + } else if (frm.doc.purpose === "Issue") { fieldnames_to_be_altered = { target_location: { read_only: 1, reqd: 0 }, source_location: { read_only: 1, reqd: 0 }, from_employee: { read_only: 1, reqd: 0 }, - to_employee: { read_only: 0, reqd: 1 } + to_employee: { read_only: 0, reqd: 1 }, }; } if (fieldnames_to_be_altered) { - Object.keys(fieldnames_to_be_altered).forEach(fieldname => { + Object.keys(fieldnames_to_be_altered).forEach((fieldname) => { let property_to_be_altered = fieldnames_to_be_altered[fieldname]; - Object.keys(property_to_be_altered).forEach(property => { + Object.keys(property_to_be_altered).forEach((property) => { let value = property_to_be_altered[property]; - frm.fields_dict['assets'].grid.update_docfield_property(fieldname, property, value); + frm.fields_dict["assets"].grid.update_docfield_property(fieldname, property, value); }); }); - frm.refresh_field('assets'); + frm.refresh_field("assets"); } - } + }, }); -frappe.ui.form.on('Asset Movement Item', { - asset: function(frm, cdt, cdn) { +frappe.ui.form.on("Asset Movement Item", { + asset: function (frm, cdt, cdn) { // on manual entry of an asset auto sets their source location / employee const asset_name = locals[cdt][cdn].asset; - if (asset_name){ - frappe.db.get_doc('Asset', asset_name).then((asset_doc) => { - if(asset_doc.location) frappe.model.set_value(cdt, cdn, 'source_location', asset_doc.location); - if(asset_doc.custodian) frappe.model.set_value(cdt, cdn, 'from_employee', asset_doc.custodian); - }).catch((err) => { - console.log(err); // eslint-disable-line - }); + if (asset_name) { + frappe.db + .get_doc("Asset", asset_name) + .then((asset_doc) => { + if (asset_doc.location) + frappe.model.set_value(cdt, cdn, "source_location", asset_doc.location); + if (asset_doc.custodian) + frappe.model.set_value(cdt, cdn, "from_employee", asset_doc.custodian); + }) + .catch((err) => { + console.log(err); // eslint-disable-line + }); } - } + }, }); diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index b85f7194f98..db9cb02a61e 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -32,7 +32,9 @@ class AssetMovement(Document): if d.source_location: if current_location != d.source_location: frappe.throw( - _("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location) + _("Asset {0} does not belongs to the location {1}").format( + d.asset, d.source_location + ) ) else: d.source_location = current_location @@ -57,19 +59,25 @@ class AssetMovement(Document): title=_("Incorrect Movement Purpose"), ) if not d.target_location: - frappe.throw(_("Target Location is required while transferring Asset {0}").format(d.asset)) + frappe.throw( + _("Target Location is required while transferring Asset {0}").format(d.asset) + ) if d.source_location == d.target_location: frappe.throw(_("Source and Target Location cannot be same")) if self.purpose == "Receipt": if not (d.source_location) and not (d.target_location or d.to_employee): frappe.throw( - _("Target Location or To Employee is required while receiving Asset {0}").format(d.asset) + _("Target Location or To Employee is required while receiving Asset {0}").format( + d.asset + ) ) elif d.source_location: if d.from_employee and not d.target_location: frappe.throw( - _("Target Location is required while receiving Asset {0} from an employee").format(d.asset) + _( + "Target Location is required while receiving Asset {0} from an employee" + ).format(d.asset) ) elif d.to_employee and d.target_location: frappe.throw( @@ -109,19 +117,17 @@ class AssetMovement(Document): # latest entry corresponds to current document's location, employee when transaction date > previous dates # In case of cancellation it corresponds to previous latest document's location, employee latest_movement_entry = frappe.db.sql( - """ + f""" SELECT asm_item.target_location, asm_item.to_employee FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm WHERE asm_item.parent=asm.name and asm_item.asset=%(asset)s and asm.company=%(company)s and - asm.docstatus=1 and {0} + asm.docstatus=1 and {cond} ORDER BY asm.transaction_date desc limit 1 - """.format( - cond - ), + """, args, ) if latest_movement_entry: diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 27e7e557f19..52590d2ba86 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -20,9 +20,7 @@ class TestAssetMovement(unittest.TestCase): make_location() def test_movement(self): - pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" - ) + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") asset = frappe.get_doc("Asset", asset_name) @@ -51,7 +49,11 @@ class TestAssetMovement(unittest.TestCase): purpose="Transfer", company=asset.company, assets=[ - {"asset": asset.name, "source_location": "Test Location", "target_location": "Test Location 2"} + { + "asset": asset.name, + "source_location": "Test Location", + "target_location": "Test Location 2", + } ], reference_doctype="Purchase Receipt", reference_name=pr.name, @@ -62,7 +64,11 @@ class TestAssetMovement(unittest.TestCase): purpose="Transfer", company=asset.company, assets=[ - {"asset": asset.name, "source_location": "Test Location 2", "target_location": "Test Location"} + { + "asset": asset.name, + "source_location": "Test Location 2", + "target_location": "Test Location", + } ], reference_doctype="Purchase Receipt", reference_name=pr.name, @@ -97,9 +103,7 @@ class TestAssetMovement(unittest.TestCase): self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") def test_last_movement_cancellation(self): - pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" - ) + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") asset = frappe.get_doc("Asset", asset_name) @@ -129,7 +133,11 @@ class TestAssetMovement(unittest.TestCase): purpose="Transfer", company=asset.company, assets=[ - {"asset": asset.name, "source_location": "Test Location", "target_location": "Test Location 2"} + { + "asset": asset.name, + "source_location": "Test Location", + "target_location": "Test Location 2", + } ], reference_doctype="Purchase Receipt", reference_name=pr.name, @@ -167,6 +175,4 @@ def create_asset_movement(**args): def make_location(): for location in ["Pune", "Mumbai", "Nagpur"]: if not frappe.db.exists("Location", location): - frappe.get_doc({"doctype": "Location", "location_name": location}).insert( - ignore_permissions=True - ) + frappe.get_doc({"doctype": "Location", "location_name": location}).insert(ignore_permissions=True) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index f9ed2cc3448..0ce178f4e6b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -1,40 +1,40 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Asset Repair', { - setup: function(frm) { - frm.fields_dict.cost_center.get_query = function(doc) { +frappe.ui.form.on("Asset Repair", { + setup: function (frm) { + frm.fields_dict.cost_center.get_query = function (doc) { return { filters: { - 'is_group': 0, - 'company': doc.company - } + is_group: 0, + company: doc.company, + }, }; }; - frm.fields_dict.project.get_query = function(doc) { + frm.fields_dict.project.get_query = function (doc) { return { filters: { - 'company': doc.company - } + company: doc.company, + }, }; }; - frm.fields_dict.warehouse.get_query = function(doc) { + frm.fields_dict.warehouse.get_query = function (doc) { return { filters: { - 'is_group': 0, - 'company': doc.company - } + is_group: 0, + company: doc.company, + }, }; }; }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.docstatus) { - frm.add_custom_button(__("View General Ledger"), function() { + frm.add_custom_button(__("View General Ledger"), function () { frappe.route_options = { - "voucher_no": frm.doc.name + voucher_no: frm.doc.name, }; frappe.set_route("query-report", "General Ledger"); }); @@ -43,55 +43,55 @@ frappe.ui.form.on('Asset Repair', { repair_status: (frm) => { if (frm.doc.completion_date && frm.doc.repair_status == "Completed") { - frappe.call ({ + frappe.call({ method: "erpnext.assets.doctype.asset_repair.asset_repair.get_downtime", args: { - "failure_date":frm.doc.failure_date, - "completion_date":frm.doc.completion_date + failure_date: frm.doc.failure_date, + completion_date: frm.doc.completion_date, }, - callback: function(r) { - if(r.message) { + callback: function (r) { + if (r.message) { frm.set_value("downtime", r.message + " Hrs"); } - } + }, }); } if (frm.doc.repair_status == "Completed") { - frm.set_value('completion_date', frappe.datetime.now_datetime()); + frm.set_value("completion_date", frappe.datetime.now_datetime()); } }, stock_items_on_form_rendered() { erpnext.setup_serial_or_batch_no(); - } + }, }); -frappe.ui.form.on('Asset Repair Consumed Item', { - item_code: function(frm, cdt, cdn) { +frappe.ui.form.on("Asset Repair Consumed Item", { + item_code: function (frm, cdt, cdn) { var item = locals[cdt][cdn]; let item_args = { - 'item_code': item.item_code, - 'warehouse': frm.doc.warehouse, - 'qty': item.consumed_quantity, - 'serial_no': item.serial_no, - 'company': frm.doc.company, + item_code: item.item_code, + warehouse: frm.doc.warehouse, + qty: item.consumed_quantity, + serial_no: item.serial_no, + company: frm.doc.company, }; frappe.call({ - method: 'erpnext.stock.utils.get_incoming_rate', + method: "erpnext.stock.utils.get_incoming_rate", args: { - args: item_args + args: item_args, + }, + callback: function (r) { + frappe.model.set_value(cdt, cdn, "valuation_rate", r.message); }, - callback: function(r) { - frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message); - } }); }, - consumed_quantity: function(frm, cdt, cdn) { + consumed_quantity: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; - frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); + frappe.model.set_value(cdt, cdn, "total_value", row.consumed_quantity * row.valuation_rate); }, }); diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 47453e30308..da933c96057 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -46,6 +46,10 @@ class AssetRepair(AccountsController): self.increase_asset_value() + if self.capitalize_repair_cost: + self.asset_doc.total_asset_cost += self.repair_cost + self.asset_doc.additional_asset_cost += self.repair_cost + if self.get("stock_consumption"): self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() @@ -68,6 +72,10 @@ class AssetRepair(AccountsController): self.decrease_asset_value() + if self.capitalize_repair_cost: + self.asset_doc.total_asset_cost -= self.repair_cost + self.asset_doc.additional_asset_cost -= self.repair_cost + if self.get("stock_consumption"): self.increase_stock_quantity() if self.get("capitalize_repair_cost"): @@ -90,9 +98,7 @@ class AssetRepair(AccountsController): def check_for_stock_items_and_warehouse(self): if not self.get("stock_items"): - frappe.throw( - _("Please enter Stock Items consumed during the Repair."), title=_("Missing Items") - ) + frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items")) if not self.warehouse: frappe.throw( _("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), @@ -165,9 +171,7 @@ class AssetRepair(AccountsController): def get_gl_entries(self): gl_entries = [] - fixed_asset_account = get_asset_account( - "fixed_asset_account", asset=self.asset, company=self.company - ) + fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company) self.get_gl_entries_for_repair_cost(gl_entries, fixed_asset_account) self.get_gl_entries_for_consumed_items(gl_entries, fixed_asset_account) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair_list.js b/erpnext/assets/doctype/asset_repair/asset_repair_list.js index 86376f40046..633c39ba77f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair_list.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair_list.js @@ -1,12 +1,12 @@ -frappe.listview_settings['Asset Repair'] = { +frappe.listview_settings["Asset Repair"] = { add_fields: ["repair_status"], - get_indicator: function(doc) { - if(doc.repair_status=="Pending") { + get_indicator: function (doc) { + if (doc.repair_status == "Pending") { return [__("Pending"), "orange"]; - } else if(doc.repair_status=="Completed") { + } else if (doc.repair_status == "Completed") { return [__("Completed"), "green"]; - } else if(doc.repair_status=="Cancelled") { + } else if (doc.repair_status == "Cancelled") { return [__("Cancelled"), "red"]; } - } + }, }; diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 3adb3516908..274c1768179 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -193,9 +193,7 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(expected_values[d.account][1], d.credit) def test_gl_entries_with_periodical_inventory(self): - frappe.db.set_value( - "Company", "_Test Company", "default_expense_account", "Cost of Goods Sold - _TC" - ) + frappe.db.set_value("Company", "_Test Company", "default_expense_account", "Cost of Goods Sold - _TC") asset_repair = create_asset_repair( capitalize_repair_cost=1, stock_consumption=1, @@ -275,9 +273,7 @@ def create_asset_repair(**args): if args.stock_consumption: asset_repair.stock_consumption = 1 - asset_repair.warehouse = args.warehouse or create_warehouse( - "Test Warehouse", company=asset.company - ) + asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company=asset.company) asset_repair.append( "stock_items", { diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js index 2bd41c13773..fd92ce7f2fe 100644 --- a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js @@ -1,15 +1,15 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Asset Shift Allocation', { - onload: function(frm) { +frappe.ui.form.on("Asset Shift Allocation", { + onload: function (frm) { frm.events.make_schedules_editable(frm); }, - make_schedules_editable: function(frm) { + make_schedules_editable: function (frm) { frm.toggle_enable("depreciation_schedule", true); frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", false); frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", false); frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", true); - } -}); \ No newline at end of file + }, +}); diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py index d9797b0c24c..e255c827f14 100644 --- a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py @@ -108,9 +108,7 @@ class AssetShiftAllocation(Document): def allocate_shift_diff_in_depr_schedule(self): asset_shift_factors_map = get_asset_shift_factors_map() - reverse_asset_shift_factors_map = { - asset_shift_factors_map[k]: k for k in asset_shift_factors_map - } + reverse_asset_shift_factors_map = {asset_shift_factors_map[k]: k for k in asset_shift_factors_map} original_shift_factors_sum = sum( flt(asset_shift_factors_map.get(schedule.shift)) for schedule in self.asset_doc.schedules @@ -139,9 +137,9 @@ class AssetShiftAllocation(Document): ) diff = 0 except Exception: - frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format( - shift_factor - diff - ) + frappe.throw( + _("Could not auto update shifts. Shift with shift factor {0} needed.") + ).format(shift_factor - diff) elif diff < 0: shift_factors = list(asset_shift_factors_map.values()) desc_shift_factors = sorted(shift_factors, reverse=True) @@ -202,9 +200,9 @@ class AssetShiftAllocation(Document): ) diff = 0 except Exception: - frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format( - shift_factor + diff - ) + frappe.throw( + _("Could not auto update shifts. Shift with shift factor {0} needed.") + ).format(shift_factor + diff) def update_asset_schedule(self): self.asset_doc.flags.shift_allocation = True @@ -239,9 +237,7 @@ def find_subsets_with_sum(numbers, k, target_sum, current_subset, result): return # Include the current number in the subset - find_subsets_with_sum( - numbers, k - 1, target_sum - numbers[0], current_subset + [numbers[0]], result - ) + find_subsets_with_sum(numbers, k - 1, target_sum - numbers[0], [*current_subset, numbers[0]], result) # Exclude the current number from the subset find_subsets_with_sum(numbers[1:], k, target_sum, current_subset, result) diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js index e6552d8370d..4795b0672ae 100644 --- a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js @@ -1,8 +1,7 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Asset Shift Factor', { +frappe.ui.form.on("Asset Shift Factor", { // refresh: function(frm) { - // } }); diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py index 4c275ce092c..d9bc2283f5b 100644 --- a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py @@ -12,9 +12,7 @@ class AssetShiftFactor(Document): def validate_default(self): if self.default: - existing_default_shift_factor = frappe.db.get_value( - "Asset Shift Factor", {"default": 1}, "name" - ) + existing_default_shift_factor = frappe.db.get_value("Asset Shift Factor", {"default": 1}, "name") if existing_default_shift_factor: frappe.throw( diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js index d07f40cdf42..7fbe6d0bd12 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js @@ -3,61 +3,61 @@ frappe.provide("erpnext.accounts.dimensions"); -frappe.ui.form.on('Asset Value Adjustment', { - setup: function(frm) { - frm.add_fetch('company', 'cost_center', 'cost_center'); - frm.set_query('cost_center', function() { +frappe.ui.form.on("Asset Value Adjustment", { + setup: function (frm) { + frm.add_fetch("company", "cost_center", "cost_center"); + frm.set_query("cost_center", function () { return { filters: { company: frm.doc.company, - is_group: 0 - } - } + is_group: 0, + }, + }; }); - frm.set_query('asset', function() { + frm.set_query("asset", function () { return { filters: { calculate_depreciation: 1, - docstatus: 1 - } + docstatus: 1, + }, }; }); }, - onload: function(frm) { - if(frm.is_new() && frm.doc.asset) { + onload: function (frm) { + if (frm.is_new() && frm.doc.asset) { frm.trigger("set_current_asset_value"); } erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, - company: function(frm) { + company: function (frm) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, - asset: function(frm) { + asset: function (frm) { frm.trigger("set_current_asset_value"); }, - finance_book: function(frm) { + finance_book: function (frm) { frm.trigger("set_current_asset_value"); }, - set_current_asset_value: function(frm) { + set_current_asset_value: function (frm) { if (frm.doc.asset) { frm.call({ method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation", args: { asset_name: frm.doc.asset, - finance_book: frm.doc.finance_book + finance_book: frm.doc.finance_book, }, - callback: function(r) { + callback: function (r) { if (r.message) { - frm.set_value('current_asset_value', r.message); + frm.set_value("current_asset_value", r.message); } - } + }, }); } - } + }, }); diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 29e7a9bdfd6..fca9bc33365 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -61,7 +61,7 @@ class AssetValueAdjustment(Document): je.naming_series = depreciation_series je.posting_date = self.date je.company = self.company - je.remark = "Depreciation Entry against {0} worth {1}".format(self.asset, self.difference_amount) + je.remark = f"Depreciation Entry against {self.asset} worth {self.difference_amount}" je.finance_book = self.finance_book credit_entry = { diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 8fdcd0c14df..7661e70fd17 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -20,9 +20,7 @@ class TestAssetValueAdjustment(unittest.TestCase): ) def test_current_asset_value(self): - pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" - ) + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") asset_doc = frappe.get_doc("Asset", asset_name) @@ -49,9 +47,7 @@ class TestAssetValueAdjustment(unittest.TestCase): self.assertEqual(current_value, 100000.0) def test_asset_depreciation_value_adjustment(self): - pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location" - ) + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location") asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") asset_doc = frappe.get_doc("Asset", asset_name) diff --git a/erpnext/assets/doctype/location/location.js b/erpnext/assets/doctype/location/location.js index 0f069b2fd8e..97d72391588 100644 --- a/erpnext/assets/doctype/location/location.js +++ b/erpnext/assets/doctype/location/location.js @@ -1,13 +1,13 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Location', { +frappe.ui.form.on("Location", { setup: function (frm) { frm.set_query("parent_location", function () { return { - "filters": { - "is_group": 1 - } + filters: { + is_group: 1, + }, }; }); }, @@ -15,10 +15,9 @@ frappe.ui.form.on('Location', { onload_post_render(frm) { if (!frm.doc.location && frm.doc.latitude && frm.doc.longitude) { frm.fields_dict.location.map.setView([frm.doc.latitude, frm.doc.longitude], 13); - } - else { - frm.doc.latitude = frm.fields_dict.location.map.getCenter()['lat']; - frm.doc.longitude = frm.fields_dict.location.map.getCenter()['lng']; + } else { + frm.doc.latitude = frm.fields_dict.location.map.getCenter()["lat"]; + frm.doc.longitude = frm.fields_dict.location.map.getCenter()["lng"]; } }, }); diff --git a/erpnext/assets/doctype/location/location.py b/erpnext/assets/doctype/location/location.py index 5bff3dd8c99..a4376a685d0 100644 --- a/erpnext/assets/doctype/location/location.py +++ b/erpnext/assets/doctype/location/location.py @@ -195,17 +195,15 @@ def get_children(doctype, parent=None, location=None, is_root=False): parent = "" return frappe.db.sql( - """ + f""" select name as value, is_group as expandable from `tabLocation` comp where - ifnull(parent_location, "")={parent} - """.format( - parent=frappe.db.escape(parent) - ), + ifnull(parent_location, "")={frappe.db.escape(parent)} + """, as_dict=1, ) diff --git a/erpnext/assets/doctype/location/location_tree.js b/erpnext/assets/doctype/location/location_tree.js index 3e105f6ca49..c3484c2469b 100644 --- a/erpnext/assets/doctype/location/location_tree.js +++ b/erpnext/assets/doctype/location/location_tree.js @@ -1,7 +1,7 @@ frappe.treeview_settings["Location"] = { ignore_fields: ["parent_location"], - get_tree_nodes: 'erpnext.assets.doctype.location.location.get_children', - add_tree_node: 'erpnext.assets.doctype.location.location.add_node', + get_tree_nodes: "erpnext.assets.doctype.location.location.get_children", + add_tree_node: "erpnext.assets.doctype.location.location.add_node", filters: [ { fieldname: "location", @@ -10,9 +10,9 @@ frappe.treeview_settings["Location"] = { label: __("Location"), get_query: function () { return { - filters: [["Location", "is_group", "=", 1]] + filters: [["Location", "is_group", "=", 1]], }; - } + }, }, ], breadcrumb: "Assets", @@ -24,10 +24,10 @@ frappe.treeview_settings["Location"] = { action: function () { frappe.new_doc("Location", true); }, - condition: 'frappe.boot.user.can_create.indexOf("Location") !== -1' - } + condition: 'frappe.boot.user.can_create.indexOf("Location") !== -1', + }, ], onload: function (treeview) { treeview.make_tree(); - } + }, }; diff --git a/erpnext/assets/doctype/location/test_location.py b/erpnext/assets/doctype/location/test_location.py index b8563cb0a29..3b5af61fd44 100644 --- a/erpnext/assets/doctype/location/test_location.py +++ b/erpnext/assets/doctype/location/test_location.py @@ -31,9 +31,7 @@ class TestLocation(unittest.TestCase): ordered_test_location_features = sorted( test_location_features, key=lambda x: x["properties"]["feature_of"] ) - ordered_formatted_locations = sorted( - formatted_locations, key=lambda x: x["properties"]["feature_of"] - ) + ordered_formatted_locations = sorted(formatted_locations, key=lambda x: x["properties"]["feature_of"]) self.assertEqual(ordered_formatted_locations, ordered_test_location_features) self.assertEqual(area, test_location.get("area")) diff --git a/erpnext/assets/doctype/maintenance_team_member/maintenance_team_member.js b/erpnext/assets/doctype/maintenance_team_member/maintenance_team_member.js index 2db712546c6..6f3521ffd85 100644 --- a/erpnext/assets/doctype/maintenance_team_member/maintenance_team_member.js +++ b/erpnext/assets/doctype/maintenance_team_member/maintenance_team_member.js @@ -1,8 +1,6 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Maintenance Team Member', { - refresh: function() { - - } +frappe.ui.form.on("Maintenance Team Member", { + refresh: function () {}, }); 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 0497addf44a..7e9ecd4d8fc 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -3,103 +3,103 @@ /* eslint-disable */ frappe.query_reports["Fixed Asset Register"] = { - "filters": [ + filters: [ { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { - fieldname:"status", + fieldname: "status", label: __("Status"), fieldtype: "Select", options: "\nIn Location\nDisposed", - default: 'In Location' + default: "In Location", }, { - fieldname:"asset_category", + fieldname: "asset_category", label: __("Asset Category"), fieldtype: "Link", - options: "Asset Category" + options: "Asset Category", }, { - fieldname:"cost_center", + fieldname: "cost_center", label: __("Cost Center"), fieldtype: "Link", - options: "Cost Center" + options: "Cost Center", }, { - fieldname:"group_by", + fieldname: "group_by", label: __("Group By"), fieldtype: "Select", options: ["--Select a group--", "Asset Category", "Location"], default: "--Select a group--", - reqd: 1 + reqd: 1, }, { - fieldname:"only_existing_assets", + fieldname: "only_existing_assets", label: __("Only existing assets"), - fieldtype: "Check" + fieldtype: "Check", }, { - fieldname:"finance_book", + fieldname: "finance_book", label: __("Finance Book"), fieldtype: "Link", options: "Finance Book", }, { - "fieldname": "include_default_book_assets", - "label": __("Include Default FB Assets"), - "fieldtype": "Check", - "default": 1 + fieldname: "include_default_book_assets", + label: __("Include Default FB Assets"), + fieldtype: "Check", + default: 1, }, { - "fieldname":"filter_based_on", - "label": __("Period Based On"), - "fieldtype": "Select", - "options": ["--Select a period--", "Fiscal Year", "Date Range"], - "default": "--Select a period--", + fieldname: "filter_based_on", + label: __("Period Based On"), + fieldtype: "Select", + options: ["--Select a period--", "Fiscal Year", "Date Range"], + default: "--Select a period--", }, { - "fieldname":"from_date", - "label": __("Start Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12), - "depends_on": "eval: doc.filter_based_on == 'Date Range'", + fieldname: "from_date", + label: __("Start Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.nowdate(), -12), + depends_on: "eval: doc.filter_based_on == 'Date Range'", }, { - "fieldname":"to_date", - "label": __("End Date"), - "fieldtype": "Date", - "default": frappe.datetime.nowdate(), - "depends_on": "eval: doc.filter_based_on == 'Date Range'", + fieldname: "to_date", + label: __("End Date"), + fieldtype: "Date", + default: frappe.datetime.nowdate(), + depends_on: "eval: doc.filter_based_on == 'Date Range'", }, { - "fieldname":"from_fiscal_year", - "label": __("Start Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", + fieldname: "from_fiscal_year", + label: __("Start Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + depends_on: "eval: doc.filter_based_on == 'Fiscal Year'", }, { - "fieldname":"to_fiscal_year", - "label": __("End Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", + fieldname: "to_fiscal_year", + label: __("End Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + depends_on: "eval: doc.filter_based_on == 'Fiscal Year'", }, { - "fieldname":"date_based_on", - "label": __("Date Based On"), - "fieldtype": "Select", - "options": ["Purchase Date", "Available For Use Date"], - "default": "Purchase Date", - "depends_on": "eval: doc.filter_based_on == 'Date Range' || doc.filter_based_on == 'Fiscal Year'", + fieldname: "date_based_on", + label: __("Date Based On"), + fieldtype: "Select", + options: ["Purchase Date", "Available For Use Date"], + default: "Purchase Date", + depends_on: "eval: doc.filter_based_on == 'Date Range' || doc.filter_based_on == 'Fiscal Year'", }, - ] + ], }; diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index 45811a93444..4ff04c80434 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -122,11 +122,7 @@ def get_data(filters): assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) for asset in assets_record: - if ( - assets_linked_to_fb - and asset.calculate_depreciation - and asset.asset_id not in assets_linked_to_fb - ): + if assets_linked_to_fb and asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb: continue asset_value = get_asset_value_after_depreciation( @@ -149,6 +145,7 @@ def get_data(filters): "asset_category": asset.asset_category, "purchase_date": asset.purchase_date, "asset_value": asset_value, + "company": asset.company, } data.append(row) @@ -240,9 +237,7 @@ def get_assets_linked_to_fb(filters): def get_asset_depreciation_amount_map(filters, finance_book): - start_date = ( - filters.from_date if filters.filter_based_on == "Date Range" else filters.year_start_date - ) + start_date = filters.from_date if filters.filter_based_on == "Date Range" else filters.year_start_date end_date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date asset = frappe.qb.DocType("Asset") @@ -259,9 +254,7 @@ def get_asset_depreciation_amount_map(filters, finance_book): .join(company) .on(company.name == asset.company) .select(asset.name.as_("asset"), Sum(gle.debit).as_("depreciation_amount")) - .where( - gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account) - ) + .where(gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)) .where(gle.debit != 0) .where(gle.is_cancelled == 0) .where(company.name == filters.company) @@ -280,9 +273,7 @@ def get_asset_depreciation_amount_map(filters, finance_book): else: query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized", "Decapitalized"])) if finance_book: - query = query.where( - (gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull()) - ) + query = query.where((gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull())) else: query = query.where((gle.finance_book.isin([""])) | (gle.finance_book.isnull())) if filters.filter_based_on in ("Date Range", "Fiscal Year"): @@ -379,30 +370,37 @@ def get_columns(filters): "label": _("Gross Purchase Amount"), "fieldname": "gross_purchase_amount", "fieldtype": "Currency", - "options": "company:currency", + "options": "Company:company:default_currency", "width": 250, }, { "label": _("Opening Accumulated Depreciation"), "fieldname": "opening_accumulated_depreciation", "fieldtype": "Currency", - "options": "company:currency", + "options": "Company:company:default_currency", "width": 250, }, { "label": _("Depreciated Amount"), "fieldname": "depreciated_amount", "fieldtype": "Currency", - "options": "company:currency", + "options": "Company:company:default_currency", "width": 250, }, { "label": _("Asset Value"), "fieldname": "asset_value", "fieldtype": "Currency", - "options": "company:currency", + "options": "Company:company:default_currency", "width": 250, }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120, + }, ] return [ @@ -433,28 +431,28 @@ def get_columns(filters): "label": _("Gross Purchase Amount"), "fieldname": "gross_purchase_amount", "fieldtype": "Currency", - "options": "company:currency", + "options": "Company:company:default_currency", "width": 100, }, { "label": _("Asset Value"), "fieldname": "asset_value", "fieldtype": "Currency", - "options": "company:currency", + "options": "Company:company:default_currency", "width": 100, }, { "label": _("Opening Accumulated Depreciation"), "fieldname": "opening_accumulated_depreciation", "fieldtype": "Currency", - "options": "company:currency", + "options": "Company:company:default_currency", "width": 90, }, { "label": _("Depreciated Amount"), "fieldname": "depreciated_amount", "fieldtype": "Currency", - "options": "company:currency", + "options": "Company:company:default_currency", "width": 100, }, { @@ -479,4 +477,11 @@ def get_columns(filters): "options": "Location", "width": 100, }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120, + }, ] diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js index dc54d606e7a..9ee0c499d98 100644 --- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js @@ -3,17 +3,31 @@ frappe.ui.form.on("Bulk Transaction Log", { refresh(frm) { - frm.add_custom_button(__('Succeeded Entries'), function() { - frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Success"}); - }, __("View")); - frm.add_custom_button(__('Failed Entries'), function() { - frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Failed"}); - }, __("View")); + frm.add_custom_button( + __("Succeeded Entries"), + function () { + frappe.set_route("List", "Bulk Transaction Log Detail", { + date: frm.doc.date, + transaction_status: "Success", + }); + }, + __("View") + ); + frm.add_custom_button( + __("Failed Entries"), + function () { + frappe.set_route("List", "Bulk Transaction Log Detail", { + date: frm.doc.date, + transaction_status: "Failed", + }); + }, + __("View") + ); if (frm.doc.failed) { - frm.add_custom_button(__('Retry Failed Transactions'), function() { + frm.add_custom_button(__("Retry Failed Transactions"), function () { frappe.call({ method: "erpnext.utilities.bulk_transaction.retry", - args: {date: frm.doc.date} + args: { date: frm.doc.date }, }); }); } diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.js b/erpnext/buying/doctype/buying_settings/buying_settings.js index 32431fc3910..11b920aea71 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.js +++ b/erpnext/buying/doctype/buying_settings/buying_settings.js @@ -1,31 +1,38 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Buying Settings', { +frappe.ui.form.on("Buying Settings", { // refresh: function(frm) { - // } }); -frappe.tour['Buying Settings'] = [ +frappe.tour["Buying Settings"] = [ { fieldname: "supp_master_name", title: "Supplier Naming By", - description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a Naming Series choose the 'Naming Series' option."), + description: __( + "By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a Naming Series choose the 'Naming Series' option." + ), }, { fieldname: "buying_price_list", title: "Default Buying Price List", - description: __("Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.") + description: __( + "Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List." + ), }, { fieldname: "po_required", title: "Purchase Order Required for Purchase Invoice & Receipt Creation", - description: __("If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.") + description: __( + "If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master." + ), }, { fieldname: "pr_required", title: "Purchase Receipt Required for Purchase Invoice Creation", - description: __("If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.") - } + description: __( + "If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master." + ), + }, ]; diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 0af93bfc902..f070c40a589 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -188,7 +188,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-11-24 10:55:51.287327", + "modified": "2024-01-12 16:42:01.894346", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -212,10 +212,30 @@ "role": "Purchase Manager", "share": 1, "write": 1 + }, + { + "read": 1, + "role": "Accounts User" + }, + { + "read": 1, + "role": "Accounts Manager" + }, + { + "read": 1, + "role": "Stock Manager" + }, + { + "read": 1, + "role": "Stock User" + }, + { + "read": 1, + "role": "Purchase User" } ], "sort_field": "modified", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 01ce8c33ff9..0230e499f4f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -639,7 +639,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1273,7 +1273,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-10-01 20:58:07.851037", + "modified": "2024-03-20 16:03:31.611808", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 8131104b825..58d7440211c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -34,7 +34,7 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class PurchaseOrder(BuyingController): def __init__(self, *args, **kwargs): - super(PurchaseOrder, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.status_updater = [ { "source_dt": "Purchase Order Item", @@ -54,7 +54,7 @@ class PurchaseOrder(BuyingController): self.set_onload("supplier_tds", supplier_tds) def validate(self): - super(PurchaseOrder, self).validate() + super().validate() self.set_status() @@ -90,7 +90,7 @@ class PurchaseOrder(BuyingController): if self.is_subcontracted: mri_compare_fields = [["project", "="]] - super(PurchaseOrder, self).validate_with_previous_doc( + super().validate_with_previous_doc( { "Supplier Quotation": { "ref_dn_field": "supplier_quotation", @@ -185,9 +185,7 @@ class PurchaseOrder(BuyingController): itemwise_min_order_qty = frappe._dict( frappe.db.sql( """select name, min_order_qty - from tabItem where name in ({0})""".format( - ", ".join(["%s"] * len(items)) - ), + from tabItem where name in ({})""".format(", ".join(["%s"] * len(items))), items, ) ) @@ -233,7 +231,9 @@ class PurchaseOrder(BuyingController): ) elif not frappe.get_value("Item", item.fg_item, "default_bom"): frappe.throw( - _("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item) + _("Row #{0}: Default BOM not found for FG Item {1}").format( + item.idx, item.fg_item + ) ) if not item.fg_item_qty: frappe.throw(_("Row #{0}: Finished Good Item Qty can not be zero").format(item.idx)) @@ -267,8 +267,9 @@ class PurchaseOrder(BuyingController): d.rate = d.base_rate / conversion_rate d.last_purchase_rate = d.rate else: - - item_last_purchase_rate = frappe.get_cached_value("Item", d.item_code, "last_purchase_rate") + item_last_purchase_rate = frappe.get_cached_value( + "Item", d.item_code, "last_purchase_rate" + ) if item_last_purchase_rate: d.base_price_list_rate = ( d.base_rate @@ -303,7 +304,7 @@ class PurchaseOrder(BuyingController): def check_modified_date(self): mod_db = frappe.db.sql("select modified from `tabPurchase Order` where name = %s", self.name) - date_diff = frappe.db.sql("select '%s' - '%s' " % (mod_db[0][0], cstr(self.modified))) + date_diff = frappe.db.sql(f"select '{mod_db[0][0]}' - '{cstr(self.modified)}' ") if date_diff and date_diff[0][0]: msgprint( @@ -317,11 +318,12 @@ class PurchaseOrder(BuyingController): self.update_requested_qty() self.update_ordered_qty() self.update_reserved_qty_for_subcontract() + self.update_blanket_order() self.notify_update() clear_doctype_notifications(self) def on_submit(self): - super(PurchaseOrder, self).on_submit() + super().on_submit() if self.is_against_so(): self.update_status_updater() @@ -344,7 +346,7 @@ class PurchaseOrder(BuyingController): def on_cancel(self): self.ignore_linked_doctypes = ("GL Entry", "Payment Ledger Entry") - super(PurchaseOrder, self).on_cancel() + super().on_cancel() if self.is_against_so(): self.update_status_updater() @@ -456,6 +458,7 @@ class PurchaseOrder(BuyingController): ) +@frappe.request_cache def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): """get last purchase rate for an item""" @@ -483,7 +486,9 @@ def close_or_unclose_purchase_orders(names, status): po = frappe.get_doc("Purchase Order", name) if po.docstatus == 1: if status == "Closed": - if po.status not in ("Cancelled", "Closed") and (po.per_received < 100 or po.per_billed < 100): + if po.status not in ("Cancelled", "Closed") and ( + po.per_received < 100 or po.per_billed < 100 + ): po.update_status(status) else: if po.status == "Closed": @@ -659,7 +664,6 @@ def make_subcontracting_order(source_name, target_doc=None): def get_mapped_subcontracting_order(source_name, target_doc=None): - if target_doc and isinstance(target_doc, str): target_doc = json.loads(target_doc) for key in ["service_items", "items", "supplied_items"]: diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_list.js b/erpnext/buying/doctype/purchase_order/purchase_order_list.js index 6594746cfc5..c1bf1f3b8d9 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_list.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order_list.js @@ -1,6 +1,14 @@ -frappe.listview_settings['Purchase Order'] = { - add_fields: ["base_grand_total", "company", "currency", "supplier", - "supplier_name", "per_received", "per_billed", "status"], +frappe.listview_settings["Purchase Order"] = { + add_fields: [ + "base_grand_total", + "company", + "currency", + "supplier", + "supplier_name", + "per_received", + "per_billed", + "status", + ], get_indicator: function (doc) { if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; @@ -10,15 +18,25 @@ frappe.listview_settings['Purchase Order'] = { return [__("Delivered"), "green", "status,=,Closed"]; } else if (flt(doc.per_received, 2) < 100 && doc.status !== "Closed") { if (flt(doc.per_billed, 2) < 100) { - return [__("To Receive and Bill"), "orange", - "per_received,<,100|per_billed,<,100|status,!=,Closed"]; + return [ + __("To Receive and Bill"), + "orange", + "per_received,<,100|per_billed,<,100|status,!=,Closed", + ]; } else { - return [__("To Receive"), "orange", - "per_received,<,100|per_billed,=,100|status,!=,Closed"]; + return [__("To Receive"), "orange", "per_received,<,100|per_billed,=,100|status,!=,Closed"]; } - } else if (flt(doc.per_received, 2) >= 100 && flt(doc.per_billed, 2) < 100 && doc.status !== "Closed") { + } else if ( + flt(doc.per_received, 2) >= 100 && + flt(doc.per_billed, 2) < 100 && + doc.status !== "Closed" + ) { return [__("To Bill"), "orange", "per_received,=,100|per_billed,<,100|status,!=,Closed"]; - } else if (flt(doc.per_received, 2) >= 100 && flt(doc.per_billed, 2) == 100 && doc.status !== "Closed") { + } else if ( + flt(doc.per_received, 2) >= 100 && + flt(doc.per_billed, 2) == 100 && + doc.status !== "Closed" + ) { return [__("Completed"), "green", "per_received,=,100|per_billed,=,100|status,!=,Closed"]; } }, @@ -26,25 +44,23 @@ frappe.listview_settings['Purchase Order'] = { var method = "erpnext.buying.doctype.purchase_order.purchase_order.close_or_unclose_purchase_orders"; listview.page.add_menu_item(__("Close"), function () { - listview.call_for_selected_items(method, { "status": "Closed" }); + listview.call_for_selected_items(method, { status: "Closed" }); }); listview.page.add_menu_item(__("Reopen"), function () { - listview.call_for_selected_items(method, { "status": "Submitted" }); + listview.call_for_selected_items(method, { status: "Submitted" }); }); - - listview.page.add_action_item(__("Purchase Invoice"), ()=>{ + listview.page.add_action_item(__("Purchase Invoice"), () => { erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice"); }); - listview.page.add_action_item(__("Purchase Receipt"), ()=>{ + listview.page.add_action_item(__("Purchase Receipt"), () => { erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt"); }); - listview.page.add_action_item(__("Advance Payment"), ()=>{ + listview.page.add_action_item(__("Advance Payment"), () => { erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry"); }); - - } + }, }; diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index b0bbc5d0c71..38cb0fbf7ab 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -11,11 +11,13 @@ from frappe.utils.data import today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.party import get_due_date_from_template -from erpnext.buying.doctype.purchase_order.purchase_order import make_inter_company_sales_order +from erpnext.buying.doctype.purchase_order.purchase_order import ( + make_inter_company_sales_order, + make_purchase_receipt, +) from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_pi_from_po, ) -from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.stock.doctype.item.test_item import make_item @@ -190,7 +192,7 @@ class TestPurchaseOrder(FrappeTestCase): po.items[0].qty = 4 po.save() po.submit() - pr = make_pr_against_po(po.name, 2) + make_pr_against_po(po.name, 2) po.load_from_db() existing_ordered_qty = get_ordered_qty() @@ -220,7 +222,7 @@ class TestPurchaseOrder(FrappeTestCase): po.items[0].qty = 4 po.save() po.submit() - pr = make_pr_against_po(po.name, 2) + make_pr_against_po(po.name, 2) po.reload() first_item_of_po = po.get("items")[0] @@ -461,9 +463,7 @@ class TestPurchaseOrder(FrappeTestCase): make_purchase_receipt as make_purchase_receipt_return, ) - pr1 = make_purchase_receipt_return( - is_return=1, return_against=pr.name, qty=-3, do_not_submit=True - ) + pr1 = make_purchase_receipt_return(is_return=1, return_against=pr.name, qty=-3, do_not_submit=True) pr1.items[0].purchase_order = po.name pr1.items[0].purchase_order_item = po.items[0].name pr1.submit() @@ -544,9 +544,7 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(po.payment_schedule[0].payment_amount, 2500.0) self.assertEqual(getdate(po.payment_schedule[0].due_date), getdate(po.transaction_date)) self.assertEqual(po.payment_schedule[1].payment_amount, 2500.0) - self.assertEqual( - getdate(po.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30) - ) + self.assertEqual(getdate(po.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30)) pi = make_pi_from_po(po.name) pi.save() @@ -556,9 +554,7 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(pi.payment_schedule[0].payment_amount, 2500.0) self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date)) self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0) - self.assertEqual( - getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30) - ) + self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30)) automatically_fetch_payment_terms(enable=0) def test_warehouse_company_validation(self): @@ -696,15 +692,15 @@ class TestPurchaseOrder(FrappeTestCase): supplier.on_hold = 0 supplier.save() - except: + except Exception: pass else: raise Exception def test_default_payment_terms(self): - due_date = get_due_date_from_template( - "_Test Payment Term Template 1", "2023-02-03", None - ).strftime("%Y-%m-%d") + due_date = get_due_date_from_template("_Test Payment Term Template 1", "2023-02-03", None).strftime( + "%Y-%m-%d" + ) self.assertEqual(due_date, "2023-03-31") def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self): @@ -802,7 +798,7 @@ class TestPurchaseOrder(FrappeTestCase): Second Purchase Order should not add on to Blanket Orders Ordered Quantity. """ - bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10) + make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10) po = create_purchase_order(item_code="_Test Item", qty=5, against_blanket_order=1) po_doc = frappe.get_doc("Purchase Order", po.get("name")) @@ -814,6 +810,30 @@ class TestPurchaseOrder(FrappeTestCase): # To test if the PO does NOT have a Blanket Order self.assertEqual(po_doc.items[0].blanket_order, None) + def test_blanket_order_on_po_close_and_open(self): + # Step - 1: Create Blanket Order + bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10) + + # Step - 2: Create Purchase Order + po = create_purchase_order( + item_code="_Test Item", qty=5, against_blanket_order=1, against_blanket=bo.name + ) + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 5) + + # Step - 3: Close Purchase Order + po.update_status("Closed") + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 0) + + # Step - 4: Re-Open Purchase Order + po.update_status("Re-open") + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 5) + def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, @@ -916,6 +936,38 @@ class TestPurchaseOrder(FrappeTestCase): self.assertRaises(frappe.ValidationError, po.save) + def test_po_billed_amount_against_return_entry(self): + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_debit_note + + # Create a Purchase Order and Fully Bill it + po = create_purchase_order() + pi = make_pi_from_po(po.name) + pi.insert() + pi.submit() + + # Debit Note - 50% Qty & enable updating PO billed amount + pi_return = make_debit_note(pi.name) + pi_return.items[0].qty = -5 + pi_return.update_billed_amount_in_purchase_order = 1 + pi_return.submit() + + # Check if the billed amount reduced + po.reload() + self.assertEqual(po.per_billed, 50) + + pi_return.reload() + pi_return.cancel() + + # Debit Note - 50% Qty & disable updating PO billed amount + pi_return = make_debit_note(pi.name) + pi_return.items[0].qty = -5 + pi_return.update_billed_amount_in_purchase_order = 0 + pi_return.submit() + + # Check if the billed amount stayed the same + po.reload() + self.assertEqual(po.per_billed, 100) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier @@ -1016,6 +1068,7 @@ def create_purchase_order(**args): "schedule_date": add_days(nowdate(), 1), "include_exploded_items": args.get("include_exploded_items", 1), "against_blanket_order": args.against_blanket_order, + "against_blanket": args.against_blanket, "material_request": args.material_request, "material_request_item": args.material_request_item, }, @@ -1044,15 +1097,11 @@ def create_pr_against_po(po, received_qty=4): def get_ordered_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): - return flt( - frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "ordered_qty") - ) + return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "ordered_qty")) def get_requested_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): - return flt( - frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty") - ) + return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty")) test_dependencies = ["BOM", "Item Price"] diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 45790b80fcb..feb1a9b8828 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -123,8 +123,7 @@ "oldfieldname": "item_code", "oldfieldtype": "Link", "options": "Item", - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "fieldname": "supplier_part_no", @@ -547,7 +546,6 @@ "fieldname": "blanket_order", "fieldtype": "Link", "label": "Blanket Order", - "no_copy": 1, "options": "Blanket Order" }, { @@ -555,7 +553,6 @@ "fieldname": "blanket_order_rate", "fieldtype": "Currency", "label": "Blanket Order Rate", - "no_copy": 1, "print_hide": 1, "read_only": 1 }, @@ -919,7 +916,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-24 19:07:34.921094", + "modified": "2024-02-05 11:23:24.859435", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 8226aa32c0e..73882d39cec 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -3,7 +3,6 @@ import json -from typing import Optional import frappe from frappe import _ @@ -27,7 +26,7 @@ class RequestforQuotation(BuyingController): self.validate_duplicate_supplier() self.validate_supplier_list() validate_for_items(self) - super(RequestforQuotation, self).set_qty_as_per_stock_uom() + super().set_qty_as_per_stock_uom() self.update_email_id() if self.docstatus < 1: @@ -253,7 +252,7 @@ class RequestforQuotation(BuyingController): def update_rfq_supplier_status(self, sup_name=None): for supplier in self.suppliers: - if sup_name == None or supplier.supplier == sup_name: + if sup_name is None or supplier.supplier == sup_name: quote_status = _("Received") for item in self.items: sqi_count = frappe.db.sql( @@ -284,9 +283,7 @@ def send_supplier_emails(rfq_name): def check_portal_enabled(reference_doctype): - if not frappe.db.get_value( - "Portal Menu Item", {"reference_doctype": reference_doctype}, "enabled" - ): + if not frappe.db.get_value("Portal Menu Item", {"reference_doctype": reference_doctype}, "enabled"): frappe.throw( _( "The Access to Request for Quotation From Portal is Disabled. To Allow Access, Enable it in Portal Settings." @@ -415,9 +412,9 @@ def create_rfq_items(sq_doc, supplier, data): def get_pdf( name: str, supplier: str, - print_format: Optional[str] = None, - language: Optional[str] = None, - letterhead: Optional[str] = None, + print_format: str | None = None, + language: str | None = None, + letterhead: str | None = None, ): doc = frappe.get_doc("Request for Quotation", name) if supplier: @@ -492,9 +489,7 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc=No @frappe.whitelist() def get_supplier_tag(): filters = {"document_type": "Supplier"} - tags = list( - set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag) - ) + tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag)) return tags @@ -507,7 +502,7 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt conditions += "and rfq.name like '%%" + txt + "%%' " if filters.get("transaction_date"): - conditions += "and rfq.transaction_date = '{0}'".format(filters.get("transaction_date")) + conditions += "and rfq.transaction_date = '{}'".format(filters.get("transaction_date")) rfq_data = frappe.db.sql( f""" diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 42fa1d923e1..da2a9d8a391 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -64,9 +64,7 @@ class TestRequestforQuotation(FrappeTestCase): rfq = make_request_for_quotation(supplier_data=supplier_wt_appos) - sq = make_supplier_quotation_from_rfq( - rfq.name, for_supplier=supplier_wt_appos[0].get("supplier") - ) + sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier_wt_appos[0].get("supplier")) sq.submit() frappe.form_dict.name = rfq.name @@ -97,9 +95,7 @@ class TestRequestforQuotation(FrappeTestCase): row = item.append("uoms", {"uom": "Kg", "conversion_factor": 2}) row.db_update() - rfq = make_request_for_quotation( - item_code="_Test Multi UOM RFQ Item", uom="Kg", conversion_factor=2 - ) + rfq = make_request_for_quotation(item_code="_Test Multi UOM RFQ Item", uom="Kg", conversion_factor=2) rfq.get("items")[0].rate = 100 rfq.supplier = rfq.suppliers[0].supplier diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index e0e33b6848b..23f25b28876 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -3,49 +3,49 @@ frappe.ui.form.on("Supplier", { setup: function (frm) { - frm.set_query('default_price_list', { 'buying': 1 }); + frm.set_query("default_price_list", { buying: 1 }); if (frm.doc.__islocal == 1) { frm.set_value("represents_company", ""); } - frm.set_query('account', 'accounts', function (doc, cdt, cdn) { + frm.set_query("account", "accounts", function (doc, cdt, cdn) { var d = locals[cdt][cdn]; return { filters: { - 'account_type': 'Payable', - 'company': d.company, - "is_group": 0 - } - } + account_type: "Payable", + company: d.company, + is_group: 0, + }, + }; }); - frm.set_query("default_bank_account", function() { + frm.set_query("default_bank_account", function () { return { filters: { - "is_company_account":1 - } - } - }); - - frm.set_query("supplier_primary_contact", function(doc) { - return { - query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact", - filters: { - "supplier": doc.name - } + is_company_account: 1, + }, }; }); - frm.set_query("supplier_primary_address", function(doc) { + frm.set_query("supplier_primary_contact", function (doc) { + return { + query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact", + filters: { + supplier: doc.name, + }, + }; + }); + + frm.set_query("supplier_primary_address", function (doc) { return { filters: { - "link_doctype": "Supplier", - "link_name": doc.name - } + link_doctype: "Supplier", + link_name: doc.name, + }, }; }); }, refresh: function (frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Supplier' } + 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); @@ -54,65 +54,94 @@ frappe.ui.form.on("Supplier", { } if (frm.doc.__islocal) { - hide_field(['address_html','contact_html']); + hide_field(["address_html", "contact_html"]); frappe.contacts.clear_address_and_contact(frm); - } - else { - unhide_field(['address_html','contact_html']); + } else { + unhide_field(["address_html", "contact_html"]); frappe.contacts.render_address_and_contact(frm); // custom buttons - frm.add_custom_button(__('Accounting Ledger'), function () { - frappe.set_route('query-report', 'General Ledger', - { party_type: 'Supplier', party: frm.doc.name, party_name: frm.doc.supplier_name }); - }, __("View")); + frm.add_custom_button( + __("Accounting Ledger"), + function () { + frappe.set_route("query-report", "General Ledger", { + party_type: "Supplier", + party: frm.doc.name, + party_name: frm.doc.supplier_name, + }); + }, + __("View") + ); - frm.add_custom_button(__('Accounts Payable'), function () { - frappe.set_route('query-report', 'Accounts Payable', { party_type: "Supplier", party: frm.doc.name }); - }, __("View")); + frm.add_custom_button( + __("Accounts Payable"), + function () { + frappe.set_route("query-report", "Accounts Payable", { + party_type: "Supplier", + party: frm.doc.name, + }); + }, + __("View") + ); - frm.add_custom_button(__('Bank Account'), function () { - erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name); - }, __('Create')); + frm.add_custom_button( + __("Bank Account"), + function () { + erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name); + }, + __("Create") + ); - frm.add_custom_button(__('Pricing Rule'), function () { - erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); - }, __('Create')); + frm.add_custom_button( + __("Pricing Rule"), + function () { + erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); + }, + __("Create") + ); - frm.add_custom_button(__('Get Supplier Group Details'), function () { - frm.trigger("get_supplier_group_details"); - }, __('Actions')); + frm.add_custom_button( + __("Get Supplier Group Details"), + function () { + frm.trigger("get_supplier_group_details"); + }, + __("Actions") + ); if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) { - frm.add_custom_button(__('Link with Customer'), function () { - frm.trigger('show_party_link_dialog'); - }, __('Actions')); + frm.add_custom_button( + __("Link with Customer"), + function () { + frm.trigger("show_party_link_dialog"); + }, + __("Actions") + ); } // indicators erpnext.utils.set_party_dashboard_indicators(frm); } }, - get_supplier_group_details: function(frm) { + get_supplier_group_details: function (frm) { frappe.call({ method: "get_supplier_group_details", doc: frm.doc, - callback: function() { + callback: function () { frm.refresh(); - } + }, }); }, - supplier_primary_address: function(frm) { + supplier_primary_address: function (frm) { if (frm.doc.supplier_primary_address) { frappe.call({ - method: 'frappe.contacts.doctype.address.address.get_address_display', + method: "frappe.contacts.doctype.address.address.get_address_display", args: { - "address_dict": frm.doc.supplier_primary_address + address_dict: frm.doc.supplier_primary_address, }, - callback: function(r) { + callback: function (r) { frm.set_value("primary_address", r.message); - } + }, }); } if (!frm.doc.supplier_primary_address) { @@ -120,56 +149,60 @@ frappe.ui.form.on("Supplier", { } }, - supplier_primary_contact: function(frm) { + supplier_primary_contact: function (frm) { if (!frm.doc.supplier_primary_contact) { frm.set_value("mobile_no", ""); frm.set_value("email_id", ""); } }, - is_internal_supplier: function(frm) { + is_internal_supplier: function (frm) { if (frm.doc.is_internal_supplier == 1) { frm.toggle_reqd("represents_company", true); - } - else { + } else { frm.toggle_reqd("represents_company", false); } }, - show_party_link_dialog: function(frm) { + show_party_link_dialog: function (frm) { const dialog = new frappe.ui.Dialog({ - title: __('Select a Customer'), - fields: [{ - fieldtype: 'Link', label: __('Customer'), - options: 'Customer', fieldname: 'customer', reqd: 1 - }], - primary_action: function({ customer }) { + title: __("Select a Customer"), + fields: [ + { + fieldtype: "Link", + label: __("Customer"), + options: "Customer", + fieldname: "customer", + reqd: 1, + }, + ], + primary_action: function ({ customer }) { frappe.call({ - method: 'erpnext.accounts.doctype.party_link.party_link.create_party_link', + method: "erpnext.accounts.doctype.party_link.party_link.create_party_link", args: { - primary_role: 'Supplier', + primary_role: "Supplier", primary_party: frm.doc.name, - secondary_party: customer + secondary_party: customer, }, freeze: true, - callback: function() { + callback: function () { dialog.hide(); frappe.msgprint({ - message: __('Successfully linked to Customer'), - alert: true + message: __("Successfully linked to Customer"), + alert: true, }); }, - error: function() { + error: function () { dialog.hide(); frappe.msgprint({ - message: __('Linking to Customer Failed. Please try again.'), - title: __('Linking Failed'), - indicator: 'red' + message: __("Linking to Customer Failed. Please try again."), + title: __("Linking Failed"), + indicator: "red", }); - } + }, }); }, - primary_action_label: __('Create Link') + primary_action_label: __("Create Link"), }); dialog.show(); - } + }, }); diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 91061c8fe8b..2774f1aeffb 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -11,9 +11,8 @@ from frappe.contacts.address_and_contact import ( ) from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options -from erpnext.accounts.party import ( # noqa +from erpnext.accounts.party import ( get_dashboard_info, - get_timeline_data, validate_party_accounts, ) from erpnext.utilities.transaction_base import TransactionBase @@ -154,6 +153,6 @@ def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, fil .where( (dynamic_link.link_name == supplier) & (dynamic_link.link_doctype == "Supplier") - & (contact.name.like("%{0}%".format(txt))) + & (contact.name.like(f"%{txt}%")) ) ).run(as_dict=False) diff --git a/erpnext/buying/doctype/supplier/supplier_list.js b/erpnext/buying/doctype/supplier/supplier_list.js index c776b001a5a..987c1e02038 100644 --- a/erpnext/buying/doctype/supplier/supplier_list.js +++ b/erpnext/buying/doctype/supplier/supplier_list.js @@ -1,8 +1,8 @@ -frappe.listview_settings['Supplier'] = { +frappe.listview_settings["Supplier"] = { add_fields: ["supplier_name", "supplier_group", "image", "on_hold"], - get_indicator: function(doc) { - if(cint(doc.on_hold)) { + get_indicator: function (doc) { + if (cint(doc.on_hold)) { return [__("On Hold"), "red"]; } - } + }, }; diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index aa89b81b5ed..d87db06a4a9 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -461,7 +461,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -927,7 +927,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-20 11:15:30.083077", + "modified": "2024-03-28 10:20:30.231915", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", @@ -995,4 +995,4 @@ "states": [], "timeline_field": "supplier", "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index e27fbe8aaa2..492a73c3a85 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -15,7 +15,7 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class SupplierQuotation(BuyingController): def validate(self): - super(SupplierQuotation, self).validate() + super().validate() if not self.status: self.status = "Draft" @@ -41,7 +41,7 @@ class SupplierQuotation(BuyingController): pass def validate_with_previous_doc(self): - super(SupplierQuotation, self).validate_with_previous_doc( + super().validate_with_previous_doc( { "Material Request": { "ref_dn_field": "prevdoc_docname", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js index 73685caa0b4..99fe24d8770 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js @@ -1,22 +1,22 @@ -frappe.listview_settings['Supplier Quotation'] = { +frappe.listview_settings["Supplier Quotation"] = { add_fields: ["supplier", "base_grand_total", "status", "company", "currency"], - get_indicator: function(doc) { - if(doc.status==="Ordered") { + get_indicator: function (doc) { + if (doc.status === "Ordered") { return [__("Ordered"), "green", "status,=,Ordered"]; - } else if(doc.status==="Rejected") { + } else if (doc.status === "Rejected") { return [__("Lost"), "gray", "status,=,Lost"]; - } else if(doc.status==="Expired") { + } else if (doc.status === "Expired") { return [__("Expired"), "gray", "status,=,Expired"]; } }, - onload: function(listview) { - listview.page.add_action_item(__("Purchase Order"), ()=>{ + onload: function (listview) { + listview.page.add_action_item(__("Purchase Order"), () => { erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order"); }); - listview.page.add_action_item(__("Purchase Invoice"), ()=>{ + listview.page.add_action_item(__("Purchase Invoice"), () => { erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice"); }); - } + }, }; diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js index b4cd852c32f..0ff0e862259 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js @@ -4,55 +4,51 @@ /* global frappe, refresh_field */ frappe.ui.form.on("Supplier Scorecard", { - setup: function(frm) { - if (frm.doc.indicator_color !== "") { - frm.set_indicator_formatter("status", function(doc) { + setup: function (frm) { + if (frm.doc.indicator_color !== "") { + frm.set_indicator_formatter("status", function (doc) { return doc.indicator_color.toLowerCase(); }); } }, - onload: function(frm) { - if (frm.doc.__unsaved == 1) { + onload: function (frm) { + if (frm.doc.__unsaved == 1) { loadAllStandings(frm); } }, - load_criteria: function(frm) { + load_criteria: function (frm) { frappe.call({ method: "erpnext.buying.doctype.supplier_scorecard_criteria.supplier_scorecard_criteria.get_criteria_list", - callback: function(r) { - frm.set_value('criteria', []); - for (var i = 0; i < r.message.length; i++) - { + callback: function (r) { + frm.set_value("criteria", []); + for (var i = 0; i < r.message.length; i++) { var row = frm.add_child("criteria"); row.criteria_name = r.message[i].name; frm.script_manager.trigger("criteria_name", row.doctype, row.name); } refresh_field("criteria"); - } + }, }); - } - + }, }); frappe.ui.form.on("Supplier Scorecard Scoring Standing", { - - standing_name: function(frm, cdt, cdn) { + standing_name: function (frm, cdt, cdn) { var d = frappe.get_doc(cdt, cdn); if (d.standing_name) { return frm.call({ method: "erpnext.buying.doctype.supplier_scorecard_standing.supplier_scorecard_standing.get_scoring_standing", child: d, args: { - standing_name: d.standing_name - } + standing_name: d.standing_name, + }, }); } - } + }, }); frappe.ui.form.on("Supplier Scorecard Scoring Criteria", { - - criteria_name: function(frm, cdt, cdn) { + criteria_name: function (frm, cdt, cdn) { var d = frappe.get_doc(cdt, cdn); if (d.criteria_name) { return frm.call({ @@ -60,36 +56,34 @@ frappe.ui.form.on("Supplier Scorecard Scoring Criteria", { args: { fieldname: "weight", doctype: "Supplier Scorecard Criteria", - filters: {name: d.criteria_name} + filters: { name: d.criteria_name }, }, - callback: function(r) { - if(r.message){ + callback: function (r) { + if (r.message) { d.weight = r.message.weight; - frm.refresh_field('criteria', 'weight'); + frm.refresh_field("criteria", "weight"); } - } + }, }); } - } + }, }); -var loadAllStandings = function(frm) { +var loadAllStandings = function (frm) { frappe.call({ method: "erpnext.buying.doctype.supplier_scorecard_standing.supplier_scorecard_standing.get_standings_list", - callback: function(r) { - for (var j = 0; j < frm.doc.standings.length; j++) - { - if(!frm.doc.standings[j].hasOwnProperty("standing_name")) { + callback: function (r) { + for (var j = 0; j < frm.doc.standings.length; j++) { + if (!frm.doc.standings[j].hasOwnProperty("standing_name")) { frm.get_field("standings").grid.grid_rows[j].remove(); } } - for (var i = 0; i < r.message.length; i++) - { + for (var i = 0; i < r.message.length; i++) { var new_row = frm.add_child("standings"); new_row.standing_name = r.message[i].name; frm.script_manager.trigger("standing_name", new_row.doctype, new_row.name); } refresh_field("standings"); - } + }, }); }; diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py index 58da8512951..8b2026aec72 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py @@ -34,7 +34,11 @@ class SupplierScorecard(Document): for c2 in self.standings: if c1 != c2: if c1.max_grade > c2.min_grade and c1.min_grade < c2.max_grade: - throw(_("Overlap in scoring between {0} and {1}").format(c1.standing_name, c2.standing_name)) + throw( + _("Overlap in scoring between {0} and {1}").format( + c1.standing_name, c2.standing_name + ) + ) if c2.min_grade == score: score = c2.max_grade if score < 100: @@ -45,7 +49,6 @@ class SupplierScorecard(Document): ) def validate_criteria_weights(self): - weight = 0 for c in self.criteria: weight += c.weight @@ -164,7 +167,6 @@ def refresh_scorecards(): @frappe.whitelist() def make_all_scorecards(docname): - sc = frappe.get_doc("Supplier Scorecard", docname) supplier = frappe.get_doc("Supplier", sc.supplier) diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js index dc5474e3b43..f6af3d456d4 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js @@ -5,13 +5,11 @@ frappe.listview_settings["Supplier Scorecard"] = { add_fields: ["indicator_color", "status"], - get_indicator: function(doc) { - + get_indicator: function (doc) { if (doc.indicator_color) { return [__(doc.status), doc.indicator_color.toLowerCase(), "status,=," + doc.status]; } else { return [__("Unknown"), "gray", "status,=,''"]; } }, - }; diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js index 9f8a2dee81d..4bd05676f8c 100644 --- a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js @@ -4,5 +4,5 @@ /* global frappe */ frappe.ui.form.on("Supplier Scorecard Criteria", { - refresh: function() {} + refresh: function () {}, }); diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py index ab7d4879c43..7433ffff793 100644 --- a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py @@ -29,8 +29,8 @@ class SupplierScorecardCriteria(Document): regex = r"\{(.*?)\}" mylist = re.finditer(regex, test_formula, re.MULTILINE | re.DOTALL) - for dummy1, match in enumerate(mylist): - for dummy2 in range(0, len(match.groups())): + for _dummy1, match in enumerate(mylist): + for _dummy2 in range(0, len(match.groups())): test_formula = test_formula.replace("{" + match.group(1) + "}", "0") try: @@ -64,8 +64,8 @@ def _get_variables(criteria): regex = r"\{(.*?)\}" mylist = re.finditer(regex, criteria.formula, re.MULTILINE | re.DOTALL) - for dummy1, match in enumerate(mylist): - for dummy2 in range(0, len(match.groups())): + for _dummy1, match in enumerate(mylist): + for _dummy2 in range(0, len(match.groups())): try: var = frappe.db.sql( """ @@ -80,6 +80,8 @@ def _get_variables(criteria): )[0] my_variables.append(var) except Exception: - frappe.throw(_("Unable to find variable:") + " " + str(match.group(1)), InvalidFormulaVariable) + frappe.throw( + _("Unable to find variable:") + " " + str(match.group(1)), InvalidFormulaVariable + ) return my_variables diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js index a4cdeb31957..267822f8ff4 100644 --- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js @@ -3,14 +3,13 @@ /* global frappe */ - frappe.ui.form.on("Supplier Scorecard Period", { - onload: function(frm) { + onload: function (frm) { let criteria_grid = frm.get_field("criteria").grid; criteria_grid.toggle_enable("criteria_name", false); criteria_grid.toggle_enable("weight", false); criteria_grid.toggle_display("max_score", true); criteria_grid.toggle_display("formula", true); criteria_grid.toggle_display("score", true); - } + }, }); diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py index a8b76db0931..c54c6aadd6a 100644 --- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py @@ -21,7 +21,6 @@ class SupplierScorecardPeriod(Document): self.calculate_score() def validate_criteria_weights(self): - weight = 0 for c in self.criteria: weight += c.weight @@ -44,14 +43,17 @@ class SupplierScorecardPeriod(Document): crit.score = min( crit.max_score, max( - 0, frappe.safe_eval(self.get_eval_statement(crit.formula), None, {"max": max, "min": min}) + 0, + frappe.safe_eval( + self.get_eval_statement(crit.formula), None, {"max": max, "min": min} + ), ), ) except Exception: frappe.throw( - _("Could not solve criteria score function for {0}. Make sure the formula is valid.").format( - crit.criteria_name - ), + _( + "Could not solve criteria score function for {0}. Make sure the formula is valid." + ).format(crit.criteria_name), frappe.ValidationError, ) crit.score = 0 @@ -82,7 +84,7 @@ class SupplierScorecardPeriod(Document): if var.value: if var.param_name in my_eval_statement: my_eval_statement = my_eval_statement.replace( - "{" + var.param_name + "}", "{:.2f}".format(var.value) + "{" + var.param_name + "}", f"{var.value:.2f}" ) else: if var.param_name in my_eval_statement: diff --git a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js index dccfcc34bb9..025b23a812f 100644 --- a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js +++ b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js @@ -4,7 +4,5 @@ /* global frappe */ frappe.ui.form.on("Supplier Scorecard Standing", { - refresh: function() { - - } + refresh: function () {}, }); diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js index 2d74fdd190a..67af0edd27f 100644 --- a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js +++ b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js @@ -4,7 +4,5 @@ /* global frappe */ frappe.ui.form.on("Supplier Scorecard Variable", { - refresh: function() { - - } + refresh: function () {}, }); diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.js b/erpnext/buying/report/procurement_tracker/procurement_tracker.js index 4d119a5cd75..df8dc9c6902 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.js +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.js @@ -3,7 +3,7 @@ /* eslint-disable */ frappe.query_reports["Procurement Tracker"] = { - "filters": [ + filters: [ { fieldname: "company", label: __("Company"), @@ -30,10 +30,10 @@ frappe.query_reports["Procurement Tracker"] = { default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], }, - ] -} + ], +}; diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py deleted file mode 100644 index 9b53421319d..00000000000 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from datetime import datetime - -import frappe -from frappe.tests.utils import FrappeTestCase - -from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt -from erpnext.buying.report.procurement_tracker.procurement_tracker import execute -from erpnext.stock.doctype.material_request.material_request import make_purchase_order -from erpnext.stock.doctype.material_request.test_material_request import make_material_request -from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - - -class TestProcurementTracker(FrappeTestCase): - pass diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.js b/erpnext/buying/report/purchase_analytics/purchase_analytics.js index fe3bbc9bd89..7de965a4586 100644 --- a/erpnext/buying/report/purchase_analytics/purchase_analytics.js +++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.js @@ -3,47 +3,47 @@ /* eslint-disable */ frappe.query_reports["Purchase Analytics"] = { - "filters": [ + filters: [ { fieldname: "tree_type", label: __("Tree Type"), fieldtype: "Select", - options: ["Supplier Group","Supplier","Item Group","Item"], + options: ["Supplier Group", "Supplier", "Item Group", "Item"], default: "Supplier", - reqd: 1 + reqd: 1, }, { fieldname: "doc_type", label: __("based_on"), fieldtype: "Select", - options: ["Purchase Order","Purchase Receipt","Purchase Invoice"], + options: ["Purchase Order", "Purchase Receipt", "Purchase Invoice"], default: "Purchase Invoice", - reqd: 1 + reqd: 1, }, { fieldname: "value_quantity", label: __("Value Or Qty"), fieldtype: "Select", options: [ - { "value": "Value", "label": __("Value") }, - { "value": "Quantity", "label": __("Quantity") }, + { value: "Value", label: __("Value") }, + { value: "Quantity", label: __("Quantity") }, ], default: "Value", - reqd: 1 + reqd: 1, }, { fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], - reqd: 1 + reqd: 1, }, { fieldname: "company", @@ -51,22 +51,21 @@ frappe.query_reports["Purchase Analytics"] = { fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { fieldname: "range", label: __("Range"), fieldtype: "Select", options: [ - { "value": "Weekly", "label": __("Weekly") }, - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Yearly", "label": __("Yearly") } + { value: "Weekly", label: __("Weekly") }, + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Yearly", label: __("Yearly") }, ], default: "Monthly", - reqd: 1 - } - + reqd: 1, + }, ], get_datatable_options(options) { return Object.assign(options, { @@ -75,9 +74,7 @@ frappe.query_reports["Purchase Analytics"] = { onCheckRow: function (data) { if (!data) return; - const data_doctype = $( - data[2].html - )[0].attributes.getNamedItem("data-doctype").value; + const data_doctype = $(data[2].html)[0].attributes.getNamedItem("data-doctype").value; const tree_type = frappe.query_report.filters[0].value; if (data_doctype != tree_type) return; @@ -85,23 +82,17 @@ frappe.query_reports["Purchase Analytics"] = { length = data.length; if (tree_type == "Supplier") { - row_values = data - .slice(4, length - 1) - .map(function (column) { - return column.content; - }); + row_values = data.slice(4, length - 1).map(function (column) { + return column.content; + }); } else if (tree_type == "Item") { - row_values = data - .slice(5, length - 1) - .map(function (column) { - return column.content; - }); + row_values = data.slice(5, length - 1).map(function (column) { + return column.content; + }); } else { - row_values = data - .slice(3, length - 1) - .map(function (column) { - return column.content; - }); + row_values = data.slice(3, length - 1).map(function (column) { + return column.content; + }); } entry = { @@ -112,13 +103,13 @@ frappe.query_reports["Purchase Analytics"] = { let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; - let element_found = new_datasets.some((element, index, array)=>{ - if(element.name == row_name){ - array.splice(index, 1) - return true + let element_found = new_datasets.some((element, index, array) => { + if (element.name == row_name) { + array.splice(index, 1); + return true; } - return false - }) + return false; + }); if (!element_found) { new_datasets.push(entry); @@ -127,12 +118,14 @@ frappe.query_reports["Purchase Analytics"] = { labels: raw_data.labels, datasets: new_datasets, }; - const new_options = Object.assign({}, frappe.query_report.chart_options, {data: new_data}); + const new_options = Object.assign({}, frappe.query_report.chart_options, { + data: new_data, + }); frappe.query_report.render_chart(new_options); frappe.query_report.raw_chart_data = new_data; }, }, }); - } -} + }, +}; diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js index 721e54e46f5..ad2a9e88737 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js @@ -3,78 +3,78 @@ /* eslint-disable */ frappe.query_reports["Purchase Order Analysis"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "width": "80", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_default("company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + width: "80", + options: "Company", + reqd: 1, + default: frappe.defaults.get_default("company"), }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.get_today(), }, { - "fieldname":"project", - "label": __("Project"), - "fieldtype": "Link", - "width": "80", - "options": "Project" + fieldname: "project", + label: __("Project"), + fieldtype: "Link", + width: "80", + options: "Project", }, { - "fieldname": "name", - "label": __("Purchase Order"), - "fieldtype": "Link", - "width": "80", - "options": "Purchase Order", - "get_query": () =>{ + fieldname: "name", + label: __("Purchase Order"), + fieldtype: "Link", + width: "80", + options: "Purchase Order", + get_query: () => { return { - filters: { "docstatus": 1 } - } - } + filters: { docstatus: 1 }, + }; + }, }, { - "fieldname": "status", - "label": __("Status"), - "fieldtype": "MultiSelectList", - "width": "80", - get_data: function(txt) { - let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"] - let options = [] - for (let option of status){ + fieldname: "status", + label: __("Status"), + fieldtype: "MultiSelectList", + width: "80", + get_data: function (txt) { + let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"]; + let options = []; + for (let option of status) { options.push({ - "value": option, - "label": __(option), - "description": "" - }) + value: option, + label: __(option), + description: "", + }); } - return options - } + return options; + }, }, { - "fieldname": "group_by_po", - "label": __("Group by Purchase Order"), - "fieldtype": "Check", - "default": 0 - } + fieldname: "group_by_po", + label: __("Group by Purchase Order"), + fieldtype: "Check", + default: 0, + }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); let format_fields = ["received_qty", "billed_amount"]; @@ -82,5 +82,5 @@ frappe.query_reports["Purchase Order Analysis"] = { value = "" + value + ""; } return value; - } + }, }; diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index b6e46302ffe..b23c3f50b9a 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -68,9 +68,7 @@ def get_data(filters): po.company, po_item.name, ) - .where( - (po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1) - ) + .where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1)) .groupby(po_item.name) .orderby(po.transaction_date) ) @@ -80,9 +78,7 @@ def get_data(filters): query = query.where(po[field] == filters.get(field)) if filters.get("from_date") and filters.get("to_date"): - query = query.where( - po.transaction_date.between(filters.get("from_date"), filters.get("to_date")) - ) + query = query.where(po.transaction_date.between(filters.get("from_date"), filters.get("to_date"))) if filters.get("status"): query = query.where(po.status.isin(filters.get("status"))) @@ -114,7 +110,7 @@ def prepare_data(data, filters): if filters.get("group_by_po"): po_name = row["purchase_order"] - if not po_name in purchase_order_map: + if po_name not in purchase_order_map: # create an entry row_copy = copy.deepcopy(row) purchase_order_map[po_name] = row_copy diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js index 90919dcc6a3..366fff191a0 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js @@ -1,8 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.require("assets/erpnext/js/purchase_trends_filters.js", function() { +frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () { frappe.query_reports["Purchase Order Trends"] = { - filters: erpnext.get_purchase_trends_filters() - } + filters: erpnext.get_purchase_trends_filters(), + }; }); diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js index d727584d0aa..5dd309bc915 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js +++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js @@ -3,74 +3,74 @@ /* eslint-disable */ frappe.query_reports["Requested Items to Order and Receive"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "width": "80", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_default("company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + width: "80", + options: "Company", + reqd: 1, + default: frappe.defaults.get_default("company"), }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.get_today(), }, { - "fieldname": "material_request", - "label": __("Material Request"), - "fieldtype": "Link", - "width": "80", - "options": "Material Request", - "get_query": () => { + fieldname: "material_request", + label: __("Material Request"), + fieldtype: "Link", + width: "80", + options: "Material Request", + get_query: () => { return { filters: { - "docstatus": 1, - "material_request_type": "Purchase", - "per_received": ["<", 100] - } - } - } + docstatus: 1, + material_request_type: "Purchase", + per_received: ["<", 100], + }, + }; + }, }, { - "fieldname": "item_code", - "label": __("Item"), - "fieldtype": "Link", - "width": "80", - "options": "Item", - "get_query": () => { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + width: "80", + options: "Item", + get_query: () => { return { - query: "erpnext.controllers.queries.item_query" - } - } + query: "erpnext.controllers.queries.item_query", + }; + }, }, { - "fieldname": "group_by_mr", - "label": __("Group by Material Request"), - "fieldtype": "Check", - "default": 0 - } + fieldname: "group_by_mr", + label: __("Group by Material Request"), + fieldtype: "Check", + default: 0, + }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (column.fieldname == "ordered_qty" && data && data.ordered_qty > 0) { value = "" + value + ""; } return value; - } + }, }; diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py index 07187352eb7..55189a722a3 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py +++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py @@ -57,9 +57,7 @@ def get_data(filters): "qty_to_receive" ), Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), - (Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))).as_( - "qty_to_order" - ), + (Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))).as_("qty_to_order"), mr_item.item_name, mr_item.description, mr.company, @@ -110,7 +108,7 @@ def prepare_data(data, filters): for row in data: # item wise map for charts - if not row["item_code"] in item_qty_map: + if row["item_code"] not in item_qty_map: item_qty_map[row["item_code"]] = { "qty": flt(row["stock_qty"], precision), "stock_qty": flt(row["stock_qty"], precision), @@ -127,7 +125,7 @@ def prepare_data(data, filters): if filters.get("group_by_mr"): # consolidated material request map for group by filter - if not row["material_request"] in material_request_map: + if row["material_request"] not in material_request_map: # create an entry with mr as key row_copy = copy.deepcopy(row) material_request_map[row["material_request"]] = row_copy diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js index 075671f4ec6..ab413925d5f 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js @@ -3,40 +3,40 @@ /* eslint-disable */ frappe.query_reports["Subcontract Order Summary"] = { - "filters": [ + filters: [ { label: __("Company"), fieldname: "company", fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { label: __("From Date"), fieldname: "from_date", fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), - reqd: 1 + reqd: 1, }, { label: __("To Date"), fieldname: "to_date", fieldtype: "Date", default: frappe.datetime.get_today(), - reqd: 1 + reqd: 1, }, { label: __("Order Type"), fieldname: "order_type", fieldtype: "Select", options: ["Purchase Order", "Subcontracting Order"], - default: "Subcontracting Order" + default: "Subcontracting Order", }, { label: __("Subcontract Order"), fieldname: "name", - fieldtype: "Data" - } - ] -}; \ No newline at end of file + fieldtype: "Data", + }, + ], +}; diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 0213051aeb7..130cadaabc1 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -112,7 +112,7 @@ def prepare_subcontracted_data(orders, supplied_items): def get_subcontracted_data(order_details, data): - for key, details in order_details.items(): + for _key, details in order_details.items(): res = details.order_item for index, row in enumerate(details.supplied_items): if index != 0: diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js index 9db769d59bf..44fbb3408ff 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js +++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js @@ -3,34 +3,34 @@ /* eslint-disable */ frappe.query_reports["Subcontracted Item To Be Received"] = { - "filters": [ + filters: [ { label: __("Order Type"), fieldname: "order_type", fieldtype: "Select", options: ["Purchase Order", "Subcontracting Order"], - default: "Subcontracting Order" + default: "Subcontracting Order", }, { fieldname: "supplier", label: __("Supplier"), fieldtype: "Link", options: "Supplier", - reqd: 1 + reqd: 1, }, { - fieldname:"from_date", + fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: frappe.datetime.get_today(), - reqd: 1 + reqd: 1, }, - ] + ], }; diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py index d13d9701f31..d90be66af94 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -62,7 +62,9 @@ class TestSubcontractedItemToBeReceived(FrappeTestCase): "from_date": frappe.utils.get_datetime( frappe.utils.add_to_date(sco.transaction_date, days=-10) ), - "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)), + "to_date": frappe.utils.get_datetime( + frappe.utils.add_to_date(sco.transaction_date, days=10) + ), } ) ) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js index 7e5338f353b..da19b5bbd36 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js @@ -3,34 +3,34 @@ /* eslint-disable */ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = { - "filters": [ + filters: [ { label: __("Order Type"), fieldname: "order_type", fieldtype: "Select", options: ["Purchase Order", "Subcontracting Order"], - default: "Subcontracting Order" + default: "Subcontracting Order", }, { fieldname: "supplier", label: __("Supplier"), fieldtype: "Link", options: "Supplier", - reqd: 1 + reqd: 1, }, { - fieldname:"from_date", + fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: frappe.datetime.get_today(), - reqd: 1 + reqd: 1, }, - ] -} + ], +}; diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index 160295776b1..7b4ec5c2f39 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -48,7 +48,9 @@ class TestSubcontractedItemToBeTransferred(FrappeTestCase): "from_date": frappe.utils.get_datetime( frappe.utils.add_to_date(sco.transaction_date, days=-10) ), - "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)), + "to_date": frappe.utils.get_datetime( + frappe.utils.add_to_date(sco.transaction_date, days=10) + ), } ) ) diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index 579c0a65ad9..f7d0d947b61 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -9,23 +9,23 @@ frappe.query_reports["Supplier Quotation Comparison"] = { options: "Company", fieldname: "company", default: frappe.defaults.get_user_default("Company"), - "reqd": 1 + reqd: 1, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.get_today(), }, { default: "", @@ -34,34 +34,34 @@ frappe.query_reports["Supplier Quotation Comparison"] = { fieldname: "item_code", fieldtype: "Link", get_query: () => { - let quote = frappe.query_report.get_filter_value('supplier_quotation'); + let quote = frappe.query_report.get_filter_value("supplier_quotation"); if (quote != "") { return { query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query", filters: { - "from": "Supplier Quotation Item", - "parent": quote - } - } + from: "Supplier Quotation Item", + parent: quote, + }, + }; } - } + }, }, { fieldname: "supplier", label: __("Supplier"), fieldtype: "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Supplier', txt); - } + get_data: function (txt) { + return frappe.db.get_link_options("Supplier", txt); + }, }, { fieldtype: "MultiSelectList", label: __("Supplier Quotation"), fieldname: "supplier_quotation", default: "", - get_data: function(txt) { - return frappe.db.get_link_options('Supplier Quotation', txt, {'docstatus': ["<", 2]}); - } + get_data: function (txt) { + return frappe.db.get_link_options("Supplier Quotation", txt, { docstatus: ["<", 2] }); + }, }, { fieldtype: "Link", @@ -70,37 +70,39 @@ frappe.query_reports["Supplier Quotation Comparison"] = { fieldname: "request_for_quotation", default: "", get_query: () => { - return { filters: { "docstatus": ["<", 2] } } - } + return { filters: { docstatus: ["<", 2] } }; + }, }, { - "fieldname":"group_by", - "label": __("Group by"), - "fieldtype": "Select", - "options": [__("Group by Supplier"), __("Group by Item")], - "default": __("Group by Supplier") + fieldname: "group_by", + label: __("Group by"), + fieldtype: "Select", + options: [ + { label: __("Group by Supplier"), value: "Group by Supplier" }, + { label: __("Group by Item"), value: "Group by Item" }, + ], + default: __("Group by Supplier"), }, { fieldtype: "Check", label: __("Include Expired"), fieldname: "include_expired", - default: 0 - } + default: 0, + }, ], formatter: (value, row, column, data, default_formatter) => { value = default_formatter(value, row, column, data); - if(column.fieldname === "valid_till" && data.valid_till){ - if(frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 1){ + if (column.fieldname === "valid_till" && data.valid_till) { + if (frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 1) { value = `
    ${value}
    `; - } - else if (frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 7){ + } else if (frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 7) { value = `
    ${value}
    `; } } - if(column.fieldname === "price_per_unit" && data.price_per_unit && data.min && data.min === 1){ + if (column.fieldname === "price_per_unit" && data.price_per_unit && data.min && data.min === 1) { value = `
    ${value}
    `; } return value; @@ -108,48 +110,53 @@ frappe.query_reports["Supplier Quotation Comparison"] = { onload: (report) => { // Create a button for setting the default supplier - report.page.add_inner_button(__("Select Default Supplier"), () => { - let reporter = frappe.query_reports["Supplier Quotation Comparison"]; - - //Always make a new one so that the latest values get updated - reporter.make_default_supplier_dialog(report); - }, __("Tools")); + report.page.add_inner_button( + __("Select Default Supplier"), + () => { + let reporter = frappe.query_reports["Supplier Quotation Comparison"]; + //Always make a new one so that the latest values get updated + reporter.make_default_supplier_dialog(report); + }, + __("Tools") + ); }, make_default_supplier_dialog: (report) => { // Get the name of the item to change - if(!report.data) return; + if (!report.data) return; let filters = report.get_values(); let item_code = filters.item_code; // Get a list of the suppliers (with a blank as well) for the user to select - let suppliers = $.map(report.data, (row, idx)=>{ return row.supplier_name }) + let suppliers = $.map(report.data, (row, idx) => { + return row.supplier_name; + }); // Create a dialog window for the user to pick their supplier let dialog = new frappe.ui.Dialog({ - title: __('Select Default Supplier'), + title: __("Select Default Supplier"), fields: [ { reqd: 1, - label: 'Supplier', - fieldtype: 'Link', - options: 'Supplier', - fieldname: 'supplier', + label: "Supplier", + fieldtype: "Link", + options: "Supplier", + fieldname: "supplier", get_query: () => { return { filters: { - 'name': ['in', suppliers] - } - } - } - } - ] + name: ["in", suppliers], + }, + }; + }, + }, + ], }); dialog.set_primary_action(__("Set Default Supplier"), () => { let values = dialog.get_values(); - if(values) { + if (values) { // Set the default_supplier field of the appropriate Item to the selected supplier frappe.call({ method: "frappe.client.set_value", @@ -163,10 +170,10 @@ frappe.query_reports["Supplier Quotation Comparison"] = { callback: (r) => { frappe.msgprint(__("Successfully Set Supplier")); dialog.hide(); - } + }, }); } }); dialog.show(); - } -} + }, +}; diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 01ff28d8103..684cd3a0f9e 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -82,18 +82,14 @@ def prepare_data(supplier_quotation_data, filters): group_wise_map = defaultdict(list) supplier_qty_price_map = {} - group_by_field = ( - "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code" - ) + group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code" company_currency = frappe.db.get_default("currency") float_precision = cint(frappe.db.get_default("float_precision")) or 2 for data in supplier_quotation_data: group = data.get(group_by_field) # get item or supplier value for this row - supplier_currency = frappe.db.get_value( - "Supplier", data.get("supplier_name"), "default_currency" - ) + supplier_currency = frappe.db.get_value("Supplier", data.get("supplier_name"), "default_currency") if supplier_currency: exchange_rate = get_exchange_rate(supplier_currency, company_currency) @@ -126,7 +122,7 @@ def prepare_data(supplier_quotation_data, filters): # map for chart preparation of the form {'supplier1': {'qty': 'price'}} supplier = data.get("supplier_name") if filters.get("item_code"): - if not supplier in supplier_qty_price_map: + if supplier not in supplier_qty_price_map: supplier_qty_price_map[supplier] = {} supplier_qty_price_map[supplier][row["qty"]] = row["price"] @@ -169,7 +165,7 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): for supplier in suppliers: entry = supplier_qty_price_map[supplier] for qty in qty_list: - if not qty in data_points_map: + if qty not in data_points_map: data_points_map[qty] = [] if qty in entry: data_points_map[qty].append(entry[qty]) diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index e904af0dce3..1d708b36980 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -3,7 +3,6 @@ import json -from typing import Dict import frappe from frappe import _ @@ -87,7 +86,7 @@ def set_stock_levels(row) -> None: row.set(field, qty_data[field]) -def validate_item_and_get_basic_data(row) -> Dict: +def validate_item_and_get_basic_data(row) -> dict: item = frappe.db.get_values( "Item", filters={"name": row.item_code}, @@ -101,12 +100,7 @@ def validate_item_and_get_basic_data(row) -> Dict: def validate_stock_item_warehouse(row, item) -> None: - if ( - item.is_stock_item == 1 - and row.qty - and not row.warehouse - and not row.get("delivered_by_supplier") - ): + if item.is_stock_item == 1 and row.qty and not row.warehouse and not row.get("delivered_by_supplier"): frappe.throw( _("Row #{1}: Warehouse is mandatory for stock Item {0}").format( frappe.bold(row.item_code), row.idx @@ -118,9 +112,7 @@ def check_on_hold_or_closed_status(doctype, docname) -> None: status = frappe.db.get_value(doctype, docname, "status") if status in ("Closed", "On Hold"): - frappe.throw( - _("{0} {1} status is {2}").format(doctype, docname, status), frappe.InvalidStatusError - ) + frappe.throw(_("{0} {1} status is {2}").format(doctype, docname, status), frappe.InvalidStatusError) @frappe.whitelist() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 47aba077b49..86e4cf684a3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3,10 +3,13 @@ import json +from collections import defaultdict import frappe from frappe import _, bold, qb, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied +from frappe.query_builder import Criterion +from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( add_days, @@ -26,6 +29,7 @@ from frappe.utils import ( import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, + get_dimensions, ) from erpnext.accounts.doctype.pricing_rule.utils import ( apply_pricing_rule_for_free_items, @@ -42,6 +46,7 @@ from erpnext.accounts.party import ( from erpnext.accounts.utils import ( create_gain_loss_journal, get_account_currency, + get_currency_precision, get_fiscal_years, validate_fiscal_year, ) @@ -84,12 +89,13 @@ force_item_fields = ( "weight_per_unit", "weight_uom", "total_weight", + "valuation_rate", ) class AccountsController(TransactionBase): def __init__(self, *args, **kwargs): - super(AccountsController, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def get_print_settings(self): print_setting_fields = [] @@ -152,6 +158,13 @@ class AccountsController(TransactionBase): if not self.get("is_return") and not self.get("is_debit_note"): self.validate_qty_is_not_zero() + if ( + self.doctype in ["Sales Invoice", "Purchase Invoice"] + and self.get("is_return") + and self.get("update_stock") + ): + self.validate_zero_qty_for_return_invoices_with_stock() + if self.get("_action") and self._action != "update_after_submit": self.set_missing_values(for_validate=True) @@ -184,19 +197,33 @@ class AccountsController(TransactionBase): self.validate_party() self.validate_currency() self.validate_party_account_currency() + self.validate_return_against_account() if self.doctype in ["Purchase Invoice", "Sales Invoice"]: - if invalid_advances := [ - x for x in self.advances if not x.reference_type or not x.reference_name - ]: + if invalid_advances := [x for x in self.advances if not x.reference_type or not x.reference_name]: frappe.throw( _( "Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry." ).format( - frappe.bold(comma_and([x.idx for x in invalid_advances])), frappe.bold(_("Advance Payments")) + frappe.bold(comma_and([x.idx for x in invalid_advances])), + frappe.bold(_("Advance Payments")), ) ) + if self.get("is_return") and self.get("return_against") and not self.get("is_pos"): + if self.get("update_outstanding_for_self"): + document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" + frappe.msgprint( + _( + "We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox.

    Or you can use {3} tool to reconcile against {1} later." + ).format( + frappe.bold(document_type), + get_link_to_form(self.doctype, self.get("return_against")), + frappe.bold("Update Outstanding for Self"), + get_link_to_form("Payment Reconciliation", "Payment Reconciliation"), + ) + ) + pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() @@ -308,6 +335,12 @@ class AccountsController(TransactionBase): ple = frappe.qb.DocType("Payment Ledger Entry") frappe.qb.from_(ple).delete().where( (ple.voucher_type == self.doctype) & (ple.voucher_no == self.name) + | ( + (ple.against_voucher_type == self.doctype) + & (ple.against_voucher_no == self.name) + & ple.delinked + == 1 + ) ).run() frappe.db.sql( "delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name) @@ -317,6 +350,18 @@ class AccountsController(TransactionBase): (self.doctype, self.name), ) + def validate_return_against_account(self): + if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against: + cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to" + cr_dr_account_label = "Debit To" if self.doctype == "Sales Invoice" else "Credit To" + cr_dr_account = self.get(cr_dr_account_field) + if frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) != cr_dr_account: + frappe.throw( + _("'{0}' account: '{1}' should match the Return Against Invoice").format( + frappe.bold(cr_dr_account_label), frappe.bold(cr_dr_account) + ) + ) + def validate_deferred_income_expense_account(self): field_map = { "Sales Invoice": "deferred_revenue_account", @@ -339,11 +384,7 @@ class AccountsController(TransactionBase): item.set(field_map.get(self.doctype), default_deferred_account) def validate_auto_repeat_subscription_dates(self): - if ( - self.get("from_date") - and self.get("to_date") - and getdate(self.from_date) > getdate(self.to_date) - ): + if self.get("from_date") and self.get("to_date") and getdate(self.from_date) > getdate(self.to_date): frappe.throw(_("To Date cannot be before From Date"), title=_("Invalid Auto Repeat Date")) def validate_deferred_start_and_end_date(self): @@ -351,11 +392,15 @@ class AccountsController(TransactionBase): if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): if not (d.service_start_date and d.service_end_date): frappe.throw( - _("Row #{0}: Service Start and End Date is required for deferred accounting").format(d.idx) + _("Row #{0}: Service Start and End Date is required for deferred accounting").format( + d.idx + ) ) elif getdate(d.service_start_date) > getdate(d.service_end_date): frappe.throw( - _("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx) + _("Row #{0}: Service Start Date cannot be greater than Service End Date").format( + d.idx + ) ) elif getdate(self.posting_date) > getdate(d.service_end_date): frappe.throw( @@ -414,7 +459,9 @@ class AccountsController(TransactionBase): if not self.cash_bank_account: # show message that the amount is not paid frappe.throw( - _("Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified") + _( + "Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified" + ) ) if cint(self.is_return) and self.grand_total > self.paid_amount: @@ -458,7 +505,11 @@ class AccountsController(TransactionBase): if date_field and self.get(date_field): validate_fiscal_year( - self.get(date_field), self.fiscal_year, self.company, self.meta.get_label(date_field), self + self.get(date_field), + self.fiscal_year, + self.company, + self.meta.get_label(date_field), + self, ) def validate_party_accounts(self): @@ -528,7 +579,9 @@ class AccountsController(TransactionBase): if tax_updated: frappe.msgprint( - _("Disabled tax included prices since this {} is an internal transfer").format(self.doctype), + _("Disabled tax included prices since this {} is an internal transfer").format( + self.doctype + ), alert=1, ) @@ -619,7 +672,7 @@ class AccountsController(TransactionBase): parent_dict[fieldname] = self.get(fieldname) if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: - document_type = "{} Item".format(self.doctype) + document_type = f"{self.doctype} Item" parent_dict.update({"document_type": document_type}) # party_name field used for customer in quotation @@ -653,14 +706,16 @@ class AccountsController(TransactionBase): if self.get("is_subcontracted"): args["is_subcontracted"] = self.is_subcontracted - ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False) + ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False) for fieldname, value in ret.items(): if item.meta.get_field(fieldname) and value is not None: if item.get(fieldname) is None or fieldname in force_item_fields: item.set(fieldname, value) - elif fieldname in ["cost_center", "conversion_factor"] and not item.get(fieldname): + elif fieldname in ["cost_center", "conversion_factor"] and not item.get( + fieldname + ): item.set(fieldname, value) elif fieldname == "serial_no": @@ -696,7 +751,8 @@ class AccountsController(TransactionBase): # Items add via promotional scheme may not have cost center set if hasattr(item, "cost_center") and not item.get("cost_center"): item.set( - "cost_center", self.get("cost_center") or erpnext.get_default_cost_center(self.company) + "cost_center", + self.get("cost_center") or erpnext.get_default_cost_center(self.company), ) if ret.get("pricing_rules"): @@ -823,9 +879,7 @@ class AccountsController(TransactionBase): if self.taxes_and_charges and frappe.get_cached_value( taxes_and_charges_doctype, self.taxes_and_charges, "disabled" ): - frappe.throw( - _("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges) - ) + frappe.throw(_("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges)) def validate_tax_account_company(self): for d in self.get("taxes"): @@ -910,6 +964,18 @@ class AccountsController(TransactionBase): return gl_dict + def validate_zero_qty_for_return_invoices_with_stock(self): + rows = [] + for item in self.items: + if not flt(item.qty): + rows.append(item) + if rows: + frappe.throw( + _( + "For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}" + ).format(frappe.bold(comma_and(["#" + str(x.idx) for x in rows]))) + ) + def validate_qty_is_not_zero(self): if self.doctype == "Purchase Receipt": return @@ -938,9 +1004,8 @@ class AccountsController(TransactionBase): self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]})) frappe.db.sql( - """delete from `tab%s` where parentfield=%s and parent = %s - and allocated_amount = 0""" - % (childtype, "%s", "%s"), + """delete from `tab{}` where parentfield={} and parent = {} + and allocated_amount = 0""".format(childtype, "%s", "%s"), (parentfield, self.name), ) @@ -1028,9 +1093,7 @@ class AccountsController(TransactionBase): return res def is_inclusive_tax(self): - is_inclusive = cint( - frappe.db.get_single_value("Accounts Settings", "show_inclusive_tax_in_print") - ) + is_inclusive = cint(frappe.db.get_single_value("Accounts Settings", "show_inclusive_tax_in_print")) if is_inclusive: is_inclusive = 0 @@ -1073,7 +1136,6 @@ class AccountsController(TransactionBase): for d in self.get("advances"): advance_exchange_rate = d.ref_exchange_rate if d.allocated_amount and self.conversion_rate != advance_exchange_rate: - base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate @@ -1118,7 +1180,9 @@ class AccountsController(TransactionBase): return True return False - def make_exchange_gain_loss_journal(self, args: dict = None) -> None: + def make_exchange_gain_loss_journal( + self, args: dict | None = None, dimensions_dict: dict | None = None + ) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments """ @@ -1130,12 +1194,13 @@ class AccountsController(TransactionBase): # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation. # and below logic is only for such scenarios if args: + precision = get_currency_precision() for arg in args: # Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount` if ( - arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0 + flt(arg.get("difference_amount", 0), precision) != 0 + or flt(arg.get("exchange_gain_loss", 0), precision) != 0 ) and arg.get("difference_account"): - party_account = arg.get("account") gain_loss_account = arg.get("difference_account") difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss") @@ -1173,6 +1238,7 @@ class AccountsController(TransactionBase): self.name, arg.get("referenced_row"), arg.get("cost_center"), + dimensions_dict, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( @@ -1185,8 +1251,8 @@ class AccountsController(TransactionBase): gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] booked = [] if gain_loss_to_book: - vtypes = [x.reference_doctype for x in gain_loss_to_book] - vnames = [x.reference_name for x in gain_loss_to_book] + [x.reference_doctype for x in gain_loss_to_book] + [x.reference_name for x in gain_loss_to_book] je = qb.DocType("Journal Entry") jea = qb.DocType("Journal Entry Account") parents = ( @@ -1227,7 +1293,8 @@ class AccountsController(TransactionBase): dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" - if d.reference_doctype == "Purchase Invoice": + # Inverse debit/credit for payable accounts + if self.is_payable_account(d.reference_doctype, party_account): dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" @@ -1253,6 +1320,7 @@ class AccountsController(TransactionBase): self.name, d.idx, self.cost_center, + dimensions_dict, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( @@ -1260,6 +1328,14 @@ class AccountsController(TransactionBase): ) ) + def is_payable_account(self, reference_doctype, account): + if reference_doctype == "Purchase Invoice" or ( + reference_doctype == "Journal Entry" + and frappe.get_cached_value("Account", account, "account_type") == "Payable" + ): + return True + return False + def make_precision_loss_gl_entry(self, gl_entries): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center @@ -1325,7 +1401,9 @@ class AccountsController(TransactionBase): "allocated_amount": flt(d.allocated_amount), "precision": d.precision("advance_amount"), "exchange_rate": ( - self.conversion_rate if self.party_account_currency != self.company_currency else 1 + self.conversion_rate + if self.party_account_currency != self.company_currency + else 1 ), "grand_total": ( self.base_grand_total @@ -1344,15 +1422,46 @@ class AccountsController(TransactionBase): if lst: from erpnext.accounts.utils import reconcile_against_document - reconcile_against_document(lst) + # pass dimension values to utility method + active_dimensions = get_dimensions()[0] + for x in lst: + for dim in active_dimensions: + if self.get(dim.fieldname): + x.update({dim.fieldname: self.get(dim.fieldname)}) + reconcile_against_document(lst, active_dimensions=active_dimensions) + + def cancel_system_generated_credit_debit_notes(self): + # Cancel 'Credit/Debit' Note Journal Entries, if found. + if self.doctype in ["Sales Invoice", "Purchase Invoice"]: + voucher_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "reference_type": self.doctype, + "reference_name": self.name, + "voucher_type": voucher_type, + "docstatus": 1, + }, + pluck="name", + ) + for x in journals: + frappe.get_doc("Journal Entry", x).cancel() def on_cancel(self): + from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( + remove_from_bank_transaction, + ) from erpnext.accounts.utils import ( cancel_exchange_gain_loss_journal, unlink_ref_doc_from_payment_entries, ) + remove_from_bank_transaction(self.doctype, self.name) + if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: + self.cancel_system_generated_credit_debit_notes() + # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) @@ -1477,9 +1586,12 @@ class AccountsController(TransactionBase): "account": item.discount_account, "against": supplier_or_customer, dr_or_cr: flt( - discount_amount * self.get("conversion_rate"), item.precision("discount_amount") + discount_amount * self.get("conversion_rate"), + item.precision("discount_amount"), + ), + dr_or_cr + "_in_account_currency": flt( + discount_amount, item.precision("discount_amount") ), - dr_or_cr + "_in_account_currency": flt(discount_amount, item.precision("discount_amount")), "cost_center": item.cost_center, "project": item.project, }, @@ -1495,10 +1607,12 @@ class AccountsController(TransactionBase): "account": income_or_expense_account, "against": supplier_or_customer, rev_dr_cr: flt( - discount_amount * self.get("conversion_rate"), item.precision("discount_amount") + discount_amount * self.get("conversion_rate"), + item.precision("discount_amount"), + ), + rev_dr_cr + "_in_account_currency": flt( + discount_amount, item.precision("discount_amount") ), - rev_dr_cr - + "_in_account_currency": flt(discount_amount, item.precision("discount_amount")), "cost_center": item.cost_center, "project": item.project or self.project, }, @@ -1530,17 +1644,15 @@ class AccountsController(TransactionBase): item_allowance = {} global_qty_allowance, global_amount_allowance = None, None - role_allowed_to_over_bill = frappe.db.get_single_value( - "Accounts Settings", "role_allowed_to_over_bill" + role_allowed_to_over_bill = frappe.get_cached_value( + "Accounts Settings", None, "role_allowed_to_over_bill" ) user_roles = frappe.get_roles() total_overbilled_amt = 0.0 reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)] - reference_details = self.get_billing_reference_details( - reference_names, ref_dt + " Item", based_on - ) + reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on) for item in self.get("items"): if not item.get(item_ref_dn): @@ -1726,17 +1838,13 @@ class AccountsController(TransactionBase): def raise_missing_debit_credit_account_error(self, party_type, party): """Raise an error if debit to/credit to account does not exist.""" - db_or_cr = ( - frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To") - ) + db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To") rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable" link_to_party = frappe.utils.get_link_to_form(party_type, party) link_to_company = frappe.utils.get_link_to_form("Company", self.company) - message = _("{0} Account not found against Customer {1}.").format( - db_or_cr, frappe.bold(party) or "" - ) + message = _("{0} Account not found against Customer {1}.").format(db_or_cr, frappe.bold(party) or "") message += "
    " + _("Please set one of the following:") + "
    " message += ( "
    • " @@ -1789,7 +1897,6 @@ class AccountsController(TransactionBase): and party_account_currency != self.company_currency and self.currency != party_account_currency ): - frappe.throw( _("Accounting Entry for {0}: {1} can only be made in currency: {2}").format( party_type, party, party_account_currency @@ -1810,9 +1917,7 @@ class AccountsController(TransactionBase): party_type, party = self.get_party() party_gle_currency = get_party_gle_currency(party_type, party, self.company) - party_account = ( - self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to") - ) + party_account = self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to") party_account_currency = get_account_currency(party_account) allow_multi_currency_invoices_against_single_party_account = frappe.db.get_singles_value( "Accounts Settings", "allow_multi_currency_invoices_against_single_party_account" @@ -1835,10 +1940,8 @@ class AccountsController(TransactionBase): consider_for_total_advance = True if adv.reference_name == linked_doc_name: frappe.db.sql( - """delete from `tab{0} Advance` - where name = %s""".format( - self.doctype - ), + f"""delete from `tab{self.doctype} Advance` + where name = %s""", adv.name, ) consider_for_total_advance = False @@ -1851,21 +1954,26 @@ class AccountsController(TransactionBase): ) def group_similar_items(self): - group_item_qty = {} - group_item_amount = {} + grouped_items = {} # to update serial number in print count = 0 + fields_to_group = frappe.get_hooks("fields_for_group_similar_items") + fields_to_group = set(fields_to_group) + for item in self.items: - group_item_qty[item.item_code] = group_item_qty.get(item.item_code, 0) + item.qty - group_item_amount[item.item_code] = group_item_amount.get(item.item_code, 0) + item.amount + item_values = grouped_items.setdefault(item.item_code, defaultdict(int)) + + for field in fields_to_group: + item_values[field] += item.get(field, 0) duplicate_list = [] for item in self.items: - if item.item_code in group_item_qty: + if item.item_code in grouped_items: count += 1 - item.qty = group_item_qty[item.item_code] - item.amount = group_item_amount[item.item_code] + + for field in fields_to_group: + item.set(field, grouped_items[item.item_code][field]) if item.qty: item.rate = flt(flt(item.amount) / flt(item.qty), item.precision("rate")) @@ -1873,7 +1981,7 @@ class AccountsController(TransactionBase): item.rate = 0 item.idx = count - del group_item_qty[item.item_code] + del grouped_items[item.item_code] else: duplicate_list.append(item) for item in duplicate_list: @@ -2050,7 +2158,9 @@ class AccountsController(TransactionBase): for d in self.get("payment_schedule"): if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date): frappe.throw( - _("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(d.idx) + _("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format( + d.idx + ) ) elif d.due_date in dates: li.append(_("{0} in row {1}").format(d.due_date, d.idx)) @@ -2058,9 +2168,7 @@ class AccountsController(TransactionBase): if li: duplicates = "
      " + "
      ".join(li) - frappe.throw( - _("Rows with duplicate due dates in other rows were found: {0}").format(duplicates) - ) + frappe.throw(_("Rows with duplicate due dates in other rows were found: {0}").format(duplicates)) def validate_payment_schedule_amount(self): if self.doctype == "Sales Invoice" and self.is_pos: @@ -2183,7 +2291,7 @@ class AccountsController(TransactionBase): jv.voucher_type = "Journal Entry" jv.posting_date = self.posting_date jv.company = self.company - jv.remark = "Adjustment for {} {}".format(self.doctype, self.name) + jv.remark = f"Adjustment for {self.doctype} {self.name}" reconcilation_entry = frappe._dict() advance_entry = frappe._dict() @@ -2193,9 +2301,7 @@ class AccountsController(TransactionBase): reconcilation_entry.party = secondary_party reconcilation_entry.reference_type = self.doctype reconcilation_entry.reference_name = self.name - reconcilation_entry.cost_center = self.cost_center or erpnext.get_default_cost_center( - self.company - ) + reconcilation_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company) advance_entry.account = primary_account advance_entry.party_type = primary_party_type @@ -2241,7 +2347,7 @@ class AccountsController(TransactionBase): def check_if_fields_updated(self, fields_to_check, child_tables): # Check if any field affecting accounting entry is altered doc_before_update = self.get_doc_before_save() - accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] + accounting_dimensions = [*get_accounting_dimensions(), "cost_center", "project"] # Check if opening entry check updated needs_repost = doc_before_update.get("is_opening") != self.is_opening @@ -2281,9 +2387,7 @@ class AccountsController(TransactionBase): @frappe.whitelist() def get_tax_rate(account_head): - return frappe.get_cached_value( - "Account", account_head, ["tax_rate", "account_name"], as_dict=True - ) + return frappe.get_cached_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) @frappe.whitelist() @@ -2313,7 +2417,7 @@ def get_taxes_and_charges(master_doctype, master_name): tax_master = frappe.get_doc(master_doctype, master_name) taxes_and_charges = [] - for i, tax in enumerate(tax_master.get("taxes")): + for _i, tax in enumerate(tax_master.get("taxes")): tax = tax.as_dict() for fieldname in default_fields + child_table_fields: @@ -2365,6 +2469,7 @@ def validate_taxes_and_charges(tax): def validate_account_head(idx, account, company, context=""): account_company = frappe.get_cached_value("Account", account, "company") + is_group = frappe.get_cached_value("Account", account, "is_group") if account_company != company: frappe.throw( @@ -2374,6 +2479,12 @@ def validate_account_head(idx, account, company, context=""): title=_("Invalid Account"), ) + if is_group: + frappe.throw( + _("Row {0}: Account {1} is a Group Account").format(idx, frappe.bold(account)), + title=_("Invalid Account"), + ) + def validate_cost_center(tax, doc): if not tax.cost_center: @@ -2425,9 +2536,7 @@ def set_balance_in_account_currency( ): if (not conversion_rate) and (account_currency != company_currency): frappe.throw( - _("Account: {0} with currency: {1} can not be selected").format( - gl_dict.account, account_currency - ) + _("Account: {0} with currency: {1} can not be selected").format(gl_dict.account, account_currency) ) gl_dict["account_currency"] = ( @@ -2437,9 +2546,7 @@ def set_balance_in_account_currency( # set debit/credit in account currency if not provided if flt(gl_dict.debit) and not flt(gl_dict.debit_in_account_currency): gl_dict.debit_in_account_currency = ( - gl_dict.debit - if account_currency == company_currency - else flt(gl_dict.debit / conversion_rate, 2) + gl_dict.debit if account_currency == company_currency else flt(gl_dict.debit / conversion_rate, 2) ) if flt(gl_dict.credit) and not flt(gl_dict.credit_in_account_currency): @@ -2459,9 +2566,7 @@ def get_advance_journal_entries( order_list, include_unallocated=True, ): - dr_or_cr = ( - "credit_in_account_currency" if party_type == "Customer" else "debit_in_account_currency" - ) + dr_or_cr = "credit_in_account_currency" if party_type == "Customer" else "debit_in_account_currency" conditions = [] if include_unallocated: @@ -2470,7 +2575,7 @@ def get_advance_journal_entries( if order_list: order_condition = ", ".join(["%s"] * len(order_list)) conditions.append( - " (t2.reference_type = '{0}' and ifnull(t2.reference_name, '') in ({1}))".format( + " (t2.reference_type = '{}' and ifnull(t2.reference_name, '') in ({}))".format( order_doctype, order_condition ) ) @@ -2479,10 +2584,10 @@ def get_advance_journal_entries( # nosemgrep journal_entries = frappe.db.sql( - """ + f""" select 'Journal Entry' as reference_type, t1.name as reference_name, - t1.remark as remarks, t2.{0} as amount, t2.name as reference_row, + t1.remark as remarks, t2.{amount_field} as amount, t2.name as reference_row, t2.reference_name as against_order, t2.exchange_rate from `tabJournal Entry` t1, `tabJournal Entry Account` t2 @@ -2490,11 +2595,9 @@ def get_advance_journal_entries( t1.name = t2.parent and t2.account = %s and t2.party_type = %s and t2.party = %s and t2.is_advance = 'Yes' and t1.docstatus = 1 - and {1} > 0 {2} - order by t1.posting_date""".format( - amount_field, dr_or_cr, reference_condition - ), - [party_account, party_type, party] + order_list, + and {dr_or_cr} > 0 {reference_condition} + order by t1.posting_date""", + [party_account, party_type, party, *order_list], as_dict=1, ) @@ -2518,86 +2621,88 @@ def get_advance_payment_entries( condition=None, payment_name=None, ): + pe = qb.DocType("Payment Entry") + per = qb.DocType("Payment Entry Reference") + party_account_field = "paid_from" if party_type == "Customer" else "paid_to" - currency_field = ( - "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" - ) + currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" payment_type = "Receive" if party_type == "Customer" else "Pay" - exchange_rate_field = ( - "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate" - ) + exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate" payment_entries_against_order, unallocated_payment_entries = [], [] - limit_cond = "limit %s" % limit if limit else "" + + if not condition: + condition = [] + + if payment_name: + condition.append(pe.name.like(f"%%{payment_name}%%")) if order_list or against_all_orders: + orders_condition = [] if order_list: - reference_condition = " and t2.reference_name in ({0})".format( - ", ".join(["%s"] * len(order_list)) + orders_condition.append(per.reference_name.isin(order_list)) + payment_entries_query = ( + qb.from_(pe) + .inner_join(per) + .on(pe.name == per.parent) + .select( + ConstantColumn("Payment Entry").as_("reference_type"), + pe.name.as_("reference_name"), + pe.remarks, + per.allocated_amount.as_("amount"), + per.name.as_("reference_row"), + per.reference_name.as_("against_order"), + pe.posting_date, + pe[currency_field].as_("currency"), + pe[exchange_rate_field].as_("exchange_rate"), ) - else: - reference_condition = "" - order_list = [] - - payment_name_filter = "" - if payment_name: - payment_name_filter = " and t1.name like '%%{0}%%'".format(payment_name) - - if not condition: - condition = "" - - payment_entries_against_order = frappe.db.sql( - """ - select - 'Payment Entry' as reference_type, t1.name as reference_name, - t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, - t2.reference_name as against_order, t1.posting_date, - t1.{0} as currency, t1.{5} as exchange_rate - from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 - where - t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s - and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 - and t2.reference_doctype = %s {2} {3} {6} - order by t1.posting_date {4} - """.format( - currency_field, - party_account_field, - reference_condition, - condition, - limit_cond, - exchange_rate_field, - payment_name_filter, - ), - [party_account, payment_type, party_type, party, order_doctype] + order_list, - as_dict=1, + .where( + (pe[party_account_field] == party_account) + & (pe.payment_type == payment_type) + & (pe.party_type == party_type) + & (pe.party == party) + & (pe.docstatus == 1) + & (per.reference_doctype == order_doctype) + ) + .where(Criterion.all(condition)) + .where(Criterion.all(orders_condition)) + .orderby(pe.posting_date) ) + if limit: + payment_entries_query = payment_entries_query.limit(limit) + + payment_entries_against_order = payment_entries_query.run(as_dict=1) + if include_unallocated: - payment_name_filter = "" - if payment_name: - payment_name_filter = " and name like '%%{0}%%'".format(payment_name) - - unallocated_payment_entries = frappe.db.sql( - """ - select 'Payment Entry' as reference_type, name as reference_name, posting_date, - remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency - from `tabPayment Entry` - where - {0} = %s and party_type = %s and party = %s and payment_type = %s - and docstatus = 1 and unallocated_amount > 0 {condition} {4} - order by posting_date {1} - """.format( - party_account_field, - limit_cond, - exchange_rate_field, - currency_field, - payment_name_filter, - condition=condition or "", - ), - (party_account, party_type, party, payment_type), - as_dict=1, + unallocated_payment_query = ( + qb.from_(pe) + .select( + ConstantColumn("Payment Entry").as_("reference_type"), + pe.name.as_("reference_name"), + pe.posting_date, + pe.remarks, + pe.unallocated_amount.as_("amount"), + pe[exchange_rate_field].as_("exchange_rate"), + pe[currency_field].as_("currency"), + ) + .where( + (pe[party_account_field] == party_account) + & (pe.party_type == party_type) + & (pe.party == party) + & (pe.payment_type == payment_type) + & (pe.docstatus == 1) + & (pe.unallocated_amount.gt(0)) + ) + .where(Criterion.all(condition)) + .orderby(pe.posting_date) ) + if limit: + unallocated_payment_query = unallocated_payment_query.limit(limit) + + unallocated_payment_entries = unallocated_payment_query.run(as_dict=1) + return list(payment_entries_against_order) + list(unallocated_payment_entries) @@ -2668,9 +2773,7 @@ def get_payment_terms( schedule = [] for d in terms_doc.get("terms"): - term_details = get_payment_term_details( - d, posting_date, grand_total, base_grand_total, bill_date - ) + term_details = get_payment_term_details(d, posting_date, grand_total, base_grand_total, bill_date) schedule.append(term_details) return schedule @@ -2788,9 +2891,7 @@ def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True): tax_row.db_insert() -def set_order_defaults( - parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item -): +def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item): """ Returns a Sales/Purchase Order Item child item containing the default values """ @@ -2806,9 +2907,7 @@ def set_order_defaults( child_item.stock_uom = item.stock_uom child_item.uom = trans_item.get("uom") or item.stock_uom child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) - conversion_factor = flt( - get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor") - ) + conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor if child_doctype == "Purchase Order Item": @@ -2955,9 +3054,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil def get_new_child_item(item_row): child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item" - return set_order_defaults( - parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row - ) + return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row) def validate_quantity(child_item, new_data): if not flt(new_data.get("qty")): @@ -2971,9 +3068,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty): frappe.throw(_("Cannot set quantity less than delivered quantity")) - if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt( - child_item.received_qty - ): + if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty): frappe.throw(_("Cannot set quantity less than received quantity")) def should_update_supplied_items(doc) -> bool: @@ -2988,9 +3083,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil item.supplied_qty or item.consumed_qty or item.returned_qty for item in doc.supplied_items ) - update_supplied_items = ( - any_qty_changed or items_added_or_removed or any_conversion_factor_changed - ) + update_supplied_items = any_qty_changed or items_added_or_removed or any_conversion_factor_changed if update_supplied_items and supplied_items_processed: frappe.throw(_("Item qty can not be updated as raw materials are already processed.")) @@ -3027,8 +3120,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty")) - prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt( - d.get("conversion_factor") + prev_con_fac, new_con_fac = ( + flt(child_item.get("conversion_factor")), + flt(d.get("conversion_factor")), ) prev_uom, new_uom = child_item.get("uom"), d.get("uom") @@ -3107,7 +3201,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if parent_doctype in sales_doctypes: child_item.margin_type = "Amount" child_item.margin_rate_or_amount = flt( - child_item.rate - child_item.price_list_rate, child_item.precision("margin_rate_or_amount") + child_item.rate - child_item.price_list_rate, + child_item.precision("margin_rate_or_amount"), ) child_item.rate_with_margin = child_item.rate else: @@ -3187,10 +3282,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.set_status() -def check_if_child_table_updated( - child_table_before_update, child_table_after_update, fields_to_check -): - accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] +def check_if_child_table_updated(child_table_before_update, child_table_after_update, fields_to_check): + accounting_dimensions = [*get_accounting_dimensions(), "cost_center", "project"] # Check if any field affecting accounting entry is altered for index, item in enumerate(child_table_after_update): for field in fields_to_check: @@ -3204,6 +3297,37 @@ def check_if_child_table_updated( return False +def merge_taxes(source_taxes, target_doc): + from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( + update_item_wise_tax_detail, + ) + + existing_taxes = target_doc.get("taxes") or [] + idx = 1 + for tax in source_taxes: + found = False + for t in existing_taxes: + if t.account_head == tax.account_head and t.cost_center == tax.cost_center: + t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount) + t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount) + update_item_wise_tax_detail(t, tax) + found = True + + if not found: + tax.charge_type = "Actual" + tax.idx = idx + idx += 1 + tax.included_in_print_rate = 0 + tax.dont_recompute_tax = 1 + tax.row_id = "" + tax.tax_amount = tax.tax_amount_after_discount_amount + tax.base_tax_amount = tax.base_tax_amount_after_discount_amount + tax.item_wise_tax_detail = tax.item_wise_tax_detail + existing_taxes.append(tax) + + target_doc.set("taxes", existing_taxes) + + @erpnext.allow_regional def validate_regional(doc): pass diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 8e9b2289079..89226453b7f 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -33,7 +33,7 @@ class BuyingController(SubcontractingController): def validate(self): self.set_rate_for_standalone_debit_note() - super(BuyingController, self).validate() + super().validate() if getattr(self, "supplier", None) and not self.supplier_name: self.supplier_name = frappe.db.get_value("Supplier", self.supplier, "supplier_name") @@ -49,9 +49,7 @@ class BuyingController(SubcontractingController): if self.doctype == "Purchase Invoice": self.validate_purchase_receipt_if_update_stock() - if self.doctype == "Purchase Receipt" or ( - self.doctype == "Purchase Invoice" and self.update_stock - ): + if self.doctype == "Purchase Receipt" or (self.doctype == "Purchase Invoice" and self.update_stock): # self.validate_purchase_return() self.validate_rejected_warehouse() self.validate_accepted_rejected_qty() @@ -67,12 +65,10 @@ class BuyingController(SubcontractingController): self.update_valuation_rate() def onload(self): - super(BuyingController, self).onload() + super().onload() self.set_onload( "backflush_based_on", - frappe.db.get_single_value( - "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" - ), + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on"), ) def set_rate_for_standalone_debit_note(self): @@ -101,7 +97,7 @@ class BuyingController(SubcontractingController): row.margin_rate_or_amount = 0.0 def set_missing_values(self, for_validate=False): - super(BuyingController, self).set_missing_values(for_validate) + super().set_missing_values(for_validate) self.set_supplier_from_item_default() self.set_price_list_currency("Buying") @@ -161,9 +157,7 @@ class BuyingController(SubcontractingController): if self.doctype not in ["Purchase Receipt", "Purchase Invoice"] or not self.is_return: return - purchase_doc_field = ( - "purchase_receipt" if self.doctype == "Purchase Receipt" else "purchase_invoice" - ) + purchase_doc_field = "purchase_receipt" if self.doctype == "Purchase Receipt" else "purchase_invoice" not_cancelled_asset = [] if self.return_against: not_cancelled_asset = [ @@ -190,8 +184,8 @@ class BuyingController(SubcontractingController): lc_voucher_data = frappe.db.sql( """select sum(applicable_charges), cost_center from `tabLanded Cost Item` - where docstatus = 1 and purchase_receipt_item = %s""", - d.name, + where docstatus = 1 and purchase_receipt_item = %s and receipt_document = %s""", + (d.name, self.name), ) d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0 if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]: @@ -494,7 +488,6 @@ class BuyingController(SubcontractingController): pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty")) if pr_qty: - if d.from_warehouse and ( (not cint(self.is_return) and self.docstatus == 1) or (cint(self.is_return) and self.docstatus == 2) @@ -559,7 +552,8 @@ class BuyingController(SubcontractingController): or (cint(self.is_return) and self.docstatus == 1) ): from_warehouse_sle = self.get_sl_entries( - d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1} + d, + {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1}, ) if flt(rejected_qty) != 0: @@ -631,7 +625,7 @@ class BuyingController(SubcontractingController): update_last_purchase_rate(self, is_submit=1) def on_cancel(self): - super(BuyingController, self).on_cancel() + super().on_cancel() if self.get("is_return"): return @@ -656,7 +650,9 @@ class BuyingController(SubcontractingController): "doctype": self.doctype, "company": self.company, "posting_date": ( - self.schedule_date if self.doctype == "Material Request" else self.transaction_date + self.schedule_date + if self.doctype == "Material Request" + else self.transaction_date ), } ) @@ -688,17 +684,21 @@ class BuyingController(SubcontractingController): asset = self.make_asset(d, is_grouped_asset=True) created_assets.append(asset) else: - for qty in range(cint(d.qty)): + for _qty in range(cint(d.qty)): asset = self.make_asset(d) created_assets.append(asset) if len(created_assets) > 5: # dont show asset form links if more than 5 assets are created messages.append( - _("{} Assets created for {}").format(len(created_assets), frappe.bold(d.item_code)) + _("{} Assets created for {}").format( + len(created_assets), frappe.bold(d.item_code) + ) ) else: - assets_link = list(map(lambda d: frappe.utils.get_link_to_form("Asset", d), created_assets)) + assets_link = list( + map(lambda d: frappe.utils.get_link_to_form("Asset", d), created_assets) + ) assets_link = frappe.bold(",".join(assets_link)) is_plural = "s" if len(created_assets) != 1 else "" @@ -709,9 +709,9 @@ class BuyingController(SubcontractingController): ) else: frappe.throw( - _("Row {}: Asset Naming Series is mandatory for the auto creation for item {}").format( - d.idx, frappe.bold(d.item_code) - ) + _( + "Row {}: Asset Naming Series is mandatory for the auto creation for item {}" + ).format(d.idx, frappe.bold(d.item_code)) ) else: messages.append( @@ -730,11 +730,8 @@ class BuyingController(SubcontractingController): item_data = frappe.db.get_value( "Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1 ) - - if is_grouped_asset: - purchase_amount = flt(row.base_amount + row.item_tax_amount) - else: - purchase_amount = flt(row.base_rate + row.item_tax_amount) + asset_quantity = row.qty if is_grouped_asset else 1 + purchase_amount = flt(row.valuation_rate) * asset_quantity asset = frappe.get_doc( { @@ -750,7 +747,7 @@ class BuyingController(SubcontractingController): "calculate_depreciation": 1, "purchase_receipt_amount": purchase_amount, "gross_purchase_amount": purchase_amount, - "asset_quantity": row.qty if is_grouped_asset else 1, + "asset_quantity": asset_quantity, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, "cost_center": row.cost_center, @@ -814,7 +811,8 @@ class BuyingController(SubcontractingController): if self.doctype == "Purchase Invoice" and not self.get("update_stock"): return - frappe.db.sql("delete from `tabAsset Movement` where reference_name=%s", self.name) + asset_movement = frappe.db.get_value("Asset Movement", {"reference_name": self.name}, "name") + frappe.delete_doc("Asset Movement", asset_movement, force=1) def validate_schedule_date(self): if not self.get("items"): @@ -876,11 +874,9 @@ def validate_item_type(doc, fieldname, message): invalid_items = [ d[0] for d in frappe.db.sql( - """ - select item_code from tabItem where name in ({0}) and {1}=0 - """.format( - item_list, fieldname - ), + f""" + select item_code from tabItem where name in ({item_list}) and {fieldname}=0 + """, as_list=True, ) ] diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 1ba10259a8e..2a1a00c5214 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -56,9 +56,7 @@ def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part variant.flags.ignore_mandatory = True variant.save() - if not frappe.db.exists( - "Item Manufacturer", {"item_code": variant.name, "manufacturer": manufacturer} - ): + if not frappe.db.exists("Item Manufacturer", {"item_code": variant.name, "manufacturer": manufacturer}): manufacturer_doc = frappe.new_doc("Item Manufacturer") manufacturer_doc.update( { @@ -122,9 +120,7 @@ def validate_is_incremental(numeric_attribute, attribute, value, item): ) -def validate_item_attribute_value( - attributes_list, attribute, attribute_value, item, from_variant=True -): +def validate_item_attribute_value(attributes_list, attribute, attribute_value, item, from_variant=True): allow_rename_attribute_value = frappe.db.get_single_value( "Item Variant Settings", "allow_rename_attribute_value" ) @@ -172,7 +168,7 @@ def get_attribute_values(item): def find_variant(template, args, variant_item_code=None): conditions = [ - """(iv_attribute.attribute={0} and iv_attribute.attribute_value={1})""".format( + """(iv_attribute.attribute={} and iv_attribute.attribute_value={})""".format( frappe.db.escape(key), frappe.db.escape(cstr(value)) ) for key, value in args.items() @@ -182,9 +178,7 @@ def find_variant(template, args, variant_item_code=None): from erpnext.e_commerce.variant_selector.utils import get_item_codes_by_attributes - possible_variants = [ - i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code - ] + possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code] for variant in possible_variants: variant = frappe.get_doc("Item", variant) @@ -360,7 +354,9 @@ def copy_attributes_to_variant(item, variant): if variant.attributes: attributes_description = item.description + " " for d in variant.attributes: - attributes_description += "
      " + d.attribute + ": " + cstr(d.attribute_value) + "
      " + attributes_description += ( + "
      " + d.attribute + ": " + cstr(d.attribute_value) + "
      " + ) if attributes_description not in variant.description: variant.description = attributes_description @@ -394,8 +390,8 @@ def make_variant_item_code(template_item_code, template_item_name, variant): abbreviations.append(abbr_or_value) if abbreviations: - variant.item_code = "{0}-{1}".format(template_item_code, "-".join(abbreviations)) - variant.item_name = "{0}-{1}".format(template_item_name, "-".join(abbreviations)) + variant.item_code = "{}-{}".format(template_item_code, "-".join(abbreviations)) + variant.item_name = "{}-{}".format(template_item_name, "-".join(abbreviations)) @frappe.whitelist() diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index b906a8a7987..629404d3c82 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -39,7 +39,7 @@ def set_print_templates_for_taxes(doc, settings): def format_columns(display_columns, compact_fields): - compact_fields = compact_fields + ["image", "item_code", "item_name"] + compact_fields = [*compact_fields, "image", "item_code", "item_name"] final_columns = [] for column in display_columns: if column not in compact_fields: diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b73ebf53ae8..be965e41b3e 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -6,9 +6,12 @@ import json from collections import defaultdict import frappe -from frappe import scrub +from frappe import qb, scrub from frappe.desk.reportview import get_filters_cond, get_match_cond +from frappe.query_builder import Criterion, CustomFunction +from frappe.query_builder.functions import Locate from frappe.utils import nowdate, unique +from pypika import Order import erpnext from erpnext.stock.get_item_details import _get_item_tax_template @@ -159,7 +162,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): account_type_condition = "AND account_type in %(account_types)s" accounts = frappe.db.sql( - """ + f""" SELECT name, parent_account FROM `tabAccount` WHERE `tabAccount`.docstatus!=2 @@ -169,20 +172,16 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): AND disabled = %(disabled)s AND (account_currency = %(currency)s or ifnull(account_currency, '') = '') AND `{searchfield}` LIKE %(txt)s - {mcond} + {get_match_cond(doctype)} ORDER BY idx DESC, name LIMIT %(limit)s offset %(offset)s - """.format( - account_type_condition=account_type_condition, - searchfield=searchfield, - mcond=get_match_cond(doctype), - ), + """, dict( account_types=filters.get("account_type"), company=filters.get("company"), disabled=filters.get("disabled", 0), currency=company_currency, - txt="%{}%".format(txt), + txt=f"%{txt}%", offset=start, limit=page_len, ), @@ -212,7 +211,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals searchfields = meta.get_search_fields() columns = "" - extra_searchfields = [field for field in searchfields if not field in ["name", "description"]] + extra_searchfields = [field for field in searchfields if field not in ["name", "description"]] if extra_searchfields: columns += ", " + ", ".join(extra_searchfields) @@ -224,7 +223,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals searchfields = searchfields + [ field for field in [searchfield or "name", "item_code", "item_group", "item_name"] - if not field in searchfields + if field not in searchfields ] searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) @@ -232,7 +231,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if filters.get("customer") or filters.get("supplier"): party = filters.get("customer") or filters.get("supplier") item_rules_list = frappe.get_all( - "Party Specific Item", filters={"party": party}, fields=["restrict_based_on", "based_on_value"] + "Party Specific Item", + filters={"party": party}, + fields=["restrict_based_on", "based_on_value"], ) filters_dict = {} @@ -329,37 +330,48 @@ def bom(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_project_name(doctype, txt, searchfield, start, page_len, filters): - doctype = "Project" - cond = "" + proj = qb.DocType("Project") + qb_filter_and_conditions = [] + qb_filter_or_conditions = [] + ifelse = CustomFunction("IF", ["condition", "then", "else"]) + if filters and filters.get("customer"): - cond = """(`tabProject`.customer = %s or - ifnull(`tabProject`.customer,"")="") and""" % ( - frappe.db.escape(filters.get("customer")) + qb_filter_and_conditions.append( + (proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == "" ) - fields = get_fields(doctype, ["name", "project_name"]) - searchfields = frappe.get_meta(doctype).get_search_fields() - searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields]) + qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"])) - return frappe.db.sql( - """select {fields} from `tabProject` - where - `tabProject`.status not in ('Completed', 'Cancelled') - and {cond} {scond} {match_cond} - order by - (case when locate(%(_txt)s, `tabProject`.name) > 0 then locate(%(_txt)s, `tabProject`.name) else 99999 end), - `tabProject`.idx desc, - `tabProject`.name asc - limit {page_len} offset {start}""".format( - fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]), - cond=cond, - scond=searchfields, - match_cond=get_match_cond(doctype), - start=start, - page_len=page_len, - ), - {"txt": "%{0}%".format(txt), "_txt": txt.replace("%", "")}, - ) + q = qb.from_(proj) + + fields = get_fields(doctype, ["name", "project_name"]) + for x in fields: + q = q.select(proj[x]) + + # don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched + searchfields = [ + x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"] + ] + + # pattern search + if txt: + for x in searchfields: + qb_filter_or_conditions.append(proj[x].like(f"%{txt}%")) + + q = q.where(Criterion.all(qb_filter_and_conditions)).where(Criterion.any(qb_filter_or_conditions)) + + # ordering + if txt: + # project_name containing search string 'txt' will be given higher precedence + q = q.orderby(ifelse(Locate(txt, proj.project_name) > 0, Locate(txt, proj.project_name), 99999)) + q = q.orderby(proj.idx, order=Order.desc).orderby(proj.name) + + if page_len: + q = q.limit(page_len) + + if start: + q = q.offset(start) + return q.run() @frappe.whitelist() @@ -370,11 +382,11 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, return frappe.db.sql( """ - select %(fields)s + select {fields} from `tabDelivery Note` - where `tabDelivery Note`.`%(key)s` like %(txt)s and + where `tabDelivery Note`.`{key}` like {txt} and `tabDelivery Note`.docstatus = 1 - and status not in ('Stopped', 'Closed') %(fcond)s + and status not in ('Stopped', 'Closed') {fcond} and ( (`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100) or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100) @@ -383,17 +395,16 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, and return_against in (select name from `tabDelivery Note` where per_billed < 100) ) ) - %(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(page_len)s offset %(start)s - """ - % { - "fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]), - "key": searchfield, - "fcond": get_filters_cond(doctype, filters, []), - "mcond": get_match_cond(doctype), - "start": start, - "page_len": page_len, - "txt": "%(txt)s", - }, + {mcond} order by `tabDelivery Note`.`{key}` asc limit {page_len} offset {start} + """.format( + fields=", ".join([f"`tabDelivery Note`.{f}" for f in fields]), + key=searchfield, + fcond=get_filters_cond(doctype, filters, []), + mcond=get_match_cond(doctype), + start=start, + page_len=page_len, + txt="%(txt)s", + ), {"txt": ("%%%s%%" % txt)}, as_dict=as_dict, ) @@ -412,7 +423,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): "item_code": filters.get("item_code"), "warehouse": filters.get("warehouse"), "posting_date": filters.get("posting_date"), - "txt": "%{0}%".format(txt), + "txt": f"%{txt}%", "start": start, "page_len": page_len, } @@ -438,7 +449,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields]) batch_nos = frappe.db.sql( - """select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom, + f"""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom, concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date) {search_columns} from `tabStock Ledger Entry` sle @@ -454,23 +465,17 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): {search_cond}) and batch.docstatus < 2 {cond} - {match_conditions} + {get_match_cond(doctype)} group by batch_no {having_clause} order by batch.expiry_date, sle.batch_no desc - limit %(page_len)s offset %(start)s""".format( - search_columns=search_columns, - cond=cond, - match_conditions=get_match_cond(doctype), - having_clause=having_clause, - search_cond=search_cond, - ), + limit %(page_len)s offset %(start)s""", args, ) return batch_nos else: return frappe.db.sql( - """select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date) + f"""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date) {search_columns} from `tabBatch` batch where batch.disabled = 0 @@ -480,16 +485,11 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): or manufacturing_date like %(txt)s {search_cond}) and docstatus < 2 - {0} - {match_conditions} + {cond} + {get_match_cond(doctype)} order by expiry_date, name desc - limit %(page_len)s offset %(start)s""".format( - cond, - search_columns=search_columns, - search_cond=search_cond, - match_conditions=get_match_cond(doctype), - ), + limit %(page_len)s offset %(start)s""", args, ) @@ -502,7 +502,7 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): if isinstance(filters, dict): for key, val in filters.items(): - if isinstance(val, (list, tuple)): + if isinstance(val, list | tuple): filter_list.append([doctype, key, val[0], val[1]]) else: filter_list.append([doctype, key, "=", val]) @@ -563,24 +563,20 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}" return frappe.db.sql( - """select tabAccount.name from `tabAccount` + f"""select tabAccount.name from `tabAccount` where (tabAccount.report_type = "Profit and Loss" or tabAccount.account_type in ("Income Account", "Temporary")) and tabAccount.is_group=0 - and tabAccount.`{key}` LIKE %(txt)s - {condition} {match_condition} - order by idx desc, name""".format( - condition=condition, match_condition=get_match_cond(doctype), key=searchfield - ), + and tabAccount.`{searchfield}` LIKE %(txt)s + {condition} {get_match_cond(doctype)} + order by idx desc, name""", {"txt": "%" + txt + "%", "company": filters.get("company", "")}, ) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_filtered_dimensions( - doctype, txt, searchfield, start, page_len, filters, reference_doctype=None -): +def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters, reference_doctype=None): from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( get_dimension_filter_map, ) @@ -594,7 +590,7 @@ def get_filtered_dimensions( searchfields = frappe.get_meta(doctype).get_search_fields() meta = frappe.get_meta(doctype) - if meta.is_tree: + if meta.is_tree and meta.has_field("is_group"): query_filters.append(["is_group", "=", 0]) if meta.has_field("disabled"): @@ -646,15 +642,13 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters): condition += "and tabAccount.company = %(company)s" return frappe.db.sql( - """select tabAccount.name from `tabAccount` + f"""select tabAccount.name from `tabAccount` where (tabAccount.report_type = "Profit and Loss" or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed", "Capital Work in Progress")) and tabAccount.is_group=0 and tabAccount.docstatus!=2 - and tabAccount.{key} LIKE %(txt)s - {condition} {match_condition}""".format( - condition=condition, key=searchfield, match_condition=get_match_cond(doctype) - ), + and tabAccount.{searchfield} LIKE %(txt)s + {condition} {get_match_cond(doctype)}""", {"company": filters.get("company", ""), "txt": "%" + txt + "%"}, ) @@ -667,17 +661,24 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters): conditions, bin_conditions = [], [] filter_dict = get_doctype_wise_filters(filters) - query = """select `tabWarehouse`.name, + warehouse_field = "name" + meta = frappe.get_meta("Warehouse") + if meta.get("show_title_field_in_link") and meta.get("title_field"): + searchfield = meta.get("title_field") + warehouse_field = meta.get("title_field") + + query = """select `tabWarehouse`.`{warehouse_field}`, CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty from `tabWarehouse` left join `tabBin` on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions} where `tabWarehouse`.`{key}` like {txt} {fcond} {mcond} - order by ifnull(`tabBin`.actual_qty, 0) desc + order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc limit {page_len} offset {start} """.format( + warehouse_field=warehouse_field, bin_conditions=get_filters_cond( doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True ), @@ -686,7 +687,7 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters): mcond=get_match_cond(doctype), start=start, page_len=page_len, - txt=frappe.db.escape("%{0}%".format(txt)), + txt=frappe.db.escape(f"%{txt}%"), ) return frappe.db.sql(query) @@ -706,9 +707,7 @@ def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): query = """select batch_id from `tabBatch` where disabled = 0 and (expiry_date >= CURRENT_DATE or expiry_date IS NULL) - and name like {txt}""".format( - txt=frappe.db.escape("%{0}%".format(txt)) - ) + and name like {txt}""".format(txt=frappe.db.escape(f"%{txt}%")) if filters and filters.get("item"): query += " and item = {item}".format(item=frappe.db.escape(filters.get("item"))) @@ -742,9 +741,7 @@ def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): select pr.name from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pritem where pr.docstatus = 1 and pritem.parent = pr.name - and pr.name like {txt}""".format( - txt=frappe.db.escape("%{0}%".format(txt)) - ) + and pr.name like {txt}""".format(txt=frappe.db.escape(f"%{txt}%")) if filters and filters.get("item_code"): query += " and pritem.item_code = {item_code}".format( @@ -761,9 +758,7 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): select pi.name from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` piitem where pi.docstatus = 1 and piitem.parent = pi.name - and pi.name like {txt}""".format( - txt=frappe.db.escape("%{0}%".format(txt)) - ) + and pi.name like {txt}""".format(txt=frappe.db.escape(f"%{txt}%")) if filters and filters.get("item_code"): query += " and piitem.item_code = {item_code}".format( @@ -785,7 +780,6 @@ def get_doctypes_for_closing(doctype, txt, searchfield, start, page_len, filters @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_tax_template(doctype, txt, searchfield, start, page_len, filters): - item_doc = frappe.get_cached_doc("Item", filters.get("item_code")) item_group = filters.get("item_group") company = filters.get("company") @@ -797,9 +791,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): item_group = item_group_doc.parent_item_group if not taxes: - return frappe.get_all( - "Item Tax Template", filters={"disabled": 0, "company": company}, as_list=True - ) + return frappe.get_all("Item Tax Template", filters={"disabled": 0, "company": company}, as_list=True) else: valid_from = filters.get("valid_from") valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from @@ -821,7 +813,7 @@ def get_fields(doctype, fields=None): meta = frappe.get_meta(doctype) fields.extend(meta.get_search_fields()) - if meta.title_field and not meta.title_field.strip() in fields: + if meta.title_field and meta.title_field.strip() not in fields: fields.insert(1, meta.title_field.strip()) return unique(fields) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index efeedc14db6..eca751f76bc 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -8,7 +8,7 @@ from frappe.model.meta import get_field_precision from frappe.utils import flt, format_datetime, get_datetime import erpnext -from erpnext.stock.utils import get_incoming_rate +from erpnext.stock.utils import get_incoming_rate, get_valuation_method class StockOverReturnError(frappe.ValidationError): @@ -26,9 +26,7 @@ def validate_return(doc): def validate_return_against(doc): if not frappe.db.exists(doc.doctype, doc.return_against): - frappe.throw( - _("Invalid {0}: {1}").format(doc.meta.get_label("return_against"), doc.return_against) - ) + frappe.throw(_("Invalid {0}: {1}").format(doc.meta.get_label("return_against"), doc.return_against)) else: ref_doc = frappe.get_doc(doc.doctype, doc.return_against) @@ -40,8 +38,8 @@ def validate_return_against(doc): and ref_doc.docstatus.is_submitted() ): # validate posting date time - return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00") - ref_posting_datetime = "%s %s" % ( + return_posting_datetime = "{} {}".format(doc.posting_date, doc.get("posting_time") or "00:00:00") + ref_posting_datetime = "{} {}".format( ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00", ) @@ -81,7 +79,7 @@ def validate_returned_items(doc): select_fields += ",rejected_qty, received_qty" for d in frappe.db.sql( - """select {0} from `tab{1} Item` where parent = %s""".format(select_fields, doc.doctype), + f"""select {select_fields} from `tab{doc.doctype} Item` where parent = %s""", doc.return_against, as_dict=1, ): @@ -116,7 +114,12 @@ def validate_returned_items(doc): ref = valid_items.get(d.item_code, frappe._dict()) validate_quantity(doc, d, ref, valid_items, already_returned_items) - if ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") and flt(d.rate) > ref.rate: + if ( + ref.rate + and flt(d.rate) > ref.rate + and doc.doctype in ("Delivery Note", "Sales Invoice") + and get_valuation_method(ref.item_code) != "Moving Average" + ): frappe.throw( _("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format( d.idx, doc.doctype, doc.return_against @@ -244,17 +247,15 @@ def get_already_returned_items(doc): sum(abs(child.received_qty) * child.conversion_factor) as received_qty""" data = frappe.db.sql( - """ - select {0} + f""" + select {column} from - `tab{1} Item` child, `tab{2}` par + `tab{doc.doctype} Item` child, `tab{doc.doctype}` par where child.parent = par.name and par.docstatus = 1 and par.is_return = 1 and par.return_against = %s group by item_code - """.format( - column, doc.doctype, doc.doctype - ), + """, doc.return_against, as_dict=1, ) @@ -287,22 +288,22 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype): party_type = "customer" fields = [ - "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), + f"sum(abs(`tab{child_doctype}`.qty)) as qty", ] if doctype != "Subcontracting Receipt": fields += [ - "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype), + f"sum(abs(`tab{child_doctype}`.stock_qty)) as stock_qty", ] if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"): fields += [ - "sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype), - "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype), + f"sum(abs(`tab{child_doctype}`.rejected_qty)) as rejected_qty", + f"sum(abs(`tab{child_doctype}`.received_qty)) as received_qty", ] if doctype == "Purchase Receipt": - fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] + fields += [f"sum(abs(`tab{child_doctype}`.received_stock_qty)) as received_stock_qty"] # Used retrun against and supplier and is_retrun because there is an index added for it data = frappe.get_all( @@ -518,8 +519,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): "docstatus": ["=", 1], }, }, - doctype - + " Item": { + doctype + " Item": { "doctype": doctype + " Item", "field_map": {"serial_no": "serial_no", "batch_no": "batch_no", "bom": "bom"}, "postprocess": update_item, @@ -618,11 +618,7 @@ def get_filters( if reference_voucher_detail_no: filters["voucher_detail_no"] = reference_voucher_detail_no - if ( - voucher_type in ["Purchase Receipt", "Purchase Invoice"] - and 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/selling_controller.py b/erpnext/controllers/selling_controller.py index 7cef623148e..5c38d7bef3a 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.item.item import set_item_default from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor -from erpnext.stock.utils import get_incoming_rate +from erpnext.stock.utils import get_incoming_rate, get_valuation_method class SellingController(StockController): @@ -23,15 +23,16 @@ class SellingController(StockController): return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total) def onload(self): - super(SellingController, self).onload() + super().onload() if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): for item in self.get("items") + (self.get("packed_items") or []): item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True)) def validate(self): - super(SellingController, self).validate() + super().validate() self.validate_items() - self.validate_max_discount() + if not self.get("is_debit_note"): + self.validate_max_discount() self.validate_selling_price() self.set_qty_as_per_stock_uom() self.set_po_nos(for_validate=True) @@ -43,7 +44,7 @@ class SellingController(StockController): self.validate_auto_repeat_subscription_dates() def set_missing_values(self, for_validate=False): - super(SellingController, self).set_missing_values(for_validate) + super().set_missing_values(for_validate) # set contact and address details for customer, if they are not mentioned self.set_missing_lead_customer_details(for_validate=for_validate) @@ -283,7 +284,10 @@ class SellingController(StockController): if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom): throw_message( - item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate (Moving Average)" + item.idx, + item.item_name, + last_valuation_rate_in_sales_uom, + "valuation rate (Moving Average)", ) def get_item_list(self): @@ -411,7 +415,8 @@ class SellingController(StockController): "Cancelled" ]: frappe.throw( - _("{0} {1} is cancelled or closed").format(_("Sales Order"), so), frappe.InvalidStatusError + _("{0} {1} is cancelled or closed").format(_("Sales Order"), so), + frappe.InvalidStatusError, ) sales_order.update_reserved_qty(so_item_rows) @@ -422,11 +427,18 @@ class SellingController(StockController): items = self.get("items") + (self.get("packed_items") or []) for d in items: - if not self.get("return_against"): + if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"): + continue + + if not self.get("return_against") or ( + get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") + ): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get("stock_qty") or d.get("actual_qty")) - if not (self.get("is_return") and d.incoming_rate): + if not d.incoming_rate or ( + get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") + ): d.incoming_rate = get_incoming_rate( { "item_code": d.item_code, @@ -579,7 +591,8 @@ class SellingController(StockController): if self.doctype in ["Sales Order", "Quotation"]: for item in self.items: item.gross_profit = flt( - ((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item) + ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), + self.precision("amount", item), ) def set_customer_address(self): @@ -656,9 +669,9 @@ class SellingController(StockController): if d.get("target_warehouse") and d.get("warehouse") == d.get("target_warehouse"): warehouse = frappe.bold(d.get("target_warehouse")) frappe.throw( - _("Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same").format( - d.idx, warehouse, warehouse - ) + _( + "Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same" + ).format(d.idx, warehouse, warehouse) ) if not self.get("is_internal_customer") and any(d.get("target_warehouse") for d in items): diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index d09001c8fc1..dd3fab05ce6 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -253,9 +253,7 @@ class StatusUpdater(Document): """select item_code, `{target_ref_field}`, `{target_field}`, parenttype, parent from `tab{target_dt}` where `{target_ref_field}` < `{target_field}` - and name=%s and docstatus=1""".format( - **args - ), + and name=%s and docstatus=1""".format(**args), args["name"], as_dict=1, ) @@ -300,9 +298,7 @@ class StatusUpdater(Document): role_allowed_to_over_bill = frappe.db.get_single_value( "Accounts Settings", "role_allowed_to_over_bill" ) - role = ( - role_allowed_to_over_deliver_receive if qty_or_amount == "qty" else role_allowed_to_over_bill - ) + role = role_allowed_to_over_deliver_receive if qty_or_amount == "qty" else role_allowed_to_over_bill overflow_percent = ( (item[args["target_field"]] - item[args["target_ref_field"]]) / item[args["target_ref_field"]] @@ -413,12 +409,11 @@ class StatusUpdater(Document): args["second_source_extra_cond"] = "" args["second_source_condition"] = frappe.db.sql( - """ select ifnull((select sum(%(second_source_field)s) - from `tab%(second_source_dt)s` - where `%(second_join_field)s`='%(detail_id)s' - and (`tab%(second_source_dt)s`.docstatus=1) - %(second_source_extra_cond)s), 0) """ - % args + """ select ifnull((select sum({second_source_field}) + from `tab{second_source_dt}` + where `{second_join_field}`='{detail_id}' + and (`tab{second_source_dt}`.docstatus=1) + {second_source_extra_cond}), 0) """.format(**args) )[0][0] if args["detail_id"]: @@ -428,11 +423,10 @@ class StatusUpdater(Document): args["source_dt_value"] = ( frappe.db.sql( """ - (select ifnull(sum(%(source_field)s), 0) - from `tab%(source_dt)s` where `%(join_field)s`='%(detail_id)s' - and (docstatus=1 %(cond)s) %(extra_cond)s) - """ - % args + (select ifnull(sum({source_field}), 0) + from `tab{source_dt}` where `{join_field}`='{detail_id}' + and (docstatus=1 {cond}) {extra_cond}) + """.format(**args) )[0][0] or 0.0 ) @@ -441,10 +435,9 @@ class StatusUpdater(Document): args["source_dt_value"] += flt(args["second_source_condition"]) frappe.db.sql( - """update `tab%(target_dt)s` - set %(target_field)s = %(source_dt_value)s %(update_modified)s - where name='%(detail_id)s'""" - % args + """update `tab{target_dt}` + set {target_field} = {source_dt_value} {update_modified} + where name='{detail_id}'""".format(**args) ) def _update_percent_field_in_targets(self, args, update_modified=True): @@ -471,26 +464,24 @@ class StatusUpdater(Document): if args.get("target_parent_field"): frappe.db.sql( - """update `tab%(target_parent_dt)s` - set %(target_parent_field)s = round( + """update `tab{target_parent_dt}` + set {target_parent_field} = round( ifnull((select - ifnull(sum(case when abs(%(target_ref_field)s) > abs(%(target_field)s) then abs(%(target_field)s) else abs(%(target_ref_field)s) end), 0) - / sum(abs(%(target_ref_field)s)) * 100 - from `tab%(target_dt)s` where parent='%(name)s' and parenttype='%(target_parent_dt)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6) - %(update_modified)s - where name='%(name)s'""" - % args + ifnull(sum(case when abs({target_ref_field}) > abs({target_field}) then abs({target_field}) else abs({target_ref_field}) end), 0) + / sum(abs({target_ref_field})) * 100 + from `tab{target_dt}` where parent='{name}' and parenttype='{target_parent_dt}' having sum(abs({target_ref_field})) > 0), 0), 6) + {update_modified} + where name='{name}'""".format(**args) ) # update field if args.get("status_field"): frappe.db.sql( - """update `tab%(target_parent_dt)s` - set %(status_field)s = (case when %(target_parent_field)s<0.001 then 'Not %(keyword)s' - else case when %(target_parent_field)s>=99.999999 then 'Fully %(keyword)s' - else 'Partly %(keyword)s' end end) - where name='%(name)s'""" - % args + """update `tab{target_parent_dt}` + set {status_field} = (case when {target_parent_field}<0.001 then 'Not {keyword}' + else case when {target_parent_field}>=99.999999 then 'Fully {keyword}' + else 'Partly {keyword}' end end) + where name='{name}'""".format(**args) ) if update_modified: @@ -503,21 +494,19 @@ class StatusUpdater(Document): args["update_modified"] = "" return - args["update_modified"] = ", modified = {0}, modified_by = {1}".format( + args["update_modified"] = ", modified = {}, modified_by = {}".format( frappe.db.escape(now()), frappe.db.escape(frappe.session.user) ) def update_billing_status_for_zero_amount_refdoc(self, ref_dt): ref_fieldname = frappe.scrub(ref_dt) - ref_docs = [ - item.get(ref_fieldname) for item in (self.get("items") or []) if item.get(ref_fieldname) - ] + ref_docs = [item.get(ref_fieldname) for item in (self.get("items") or []) if item.get(ref_fieldname)] if not ref_docs: return zero_amount_refdocs = frappe.db.sql_list( - """ + f""" SELECT name from @@ -526,9 +515,7 @@ class StatusUpdater(Document): docstatus = 1 and base_net_total = 0 and name in %(ref_docs)s - """.format( - ref_dt=ref_dt - ), + """, {"ref_docs": ref_docs}, ) @@ -539,9 +526,8 @@ class StatusUpdater(Document): for ref_dn in zero_amount_refdoc: ref_doc_qty = flt( frappe.db.sql( - """select ifnull(sum(qty), 0) from `tab%s Item` - where parent=%s""" - % (ref_dt, "%s"), + """select ifnull(sum(qty), 0) from `tab{} Item` + where parent={}""".format(ref_dt, "%s"), (ref_dn), )[0][0] ) @@ -549,8 +535,7 @@ class StatusUpdater(Document): billed_qty = flt( frappe.db.sql( """select ifnull(sum(qty), 0) - from `tab%s Item` where %s=%s and docstatus=1""" - % (self.doctype, ref_fieldname, "%s"), + from `tab{} Item` where {}={} and docstatus=1""".format(self.doctype, ref_fieldname, "%s"), (ref_dn), )[0][0] ) @@ -573,6 +558,7 @@ class StatusUpdater(Document): ref_doc.set_status(update=True) +@frappe.request_cache def get_allowance_for( item_code, item_allowance=None, @@ -602,20 +588,20 @@ def get_allowance_for( global_amount_allowance, ) - qty_allowance, over_billing_allowance = frappe.db.get_value( + qty_allowance, over_billing_allowance = frappe.get_cached_value( "Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"] ) if qty_or_amount == "qty" and not qty_allowance: - if global_qty_allowance == None: + if global_qty_allowance is None: global_qty_allowance = flt( - frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance") + frappe.get_cached_value("Stock Settings", None, "over_delivery_receipt_allowance") ) qty_allowance = global_qty_allowance elif qty_or_amount == "amount" and not over_billing_allowance: - if global_amount_allowance == None: + if global_amount_allowance is None: global_amount_allowance = flt( - frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") + frappe.get_cached_value("Accounts Settings", None, "over_billing_allowance") ) over_billing_allowance = global_amount_allowance diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 403f71be5eb..15a79c8efb4 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -3,10 +3,9 @@ import json from collections import defaultdict -from typing import List, Tuple import frappe -from frappe import _ +from frappe import _, bold from frappe.utils import cint, cstr, flt, get_link_to_form, getdate import erpnext @@ -42,7 +41,7 @@ class BatchExpiredError(frappe.ValidationError): class StockController(AccountsController): def validate(self): - super(StockController, self).validate() + super().validate() if not self.get("is_return"): self.validate_inspection() self.validate_serialized_batch() @@ -126,10 +125,7 @@ class StockController(AccountsController): # remove extra whitespace and store one serial no on each line row.serial_no = clean_serial_no_string(row.serial_no) - def get_gl_entries( - self, warehouse_account=None, default_expense_account=None, default_cost_center=None - ): - + def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): if not warehouse_account: warehouse_account = get_warehouse_account_map(self.company) @@ -167,7 +163,9 @@ class StockController(AccountsController): "project": item_row.project or self.get("project"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "debit": flt(sle.stock_value_difference, precision), - "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", + "is_opening": item_row.get("is_opening") + or self.get("is_opening") + or "No", }, warehouse_account[sle.warehouse]["account_currency"], item=item_row, @@ -183,7 +181,9 @@ class StockController(AccountsController): "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "debit": -1 * flt(sle.stock_value_difference, precision), "project": item_row.get("project") or self.get("project"), - "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", + "is_opening": item_row.get("is_opening") + or self.get("is_opening") + or "No", }, item=item_row, ) @@ -250,9 +250,7 @@ class StockController(AccountsController): def get_debit_field_precision(self): if not frappe.flags.debit_field_precision: - frappe.flags.debit_field_precision = frappe.get_precision( - "GL Entry", "debit_in_account_currency" - ) + frappe.flags.debit_field_precision = frappe.get_precision("GL Entry", "debit_in_account_currency") return frappe.flags.debit_field_precision @@ -285,7 +283,7 @@ class StockController(AccountsController): return details - def get_items_and_warehouses(self) -> Tuple[List[str], List[str]]: + def get_items_and_warehouses(self) -> tuple[list[str], list[str]]: """Get list of items and warehouses affected by a transaction""" if not (hasattr(self, "items") or hasattr(self, "packed_items")): @@ -395,18 +393,11 @@ class StockController(AccountsController): if not d.batch_no: continue - frappe.db.set_value( - "Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None - ) + frappe.db.set_value("Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None) d.batch_no = None d.db_set("batch_no", None) - for data in frappe.get_all( - "Batch", {"reference_name": self.name, "reference_doctype": self.doctype} - ): - frappe.delete_doc("Batch", data.name) - def get_sl_entries(self, d, args): sl_dict = frappe._dict( { @@ -525,9 +516,7 @@ class StockController(AccountsController): if item_codes: serialized_items = frappe.db.sql_list( """select name from `tabItem` - where has_serial_no=1 and name in ({})""".format( - ", ".join(["%s"] * len(item_codes)) - ), + where has_serial_no=1 and name in ({})""".format(", ".join(["%s"] * len(item_codes))), tuple(item_codes), ) @@ -615,16 +604,12 @@ class StockController(AccountsController): def validate_qi_submission(self, row): """Check if QI is submitted on row level, during submission""" - action = frappe.db.get_single_value( - "Stock Settings", "action_if_quality_inspection_is_not_submitted" - ) + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") if not qa_docstatus == 1: link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection) - msg = ( - f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" - ) + msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" if action == "Stop": frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) else: @@ -673,6 +658,9 @@ class StockController(AccountsController): self.validate_in_transit_warehouses() self.validate_multi_currency() self.validate_packed_items() + + if self.get("is_internal_supplier") and self.docstatus == 1: + self.validate_internal_transfer_qty() else: self.validate_internal_transfer_warehouse() @@ -685,9 +673,7 @@ class StockController(AccountsController): row.from_warehouse = None def validate_in_transit_warehouses(self): - if ( - self.doctype == "Sales Invoice" and self.get("update_stock") - ) or self.doctype == "Delivery Note": + if (self.doctype == "Sales Invoice" and self.get("update_stock")) or self.doctype == "Delivery Note": for item in self.get("items"): if not item.target_warehouse: frappe.throw( @@ -711,6 +697,116 @@ class StockController(AccountsController): if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"): frappe.throw(_("Packed Items cannot be transferred internally")) + def validate_internal_transfer_qty(self): + if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]: + return + + item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty() + if not item_wise_transfer_qty: + return + + item_wise_received_qty = self.get_item_wise_inter_received_qty() + precision = frappe.get_precision(self.doctype + " Item", "qty") + + over_receipt_allowance = frappe.db.get_single_value( + "Stock Settings", "over_delivery_receipt_allowance" + ) + + parent_doctype = { + "Purchase Receipt": "Delivery Note", + "Purchase Invoice": "Sales Invoice", + }.get(self.doctype) + + for key, transferred_qty in item_wise_transfer_qty.items(): + recevied_qty = flt(item_wise_received_qty.get(key), precision) + if over_receipt_allowance: + transferred_qty = transferred_qty + flt( + transferred_qty * over_receipt_allowance / 100, precision + ) + + if recevied_qty > flt(transferred_qty, precision): + frappe.throw( + _("For Item {0} cannot be received more than {1} qty against the {2} {3}").format( + bold(key[1]), + bold(flt(transferred_qty, precision)), + bold(parent_doctype), + get_link_to_form(parent_doctype, self.get("inter_company_reference")), + ) + ) + + def get_item_wise_inter_transfer_qty(self): + reference_field = "inter_company_reference" + if self.doctype == "Purchase Invoice": + reference_field = "inter_company_invoice_reference" + + parent_doctype = { + "Purchase Receipt": "Delivery Note", + "Purchase Invoice": "Sales Invoice", + }.get(self.doctype) + + child_doctype = parent_doctype + " Item" + + parent_tab = frappe.qb.DocType(parent_doctype) + child_tab = frappe.qb.DocType(child_doctype) + + query = ( + frappe.qb.from_(parent_doctype) + .inner_join(child_tab) + .on(child_tab.parent == parent_tab.name) + .select( + child_tab.name, + child_tab.item_code, + child_tab.qty, + ) + .where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1)) + ) + + data = query.run(as_dict=True) + item_wise_transfer_qty = defaultdict(float) + for row in data: + item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty) + + return item_wise_transfer_qty + + def get_item_wise_inter_received_qty(self): + child_doctype = self.doctype + " Item" + + parent_tab = frappe.qb.DocType(self.doctype) + child_tab = frappe.qb.DocType(child_doctype) + + query = ( + frappe.qb.from_(self.doctype) + .inner_join(child_tab) + .on(child_tab.parent == parent_tab.name) + .select( + child_tab.item_code, + child_tab.qty, + ) + .where(parent_tab.docstatus < 2) + ) + + if self.doctype == "Purchase Invoice": + query = query.select( + child_tab.sales_invoice_item.as_("name"), + ) + + query = query.where( + parent_tab.inter_company_invoice_reference == self.inter_company_invoice_reference + ) + else: + query = query.select( + child_tab.delivery_note_item.as_("name"), + ) + + query = query.where(parent_tab.inter_company_reference == self.inter_company_reference) + + data = query.run(as_dict=True) + item_wise_transfer_qty = defaultdict(float) + for row in data: + item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty) + + return item_wise_transfer_qty + def validate_putaway_capacity(self): # if over receipt is attempted while 'apply putaway rule' is disabled # and if rule was applied on the transaction, validate it. @@ -723,6 +819,9 @@ class StockController(AccountsController): "Stock Reconciliation", ) + if not frappe.get_all("Putaway Rule", limit=1): + return + if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: valid_doctype = False @@ -743,7 +842,9 @@ class StockController(AccountsController): if self.doctype == "Stock Reconciliation": stock_qty = flt(item.qty) else: - stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) + stock_qty = ( + flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) + ) rule_name = rule.get("name") if not rule_map[rule_name]: @@ -759,9 +860,7 @@ class StockController(AccountsController): frappe.throw(msg=message, title=_("Over Receipt")) def prepare_over_receipt_message(self, rule, values): - message = _( - "{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}." - ).format( + message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.").format( frappe.bold(values["qty_put"]), frappe.bold(values["item"]), frappe.bold(values["warehouse"]), @@ -812,7 +911,6 @@ class StockController(AccountsController): item=None, posting_date=None, ): - gl_entry = { "account": account, "cost_center": cost_center, @@ -939,9 +1037,7 @@ def future_sle_exists(args, sl_entries=None): and is_cancelled = 0 GROUP BY item_code, warehouse - """.format( - " or ".join(or_conditions) - ), + """.format(" or ".join(or_conditions)), args, as_dict=1, ) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 34d3e700ccc..6c9f066ff55 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -17,7 +17,7 @@ from erpnext.stock.utils import get_incoming_rate class SubcontractingController(StockController): def __init__(self, *args, **kwargs): - super(SubcontractingController, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if self.get("is_old_subcontracting_flow"): self.subcontract_data = frappe._dict( { @@ -49,7 +49,7 @@ class SubcontractingController(StockController): self.validate_items() self.create_raw_materials_supplied() else: - super(SubcontractingController, self).validate() + super().validate() def remove_empty_rows(self): for key in ["service_items", "items", "supplied_items"]: @@ -85,11 +85,15 @@ class SubcontractingController(StockController): bom = frappe.get_doc("BOM", item.bom) if not bom.is_active: frappe.throw( - _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name) + _("Row {0}: Please select an active BOM for Item {1}.").format( + item.idx, item.item_name + ) ) if bom.item != item.item_code: frappe.throw( - _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name) + _("Row {0}: Please select an valid BOM for Item {1}.").format( + item.idx, item.item_name + ) ) else: frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name)) @@ -165,7 +169,6 @@ class SubcontractingController(StockController): fields=["item_code", "(qty - received_qty) as qty", "parent", "name"], filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)}, ): - self.qty_to_be_received[(row.item_code, row.parent)] += row.qty def __get_transferred_items(self): @@ -253,9 +256,7 @@ class SubcontractingController(StockController): if not receipt_items: return ([], {}) if return_consumed_items else None - receipt_items = { - item.name: item.get(self.subcontract_data.order_field) for item in receipt_items - } + receipt_items = {item.name: item.get(self.subcontract_data.order_field) for item in receipt_items} consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys()) if return_consumed_items: @@ -343,7 +344,7 @@ class SubcontractingController(StockController): i += 1 def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): - doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" + doctype = "BOM Explosion Item" if exploded_item else "BOM Item" fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] alias_dict = { @@ -370,9 +371,7 @@ class SubcontractingController(StockController): [doctype, "sourced_by_supplier", "=", 0], ] - return ( - frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or [] - ) + return frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or [] def __update_reserve_warehouse(self, row, item): if self.doctype == self.subcontract_data.order_doctype: @@ -433,8 +432,11 @@ class SubcontractingController(StockController): self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) self.available_materials[key]["batch_no"][batch_no] = 0 - if abs(qty) > 0 and not new_rm_obj: + if new_rm_obj: + self.remove(rm_obj) + elif abs(qty) > 0: self.__set_consumed_qty(rm_obj, qty) + else: self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) self.__set_serial_nos(item_row, rm_obj) @@ -444,6 +446,16 @@ class SubcontractingController(StockController): rm_obj = self.append(self.raw_material_table, bom_item) rm_obj.reference_name = item_row.name + if self.doctype == self.subcontract_data.order_doctype: + rm_obj.required_qty = qty + rm_obj.amount = rm_obj.required_qty * rm_obj.rate + else: + rm_obj.consumed_qty = 0 + setattr( + rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field) + ) + self.__set_batch_nos(bom_item, item_row, rm_obj, qty) + if self.doctype == "Subcontracting Receipt": args = frappe._dict( { @@ -462,16 +474,6 @@ class SubcontractingController(StockController): ) rm_obj.rate = get_incoming_rate(args) - if self.doctype == self.subcontract_data.order_doctype: - rm_obj.required_qty = qty - rm_obj.amount = rm_obj.required_qty * rm_obj.rate - else: - rm_obj.consumed_qty = 0 - setattr( - rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field) - ) - self.__set_batch_nos(bom_item, item_row, rm_obj, qty) - def __get_qty_based_on_material_transfer(self, item_row, transfer_item): key = (item_row.item_code, item_row.get(self.subcontract_data.order_field)) @@ -525,6 +527,10 @@ class SubcontractingController(StockController): (row.item_code, row.get(self.subcontract_data.order_field)) ] -= row.qty + def __reset_idx(self): + for idx, item in enumerate(self.get(self.raw_material_table)): + item.idx = idx + 1 + def __prepare_supplied_items(self): self.initialized_fields() self.__get_subcontract_orders() @@ -532,6 +538,7 @@ class SubcontractingController(StockController): self.get_available_materials() self.__remove_changed_rows() self.__set_supplied_items() + self.__reset_idx() def __validate_batch_no(self, row, key): if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get( @@ -801,7 +808,8 @@ class SubcontractingController(StockController): if mr_obj.status in ["Stopped", "Cancelled"]: frappe.throw( - _("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError + _("Material Request {0} is cancelled or stopped").format(mr), + frappe.InvalidStatusError, ) mr_obj.update_requested_qty(mr_item_rows) @@ -884,8 +892,10 @@ def make_rm_stock_entry( for fg_item_code in fg_item_code_list: for rm_item in rm_items: - - if rm_item.get("main_item_code") == fg_item_code or rm_item.get("item_code") == fg_item_code: + if ( + rm_item.get("main_item_code") == fg_item_code + or rm_item.get("item_code") == fg_item_code + ): rm_item_code = rm_item.get("rm_item_code") items_dict = { @@ -896,13 +906,16 @@ def make_rm_stock_entry( "description": item_wh.get(rm_item_code, {}).get("description", ""), "qty": rm_item.get("qty") or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0), - "from_warehouse": rm_item.get("warehouse") or rm_item.get("reserve_warehouse"), + "from_warehouse": rm_item.get("warehouse") + or rm_item.get("reserve_warehouse"), "to_warehouse": subcontract_order.supplier_warehouse, "stock_uom": rm_item.get("stock_uom"), "serial_no": rm_item.get("serial_no"), "batch_no": rm_item.get("batch_no"), "main_item_code": fg_item_code, - "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), + "allow_alternative_item": item_wh.get(rm_item_code, {}).get( + "allow_alternative_item" + ), } } @@ -916,9 +929,7 @@ def make_rm_stock_entry( frappe.throw(_("No Items selected for transfer.")) -def add_items_in_ste( - ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_detail", batch_no=None -): +def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_detail", batch_no=None): item = ste_doc.append("items", row.item_details) rm_detail = list(set(row.get(f"{rm_detail_field}s")).intersection(rm_details)) @@ -963,7 +974,7 @@ def make_return_stock_entry_for_subcontract( ste_doc.company = order_doc.company ste_doc.is_return = 1 - for key, value in available_materials.items(): + for _key, value in available_materials.items(): if not value.qty: continue @@ -981,9 +992,7 @@ def make_return_stock_entry_for_subcontract( @frappe.whitelist() -def get_materials_from_supplier( - subcontract_order, rm_details, order_doctype="Subcontracting Order" -): +def get_materials_from_supplier(subcontract_order, rm_details, order_doctype="Subcontracting Order"): if isinstance(rm_details, str): rm_details = json.loads(rm_details) @@ -997,6 +1006,4 @@ def get_materials_from_supplier( _("Materials are already received against the {0} {1}").format(order_doctype, subcontract_order) ) - return make_return_stock_entry_for_subcontract( - doc.available_materials, doc, rm_details, order_doctype - ) + return make_return_stock_entry_for_subcontract(doc.available_materials, doc, rm_details, order_doctype) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index e24618af103..e92356e30a8 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -21,7 +21,7 @@ from erpnext.stock.get_item_details import _get_item_tax_template from erpnext.utilities.regional import temporary_flag -class calculate_taxes_and_totals(object): +class calculate_taxes_and_totals: def __init__(self, doc: Document): self.doc = doc frappe.flags.round_off_applicable_accounts = [] @@ -155,7 +155,8 @@ class calculate_taxes_and_totals(object): elif item.price_list_rate: if not item.rate or (item.pricing_rules and item.discount_percentage > 0): item.rate = flt( - item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate") + item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)), + item.precision("rate"), ) item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) @@ -176,7 +177,8 @@ class calculate_taxes_and_totals(object): item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) if flt(item.rate_with_margin) > 0: item.rate = flt( - item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate") + item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), + item.precision("rate"), ) if item.discount_amount and not item.discount_percentage: @@ -192,7 +194,9 @@ class calculate_taxes_and_totals(object): item.net_rate = item.rate if ( - not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt" + not item.qty + and self.doc.get("is_return") + and self.doc.get("doctype") != "Purchase Receipt" ): item.amount = flt(-1 * item.rate, item.precision("amount")) elif not item.qty and self.doc.get("is_debit_note"): @@ -278,7 +282,9 @@ class calculate_taxes_and_totals(object): item.net_amount = flt(amount / (1 + cumulated_tax_fraction)) item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate")) - item.discount_percentage = flt(item.discount_percentage, item.precision("discount_percentage")) + item.discount_percentage = flt( + item.discount_percentage, item.precision("discount_percentage") + ) self._set_in_company_currency(item, ["net_rate", "net_amount"]) @@ -339,7 +345,6 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"]) def calculate_shipping_charges(self): - # Do not apply shipping rule for POS if self.doc.get("is_pos"): return @@ -351,9 +356,7 @@ class calculate_taxes_and_totals(object): self._calculate() def calculate_taxes(self): - rounding_adjustment_computed = self.doc.get("is_consolidated") and self.doc.get( - "rounding_adjustment" - ) + rounding_adjustment_computed = self.doc.get("is_consolidated") and self.doc.get("rounding_adjustment") if not rounding_adjustment_computed: self.doc.rounding_adjustment = 0 @@ -391,7 +394,9 @@ class calculate_taxes_and_totals(object): # set tax after discount tax.tax_amount_after_discount_amount += current_tax_amount - current_tax_amount = self.get_tax_amount_if_for_valuation_or_deduction(current_tax_amount, tax) + current_tax_amount = self.get_tax_amount_if_for_valuation_or_deduction( + current_tax_amount, tax + ) # note: grand_total_for_current_item contains the contribution of # item's amount, previously applied tax and the current tax on that item @@ -455,9 +460,7 @@ class calculate_taxes_and_totals(object): if tax.charge_type == "Actual": # distribute the tax amount proportionally to each item row actual = flt(tax.tax_amount, tax.precision("tax_amount")) - current_tax_amount = ( - item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0 - ) + current_tax_amount = item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0 elif tax.charge_type == "On Net Total": current_tax_amount = (tax_rate / 100.0) * item.net_amount @@ -571,9 +574,7 @@ class calculate_taxes_and_totals(object): else self.doc.base_net_total ) - self._set_in_company_currency( - self.doc, ["taxes_and_charges_added", "taxes_and_charges_deducted"] - ) + self._set_in_company_currency(self.doc, ["taxes_and_charges_added", "taxes_and_charges_deducted"]) self.doc.round_floats_in(self.doc, ["grand_total", "base_grand_total"]) @@ -627,12 +628,11 @@ class calculate_taxes_and_totals(object): frappe.throw(_("Please select Apply Discount On")) self.doc.base_discount_amount = flt( - self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") + self.doc.discount_amount * self.doc.conversion_rate, + self.doc.precision("base_discount_amount"), ) - if self.doc.apply_discount_on == "Grand Total" and self.doc.get( - "is_cash_or_non_trade_discount" - ): + if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): self.discount_amount_applied = True return @@ -657,12 +657,17 @@ class calculate_taxes_and_totals(object): or total_for_discount_amount == self.doc.net_total ) and i == len(self._items) - 1: discount_amount_loss = flt( - self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total") + self.doc.net_total - net_total - self.doc.discount_amount, + self.doc.precision("net_total"), ) - item.net_amount = flt(item.net_amount + discount_amount_loss, item.precision("net_amount")) + item.net_amount = flt( + item.net_amount + discount_amount_loss, item.precision("net_amount") + ) - item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0 + item.net_rate = ( + flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0 + ) self._set_in_company_currency(item, ["net_rate", "net_amount"]) @@ -818,7 +823,6 @@ class calculate_taxes_and_totals(object): self.calculate_paid_amount() def calculate_paid_amount(self): - paid_amount = base_paid_amount = 0.0 if self.doc.is_pos: @@ -918,7 +922,9 @@ class calculate_taxes_and_totals(object): total_paid_amount = 0 for payment in self.doc.get("payments"): total_paid_amount += ( - payment.amount if self.doc.party_account_currency == self.doc.currency else payment.base_amount + payment.amount + if self.doc.party_account_currency == self.doc.currency + else payment.base_amount ) pending_amount = total_amount_to_pay - total_paid_amount @@ -992,7 +998,7 @@ def update_itemised_tax_data(doc): @erpnext.allow_regional def get_itemised_tax_breakup_header(item_doctype, tax_accounts): - return [_("Item"), _("Taxable Amount")] + tax_accounts + return [_("Item"), _("Taxable Amount"), *tax_accounts] @erpnext.allow_regional @@ -1005,7 +1011,7 @@ def get_itemised_tax_breakup_data(doc): for item_code, taxes in itemised_tax.items(): itemised_tax_data.append( frappe._dict( - {"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code), **taxes} + {"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code, 0), **taxes} ) ) @@ -1060,7 +1066,7 @@ def get_rounded_tax_amount(itemised_tax, precision): row["tax_amount"] = flt(row["tax_amount"], precision) -class init_landed_taxes_and_totals(object): +class init_landed_taxes_and_totals: def __init__(self, doc): self.doc = doc self.tax_field = "taxes" if self.doc.doctype == "Landed Cost Voucher" else "additional_costs" diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 97d3c5c32de..4ada8e60d9b 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1,18 +1,15 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -import unittest import frappe from frappe import qb from frappe.query_builder.functions import Sum -from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, getdate, nowdate +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, getdate, nowdate -from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry -from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account from erpnext.stock.doctype.item.test_item import create_item @@ -56,6 +53,9 @@ class TestAccountsController(FrappeTestCase): 20 series - Sales Invoice against Journals 30 series - Sales Invoice against Credit Notes 40 series - Company default Cost center is unset + 50 series - Journals against Journals + 60 series - Journals against Payment Entries + 90 series - Dimension inheritence """ def setUp(self): @@ -135,18 +135,42 @@ class TestAccountsController(FrappeTestCase): acc = frappe.get_doc("Account", name) self.debtors_usd = acc.name + account_name = "Creditors USD" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Payable - " + self.company_abbr + acc.company = self.company + acc.account_currency = "USD" + acc.account_type = "Payable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.creditors_usd = acc.name + def create_sales_invoice( self, qty=1, rate=1, conversion_rate=80, - posting_date=nowdate(), + posting_date=None, do_not_save=False, do_not_submit=False, ): """ Helper function to populate default values in sales invoice """ + if posting_date is None: + posting_date = nowdate() + sinv = create_sales_invoice( qty=qty, rate=rate, @@ -172,11 +196,14 @@ class TestAccountsController(FrappeTestCase): return sinv def create_payment_entry( - self, amount=1, source_exc_rate=75, posting_date=nowdate(), customer=None + self, amount=1, source_exc_rate=75, posting_date=None, customer=None, submit=True ): """ Helper function to populate default values in payment entry """ + if posting_date is None: + posting_date = nowdate() + payment = create_payment_entry( company=self.company, payment_type="Receive", @@ -291,9 +318,7 @@ class TestAccountsController(FrappeTestCase): .run(as_dict=True)[0] ) self.assertEqual(outstanding, current_outstanding.outstanding) - self.assertEqual( - outstanding_in_account_currency, current_outstanding.outstanding_in_account_currency - ) + self.assertEqual(outstanding_in_account_currency, current_outstanding.outstanding_in_account_currency) def test_10_payment_against_sales_invoice(self): # Sales Invoice in Foreign Currency @@ -399,7 +424,6 @@ class TestAccountsController(FrappeTestCase): adv.reload() # sales invoice with advance(partial amount) - rate = 80 rate_in_account_currency = 1 si = self.create_sales_invoice( qty=2, conversion_rate=80, rate=rate_in_account_currency, do_not_submit=True @@ -786,7 +810,9 @@ class TestAccountsController(FrappeTestCase): self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) # Exchange Gain/Loss Journal should've been created. - exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name] + exc_je_for_si = [ + x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name + ] exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) self.assertNotEqual(exc_je_for_si, []) self.assertEqual(len(exc_je_for_si), 1) @@ -1107,18 +1133,18 @@ class TestAccountsController(FrappeTestCase): cr_note.reload() cr_note.cancel() - # Exchange Gain/Loss Journal should've been created. + # with the introduction of 'cancel_system_generated_credit_debit_notes' in accounts controller + # JE(Credit Note) will be cancelled once the parent is cancelled exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name) - self.assertNotEqual(exc_je_for_si, []) - self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 0) self.assertEqual(len(exc_je_for_cr), 0) - # The Credit Note JE is still active and is referencing the sales invoice - # So, outstanding stays the same + # No references, full outstanding si.reload() - self.assertEqual(si.outstanding_amount, 1) - self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) def test_40_cost_center_from_payment_entry(self): """ @@ -1251,7 +1277,463 @@ class TestAccountsController(FrappeTestCase): with self.subTest(x=x): self.assertEqual( [self.cost_center, self.cost_center], - frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="cost_center"), + frappe.db.get_all( + "Journal Entry Account", filters={"parent": x.parent}, pluck="cost_center" + ), ) frappe.db.set_value("Company", self.company, "cost_center", cc) + + def setup_dimensions(self): + if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): + frappe.get_doc( + { + "doctype": "Accounting Dimension", + "document_type": "Department", + } + ).insert() + else: + dimension = frappe.get_doc("Accounting Dimension", "Department") + dimension.disabled = 0 + dimension.save() + + if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): + dimension1 = frappe.get_doc( + { + "doctype": "Accounting Dimension", + "document_type": "Location", + } + ) + dimension1.append( + "dimension_defaults", + { + "company": "_Test Company", + "reference_document": "Location", + "default_dimension": "Block 1", + "mandatory_for_bs": 0, + "mandatory_for_pl": 0, + }, + ) + + dimension1.insert() + dimension1.save() + else: + dimension1 = frappe.get_doc("Accounting Dimension", "Location") + dimension1.disabled = 0 + dimension1.save() + + def disable_dimensions(self): + if frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): + dimension = frappe.get_doc("Accounting Dimension", "Department") + dimension.disabled = 1 + dimension.save() + + if frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): + dimension1 = frappe.get_doc("Accounting Dimension", "Location") + dimension1.disabled = 1 + dimension1.save() + + def test_90_dimensions_filter(self): + """ + Test workings of dimension filters + """ + self.setup_dimensions() + rate_in_account_currency = 1 + + # Invoices + si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si1.department = "Management" + si1.save().submit() + + si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si2.department = "Operations" + si2.save().submit() + + # Payments + cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) + cr_note1.department = "Management" + cr_note1.is_return = 1 + cr_note1.save().submit() + + cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) + cr_note2.department = "Legal" + cr_note2.is_return = 1 + cr_note2.save().submit() + + pe1 = get_payment_entry(si1.doctype, si1.name) + pe1.references = [] + pe1.department = "Research & Development" + pe1.save().submit() + + pe2 = get_payment_entry(si1.doctype, si1.name) + pe2.references = [] + pe2.department = "Management" + pe2.save().submit() + + je1 = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=-75, + acc2_exc_rate=1, + ) + je1.accounts[0].party_type = "Customer" + je1.accounts[0].party = self.customer + je1.accounts[0].department = "Management" + je1.save().submit() + + # assert dimension filter's result + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 2) + self.assertEqual(len(pr.payments), 5) + + pr.department = "Legal" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 1) + + pr.department = "Management" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 3) + + pr.department = "Research & Development" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 1) + self.disable_dimensions() + + def test_91_cr_note_should_inherit_dimension(self): + self.setup_dimensions() + rate_in_account_currency = 1 + + # Invoice + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si.department = "Management" + si.save().submit() + + # Payment + cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) + cr_note.department = "Management" + cr_note.is_return = 1 + cr_note.save().submit() + + pr = self.create_payment_reconciliation() + pr.department = "Management" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be 2 journals, JE(Cr Note) and JE(Exchange Gain/Loss) + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_cr_note = self.get_journals_for(cr_note.doctype, cr_note.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_cr_note), 2) + self.assertEqual(exc_je_for_si, exc_je_for_cr_note) + + for x in exc_je_for_si + exc_je_for_cr_note: + with self.subTest(x=x): + self.assertEqual( + [cr_note.department, cr_note.department], + frappe.db.get_all( + "Journal Entry Account", filters={"parent": x.parent}, pluck="department" + ), + ) + self.disable_dimensions() + + def test_92_dimension_inhertiance_exc_gain_loss(self): + # Sales Invoice in Foreign Currency + self.setup_dimensions() + rate_in_account_currency = 1 + dpt = "Research & Development" + + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True) + si.department = dpt + si.save().submit() + + pe = self.create_payment_entry(amount=1, source_exc_rate=82).save() + pe.department = dpt + pe = pe.save().submit() + + pr = self.create_payment_reconciliation() + pr.department = dpt + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # Exc Gain/Loss journals should inherit dimension from parent + journals = self.get_journals_for(si.doctype, si.name) + self.assertEqual( + [dpt, dpt], + frappe.db.get_all( + "Journal Entry Account", + filters={"parent": ("in", [x.parent for x in journals])}, + pluck="department", + ), + ) + self.disable_dimensions() + + def test_93_dimension_inheritance_on_advance(self): + self.setup_dimensions() + dpt = "Research & Development" + + adv = self.create_payment_entry(amount=1, source_exc_rate=85) + adv.department = dpt + adv.save().submit() + adv.reload() + + # Sales Invoices in different exchange rates + si = self.create_sales_invoice(qty=1, conversion_rate=82, rate=1, do_not_submit=True) + si.department = dpt + advances = si.get_advance_entries() + self.assertEqual(len(advances), 1) + self.assertEqual(advances[0].reference_name, adv.name) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "reference_row": advances[0].reference_row, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": advances[0].exchange_rate, + "remarks": advances[0].remarks, + }, + ) + si = si.save().submit() + + # Outstanding in both currencies should be '0' + adv.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exc Gain/Loss journals should inherit dimension from parent + journals = self.get_journals_for(si.doctype, si.name) + self.assertEqual( + [dpt, dpt], + frappe.db.get_all( + "Journal Entry Account", + filters={"parent": ("in", [x.parent for x in journals])}, + pluck="department", + ), + ) + self.disable_dimensions() + + def test_50_journal_against_journal(self): + # Invoice in Foreign Currency + journal_as_invoice = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=83, + acc2=self.cash, + acc1_amount=1, + acc2_amount=83, + acc2_exc_rate=1, + ) + journal_as_invoice.accounts[0].party_type = "Customer" + journal_as_invoice.accounts[0].party = self.customer + journal_as_invoice = journal_as_invoice.save().submit() + + # Payment + journal_as_payment = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=-75, + acc2_exc_rate=1, + ) + journal_as_payment.accounts[0].party_type = "Customer" + journal_as_payment.accounts[0].party = self.customer + journal_as_payment = journal_as_payment.save().submit() + + # Reconcile the remaining amount + pr = self.create_payment_reconciliation() + # pr.receivable_payable_account = self.debit_usd + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be no outstanding in both currencies + journal_as_invoice.reload() + self.assert_ledger_outstanding(journal_as_invoice.doctype, journal_as_invoice.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(journal_as_invoice.doctype, journal_as_invoice.name) + exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual( + len(exc_je_for_si), 2 + ) # payment also has reference. so, there are 2 journals referencing invoice + self.assertEqual(len(exc_je_for_je), 1) + self.assertIn(exc_je_for_je[0], exc_je_for_si) + + # Cancel Payment + journal_as_payment.reload() + journal_as_payment.cancel() + + journal_as_invoice.reload() + self.assert_ledger_outstanding(journal_as_invoice.doctype, journal_as_invoice.name, 83.0, 1.0) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(journal_as_invoice.doctype, journal_as_invoice.name) + exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_je, []) + + def test_60_payment_entry_against_journal(self): + # Invoices + exc_rate1 = 75 + exc_rate2 = 77 + amount = 1 + je1 = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=exc_rate1, + acc2=self.cash, + acc1_amount=amount, + acc2_amount=(amount * 75), + acc2_exc_rate=1, + ) + je1.accounts[0].party_type = "Customer" + je1.accounts[0].party = self.customer + je1 = je1.save().submit() + + je2 = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=exc_rate2, + acc2=self.cash, + acc1_amount=amount, + acc2_amount=(amount * exc_rate2), + acc2_exc_rate=1, + ) + je2.accounts[0].party_type = "Customer" + je2.accounts[0].party = self.customer + je2 = je2.save().submit() + + # Payment + pe = self.create_payment_entry(amount=2, source_exc_rate=exc_rate1).save().submit() + + pr = self.create_payment_reconciliation() + pr.receivable_payable_account = self.debit_usd + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 2) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be no outstanding in both currencies + self.assert_ledger_outstanding(je1.doctype, je1.name, 0.0, 0.0) + self.assert_ledger_outstanding(je2.doctype, je2.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created only for JE2 + exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) + exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name) + self.assertEqual(exc_je_for_je1, []) + self.assertEqual(len(exc_je_for_je2), 1) + + # Cancel Payment + pe.reload() + pe.cancel() + + self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount) + self.assert_ledger_outstanding(je2.doctype, je2.name, (amount * exc_rate2), amount) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) + exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name) + self.assertEqual(exc_je_for_je1, []) + self.assertEqual(exc_je_for_je2, []) + + def test_61_payment_entry_against_journal_for_payable_accounts(self): + # Invoices + exc_rate1 = 75 + exc_rate2 = 77 + amount = 1 + je1 = self.create_journal_entry( + acc1=self.creditors_usd, + acc1_exc_rate=exc_rate1, + acc2=self.cash, + acc1_amount=-amount, + acc2_amount=(-amount * 75), + acc2_exc_rate=1, + ) + je1.accounts[0].party_type = "Supplier" + je1.accounts[0].party = self.supplier + je1 = je1.save().submit() + + # Payment + pe = create_payment_entry( + company=self.company, + payment_type="Pay", + party_type="Supplier", + party=self.supplier, + paid_from=self.cash, + paid_to=self.creditors_usd, + paid_amount=amount, + ) + pe.target_exchange_rate = exc_rate2 + pe.received_amount = amount + pe.paid_amount = amount * exc_rate2 + pe.save().submit() + + pr = frappe.get_doc( + { + "doctype": "Payment Reconciliation", + "company": self.company, + "party_type": "Supplier", + "party": self.supplier, + "receivable_payable_account": get_party_account("Supplier", self.supplier, self.company), + } + ) + pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be no outstanding in both currencies + self.assert_ledger_outstanding(je1.doctype, je1.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created + exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) + self.assertEqual(len(exc_je_for_je1), 1) + + # Cancel Payment + pe.reload() + pe.cancel() + + self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) + self.assertEqual(exc_je_for_je1, []) diff --git a/erpnext/controllers/tests/test_mapper.py b/erpnext/controllers/tests/test_mapper.py index 919bcdab660..9dbcf6cafd2 100644 --- a/erpnext/controllers/tests/test_mapper.py +++ b/erpnext/controllers/tests/test_mapper.py @@ -29,7 +29,6 @@ class TestMapper(unittest.TestCase): self.assertEqual(set(d for d in src_items), set(d.item_code for d in updated_so.items)) def make_quotation(self, item_list, customer): - qtn = frappe.get_doc( { "doctype": "Quotation", diff --git a/erpnext/controllers/tests/test_qty_based_taxes.py b/erpnext/controllers/tests/test_qty_based_taxes.py index 2e9dfd2faa0..e7896b57f23 100644 --- a/erpnext/controllers/tests/test_qty_based_taxes.py +++ b/erpnext/controllers/tests/test_qty_based_taxes.py @@ -25,7 +25,7 @@ class TestTaxes(unittest.TestCase): "account_name": uuid4(), "account_type": "Tax", "company": self.company.name, - "parent_account": "Duties and Taxes - {self.company.abbr}".format(self=self), + "parent_account": f"Duties and Taxes - {self.company.abbr}", } ).insert() self.item_group = frappe.get_doc( diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 60d1733021c..904f75f64de 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -7,13 +7,10 @@ from erpnext.controllers import queries def add_default_params(func, doctype): - return partial( - func, doctype=doctype, txt="", searchfield="name", start=0, page_len=20, filters=None - ) + return partial(func, doctype=doctype, txt="", searchfield="name", start=0, page_len=20, filters=None) class TestQueries(unittest.TestCase): - # All tests are based on doctype/test_records.json def assert_nested_in(self, item, container): @@ -68,7 +65,7 @@ class TestQueries(unittest.TestCase): self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1) def test_project_query(self): - query = add_default_params(queries.get_project_name, "BOM") + query = add_default_params(queries.get_project_name, "Project") self.assertGreaterEqual(len(query(txt="_Test Project")), 1) diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 68af142afcb..4e5df6e9195 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -423,7 +423,7 @@ class TestSubcontractingController(FrappeTestCase): scr1.set_missing_values() scr1.submit() - for key, value in get_supplied_items(scr1).items(): + for _key, value in get_supplied_items(scr1).items(): self.assertEqual(value.qty, 4) scr2 = make_subcontracting_receipt(sco.name) @@ -434,7 +434,7 @@ class TestSubcontractingController(FrappeTestCase): scr2.set_missing_values() scr2.submit() - for key, value in get_supplied_items(scr2).items(): + for _key, value in get_supplied_items(scr2).items(): self.assertEqual(value.qty, 4) scr3 = make_subcontracting_receipt(sco.name) @@ -444,7 +444,7 @@ class TestSubcontractingController(FrappeTestCase): scr3.set_missing_values() scr3.submit() - for key, value in get_supplied_items(scr3).items(): + for _key, value in get_supplied_items(scr3).items(): self.assertEqual(value.qty, 2) def test_item_with_batch_based_on_material_transfer(self): @@ -551,7 +551,7 @@ class TestSubcontractingController(FrappeTestCase): scr2.set_missing_values() scr2.submit() - for key, value in get_supplied_items(scr2).items(): + for value in get_supplied_items(scr2).values(): self.assertEqual(value.qty, 4) scr3 = make_subcontracting_receipt(sco.name) @@ -561,7 +561,7 @@ class TestSubcontractingController(FrappeTestCase): scr3.set_missing_values() scr3.submit() - for key, value in get_supplied_items(scr3).items(): + for value in get_supplied_items(scr3).values(): self.assertEqual(value.qty, 1) def test_partial_transfer_serial_no_components_based_on_material_transfer(self): @@ -729,7 +729,7 @@ class TestSubcontractingController(FrappeTestCase): scr1.load_from_db() scr1.supplied_items[0].consumed_qty = 5 - scr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0] + scr1.supplied_items[0].batch_no = next(iter(transferred_batch_no.keys())) scr1.save() scr1.submit() @@ -872,9 +872,7 @@ def make_stock_in_entry(**args): def update_item_details(child_row, details): details.qty += ( - child_row.get("qty") - if child_row.doctype == "Stock Entry Detail" - else child_row.get("consumed_qty") + child_row.get("qty") if child_row.doctype == "Stock Entry Detail" else child_row.get("consumed_qty") ) if child_row.serial_no: @@ -967,7 +965,9 @@ def make_raw_materials(): make_item(item, properties) -def make_service_item(item, properties={}): +def make_service_item(item, properties=None): + if properties is None: + properties = {} if not frappe.db.exists("Item", item): properties.update({"is_stock_item": 0}) make_item(item, properties) @@ -1015,9 +1015,7 @@ def make_bom_for_subcontracted_items(): def set_backflush_based_on(based_on): - frappe.db.set_value( - "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", based_on - ) + frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", based_on) def get_subcontracting_order(**args): diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py index 1471543f1b2..ab5525a7f06 100644 --- a/erpnext/controllers/tests/test_transaction_base.py +++ b/erpnext/controllers/tests/test_transaction_base.py @@ -44,7 +44,10 @@ class TestUtils(unittest.TestCase): item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC" ), frappe._dict( - item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1 + item_code="_Test FG Item", + qty=4, + t_warehouse="_Test Warehouse 1 - _TC", + is_finished_item=1, ), ], ) diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 1fb722e1123..18fe7767c5d 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -100,12 +100,11 @@ def get_data(filters, conditions): else: inc = 1 data1 = frappe.db.sql( - """ select %s from `tab%s` t1, `tab%s Item` t2 %s - where t2.parent = t1.name and t1.company = %s and %s between %s and %s and - t1.docstatus = 1 %s %s - group by %s - """ - % ( + """ select {} from `tab{}` t1, `tab{} Item` t2 {} + where t2.parent = t1.name and t1.company = {} and {} between {} and {} and + t1.docstatus = 1 {} {} + group by {} + """.format( query_details, conditions["trans"], conditions["trans"], @@ -130,11 +129,10 @@ def get_data(filters, conditions): # to get distinct value of col specified by group_by in filter row = frappe.db.sql( - """select DISTINCT(%s) from `tab%s` t1, `tab%s Item` t2 %s - where t2.parent = t1.name and t1.company = %s and %s between %s and %s - and t1.docstatus = 1 and %s = %s %s %s - """ - % ( + """select DISTINCT({}) from `tab{}` t1, `tab{} Item` t2 {} + where t2.parent = t1.name and t1.company = {} and {} between {} and {} + and t1.docstatus = 1 and {} = {} {} {} + """.format( sel_col, conditions["trans"], conditions["trans"], @@ -157,11 +155,10 @@ def get_data(filters, conditions): # get data for group_by filter row1 = frappe.db.sql( - """ select %s , %s from `tab%s` t1, `tab%s Item` t2 %s - where t2.parent = t1.name and t1.company = %s and %s between %s and %s - and t1.docstatus = 1 and %s = %s and %s = %s %s %s - """ - % ( + """ select {} , {} from `tab{}` t1, `tab{} Item` t2 {} + where t2.parent = t1.name and t1.company = {} and {} between {} and {} + and t1.docstatus = 1 and {} = {} and {} = {} {} {} + """.format( sel_col, conditions["period_wise_select"], conditions["trans"], @@ -190,12 +187,11 @@ def get_data(filters, conditions): data.append(des) else: data = frappe.db.sql( - """ select %s from `tab%s` t1, `tab%s Item` t2 %s - where t2.parent = t1.name and t1.company = %s and %s between %s and %s and - t1.docstatus = 1 %s %s - group by %s - """ - % ( + """ select {} from `tab{}` t1, `tab{} Item` t2 {} + where t2.parent = t1.name and t1.company = {} and {} between {} and {} and + t1.docstatus = 1 {} {} + group by {} + """.format( query_details, conditions["trans"], conditions["trans"], @@ -260,13 +256,13 @@ def get_period_wise_columns(bet_dates, period, pwc): def get_period_wise_query(bet_dates, trans_date, query_details): - query_details += """SUM(IF(t1.%(trans_date)s BETWEEN '%(sd)s' AND '%(ed)s', t2.stock_qty, NULL)), - SUM(IF(t1.%(trans_date)s BETWEEN '%(sd)s' AND '%(ed)s', t2.base_net_amount, NULL)), - """ % { - "trans_date": trans_date, - "sd": bet_dates[0], - "ed": bet_dates[1], - } + query_details += """SUM(IF(t1.{trans_date} BETWEEN '{sd}' AND '{ed}', t2.stock_qty, NULL)), + SUM(IF(t1.{trans_date} BETWEEN '{sd}' AND '{ed}', t2.base_net_amount, NULL)), + """.format( + trans_date=trans_date, + sd=bet_dates[0], + ed=bet_dates[1], + ) return query_details @@ -282,7 +278,7 @@ def get_period_date_ranges(period, fiscal_year=None, year_start_date=None): increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(period) period_date_ranges = [] - for i in range(1, 13, increment): + for _i in range(1, 13, increment): period_end_date = getdate(year_start_date) + relativedelta(months=increment, days=-1) if period_end_date > getdate(year_end_date): period_end_date = year_end_date @@ -331,9 +327,7 @@ def based_wise_columns_query(based_on, trans): "Territory:Link/Territory:120", ] based_on_details["based_on_select"] = "t1.customer_name, t1.territory, " - based_on_details["based_on_group_by"] = ( - "t1.party_name" if trans == "Quotation" else "t1.customer" - ) + based_on_details["based_on_group_by"] = "t1.party_name" if trans == "Quotation" else "t1.customer" based_on_details["addl_tables"] = "" elif based_on == "Customer Group": diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index 7c3c38706dc..22c68dd3632 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -81,9 +81,7 @@ def get_transaction_list( filters["docstatus"] = ["<", "2"] if doctype in ["Supplier Quotation", "Purchase Invoice"] else 1 if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation": - parties_doctype = ( - "Request for Quotation Supplier" if doctype == "Request for Quotation" else doctype - ) + parties_doctype = "Request for Quotation Supplier" if doctype == "Request for Quotation" else doctype # find party for this contact customers, suppliers = get_customers_suppliers(parties_doctype, user) diff --git a/erpnext/crm/doctype/appointment/appointment.js b/erpnext/crm/doctype/appointment/appointment.js index ca38121b1cf..54aa9a6b716 100644 --- a/erpnext/crm/doctype/appointment/appointment.js +++ b/erpnext/crm/doctype/appointment/appointment.js @@ -1,26 +1,26 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Appointment', { - refresh: function(frm) { - if(frm.doc.lead){ - frm.add_custom_button(frm.doc.lead,()=>{ +frappe.ui.form.on("Appointment", { + refresh: function (frm) { + if (frm.doc.lead) { + frm.add_custom_button(frm.doc.lead, () => { frappe.set_route("Form", "Lead", frm.doc.lead); }); } - if(frm.doc.calendar_event){ - frm.add_custom_button(__(frm.doc.calendar_event),()=>{ + if (frm.doc.calendar_event) { + frm.add_custom_button(__(frm.doc.calendar_event), () => { frappe.set_route("Form", "Event", frm.doc.calendar_event); }); } }, - onload: function(frm){ - frm.set_query("appointment_with", function(){ + onload: function (frm) { + frm.set_query("appointment_with", function () { return { - filters : { - "name": ["in", ["Customer", "Lead"]] - } + filters: { + name: ["in", ["Customer", "Lead"]], + }, }; }); - } + }, }); diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index bd49bdc925c..03ef9c9f2e0 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -178,7 +178,9 @@ class Appointment(Document): "starts_on": self.scheduled_time, "status": "Open", "type": "Public", - "send_reminder": frappe.db.get_single_value("Appointment Booking Settings", "email_reminders"), + "send_reminder": frappe.db.get_single_value( + "Appointment Booking Settings", "email_reminders" + ), "event_participants": [ dict(reference_doctype=self.appointment_with, reference_docname=self.party) ], @@ -231,9 +233,7 @@ def _get_agent_list_as_strings(): def _check_agent_availability(agent_email, scheduled_time): - appointemnts_at_scheduled_time = frappe.get_all( - "Appointment", filters={"scheduled_time": scheduled_time} - ) + appointemnts_at_scheduled_time = frappe.get_all("Appointment", filters={"scheduled_time": scheduled_time}) for appointment in appointemnts_at_scheduled_time: if appointment._assign == agent_email: return False diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js index 0c64eb8e822..255e665a4d2 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js @@ -1,10 +1,14 @@ -frappe.ui.form.on('Appointment Booking Settings', 'validate',check_times); +frappe.ui.form.on("Appointment Booking Settings", "validate", check_times); function check_times(frm) { $.each(frm.doc.availability_of_slots || [], function (i, d) { - let from_time = Date.parse('01/01/2019 ' + d.from_time); - let to_time = Date.parse('01/01/2019 ' + d.to_time); + let from_time = Date.parse("01/01/2019 " + d.from_time); + let to_time = Date.parse("01/01/2019 " + d.to_time); if (from_time > to_time) { - frappe.throw(__('In row {0} of Appointment Booking Slots: "To Time" must be later than "From Time".', [i + 1])); + frappe.throw( + __('In row {0} of Appointment Booking Slots: "To Time" must be later than "From Time".', [ + i + 1, + ]) + ); } }); } diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index e43f4601e9c..cd09172b0c8 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -3,6 +3,7 @@ import datetime +import typing import frappe from frappe import _ @@ -10,7 +11,7 @@ from frappe.model.document import Document class AppointmentBookingSettings(Document): - agent_list = [] # Hack + agent_list: typing.ClassVar[list] = [] # Hack min_date = "01/01/1970 " format_string = "%d/%m/%Y %H:%M:%S" @@ -19,13 +20,13 @@ class AppointmentBookingSettings(Document): def save(self): self.number_of_agents = len(self.agent_list) - super(AppointmentBookingSettings, self).save() + super().save() def validate_availability_of_slots(self): for record in self.availability_of_slots: from_time = datetime.datetime.strptime(self.min_date + record.from_time, self.format_string) to_time = datetime.datetime.strptime(self.min_date + record.to_time, self.format_string) - timedelta = to_time - from_time + to_time - from_time self.validate_from_and_to_time(from_time, to_time, record) self.duration_is_divisible(from_time, to_time) @@ -39,6 +40,4 @@ class AppointmentBookingSettings(Document): def duration_is_divisible(self, from_time, to_time): timedelta = to_time - from_time if timedelta.total_seconds() % (self.appointment_duration * 60): - frappe.throw( - _("The difference between from time and To Time must be a multiple of Appointment") - ) + frappe.throw(_("The difference between from time and To Time must be a multiple of Appointment")) diff --git a/erpnext/crm/doctype/campaign/campaign.js b/erpnext/crm/doctype/campaign/campaign.js index cac45c682cb..9e4a0a95362 100644 --- a/erpnext/crm/doctype/campaign/campaign.js +++ b/erpnext/crm/doctype/campaign/campaign.js @@ -1,17 +1,25 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Campaign', { - refresh: function(frm) { +frappe.ui.form.on("Campaign", { + refresh: function (frm) { erpnext.toggle_naming_series(); if (frm.is_new()) { - frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series"); + frm.toggle_display( + "naming_series", + frappe.boot.sysdefaults.campaign_naming_by == "Naming Series" + ); } else { - cur_frm.add_custom_button(__("View Leads"), function() { - frappe.route_options = {"source": "Campaign", "campaign_name": frm.doc.name}; - frappe.set_route("List", "Lead"); - }, "fa fa-list", true); + cur_frm.add_custom_button( + __("View Leads"), + function () { + frappe.route_options = { source: "Campaign", campaign_name: frm.doc.name }; + frappe.set_route("List", "Lead"); + }, + "fa fa-list", + true + ); } - } + }, }); diff --git a/erpnext/crm/doctype/competitor/competitor.js b/erpnext/crm/doctype/competitor/competitor.js index a5b617dc74c..abd59346b8a 100644 --- a/erpnext/crm/doctype/competitor/competitor.js +++ b/erpnext/crm/doctype/competitor/competitor.js @@ -1,8 +1,7 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Competitor', { +frappe.ui.form.on("Competitor", { // refresh: function(frm) { - // } }); diff --git a/erpnext/crm/doctype/contract/contract.js b/erpnext/crm/doctype/contract/contract.js index 7848de7a727..8d44c22db28 100644 --- a/erpnext/crm/doctype/contract/contract.js +++ b/erpnext/crm/doctype/contract/contract.js @@ -5,12 +5,12 @@ frappe.ui.form.on("Contract", { contract_template: function (frm) { if (frm.doc.contract_template) { frappe.call({ - method: 'erpnext.crm.doctype.contract_template.contract_template.get_contract_template', + method: "erpnext.crm.doctype.contract_template.contract_template.get_contract_template", args: { template_name: frm.doc.contract_template, - doc: frm.doc + doc: frm.doc, }, - callback: function(r) { + callback: function (r) { if (r && r.message) { let contract_template = r.message.contract_template; frm.set_value("contract_terms", r.message.contract_terms); @@ -18,15 +18,15 @@ frappe.ui.form.on("Contract", { if (frm.doc.requires_fulfilment) { // Populate the fulfilment terms table from a contract template, if any - r.message.contract_template.fulfilment_terms.forEach(element => { + r.message.contract_template.fulfilment_terms.forEach((element) => { let d = frm.add_child("fulfilment_terms"); d.requirement = element.requirement; }); frm.refresh_field("fulfilment_terms"); } } - } + }, }); } - } + }, }); diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index 1c2470b6e4e..db23d570644 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -13,12 +13,12 @@ class Contract(Document): name = self.party_name if self.contract_template: - name += " - {} Agreement".format(self.contract_template) + name += f" - {self.contract_template} Agreement" # If identical, append contract name with the next number in the iteration if frappe.db.exists("Contract", name): - count = len(frappe.get_all("Contract", filters={"name": ["like", "%{}%".format(name)]})) - name = "{} - {}".format(name, count) + count = len(frappe.get_all("Contract", filters={"name": ["like", f"%{name}%"]})) + name = f"{name} - {count}" self.name = _(name) diff --git a/erpnext/crm/doctype/contract/contract_list.js b/erpnext/crm/doctype/contract/contract_list.js index 7d5609651a1..e1f8c22f70e 100644 --- a/erpnext/crm/doctype/contract/contract_list.js +++ b/erpnext/crm/doctype/contract/contract_list.js @@ -1,4 +1,4 @@ -frappe.listview_settings['Contract'] = { +frappe.listview_settings["Contract"] = { add_fields: ["status"], get_indicator: function (doc) { if (doc.status == "Unsigned") { diff --git a/erpnext/crm/doctype/contract_fulfilment_checklist/contract_fulfilment_checklist.js b/erpnext/crm/doctype/contract_fulfilment_checklist/contract_fulfilment_checklist.js index f0525b13e22..bdc3fcc4696 100644 --- a/erpnext/crm/doctype/contract_fulfilment_checklist/contract_fulfilment_checklist.js +++ b/erpnext/crm/doctype/contract_fulfilment_checklist/contract_fulfilment_checklist.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Contract Fulfilment Checklist', { - refresh: function(frm) { - - } +frappe.ui.form.on("Contract Fulfilment Checklist", { + refresh: function (frm) {}, }); diff --git a/erpnext/crm/doctype/contract_template/contract_template.js b/erpnext/crm/doctype/contract_template/contract_template.js index 4f7c9a8dc97..5044bbe996c 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.js +++ b/erpnext/crm/doctype/contract_template/contract_template.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Contract Template', { - refresh: function(frm) { - - } +frappe.ui.form.on("Contract Template", { + refresh: function (frm) {}, }); diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.js b/erpnext/crm/doctype/crm_settings/crm_settings.js index c6569d8122e..0fb695a3da4 100644 --- a/erpnext/crm/doctype/crm_settings/crm_settings.js +++ b/erpnext/crm/doctype/crm_settings/crm_settings.js @@ -1,8 +1,7 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('CRM Settings', { +frappe.ui.form.on("CRM Settings", { // refresh: function(frm) { - // } }); diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.js b/erpnext/crm/doctype/email_campaign/email_campaign.js index b0e93536094..7b090a06ef5 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.js +++ b/erpnext/crm/doctype/email_campaign/email_campaign.js @@ -1,8 +1,8 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Email Campaign', { - email_campaign_for: function(frm) { - frm.set_value('recipient', ''); - } +frappe.ui.form.on("Email Campaign", { + email_campaign_for: function (frm) { + frm.set_value("recipient", ""); + }, }); diff --git a/erpnext/crm/doctype/email_campaign/email_campaign_list.js b/erpnext/crm/doctype/email_campaign/email_campaign_list.js index adc399da0f0..3020921e2e4 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign_list.js +++ b/erpnext/crm/doctype/email_campaign/email_campaign_list.js @@ -1,11 +1,11 @@ -frappe.listview_settings['Email Campaign'] = { - get_indicator: function(doc) { +frappe.listview_settings["Email Campaign"] = { + get_indicator: function (doc) { var colors = { - "Unsubscribed": "red", - "Scheduled": "blue", + Unsubscribed: "red", + Scheduled: "blue", "In Progress": "orange", - "Completed": "green" + Completed: "green", }; return [__(doc.status), colors[doc.status], "status,=," + doc.status]; - } + }, }; diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index b98a27ede8e..8336130d50b 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -5,46 +5,50 @@ frappe.provide("erpnext"); cur_frm.email_field = "email_id"; erpnext.LeadController = class LeadController extends frappe.ui.form.Controller { - setup () { + setup() { this.frm.make_methods = { - 'Customer': this.make_customer, - 'Quotation': this.make_quotation, - 'Opportunity': this.make_opportunity + Customer: this.make_customer, + Quotation: this.make_quotation, + Opportunity: this.make_opportunity, }; // For avoiding integration issues. - this.frm.set_df_property('first_name', 'reqd', true); + this.frm.set_df_property("first_name", "reqd", true); } - onload () { + onload() { this.frm.set_query("customer", function (doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.customer_query" } + return { query: "erpnext.controllers.queries.customer_query" }; }); this.frm.set_query("lead_owner", function (doc, cdt, cdn) { - return { query: "frappe.core.doctype.user.user.user_query" } + return { query: "frappe.core.doctype.user.user.user_query" }; }); } - refresh () { + refresh() { var me = this; let doc = this.frm.doc; erpnext.toggle_naming_series(); frappe.dynamic_link = { doc: doc, - fieldname: 'name', - doctype: 'Lead' + 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")); - this.frm.add_custom_button(__("Opportunity"), function() { - me.frm.trigger("make_opportunity"); - }, __("Create")); + this.frm.add_custom_button( + __("Opportunity"), + function () { + me.frm.trigger("make_opportunity"); + }, + __("Create") + ); this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create")); if (!doc.__onload.linked_prospects.length) { this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create")); - this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action')); + this.frm.add_custom_button(__("Add to Prospect"), this.add_lead_to_prospect, __("Action")); } } @@ -58,50 +62,54 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller this.show_activities(); } - add_lead_to_prospect () { - frappe.prompt([ - { - fieldname: 'prospect', - label: __('Prospect'), - fieldtype: 'Link', - options: 'Prospect', - reqd: 1 - } - ], - function(data) { - frappe.call({ - method: 'erpnext.crm.doctype.lead.lead.add_lead_to_prospect', - args: { - 'lead': cur_frm.doc.name, - 'prospect': data.prospect + add_lead_to_prospect() { + frappe.prompt( + [ + { + fieldname: "prospect", + label: __("Prospect"), + fieldtype: "Link", + options: "Prospect", + reqd: 1, }, - callback: function(r) { - if (!r.exc) { - frm.reload_doc(); - } - }, - freeze: true, - freeze_message: __('Adding Lead to Prospect...') - }); - }, __('Add Lead to Prospect'), __('Add')); + ], + function (data) { + frappe.call({ + method: "erpnext.crm.doctype.lead.lead.add_lead_to_prospect", + args: { + lead: cur_frm.doc.name, + prospect: data.prospect, + }, + callback: function (r) { + if (!r.exc) { + frm.reload_doc(); + } + }, + freeze: true, + freeze_message: __("Adding Lead to Prospect..."), + }); + }, + __("Add Lead to Prospect"), + __("Add") + ); } - make_customer () { + make_customer() { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_customer", - frm: cur_frm - }) + frm: cur_frm, + }); } - make_quotation () { + make_quotation() { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_quotation", - frm: cur_frm - }) + frm: cur_frm, + }); } - make_prospect () { - frappe.model.with_doctype("Prospect", function() { + make_prospect() { + frappe.model.with_doctype("Prospect", function () { let prospect = frappe.model.get_new_doc("Prospect"); prospect.company_name = cur_frm.doc.company_name; prospect.no_of_employees = cur_frm.doc.no_of_employees; @@ -113,14 +121,14 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller prospect.prospect_owner = cur_frm.doc.lead_owner; prospect.notes = cur_frm.doc.notes; - let leads_row = frappe.model.add_child(prospect, 'leads'); + let leads_row = frappe.model.add_child(prospect, "leads"); leads_row.lead = cur_frm.doc.name; frappe.set_route("Form", "Prospect", prospect.name); }); } - company_name () { + company_name() { if (!this.frm.doc.lead_name) { this.frm.set_value("lead_name", this.frm.doc.company_name); } @@ -149,86 +157,91 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller } }; - extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm })); frappe.ui.form.on("Lead", { - make_opportunity: async function(frm) { - let existing_prospect = (await frappe.db.get_value("Prospect Lead", - { - "lead": frm.doc.name - }, - "name", null, "Prospect" - )).message.name; + make_opportunity: async function (frm) { + let existing_prospect = ( + await frappe.db.get_value( + "Prospect Lead", + { + lead: frm.doc.name, + }, + "name", + null, + "Prospect" + ) + ).message.name; if (!existing_prospect) { var fields = [ { - "label": "Create Prospect", - "fieldname": "create_prospect", - "fieldtype": "Check", - "default": 1 + label: "Create Prospect", + fieldname: "create_prospect", + fieldtype: "Check", + default: 1, }, { - "label": "Prospect Name", - "fieldname": "prospect_name", - "fieldtype": "Data", - "default": frm.doc.company_name, - "depends_on": "create_prospect" - } + label: "Prospect Name", + fieldname: "prospect_name", + fieldtype: "Data", + default: frm.doc.company_name, + depends_on: "create_prospect", + }, ]; } - let existing_contact = (await frappe.db.get_value("Contact", - { - "first_name": frm.doc.first_name || frm.doc.lead_name, - "last_name": frm.doc.last_name - }, - "name" - )).message.name; + let existing_contact = ( + await frappe.db.get_value( + "Contact", + { + first_name: frm.doc.first_name || frm.doc.lead_name, + last_name: frm.doc.last_name, + }, + "name" + ) + ).message.name; if (!existing_contact) { - fields.push( - { - "label": "Create Contact", - "fieldname": "create_contact", - "fieldtype": "Check", - "default": "1" - } - ); + fields.push({ + label: "Create Contact", + fieldname: "create_contact", + fieldtype: "Check", + default: "1", + }); } if (fields) { var d = new frappe.ui.Dialog({ - title: __('Create Opportunity'), + title: __("Create Opportunity"), fields: fields, - primary_action: function() { + primary_action: function () { var data = d.get_values(); frappe.call({ - method: 'create_prospect_and_contact', + method: "create_prospect_and_contact", doc: frm.doc, args: { data: data, }, freeze: true, - callback: function(r) { + callback: function (r) { if (!r.exc) { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_opportunity", - frm: frm + frm: frm, }); } d.hide(); - } + }, }); }, - primary_action_label: __('Create') + primary_action_label: __("Create"), }); d.show(); } else { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_opportunity", - frm: frm + frm: frm, }); } - } -}); \ No newline at end of file + }, +}); diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index cbef699ce61..3135bcff049 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -19,7 +19,7 @@ from erpnext.selling.doctype.customer.customer import parse_full_name class Lead(SellingController, CRMNote): def get_feed(self): - return "{0}: {1}".format(_(self.status), self.lead_name) + return f"{_(self.status)}: {self.lead_name}" def onload(self): customer = frappe.db.get_value("Customer", {"lead_name": self.name}) @@ -122,9 +122,7 @@ class Lead(SellingController, CRMNote): self.contact_doc.save() def update_prospect(self): - lead_row_name = frappe.db.get_value( - "Prospect Lead", filters={"lead": self.name}, fieldname="name" - ) + lead_row_name = frappe.db.get_value("Prospect Lead", filters={"lead": self.name}, fieldname="name") if lead_row_name: lead_row = frappe.get_doc("Prospect Lead", lead_row_name) lead_row.update( @@ -174,9 +172,7 @@ class Lead(SellingController, CRMNote): ) def has_lost_quotation(self): - return frappe.db.get_value( - "Quotation", {"party_name": self.name, "docstatus": 1, "status": "Lost"} - ) + return frappe.db.get_value("Quotation", {"party_name": self.name, "docstatus": 1, "status": "Lost"}) @frappe.whitelist() def create_prospect_and_contact(self, data): @@ -448,8 +444,8 @@ def get_lead_with_phone_number(number): leads = frappe.get_all( "Lead", or_filters={ - "phone": ["like", "%{}".format(number)], - "mobile_no": ["like", "%{}".format(number)], + "phone": ["like", f"%{number}"], + "mobile_no": ["like", f"%{number}"], }, limit=1, order_by="creation DESC", @@ -476,9 +472,7 @@ def add_lead_to_prospect(lead, prospect): link_open_events("Lead", lead, prospect) frappe.msgprint( - _("Lead {0} has been added to prospect {1}.").format( - frappe.bold(lead), frappe.bold(prospect.name) - ), + _("Lead {0} has been added to prospect {1}.").format(frappe.bold(lead), frappe.bold(prospect.name)), title=_("Lead -> Prospect"), indicator="green", ) diff --git a/erpnext/crm/doctype/lead/lead_list.js b/erpnext/crm/doctype/lead/lead_list.js index dbeaf608ffc..97415251a93 100644 --- a/erpnext/crm/doctype/lead/lead_list.js +++ b/erpnext/crm/doctype/lead/lead_list.js @@ -1,28 +1,42 @@ -frappe.listview_settings['Lead'] = { - onload: function(listview) { +frappe.listview_settings["Lead"] = { + onload: function (listview) { if (frappe.boot.user.can_create.includes("Prospect")) { - listview.page.add_action_item(__("Create Prospect"), function() { - frappe.model.with_doctype("Prospect", function() { + listview.page.add_action_item(__("Create Prospect"), function () { + frappe.model.with_doctype("Prospect", function () { let prospect = frappe.model.get_new_doc("Prospect"); let leads = listview.get_checked_items(); - frappe.db.get_value("Lead", leads[0].name, ["company_name", "no_of_employees", "industry", "market_segment", "territory", "fax", "website", "lead_owner"], (r) => { - prospect.company_name = r.company_name; - prospect.no_of_employees = r.no_of_employees; - prospect.industry = r.industry; - prospect.market_segment = r.market_segment; - prospect.territory = r.territory; - prospect.fax = r.fax; - prospect.website = r.website; - prospect.prospect_owner = r.lead_owner; + frappe.db.get_value( + "Lead", + leads[0].name, + [ + "company_name", + "no_of_employees", + "industry", + "market_segment", + "territory", + "fax", + "website", + "lead_owner", + ], + (r) => { + prospect.company_name = r.company_name; + prospect.no_of_employees = r.no_of_employees; + prospect.industry = r.industry; + prospect.market_segment = r.market_segment; + prospect.territory = r.territory; + prospect.fax = r.fax; + prospect.website = r.website; + prospect.prospect_owner = r.lead_owner; - leads.forEach(function(lead) { - let lead_prospect_row = frappe.model.add_child(prospect, 'leads'); - lead_prospect_row.lead = lead.name; - }); - frappe.set_route("Form", "Prospect", prospect.name); - }); + leads.forEach(function (lead) { + let lead_prospect_row = frappe.model.add_child(prospect, "leads"); + lead_prospect_row.lead = lead.name; + }); + frappe.set_route("Form", "Prospect", prospect.name); + } + ); }); }); } - } + }, }; diff --git a/erpnext/crm/doctype/lead/test_lead.py b/erpnext/crm/doctype/lead/test_lead.py index 8fe688de46d..bab9947b333 100644 --- a/erpnext/crm/doctype/lead/test_lead.py +++ b/erpnext/crm/doctype/lead/test_lead.py @@ -134,9 +134,7 @@ class TestLead(unittest.TestCase): self.assertEqual(event.event_participants[1].reference_docname, opportunity.name) self.assertTrue( - frappe.db.get_value( - "ToDo", {"reference_type": "Opportunity", "reference_name": opportunity.name} - ) + frappe.db.get_value("ToDo", {"reference_type": "Opportunity", "reference_name": opportunity.name}) ) def test_copy_events_from_lead_to_prospect(self): @@ -194,7 +192,7 @@ def make_lead(**args): "doctype": "Lead", "first_name": args.first_name or "_Test", "last_name": args.last_name or "Lead", - "email_id": args.email_id or "new_lead_{}@example.com".format(random_string(5)), + "email_id": args.email_id or f"new_lead_{random_string(5)}@example.com", "company_name": args.company_name or "_Test Company", } ).insert() diff --git a/erpnext/crm/doctype/lead_source/lead_source.js b/erpnext/crm/doctype/lead_source/lead_source.js index 3cbe6492090..5efc750601a 100644 --- a/erpnext/crm/doctype/lead_source/lead_source.js +++ b/erpnext/crm/doctype/lead_source/lead_source.js @@ -1,8 +1,7 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Lead Source', { +frappe.ui.form.on("Lead Source", { // refresh: function(frm) { - // } }); diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js index 7d6b3955cde..70cf4df9519 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -1,23 +1,29 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('LinkedIn Settings', { - onload: function(frm) { - if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) { +frappe.ui.form.on("LinkedIn Settings", { + onload: function (frm) { + if (frm.doc.session_status == "Expired" && frm.doc.consumer_key && frm.doc.consumer_secret) { frappe.confirm( - __('Session not valid. Do you want to login?'), - function(){ + __("Session not valid. Do you want to login?"), + function () { frm.trigger("login"); }, - function(){ + function () { window.close(); } ); } - frm.dashboard.set_headline(__("For more information, {0}.", [`${__('click here')}`])); + frm.dashboard.set_headline( + __("For more information, {0}.", [ + `${__( + "click here" + )}`, + ]) + ); }, - refresh: function(frm) { - if (frm.doc.session_status=="Expired"){ + refresh: function (frm) { + if (frm.doc.session_status == "Expired") { let msg = __("Session not active. Save document to login."); frm.dashboard.set_headline_alert( `
      @@ -28,19 +34,18 @@ frappe.ui.form.on('LinkedIn Settings', { ); } - if (frm.doc.session_status=="Active"){ + if (frm.doc.session_status == "Active") { let d = new Date(frm.doc.modified); - d.setDate(d.getDate()+60); + d.setDate(d.getDate() + 60); let dn = new Date(); let days = d.getTime() - dn.getTime(); - days = Math.floor(days/(1000 * 3600 * 24)); - let msg,color; + days = Math.floor(days / (1000 * 3600 * 24)); + let msg, color; - if (days>0){ + if (days > 0) { msg = __("Your session will be expire in {0} days.", [days]); color = "green"; - } - else { + } else { msg = __("Session is expired. Save doc to login."); color = "red"; } @@ -54,21 +59,23 @@ frappe.ui.form.on('LinkedIn Settings', { ); } }, - login: function(frm) { - if (frm.doc.consumer_key && frm.doc.consumer_secret){ + login: function (frm) { + if (frm.doc.consumer_key && frm.doc.consumer_secret) { frappe.dom.freeze(); - frappe.call({ - doc: frm.doc, - method: "get_authorization_url", - callback : function(r) { - window.location.href = r.message; - } - }).fail(function() { - frappe.dom.unfreeze(); - }); + frappe + .call({ + doc: frm.doc, + method: "get_authorization_url", + callback: function (r) { + window.location.href = r.message; + }, + }) + .fail(function () { + frappe.dom.unfreeze(); + }); } }, - after_save: function(frm) { + after_save: function (frm) { frm.trigger("login"); - } + }, }); diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index 64b3a017b46..1b535e14c46 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -19,14 +19,14 @@ class LinkedInSettings(Document): { "response_type": "code", "client_id": self.consumer_key, - "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format( + "redirect_uri": "{}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format( frappe.utils.get_url() ), "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social", } ) - url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params) + url = f"https://www.linkedin.com/oauth/v2/authorization?{params}" return url @@ -37,7 +37,7 @@ class LinkedInSettings(Document): "code": code, "client_id": self.consumer_key, "client_secret": self.get_password(fieldname="consumer_secret"), - "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format( + "redirect_uri": "{}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format( frappe.utils.get_url() ), } @@ -80,7 +80,7 @@ class LinkedInSettings(Document): body = { "registerUploadRequest": { "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], - "owner": "urn:li:organization:{0}".format(self.company_id), + "owner": f"urn:li:organization:{self.company_id}", "serviceRelationships": [ {"relationshipType": "OWNER", "identifier": "urn:li:userGeneratedContent"} ], @@ -100,7 +100,7 @@ class LinkedInSettings(Document): if response.status_code < 200 and response.status_code > 299: frappe.throw( _("Error While Uploading Image"), - title="{0} {1}".format(response.status_code, response.reason), + title=f"{response.status_code} {response.reason}", ) return None return asset @@ -115,7 +115,7 @@ class LinkedInSettings(Document): body = { "distribution": {"linkedInDistributionTarget": {}}, - "owner": "urn:li:organization:{0}".format(self.company_id), + "owner": f"urn:li:organization:{self.company_id}", "subject": title, "text": {"text": text}, } @@ -136,13 +136,13 @@ class LinkedInSettings(Document): if response.status_code not in [201, 200]: raise - except Exception as e: + except Exception: self.api_error(response) return response def get_headers(self): - return {"Authorization": "Bearer {}".format(self.access_token)} + return {"Authorization": f"Bearer {self.access_token}"} def get_reference_url(self, text): import re @@ -155,7 +155,7 @@ class LinkedInSettings(Document): def delete_post(self, post_id): try: response = requests.delete( - url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), + url=f"https://api.linkedin.com/v2/shares/urn:li:share:{post_id}", headers=self.get_headers(), ) if response.status_code != 200: @@ -164,7 +164,7 @@ class LinkedInSettings(Document): self.api_error(response) def get_post(self, post_id): - url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format( + url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{}&shares[0]=urn:li:share:{}".format( self.company_id, post_id ) diff --git a/erpnext/crm/doctype/lost_reason_detail/lost_reason_detail.js b/erpnext/crm/doctype/lost_reason_detail/lost_reason_detail.js index 08fbdad4a5a..c3081f8a4d0 100644 --- a/erpnext/crm/doctype/lost_reason_detail/lost_reason_detail.js +++ b/erpnext/crm/doctype/lost_reason_detail/lost_reason_detail.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Lost Reason Detail', { - refresh: function() { - - } +frappe.ui.form.on("Lost Reason Detail", { + refresh: function () {}, }); diff --git a/erpnext/crm/doctype/market_segment/market_segment.js b/erpnext/crm/doctype/market_segment/market_segment.js index 94ffdee75d9..b089c52b5c9 100644 --- a/erpnext/crm/doctype/market_segment/market_segment.js +++ b/erpnext/crm/doctype/market_segment/market_segment.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Market Segment', { - refresh: function(frm) { - - } +frappe.ui.form.on("Market Segment", { + refresh: function (frm) {}, }); diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 2a8d65f4866..a0301f9ea0a 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -304,9 +304,7 @@ def make_quotation(source_name, target_doc=None): quotation.conversion_rate = exchange_rate # get default taxes - taxes = get_default_taxes_and_charges( - "Sales Taxes and Charges Template", company=quotation.company - ) + taxes = get_default_taxes_and_charges("Sales Taxes and Charges Template", company=quotation.company) if taxes.get("taxes"): quotation.update(taxes) @@ -412,9 +410,7 @@ def set_multiple_status(names, status): def auto_close_opportunity(): """auto close the `Replied` Opportunities after 7 days""" - auto_close_after_days = ( - frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15 - ) + auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15 table = frappe.qb.DocType("Opportunity") opportunities = ( diff --git a/erpnext/crm/doctype/opportunity/opportunity_list.js b/erpnext/crm/doctype/opportunity/opportunity_list.js index 24b05145fd2..5028ae178c7 100644 --- a/erpnext/crm/doctype/opportunity/opportunity_list.js +++ b/erpnext/crm/doctype/opportunity/opportunity_list.js @@ -1,31 +1,31 @@ -frappe.listview_settings['Opportunity'] = { +frappe.listview_settings["Opportunity"] = { add_fields: ["customer_name", "opportunity_type", "opportunity_from", "status"], - get_indicator: function(doc) { + get_indicator: function (doc) { var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - if(doc.status=="Quotation") { + if (doc.status == "Quotation") { indicator[1] = "green"; } return indicator; }, - onload: function(listview) { + onload: function (listview) { var method = "erpnext.crm.doctype.opportunity.opportunity.set_multiple_status"; - listview.page.add_menu_item(__("Set as Open"), function() { - listview.call_for_selected_items(method, {"status": "Open"}); + listview.page.add_menu_item(__("Set as Open"), function () { + listview.call_for_selected_items(method, { status: "Open" }); }); - listview.page.add_menu_item(__("Set as Closed"), function() { - listview.call_for_selected_items(method, {"status": "Closed"}); + listview.page.add_menu_item(__("Set as Closed"), function () { + listview.call_for_selected_items(method, { status: "Closed" }); }); - if(listview.page.fields_dict.opportunity_from) { - listview.page.fields_dict.opportunity_from.get_query = function() { + if (listview.page.fields_dict.opportunity_from) { + listview.page.fields_dict.opportunity_from.get_query = function () { return { - "filters": { - "name": ["in", ["Customer", "Lead"]], - } + filters: { + name: ["in", ["Customer", "Lead"]], + }, }; }; } - } + }, }; diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py index 1ff3267e719..79aca1b0033 100644 --- a/erpnext/crm/doctype/opportunity/test_opportunity.py +++ b/erpnext/crm/doctype/opportunity/test_opportunity.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import add_days, now_datetime, random_string, today +from frappe.utils import now_datetime, random_string, today from erpnext.crm.doctype.lead.lead import make_customer from erpnext.crm.doctype.lead.test_lead import make_lead @@ -32,9 +32,7 @@ class TestOpportunity(unittest.TestCase): self.assertTrue(opp_doc.party_name) self.assertEqual(opp_doc.opportunity_from, "Lead") - self.assertEqual( - frappe.db.get_value("Lead", opp_doc.party_name, "email_id"), opp_doc.contact_email - ) + self.assertEqual(frappe.db.get_value("Lead", opp_doc.party_name, "email_id"), opp_doc.contact_email) # create new customer and create new contact against 'new.opportunity@example.com' customer = make_customer(opp_doc.party_name).insert(ignore_permissions=True) @@ -53,9 +51,7 @@ class TestOpportunity(unittest.TestCase): self.assertEqual(opportunity_doc.total, 2200) def test_carry_forward_of_email_and_comments(self): - frappe.db.set_value( - "CRM Settings", "CRM Settings", "carry_forward_communication_and_comments", 1 - ) + frappe.db.set_value("CRM Settings", "CRM Settings", "carry_forward_communication_and_comments", 1) lead_doc = make_lead() lead_doc.add_comment("Comment", text="Test Comment 1") lead_doc.add_comment("Comment", text="Test Comment 2") @@ -66,9 +62,7 @@ class TestOpportunity(unittest.TestCase): opportunity_comment_count = frappe.db.count( "Comment", {"reference_doctype": opp_doc.doctype, "reference_name": opp_doc.name} ) - opportunity_communication_count = len( - get_linked_communication_list(opp_doc.doctype, opp_doc.name) - ) + opportunity_communication_count = len(get_linked_communication_list(opp_doc.doctype, opp_doc.name)) self.assertEqual(opportunity_comment_count, 2) self.assertEqual(opportunity_communication_count, 2) @@ -79,7 +73,7 @@ class TestOpportunity(unittest.TestCase): def make_opportunity_from_lead(): - new_lead_email_id = "new{}@example.com".format(random_string(5)) + new_lead_email_id = f"new{random_string(5)}@example.com" args = { "doctype": "Opportunity", "contact_email": new_lead_email_id, @@ -128,9 +122,7 @@ def make_opportunity(**args): return opp_doc -def create_communication( - reference_doctype, reference_name, sender, sent_or_received=None, creation=None -): +def create_communication(reference_doctype, reference_name, sender, sent_or_received=None, creation=None): communication = frappe.get_doc( { "doctype": "Communication", diff --git a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.js b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.js index 877dd592b22..3f08355442f 100644 --- a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.js +++ b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Opportunity Lost Reason', { - refresh: function() { - - } +frappe.ui.form.on("Opportunity Lost Reason", { + refresh: function () {}, }); diff --git a/erpnext/crm/doctype/opportunity_type/opportunity_type.js b/erpnext/crm/doctype/opportunity_type/opportunity_type.js index 174625e73c6..18213ad0cdf 100644 --- a/erpnext/crm/doctype/opportunity_type/opportunity_type.js +++ b/erpnext/crm/doctype/opportunity_type/opportunity_type.js @@ -1,8 +1,6 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Opportunity Type', { - refresh: function(frm) { - - } +frappe.ui.form.on("Opportunity Type", { + refresh: function (frm) {}, }); diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js index 495ed291ae9..62039b33b98 100644 --- a/erpnext/crm/doctype/prospect/prospect.js +++ b/erpnext/crm/doctype/prospect/prospect.js @@ -1,25 +1,33 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Prospect', { - refresh (frm) { +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({ - method: "erpnext.crm.doctype.prospect.prospect.make_customer", - frm: frm - }); - }, __("Create")); + frm.add_custom_button( + __("Customer"), + function () { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.prospect.prospect.make_customer", + frm: frm, + }); + }, + __("Create") + ); } if (!frm.is_new() && frappe.boot.user.can_create.includes("Opportunity")) { - frm.add_custom_button(__("Opportunity"), function() { - frappe.model.open_mapped_doc({ - method: "erpnext.crm.doctype.prospect.prospect.make_opportunity", - frm: frm - }); - }, __("Create")); + frm.add_custom_button( + __("Opportunity"), + function () { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.prospect.prospect.make_opportunity", + frm: frm, + }); + }, + __("Create") + ); } if (!frm.is_new()) { @@ -31,7 +39,7 @@ frappe.ui.form.on('Prospect', { frm.trigger("show_activities"); }, - show_notes (frm) { + show_notes(frm) { const crm_notes = new erpnext.utils.CRMNotes({ frm: frm, notes_wrapper: $(frm.fields_dict.notes_html.wrapper), @@ -39,7 +47,7 @@ frappe.ui.form.on('Prospect', { crm_notes.refresh(); }, - show_activities (frm) { + show_activities(frm) { const crm_activities = new erpnext.utils.CRMActivities({ frm: frm, open_activities_wrapper: $(frm.fields_dict.open_activities_html.wrapper), @@ -47,6 +55,5 @@ frappe.ui.form.on('Prospect', { form_wrapper: $(frm.wrapper), }); crm_activities.refresh(); - } - + }, }); diff --git a/erpnext/crm/doctype/prospect/test_prospect.py b/erpnext/crm/doctype/prospect/test_prospect.py index 874f84ca843..c3930ee6c93 100644 --- a/erpnext/crm/doctype/prospect/test_prospect.py +++ b/erpnext/crm/doctype/prospect/test_prospect.py @@ -34,7 +34,7 @@ def make_prospect(**args): prospect_doc = frappe.get_doc( { "doctype": "Prospect", - "company_name": args.company_name or "_Test Company {}".format(random_string(3)), + "company_name": args.company_name or f"_Test Company {random_string(3)}", } ).insert() diff --git a/erpnext/crm/doctype/sales_stage/sales_stage.js b/erpnext/crm/doctype/sales_stage/sales_stage.js index 0447f783ce6..c6811665a63 100644 --- a/erpnext/crm/doctype/sales_stage/sales_stage.js +++ b/erpnext/crm/doctype/sales_stage/sales_stage.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Sales Stage', { - refresh: function(frm) { - - } +frappe.ui.form.on("Sales Stage", { + refresh: function (frm) {}, }); diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index d4ac0bad16c..f7d0efc83d7 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -1,7 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Social Media Post', { - validate: function(frm) { +frappe.ui.form.on("Social Media Post", { + validate: function (frm) { if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) { frappe.throw(__("Select atleast one Social Media Platform to Share on.")); } @@ -12,111 +12,116 @@ frappe.ui.form.on('Social Media Post', { frappe.throw(__("Scheduled Time must be a future time.")); } } - frm.trigger('validate_tweet_length'); + frm.trigger("validate_tweet_length"); }, - text: function(frm) { + text: function (frm) { if (frm.doc.text) { - frm.set_df_property('text', 'description', `${frm.doc.text.length}/280`); - frm.refresh_field('text'); - frm.trigger('validate_tweet_length'); + frm.set_df_property("text", "description", `${frm.doc.text.length}/280`); + frm.refresh_field("text"); + frm.trigger("validate_tweet_length"); } }, - validate_tweet_length: function(frm) { + validate_tweet_length: function (frm) { if (frm.doc.text && frm.doc.text.length > 280) { frappe.throw(__("Tweet length Must be less than 280.")); } }, - onload: function(frm) { - frm.trigger('make_dashboard'); + onload: function (frm) { + frm.trigger("make_dashboard"); }, - make_dashboard: function(frm) { + make_dashboard: function (frm) { if (frm.doc.post_status == "Posted") { frappe.call({ doc: frm.doc, - method: 'get_post', + method: "get_post", freeze: true, callback: (r) => { if (!r.message) { return; } - let datasets = [], colors = []; + let datasets = [], + colors = []; if (r.message && r.message.twitter) { - colors.push('#1DA1F2'); + colors.push("#1DA1F2"); datasets.push({ - name: 'Twitter', - values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count] + name: "Twitter", + values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count], }); } if (r.message && r.message.linkedin) { - colors.push('#0077b5'); + colors.push("#0077b5"); datasets.push({ - name: 'LinkedIn', - values: [r.message.linkedin.totalShareStatistics.likeCount, r.message.linkedin.totalShareStatistics.shareCount] + name: "LinkedIn", + values: [ + r.message.linkedin.totalShareStatistics.likeCount, + r.message.linkedin.totalShareStatistics.shareCount, + ], }); } if (datasets.length) { frm.dashboard.render_graph({ data: { - labels: ['Likes', 'Retweets/Shares'], - datasets: datasets + labels: ["Likes", "Retweets/Shares"], + datasets: datasets, }, title: __("Post Metrics"), - type: 'bar', + type: "bar", height: 300, - colors: colors + colors: colors, }); } - } + }, }); } }, - refresh: function(frm) { - frm.trigger('text'); + refresh: function (frm) { + frm.trigger("text"); if (frm.doc.docstatus === 1) { - if (!['Posted', 'Deleted'].includes(frm.doc.post_status)) { - frm.trigger('add_post_btn'); + if (!["Posted", "Deleted"].includes(frm.doc.post_status)) { + frm.trigger("add_post_btn"); } - if (frm.doc.post_status !='Deleted') { - frm.add_custom_button(__('Delete Post'), function() { - frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'), - function() { + if (frm.doc.post_status != "Deleted") { + frm.add_custom_button(__("Delete Post"), function () { + frappe.confirm( + __("Are you sure want to delete the Post from Social Media platforms?"), + function () { frappe.call({ doc: frm.doc, - method: 'delete_post', + method: "delete_post", freeze: true, callback: () => { frm.reload_doc(); - } + }, }); } ); }); } - if (frm.doc.post_status !='Deleted') { - let html=''; + if (frm.doc.post_status != "Deleted") { + let html = ""; if (frm.doc.twitter) { let color = frm.doc.twitter_post_id ? "green" : "red"; let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; html += `
      Twitter : ${status} -
      ` ; +
      `; } if (frm.doc.linkedin) { let color = frm.doc.linkedin_post_id ? "green" : "red"; let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; html += `
      LinkedIn : ${status} -
      ` ; +
    `; } html = `
    ${html}
    `; frm.dashboard.set_headline_alert(html); @@ -124,16 +129,16 @@ frappe.ui.form.on('Social Media Post', { } }, - add_post_btn: function(frm) { - frm.add_custom_button(__('Post Now'), function() { + add_post_btn: function (frm) { + frm.add_custom_button(__("Post Now"), function () { frappe.call({ doc: frm.doc, - method: 'post', + method: "post", freeze: true, - callback: function() { + callback: function () { frm.reload_doc(); - } + }, }); }); - } + }, }); diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py index 55db29a627a..5822f3cc51a 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -26,7 +26,7 @@ class SocialMediaPost(Document): def submit(self): if self.scheduled_time: self.post_status = "Scheduled" - super(SocialMediaPost, self).submit() + super().submit() def on_cancel(self): self.db_set("post_status", "Cancelled") diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js index a8c8272ad08..ab7b44c9eaa 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post_list.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post_list.js @@ -1,11 +1,14 @@ -frappe.listview_settings['Social Media Post'] = { +frappe.listview_settings["Social Media Post"] = { add_fields: ["status", "post_status"], - get_indicator: function(doc) { - return [__(doc.post_status), { - "Scheduled": "orange", - "Posted": "green", - "Error": "red", - "Deleted": "red" - }[doc.post_status]]; - } -} + get_indicator: function (doc) { + return [ + __(doc.post_status), + { + Scheduled: "orange", + Posted: "green", + Error: "red", + Deleted: "red", + }[doc.post_status], + ]; + }, +}; diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index c322092d6f3..a84e01d07c4 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -1,31 +1,38 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Twitter Settings', { - onload: function(frm) { - if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ +frappe.ui.form.on("Twitter Settings", { + onload: function (frm) { + if (frm.doc.session_status == "Expired" && frm.doc.consumer_key && frm.doc.consumer_secret) { frappe.confirm( - __('Session not valid, Do you want to login?'), - function(){ + __("Session not valid, Do you want to login?"), + function () { frm.trigger("login"); }, - function(){ + function () { window.close(); } ); } - frm.dashboard.set_headline(__("For more information, {0}.", [`${__('click here')}`])); + frm.dashboard.set_headline( + __("For more information, {0}.", [ + `${__( + "click here" + )}`, + ]) + ); }, - refresh: function(frm) { - let msg, color, flag=false; + refresh: function (frm) { + let msg, + color, + flag = false; if (frm.doc.session_status == "Active") { msg = __("Session Active"); - color = 'green'; + color = "green"; flag = true; - } - else if(frm.doc.consumer_key && frm.doc.consumer_secret) { + } else if (frm.doc.consumer_key && frm.doc.consumer_secret) { msg = __("Session Not Active. Save doc to login."); - color = 'red'; + color = "red"; flag = true; } @@ -39,21 +46,23 @@ frappe.ui.form.on('Twitter Settings', { ); } }, - login: function(frm) { - if (frm.doc.consumer_key && frm.doc.consumer_secret){ + login: function (frm) { + if (frm.doc.consumer_key && frm.doc.consumer_secret) { frappe.dom.freeze(); - frappe.call({ - doc: frm.doc, - method: "get_authorize_url", - callback : function(r) { - window.location.href = r.message; - } - }).fail(function() { - frappe.dom.unfreeze(); - }); + frappe + .call({ + doc: frm.doc, + method: "get_authorize_url", + callback: function (r) { + window.location.href = r.message; + }, + }) + .fail(function () { + frappe.dom.unfreeze(); + }); } }, - after_save: function(frm) { + after_save: function (frm) { frm.trigger("login"); - } + }, }); diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index 42874ddeea5..c3a0177c487 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -16,10 +16,8 @@ from tweepy.error import TweepError class TwitterSettings(Document): @frappe.whitelist() def get_authorize_url(self): - callback_url = ( - "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format( - frappe.utils.get_url() - ) + callback_url = "{}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format( + frappe.utils.get_url() ) auth = tweepy.OAuthHandler( self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url @@ -27,10 +25,12 @@ class TwitterSettings(Document): try: redirect_url = auth.get_authorization_url() return redirect_url - except tweepy.TweepError as e: + except tweepy.TweepError: frappe.msgprint(_("Error! Failed to get request token.")) frappe.throw( - _("Invalid {0} or {1}").format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key")) + _("Invalid {0} or {1}").format( + frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key") + ) ) def get_access_token(self, oauth_token, oauth_verifier): @@ -59,7 +59,7 @@ class TwitterSettings(Document): frappe.local.response["type"] = "redirect" frappe.local.response["location"] = get_url_to_form("Twitter Settings", "Twitter Settings") - except TweepError as e: + except TweepError: frappe.msgprint(_("Error! Failed to get access token.")) frappe.throw(_("Invalid Consumer Key or Consumer Secret Key")) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 6bcfcb7e626..cacc5a16607 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -3,7 +3,6 @@ import frappe @frappe.whitelist() def get_last_interaction(contact=None, lead=None): - if not contact and not lead: return @@ -23,16 +22,14 @@ def get_last_interaction(contact=None, lead=None): # remove extra appended 'OR' query_condition = query_condition[:-2] last_communication = frappe.db.sql( - """ + f""" SELECT `name`, `content` FROM `tabCommunication` WHERE `sent_or_received`='Received' - AND ({}) + AND ({query_condition}) ORDER BY `modified` LIMIT 1 - """.format( - query_condition - ), + """, values, as_dict=1, ) # nosec diff --git a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js index 0c4e7f2dabe..a1e51f0654d 100644 --- a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js +++ b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js @@ -1,18 +1,18 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.query_reports["Campaign Efficiency"] = { - "filters": [ + filters: [ { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], - } - ] + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + }, + ], }; diff --git a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py index be7f5ca29b3..6d01bd289d3 100644 --- a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py +++ b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py @@ -33,13 +33,11 @@ def get_lead_data(filters, based_on): conditions = get_filter_conditions(filters) lead_details = frappe.db.sql( - """ + f""" select {based_on_field}, name from `tabLead` where {based_on_field} is not null and {based_on_field} != '' {conditions} - """.format( - based_on_field=based_on_field, conditions=conditions - ), + """, filters, as_dict=1, ) @@ -82,9 +80,7 @@ def get_lead_quotation_count(leads): where quotation_to = 'Lead' and party_name in (%s)""" % ", ".join(["%s"] * len(leads)), tuple(leads), - )[0][ - 0 - ] # nosec + )[0][0] # nosec def get_lead_opp_count(leads): diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js index fe5707af296..b2ebe2914a9 100644 --- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js @@ -3,41 +3,43 @@ /* eslint-disable */ frappe.query_reports["First Response Time for Opportunity"] = { - "filters": [ + filters: [ { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30) + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_days(frappe.datetime.nowdate(), -30), }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.nowdate() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.nowdate(), }, ], get_chart_data: function (_columns, result) { return { data: { - labels: result.map(d => d.creation_date), - datasets: [{ - name: "First Response Time", - values: result.map(d => d.first_response_time) - }] + labels: result.map((d) => d.creation_date), + datasets: [ + { + name: "First Response Time", + values: result.map((d) => d.first_response_time), + }, + ], }, type: "line", tooltipOptions: { - formatTooltipY: d => { + formatTooltipY: (d) => { let duration_options = { hide_days: 0, - hide_seconds: 0 + hide_seconds: 0, }; return frappe.utils.get_formatted_duration(d, duration_options); - } - } - } - } + }, + }, + }; + }, }; diff --git a/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js b/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js index eeb8984513e..baa34aebd10 100644 --- a/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js +++ b/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js @@ -3,20 +3,20 @@ /* eslint-disable */ frappe.query_reports["Lead Conversion Time"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - 'reqd': 1, - "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30) + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_days(frappe.datetime.nowdate(), -30), }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - 'reqd': 1, - "default":frappe.datetime.nowdate() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.nowdate(), }, - ] + ], }; diff --git a/erpnext/crm/report/lead_details/lead_details.js b/erpnext/crm/report/lead_details/lead_details.js index 2f6d24224fb..f2866e17800 100644 --- a/erpnext/crm/report/lead_details/lead_details.js +++ b/erpnext/crm/report/lead_details/lead_details.js @@ -3,50 +3,50 @@ /* eslint-disable */ frappe.query_reports["Lead Details"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -12), - "reqd": 1 + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -12), + reqd: 1, }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1 + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"status", - "label": __("Status"), - "fieldtype": "Select", + fieldname: "status", + label: __("Status"), + fieldtype: "Select", options: [ - { "value": "Lead", "label": __("Lead") }, - { "value": "Open", "label": __("Open") }, - { "value": "Replied", "label": __("Replied") }, - { "value": "Opportunity", "label": __("Opportunity") }, - { "value": "Quotation", "label": __("Quotation") }, - { "value": "Lost Quotation", "label": __("Lost Quotation") }, - { "value": "Interested", "label": __("Interested") }, - { "value": "Converted", "label": __("Converted") }, - { "value": "Do Not Contact", "label": __("Do Not Contact") }, + { value: "Lead", label: __("Lead") }, + { value: "Open", label: __("Open") }, + { value: "Replied", label: __("Replied") }, + { value: "Opportunity", label: __("Opportunity") }, + { value: "Quotation", label: __("Quotation") }, + { value: "Lost Quotation", label: __("Lost Quotation") }, + { value: "Interested", label: __("Interested") }, + { value: "Converted", label: __("Converted") }, + { value: "Do Not Contact", label: __("Do Not Contact") }, ], }, { - "fieldname":"territory", - "label": __("Territory"), - "fieldtype": "Link", - "options": "Territory", - } - ] + fieldname: "territory", + label: __("Territory"), + fieldtype: "Link", + options: "Territory", + }, + ], }; diff --git a/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.js b/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.js index 6fc52a1afc9..bd64641dcc2 100644 --- a/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.js +++ b/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.js @@ -1,17 +1,18 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt - frappe.query_reports["Lead Owner Efficiency"] = { - "filters": [ - { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], - }, - { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], - } - ]}; +frappe.query_reports["Lead Owner Efficiency"] = { + filters: [ + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + }, + ], +}; diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.js b/erpnext/crm/report/lost_opportunity/lost_opportunity.js index 927c54df072..1e8e13bf24d 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.js +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.js @@ -3,59 +3,59 @@ /* eslint-disable */ frappe.query_reports["Lost Opportunity"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -12), - "reqd": 1 + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -12), + reqd: 1, }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1 + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"lost_reason", - "label": __("Lost Reason"), - "fieldtype": "Link", - "options": "Opportunity Lost Reason" + fieldname: "lost_reason", + label: __("Lost Reason"), + fieldtype: "Link", + options: "Opportunity Lost Reason", }, { - "fieldname":"territory", - "label": __("Territory"), - "fieldtype": "Link", - "options": "Territory" + fieldname: "territory", + label: __("Territory"), + fieldtype: "Link", + options: "Territory", }, { - "fieldname":"opportunity_from", - "label": __("Opportunity From"), - "fieldtype": "Link", - "options": "DocType", - "get_query": function() { + fieldname: "opportunity_from", + label: __("Opportunity From"), + fieldtype: "Link", + options: "DocType", + get_query: function () { return { - "filters": { - "name": ["in", ["Customer", "Lead"]], - } - } - } + filters: { + name: ["in", ["Customer", "Lead"]], + }, + }; + }, }, { - "fieldname":"party_name", - "label": __("Party"), - "fieldtype": "Dynamic Link", - "options": "opportunity_from" + fieldname: "party_name", + label: __("Party"), + fieldtype: "Dynamic Link", + options: "opportunity_from", }, - ] + ], }; diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index b37cfa449fe..eb09711667a 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -67,7 +67,7 @@ def get_columns(): def get_data(filters): return frappe.db.sql( - """ + f""" SELECT `tabOpportunity`.name, `tabOpportunity`.opportunity_from, @@ -79,17 +79,15 @@ def get_data(filters): `tabOpportunity`.territory FROM `tabOpportunity` - {join} + {get_join(filters)} WHERE `tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s - {conditions} + {get_conditions(filters)} GROUP BY `tabOpportunity`.name ORDER BY - `tabOpportunity`.creation asc """.format( - conditions=get_conditions(filters), join=get_join(filters) - ), + `tabOpportunity`.creation asc """, filters, as_dict=1, ) @@ -119,9 +117,7 @@ def get_join(filters): join = """JOIN `tabOpportunity Lost Reason Detail` ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and - `tabOpportunity Lost Reason Detail`.lost_reason = '{0}' - """.format( - filters.get("lost_reason") - ) + `tabOpportunity Lost Reason Detail`.lost_reason = '{}' + """.format(filters.get("lost_reason")) return join diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js index 7cd1710a7f2..5c449284ab6 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -3,26 +3,25 @@ /* eslint-disable */ frappe.query_reports["Opportunity Summary by Sales Stage"] = { - "filters": [ + filters: [ { fieldname: "based_on", label: __("Based On"), fieldtype: "Select", options: "Opportunity Owner\nSource\nOpportunity Type", - default: "Opportunity Owner" + default: "Opportunity Owner", }, { fieldname: "data_based_on", label: __("Data Based On"), fieldtype: "Select", options: "Number\nAmount", - default: "Number" + default: "Number", }, { fieldname: "from_date", label: __("From Date"), fieldtype: "Date", - }, { fieldname: "to_date", @@ -33,14 +32,14 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", - get_data: function() { + get_data: function () { return [ - {value: "Open", description: "Status"}, - {value: "Converted", description: "Status"}, - {value: "Quotation", description: "Status"}, - {value: "Replied", description: "Status"} - ] - } + { value: "Open", description: "Status" }, + { value: "Converted", description: "Status" }, + { value: "Quotation", description: "Status" }, + { value: "Replied", description: "Status" }, + ]; + }, }, { fieldname: "opportunity_source", @@ -59,7 +58,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { label: __("Company"), fieldtype: "Link", options: "Company", - default: frappe.defaults.get_user_default("Company") - } - ] + default: frappe.defaults.get_user_default("Company"), + }, + ], }; diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 809311777b7..5a36c999747 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -14,7 +14,7 @@ def execute(filters=None): return OpportunitySummaryBySalesStage(filters).run() -class OpportunitySummaryBySalesStage(object): +class OpportunitySummaryBySalesStage: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -199,7 +199,6 @@ class OpportunitySummaryBySalesStage(object): return filters def get_chart_data(self): - labels = [] datasets = [] values = [0] * len(self.sales_stage_list) diff --git a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.js b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.js index 6f37719f639..d6259414823 100644 --- a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.js +++ b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.js @@ -2,24 +2,24 @@ // For license information, please see license.txt frappe.query_reports["Prospects Engaged But Not Converted"] = { - "filters": [ + filters: [ { - "fieldname": "lead", - "label": __("Lead"), - "fieldtype": "Link", - "options": "Lead" + fieldname: "lead", + label: __("Lead"), + fieldtype: "Link", + options: "Lead", }, { - "fieldname": "no_of_interaction", - "label": __("Number of Interaction"), - "fieldtype": "Int", - "default": 1 + fieldname: "no_of_interaction", + label: __("Number of Interaction"), + fieldtype: "Int", + default: 1, }, { - "fieldname": "lead_age", - "label": __("Minimum Lead Age (Days)"), - "fieldtype": "Int", - "default": 60 + fieldname: "lead_age", + label: __("Minimum Lead Age (Days)"), + fieldtype: "Int", + default: 60, }, - ] -} + ], +}; diff --git a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py index 50c42efe3c5..39b49b20f51 100644 --- a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py +++ b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py @@ -62,9 +62,7 @@ def get_data(filters): lead_details = [] lead_filters = get_lead_filters(filters) - for lead in frappe.get_all( - "Lead", fields=["name", "lead_name", "company_name"], filters=lead_filters - ): + for lead in frappe.get_all("Lead", fields=["name", "lead_name", "company_name"], filters=lead_filters): data = frappe.db.sql( """ select @@ -90,7 +88,7 @@ def get_data(filters): ) for lead_info in data: - lead_data = [lead.name, lead.lead_name, lead.company_name] + list(lead_info) + lead_data = [lead.name, lead.lead_name, lead.company_name, *list(lead_info)] lead_details.append(lead_data) return lead_details diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js index 1426f4b6fd2..149a97a907a 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js @@ -3,68 +3,68 @@ /* eslint-disable */ frappe.query_reports["Sales Pipeline Analytics"] = { - "filters": [ + filters: [ { fieldname: "pipeline_by", label: __("Pipeline By"), fieldtype: "Select", options: "Owner\nSales Stage", - default: "Owner" + default: "Owner", }, { fieldname: "from_date", label: __("From Date"), - fieldtype: "Date" + fieldtype: "Date", }, { fieldname: "to_date", label: __("To Date"), - fieldtype: "Date" + fieldtype: "Date", }, { fieldname: "range", label: __("Range"), fieldtype: "Select", options: "Monthly\nQuarterly", - default: "Monthly" + default: "Monthly", }, { fieldname: "assigned_to", label: __("Assigned To"), fieldtype: "Link", - options: "User" + options: "User", }, { fieldname: "status", label: __("Status"), fieldtype: "Select", - options: "Open\nQuotation\nConverted\nReplied" + options: "Open\nQuotation\nConverted\nReplied", }, { fieldname: "based_on", label: __("Based On"), fieldtype: "Select", options: "Number\nAmount", - default: "Number" + default: "Number", }, { fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", - default: frappe.defaults.get_user_default("Company") + default: frappe.defaults.get_user_default("Company"), }, { fieldname: "opportunity_source", label: __("Opportunity Source"), fieldtype: "Link", - options: "Lead Source" + options: "Lead Source", }, { fieldname: "opportunity_type", label: __("Opportunity Type"), fieldtype: "Link", - options: "Opportunity Type" + options: "Opportunity Type", }, - ] + ], }; diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index dea3f2dd36d..9cc69d24a2b 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -17,7 +17,7 @@ def execute(filters=None): return SalesPipelineAnalytics(filters).run() -class SalesPipelineAnalytics(object): +class SalesPipelineAnalytics: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -41,7 +41,9 @@ class SalesPipelineAnalytics(object): month_list = self.get_month_list() for month in month_list: - self.columns.append({"fieldname": month, "fieldtype": based_on, "label": month, "width": 200}) + self.columns.append( + {"fieldname": month, "fieldtype": based_on, "label": _(month), "width": 200} + ) elif self.filters.get("range") == "Quarterly": for quarter in range(1, 5): @@ -96,7 +98,7 @@ class SalesPipelineAnalytics(object): "Opportunity", filters=self.get_conditions(), fields=[self.based_on, self.data_based_on, self.duration], - group_by="{},{}".format(self.group_by_based_on, self.group_by_period), + group_by=f"{self.group_by_based_on},{self.group_by_period}", order_by=self.group_by_period, ) @@ -156,7 +158,7 @@ class SalesPipelineAnalytics(object): for column in self.columns: if column["fieldname"] != "opportunity_owner" and column["fieldname"] != "sales_stage": - labels.append(column["fieldname"]) + labels.append(_(column["fieldname"])) self.chart = {"data": {"labels": labels, "datasets": datasets}, "type": "line"} @@ -228,7 +230,7 @@ class SalesPipelineAnalytics(object): current_date = date.today() month_number = date.today().month - for month in range(month_number, 13): + for _month in range(month_number, 13): month_list.append(current_date.strftime("%B")) current_date = current_date + relativedelta(months=1) diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py index 737452021c0..c543c387c03 100644 --- a/erpnext/crm/utils.py +++ b/erpnext/crm/utils.py @@ -94,9 +94,7 @@ def get_linked_prospect(reference_doctype, reference_name): "Opportunity", reference_name, ["opportunity_from", "party_name"] ) if opportunity_from == "Lead": - prospect = frappe.db.get_value( - "Prospect Opportunity", {"opportunity": reference_name}, "parent" - ) + prospect = frappe.db.get_value("Prospect Opportunity", {"opportunity": reference_name}, "parent") if opportunity_from == "Prospect": prospect = party_name diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py index bfada0faa7a..e2cccda2ba6 100644 --- a/erpnext/e_commerce/api.py +++ b/erpnext/e_commerce/api.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js index c37fa2f6eae..d533eb82450 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js @@ -2,57 +2,58 @@ // For license information, please see license.txt frappe.ui.form.on("E Commerce Settings", { - onload: function(frm) { - if(frm.doc.__onload && frm.doc.__onload.quotation_series) { + onload: function (frm) { + if (frm.doc.__onload && frm.doc.__onload.quotation_series) { frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series; frm.refresh_field("quotation_series"); } - frm.set_query('payment_gateway_account', function() { - return { 'filters': { 'payment_channel': "Email" } }; + frm.set_query("payment_gateway_account", function () { + return { filters: { payment_channel: "Email" } }; }); }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.enabled) { - frm.get_field('store_page_docs').$wrapper.removeClass('hide-control').html( - `
    ${__("Follow these steps to create a landing page for your store")}: + frm.get_field("store_page_docs") + .$wrapper.removeClass("hide-control") + .html( + `
    ${__("Follow these steps to create a landing page for your store")}: docs/store-landing-page
    ` - ); + ); } frappe.model.with_doctype("Website Item", () => { - const web_item_meta = frappe.get_meta('Website Item'); + const web_item_meta = frappe.get_meta("Website Item"); - const valid_fields = web_item_meta.fields.filter(df => - ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden - ).map(df => - ({ label: df.label, value: df.fieldname }) - ); + const valid_fields = web_item_meta.fields + .filter((df) => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden) + .map((df) => ({ label: df.label, value: df.fieldname })); frm.get_field("filter_fields").grid.update_docfield_property( - 'fieldname', 'options', valid_fields + "fieldname", + "options", + valid_fields ); }); }, - enabled: function(frm) { + enabled: function (frm) { if (frm.doc.enabled === 1) { - frm.set_value('enable_variants', 1); - } - else { - frm.set_value('company', ''); - frm.set_value('price_list', ''); - frm.set_value('default_customer_group', ''); - frm.set_value('quotation_series', ''); + frm.set_value("enable_variants", 1); + } else { + frm.set_value("company", ""); + frm.set_value("price_list", ""); + frm.set_value("default_customer_group", ""); + frm.set_value("quotation_series", ""); } }, - enable_checkout: function(frm) { + enable_checkout: function (frm) { if (frm.doc.enable_checkout) { erpnext.utils.check_payments_app(); } - } + }, }); diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index e6f08f708a8..31b3197e12d 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -31,6 +31,7 @@ "column_break_21", "default_customer_group", "quotation_series", + "show_actual_qty", "checkout_settings_section", "enable_checkout", "show_price_in_quotation", @@ -366,12 +367,19 @@ "fieldtype": "Check", "label": "Enable Redisearch", "read_only_depends_on": "eval:!doc.is_redisearch_loaded" + }, + { + "default": "1", + "description": "If enabled Actual Qty will be shown as In Stock on the product page instead of Projected Qty.", + "fieldname": "show_actual_qty", + "fieldtype": "Check", + "label": "Show Actual Qty" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-04-01 18:35:56.106756", + "modified": "2024-01-10 21:06:45.386977", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py index c27d29a62cd..14798e0d3cd 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt diff --git a/erpnext/e_commerce/doctype/item_review/item_review.js b/erpnext/e_commerce/doctype/item_review/item_review.js index a57c370287b..69fc1cfff4e 100644 --- a/erpnext/e_commerce/doctype/item_review/item_review.js +++ b/erpnext/e_commerce/doctype/item_review/item_review.js @@ -1,8 +1,7 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Item Review', { +frappe.ui.form.on("Item Review", { // refresh: function(frm) { - // } }); diff --git a/erpnext/e_commerce/doctype/item_review/item_review.py b/erpnext/e_commerce/doctype/item_review/item_review.py index 3e540e38853..5fe7d4d8fdb 100644 --- a/erpnext/e_commerce/doctype/item_review/item_review.py +++ b/erpnext/e_commerce/doctype/item_review/item_review.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt diff --git a/erpnext/e_commerce/doctype/item_review/test_item_review.py b/erpnext/e_commerce/doctype/item_review/test_item_review.py index 8a4befc800a..c3f76623c9c 100644 --- a/erpnext/e_commerce/doctype/item_review/test_item_review.py +++ b/erpnext/e_commerce/doctype/item_review/test_item_review.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe from frappe.core.doctype.user_permission.test_user_permission import create_user +from frappe.tests.utils import FrappeTestCase from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import ( setup_e_commerce_settings, @@ -19,7 +18,7 @@ from erpnext.e_commerce.shopping_cart.cart import get_party from erpnext.stock.doctype.item.test_item import make_item -class TestItemReview(unittest.TestCase): +class TestItemReview(FrappeTestCase): def setUp(self): item = make_item("Test Mobile Phone") if not frappe.db.exists("Website Item", {"item_code": "Test Mobile Phone"}): @@ -29,8 +28,7 @@ class TestItemReview(unittest.TestCase): frappe.local.shopping_cart_settings = None def tearDown(self): - frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete() - setup_e_commerce_settings({"enable_reviews": 0}) + frappe.db.rollback() def test_add_and_get_item_reviews_from_customer(self): "Add / Get Reviews from a User that is a valid customer (has added to cart or purchased in the past)" @@ -44,7 +42,7 @@ class TestItemReview(unittest.TestCase): # post review on "Test Mobile Phone" try: - add_item_review(web_item, "Great Product", 3, "Would recommend this product") + add_item_review(web_item, "Great Product", 1, "Would recommend this product") review_name = frappe.db.get_value("Item Review", {"website_item": web_item}) except Exception: self.fail(f"Error while publishing review for {web_item}") @@ -52,8 +50,7 @@ class TestItemReview(unittest.TestCase): review_data = get_item_reviews(web_item, 0, 10) self.assertEqual(len(review_data.reviews), 1) - self.assertEqual(review_data.average_rating, 3) - self.assertEqual(review_data.reviews_per_rating[2], 100) + self.assertEqual(review_data.average_rating, 1) # tear down frappe.set_user("Administrator") diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py index 8eebfdb83af..86ca1f2cb61 100644 --- a/erpnext/e_commerce/doctype/website_item/test_website_item.py +++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe @@ -24,10 +22,11 @@ WEBITEM_PRICE_TESTS = ( "test_website_item_price_for_guest_user", ) +from frappe.tests.utils import FrappeTestCase -class TestWebsiteItem(unittest.TestCase): - @classmethod - def setUpClass(cls): + +class TestWebsiteItem(FrappeTestCase): + def setUp(self): setup_e_commerce_settings( { "company": "_Test Company", @@ -37,11 +36,6 @@ class TestWebsiteItem(unittest.TestCase): } ) - @classmethod - def tearDownClass(cls): - frappe.db.rollback() - - def setUp(self): if self._testMethodName in WEBITEM_DESK_TESTS: make_item( "Test Web Item", @@ -75,6 +69,9 @@ class TestWebsiteItem(unittest.TestCase): customer="_Test Customer", ) + def tearDown(self): + frappe.db.rollback() + def test_index_creation(self): "Check if index is getting created in db." from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update @@ -348,9 +345,7 @@ class TestWebsiteItem(unittest.TestCase): ) # stock up item - stock_entry = make_stock_entry( - item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100 - ) + stock_entry = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100) # check if stock details are fetched and item is in stock with warehouse set data = get_product_info_for_website(item_code, skip_quotation_creation=True) @@ -430,9 +425,7 @@ class TestWebsiteItem(unittest.TestCase): web_item = create_regular_web_item(item_code) # price visible to guests - setup_e_commerce_settings( - {"enable_recommendations": 1, "show_price": 1, "hide_price_for_guest": 0} - ) + setup_e_commerce_settings({"enable_recommendations": 1, "show_price": 1, "hide_price_for_guest": 0}) # create recommended web item and price for it recommended_web_item = create_regular_web_item("Test Mobile Phone 1") diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js index b6595cce8a9..74dd7b8c90f 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.js +++ b/erpnext/e_commerce/doctype/website_item/website_item.js @@ -1,37 +1,49 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Website Item', { +frappe.ui.form.on("Website Item", { onload: (frm) => { // should never check Private frm.fields_dict["website_image"].df.is_private = 0; }, refresh: (frm) => { - frm.add_custom_button(__("Prices"), function() { - frappe.set_route("List", "Item Price", {"item_code": frm.doc.item_code}); - }, __("View")); + frm.add_custom_button( + __("Prices"), + function () { + frappe.set_route("List", "Item Price", { item_code: frm.doc.item_code }); + }, + __("View") + ); - frm.add_custom_button(__("Stock"), function() { - frappe.route_options = { - "item_code": frm.doc.item_code - }; - frappe.set_route("query-report", "Stock Balance"); - }, __("View")); + frm.add_custom_button( + __("Stock"), + function () { + frappe.route_options = { + item_code: frm.doc.item_code, + }; + frappe.set_route("query-report", "Stock Balance"); + }, + __("View") + ); - frm.add_custom_button(__("E Commerce Settings"), function() { - frappe.set_route("Form", "E Commerce Settings"); - }, __("View")); + frm.add_custom_button( + __("E Commerce Settings"), + function () { + frappe.set_route("Form", "E Commerce Settings"); + }, + __("View") + ); }, copy_from_item_group: (frm) => { return frm.call({ doc: frm.doc, - method: "copy_specification_from_item_group" + method: "copy_specification_from_item_group", }); }, set_meta_tags: (frm) => { frappe.utils.set_meta_tag(frm.doc.route); - } + }, }); diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 81b8ecab48e..f7637cb3553 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt import json -from typing import TYPE_CHECKING, List, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: from erpnext.stock.doctype.item.item import Item @@ -45,10 +44,10 @@ class WebsiteItem(WebsiteGenerator): self.name = make_autoname(naming_series, doc=self) def onload(self): - super(WebsiteItem, self).onload() + super().onload() def validate(self): - super(WebsiteItem, self).validate() + super().validate() if not self.item_code: frappe.throw(_("Item Code is required"), title=_("Mandatory")) @@ -78,7 +77,7 @@ class WebsiteItem(WebsiteGenerator): self.update_template_item() def on_trash(self): - super(WebsiteItem, self).on_trash() + super().on_trash() delete_item_from_index(self) self.publish_unpublish_desk_item(publish=False) @@ -197,7 +196,7 @@ class WebsiteItem(WebsiteGenerator): } ).save() - except IOError: + except OSError: self.website_image = None if file_doc: @@ -234,9 +233,7 @@ class WebsiteItem(WebsiteGenerator): context.reviews = context.reviews[:4] context.wished = False - if frappe.db.exists( - "Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user} - ): + if frappe.db.exists("Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user}): context.wished = True context.user_is_customer = check_if_user_is_customer() @@ -256,9 +253,7 @@ class WebsiteItem(WebsiteGenerator): ) # make an attribute-value map for easier access in templates - variant.attribute_map = frappe._dict( - {attr.attribute: attr.value for attr in variant.attributes} - ) + variant.attribute_map = frappe._dict({attr.attribute: attr.value for attr in variant.attributes}) for attr in variant.attributes: values = attribute_values_available.setdefault(attr.attribute, []) @@ -283,7 +278,6 @@ class WebsiteItem(WebsiteGenerator): filters={"parent": attr.attribute}, order_by="idx asc", ): - if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): values.append(attr_value.attribute_value) @@ -311,9 +305,7 @@ class WebsiteItem(WebsiteGenerator): def set_shopping_cart_data(self, context): from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website - context.shopping_cart = get_product_info_for_website( - self.item_code, skip_quotation_creation=True - ) + context.shopping_cart = get_product_info_for_website(self.item_code, skip_quotation_creation=True) @frappe.whitelist() def copy_specification_from_item_group(self): @@ -425,7 +417,7 @@ def check_if_user_is_customer(user=None): @frappe.whitelist() -def make_website_item(doc: "Item", save: bool = True) -> Union["WebsiteItem", List[str]]: +def make_website_item(doc: "Item", save: bool = True) -> Union["WebsiteItem", list[str]]: "Make Website Item from Item. Used via Form UI or patch." if not doc: diff --git a/erpnext/e_commerce/doctype/website_item/website_item_list.js b/erpnext/e_commerce/doctype/website_item/website_item_list.js index b9dd9214a38..220746c9845 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item_list.js +++ b/erpnext/e_commerce/doctype/website_item/website_item_list.js @@ -1,20 +1,20 @@ -frappe.listview_settings['Website Item'] = { +frappe.listview_settings["Website Item"] = { add_fields: ["item_name", "web_item_name", "published", "website_image", "has_variants", "variant_of"], filters: [["published", "=", "1"]], - get_indicator: function(doc) { + get_indicator: function (doc) { if (doc.has_variants && doc.published) { return [__("Template"), "orange", "has_variants,=,Yes|published,=,1"]; } else if (doc.has_variants && !doc.published) { return [__("Template"), "grey", "has_variants,=,Yes|published,=,0"]; - } else if (doc.variant_of && doc.published) { + } else if (doc.variant_of && doc.published) { return [__("Variant"), "blue", "published,=,1|variant_of,=," + doc.variant_of]; - } else if (doc.variant_of && !doc.published) { + } else if (doc.variant_of && !doc.published) { return [__("Variant"), "grey", "published,=,0|variant_of,=," + doc.variant_of]; } else if (doc.published) { return [__("Published"), "green", "published,=,1"]; } else { return [__("Not Published"), "grey", "published,=,0"]; } - } -}; \ No newline at end of file + }, +}; diff --git a/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py b/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py index 91148b8b048..ebe016cfab2 100644 --- a/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py +++ b/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt diff --git a/erpnext/e_commerce/doctype/website_offer/website_offer.py b/erpnext/e_commerce/doctype/website_offer/website_offer.py index 8c92f75a1e9..d64254ac9bd 100644 --- a/erpnext/e_commerce/doctype/website_offer/website_offer.py +++ b/erpnext/e_commerce/doctype/website_offer/website_offer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt diff --git a/erpnext/e_commerce/doctype/wishlist/test_wishlist.py b/erpnext/e_commerce/doctype/wishlist/test_wishlist.py index 9d27126fdb2..b551f923dac 100644 --- a/erpnext/e_commerce/doctype/wishlist/test_wishlist.py +++ b/erpnext/e_commerce/doctype/wishlist/test_wishlist.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt import unittest @@ -42,17 +41,13 @@ class TestWishlist(unittest.TestCase): # add second item to wishlist add_to_wishlist("Test Phone Series Y") - wishlist_length = frappe.db.get_value( - "Wishlist Item", {"parent": frappe.session.user}, "count(*)" - ) + wishlist_length = frappe.db.get_value("Wishlist Item", {"parent": frappe.session.user}, "count(*)") self.assertEqual(wishlist_length, 2) remove_from_wishlist("Test Phone Series X") remove_from_wishlist("Test Phone Series Y") - wishlist_length = frappe.db.get_value( - "Wishlist Item", {"parent": frappe.session.user}, "count(*)" - ) + wishlist_length = frappe.db.get_value("Wishlist Item", {"parent": frappe.session.user}, "count(*)") self.assertIsNone(frappe.db.exists("Wishlist Item", {"parent": frappe.session.user})) self.assertEqual(wishlist_length, 0) @@ -75,9 +70,7 @@ class TestWishlist(unittest.TestCase): # check wishlist and its content for users self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user.name})) self.assertTrue( - frappe.db.exists( - "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name} - ) + frappe.db.exists("Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name}) ) self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user_1.name})) @@ -97,18 +90,14 @@ class TestWishlist(unittest.TestCase): ) ) self.assertTrue( - frappe.db.exists( - "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name} - ) + frappe.db.exists("Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name}) ) # remove item for first user frappe.set_user(test_user.name) remove_from_wishlist("Test Phone Series X") self.assertFalse( - frappe.db.exists( - "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name} - ) + frappe.db.exists("Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name}) ) # tear down diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.js b/erpnext/e_commerce/doctype/wishlist/wishlist.js index d96e552ecdb..54a0a2381b1 100644 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.js +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.js @@ -1,8 +1,7 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Wishlist', { +frappe.ui.form.on("Wishlist", { // refresh: function(frm) { - // } }); diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py index eb74027d77d..86672c91d7a 100644 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.py +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt diff --git a/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py b/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py index 75ebccbc1b7..99f0fcd3475 100644 --- a/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py +++ b/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt diff --git a/erpnext/e_commerce/legacy_search.py b/erpnext/e_commerce/legacy_search.py index ef8e86d4428..9d36bfd5851 100644 --- a/erpnext/e_commerce/legacy_search.py +++ b/erpnext/e_commerce/legacy_search.py @@ -114,9 +114,7 @@ class ProductSearch(FullTextSearch): def get_all_published_items(): - return frappe.get_all( - "Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code" - ) + return frappe.get_all("Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code") def update_index_for_path(path): diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py index e5e5e97f86a..7e2904793e2 100644 --- a/erpnext/e_commerce/product_data_engine/filters.py +++ b/erpnext/e_commerce/product_data_engine/filters.py @@ -24,9 +24,7 @@ class ProductFiltersBuilder: # filter valid field filters i.e. those that exist in Website Item web_item_meta = frappe.get_meta("Website Item", cached=True) - fields = [ - web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field) - ] + fields = [web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field)] for df in fields: item_filters, item_or_filters = {"published": 1}, [] @@ -41,14 +39,24 @@ class ProductFiltersBuilder: item_or_filters.extend( [ ["item_group", "in", include_groups], - ["Website Item Group", "item_group", "=", self.item_group], # consider website item groups + [ + "Website Item Group", + "item_group", + "=", + self.item_group, + ], # consider website item groups ] ) else: item_or_filters.extend( [ ["item_group", "=", self.item_group], - ["Website Item Group", "item_group", "=", self.item_group], # consider website item groups + [ + "Website Item Group", + "item_group", + "=", + self.item_group, + ], # consider website item groups ] ) diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index 975f87608a6..1c1275051e2 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -56,7 +56,7 @@ class ProductQuery: """ # track if discounts included in field filters self.filter_with_discount = bool(fields.get("discount")) - result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0 + result, discount_list, _website_item_groups, cart_items, count = [], [], [], [], 0 if fields: self.build_fields_filters(fields) @@ -215,7 +215,7 @@ class ProductQuery: search_fields.discard("web_long_description") # Build or filters for query - search = "%{}%".format(search_term) + search = f"%{search_term}%" for field in search_fields: self.or_filters.append([field, "like", search]) diff --git a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py index c3b6ed5da25..76cabfffc87 100644 --- a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py +++ b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py @@ -136,9 +136,7 @@ class TestProductDataEngine(unittest.TestCase): field_filters = {"item_group": "Raw Material"} engine = ProductQuery() - result = engine.query( - attributes={}, fields=field_filters, search_term=None, start=0, item_group=None - ) + result = engine.query(attributes={}, fields=field_filters, search_term=None, start=0, item_group=None) items = result.get("items") # check if only 'Raw Material' are fetched in the right order @@ -229,9 +227,7 @@ class TestProductDataEngine(unittest.TestCase): frappe.local.shopping_cart_settings = None engine = ProductQuery() - result = engine.query( - attributes={}, fields=field_filters, search_term=None, start=0, item_group=None - ) + result = engine.query(attributes={}, fields=field_filters, search_term=None, start=0, item_group=None) items = result.get("items") # check if only product with 10% and below discount are fetched @@ -293,12 +289,8 @@ class TestProductDataEngine(unittest.TestCase): ), ) - frappe.db.set_value( - "Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier" - ) - frappe.db.set_value( - "Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1" - ) + frappe.db.set_value("Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier") + frappe.db.set_value("Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1") settings = frappe.get_doc("E Commerce Settings") settings.append("filter_fields", {"fieldname": "supplier"}) @@ -316,9 +308,7 @@ class TestProductDataEngine(unittest.TestCase): # test if custom filter works in query field_filters = {"supplier": "_Test Supplier 1"} engine = ProductQuery() - result = engine.query( - attributes={}, fields=field_filters, search_term=None, start=0, item_group=None - ) + result = engine.query(attributes={}, fields=field_filters, search_term=None, start=0, item_group=None) items = result.get("items") # check if only 'Raw Material' are fetched in the right order diff --git a/erpnext/e_commerce/product_ui/grid.js b/erpnext/e_commerce/product_ui/grid.js index 20a6c30b52b..1f0d1fbf43a 100644 --- a/erpnext/e_commerce/product_ui/grid.js +++ b/erpnext/e_commerce/product_ui/grid.js @@ -20,9 +20,9 @@ erpnext.ProductGrid = class { let me = this; let html = ``; - this.items.forEach(item => { + this.items.forEach((item) => { let title = item.web_item_name || item.item_name || item.item_code || ""; - title = title.length > 90 ? title.substr(0, 90) + "..." : title; + title = title.length > 90 ? title.substr(0, 90) + "..." : title; html += `
    `; html += me.get_image_html(item, title); @@ -40,17 +40,17 @@ erpnext.ProductGrid = class { if (image) { return ` `; } else { return ` @@ -73,11 +73,10 @@ erpnext.ProductGrid = class { if (settings.enabled) { body_html += this.get_cart_indicator(item); } - } body_html += `
    `; - body_html += `
    ${ item.item_group || '' }
    `; + body_html += `
    ${item.item_group || ""}
    `; if (item.formatted_price) { body_html += this.get_price_html(item); @@ -92,9 +91,9 @@ erpnext.ProductGrid = class { get_title(item, title) { let title_html = ` - +
    - ${ title || '' } + ${title || ""}
    `; @@ -104,10 +103,10 @@ erpnext.ProductGrid = class { get_wishlist_icon(item) { let icon_class = item.wished ? "wished" : "not-wished"; return ` -
    + let table_footer = + hidden_logs && hidden_logs.length > 0 + ? ` - `: ""; + ` + : ""; if (field === "fixed_error_log_preview") { rows_head = ` - ` - table_caption = "Resolved Issues" + `; + table_caption = "Resolved Issues"; } else { rows_head = ` - ` - table_caption = "Error Log" + `; + table_caption = "Error Log"; } frm.get_field(field).$wrapper.html(` @@ -144,7 +152,7 @@ frappe.ui.form.on("Tally Migration", { summary[row.doc.doctype] = 1; } } - return summary + return summary; }, {}); console.table(summary); }, @@ -177,7 +185,7 @@ frappe.ui.form.on("Tally Migration", { let hidden_logs = completed_log.slice(20); frm.events.render_html_table(frm, logs, hidden_logs, "fixed_error_log_preview"); - } + }, }); erpnext.tally_migration.getError = (traceback) => { @@ -186,31 +194,33 @@ erpnext.tally_migration.getError = (traceback) => { let message; if (is_multiline) { - let exc_error_idx = traceback.trim().lastIndexOf("\n") + 1 - let error_line = traceback.substr(exc_error_idx) - let split_str_idx = (error_line.indexOf(':') > 0) ? error_line.indexOf(':') + 1 : 0; + let exc_error_idx = traceback.trim().lastIndexOf("\n") + 1; + let error_line = traceback.substr(exc_error_idx); + let split_str_idx = error_line.indexOf(":") > 0 ? error_line.indexOf(":") + 1 : 0; message = error_line.slice(split_str_idx).trim(); } else { message = traceback; } - return message -} + return message; +}; erpnext.tally_migration.cleanDoc = (obj) => { /* Strips all null and empty values of your JSON object */ let temp = obj; - $.each(temp, function(key, value){ - if (value === "" || value === null){ + $.each(temp, function (key, value) { + if (value === "" || value === null) { delete obj[key]; - } else if (Object.prototype.toString.call(value) === '[object Object]') { + } else if (Object.prototype.toString.call(value) === "[object Object]") { erpnext.tally_migration.cleanDoc(value); } else if ($.isArray(value)) { - $.each(value, function (k,v) { erpnext.tally_migration.cleanDoc(v); }); + $.each(value, function (k, v) { + erpnext.tally_migration.cleanDoc(v); + }); } }); return temp; -} +}; erpnext.tally_migration.unresolve = (document) => { /* Mark document migration as unresolved ie. move to failed error log */ @@ -218,9 +228,9 @@ erpnext.tally_migration.unresolve = (document) => { let failed_log = erpnext.tally_migration.failed_import_log; let fixed_log = erpnext.tally_migration.fixed_errors_log; - let modified_fixed_log = fixed_log.filter(row => { + let modified_fixed_log = fixed_log.filter((row) => { if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) { - return row + return row; } }); @@ -231,7 +241,7 @@ erpnext.tally_migration.unresolve = (document) => { frm.dirty(); frm.save(); -} +}; erpnext.tally_migration.resolve = (document) => { /* Mark document migration as resolved ie. move to fixed error log */ @@ -239,9 +249,9 @@ erpnext.tally_migration.resolve = (document) => { let failed_log = erpnext.tally_migration.failed_import_log; let fixed_log = erpnext.tally_migration.fixed_errors_log; - let modified_failed_log = failed_log.filter(row => { + let modified_failed_log = failed_log.filter((row) => { if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) { - return row + return row; } }); fixed_log.push({ doc: document, exc: `Solved on ${Date()}` }); @@ -251,27 +261,27 @@ erpnext.tally_migration.resolve = (document) => { frm.dirty(); frm.save(); -} +}; erpnext.tally_migration.create_new_doc = (document) => { /* Mark as resolved and create new document */ erpnext.tally_migration.resolve(document); return frappe.call({ type: "POST", - method: 'erpnext.erpnext_integrations.doctype.tally_migration.tally_migration.new_doc', + method: "erpnext.erpnext_integrations.doctype.tally_migration.tally_migration.new_doc", args: { - document + document, }, freeze: true, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { frappe.model.sync(r.message); frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = true; frappe.set_route("Form", r.message.doctype, r.message.name); } - } + }, }); -} +}; erpnext.tally_migration.get_html_rows = (logs, field) => { let index = 0; @@ -304,14 +314,18 @@ erpnext.tally_migration.get_html_rows = (logs, field) => { `; let create_button = ` - ` + `; let mark_as_unresolved = ` - ` + `; if (field === "fixed_error_log_preview") { return ` @@ -343,7 +357,8 @@ erpnext.tally_migration.get_html_rows = (logs, field) => { `; } - }).join(""); + }) + .join(""); - return rows -} + return rows; +}; diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index e6840f505be..0a27a647129 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -96,9 +96,7 @@ class TallyMigration(Document): self.default_cost_center, self.default_round_off_account = frappe.db.get_value( "Company", self.erpnext_company, ["cost_center", "round_off_account"] ) - self.default_warehouse = frappe.db.get_value( - "Stock Settings", "Stock Settings", "default_warehouse" - ) + self.default_warehouse = frappe.db.get_value("Stock Settings", "Stock Settings", "default_warehouse") def _process_master_data(self): def get_company_name(collection): @@ -159,7 +157,7 @@ class TallyMigration(Document): def get_children_and_parent_dict(accounts): children, parents = {}, {} - for parent, account, is_group in accounts: + for parent, account, _is_group in accounts: children.setdefault(parent, set()).add(account) parents.setdefault(account, set()).add(parent) parents[account].update(parents.get(parent, [])) @@ -204,7 +202,9 @@ class TallyMigration(Document): { "doctype": party_type, "customer_name": account.NAME.string.strip(), - "tax_id": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None, + "tax_id": account.INCOMETAXNUMBER.string.strip() + if account.INCOMETAXNUMBER + else None, "customer_group": "All Customer Groups", "territory": "All Territories", "customer_type": "Individual", @@ -218,7 +218,9 @@ class TallyMigration(Document): { "doctype": party_type, "supplier_name": account.NAME.string.strip(), - "pan": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None, + "pan": account.INCOMETAXNUMBER.string.strip() + if account.INCOMETAXNUMBER + else None, "supplier_group": "All Supplier Groups", "supplier_type": "Individual", } @@ -234,7 +236,9 @@ class TallyMigration(Document): "address_line2": address[140:].strip(), "country": account.COUNTRYNAME.string.strip() if account.COUNTRYNAME else None, "state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None, - "gst_state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None, + "gst_state": account.LEDSTATENAME.string.strip() + if account.LEDSTATENAME + else None, "pin_code": account.PINCODE.string.strip() if account.PINCODE else None, "mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, "phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, @@ -576,7 +580,7 @@ class TallyMigration(Document): if new_year.year_start_date.year == new_year.year_end_date.year: new_year.year = new_year.year_start_date.year else: - new_year.year = "{}-{}".format(new_year.year_start_date.year, new_year.year_end_date.year) + new_year.year = f"{new_year.year_start_date.year}-{new_year.year_end_date.year}" new_year.save() oldest_year = new_year diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js index 2925db82e33..eb944d9f279 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js @@ -1,37 +1,35 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('TaxJar Settings', { +frappe.ui.form.on("TaxJar Settings", { is_sandbox: (frm) => { frm.toggle_reqd("api_key", !frm.doc.is_sandbox); frm.toggle_reqd("sandbox_api_key", frm.doc.is_sandbox); }, on_load: (frm) => { - frm.set_query('shipping_account_head', function() { + frm.set_query("shipping_account_head", function () { return { filters: { - 'company': frm.doc.company - } + company: frm.doc.company, + }, }; }); - frm.set_query('tax_account_head', function() { + frm.set_query("tax_account_head", function () { return { filters: { - 'company': frm.doc.company - } + company: frm.doc.company, + }, }; }); }, refresh: (frm) => { - frm.add_custom_button(__('Update Nexus List'), function() { + frm.add_custom_button(__("Update Nexus List"), function () { frm.call({ doc: frm.doc, - method: 'update_nexus_list' + method: "update_nexus_list", }); }); }, - - }); diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py index 2148863c556..e649707c806 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py @@ -23,9 +23,7 @@ class TaxJarSettings(Document): "Custom Field", {"dt": ("in", ["Item", "Sales Invoice Item"]), "fieldname": "product_tax_category"}, ) - fields_hidden = frappe.get_value( - "Custom Field", {"dt": ("in", ["Sales Invoice Item"])}, "hidden" - ) + fields_hidden = frappe.get_value("Custom Field", {"dt": ("in", ["Sales Invoice Item"])}, "hidden") if TAXJAR_CREATE_TRANSACTIONS or TAXJAR_CALCULATE_TAX or TAXJAR_SANDBOX_MODE: if not fields_already_exist: @@ -70,13 +68,11 @@ def toggle_tax_category_fields(hidden): "hidden", hidden, ) - frappe.set_value( - "Custom Field", {"dt": "Item", "fieldname": "product_tax_category"}, "hidden", hidden - ) + frappe.set_value("Custom Field", {"dt": "Item", "fieldname": "product_tax_category"}, "hidden", hidden) def add_product_tax_categories(): - with open(os.path.join(os.path.dirname(__file__), "product_tax_category_data.json"), "r") as f: + with open(os.path.join(os.path.dirname(__file__), "product_tax_category_data.json")) as f: tax_categories = json.loads(f.read()) create_tax_categories(tax_categories["categories"]) diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js index d7a3d36a5f1..dc3941b3073 100644 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js @@ -1,47 +1,47 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Woocommerce Settings', { - refresh (frm) { +frappe.ui.form.on("Woocommerce Settings", { + refresh(frm) { frm.trigger("add_button_generate_secret"); frm.trigger("check_enabled"); - frm.set_query("tax_account", ()=>{ + frm.set_query("tax_account", () => { return { - "filters": { - "company": frappe.defaults.get_default("company"), - "is_group": 0 - } + filters: { + company: frappe.defaults.get_default("company"), + is_group: 0, + }, }; }); }, - enable_sync (frm) { + enable_sync(frm) { frm.trigger("check_enabled"); }, add_button_generate_secret(frm) { - frm.add_custom_button(__('Generate Secret'), () => { - frappe.confirm( - __("Apps using current key won't be able to access, are you sure?"), - () => { - frappe.call({ - type:"POST", - method:"erpnext.erpnext_integrations.doctype.woocommerce_settings.woocommerce_settings.generate_secret", - }).done(() => { + frm.add_custom_button(__("Generate Secret"), () => { + frappe.confirm(__("Apps using current key won't be able to access, are you sure?"), () => { + frappe + .call({ + type: "POST", + method: "erpnext.erpnext_integrations.doctype.woocommerce_settings.woocommerce_settings.generate_secret", + }) + .done(() => { frm.reload_doc(); - }).fail(() => { + }) + .fail(() => { frappe.msgprint(__("Could not generate Secret")); }); - } - ); + }); }); }, - check_enabled (frm) { + check_enabled(frm) { frm.set_df_property("woocommerce_server_url", "reqd", frm.doc.enable_sync); frm.set_df_property("api_consumer_key", "reqd", frm.doc.enable_sync); frm.set_df_property("api_consumer_secret", "reqd", frm.doc.enable_sync); - } + }, }); frappe.ui.form.on("Woocommerce Settings", "onload", function () { @@ -51,6 +51,6 @@ frappe.ui.form.on("Woocommerce Settings", "onload", function () { $.each(r.message, function (key, value) { set_field_options(key, value); }); - } + }, }); }); diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py index 4aa98aab56b..99a39d7da88 100644 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py @@ -67,7 +67,7 @@ class WoocommerceSettings(Document): # for CI Test to work url = "http://localhost:8000" - server_url = "{uri.scheme}://{uri.netloc}".format(uri=urlparse(url)) + server_url = f"{urlparse(url).scheme}://{urlparse(url).netloc}" delivery_url = server_url + endpoint self.endpoint = delivery_url diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index fd0f7835759..a7b7c3ee306 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -23,7 +23,7 @@ def handle_incoming_call(**kwargs): create_call_log(call_payload) else: update_call_log(call_payload, call_log=call_log) - except Exception as e: + except Exception: frappe.db.rollback() exotel_settings.log_error("Error in Exotel incoming call") frappe.db.commit() @@ -86,7 +86,7 @@ def create_call_log(call_payload): @frappe.whitelist() def get_call_status(call_id): - endpoint = get_exotel_endpoint("Calls/{call_id}.json".format(call_id=call_id)) + endpoint = get_exotel_endpoint(f"Calls/{call_id}.json") response = requests.get(endpoint) status = response.json().get("Call", {}).get("Status") return status @@ -95,9 +95,7 @@ def get_call_status(call_id): @frappe.whitelist() def make_a_call(from_number, to_number, caller_id): endpoint = get_exotel_endpoint("Calls/connect.json?details=true") - response = requests.post( - endpoint, data={"From": from_number, "To": to_number, "CallerId": caller_id} - ) + response = requests.post(endpoint, data={"From": from_number, "To": to_number, "CallerId": caller_id}) return response.json() diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py index 2d9093b6e92..0535fc9e4ae 100644 --- a/erpnext/erpnext_integrations/taxjar_integration.py +++ b/erpnext/erpnext_integrations/taxjar_integration.py @@ -113,9 +113,7 @@ def get_client(): def create_transaction(doc, method): - TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value( - "TaxJar Settings", "taxjar_create_transactions" - ) + TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") """Create an order transaction in TaxJar""" @@ -156,9 +154,7 @@ def create_transaction(doc, method): def delete_transaction(doc, method): """Delete an existing TaxJar order transaction""" - TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value( - "TaxJar Settings", "taxjar_create_transactions" - ) + TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") if not TAXJAR_CREATE_TRANSACTIONS: return @@ -258,7 +254,7 @@ def set_sales_tax(doc, method): if not tax_dict: # Remove existing tax rows if address is changed from a taxable state/country - setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD]) + doc.taxes = [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD] return # check if delivering within a nexus @@ -267,7 +263,7 @@ def set_sales_tax(doc, method): tax_data = validate_tax_request(tax_dict) if tax_data is not None: if not tax_data.amount_to_collect: - setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD]) + doc.taxes = [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD] elif tax_data.amount_to_collect > 0: # Loop through tax rows for existing Sales Tax entry # If none are found, add a row with the tax amount diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index 981486eb309..55316f588ac 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -15,7 +15,9 @@ def validate_webhooks_request(doctype, hmac_key, secret_key="secret"): if frappe.request and settings and settings.get(secret_key) and not frappe.flags.in_test: sig = base64.b64encode( - hmac.new(settings.get(secret_key).encode("utf8"), frappe.request.data, hashlib.sha256).digest() + hmac.new( + settings.get(secret_key).encode("utf8"), frappe.request.data, hashlib.sha256 + ).digest() ) if frappe.request.data and not sig == bytes(frappe.get_request_header(hmac_key).encode()): @@ -28,7 +30,7 @@ def validate_webhooks_request(doctype, hmac_key, secret_key="secret"): def get_webhook_address(connector_name, method, exclude_uri=False, force_https=False): - endpoint = "erpnext.erpnext_integrations.connectors.{0}.{1}".format(connector_name, method) + endpoint = f"erpnext.erpnext_integrations.connectors.{connector_name}.{method}" if exclude_uri: return endpoint diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 054f9dedabe..021cf4deb63 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -1,5 +1,3 @@ -from frappe import _ - app_name = "erpnext" app_title = "ERPNext" app_publisher = "Frappe Technologies Pvt. Ltd." @@ -18,7 +16,7 @@ app_include_js = "erpnext.bundle.js" app_include_css = "erpnext.bundle.css" web_include_js = "erpnext-web.bundle.js" web_include_css = "erpnext-web.bundle.css" -email_css = "email_erpnext.bundle.css" +email_css = "erpnext_email.bundle.scss" doctype_js = { "Address": "public/js/address.js", @@ -92,7 +90,7 @@ website_route_rules = [ { "from_route": "/orders/", "to_route": "order", - "defaults": {"doctype": "Sales Order", "parents": [{"label": _("Orders"), "route": "orders"}]}, + "defaults": {"doctype": "Sales Order", "parents": [{"label": "Orders", "route": "orders"}]}, }, {"from_route": "/invoices", "to_route": "Sales Invoice"}, { @@ -100,7 +98,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Sales Invoice", - "parents": [{"label": _("Invoices"), "route": "invoices"}], + "parents": [{"label": "Invoices", "route": "invoices"}], }, }, {"from_route": "/supplier-quotations", "to_route": "Supplier Quotation"}, @@ -109,7 +107,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Supplier Quotation", - "parents": [{"label": _("Supplier Quotation"), "route": "supplier-quotations"}], + "parents": [{"label": "Supplier Quotation", "route": "supplier-quotations"}], }, }, {"from_route": "/purchase-orders", "to_route": "Purchase Order"}, @@ -118,7 +116,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Purchase Order", - "parents": [{"label": _("Purchase Order"), "route": "purchase-orders"}], + "parents": [{"label": "Purchase Order", "route": "purchase-orders"}], }, }, {"from_route": "/purchase-invoices", "to_route": "Purchase Invoice"}, @@ -127,7 +125,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Purchase Invoice", - "parents": [{"label": _("Purchase Invoice"), "route": "purchase-invoices"}], + "parents": [{"label": "Purchase Invoice", "route": "purchase-invoices"}], }, }, {"from_route": "/quotations", "to_route": "Quotation"}, @@ -136,7 +134,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Quotation", - "parents": [{"label": _("Quotations"), "route": "quotations"}], + "parents": [{"label": "Quotations", "route": "quotations"}], }, }, {"from_route": "/shipments", "to_route": "Delivery Note"}, @@ -145,7 +143,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Delivery Note", - "parents": [{"label": _("Shipments"), "route": "shipments"}], + "parents": [{"label": "Shipments", "route": "shipments"}], }, }, {"from_route": "/rfq", "to_route": "Request for Quotation"}, @@ -154,14 +152,14 @@ website_route_rules = [ "to_route": "rfq", "defaults": { "doctype": "Request for Quotation", - "parents": [{"label": _("Request for Quotation"), "route": "rfq"}], + "parents": [{"label": "Request for Quotation", "route": "rfq"}], }, }, {"from_route": "/addresses", "to_route": "Address"}, { "from_route": "/addresses/", "to_route": "addresses", - "defaults": {"doctype": "Address", "parents": [{"label": _("Addresses"), "route": "addresses"}]}, + "defaults": {"doctype": "Address", "parents": [{"label": "Addresses", "route": "addresses"}]}, }, {"from_route": "/boms", "to_route": "BOM"}, {"from_route": "/timesheets", "to_route": "Timesheet"}, @@ -171,78 +169,78 @@ website_route_rules = [ "to_route": "material_request_info", "defaults": { "doctype": "Material Request", - "parents": [{"label": _("Material Request"), "route": "material-requests"}], + "parents": [{"label": "Material Request", "route": "material-requests"}], }, }, {"from_route": "/project", "to_route": "Project"}, ] standard_portal_menu_items = [ - {"title": _("Projects"), "route": "/project", "reference_doctype": "Project"}, + {"title": "Projects", "route": "/project", "reference_doctype": "Project"}, { - "title": _("Request for Quotations"), + "title": "Request for Quotations", "route": "/rfq", "reference_doctype": "Request for Quotation", "role": "Supplier", }, { - "title": _("Supplier Quotation"), + "title": "Supplier Quotation", "route": "/supplier-quotations", "reference_doctype": "Supplier Quotation", "role": "Supplier", }, { - "title": _("Purchase Orders"), + "title": "Purchase Orders", "route": "/purchase-orders", "reference_doctype": "Purchase Order", "role": "Supplier", }, { - "title": _("Purchase Invoices"), + "title": "Purchase Invoices", "route": "/purchase-invoices", "reference_doctype": "Purchase Invoice", "role": "Supplier", }, { - "title": _("Quotations"), + "title": "Quotations", "route": "/quotations", "reference_doctype": "Quotation", "role": "Customer", }, { - "title": _("Orders"), + "title": "Orders", "route": "/orders", "reference_doctype": "Sales Order", "role": "Customer", }, { - "title": _("Invoices"), + "title": "Invoices", "route": "/invoices", "reference_doctype": "Sales Invoice", "role": "Customer", }, { - "title": _("Shipments"), + "title": "Shipments", "route": "/shipments", "reference_doctype": "Delivery Note", "role": "Customer", }, - {"title": _("Issues"), "route": "/issues", "reference_doctype": "Issue", "role": "Customer"}, - {"title": _("Addresses"), "route": "/addresses", "reference_doctype": "Address"}, + {"title": "Issues", "route": "/issues", "reference_doctype": "Issue", "role": "Customer"}, + {"title": "Addresses", "route": "/addresses", "reference_doctype": "Address"}, { - "title": _("Timesheets"), + "title": "Timesheets", "route": "/timesheets", "reference_doctype": "Timesheet", "role": "Customer", }, - {"title": _("Newsletter"), "route": "/newsletters", "reference_doctype": "Newsletter"}, + {"title": "Newsletter", "route": "/newsletters", "reference_doctype": "Newsletter"}, { - "title": _("Material Request"), + "title": "Material Request", "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer", }, - {"title": _("Appointment Booking"), "route": "/book_appointment"}, + {"title": "Appointment Booking", "route": "/book_appointment"}, ] default_roles = [ @@ -255,9 +253,7 @@ sounds = [ {"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2}, ] -has_upload_permission = { - "Employee": "erpnext.setup.doctype.employee.employee.has_upload_permission" -} +has_upload_permission = {"Employee": "erpnext.setup.doctype.employee.employee.has_upload_permission"} has_website_permission = { "Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission", @@ -301,7 +297,10 @@ period_closing_doctypes = [ doc_events = { "*": { - "validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply", + "validate": [ + "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply", + "erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.check_for_running_deletion_job", + ], }, tuple(period_closing_doctypes): { "validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save", @@ -448,6 +447,7 @@ scheduler_events = { "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email", "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily", + "erpnext.accounts.utils.run_ledger_health_checks", ], "weekly": [ "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly", @@ -477,9 +477,7 @@ default_mail_footer = """ """ -get_translated_dict = { - ("doctype", "Global Defaults"): "frappe.geo.country_info.get_translated_dict" -} +get_translated_dict = {("doctype", "Global Defaults"): "frappe.geo.country_info.get_translated_dict"} bot_parsers = [ "erpnext.utilities.bot.FindItemBot", @@ -550,6 +548,8 @@ accounting_dimension_doctypes = [ "Account Closing Balance", "Supplier Quotation", "Supplier Quotation Item", + "Payment Reconciliation", + "Payment Reconciliation Allocation", ] # get matching queries for Bank Reconciliation @@ -558,9 +558,7 @@ get_matching_queries = ( ) regional_overrides = { - "France": { - "erpnext.tests.test_regional.test_method": "erpnext.regional.france.utils.test_method" - }, + "France": {"erpnext.tests.test_regional.test_method": "erpnext.regional.france.utils.test_method"}, "United Arab Emirates": { "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data", "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries": "erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries", @@ -631,12 +629,12 @@ global_search_doctypes = { ], } -additional_timeline_content = { - "*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"] -} +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", "erpnext.startup.boot.bootinfo", ] + +fields_for_group_similar_items = ["qty", "amount"] diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js index 58179416b1a..1ea33645631 100644 --- a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js +++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js @@ -1,4 +1,4 @@ -frappe.provide('frappe.dashboards.chart_sources'); +frappe.provide("frappe.dashboards.chart_sources"); frappe.dashboards.chart_sources["Top 10 Pledged Loan Securities"] = { method: "erpnext.loan_management.dashboard_chart_source.top_10_pledged_loan_securities.top_10_pledged_loan_securities.get_data", @@ -8,7 +8,7 @@ frappe.dashboards.chart_sources["Top 10 Pledged Loan Securities"] = { label: __("Company"), fieldtype: "Link", options: "Company", - default: frappe.defaults.get_user_default("Company") - } - ] + default: frappe.defaults.get_user_default("Company"), + }, + ], }; diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py index aab3d8ccb57..52734375107 100644 --- a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py +++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py @@ -45,16 +45,14 @@ def get_data( unpledges = frappe._dict( frappe.db.sql( - """ + f""" SELECT u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE u.parent = up.name AND up.status = 'Approved' {conditions} GROUP BY u.loan_security - """.format( - conditions=conditions - ), + """, filters, as_list=1, ) @@ -62,16 +60,14 @@ def get_data( pledges = frappe._dict( frappe.db.sql( - """ + f""" SELECT p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' {conditions} GROUP BY p.loan_security - """.format( - conditions=conditions - ), + """, filters, as_list=1, ) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 4a333df5a59..eb90cf3b826 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -336,7 +336,6 @@ def get_sanctioned_amount_limit(applicant_type, applicant, company): def validate_repayment_method( repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan ): - if is_term_loan and not repayment_method: frappe.throw(_("Repayment Method is mandatory for term loans")) diff --git a/erpnext/loan_management/doctype/loan/loan_list.js b/erpnext/loan_management/doctype/loan/loan_list.js index 6591b729968..ac011627377 100644 --- a/erpnext/loan_management/doctype/loan/loan_list.js +++ b/erpnext/loan_management/doctype/loan/loan_list.js @@ -1,16 +1,16 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.listview_settings['Loan'] = { - get_indicator: function(doc) { +frappe.listview_settings["Loan"] = { + get_indicator: function (doc) { var status_color = { - "Draft": "red", - "Sanctioned": "blue", - "Disbursed": "orange", + Draft: "red", + Sanctioned: "blue", + Disbursed: "orange", "Partially Disbursed": "yellow", "Loan Closure Requested": "green", - "Closed": "green" + Closed: "green", }; - return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + return [__(doc.status), status_color[doc.status], "status,=," + doc.status]; }, }; diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 388e65d9e58..1f4b44e7690 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -184,7 +184,6 @@ class TestLoan(unittest.TestCase): self.assertEqual(flt(loan.total_payment, 0), 302712) def test_loan_with_security(self): - pledge = [ { "loan_security": "Test Security 1", @@ -266,13 +265,9 @@ class TestLoan(unittest.TestCase): # Make First Loan pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant3, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant3, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant3, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() # Make second loan greater than the sanctioned amount @@ -284,14 +279,10 @@ class TestLoan(unittest.TestCase): def test_regular_loan_repayment(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant2, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() self.assertEqual(loan.loan_amount, 1000000) @@ -311,18 +302,14 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - repayment_entry = create_repayment_entry( - loan.name, self.applicant2, add_days(last_date, 10), 111119 - ) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111119) repayment_entry.save() repayment_entry.submit() penalty_amount = (accrued_interest_amount * 5 * 25) / 100 self.assertEqual(flt(repayment_entry.penalty_amount, 0), flt(penalty_amount, 0)) - amounts = frappe.db.get_all( - "Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount"] - ) + amounts = frappe.db.get_all("Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount"]) loan.load_from_db() @@ -344,14 +331,10 @@ class TestLoan(unittest.TestCase): def test_loan_closure(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant2, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() self.assertEqual(loan.loan_amount, 1000000) @@ -415,15 +398,11 @@ class TestLoan(unittest.TestCase): loan.submit() - make_loan_disbursement_entry( - loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1) - ) + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - repayment_entry = create_repayment_entry( - loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75 - ) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75) repayment_entry.submit() @@ -511,14 +490,10 @@ class TestLoan(unittest.TestCase): def test_loan_security_unpledge(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant2, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() self.assertEqual(loan.loan_amount, 1000000) @@ -571,14 +546,10 @@ class TestLoan(unittest.TestCase): {"loan_security": "Test Security 2", "qty": 4000.00}, ] - loan_application = create_loan_application( - "_Test Company", self.applicant2, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() self.assertEqual(loan.loan_amount, 1000000) @@ -589,9 +560,7 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - repayment_entry = create_repayment_entry( - loan.name, self.applicant2, add_days(last_date, 5), 600000 - ) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), 600000) repayment_entry.submit() unpledge_map = {"Test Security 2": 2000} @@ -607,14 +576,10 @@ class TestLoan(unittest.TestCase): def test_sanctioned_loan_security_unpledge(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant2, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() self.assertEqual(loan.loan_amount, 1000000) @@ -693,14 +658,10 @@ class TestLoan(unittest.TestCase): def test_pending_loan_amount_after_closure_request(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant2, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() self.assertEqual(loan.loan_amount, 1000000) @@ -743,14 +704,10 @@ class TestLoan(unittest.TestCase): def test_partial_unaccrued_interest_payment(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant2, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() self.assertEqual(loan.loan_amount, 1000000) @@ -807,12 +764,9 @@ class TestLoan(unittest.TestCase): loan, dummy = create_loan_scenario_for_penalty(self) amounts = calculate_amounts(loan.name, "2019-11-30 00:00:00") - first_penalty = 10000 second_penalty = amounts["penalty_amount"] - 10000 - repayment_entry = create_repayment_entry( - loan.name, self.applicant2, "2019-11-30 00:00:00", 10000 - ) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, "2019-11-30 00:00:00", 10000) repayment_entry.submit() amounts = calculate_amounts(loan.name, "2019-11-30 00:00:01") @@ -829,14 +783,10 @@ class TestLoan(unittest.TestCase): def test_loan_write_off_limit(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant2, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() self.assertEqual(loan.loan_amount, 1000000) @@ -881,14 +831,10 @@ class TestLoan(unittest.TestCase): def test_loan_repayment_against_partially_disbursed_loan(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant2, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() first_date = "2019-10-01" @@ -899,21 +845,15 @@ class TestLoan(unittest.TestCase): loan.load_from_db() self.assertEqual(loan.status, "Partially Disbursed") - create_repayment_entry( - loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount / 3) - ) + create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount / 3)) def test_loan_amount_write_off(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant2, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() self.assertEqual(loan.loan_amount, 1000000) @@ -1026,9 +966,7 @@ def create_loan_scenario_for_penalty(doc): loan_application = create_loan_application("_Test Company", doc.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - doc.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(doc.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() first_date = "2019-10-01" @@ -1040,9 +978,7 @@ def create_loan_scenario_for_penalty(doc): amounts = calculate_amounts(loan.name, add_days(last_date, 1)) paid_amount = amounts["interest_amount"] / 2 - repayment_entry = create_repayment_entry( - loan.name, doc.applicant2, add_days(last_date, 5), paid_amount - ) + repayment_entry = create_repayment_entry(loan.name, doc.applicant2, add_days(last_date, 5), paid_amount) repayment_entry.submit() @@ -1154,7 +1090,6 @@ def create_loan_type( repayment_schedule_type=None, repayment_date_on=None, ): - if not frappe.db.exists("Loan Type", loan_name): loan_type = frappe.get_doc( { @@ -1228,7 +1163,6 @@ def create_loan_security(): def create_loan_security_pledge(applicant, pledges, loan_application=None, loan=None): - lsp = frappe.new_doc("Loan Security Pledge") lsp.applicant_type = "Customer" lsp.applicant = applicant @@ -1248,7 +1182,6 @@ def create_loan_security_pledge(applicant, pledges, loan_application=None, loan= def make_loan_disbursement_entry(loan, amount, disbursement_date=None): - loan_disbursement_entry = frappe.get_doc( { "doctype": "Loan Disbursement", @@ -1267,14 +1200,12 @@ def make_loan_disbursement_entry(loan, amount, disbursement_date=None): def create_loan_security_price(loan_security, loan_security_price, uom, from_date, to_date): - if not frappe.db.get_value( "Loan Security Price", {"loan_security": loan_security, "valid_from": ("<=", from_date), "valid_upto": (">=", to_date)}, "name", ): - - lsp = frappe.get_doc( + frappe.get_doc( { "doctype": "Loan Security Price", "loan_security": loan_security, @@ -1287,7 +1218,6 @@ def create_loan_security_price(loan_security, loan_security_price, uom, from_dat def create_repayment_entry(loan, applicant, posting_date, paid_amount): - lr = frappe.get_doc( { "doctype": "Loan Repayment", @@ -1350,7 +1280,6 @@ def create_loan( repayment_start_date=None, posting_date=None, ): - loan = frappe.get_doc( { "doctype": "Loan", @@ -1408,7 +1337,6 @@ def create_loan_with_security( def create_demand_loan(applicant, loan_type, loan_application, posting_date=None): - loan = frappe.get_doc( { "doctype": "Loan", diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index 5f040e22c97..c7dfb3e92bb 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -53,9 +53,7 @@ class LoanApplication(Document): maximum_loan_limit = frappe.db.get_value("Loan Type", self.loan_type, "maximum_loan_amount") if maximum_loan_limit and self.loan_amount > maximum_loan_limit: - frappe.throw( - _("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit) - ) + frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit)) if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount: frappe.throw( @@ -83,7 +81,6 @@ class LoanApplication(Document): def set_pledge_amount(self): for proposed_pledge in self.proposed_pledges: - if not proposed_pledge.qty and not proposed_pledge.amount: frappe.throw(_("Qty or Amount is mandatroy for loan security")) @@ -98,7 +95,6 @@ class LoanApplication(Document): ) def get_repayment_details(self): - if self.is_term_loan: if self.repayment_method == "Repay Over Number of Periods": self.repayment_amount = get_monthly_repayment_amount( @@ -110,9 +106,14 @@ class LoanApplication(Document): if monthly_interest_rate: min_repayment_amount = self.loan_amount * monthly_interest_rate if self.repayment_amount - min_repayment_amount <= 0: - frappe.throw(_("Repayment Amount must be greater than " + str(flt(min_repayment_amount, 2)))) + frappe.throw( + _("Repayment Amount must be greater than " + str(flt(min_repayment_amount, 2))) + ) self.repayment_periods = math.ceil( - (math.log(self.repayment_amount) - math.log(self.repayment_amount - min_repayment_amount)) + ( + math.log(self.repayment_amount) + - math.log(self.repayment_amount - min_repayment_amount) + ) / (math.log(1 + monthly_interest_rate)) ) else: @@ -206,7 +207,6 @@ def create_pledge(loan_application, loan=None): lsp.loan = loan for pledge in loan_application_doc.proposed_pledges: - lsp.append( "securities", { diff --git a/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.js b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.js index 8aec63ad75f..3042c6c7aee 100644 --- a/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.js +++ b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.js @@ -1,8 +1,7 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Loan Balance Adjustment', { +frappe.ui.form.on("Loan Balance Adjustment", { // refresh: function(frm) { - // } }); diff --git a/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py index 0a576d69692..9099292c4fa 100644 --- a/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py +++ b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py @@ -97,9 +97,9 @@ class LoanBalanceAdjustment(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] loan_account = frappe.db.get_value("Loan", self.loan, "loan_account") - remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan) + remarks = f"{self.adjustment_type.capitalize()} against loan {self.loan}" if self.reference_number: - remarks += "with reference no. {}".format(self.reference_number) + remarks += f"with reference no. {self.reference_number}" loan_entry = { "account": loan_account, diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index ec117a12ad0..3d649f3613d 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -182,18 +182,14 @@ def get_total_pledged_security_value(loan): ) ) - hair_cut_map = frappe._dict( - frappe.get_all("Loan Security", fields=["name", "haircut"], as_list=1) - ) + hair_cut_map = frappe._dict(frappe.get_all("Loan Security", fields=["name", "haircut"], as_list=1)) security_value = 0.0 pledged_securities = get_pledged_security_qty(loan) for security, qty in pledged_securities.items(): after_haircut_percentage = 100 - hair_cut_map.get(security) - security_value += ( - loan_security_price_map.get(security, 0) * qty * after_haircut_percentage - ) / 100 + security_value += (loan_security_price_map.get(security, 0) * qty * after_haircut_percentage) / 100 return security_value @@ -241,10 +237,7 @@ def get_disbursal_amount(loan, on_current_security_price=0): disbursal_amount = flt(security_value) - flt(pending_principal_amount) - if ( - loan_details.is_term_loan - and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount - ): + if loan_details.is_term_loan and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount: disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount return disbursal_amount diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 9cc6ec9d4b4..6df4ad23352 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -8,7 +8,6 @@ from frappe.utils import ( add_days, add_to_date, date_diff, - flt, get_datetime, get_first_day, get_last_day, @@ -76,9 +75,7 @@ class TestLoanDisbursement(unittest.TestCase): def test_loan_topup(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant, "Demand Loan", pledge) create_pledge(loan_application) loan = create_demand_loan( @@ -92,18 +89,14 @@ class TestLoanDisbursement(unittest.TestCase): no_of_days = date_diff(last_date, first_date) + 1 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( - days_in_year(get_datetime().year) * 100 - ) + (loan.loan_amount * loan.rate_of_interest * no_of_days) / (days_in_year(get_datetime().year) * 100) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 1)) # Should not be able to create loan disbursement entry before repayment - self.assertRaises( - frappe.ValidationError, make_loan_disbursement_entry, loan.name, 500000, first_date - ) + self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name, 500000, first_date) repayment_entry = create_repayment_entry( loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89 @@ -125,14 +118,10 @@ class TestLoanDisbursement(unittest.TestCase): def test_loan_topup_with_additional_pledge(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan( - self.applicant, "Demand Loan", loan_application, posting_date="2019-10-01" - ) + loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date="2019-10-01") loan.submit() self.assertEqual(loan.loan_amount, 1000000) @@ -145,7 +134,7 @@ class TestLoanDisbursement(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date=last_date) amounts = calculate_amounts(loan.name, add_days(last_date, 1)) - previous_interest = amounts["interest_amount"] + amounts["interest_amount"] pledge1 = [{"loan_security": "Test Security 1", "qty": 2000.00}] @@ -157,6 +146,6 @@ class TestLoanDisbursement(unittest.TestCase): amounts = calculate_amounts(loan.name, add_days(last_date, 15)) per_day_interest = get_per_day_interest(1500000, 13.5, "2019-10-30") - interest = per_day_interest * 15 + per_day_interest * 15 self.assertEqual(amounts["pending_principal_amount"], 1500000) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index e72d694b0f1..a219aaefb16 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -72,7 +72,7 @@ class LoanInterestAccrual(AccountsController): "credit_in_account_currency": self.interest_amount, "against_voucher_type": "Loan", "against_voucher": self.loan, - "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( + "remarks": ("Interest accrued from {} to {} against loan: {}").format( self.last_accrual_date, self.posting_date, self.loan ), "cost_center": cost_center, @@ -88,9 +88,7 @@ class LoanInterestAccrual(AccountsController): # For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and # rate of interest is 13.5 then first loan interest accural will be on '01-10-2019' # which means interest will be accrued for 30 days which should be equal to 11095.89 -def calculate_accrual_amount_for_demand_loans( - loan, posting_date, process_loan_interest, accrual_type -): +def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type): from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( calculate_amounts, get_pending_principal_amount, @@ -104,9 +102,7 @@ def calculate_accrual_amount_for_demand_loans( pending_principal_amount = get_pending_principal_amount(loan) - interest_per_day = get_per_day_interest( - pending_principal_amount, loan.rate_of_interest, posting_date - ) + interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date) payable_interest = interest_per_day * no_of_days pending_amounts = calculate_amounts(loan.name, posting_date, payment_type="Loan Closure") @@ -170,9 +166,7 @@ def make_accrual_interest_entry_for_demand_loans( ) for loan in open_loans: - calculate_accrual_amount_for_demand_loans( - loan, posting_date, process_loan_interest, accrual_type - ) + calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type) def make_accrual_interest_entry_for_term_loans( @@ -223,7 +217,7 @@ def get_term_loans(date, term_loan=None, loan_type=None): condition += " AND l.loan_type = %s" % frappe.db.escape(loan_type) term_loans = frappe.db.sql( - """SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account, + f"""SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account, l.interest_income_account, l.is_term_loan, l.disbursement_date, l.applicant_type, l.applicant, l.rate_of_interest, l.total_interest_payable, l.repayment_start_date, rs.name as payment_entry, rs.payment_date, rs.principal_amount, rs.interest_amount, rs.is_accrued , rs.balance_loan_amount @@ -232,12 +226,10 @@ def get_term_loans(date, term_loan=None, loan_type=None): AND l.docstatus=1 AND l.is_term_loan =1 AND rs.payment_date <= %s - AND rs.is_accrued=0 {0} + AND rs.is_accrued=0 {condition} AND rs.principal_amount > 0 AND l.status = 'Disbursed' - ORDER BY rs.payment_date""".format( - condition - ), + ORDER BY rs.payment_date""", (getdate(date)), as_dict=1, ) @@ -256,9 +248,7 @@ def make_loan_interest_accrual_entry(args): loan_interest_accrual.loan_account = args.loan_account loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision) loan_interest_accrual.interest_amount = flt(args.interest_amount, precision) - loan_interest_accrual.total_pending_interest_amount = flt( - args.total_pending_interest_amount, precision - ) + loan_interest_accrual.total_pending_interest_amount = flt(args.total_pending_interest_amount, precision) loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision) loan_interest_accrual.posting_date = args.posting_date or nowdate() loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest @@ -323,6 +313,4 @@ def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None): if not posting_date: posting_date = getdate() - return flt( - (principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) - ) + return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index fd59393b827..ef209d340fa 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -60,9 +60,7 @@ class TestLoanInterestAccrual(unittest.TestCase): def test_loan_interest_accural(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant, "Demand Loan", pledge) create_pledge(loan_application) loan = create_demand_loan( self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) @@ -86,9 +84,7 @@ class TestLoanInterestAccrual(unittest.TestCase): def test_accumulated_amounts(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application( - "_Test Company", self.applicant, "Demand Loan", pledge - ) + loan_application = create_loan_application("_Test Company", self.applicant, "Demand Loan", pledge) create_pledge(loan_application) loan = create_demand_loan( self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) diff --git a/erpnext/loan_management/doctype/loan_refund/loan_refund.js b/erpnext/loan_management/doctype/loan_refund/loan_refund.js index f108bf7a281..c507aa5abaa 100644 --- a/erpnext/loan_management/doctype/loan_refund/loan_refund.js +++ b/erpnext/loan_management/doctype/loan_refund/loan_refund.js @@ -1,8 +1,7 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Loan Refund', { +frappe.ui.form.on("Loan Refund", { // refresh: function(frm) { - // } }); diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 0b057f85aa3..e11e3052983 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -70,7 +70,9 @@ class LoanRepayment(AccountsController): shortfall_amount = flt( frappe.db.get_value( - "Loan Security Shortfall", {"loan": self.against_loan, "status": "Pending"}, "shortfall_amount" + "Loan Security Shortfall", + {"loan": self.against_loan, "status": "Pending"}, + "shortfall_amount", ) ) @@ -94,10 +96,10 @@ class LoanRepayment(AccountsController): ) if future_repayment_date: - frappe.throw("Repayment already made till date {0}".format(get_datetime(future_repayment_date))) + frappe.throw(f"Repayment already made till date {get_datetime(future_repayment_date)}") def validate_amount(self): - precision = cint(frappe.db.get_default("currency_precision")) or 2 + cint(frappe.db.get_default("currency_precision")) or 2 if not self.amount_paid: frappe.throw(_("Amount paid cannot be zero")) @@ -115,7 +117,9 @@ class LoanRepayment(AccountsController): ) no_of_days = ( - flt(flt(self.total_interest_paid - self.interest_payable, precision) / per_day_interest, 0) + flt( + flt(self.total_interest_paid - self.interest_payable, precision) / per_day_interest, 0 + ) - 1 ) @@ -139,7 +143,9 @@ class LoanRepayment(AccountsController): "repayment_details", { "loan_interest_accrual": lia.name, - "paid_interest_amount": flt(self.total_interest_paid - self.interest_payable, precision), + "paid_interest_amount": flt( + self.total_interest_paid - self.interest_payable, precision + ), "paid_principal_amount": 0.0, "accrual_type": "Repayment", }, @@ -263,9 +269,7 @@ class LoanRepayment(AccountsController): if future_accrual_date: frappe.throw( - "Cannot cancel. Interest accruals already processed till {0}".format( - get_datetime(future_accrual_date) - ) + f"Cannot cancel. Interest accruals already processed till {get_datetime(future_accrual_date)}" ) def update_repayment_schedule(self, cancel=0): @@ -337,9 +341,7 @@ class LoanRepayment(AccountsController): return interest_paid, updated_entries - def allocate_principal_amount_for_term_loans( - self, interest_paid, repayment_details, updated_entries - ): + def allocate_principal_amount_for_term_loans(self, interest_paid, repayment_details, updated_entries): if interest_paid > 0: for lia, amounts in repayment_details.get("pending_accrual_entries", []).items(): paid_principal = 0 @@ -396,18 +398,16 @@ class LoanRepayment(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] if self.shortfall_amount and self.amount_paid > self.shortfall_amount: - remarks = "Shortfall repayment of {0}.
    Repayment against loan {1}".format( + remarks = "Shortfall repayment of {}.
    Repayment against loan {}".format( self.shortfall_amount, self.against_loan ) elif self.shortfall_amount: - remarks = "Shortfall repayment of {0} against loan {1}".format( - self.shortfall_amount, self.against_loan - ) + remarks = f"Shortfall repayment of {self.shortfall_amount} against loan {self.against_loan}" else: remarks = "Repayment against loan " + self.against_loan if self.reference_number: - remarks += "with reference no. {}".format(self.reference_number) + remarks += f"with reference no. {self.reference_number}" if hasattr(self, "repay_from_salary") and self.repay_from_salary: payment_account = self.payroll_payable_account @@ -513,7 +513,6 @@ def create_repayment_entry( payroll_payable_account=None, process_payroll_accounting_entry_based_on_employee=0, ): - lr = frappe.get_doc( { "doctype": "Loan Repayment", @@ -617,9 +616,7 @@ def regenerate_repayment_schedule(loan, cancel=0): balance_amount = get_pending_principal_amount(loan_doc) if loan_doc.repayment_method == "Repay Fixed Amount per Period": - monthly_repayment_amount = flt( - balance_amount / (original_repayment_schedule_len - accrued_entries) - ) + monthly_repayment_amount = flt(balance_amount / (original_repayment_schedule_len - accrued_entries)) else: repayment_period = loan_doc.repayment_periods - accrued_entries if not cancel and repayment_period > 0: @@ -718,9 +715,7 @@ def get_amounts(amounts, against_loan, posting_date): if ( no_of_late_days > 0 - and ( - not (hasattr(against_loan_doc, "repay_from_salary") and against_loan_doc.repay_from_salary) - ) + and (not (hasattr(against_loan_doc, "repay_from_salary") and against_loan_doc.repay_from_salary)) and entry.accrual_type == "Regular" ): penalty_amount += ( diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.js b/erpnext/loan_management/doctype/loan_security/loan_security.js index 0e815af76a0..9db3a33bd2d 100644 --- a/erpnext/loan_management/doctype/loan_security/loan_security.js +++ b/erpnext/loan_management/doctype/loan_security/loan_security.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Loan Security', { +frappe.ui.form.on("Loan Security", { // refresh: function(frm) { - // } }); diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js index 48ca392edf7..be908997487 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js @@ -1,43 +1,48 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Loan Security Pledge', { - calculate_amounts: function(frm, cdt, cdn) { +frappe.ui.form.on("Loan Security Pledge", { + calculate_amounts: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; - frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); - frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100))); + frappe.model.set_value(cdt, cdn, "amount", row.qty * row.loan_security_price); + frappe.model.set_value( + cdt, + cdn, + "post_haircut_amount", + cint(row.amount - (row.amount * row.haircut) / 100) + ); let amount = 0; let maximum_amount = 0; - $.each(frm.doc.securities || [], function(i, item){ + $.each(frm.doc.securities || [], function (i, item) { amount += item.amount; maximum_amount += item.post_haircut_amount; }); - frm.set_value('total_security_value', amount); - frm.set_value('maximum_loan_value', maximum_amount); - } + frm.set_value("total_security_value", amount); + frm.set_value("maximum_loan_value", maximum_amount); + }, }); frappe.ui.form.on("Pledge", { - loan_security: function(frm, cdt, cdn) { + loan_security: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; if (row.loan_security) { frappe.call({ method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price", args: { - loan_security: row.loan_security + loan_security: row.loan_security, }, - callback: function(r) { - frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message); + callback: function (r) { + frappe.model.set_value(cdt, cdn, "loan_security_price", r.message); frm.events.calculate_amounts(frm, cdt, cdn); - } + }, }); } }, - qty: function(frm, cdt, cdn) { + qty: function (frm, cdt, cdn) { frm.events.calculate_amounts(frm, cdt, cdn); }, }); diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index f0d59542753..238a9c9e571 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -74,7 +74,6 @@ class LoanSecurityPledge(Document): maximum_loan_value = 0 for pledge in self.securities: - if not pledge.qty and not pledge.amount: frappe.throw(_("Qty or Amount is mandatory for loan security!")) diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js index 174d1b0d62b..c8f36ec5eee 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js @@ -2,14 +2,14 @@ // License: GNU General Public License v3. See license.txt // render -frappe.listview_settings['Loan Security Pledge'] = { +frappe.listview_settings["Loan Security Pledge"] = { add_fields: ["status"], - get_indicator: function(doc) { + get_indicator: function (doc) { var status_color = { - "Unpledged": "orange", - "Pledged": "green", - "Partially Pledged": "green" + Unpledged: "orange", + Pledged: "green", + "Partially Pledged": "green", }; - return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; - } + return [__(doc.status), status_color[doc.status], "status,=," + doc.status]; + }, }; diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js index 31b4ec7249e..b79ccb51e37 100644 --- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Loan Security Price', { +frappe.ui.form.on("Loan Security Price", { // refresh: function(frm) { - // } }); diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py index 45c4459ac3f..3bee111aab1 100644 --- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py @@ -13,7 +13,6 @@ class LoanSecurityPrice(Document): self.validate_dates() def validate_dates(self): - if self.valid_from > self.valid_upto: frappe.throw(_("Valid From Time must be lesser than Valid Upto Time.")) diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js index f26c138371e..504a5d9748a 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js @@ -1,25 +1,25 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Loan Security Shortfall', { - refresh: function(frm) { - frm.add_custom_button(__("Add Loan Security"), function() { - frm.trigger('shortfall_action'); +frappe.ui.form.on("Loan Security Shortfall", { + refresh: function (frm) { + frm.add_custom_button(__("Add Loan Security"), function () { + frm.trigger("shortfall_action"); }); }, - shortfall_action: function(frm) { + shortfall_action: function (frm) { frappe.call({ method: "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.add_security", args: { - 'loan': frm.doc.loan + loan: frm.doc.loan, }, - callback: function(r) { + callback: function (r) { if (r.message) { let doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); } - } + }, }); - } + }, }); diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index b901e626b43..f3bcc60fc5b 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -47,9 +47,7 @@ def update_shortfall_status(loan, security_value, on_cancel=0): @frappe.whitelist() def add_security(loan): - loan_details = frappe.db.get_value( - "Loan", loan, ["applicant", "company", "applicant_type"], as_dict=1 - ) + loan_details = frappe.db.get_value("Loan", loan, ["applicant", "company", "applicant_type"], as_dict=1) loan_security_pledge = frappe.new_doc("Loan Security Pledge") loan_security_pledge.loan = loan @@ -61,7 +59,6 @@ def add_security(loan): def check_for_ltv_shortfall(process_loan_security_shortfall): - update_time = get_datetime() loan_security_price_map = frappe._dict( @@ -93,8 +90,6 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): ) ) - loan_security_map = {} - for loan in loans: if loan.status == "Disbursed": outstanding_amount = ( diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js index 3a1e0689c1d..5e277d3d6ba 100644 --- a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Loan Security Type', { +frappe.ui.form.on("Loan Security Type", { // refresh: function(frm) { - // }, }); diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js index 82232062774..b6803e00de9 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js @@ -1,11 +1,10 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Loan Security Unpledge', { - refresh: function(frm) { - - if (frm.doc.docstatus == 1 && frm.doc.status == 'Approved') { - frm.set_df_property('status', 'read_only', 1); +frappe.ui.form.on("Loan Security Unpledge", { + refresh: function (frm) { + if (frm.doc.docstatus == 1 && frm.doc.status == "Approved") { + frm.set_df_property("status", "read_only", 1); } - } + }, }); diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 0ab7beb0fc7..b24a48b6521 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -39,9 +39,7 @@ class LoanSecurityUnpledge(Document): pledge_qty_map = get_pledged_security_qty(self.loan) - ltv_ratio_map = frappe._dict( - frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1) - ) + frappe._dict(frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1)) loan_security_price_map = frappe._dict( frappe.get_all( @@ -129,7 +127,7 @@ class LoanSecurityUnpledge(Document): pledged_qty = 0 current_pledges = get_pledged_security_qty(self.loan) - for security, qty in current_pledges.items(): + for _security, qty in current_pledges.items(): pledged_qty += qty if not pledged_qty: @@ -138,7 +136,6 @@ class LoanSecurityUnpledge(Document): @frappe.whitelist() def get_pledged_security_qty(loan): - current_pledges = {} unpledges = frappe._dict( diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js index 196ebbb96ad..52624fa1436 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js @@ -2,13 +2,13 @@ // License: GNU General Public License v3. See license.txt // render -frappe.listview_settings['Loan Security Unpledge'] = { +frappe.listview_settings["Loan Security Unpledge"] = { add_fields: ["status"], - get_indicator: function(doc) { + get_indicator: function (doc) { var status_color = { - "Requested": "orange", - "Approved": "green", + Requested: "orange", + Approved: "green", }; - return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; - } + return [__(doc.status), status_color[doc.status], "status,=," + doc.status]; + }, }; diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.js b/erpnext/loan_management/doctype/loan_type/loan_type.js index 9f9137cfbcd..bb0bd8106f9 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.js +++ b/erpnext/loan_management/doctype/loan_type/loan_type.js @@ -1,16 +1,16 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Loan Type', { - onload: function(frm) { +frappe.ui.form.on("Loan Type", { + onload: function (frm) { $.each(["penalty_income_account", "interest_income_account"], function (i, field) { frm.set_query(field, function () { return { - "filters": { - "company": frm.doc.company, - "root_type": "Income", - "is_group": 0 - } + filters: { + company: frm.doc.company, + root_type: "Income", + is_group: 0, + }, }; }); }); @@ -18,13 +18,13 @@ frappe.ui.form.on('Loan Type', { $.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) { frm.set_query(field, function () { return { - "filters": { - "company": frm.doc.company, - "root_type": "Asset", - "is_group": 0 - } + filters: { + company: frm.doc.company, + root_type: "Asset", + is_group: 0, + }, }; }); }); - } + }, }); diff --git a/erpnext/loan_management/doctype/pledge/pledge.js b/erpnext/loan_management/doctype/pledge/pledge.js index fb6ab107782..faee76dbd3a 100644 --- a/erpnext/loan_management/doctype/pledge/pledge.js +++ b/erpnext/loan_management/doctype/pledge/pledge.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Pledge', { +frappe.ui.form.on("Pledge", { // refresh: function(frm) { - // } }); diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js index c596be2d2a8..45cc07344d8 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Process Loan Interest Accrual', { +frappe.ui.form.on("Process Loan Interest Accrual", { // refresh: function(frm) { - // } }); diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index 25c72d91a7c..a1d9abdd35c 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -56,7 +56,6 @@ def process_loan_interest_accrual_for_demand_loans( def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None): - if not term_loan_accrual_pending(posting_date or nowdate(), loan=loan): return diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js index 645e3ada9a8..5ac98af15e3 100644 --- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js @@ -1,8 +1,8 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Process Loan Security Shortfall', { - onload: function(frm) { - frm.set_value('update_time', frappe.datetime.now_datetime()); - } +frappe.ui.form.on("Process Loan Security Shortfall", { + onload: function (frm) { + frm.set_value("update_time", frappe.datetime.now_datetime()); + }, }); diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js index 5361e7ca2a5..af647d974bf 100644 --- a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Sanctioned Loan Amount', { +frappe.ui.form.on("Sanctioned Loan Amount", { // refresh: function(frm) { - // } }); diff --git a/erpnext/loan_management/loan_common.js b/erpnext/loan_management/loan_common.js index 247e30b8439..72f40c5700f 100644 --- a/erpnext/loan_management/loan_common.js +++ b/erpnext/loan_management/loan_common.js @@ -2,37 +2,42 @@ // For license information, please see license.txt frappe.ui.form.on(cur_frm.doctype, { - refresh: function(frm) { - if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off'].includes(frm.doc.doctype) - && frm.doc.docstatus > 0) { + refresh: function (frm) { + if ( + ["Loan Disbursement", "Loan Repayment", "Loan Interest Accrual", "Loan Write Off"].includes( + frm.doc.doctype + ) && + frm.doc.docstatus > 0 + ) { + frm.add_custom_button( + __("Accounting Ledger"), + function () { + frappe.route_options = { + voucher_no: frm.doc.name, + company: frm.doc.company, + from_date: moment(frm.doc.posting_date).format("YYYY-MM-DD"), + to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), + show_cancelled_entries: frm.doc.docstatus === 2, + }; - frm.add_custom_button(__("Accounting Ledger"), function() { - frappe.route_options = { - voucher_no: frm.doc.name, - company: frm.doc.company, - from_date: moment(frm.doc.posting_date).format('YYYY-MM-DD'), - to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), - show_cancelled_entries: frm.doc.docstatus === 2 - }; - - frappe.set_route("query-report", "General Ledger"); - },__("View")); + frappe.set_route("query-report", "General Ledger"); + }, + __("View") + ); } }, - applicant: function(frm) { + applicant: function (frm) { if (!["Loan Application", "Loan"].includes(frm.doc.doctype)) { return; } if (frm.doc.applicant) { - frappe.model.with_doc(frm.doc.applicant_type, frm.doc.applicant, function() { + frappe.model.with_doc(frm.doc.applicant_type, frm.doc.applicant, function () { var applicant = frappe.model.get_doc(frm.doc.applicant_type, frm.doc.applicant); - frm.set_value("applicant_name", - applicant.employee_name || applicant.member_name); + frm.set_value("applicant_name", applicant.employee_name || applicant.member_name); }); - } - else { + } else { frm.set_value("applicant_name", null); } - } + }, }); diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js index 73d60c40458..c85fce0f380 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js @@ -3,14 +3,14 @@ /* eslint-disable */ frappe.query_reports["Applicant-Wise Loan Security Exposure"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 - } - ] + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + ], }; diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py index 02da8109ca9..2f1bcb1d8a7 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -188,16 +188,14 @@ def get_applicant_wise_total_loan_security_qty(filters, loan_security_details): conditions = "AND company = %(company)s" unpledges = frappe.db.sql( - """ + f""" SELECT up.applicant, u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE u.parent = up.name AND up.status = 'Approved' {conditions} GROUP BY up.applicant, u.loan_security - """.format( - conditions=conditions - ), + """, filters, as_dict=1, ) @@ -206,16 +204,14 @@ def get_applicant_wise_total_loan_security_qty(filters, loan_security_details): applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty) pledges = frappe.db.sql( - """ + f""" SELECT lp.applicant_type, lp.applicant, p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' {conditions} GROUP BY lp.applicant, p.loan_security - """.format( - conditions=conditions - ), + """, filters, as_dict=1, ) diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js index 458c79a1ea8..53ff92fa157 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js @@ -3,48 +3,48 @@ /* eslint-disable */ frappe.query_reports["Loan Interest Report"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"applicant_type", - "label": __("Applicant Type"), - "fieldtype": "Select", - "options": ["Customer", "Employee"], - "reqd": 1, - "default": "Customer", - on_change: function() { - frappe.query_report.set_filter_value('applicant', ""); - } + fieldname: "applicant_type", + label: __("Applicant Type"), + fieldtype: "Select", + options: ["Customer", "Employee"], + reqd: 1, + default: "Customer", + on_change: function () { + frappe.query_report.set_filter_value("applicant", ""); + }, }, { - "fieldname": "applicant", - "label": __("Applicant"), - "fieldtype": "Dynamic Link", - "get_options": function() { - var applicant_type = frappe.query_report.get_filter_value('applicant_type'); - var applicant = frappe.query_report.get_filter_value('applicant'); - if(applicant && !applicant_type) { + fieldname: "applicant", + label: __("Applicant"), + fieldtype: "Dynamic Link", + get_options: function () { + var applicant_type = frappe.query_report.get_filter_value("applicant_type"); + var applicant = frappe.query_report.get_filter_value("applicant"); + if (applicant && !applicant_type) { frappe.throw(__("Please select Applicant Type first")); } return applicant_type; - } + }, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", }, { - "fieldname":"to_date", - "label": __("From Date"), - "fieldtype": "Date", + fieldname: "to_date", + label: __("From Date"), + fieldtype: "Date", }, - ] + ], }; diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index 58a7880a459..9e1c496779c 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -219,9 +219,7 @@ def get_active_loan_details(filters): def get_sanctioned_amount_map(): return frappe._dict( - frappe.get_all( - "Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"], as_list=1 - ) + frappe.get_all("Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"], as_list=1) ) @@ -309,9 +307,7 @@ def get_interest_accruals(loans, filters): def get_penal_interest_rate_map(): - return frappe._dict( - frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1) - ) + return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1)) def get_loan_wise_pledges(filters): @@ -324,16 +320,14 @@ def get_loan_wise_pledges(filters): conditions = "AND company = %(company)s" unpledges = frappe.db.sql( - """ + f""" SELECT up.loan, u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE u.parent = up.name AND up.status = 'Approved' {conditions} GROUP BY up.loan, u.loan_security - """.format( - conditions=conditions - ), + """, filters, as_dict=1, ) @@ -342,16 +336,14 @@ def get_loan_wise_pledges(filters): loan_wise_unpledges.setdefault((unpledge.loan, unpledge.loan_security), unpledge.qty) pledges = frappe.db.sql( - """ + f""" SELECT lp.loan, p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' {conditions} GROUP BY lp.loan, p.loan_security - """.format( - conditions=conditions - ), + """, filters, as_dict=1, ) diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js index ed5e937c996..0274715a319 100644 --- a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js @@ -3,39 +3,38 @@ /* eslint-disable */ frappe.query_reports["Loan Repayment and Closure"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"applicant_type", - "label": __("Applicant Type"), - "fieldtype": "Select", - "options": ["Customer", "Employee"], - "reqd": 1, - "default": "Customer", - on_change: function() { - frappe.query_report.set_filter_value('applicant', ""); - } + fieldname: "applicant_type", + label: __("Applicant Type"), + fieldtype: "Select", + options: ["Customer", "Employee"], + reqd: 1, + default: "Customer", + on_change: function () { + frappe.query_report.set_filter_value("applicant", ""); + }, }, { - "fieldname": "applicant", - "label": __("Applicant"), - "fieldtype": "Dynamic Link", - "get_options": function() { - var applicant_type = frappe.query_report.get_filter_value('applicant_type'); - var applicant = frappe.query_report.get_filter_value('applicant'); - if(applicant && !applicant_type) { + fieldname: "applicant", + label: __("Applicant"), + fieldtype: "Dynamic Link", + get_options: function () { + var applicant_type = frappe.query_report.get_filter_value("applicant_type"); + var applicant = frappe.query_report.get_filter_value("applicant"); + if (applicant && !applicant_type) { frappe.throw(__("Please select Applicant Type first")); } return applicant_type; - } - + }, }, - ] + ], }; diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js index 777f29624a7..5650d89f495 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js @@ -3,14 +3,14 @@ /* eslint-disable */ frappe.query_reports["Loan Security Exposure"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 - } - ] + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + ], }; diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.js b/erpnext/loan_management/report/loan_security_status/loan_security_status.js index 6e6191c7e44..6e5195b72f3 100644 --- a/erpnext/loan_management/report/loan_security_status/loan_security_status.js +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.js @@ -3,44 +3,44 @@ /* eslint-disable */ frappe.query_reports["Loan Security Status"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"applicant_type", - "label": __("Applicant Type"), - "fieldtype": "Select", - "options": ["Customer", "Employee"], - "reqd": 1, - "default": "Customer", - on_change: function() { - frappe.query_report.set_filter_value('applicant', ""); - } + fieldname: "applicant_type", + label: __("Applicant Type"), + fieldtype: "Select", + options: ["Customer", "Employee"], + reqd: 1, + default: "Customer", + on_change: function () { + frappe.query_report.set_filter_value("applicant", ""); + }, }, { - "fieldname": "applicant", - "label": __("Applicant"), - "fieldtype": "Dynamic Link", - "get_options": function() { - var applicant_type = frappe.query_report.get_filter_value('applicant_type'); - var applicant = frappe.query_report.get_filter_value('applicant'); - if(applicant && !applicant_type) { + fieldname: "applicant", + label: __("Applicant"), + fieldtype: "Dynamic Link", + get_options: function () { + var applicant_type = frappe.query_report.get_filter_value("applicant_type"); + var applicant = frappe.query_report.get_filter_value("applicant"); + if (applicant && !applicant_type) { frappe.throw(__("Please select Applicant Type first")); } return applicant_type; - } + }, }, { - "fieldname":"pledge_status", - "label": __("Pledge Status"), - "fieldtype": "Select", - "options": ["", "Requested", "Pledged", "Partially Pledged", "Unpledged"], + fieldname: "pledge_status", + label: __("Pledge Status"), + fieldtype: "Select", + options: ["", "Requested", "Pledged", "Partially Pledged", "Unpledged"], }, - ] + ], }; diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.py b/erpnext/loan_management/report/loan_security_status/loan_security_status.py index 9a5a18001ea..acec5271ad1 100644 --- a/erpnext/loan_management/report/loan_security_status/loan_security_status.py +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.py @@ -61,12 +61,11 @@ def get_columns(filters): def get_data(filters): - data = [] conditions = get_conditions(filters) loan_security_pledges = frappe.db.sql( - """ + f""" SELECT p.name, p.applicant, p.loan, p.status, p.pledge_time, c.loan_security, c.qty, c.loan_security_price, c.amount @@ -77,9 +76,7 @@ def get_data(filters): AND c.parent = p.name AND p.company = %(company)s {conditions} - """.format( - conditions=conditions - ), + """, (filters), as_dict=1, ) # nosec diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js index 5252798ba57..4613a0fc840 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js @@ -2,15 +2,15 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.maintenance"); -frappe.ui.form.on('Maintenance Schedule', { +frappe.ui.form.on("Maintenance Schedule", { setup: function (frm) { - frm.set_query('contact_person', erpnext.queries.contact_query); - frm.set_query('customer_address', erpnext.queries.address_query); - frm.set_query('customer', erpnext.queries.customer); + frm.set_query("contact_person", erpnext.queries.contact_query); + frm.set_query("customer_address", erpnext.queries.address_query); + frm.set_query("customer", erpnext.queries.customer); }, onload: function (frm) { if (!frm.doc.status) { - frm.set_value({ status: 'Draft' }); + frm.set_value({ status: "Draft" }); } if (frm.doc.__islocal) { frm.set_value({ transaction_date: frappe.datetime.get_today() }); @@ -18,128 +18,136 @@ frappe.ui.form.on('Maintenance Schedule', { }, refresh: function (frm) { setTimeout(() => { - frm.toggle_display('generate_schedule', !(frm.is_new() || frm.doc.docstatus)); - frm.toggle_display('schedule', !(frm.is_new())); + frm.toggle_display("generate_schedule", !(frm.is_new() || frm.doc.docstatus)); + frm.toggle_display("schedule", !frm.is_new()); }, 10); }, customer: function (frm) { - erpnext.utils.get_party_details(frm) + erpnext.utils.get_party_details(frm); }, customer_address: function (frm) { - erpnext.utils.get_address_display(frm, 'customer_address', 'address_display'); + erpnext.utils.get_address_display(frm, "customer_address", "address_display"); }, contact_person: function (frm) { erpnext.utils.get_contact_details(frm); }, generate_schedule: function (frm) { if (frm.is_new()) { - frappe.msgprint(__('Please save first')); + frappe.msgprint(__("Please save first")); } else { - frm.call('generate_schedule'); + frm.call("generate_schedule"); } - } -}) + }, +}); // TODO commonify this code erpnext.maintenance.MaintenanceSchedule = class MaintenanceSchedule extends frappe.ui.form.Controller { refresh() { - frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} + frappe.dynamic_link = { doc: this.frm.doc, fieldname: "customer", doctype: "Customer" }; var me = this; if (this.frm.doc.docstatus === 0) { - this.frm.add_custom_button(__('Sales Order'), + this.frm.add_custom_button( + __("Sales Order"), function () { erpnext.utils.map_current_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_maintenance_schedule", source_doctype: "Sales Order", target: me.frm, setters: { - customer: me.frm.doc.customer || undefined + customer: me.frm.doc.customer || undefined, }, get_query_filters: { docstatus: 1, - company: me.frm.doc.company - } + company: me.frm.doc.company, + }, }); - }, __("Get Items From")); + }, + __("Get Items From") + ); } else if (this.frm.doc.docstatus === 1) { let schedules = me.frm.doc.schedules; - let flag = schedules.some(schedule => schedule.completion_status === "Pending"); + let flag = schedules.some((schedule) => schedule.completion_status === "Pending"); if (flag) { - this.frm.add_custom_button(__('Maintenance Visit'), function () { - let options = ""; + this.frm.add_custom_button( + __("Maintenance Visit"), + function () { + let options = ""; - me.frm.call('get_pending_data', {data_type: "items"}).then(r => { - options = r.message; + me.frm.call("get_pending_data", { data_type: "items" }).then((r) => { + options = r.message; - let schedule_id = ""; - let d = new frappe.ui.Dialog({ - title: __("Enter Visit Details"), - fields: [{ - fieldtype: "Select", - fieldname: "item_name", - label: __("Item Name"), - options: options, - reqd: 1, - onchange: function () { - let field = d.get_field("scheduled_date"); - me.frm.call('get_pending_data', - { - item_name: this.value, - data_type: "date" - }).then(r => { - field.df.options = r.message; - field.refresh(); - }); - } - }, - { - label: __('Scheduled Date'), - fieldname: 'scheduled_date', - fieldtype: 'Select', - options: "", - reqd: 1, - onchange: function () { - let field = d.get_field('item_name'); - me.frm.call( - 'get_pending_data', - { - item_name: field.value, - s_date: this.value, - data_type: "id" - }).then(r => { - schedule_id = r.message; - }); - } - }, - ], - primary_action_label: 'Create Visit', - primary_action(values) { - frappe.call({ - method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit", - args: { - item_name: values.item_name, - s_id: schedule_id, - source_name: me.frm.doc.name, + let schedule_id = ""; + let d = new frappe.ui.Dialog({ + title: __("Enter Visit Details"), + fields: [ + { + fieldtype: "Select", + fieldname: "item_name", + label: __("Item Name"), + options: options, + reqd: 1, + onchange: function () { + let field = d.get_field("scheduled_date"); + me.frm + .call("get_pending_data", { + item_name: this.value, + data_type: "date", + }) + .then((r) => { + field.df.options = r.message; + field.refresh(); + }); + }, }, - callback: function (r) { - if (!r.exc) { - frappe.model.sync(r.message); - frappe.set_route("Form", r.message.doctype, r.message.name); - } - } - }); - d.hide(); - } + { + label: __("Scheduled Date"), + fieldname: "scheduled_date", + fieldtype: "Select", + options: "", + reqd: 1, + onchange: function () { + let field = d.get_field("item_name"); + me.frm + .call("get_pending_data", { + item_name: field.value, + s_date: this.value, + data_type: "id", + }) + .then((r) => { + schedule_id = r.message; + }); + }, + }, + ], + primary_action_label: "Create Visit", + primary_action(values) { + frappe.call({ + method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit", + args: { + item_name: values.item_name, + s_id: schedule_id, + source_name: me.frm.doc.name, + }, + callback: function (r) { + if (!r.exc) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + }, + }); + d.hide(); + }, + }); + d.show(); }); - d.show(); - }); - }, __('Create')); + }, + __("Create") + ); } } } - }; -extend_cscript(cur_frm.cscript, new erpnext.maintenance.MaintenanceSchedule({frm: cur_frm})); +extend_cscript(cur_frm.cscript, new erpnext.maintenance.MaintenanceSchedule({ frm: cur_frm })); diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 95e2d694a58..08e56e06c73 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -80,7 +80,7 @@ class MaintenanceSchedule(TransactionBase): self.update_amc_date(serial_nos, d.end_date) no_email_sp = [] - if d.sales_person not in email_map: + if d.sales_person and d.sales_person not in email_map: sp = frappe.get_doc("Sales Person", d.sales_person) try: email_map[d.sales_person] = sp.get_email_id() @@ -94,12 +94,11 @@ class MaintenanceSchedule(TransactionBase): ).format(self.owner, "
    " + "
    ".join(no_email_sp)) ) - scheduled_date = frappe.db.sql( - """select scheduled_date from - `tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and - parent=%s""", - (d.sales_person, d.item_code, self.name), - as_dict=1, + scheduled_date = frappe.db.get_all( + "Maintenance Schedule Detail", + {"parent": self.name, "item_code": d.item_code}, + ["scheduled_date"], + as_list=False, ) for key in scheduled_date: @@ -127,7 +126,7 @@ class MaintenanceSchedule(TransactionBase): date_diff = (getdate(end_date) - getdate(start_date)).days add_by = date_diff / no_of_visit - for visit in range(cint(no_of_visit)): + for _visit in range(cint(no_of_visit)): if getdate(start_date_copy) < getdate(end_date): start_date_copy = add_days(start_date_copy, add_by) if len(schedule_list) < no_of_visit: @@ -154,9 +153,8 @@ class MaintenanceSchedule(TransactionBase): ) if not validated and holidays: - # max iterations = len(holidays) - for i in range(len(holidays)): + for _i in range(len(holidays)): if schedule_date in holidays: schedule_date = add_days(schedule_date, -1) else: @@ -195,8 +193,6 @@ class MaintenanceSchedule(TransactionBase): throw(_("Please select Start Date and End Date for Item {0}").format(d.item_code)) elif not d.no_of_visits: throw(_("Please mention no of visits required")) - elif not d.sales_person: - throw(_("Please select a Sales Person for item: {0}").format(d.item_name)) if getdate(d.start_date) >= getdate(d.end_date): throw(_("Start date should be less than end date for Item {0}").format(d.item_code)) @@ -217,7 +213,7 @@ class MaintenanceSchedule(TransactionBase): doc_before_save = self.get_doc_before_save() if not doc_before_save: return - for prev_item, item in zip(doc_before_save.items, self.items): + for prev_item, item in zip(doc_before_save.items, self.items, strict=False): fields = [ "item_code", "start_date", @@ -282,9 +278,7 @@ class MaintenanceSchedule(TransactionBase): ) ) - if sr_details.amc_expiry_date and getdate(sr_details.amc_expiry_date) >= getdate( - amc_start_date - ): + if sr_details.amc_expiry_date and getdate(sr_details.amc_expiry_date) >= getdate(amc_start_date): throw( _("Serial No {0} is under maintenance contract upto {1}").format( serial_no, sr_details.amc_expiry_date @@ -392,16 +386,28 @@ def get_serial_nos_from_schedule(item_code, schedule=None): def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None): from frappe.model.mapper import get_mapped_doc + def condition(doc): + if s_id: + return doc.name == s_id + elif item_name: + return doc.item_name == item_name + + return True + def update_status_and_detail(source, target, parent): target.maintenance_type = "Scheduled" - target.maintenance_schedule_detail = s_id def update_serial(source, target, parent): - serial_nos = get_serial_nos(target.serial_no) - if len(serial_nos) == 1: - target.serial_no = serial_nos[0] - else: - target.serial_no = "" + if source.item_reference: + if serial_nos := frappe.db.get_value( + "Maintenance Schedule Item", source.item_reference, "serial_no" + ): + serial_nos = serial_nos.split("\n") + + if len(serial_nos) == 1: + target.serial_no = serial_nos[0] + else: + target.serial_no = "" doclist = get_mapped_doc( "Maintenance Schedule", @@ -413,10 +419,13 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No "validation": {"docstatus": ["=", 1]}, "postprocess": update_status_and_detail, }, - "Maintenance Schedule Item": { + "Maintenance Schedule Detail": { "doctype": "Maintenance Visit Purpose", - "condition": lambda doc: doc.item_name == item_name if item_name else True, - "field_map": {"sales_person": "service_person"}, + "condition": condition, + "field_map": { + "sales_person": "service_person", + "name": "maintenance_schedule_detail", + }, "postprocess": update_serial, }, }, diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index e2f6cb3a6cc..0a05791b1e9 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -2,11 +2,11 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.maintenance"); -frappe.ui.form.on('Maintenance Visit', { +frappe.ui.form.on("Maintenance Visit", { setup: function (frm) { - frm.set_query('contact_person', erpnext.queries.contact_query); - frm.set_query('customer_address', erpnext.queries.address_query); - frm.set_query('customer', erpnext.queries.customer); + frm.set_query("contact_person", erpnext.queries.contact_query); + frm.set_query("customer_address", erpnext.queries.address_query); + frm.set_query("customer", erpnext.queries.customer); }, onload: function (frm) { // filters for serial no based on item code @@ -15,42 +15,44 @@ frappe.ui.form.on('Maintenance Visit', { if (!item_code) { return; } - frappe.call({ - method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule", - args: { - schedule: frm.doc.maintenance_schedule, - item_code: item_code - } - }).then((r) => { - let serial_nos = r.message; - frm.set_query('serial_no', 'purposes', () => { - if (serial_nos.length > 0) { + frappe + .call({ + method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule", + args: { + schedule: frm.doc.maintenance_schedule, + item_code: item_code, + }, + }) + .then((r) => { + let serial_nos = r.message; + frm.set_query("serial_no", "purposes", () => { + if (serial_nos.length > 0) { + return { + filters: { + item_code: item_code, + name: ["in", serial_nos], + }, + }; + } return { filters: { - 'item_code': item_code, - 'name': ["in", serial_nos] - } + item_code: item_code, + }, }; - } - return { - filters: { - 'item_code': item_code - } - }; + }); }); - }); } else { - frm.set_query('serial_no', 'purposes', (frm, cdt, cdn) => { + frm.set_query("serial_no", "purposes", (frm, cdt, cdn) => { let row = locals[cdt][cdn]; return { filters: { - 'item_code': row.item_code - } + item_code: row.item_code, + }, }; }); } if (!frm.doc.status) { - frm.set_value({ status: 'Draft' }); + frm.set_value({ status: "Draft" }); } if (frm.doc.__islocal) { frm.set_value({ mntc_date: frappe.datetime.get_today() }); @@ -60,25 +62,26 @@ frappe.ui.form.on('Maintenance Visit', { erpnext.utils.get_party_details(frm); }, customer_address: function (frm) { - erpnext.utils.get_address_display(frm, 'customer_address', 'address_display'); + erpnext.utils.get_address_display(frm, "customer_address", "address_display"); }, contact_person: function (frm) { erpnext.utils.get_contact_details(frm); - } -}) + }, +}); // TODO commonify this code erpnext.maintenance.MaintenanceVisit = class MaintenanceVisit extends frappe.ui.form.Controller { refresh() { - frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} + frappe.dynamic_link = { doc: this.frm.doc, fieldname: "customer", doctype: "Customer" }; var me = this; if (this.frm.doc.docstatus === 0) { - this.frm.add_custom_button(__('Maintenance Schedule'), + this.frm.add_custom_button( + __("Maintenance Schedule"), function () { if (!me.frm.doc.customer) { - frappe.msgprint(__('Please select Customer first')); + frappe.msgprint(__("Please select Customer first")); return; } erpnext.utils.map_current_doc({ @@ -90,11 +93,14 @@ erpnext.maintenance.MaintenanceVisit = class MaintenanceVisit extends frappe.ui. }, get_query_filters: { docstatus: 1, - company: me.frm.doc.company - } - }) - }, __("Get Items From")); - this.frm.add_custom_button(__('Warranty Claim'), + company: me.frm.doc.company, + }, + }); + }, + __("Get Items From") + ); + this.frm.add_custom_button( + __("Warranty Claim"), function () { erpnext.utils.map_current_doc({ method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit", @@ -106,14 +112,17 @@ erpnext.maintenance.MaintenanceVisit = class MaintenanceVisit extends frappe.ui. }, get_query_filters: { status: ["in", "Open, Work in Progress"], - company: me.frm.doc.company - } - }) - }, __("Get Items From")); - this.frm.add_custom_button(__('Sales Order'), + company: me.frm.doc.company, + }, + }); + }, + __("Get Items From") + ); + this.frm.add_custom_button( + __("Sales Order"), function () { if (!me.frm.doc.customer) { - frappe.msgprint(__('Please select Customer first')); + frappe.msgprint(__("Please select Customer first")); return; } erpnext.utils.map_current_doc({ @@ -127,11 +136,13 @@ erpnext.maintenance.MaintenanceVisit = class MaintenanceVisit extends frappe.ui. docstatus: 1, company: me.frm.doc.company, order_type: me.frm.doc.order_type, - } - }) - }, __("Get Items From")); + }, + }); + }, + __("Get Items From") + ); } } }; -extend_cscript(cur_frm.cscript, new erpnext.maintenance.MaintenanceVisit({frm: cur_frm})); +extend_cscript(cur_frm.cscript, new erpnext.maintenance.MaintenanceVisit({ frm: cur_frm })); diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py index 0d319bf7424..2254eb66f04 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py @@ -23,20 +23,43 @@ class MaintenanceVisit(TransactionBase): frappe.throw(_("Add Items in the Purpose Table"), title=_("Purposes Required")) def validate_maintenance_date(self): - if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail: - item_ref = frappe.db.get_value( - "Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference" - ) - if item_ref: - start_date, end_date = frappe.db.get_value( - "Maintenance Schedule Item", item_ref, ["start_date", "end_date"] + if self.maintenance_type == "Scheduled": + if self.maintenance_schedule_detail: + item_ref = frappe.db.get_value( + "Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference" ) - if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime( - self.mntc_date - ) > get_datetime(end_date): - frappe.throw( - _("Date must be between {0} and {1}").format(format_date(start_date), format_date(end_date)) + if item_ref: + start_date, end_date = frappe.db.get_value( + "Maintenance Schedule Item", item_ref, ["start_date", "end_date"] ) + if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime( + self.mntc_date + ) > get_datetime(end_date): + frappe.throw( + _("Date must be between {0} and {1}").format( + format_date(start_date), format_date(end_date) + ) + ) + else: + for purpose in self.purposes: + if purpose.maintenance_schedule_detail: + item_ref = frappe.db.get_value( + "Maintenance Schedule Detail", + purpose.maintenance_schedule_detail, + "item_reference", + ) + if item_ref: + start_date, end_date = frappe.db.get_value( + "Maintenance Schedule Item", item_ref, ["start_date", "end_date"] + ) + if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime( + self.mntc_date + ) > get_datetime(end_date): + frappe.throw( + _("Date must be between {0} and {1}").format( + format_date(start_date), format_date(end_date) + ) + ) def validate(self): self.validate_serial_no() @@ -49,6 +72,7 @@ class MaintenanceVisit(TransactionBase): if not cancel: status = self.completion_status actual_date = self.mntc_date + if self.maintenance_schedule_detail: frappe.db.set_value( "Maintenance Schedule Detail", self.maintenance_schedule_detail, "completion_status", status @@ -56,6 +80,21 @@ class MaintenanceVisit(TransactionBase): frappe.db.set_value( "Maintenance Schedule Detail", self.maintenance_schedule_detail, "actual_date", actual_date ) + else: + for purpose in self.purposes: + if purpose.maintenance_schedule_detail: + frappe.db.set_value( + "Maintenance Schedule Detail", + purpose.maintenance_schedule_detail, + "completion_status", + status, + ) + frappe.db.set_value( + "Maintenance Schedule Detail", + purpose.maintenance_schedule_detail, + "actual_date", + actual_date, + ) def update_customer_issue(self, flag): if not self.maintenance_schedule: diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit_list.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit_list.js index e98979deb1b..c47706e33d9 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit_list.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit_list.js @@ -1,11 +1,15 @@ -frappe.listview_settings['Maintenance Visit'] = { +frappe.listview_settings["Maintenance Visit"] = { add_fields: ["customer", "customer_name", "completion_status", "maintenance_type"], - get_indicator: function(doc) { + get_indicator: function (doc) { var s = doc.completion_status || "Pending"; - return [__(s), { - "Pending": "blue", - "Partially Completed": "orange", - "Fully Completed": "green" - }[s], "completion_status,=," + doc.completion_status]; - } + return [ + __(s), + { + Pending: "blue", + "Partially Completed": "orange", + "Fully Completed": "green", + }[s], + "completion_status,=," + doc.completion_status, + ]; + }, }; diff --git a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json index ba053555531..a5a63c4c4de 100644 --- a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json +++ b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json @@ -17,7 +17,8 @@ "work_details", "work_done", "prevdoc_doctype", - "prevdoc_docname" + "prevdoc_docname", + "maintenance_schedule_detail" ], "fields": [ { @@ -49,6 +50,8 @@ "options": "Serial No" }, { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, "fieldname": "description", "fieldtype": "Text Editor", "in_list_view": 1, @@ -56,7 +59,6 @@ "oldfieldname": "description", "oldfieldtype": "Small Text", "print_width": "300px", - "reqd": 1, "width": "300px" }, { @@ -103,12 +105,19 @@ { "fieldname": "section_break_6", "fieldtype": "Section Break" + }, + { + "fieldname": "maintenance_schedule_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "Maintenance Schedule Detail", + "options": "Maintenance Schedule Detail" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-02-27 11:09:33.114458", + "modified": "2024-01-05 21:46:53.239830", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit Purpose", diff --git a/erpnext/manufacturing/dashboard_chart/completed_operation/completed_operation.json b/erpnext/manufacturing/dashboard_chart/completed_operation/completed_operation.json index d74ae2faf4d..146214af429 100644 --- a/erpnext/manufacturing/dashboard_chart/completed_operation/completed_operation.json +++ b/erpnext/manufacturing/dashboard_chart/completed_operation/completed_operation.json @@ -12,12 +12,13 @@ "is_public": 1, "is_standard": 1, "last_synced_on": "2020-07-21 16:57:09.767009", - "modified": "2020-07-21 16:57:55.719802", + "modified": "2024-01-10 12:21:25.134075", "modified_by": "Administrator", "module": "Manufacturing", "name": "Completed Operation", "number_of_groups": 0, "owner": "Administrator", + "parent_document_type": "Work Order", "time_interval": "Quarterly", "timeseries": 1, "timespan": "Last Year", diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js index 7b26a14a57b..ead247cc222 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js @@ -1,60 +1,72 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Blanket Order', { - onload: function(frm) { - frm.trigger('set_tc_name_filter'); +frappe.ui.form.on("Blanket Order", { + onload: function (frm) { + frm.trigger("set_tc_name_filter"); }, - setup: function(frm) { + setup: function (frm) { frm.custom_make_buttons = { - 'Purchase Order': 'Purchase Order', - 'Sales Order': 'Sales Order', - 'Quotation': 'Quotation', + "Purchase Order": "Purchase Order", + "Sales Order": "Sales Order", + Quotation: "Quotation", }; frm.add_fetch("customer", "customer_name", "customer_name"); frm.add_fetch("supplier", "supplier_name", "supplier_name"); }, - refresh: function(frm) { + refresh: function (frm) { erpnext.hide_company(); if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) { - frm.add_custom_button(__("Sales Order"), function() { - frappe.model.open_mapped_doc({ - method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order", - frm: frm, - args: { - doctype: 'Sales Order' - } - }); - }, __('Create')); + frm.add_custom_button( + __("Sales Order"), + function () { + frappe.model.open_mapped_doc({ + method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order", + frm: frm, + args: { + doctype: "Sales Order", + }, + }); + }, + __("Create") + ); - frm.add_custom_button(__("Quotation"), function() { - frappe.model.open_mapped_doc({ - method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order", - frm: frm, - args: { - doctype: 'Quotation' - } - }); - }, __('Create')); + frm.add_custom_button( + __("Quotation"), + function () { + frappe.model.open_mapped_doc({ + method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order", + frm: frm, + args: { + doctype: "Quotation", + }, + }); + }, + __("Create") + ); } if (frm.doc.supplier && frm.doc.docstatus === 1) { - frm.add_custom_button(__("Purchase Order"), function(){ - frappe.model.open_mapped_doc({ - method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order", - frm: frm, - args: { - doctype: 'Purchase Order' - } - }); - }, __('Create')); + frm.add_custom_button( + __("Purchase Order"), + function () { + frappe.model.open_mapped_doc({ + method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order", + frm: frm, + args: { + doctype: "Purchase Order", + }, + }); + }, + __("Create") + ); } }, - onload_post_render: function(frm) { + onload_post_render: function (frm) { frm.get_field("items").grid.set_multiple_add("item_code", "qty"); }, @@ -66,28 +78,28 @@ frappe.ui.form.on('Blanket Order', { }); }, - set_tc_name_filter: function(frm) { - if (frm.doc.blanket_order_type === 'Selling') { - frm.set_df_property("customer","reqd", 1); - frm.set_df_property("supplier","reqd", 0); + set_tc_name_filter: function (frm) { + if (frm.doc.blanket_order_type === "Selling") { + frm.set_df_property("customer", "reqd", 1); + frm.set_df_property("supplier", "reqd", 0); frm.set_value("supplier", ""); - frm.set_query("tc_name", function() { + frm.set_query("tc_name", function () { return { filters: { selling: 1 } }; }); } - if (frm.doc.blanket_order_type === 'Purchasing') { - frm.set_df_property("supplier","reqd", 1); - frm.set_df_property("customer","reqd", 0); + if (frm.doc.blanket_order_type === "Purchasing") { + frm.set_df_property("supplier", "reqd", 1); + frm.set_df_property("customer", "reqd", 0); frm.set_value("customer", ""); - frm.set_query("tc_name", function() { + frm.set_query("tc_name", function () { return { filters: { buying: 1 } }; }); } }, blanket_order_type: function (frm) { - frm.trigger('set_tc_name_filter'); - } + frm.trigger("set_tc_name_filter"); + }, }); diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index 0135a4f9712..aaa127591fe 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -16,11 +16,49 @@ class BlanketOrder(Document): def validate(self): self.validate_dates() self.validate_duplicate_items() + self.set_party_item_code() def validate_dates(self): if getdate(self.from_date) > getdate(self.to_date): frappe.throw(_("From date cannot be greater than To date")) + def set_party_item_code(self): + item_ref = {} + if self.blanket_order_type == "Selling": + item_ref = self.get_customer_items_ref() + else: + item_ref = self.get_supplier_items_ref() + + if not item_ref: + return + + for row in self.items: + row.party_item_code = item_ref.get(row.item_code) + + def get_customer_items_ref(self): + items = [d.item_code for d in self.items] + + return frappe._dict( + frappe.get_all( + "Item Customer Detail", + filters={"parent": ("in", items), "customer_name": self.customer}, + fields=["parent", "ref_code"], + as_list=True, + ) + ) + + def get_supplier_items_ref(self): + items = [d.item_code for d in self.items] + + return frappe._dict( + frappe.get_all( + "Item Supplier", + filters={"parent": ("in", items), "supplier": self.supplier}, + fields=["parent", "supplier_part_no"], + as_list=True, + ) + ) + def validate_duplicate_items(self): item_list = [] for item in self.items: @@ -65,6 +103,7 @@ def make_order(source_name): def update_item(source, target, source_parent): target_qty = source.get("qty") - source.get("ordered_qty") target.qty = target_qty if not flt(target_qty) < 0 else 0 + target.rate = source.get("rate") item = get_item_defaults(target.item_code, source_parent.company) if item: target.item_name = item.get("item_name") @@ -86,6 +125,10 @@ def make_order(source_name): }, }, ) + + if target_doc.doctype == "Purchase Order": + target_doc.set_missing_values() + return target_doc @@ -118,7 +161,7 @@ def validate_against_blanket_order(order_doc): allowed_qty = remaining_qty + (remaining_qty * (allowance / 100)) if allowed_qty < item_data[item.item_code]: frappe.throw( - _("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format( - item.item_code, allowed_qty, bo_name - ) + _( + "Item {0} cannot be ordered more than {1} against Blanket Order {2}." + ).format(item.item_code, allowed_qty, bo_name) ) diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index e9fc25b5bcb..a6eb18f47bc 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -5,6 +5,7 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import add_months, today from erpnext import get_company_currency +from erpnext.stock.doctype.item.test_item import make_item from .blanket_order import make_order @@ -90,6 +91,28 @@ class TestBlanketOrder(FrappeTestCase): frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10) po.submit() + def test_party_item_code(self): + item_doc = make_item("_Test Item 1 for Blanket Order") + item_code = item_doc.name + + customer = "_Test Customer" + supplier = "_Test Supplier" + + if not frappe.db.exists("Item Customer Detail", {"customer_name": customer, "parent": item_code}): + item_doc.append("customer_items", {"customer_name": customer, "ref_code": "CUST-REF-1"}) + item_doc.save() + + if not frappe.db.exists("Item Supplier", {"supplier": supplier, "parent": item_code}): + item_doc.append("supplier_items", {"supplier": supplier, "supplier_part_no": "SUPP-PART-1"}) + item_doc.save() + + # Blanket Order for Selling + bo = make_blanket_order(blanket_order_type="Selling", customer=customer, item_code=item_code) + self.assertEqual(bo.items[0].party_item_code, "CUST-REF-1") + + bo = make_blanket_order(blanket_order_type="Purchasing", supplier=supplier, item_code=item_code) + self.assertEqual(bo.items[0].party_item_code, "SUPP-PART-1") + def make_blanket_order(**args): args = frappe._dict(args) diff --git a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json index 977ad547f55..aa7831fd6b8 100644 --- a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json +++ b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2018-05-24 07:20:04.255236", "doctype": "DocType", "editable_grid": 1, @@ -6,6 +7,7 @@ "field_order": [ "item_code", "item_name", + "party_item_code", "column_break_3", "qty", "rate", @@ -62,10 +64,17 @@ "fieldname": "terms_and_conditions", "fieldtype": "Text", "label": "Terms and Conditions" + }, + { + "fieldname": "party_item_code", + "fieldtype": "Data", + "label": "Party Item Code", + "read_only": 1 } ], "istable": 1, - "modified": "2019-11-18 19:37:46.245878", + "links": [], + "modified": "2024-02-14 18:25:26.479672", "modified_by": "Administrator", "module": "Manufacturing", "name": "Blanket Order Item", @@ -74,5 +83,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 231c476734c..b7ab6ef3872 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -6,139 +6,150 @@ frappe.provide("erpnext.bom"); frappe.ui.form.on("BOM", { setup(frm) { frm.custom_make_buttons = { - 'Work Order': 'Work Order', - 'Quality Inspection': 'Quality Inspection' + "Work Order": "Work Order", + "Quality Inspection": "Quality Inspection", }; - frm.set_query("bom_no", "items", function() { + frm.set_query("bom_no", "items", function () { return { filters: { - 'currency': frm.doc.currency, - 'company': frm.doc.company - } + currency: frm.doc.currency, + company: frm.doc.company, + }, }; }); - frm.set_query("source_warehouse", "items", function() { + frm.set_query("source_warehouse", "items", function () { return { filters: { - 'company': frm.doc.company - } + company: frm.doc.company, + }, }; }); - frm.set_query("item", function() { + frm.set_query("item", function () { return { query: "erpnext.manufacturing.doctype.bom.bom.item_query", filters: { - "is_stock_item": 1 - } + is_stock_item: 1, + }, }; }); - frm.set_query("project", function() { - return{ - filters:[ - ['Project', 'status', 'not in', 'Completed, Cancelled'] - ] + frm.set_query("project", function () { + return { + filters: [["Project", "status", "not in", "Completed, Cancelled"]], }; }); - frm.set_query("item_code", "items", function(doc) { + frm.set_query("item_code", "items", function (doc) { return { query: "erpnext.manufacturing.doctype.bom.bom.item_query", filters: { - "include_item_in_manufacturing": 1, - "is_fixed_asset": 0 - } + include_item_in_manufacturing: 1, + is_fixed_asset: 0, + }, }; }); - frm.set_query("bom_no", "items", function(doc, cdt, cdn) { + frm.set_query("bom_no", "items", function (doc, cdt, cdn) { var d = locals[cdt][cdn]; return { filters: { - 'item': d.item_code, - 'is_active': 1, - 'docstatus': 1 - } + item: d.item_code, + is_active: 1, + docstatus: 1, + }, }; }); }, - validate: function(frm) { + validate: function (frm) { if (frm.doc.fg_based_operating_cost && frm.doc.with_operations) { - frappe.throw({message: __("Please check either with operations or FG Based Operating Cost."), title: __("Mandatory")}); + frappe.throw({ + message: __("Please check either with operations or FG Based Operating Cost."), + title: __("Mandatory"), + }); } }, - with_operations: function(frm) { + with_operations: function (frm) { frm.set_df_property("fg_based_operating_cost", "hidden", frm.doc.with_operations ? 1 : 0); }, - fg_based_operating_cost: function(frm) { + fg_based_operating_cost: function (frm) { frm.set_df_property("with_operations", "hidden", frm.doc.fg_based_operating_cost ? 1 : 0); }, - onload_post_render: function(frm) { + onload_post_render: function (frm) { frm.get_field("items").grid.set_multiple_add("item_code", "qty"); }, refresh(frm) { frm.toggle_enable("item", frm.doc.__islocal); - frm.set_indicator_formatter('item_code', - function(doc) { - if (doc.original_item){ - return (doc.item_code != doc.original_item) ? "orange" : "" - } - return "" + frm.set_indicator_formatter("item_code", function (doc) { + if (doc.original_item) { + return doc.item_code != doc.original_item ? "orange" : ""; } - ) + return ""; + }); - if (!frm.is_new() && frm.doc.docstatus<2) { - frm.add_custom_button(__("Update Cost"), function() { + if (!frm.is_new() && frm.doc.docstatus < 2) { + frm.add_custom_button(__("Update Cost"), function () { frm.events.update_cost(frm, true); }); - frm.add_custom_button(__("Browse BOM"), function() { + frm.add_custom_button(__("Browse BOM"), function () { frappe.route_options = { - "bom": frm.doc.name + bom: frm.doc.name, }; frappe.set_route("Tree", "BOM"); }); } if (!frm.is_new() && !frm.doc.docstatus == 0) { - frm.add_custom_button(__("New Version"), function() { + frm.add_custom_button(__("New Version"), function () { let new_bom = frappe.model.copy_doc(frm.doc); frappe.set_route("Form", "BOM", new_bom.name); }); } - if(frm.doc.docstatus==1) { - frm.add_custom_button(__("Work Order"), function() { - frm.trigger("make_work_order"); - }, __("Create")); + if (frm.doc.docstatus == 1) { + frm.add_custom_button( + __("Work Order"), + function () { + frm.trigger("make_work_order"); + }, + __("Create") + ); if (frm.doc.has_variants) { - frm.add_custom_button(__("Variant BOM"), function() { - frm.trigger("make_variant_bom"); - }, __("Create")); + frm.add_custom_button( + __("Variant BOM"), + function () { + frm.trigger("make_variant_bom"); + }, + __("Create") + ); } if (frm.doc.inspection_required) { - frm.add_custom_button(__("Quality Inspection"), function() { - frm.trigger("make_quality_inspection"); - }, __("Create")); + frm.add_custom_button( + __("Quality Inspection"), + function () { + frm.trigger("make_quality_inspection"); + }, + __("Create") + ); } - frm.page.set_inner_btn_group_as_primary(__('Create')); + frm.page.set_inner_btn_group_as_primary(__("Create")); } - if(frm.doc.items && frm.doc.allow_alternative_item) { - const has_alternative = frm.doc.items.find(i => i.allow_alternative_item === 1); + if (frm.doc.items && frm.doc.allow_alternative_item) { + const has_alternative = frm.doc.items.find((i) => i.allow_alternative_item === 1); if (frm.doc.docstatus == 0 && has_alternative) { - frm.add_custom_button(__('Alternate Item'), () => { + frm.add_custom_button(__("Alternate Item"), () => { erpnext.utils.select_alternate_items({ frm: frm, child_docname: "items", @@ -146,23 +157,26 @@ frappe.ui.form.on("BOM", { child_doctype: "BOM Item", original_item_field: "original_item", condition: (d) => { - if (d.allow_alternative_item) {return true;} - } - }) + if (d.allow_alternative_item) { + return true; + } + }, + }); }); } } - if (frm.doc.has_variants) { - frm.set_intro(__('This is a Template BOM and will be used to make the work order for {0} of the item {1}', - [ + frm.set_intro( + __("This is a Template BOM and will be used to make the work order for {0} of the item {1}", [ `variants`, `${frm.doc.item}`, - ]), true); + ]), + true + ); frm.$wrapper.find(".variants-intro").on("click", () => { - frappe.set_route("List", "Item", {"variant_of": frm.doc.item}); + frappe.set_route("List", "Item", { variant_of: frm.doc.item }); }); } }, @@ -176,38 +190,43 @@ frappe.ui.form.on("BOM", { item: item, qty: data.qty || 0.0, project: frm.doc.project, - variant_items: variant_items + variant_items: variant_items, }, freeze: true, callback(r) { - if(r.message) { + if (r.message) { let doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); } - } + }, }); }); }, make_variant_bom(frm) { - frm.events.setup_variant_prompt(frm, "Variant BOM", (frm, item, data, variant_items) => { - frappe.call({ - method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom", - args: { - source_name: frm.doc.name, - bom_no: frm.doc.name, - item: item, - variant_items: variant_items - }, - freeze: true, - callback(r) { - if(r.message) { - let doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name); - } - } - }); - }, true); + frm.events.setup_variant_prompt( + frm, + "Variant BOM", + (frm, item, data, variant_items) => { + frappe.call({ + method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom", + args: { + source_name: frm.doc.name, + bom_no: frm.doc.name, + item: item, + variant_items: variant_items, + }, + freeze: true, + callback(r) { + if (r.message) { + let doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }, + }); + }, + true + ); }, setup_variant_prompt(frm, title, callback, skip_qty_field) { @@ -215,27 +234,27 @@ frappe.ui.form.on("BOM", { if (frm.doc.has_variants) { fields.push({ - fieldtype: 'Link', - label: __('Variant Item'), - fieldname: 'item', + fieldtype: "Link", + label: __("Variant Item"), + fieldname: "item", options: "Item", reqd: 1, get_query() { return { query: "erpnext.controllers.queries.item_query", filters: { - "variant_of": frm.doc.item - } + variant_of: frm.doc.item, + }, }; - } + }, }); } if (!skip_qty_field) { fields.push({ - fieldtype: 'Float', - label: __('Qty To Manufacture'), - fieldname: 'qty', + fieldtype: "Float", + label: __("Qty To Manufacture"), + fieldname: "qty", reqd: 1, default: 1, onchange: () => { @@ -244,29 +263,23 @@ frappe.ui.form.on("BOM", { acc[item.item_code] = item.qty; return acc; }, {}); - const mf_qty = cur_dialog.fields_list.filter( - (f) => f.df.fieldname === "qty" - )[0]?.value; - const items = cur_dialog.fields.filter( - (f) => f.fieldname === "items" - )[0]?.data; + const mf_qty = cur_dialog.fields_list.filter((f) => f.df.fieldname === "qty")[0]?.value; + const items = cur_dialog.fields.filter((f) => f.fieldname === "items")[0]?.data; if (!items) { return; } items.forEach((item) => { - item.qty = - (variant_items_map[item.item_code] * mf_qty) / - quantity; + item.qty = (variant_items_map[item.item_code] * mf_qty) / quantity; }); cur_dialog.refresh(); - } + }, }); } - var has_template_rm = frm.doc.items.filter(d => d.has_variants === 1) || []; + var has_template_rm = frm.doc.items.filter((d) => d.has_variants === 1) || []; if (has_template_rm && has_template_rm.length > 0) { fields.push({ fieldname: "items", @@ -296,10 +309,10 @@ frappe.ui.form.on("BOM", { return { query: "erpnext.controllers.queries.item_query", filters: { - "variant_of": data.item_code - } + variant_of: data.item_code, + }, }; - } + }, }, { fieldname: "qty", @@ -312,44 +325,48 @@ frappe.ui.form.on("BOM", { fieldname: "source_warehouse", label: __("Source Warehouse"), fieldtype: "Link", - options: "Warehouse" + options: "Warehouse", }, { fieldname: "operation", label: __("Operation"), fieldtype: "Data", hidden: 1, - } + }, ], in_place_edit: true, data: [], - get_data () { + get_data() { return []; }, }); } - let dialog = frappe.prompt(fields, data => { - let item = data.item || frm.doc.item; - let variant_items = data.items || []; + let dialog = frappe.prompt( + fields, + (data) => { + let item = data.item || frm.doc.item; + let variant_items = data.items || []; - variant_items.forEach(d => { - if (!d.variant_item_code) { - frappe.throw(__("Select variant item code for the template item {0}", [d.item_code])); - } - }) + variant_items.forEach((d) => { + if (!d.variant_item_code) { + frappe.throw(__("Select variant item code for the template item {0}", [d.item_code])); + } + }); - callback(frm, item, data, variant_items); + callback(frm, item, data, variant_items); + }, + __(title), + __("Create") + ); - }, __(title), __("Create")); - - has_template_rm.forEach(d => { + has_template_rm.forEach((d) => { dialog.fields_dict.items.df.data.push({ - "item_code": d.item_code, - "variant_item_code": "", - "qty": d.qty, - "source_warehouse": d.source_warehouse, - "operation": d.operation + item_code: d.item_code, + variant_item_code: "", + qty: d.qty, + source_warehouse: d.source_warehouse, + operation: d.operation, }); }); @@ -361,11 +378,11 @@ frappe.ui.form.on("BOM", { make_quality_inspection(frm) { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.quality_inspection.quality_inspection.make_quality_inspection", - frm: frm - }) + frm: frm, + }); }, - update_cost(frm, save_doc=false) { + update_cost(frm, save_doc = false) { return frappe.call({ doc: frm.doc, method: "update_cost", @@ -373,12 +390,12 @@ frappe.ui.form.on("BOM", { args: { update_parent: true, save: save_doc, - from_child_bom: false + from_child_bom: false, }, callback(r) { refresh_field("items"); - if(!r.exc) frm.refresh_fields(); - } + if (!r.exc) frm.refresh_fields(); + }, }); }, @@ -400,39 +417,39 @@ frappe.ui.form.on("BOM", { erpnext.bom.calculate_op_cost(frm.doc); erpnext.bom.calculate_total(frm.doc); } - } + }, }); } }, process_loss_percentage(frm) { - let qty = 0.0 + let qty = 0.0; if (frm.doc.process_loss_percentage) { qty = (frm.doc.quantity * frm.doc.process_loss_percentage) / 100; } frm.set_value("process_loss_qty", qty); - } + }, }); erpnext.bom.BomController = class BomController extends erpnext.TransactionController { conversion_rate(doc) { - if(this.frm.doc.currency === this.get_company_currency()) { + if (this.frm.doc.currency === this.get_company_currency()) { this.frm.set_value("conversion_rate", 1.0); } else { erpnext.bom.update_cost(doc); } } - item_code(doc, cdt, cdn){ + item_code(doc, cdt, cdn) { var scrap_items = false; var child = locals[cdt][cdn]; - if (child.doctype == 'BOM Scrap Item') { + if (child.doctype == "BOM Scrap Item") { scrap_items = true; } if (child.bom_no) { - child.bom_no = ''; + child.bom_no = ""; } get_bom_material_detail(doc, cdt, cdn, scrap_items); @@ -460,26 +477,26 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr } }; -extend_cscript(cur_frm.cscript, new erpnext.bom.BomController({frm: cur_frm})); +extend_cscript(cur_frm.cscript, new erpnext.bom.BomController({ frm: cur_frm })); -cur_frm.cscript.hour_rate = function(doc) { +cur_frm.cscript.hour_rate = function (doc) { erpnext.bom.calculate_op_cost(doc); erpnext.bom.calculate_total(doc); }; cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate; -cur_frm.cscript.bom_no = function(doc, cdt, cdn) { +cur_frm.cscript.bom_no = function (doc, cdt, cdn) { get_bom_material_detail(doc, cdt, cdn, false); }; -cur_frm.cscript.is_default = function(doc) { +cur_frm.cscript.is_default = function (doc) { if (doc.is_default) cur_frm.set_value("is_active", 1); }; -var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { +var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) { if (!doc.company) { - frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")}); + frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") }); } var d = locals[cdt][cdn]; @@ -488,20 +505,20 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { doc: doc, method: "get_bom_material_detail", args: { - "company": doc.company, - "item_code": d.item_code, - "bom_no": d.bom_no != null ? d.bom_no: '', - "scrap_items": scrap_items, - "qty": d.qty, - "stock_qty": d.stock_qty, - "include_item_in_manufacturing": d.include_item_in_manufacturing, - "uom": d.uom, - "stock_uom": d.stock_uom, - "conversion_factor": d.conversion_factor, - "sourced_by_supplier": d.sourced_by_supplier, - "do_not_explode": d.do_not_explode + company: doc.company, + item_code: d.item_code, + bom_no: d.bom_no != null ? d.bom_no : "", + scrap_items: scrap_items, + qty: d.qty, + stock_qty: d.stock_qty, + include_item_in_manufacturing: d.include_item_in_manufacturing, + uom: d.uom, + stock_uom: d.stock_uom, + conversion_factor: d.conversion_factor, + sourced_by_supplier: d.sourced_by_supplier, + do_not_explode: d.do_not_explode, }, - callback: function(r) { + callback: function (r) { d = locals[cdt][cdn]; $.extend(d, r.message); @@ -513,18 +530,18 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); }, - freeze: true + freeze: true, }); } }; -cur_frm.cscript.qty = function(doc) { +cur_frm.cscript.qty = function (doc) { erpnext.bom.calculate_rm_cost(doc); erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); }; -cur_frm.cscript.rate = function(doc, cdt, cdn) { +cur_frm.cscript.rate = function (doc, cdt, cdn) { var d = locals[cdt][cdn]; const is_scrap_item = cdt == "BOM Scrap Item"; @@ -538,52 +555,60 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) { } }; -erpnext.bom.update_cost = function(doc) { +erpnext.bom.update_cost = function (doc) { erpnext.bom.calculate_op_cost(doc); erpnext.bom.calculate_rm_cost(doc); erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); }; -erpnext.bom.calculate_op_cost = function(doc) { +erpnext.bom.calculate_op_cost = function (doc) { doc.operating_cost = 0.0; doc.base_operating_cost = 0.0; - if(doc.with_operations) { + if (doc.with_operations) { doc.operations.forEach((item) => { - let operating_cost = flt(flt(item.hour_rate) * flt(item.time_in_mins) / 60, 2); + let operating_cost = flt((flt(item.hour_rate) * flt(item.time_in_mins)) / 60, 2); let base_operating_cost = flt(operating_cost * doc.conversion_rate, 2); - frappe.model.set_value('BOM Operation',item.name, { - "operating_cost": operating_cost, - "base_operating_cost": base_operating_cost + frappe.model.set_value("BOM Operation", item.name, { + operating_cost: operating_cost, + base_operating_cost: base_operating_cost, }); doc.operating_cost += operating_cost; doc.base_operating_cost += base_operating_cost; }); - } else if(doc.fg_based_operating_cost) { + } else if (doc.fg_based_operating_cost) { let total_operating_cost = doc.quantity * flt(doc.operating_cost_per_bom_quantity); doc.operating_cost = total_operating_cost; doc.base_operating_cost = flt(total_operating_cost * doc.conversion_rate, 2); } - refresh_field(['operating_cost', 'base_operating_cost']); + refresh_field(["operating_cost", "base_operating_cost"]); }; // rm : raw material -erpnext.bom.calculate_rm_cost = function(doc) { +erpnext.bom.calculate_rm_cost = function (doc) { var rm = doc.items || []; var total_rm_cost = 0; var base_total_rm_cost = 0; - for(var i=0;i { - d.allow_alternative_item = r.allow_alternative_item - }) + frappe.db.get_value("Item", { name: d.item_code }, "allow_alternative_item", (r) => { + d.allow_alternative_item = r.allow_alternative_item; + }); refresh_field("allow_alternative_item", d.name, d.parentfield); }); -frappe.ui.form.on("BOM Item", "sourced_by_supplier", function(frm, cdt, cdn) { +frappe.ui.form.on("BOM Item", "sourced_by_supplier", function (frm, cdt, cdn) { var d = locals[cdt][cdn]; if (d.sourced_by_supplier) { d.rate = 0; @@ -700,7 +729,7 @@ frappe.ui.form.on("BOM Item", "sourced_by_supplier", function(frm, cdt, cdn) { } }); -frappe.ui.form.on("BOM Item", "rate", function(frm, cdt, cdn) { +frappe.ui.form.on("BOM Item", "rate", function (frm, cdt, cdn) { var d = locals[cdt][cdn]; if (d.sourced_by_supplier) { d.rate = 0; @@ -708,37 +737,41 @@ frappe.ui.form.on("BOM Item", "rate", function(frm, cdt, cdn) { } }); -frappe.ui.form.on("BOM Operation", "operations_remove", function(frm) { +frappe.ui.form.on("BOM Operation", "operations_remove", function (frm) { erpnext.bom.calculate_op_cost(frm.doc); erpnext.bom.calculate_total(frm.doc); }); -frappe.ui.form.on("BOM Item", "items_remove", function(frm) { +frappe.ui.form.on("BOM Item", "items_remove", function (frm) { erpnext.bom.calculate_rm_cost(frm.doc); erpnext.bom.calculate_total(frm.doc); }); -frappe.tour['BOM'] = [ +frappe.tour["BOM"] = [ { fieldname: "item", title: "Item", - description: __("Select the Item to be manufactured. The Item name, UoM, Company, and Currency will be fetched automatically.") + description: __( + "Select the Item to be manufactured. The Item name, UoM, Company, and Currency will be fetched automatically." + ), }, { fieldname: "quantity", title: "Quantity", - description: __("Enter the quantity of the Item that will be manufactured from this Bill of Materials.") + description: __( + "Enter the quantity of the Item that will be manufactured from this Bill of Materials." + ), }, { fieldname: "with_operations", title: "With Operations", - description: __("To add Operations tick the 'With Operations' checkbox.") + description: __("To add Operations tick the 'With Operations' checkbox."), }, { fieldname: "items", title: "Raw Materials", - description: __("Select the raw materials (Items) required to manufacture the Item") - } + description: __("Select the raw materials (Items) required to manufacture the Item"), + }, ]; frappe.ui.form.on("BOM Scrap Item", { diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index d02402299e3..28c33e47fb9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -214,6 +214,7 @@ "options": "\nWork Order\nJob Card" }, { + "default": "1", "fieldname": "conversion_rate", "fieldtype": "Float", "label": "Conversion Rate", @@ -606,7 +607,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-04-06 12:47:58.514795", + "modified": "2023-12-26 19:34:08.159312", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", @@ -645,4 +646,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6151af7e3ef..f18ca1f796a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -5,7 +5,6 @@ import functools import re from collections import deque from operator import itemgetter -from typing import Dict, List import frappe from frappe import _ @@ -33,11 +32,9 @@ class BOMTree: # ref: https://docs.python.org/3/reference/datamodel.html#slots __slots__ = ["name", "child_items", "is_bom", "item_code", "qty", "exploded_qty", "bom_qty"] - def __init__( - self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1 - ) -> None: + def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None: self.name = name # name of node, BOM number if is_bom else item_code - self.child_items: List["BOMTree"] = [] # list of child items + self.child_items: list["BOMTree"] = [] # list of child items self.is_bom = is_bom # true if the node is a BOM and not a leaf item self.item_code: str = None # item_code associated with node self.qty = qty # required unit quantity to make one unit of parent item. @@ -63,7 +60,7 @@ class BOMTree: BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty) ) - def level_order_traversal(self) -> List["BOMTree"]: + def level_order_traversal(self) -> list["BOMTree"]: """Get level order traversal of tree. E.g. for following tree the traversal will return list of nodes in order from top to bottom. BOM: @@ -152,7 +149,7 @@ class BOM(WebsiteGenerator): self.name = name @staticmethod - def get_next_version_index(existing_boms: List[str]) -> int: + def get_next_version_index(existing_boms: list[str]) -> int: # split by "/" and "-" delimiters = ["/", "-"] pattern = "|".join(map(re.escape, delimiters)) @@ -341,10 +338,7 @@ class BOM(WebsiteGenerator): return ret_item def validate_bom_currency(self, item): - if ( - item.get("bom_no") - and frappe.db.get_value("BOM", item.get("bom_no"), "currency") != self.currency - ): + if item.get("bom_no") and frappe.db.get_value("BOM", item.get("bom_no"), "currency") != self.currency: frappe.throw( _("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}").format( item.idx, item.bom_no, self.currency @@ -361,9 +355,9 @@ class BOM(WebsiteGenerator): rate = get_valuation_rate(arg) elif arg: # Customer Provided parts and Supplier sourced parts will have zero rate - if not frappe.db.get_value( - "Item", arg["item_code"], "is_customer_provided_item" - ) and not arg.get("sourced_by_supplier"): + if not frappe.db.get_value("Item", arg["item_code"], "is_customer_provided_item") and not arg.get( + "sourced_by_supplier" + ): if arg.get("bom_no") and self.set_rate_of_sub_assembly_item_based_on_bom: rate = flt(self.get_bom_unitcost(arg["bom_no"])) * (arg.get("conversion_factor") or 1) else: @@ -379,7 +373,8 @@ class BOM(WebsiteGenerator): ) else: frappe.msgprint( - _("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]), alert=True + _("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]), + alert=True, ) return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) @@ -661,6 +656,9 @@ class BOM(WebsiteGenerator): base_total_rm_cost = 0 for d in self.get("items"): + if not d.is_stock_item and self.rm_cost_as_per == "Valuation Rate": + continue + old_rate = d.rate d.rate = self.get_rm_rate( { @@ -724,7 +722,7 @@ class BOM(WebsiteGenerator): # Only db_update if changed row.db_update() - def get_rm_rate_map(self) -> Dict[str, float]: + def get_rm_rate_map(self) -> dict[str, float]: "Create Raw Material-Rate map for Exploded Items. Fetch rate from Items table or Subassembly BOM." rm_rate_map = {} @@ -975,8 +973,7 @@ def get_valuation_rate(data): frappe.qb.from_(sle) .select(sle.valuation_rate) .where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0)) - .orderby(sle.posting_date, order=frappe.qb.desc) - .orderby(sle.posting_time, order=frappe.qb.desc) + .orderby(sle.posting_datetime, order=frappe.qb.desc) .orderby(sle.creation, order=frappe.qb.desc) .limit(1) ).run(as_dict=True) @@ -1094,9 +1091,7 @@ def get_bom_items_as_dict( @frappe.whitelist() def get_bom_items(bom, company, qty=1, fetch_exploded=1): - items = get_bom_items_as_dict( - bom, company, qty, fetch_exploded, include_non_stock_items=True - ).values() + items = get_bom_items_as_dict(bom, company, qty, fetch_exploded, include_non_stock_items=True).values() items = list(items) items.sort(key=functools.cmp_to_key(lambda a, b: a.item_code > b.item_code and 1 or -1)) return items @@ -1228,9 +1223,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None): ) if work_order and work_order.additional_operating_cost and work_order.qty: - additional_operating_cost_per_unit = flt(work_order.additional_operating_cost) / flt( - work_order.qty - ) + additional_operating_cost_per_unit = flt(work_order.additional_operating_cost) / flt(work_order.qty) if additional_operating_cost_per_unit: stock_entry.append( @@ -1292,7 +1285,7 @@ def get_bom_diff(bom1, bom2): # check for deletions for d in old_value: - if not d.get(identifier) in new_row_by_identifier: + if d.get(identifier) not in new_row_by_identifier: out.removed.append([df.fieldname, d.as_dict()]) return out @@ -1307,14 +1300,12 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): order_by = "idx desc, name, item_name" fields = ["name", "item_name", "item_group", "description"] - fields.extend( - [field for field in searchfields if not field in ["name", "item_group", "description"]] - ) + fields.extend([field for field in searchfields if field not in ["name", "item_group", "description"]]) searchfields = searchfields + [ field for field in [searchfield or "name", "item_code", "item_group", "item_name"] - if not field in searchfields + if field not in searchfields ] query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())} @@ -1322,12 +1313,12 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): or_cond_filters = {} if txt: for s_field in searchfields: - or_cond_filters[s_field] = ("like", "%{0}%".format(txt)) + or_cond_filters[s_field] = ("like", f"%{txt}%") barcodes = frappe.get_all( "Item Barcode", fields=["distinct parent as item_code"], - filters={"barcode": ("like", "%{0}%".format(txt))}, + filters={"barcode": ("like", f"%{txt}%")}, ) barcodes = [d.item_code for d in barcodes] @@ -1392,3 +1383,45 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): ) return doc + + +def get_op_cost_from_sub_assemblies(bom_no, op_cost=0): + # Get operating cost from sub-assemblies + + bom_items = frappe.get_all( + "BOM Item", filters={"parent": bom_no, "docstatus": 1}, fields=["bom_no"], order_by="idx asc" + ) + + for row in bom_items: + if not row.bom_no: + continue + + if cost := frappe.get_cached_value("BOM", row.bom_no, "operating_cost_per_bom_quantity"): + op_cost += flt(cost) + get_op_cost_from_sub_assemblies(row.bom_no, op_cost) + + return op_cost + + +def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None): + if not scrap_items: + scrap_items = {} + + bom_items = frappe.get_all( + "BOM Item", + filters={"parent": bom_no, "docstatus": 1}, + fields=["bom_no", "qty"], + order_by="idx asc", + ) + + for row in bom_items: + if not row.bom_no: + continue + + qty = flt(row.qty) * flt(qty) + items = get_bom_items_as_dict(row.bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1) + scrap_items.update(items) + + get_scrap_items_from_sub_assemblies(row.bom_no, company, qty, scrap_items) + + return scrap_items diff --git a/erpnext/manufacturing/doctype/bom/bom_list.js b/erpnext/manufacturing/doctype/bom/bom_list.js index 4b5887f180c..a26df545f85 100644 --- a/erpnext/manufacturing/doctype/bom/bom_list.js +++ b/erpnext/manufacturing/doctype/bom/bom_list.js @@ -1,16 +1,16 @@ -frappe.listview_settings['BOM'] = { +frappe.listview_settings["BOM"] = { add_fields: ["is_active", "is_default", "total_cost", "has_variants"], - get_indicator: function(doc) { - if(doc.is_active && doc.has_variants) { + get_indicator: function (doc) { + if (doc.is_active && doc.has_variants) { return [__("Template"), "orange", "has_variants,=,Yes"]; - } else if(doc.is_default) { + } else if (doc.is_default) { return [__("Default"), "green", "is_default,=,Yes"]; - } else if(doc.is_active) { + } else if (doc.is_active) { return [__("Active"), "blue", "is_active,=,Yes"]; - } else if(!doc.is_active) { + } else if (!doc.is_active) { return [__("Not active"), "gray", "is_active,=,No"]; } - } + }, }; frappe.help.youtube_id["BOM"] = "hDV0c1OeWLo"; diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js index fb99add12cf..534de0e654b 100644 --- a/erpnext/manufacturing/doctype/bom/bom_tree.js +++ b/erpnext/manufacturing/doctype/bom/bom_tree.js @@ -1,12 +1,12 @@ frappe.treeview_settings["BOM"] = { - get_tree_nodes: 'erpnext.manufacturing.doctype.bom.bom.get_children', + get_tree_nodes: "erpnext.manufacturing.doctype.bom.bom.get_children", filters: [ { fieldname: "bom", - fieldtype:"Link", + fieldtype: "Link", options: "BOM", - label: __("BOM") - } + label: __("BOM"), + }, ], title: "BOM", breadcrumb: "Manufacturing", @@ -14,21 +14,21 @@ frappe.treeview_settings["BOM"] = { root_label: "BOM", //fieldname from filters get_tree_root: false, show_expand_all: false, - get_label: function(node) { - if(node.data.qty) { + get_label: function (node) { + if (node.data.qty) { return node.data.qty + " x " + node.data.item_code; } else { return node.data.item_code || node.data.value; } }, - onload: function(me) { + onload: function (me) { var label = frappe.get_route()[0] + "/" + frappe.get_route()[1]; - if(frappe.pages[label]) { + if (frappe.pages[label]) { delete frappe.pages[label]; } var filter = me.opts.filters[0]; - if(frappe.route_options && frappe.route_options[filter.fieldname]) { + if (frappe.route_options && frappe.route_options[filter.fieldname]) { var val = frappe.route_options[filter.fieldname]; delete frappe.route_options[filter.fieldname]; filter.default = ""; @@ -41,28 +41,27 @@ frappe.treeview_settings["BOM"] = { toolbar: [ { toggle_btn: true }, { - label:__("Edit"), - condition: function(node) { + label: __("Edit"), + condition: function (node) { return node.expandable; }, - click: function(node) { - + click: function (node) { frappe.set_route("Form", "BOM", node.data.value); - } - } + }, + }, ], menu_items: [ { label: __("New BOM"), - action: function() { - frappe.new_doc("BOM", true) + action: function () { + frappe.new_doc("BOM", true); }, - condition: 'frappe.boot.user.can_create.indexOf("BOM") !== -1' - } + condition: 'frappe.boot.user.can_create.indexOf("BOM") !== -1', + }, ], - onrender: function(node) { - if(node.is_root && node.data.value!="BOM") { - frappe.model.with_doc("BOM", node.data.value, function() { + onrender: function (node) { + if (node.is_root && node.data.value != "BOM") { + frappe.model.with_doc("BOM", node.data.value, function () { var bom = frappe.model.get_doc("BOM", node.data.value); node.data.image = escape(bom.image) || ""; node.data.description = bom.description || ""; @@ -70,5 +69,5 @@ frappe.treeview_settings["BOM"] = { }); } }, - view_template: 'bom_item_preview' -} + view_template: "bom_item_preview", +}; diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 051b475bcc3..d02b51ca6e7 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -10,7 +10,6 @@ from frappe.tests.utils import FrappeTestCase, timeout from frappe.utils import cstr, flt from erpnext.controllers.tests.test_subcontracting_controller import ( - make_stock_in_entry, set_backflush_based_on, ) from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom @@ -200,9 +199,7 @@ class TestBOM(FrappeTestCase): reset_item_valuation_rate( item_code="_Test Item", - warehouse_list=frappe.get_all( - "Warehouse", {"is_group": 0, "company": bom.company}, pluck="name" - ), + warehouse_list=frappe.get_all("Warehouse", {"is_group": 0, "company": bom.company}, pluck="name"), qty=200, rate=200, ) @@ -344,7 +341,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(len(reqd_order), len(created_order)) - for reqd_item, created_item in zip(reqd_order, created_order): + for reqd_item, created_item in zip(reqd_order, created_order, strict=False): self.assertEqual(reqd_item, created_item.item_code) @timeout @@ -383,7 +380,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(len(reqd_order), len(created_order)) - for reqd_item, created_item in zip(reqd_order, created_order): + for reqd_item, created_item in zip(reqd_order, created_order, strict=False): self.assertEqual(reqd_item.item_code, created_item.item_code) self.assertEqual(reqd_item.qty, created_item.qty) self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty) @@ -452,9 +449,7 @@ class TestBOM(FrappeTestCase): test_items = query(txt="_Test") filtered = query(txt="_Test Item 2") - self.assertNotEqual( - len(test_items), len(filtered), msg="Item filtering showing excessive results" - ) + self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results") self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results") @timeout @@ -516,7 +511,6 @@ class TestBOM(FrappeTestCase): @timeout def test_version_index(self): - bom = frappe.new_doc("BOM") version_index_test_cases = [ @@ -571,7 +565,6 @@ class TestBOM(FrappeTestCase): @timeout def test_clear_inpection_quality(self): - bom = frappe.copy_doc(test_records[2], ignore_no_copy=True) bom.docstatus = 0 bom.is_default = 0 @@ -686,9 +679,7 @@ class TestBOM(FrappeTestCase): bom = make_bom(item=fg_item, raw_materials=[rm_item]) - create_stock_reconciliation( - item_code=rm_item, warehouse="_Test Warehouse - _TC", qty=100, rate=600 - ) + create_stock_reconciliation(item_code=rm_item, warehouse="_Test Warehouse - _TC", qty=100, rate=600) bom.load_from_db() bom.update_cost() @@ -698,6 +689,33 @@ class TestBOM(FrappeTestCase): bom.update_cost() self.assertFalse(bom.flags.cost_updated) + def test_bom_with_service_item_cost(self): + rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 1000.0}).name + + service_item = make_item(properties={"is_stock_item": 0}).name + + fg_item = make_item(properties={"is_stock_item": 1}).name + + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + bom = make_bom(item=fg_item, raw_materials=[rm_item, service_item], do_not_save=True) + bom.rm_cost_as_per = "Valuation Rate" + + for row in bom.items: + if row.item_code == service_item: + row.rate = 566.00 + else: + row.rate = 800.00 + + bom.save() + + for row in bom.items: + if row.item_code == service_item: + self.assertEqual(row.is_stock_item, 0) + self.assertEqual(row.rate, 566.00) + else: + self.assertEqual(row.is_stock_item, 1) + def test_do_not_include_manufacturing_and_fixed_items(self): from erpnext.manufacturing.doctype.bom.bom import item_query @@ -764,7 +782,9 @@ def create_nested_bom(tree, prefix="_Test bom "): for item_code, subtree in bom_tree.items(): bom_item_code = prefix + item_code if not frappe.db.exists("Item", bom_item_code): - frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert() + frappe.get_doc( + doctype="Item", item_code=bom_item_code, item_group="_Test Item Group" + ).insert() create_items(subtree) create_items(tree) diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json index c75ac32cd12..27ecd57b873 100644 --- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json +++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json @@ -37,7 +37,8 @@ "oldfieldname": "item_code", "oldfieldtype": "Link", "options": "Item", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "item_name", @@ -170,7 +171,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:35:40.856895", + "modified": "2024-01-02 13:49:36.211586", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Explosion Item", diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index cb58af1f29a..dfd66120984 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -14,6 +14,7 @@ "bom_no", "source_warehouse", "allow_alternative_item", + "is_stock_item", "section_break_5", "description", "col_break1", @@ -185,7 +186,7 @@ "in_list_view": 1, "label": "Rate", "options": "currency", - "read_only": 1, + "read_only_depends_on": "eval:doc.is_stock_item == 1", "reqd": 1 }, { @@ -284,13 +285,21 @@ "fieldname": "do_not_explode", "fieldtype": "Check", "label": "Do Not Explode" + }, + { + "default": "0", + "fetch_from": "item_code.is_stock_item", + "fieldname": "is_stock_item", + "fieldtype": "Check", + "label": "Is Stock Item", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:35:51.378513", + "modified": "2023-12-20 16:21:55.477883", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js index 6da808e26d1..7664c299b1a 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js @@ -1,8 +1,7 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('BOM Update Log', { +frappe.ui.form.on("BOM Update Log", { // refresh: function(frm) { - // } }); diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index e9867468f98..02c43a8beff 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,7 +1,7 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt import json -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any import frappe from frappe import _ @@ -30,9 +30,7 @@ class BOMUpdateLog(Document): table = DocType("BOM Update Log") frappe.db.delete( table, - filters=( - (table.modified < (Now() - Interval(days=days))) & (table.update_type == "Update Cost") - ), + filters=((table.modified < (Now() - Interval(days=days))) & (table.update_type == "Update Cost")), ) def validate(self): @@ -102,7 +100,7 @@ class BOMUpdateLog(Document): def run_replace_bom_job( doc: "BOMUpdateLog", - boms: Optional[Dict[str, str]] = None, + boms: dict[str, str] | None = None, ) -> None: try: doc.db_set("status", "In Progress") @@ -125,8 +123,8 @@ def run_replace_bom_job( def process_boms_cost_level_wise( - update_doc: "BOMUpdateLog", parent_boms: List[str] = None -) -> Union[None, Tuple]: + update_doc: "BOMUpdateLog", parent_boms: list[str] | None = None +) -> None | tuple: "Queue jobs at the start of new BOM Level in 'Update Cost' Jobs." current_boms = {} @@ -159,9 +157,7 @@ def process_boms_cost_level_wise( handle_exception(update_doc) -def queue_bom_cost_jobs( - current_boms_list: List[str], update_doc: "BOMUpdateLog", current_level: int -) -> None: +def queue_bom_cost_jobs(current_boms_list: list[str], update_doc: "BOMUpdateLog", current_level: int) -> None: "Queue batches of 20k BOMs of the same level to process parallelly" batch_no = 0 @@ -245,8 +241,8 @@ def resume_bom_cost_update_jobs(): def get_processed_current_boms( - log: Dict[str, Any], bom_batches: Dict[str, Any] -) -> Tuple[List[str], Dict[str, Any]]: + log: dict[str, Any], bom_batches: dict[str, Any] +) -> tuple[list[str], dict[str, Any]]: """ Aggregate all BOMs from BOM Update Batch rows into 'processed_boms' field and into current boms list. diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js index bc709d8fc43..cdaee70860b 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js @@ -1,11 +1,11 @@ -frappe.listview_settings['BOM Update Log'] = { +frappe.listview_settings["BOM Update Log"] = { add_fields: ["status"], get_indicator: (doc) => { let status_map = { - "Queued": "orange", + Queued: "orange", "In Progress": "blue", - "Completed": "green", - "Failed": "red" + Completed: "green", + Failed: "red", }; return [__(doc.status), status_map[doc.status], "status,=," + doc.status]; @@ -15,16 +15,14 @@ frappe.listview_settings['BOM Update Log'] = { return; } - let sidebar_entry = $( - '' - ).appendTo(cur_list.page.sidebar); + let sidebar_entry = $('').appendTo( + cur_list.page.sidebar + ); let message = __("Note: Automatic log deletion only applies to logs of type Update Cost"); $(`
    ${message}
    `).appendTo(sidebar_entry); frappe.require("logtypes.bundle.js", () => { frappe.utils.logtypes.show_log_retention_message(cur_list.doctype); }); - - }, -}; \ No newline at end of file +}; diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index a2919b79b80..fed7a002e75 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -4,7 +4,7 @@ import copy import json from collections import defaultdict -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog @@ -13,7 +13,7 @@ import frappe from frappe import _ -def replace_bom(boms: Dict, log_name: str) -> None: +def replace_bom(boms: dict, log_name: str) -> None: "Replace current BOM with new BOM in parent BOMs." current_bom = boms.get("current_bom") @@ -43,9 +43,7 @@ def replace_bom(boms: Dict, log_name: str) -> None: bom_obj.save_version() -def update_cost_in_level( - doc: "BOMUpdateLog", bom_list: List[str], batch_name: Union[int, str] -) -> None: +def update_cost_in_level(doc: "BOMUpdateLog", bom_list: list[str], batch_name: int | str) -> None: "Updates Cost for BOMs within a given level. Runs via background jobs." try: @@ -69,7 +67,7 @@ def update_cost_in_level( frappe.db.commit() # nosemgrep -def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List: +def get_ancestor_boms(new_bom: str, bom_list: list | None = None) -> list: "Recursively get all ancestors of BOM." bom_list = bom_list or [] @@ -86,10 +84,12 @@ def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List: if new_bom == d.parent: frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) - bom_list.append(d.parent) + if d.parent not in tuple(bom_list): + bom_list.append(d.parent) + get_ancestor_boms(d.parent, bom_list) - return list(set(bom_list)) + return bom_list def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None: @@ -99,9 +99,7 @@ def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str .set(bom_item.bom_no, new_bom) .set(bom_item.rate, unit_cost) .set(bom_item.amount, (bom_item.stock_qty * unit_cost)) - .where( - (bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM") - ) + .where((bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")) ).run() @@ -114,7 +112,7 @@ def get_bom_unit_cost(bom_name: str) -> float: return frappe.utils.flt(new_bom_unitcost[0][0]) -def update_cost_in_boms(bom_list: List[str]) -> None: +def update_cost_in_boms(bom_list: list[str]) -> None: "Updates cost in given BOMs. Returns current and total updated BOMs." for index, bom in enumerate(bom_list): @@ -126,9 +124,7 @@ def update_cost_in_boms(bom_list: List[str]) -> None: frappe.db.commit() # nosemgrep -def get_next_higher_level_boms( - child_boms: List[str], processed_boms: Dict[str, bool] -) -> List[str]: +def get_next_higher_level_boms(child_boms: list[str], processed_boms: dict[str, bool]) -> list[str]: "Generate immediate higher level dependants with no unresolved dependencies (children)." def _all_children_are_processed(parent_bom): @@ -154,7 +150,7 @@ def get_next_higher_level_boms( return list(resolved_dependants) -def get_leaf_boms() -> List[str]: +def get_leaf_boms() -> list[str]: "Get BOMs that have no dependencies." bom = frappe.qb.DocType("BOM") @@ -207,7 +203,7 @@ def _generate_dependence_map() -> defaultdict: return child_parent_map, parent_child_map -def set_values_in_log(log_name: str, values: Dict[str, Any], commit: bool = False) -> None: +def set_values_in_log(log_name: str, values: dict[str, Any], commit: bool = False) -> None: "Update BOM Update Log record." if not values: diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py index b38fc8976b2..13bf69f672f 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -57,6 +57,66 @@ class TestBOMUpdateLog(FrappeTestCase): log.reload() self.assertEqual(log.status, "Completed") + def test_bom_replace_for_root_bom(self): + """ + - B-Item A (Root Item) + - B-Item B + - B-Item C + - B-Item D + - B-Item E + - B-Item F + + Create New BOM for B-Item E with B-Item G and replace it in the above BOM. + """ + + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.item.test_item import make_item + + items = ["B-Item A", "B-Item B", "B-Item C", "B-Item D", "B-Item E", "B-Item F", "B-Item G"] + + for item_code in items: + if not frappe.db.exists("Item", item_code): + make_item(item_code) + + for item_code in items: + remove_bom(item_code) + + bom_tree = {"B-Item A": {"B-Item B": {"B-Item C": {}}, "B-Item D": {"B-Item E": {"B-Item F": {}}}}} + + root_bom = create_nested_bom(bom_tree, prefix="") + + exploded_items = frappe.get_all( + "BOM Explosion Item", filters={"parent": root_bom.name}, fields=["item_code"] + ) + + exploded_items = [item.item_code for item in exploded_items] + expected_exploded_items = ["B-Item C", "B-Item F"] + self.assertEqual(sorted(exploded_items), sorted(expected_exploded_items)) + + old_bom = frappe.db.get_value("BOM", {"item": "B-Item E"}, "name") + bom_tree = {"B-Item E": {"B-Item G": {}}} + + new_bom = create_nested_bom(bom_tree, prefix="") + enqueue_replace_bom(boms=frappe._dict(current_bom=old_bom, new_bom=new_bom.name)) + + exploded_items = frappe.get_all( + "BOM Explosion Item", filters={"parent": root_bom.name}, fields=["item_code"] + ) + + exploded_items = [item.item_code for item in exploded_items] + expected_exploded_items = ["B-Item C", "B-Item G"] + self.assertEqual(sorted(exploded_items), sorted(expected_exploded_items)) + + +def remove_bom(item_code): + boms = frappe.get_all("BOM", fields=["docstatus", "name"], filters={"item": item_code}) + + for row in boms: + if row.docstatus == 1: + frappe.get_doc("BOM", row.name).cancel() + + frappe.delete_doc("BOM", row.name) + def update_cost_in_all_boms_in_test(): """ diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index 7ba6517a4fb..5cdb425bffc 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -1,24 +1,24 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('BOM Update Tool', { - setup: function(frm) { - frm.set_query("current_bom", function() { +frappe.ui.form.on("BOM Update Tool", { + setup: function (frm) { + frm.set_query("current_bom", function () { return { query: "erpnext.controllers.queries.bom", - filters: {name: "!" + frm.doc.new_bom} + filters: { name: "!" + frm.doc.new_bom }, }; }); - frm.set_query("new_bom", function() { + frm.set_query("new_bom", function () { return { query: "erpnext.controllers.queries.bom", - filters: {name: "!" + frm.doc.current_bom} + filters: { name: "!" + frm.doc.current_bom }, }; }); }, - refresh: function(frm) { + refresh: function (frm) { frm.disable_save(); frm.events.disable_button(frm, "replace"); @@ -27,7 +27,7 @@ frappe.ui.form.on('BOM Update Tool', { }); }, - disable_button: (frm, field, disable=true) => { + disable_button: (frm, field, disable = true) => { frm.get_field(field).input.disabled = disable; }, @@ -50,15 +50,15 @@ frappe.ui.form.on('BOM Update Tool', { freeze: true, args: { boms: { - "current_bom": frm.doc.current_bom, - "new_bom": frm.doc.new_bom - } + current_bom: frm.doc.current_bom, + new_bom: frm.doc.new_bom, + }, }, - callback: result => { + callback: (result) => { if (result && result.message && !result.exc) { frm.events.confirm_job_start(frm, result.message); } - } + }, }); } }, @@ -67,20 +67,22 @@ frappe.ui.form.on('BOM Update Tool', { frappe.call({ method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_update_cost", freeze: true, - callback: result => { + callback: (result) => { if (result && result.message && !result.exc) { frm.events.confirm_job_start(frm, result.message); } - } + }, }); }, confirm_job_start: (frm, log_data) => { let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true); frappe.msgprint({ - "message": __("BOM Updation is queued and may take a few minutes. Check {0} for progress.", [log_link]), - "title": __("BOM Update Initiated"), - "indicator": "blue" + message: __("BOM Updation is queued and may take a few minutes. Check {0} for progress.", [ + log_link, + ]), + title: __("BOM Update Initiated"), + indicator: "blue", }); - } + }, }); diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index d16fcd08326..288b7be35ee 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -2,7 +2,7 @@ # For license information, please see license.txt import json -from typing import TYPE_CHECKING, Dict, Literal, Optional, Union +from typing import TYPE_CHECKING, Literal if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog @@ -16,9 +16,7 @@ class BOMUpdateTool(Document): @frappe.whitelist() -def enqueue_replace_bom( - boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None -) -> "BOMUpdateLog": +def enqueue_replace_bom(boms: dict | str | None = None, args: dict | str | None = None) -> "BOMUpdateLog": """Returns a BOM Update Log (that queues a job) for BOM Replacement.""" boms = boms or args if isinstance(boms, str): @@ -48,7 +46,7 @@ def auto_update_latest_price_in_all_boms() -> None: def create_bom_update_log( - boms: Optional[Dict[str, str]] = None, + boms: dict[str, str] | None = None, update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM", ) -> "BOMUpdateLog": """Creates a BOM Update Log that handles the background job.""" diff --git a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js index 3b7f5ba8d7f..516e33c7a3b 100644 --- a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js +++ b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js @@ -1,8 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Downtime Entry', { +frappe.ui.form.on("Downtime Entry", { // refresh: function(frm) { - // } }); diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 4a46d577445..db9c8849f45 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -1,29 +1,27 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Job Card', { - setup: function(frm) { - frm.set_query('operation', function() { +frappe.ui.form.on("Job Card", { + setup: function (frm) { + frm.set_query("operation", function () { return { - query: 'erpnext.manufacturing.doctype.job_card.job_card.get_operations', + query: "erpnext.manufacturing.doctype.job_card.job_card.get_operations", filters: { - 'work_order': frm.doc.work_order - } + work_order: frm.doc.work_order, + }, }; }); - frm.set_indicator_formatter('sub_operation', - function(doc) { - if (doc.status == "Pending") { - return "red"; - } else { - return doc.status === "Complete" ? "green" : "orange"; - } + frm.set_indicator_formatter("sub_operation", function (doc) { + if (doc.status == "Pending") { + return "red"; + } else { + return doc.status === "Complete" ? "green" : "orange"; } - ); + }); }, - refresh: function(frm) { + refresh: function (frm) { frappe.flags.pause_job = 0; frappe.flags.resume_job = 0; let has_items = frm.doc.items && frm.doc.items.length; @@ -33,8 +31,7 @@ frappe.ui.form.on('Job Card', { return; } - let has_stock_entry = frm.doc.__onload && - frm.doc.__onload.has_stock_entry ? true : false; + let has_stock_entry = frm.doc.__onload && frm.doc.__onload.has_stock_entry ? true : false; frm.toggle_enable("for_quantity", !has_stock_entry); @@ -60,33 +57,45 @@ frappe.ui.form.on('Job Card', { } if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) { - frm.trigger('setup_corrective_job_card'); + frm.trigger("setup_corrective_job_card"); } - frm.set_query("quality_inspection", function() { + frm.set_query("quality_inspection", function () { return { query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", filters: { - "item_code": frm.doc.production_item, - "reference_name": frm.doc.name - } + item_code: frm.doc.production_item, + reference_name: frm.doc.name, + }, }; }); frm.trigger("toggle_operation_number"); - if (frm.doc.docstatus == 0 && !frm.is_new() && - (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) - && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { - + if ( + frm.doc.docstatus == 0 && + !frm.is_new() && + (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) && + (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty) + ) { // if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started" // and if stock mvt for WIP is required if (frm.doc.work_order) { - frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => { - if (result.skip_transfer === 1 || result.status == 'In Process' || frm.doc.transferred_qty > 0 || !frm.doc.items.length) { - frm.trigger("prepare_timer_buttons"); + frappe.db.get_value( + "Work Order", + frm.doc.work_order, + ["skip_transfer", "status"], + (result) => { + if ( + result.skip_transfer === 1 || + result.status == "In Process" || + frm.doc.transferred_qty > 0 || + !frm.doc.items.length + ) { + frm.trigger("prepare_timer_buttons"); + } } - }); + ); } else { frm.trigger("prepare_timer_buttons"); } @@ -95,89 +104,103 @@ frappe.ui.form.on('Job Card', { frm.trigger("setup_quality_inspection"); if (frm.doc.work_order) { - frappe.db.get_value('Work Order', frm.doc.work_order, - 'transfer_material_against').then((r) => { - if (r.message.transfer_material_against == 'Work Order') { - frm.set_df_property('items', 'hidden', 1); + frappe.db.get_value("Work Order", frm.doc.work_order, "transfer_material_against").then((r) => { + if (r.message.transfer_material_against == "Work Order") { + frm.set_df_property("items", "hidden", 1); } }); } }, - setup_quality_inspection: function(frm) { + setup_quality_inspection: function (frm) { let quality_inspection_field = frm.get_docfield("quality_inspection"); - quality_inspection_field.get_route_options_for_new_doc = function(frm) { - return { - "inspection_type": "In Process", - "reference_type": "Job Card", - "reference_name": frm.doc.name, - "item_code": frm.doc.production_item, - "item_name": frm.doc.item_name, - "item_serial_no": frm.doc.serial_no, - "batch_no": frm.doc.batch_no, - "quality_inspection_template": frm.doc.quality_inspection_template, + quality_inspection_field.get_route_options_for_new_doc = function (frm) { + return { + inspection_type: "In Process", + reference_type: "Job Card", + reference_name: frm.doc.name, + item_code: frm.doc.production_item, + item_name: frm.doc.item_name, + item_serial_no: frm.doc.serial_no, + batch_no: frm.doc.batch_no, + quality_inspection_template: frm.doc.quality_inspection_template, }; }; }, - setup_corrective_job_card: function(frm) { - frm.add_custom_button(__('Corrective Job Card'), () => { - let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation); + setup_corrective_job_card: function (frm) { + frm.add_custom_button( + __("Corrective Job Card"), + () => { + let operations = frm.doc.sub_operations.map((d) => d.sub_operation).concat(frm.doc.operation); - let fields = [ - { - fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation', - fieldname: 'operation', get_query() { - return { - filters: { - "is_corrective_operation": 1 - } - }; - } - }, { - fieldtype: 'Link', label: __('For Operation'), options: 'Operation', - fieldname: 'for_operation', get_query() { - return { - filters: { - "name": ["in", operations] - } - }; - } - } - ]; + let fields = [ + { + fieldtype: "Link", + label: __("Corrective Operation"), + options: "Operation", + fieldname: "operation", + get_query() { + return { + filters: { + is_corrective_operation: 1, + }, + }; + }, + }, + { + fieldtype: "Link", + label: __("For Operation"), + options: "Operation", + fieldname: "for_operation", + get_query() { + return { + filters: { + name: ["in", operations], + }, + }; + }, + }, + ]; - frappe.prompt(fields, d => { - frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); - }, __("Select Corrective Operation")); - }, __('Make')); + frappe.prompt( + fields, + (d) => { + frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); + }, + __("Select Corrective Operation") + ); + }, + __("Make") + ); }, - make_corrective_job_card: function(frm, operation, for_operation) { + make_corrective_job_card: function (frm, operation, for_operation) { frappe.call({ - method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card', + method: "erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card", args: { source_name: frm.doc.name, operation: operation, - for_operation: for_operation + for_operation: for_operation, }, - callback: function(r) { + callback: function (r) { if (r.message) { frappe.model.sync(r.message); frappe.set_route("Form", r.message.doctype, r.message.name); } - } + }, }); }, - operation: function(frm) { + operation: function (frm) { frm.trigger("toggle_operation_number"); if (frm.doc.operation && frm.doc.work_order) { frappe.call({ method: "erpnext.manufacturing.doctype.job_card.job_card.get_operation_details", args: { - "work_order":frm.doc.work_order, - "operation":frm.doc.operation + work_order: frm.doc.work_order, + operation: frm.doc.operation, }, callback: function (r) { if (r.message) { @@ -187,11 +210,13 @@ frappe.ui.form.on('Job Card', { let args = []; r.message.forEach((row) => { - args.push({ "label": row.idx, "value": row.name }); + args.push({ label: row.idx, value: row.name }); }); - let description = __("Operation {0} added multiple times in the work order {1}", - [frm.doc.operation, frm.doc.work_order]); + let description = __("Operation {0} added multiple times in the work order {1}", [ + frm.doc.operation, + frm.doc.work_order, + ]); frm.set_df_property("operation_row_number", "options", args); frm.set_df_property("operation_row_number", "description", description); @@ -199,8 +224,8 @@ frappe.ui.form.on('Job Card', { frm.trigger("toggle_operation_number"); } - } - }) + }, + }); } }, @@ -215,16 +240,24 @@ frappe.ui.form.on('Job Card', { frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation); }, - prepare_timer_buttons: function(frm) { + prepare_timer_buttons: function (frm) { frm.trigger("make_dashboard"); if (!frm.doc.started_time && !frm.doc.current_time) { frm.add_custom_button(__("Start Job"), () => { if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) { - frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'), - options: "Job Card Time Log", fieldname: 'employees'}, d => { - frm.events.start_job(frm, "Work In Progress", d.employees); - }, __("Assign Job to Employee")); + frappe.prompt( + { + fieldtype: "Table MultiSelect", + label: __("Select Employees"), + options: "Job Card Time Log", + fieldname: "employees", + }, + (d) => { + frm.events.start_job(frm, "Work In Progress", d.employees); + }, + __("Assign Job to Employee") + ); } else { frm.events.start_job(frm, "Work In Progress", frm.doc.employee); } @@ -246,16 +279,24 @@ frappe.ui.form.on('Job Card', { set_qty = false; let last_op_row = sub_operations[sub_operations.length - 2]; - if (last_op_row.status == 'Complete') { + if (last_op_row.status == "Complete") { set_qty = true; } } if (set_qty) { - frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), - fieldname: 'qty', default: frm.doc.for_quantity}, data => { - frm.events.complete_job(frm, "Complete", data.qty); - }, __("Enter Value")); + frappe.prompt( + { + fieldtype: "Float", + label: __("Completed Quantity"), + fieldname: "qty", + default: frm.doc.for_quantity, + }, + (data) => { + frm.events.complete_job(frm, "Complete", data.qty); + }, + __("Enter Value") + ); } else { frm.events.complete_job(frm, "Complete", 0.0); } @@ -263,64 +304,63 @@ frappe.ui.form.on('Job Card', { } }, - start_job: function(frm, status, employee) { + start_job: function (frm, status, employee) { const args = { job_card_id: frm.doc.name, start_time: frappe.datetime.now_datetime(), employees: employee, - status: status + status: status, }; frm.events.make_time_log(frm, args); }, - complete_job: function(frm, status, completed_qty) { + complete_job: function (frm, status, completed_qty) { const args = { job_card_id: frm.doc.name, complete_time: frappe.datetime.now_datetime(), status: status, - completed_qty: completed_qty + completed_qty: completed_qty, }; frm.events.make_time_log(frm, args); }, - make_time_log: function(frm, args) { + make_time_log: function (frm, args) { frm.events.update_sub_operation(frm, args); frappe.call({ method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log", args: { - args: args + args: args, }, freeze: true, callback: function () { frm.reload_doc(); frm.trigger("make_dashboard"); - } + }, }); }, - update_sub_operation: function(frm, args) { + update_sub_operation: function (frm, args) { if (frm.doc.sub_operations && frm.doc.sub_operations.length) { - let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete'); + let sub_operations = frm.doc.sub_operations.filter((d) => d.status != "Complete"); if (sub_operations && sub_operations.length) { args["sub_operation"] = sub_operations[0].sub_operation; } } }, - validate: function(frm) { + validate: function (frm) { if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) { frm.trigger("reset_timer"); } }, - reset_timer: function(frm) { - frm.set_value('started_time' , ''); + reset_timer: function (frm) { + frm.set_value("started_time", ""); }, - make_dashboard: function(frm) { - if(frm.doc.__islocal) - return; + make_dashboard: function (frm) { + if (frm.doc.__islocal) return; frm.dashboard.refresh(); const timer = ` @@ -340,12 +380,15 @@ frappe.ui.form.on('Job Card', { if (frm.doc.status == "On Hold") { updateStopwatch(currentIncrement); } else { - currentIncrement += moment(frappe.datetime.now_datetime()).diff(moment(frm.doc.started_time),"seconds"); + currentIncrement += moment(frappe.datetime.now_datetime()).diff( + moment(frm.doc.started_time), + "seconds" + ); initialiseTimer(); } function initialiseTimer() { - const interval = setInterval(function() { + const interval = setInterval(function () { var current = setCurrentIncrement(); updateStopwatch(current); }, 1000); @@ -353,12 +396,18 @@ frappe.ui.form.on('Job Card', { function updateStopwatch(increment) { var hours = Math.floor(increment / 3600); - var minutes = Math.floor((increment - (hours * 3600)) / 60); - var seconds = increment - (hours * 3600) - (minutes * 60); + var minutes = Math.floor((increment - hours * 3600) / 60); + var seconds = increment - hours * 3600 - minutes * 60; - $(section).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString()); - $(section).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString()); - $(section).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString()); + $(section) + .find(".hours") + .text(hours < 10 ? "0" + hours.toString() : hours.toString()); + $(section) + .find(".minutes") + .text(minutes < 10 ? "0" + minutes.toString() : minutes.toString()); + $(section) + .find(".seconds") + .text(seconds < 10 ? "0" + seconds.toString() : seconds.toString()); } function setCurrentIncrement() { @@ -368,69 +417,67 @@ frappe.ui.form.on('Job Card', { } }, - hide_timer: function(frm) { + hide_timer: function (frm) { frm.toolbar.page.inner_toolbar.find(".stopwatch").remove(); }, - for_quantity: function(frm) { + for_quantity: function (frm) { frm.doc.items = []; frm.call({ method: "get_required_items", doc: frm.doc, - callback: function() { + callback: function () { refresh_field("items"); - } - }) + }, + }); }, - make_material_request: function(frm) { + make_material_request: function (frm) { frappe.model.open_mapped_doc({ method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request", frm: frm, - run_link_triggers: true + run_link_triggers: true, }); }, - make_stock_entry: function(frm) { + make_stock_entry: function (frm) { frappe.model.open_mapped_doc({ method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry", frm: frm, - run_link_triggers: true + run_link_triggers: true, }); }, - timer: function(frm) { - return `` + timer: function (frm) { + return ``; }, - set_total_completed_qty: function(frm) { + set_total_completed_qty: function (frm) { frm.doc.total_completed_qty = 0; - frm.doc.time_logs.forEach(d => { + frm.doc.time_logs.forEach((d) => { if (d.completed_qty) { frm.doc.total_completed_qty += d.completed_qty; } }); if (frm.doc.total_completed_qty && frm.doc.for_quantity > frm.doc.total_completed_qty) { - let flt_precision = precision('for_quantity', frm.doc); - let process_loss_qty = ( - flt(frm.doc.for_quantity, flt_precision) - - flt(frm.doc.total_completed_qty, flt_precision) - ); + let flt_precision = precision("for_quantity", frm.doc); + let process_loss_qty = + flt(frm.doc.for_quantity, flt_precision) - flt(frm.doc.total_completed_qty, flt_precision); - frm.set_value('process_loss_qty', process_loss_qty); + frm.set_value("process_loss_qty", process_loss_qty); } refresh_field("total_completed_qty"); - } + }, }); -frappe.ui.form.on('Job Card Time Log', { - completed_qty: function(frm) { +frappe.ui.form.on("Job Card Time Log", { + completed_qty: function (frm) { frm.events.set_total_completed_qty(frm); }, - to_time: function(frm) { - frm.set_value('started_time', ''); - } -}) + to_time: function (frm) { + frm.set_value("started_time", ""); + }, +}); diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 455aa7e5766..8c562eff83f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -2,7 +2,6 @@ # For license information, please see license.txt import datetime import json -from typing import Optional import frappe from frappe import _, bold @@ -52,9 +51,7 @@ class JobCardOverTransferError(frappe.ValidationError): class JobCard(Document): def onload(self): - excess_transfer = frappe.db.get_single_value( - "Manufacturing Settings", "job_card_excess_transfer" - ) + excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") self.set_onload("job_card_excess_transfer", excess_transfer) self.set_onload("work_order_closed", self.is_work_order_closed()) self.set_onload("has_stock_entry", self.has_stock_entry()) @@ -83,9 +80,7 @@ class JobCard(Document): wo_qty = flt(frappe.get_cached_value("Work Order", self.work_order, "qty")) - completed_qty = flt( - frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty") - ) + completed_qty = flt(frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty")) over_production_percentage = flt( frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") @@ -163,7 +158,7 @@ class JobCard(Document): for row in self.sub_operations: self.total_completed_qty += row.completed_qty - def get_overlap_for(self, args, check_next_available_slot=False): + def get_overlap_for(self, args): production_capacity = 1 jc = frappe.qb.DocType("Job Card") @@ -175,9 +170,6 @@ class JobCard(Document): ((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)), ] - if check_next_available_slot: - time_conditions.append(((jctl.from_time >= args.from_time) & (jctl.to_time >= args.to_time))) - query = ( frappe.qb.from_(jctl) .from_(jc) @@ -218,7 +210,7 @@ class JobCard(Document): if not self.has_overlap(production_capacity, existing_time_logs): return {} - if self.workstation_type: + if not self.workstation and self.workstation_type: if workstation := self.get_workstation_based_on_available_slot(existing_time_logs): self.workstation = workstation return None @@ -227,38 +219,42 @@ class JobCard(Document): def has_overlap(self, production_capacity, time_logs): overlap = False - if production_capacity == 1 and len(time_logs) > 0: + if production_capacity == 1 and len(time_logs) >= 1: return True + if not len(time_logs): + return False - # Check overlap exists or not between the overlapping time logs with the current Job Card - for row in time_logs: - count = 1 - for next_row in time_logs: - if row.name == next_row.name: - continue - - if ( - ( - get_datetime(next_row.from_time) >= get_datetime(row.from_time) - and get_datetime(next_row.from_time) <= get_datetime(row.to_time) - ) - or ( - get_datetime(next_row.to_time) >= get_datetime(row.from_time) - and get_datetime(next_row.to_time) <= get_datetime(row.to_time) - ) - or ( - get_datetime(next_row.from_time) <= get_datetime(row.from_time) - and get_datetime(next_row.to_time) >= get_datetime(row.to_time) - ) - ): - count += 1 - - if count > production_capacity: - return True - + # sorting overlapping job cards as per from_time + time_logs = sorted(time_logs, key=lambda x: x.get("from_time")) + # alloted_capacity has key number starting from 1. Key number will increment by 1 if non sequential job card found + # if key number reaches/crosses to production_capacity means capacity is full and overlap error generated + # this will store last to_time of sequential job cards + alloted_capacity = {1: time_logs[0]["to_time"]} + # flag for sequential Job card found + sequential_job_card_found = False + for i in range(1, len(time_logs)): + # scanning for all Existing keys + for key in alloted_capacity.keys(): + # if current Job Card from time is greater than last to_time in that key means these job card are sequential + if alloted_capacity[key] <= time_logs[i]["from_time"]: + # So update key's value with last to_time + alloted_capacity[key] = time_logs[i]["to_time"] + # flag is true as we get sequential Job Card for that key + sequential_job_card_found = True + # Immediately break so that job card to time is not added with any other key except this + break + # if sequential job card not found above means it is overlapping so increment key number to alloted_capacity + if not sequential_job_card_found: + # increment key number + key = key + 1 + # for that key last to time is assigned. + alloted_capacity[key] = time_logs[i]["to_time"] + if len(alloted_capacity) >= production_capacity: + # if number of keys greater or equal to production caoacity means full capacity is utilized and we should throw overlap error + return True return overlap - def get_workstation_based_on_available_slot(self, existing) -> Optional[str]: + def get_workstation_based_on_available_slot(self, existing) -> str | None: workstations = get_workstations(self.workstation_type) if workstations: busy_workstations = [row.workstation for row in existing] @@ -275,20 +271,36 @@ class JobCard(Document): self.check_workstation_time(row) def validate_overlap_for_workstation(self, args, row): + if args.get("to_time") and get_datetime(args.to_time) < get_datetime(args.from_time): + args.to_time = add_to_date(row.planned_start_time, minutes=row.remaining_time_in_mins) + # get the last record based on the to time from the job card - data = self.get_overlap_for(args, check_next_available_slot=True) + data = self.get_overlap_for(args) + + if not data: + row.planned_start_time = args.from_time + return + if data: if not self.workstation: self.workstation = data.workstation - row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations()) + if data.get("planned_start_time"): + args.planned_start_time = get_datetime(data.planned_start_time) + else: + args.planned_start_time = get_datetime(data.to_time + get_mins_between_operations()) + + args.from_time = args.planned_start_time + args.to_time = add_to_date(args.planned_start_time, minutes=row.remaining_time_in_mins) + + self.validate_overlap_for_workstation(args, row) def check_workstation_time(self, row): workstation_doc = frappe.get_cached_doc("Workstation", self.workstation) if not workstation_doc.working_hours or cint( frappe.db.get_single_value("Manufacturing Settings", "allow_overtime") ): - if get_datetime(row.planned_end_time) < get_datetime(row.planned_start_time): + if get_datetime(row.planned_end_time) <= get_datetime(row.planned_start_time): row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.time_in_mins) row.remaining_time_in_mins = 0.0 else: @@ -322,7 +334,9 @@ class JobCard(Document): # If remaining time fit in workstation time logs else split hours as per workstation time if time_in_mins > row.remaining_time_in_mins: - row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.remaining_time_in_mins) + row.planned_end_time = add_to_date( + row.planned_start_time, minutes=row.remaining_time_in_mins + ) row.remaining_time_in_mins = 0 else: row.planned_end_time = add_to_date(row.planned_start_time, minutes=time_in_mins) @@ -358,7 +372,7 @@ class JobCard(Document): { "to_time": get_datetime(args.get("complete_time")), "operation": args.get("sub_operation"), - "completed_qty": args.get("completed_qty") or 0.0, + "completed_qty": (args.get("completed_qty") if last_row.idx == row.idx else 0.0), } ) elif args.get("start_time"): @@ -442,7 +456,9 @@ class JobCard(Document): row.completed_time = row.completed_time / len(set(operation_deatils.employee)) if operation_deatils.completed_qty: - row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee)) + row.completed_qty = operation_deatils.completed_qty / len( + set(operation_deatils.employee) + ) else: row.status = "Pending" row.completed_time = 0.0 @@ -513,10 +529,7 @@ class JobCard(Document): ) def validate_job_card(self): - if ( - self.work_order - and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped" - ): + if self.work_order and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped": frappe.throw( _("Transaction not allowed against stopped Work Order {0}").format( get_link_to_form("Work Order", self.work_order) @@ -535,9 +548,7 @@ class JobCard(Document): flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision) ) - if self.for_quantity and flt(total_completed_qty, precision) != flt( - self.for_quantity, precision - ): + if self.for_quantity and flt(total_completed_qty, precision) != flt(self.for_quantity, precision): total_completed_qty_label = bold(_("Total Completed Qty")) qty_to_manufacture = bold(_("Qty to Manufacture")) @@ -571,9 +582,8 @@ class JobCard(Document): return for_quantity, time_in_mins, process_loss_qty = 0, 0, 0 - from_time_list, to_time_list = [], [] + _from_time_list, _to_time_list = [], [] - field = "operation_id" data = self.get_current_operation_data() if data and len(data) > 0: for_quantity = flt(data[0].completed_qty) @@ -609,9 +619,7 @@ class JobCard(Document): if wo.produced_qty > for_quantity + process_loss_qty: first_part_msg = _( "The {0} {1} is used to calculate the valuation cost for the finished good {2}." - ).format( - frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item) - ) + ).format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)) second_part_msg = _( "Kindly cancel the Manufacturing Entries first against the work order {0}." @@ -678,9 +686,7 @@ class JobCard(Document): from frappe.query_builder.functions import Sum job_card_items_transferred_qty = {} - job_card_items = [ - x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item") - ] + job_card_items = [x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item")] if job_card_items: se = frappe.qb.DocType("Stock Entry") @@ -805,9 +811,7 @@ class JobCard(Document): def set_wip_warehouse(self): if not self.wip_warehouse: - self.wip_warehouse = frappe.db.get_single_value( - "Manufacturing Settings", "default_wip_warehouse" - ) + self.wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") def validate_operation_id(self): if ( @@ -847,7 +851,7 @@ class JobCard(Document): order_by="sequence_id, idx", ) - message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format( + message = "Job Card {}: As per the sequence of the operations in the work order {}".format( bold(self.name), bold(get_link_to_form("Work Order", self.work_order)) ) @@ -912,7 +916,7 @@ def get_operations(doctype, txt, searchfield, start, page_len, filters): return [] args = {"parent": filters.get("work_order")} if txt: - args["operation"] = ("like", "%{0}%".format(txt)) + args["operation"] = ("like", f"%{txt}%") return frappe.get_all( "Work Order Operation", @@ -1033,16 +1037,14 @@ def get_job_details(start, end, filters=None): conditions = get_filters_cond("Job Card", filters, []) job_cards = frappe.db.sql( - """ SELECT `tabJob Card`.name, `tabJob Card`.work_order, + f""" SELECT `tabJob Card`.name, `tabJob Card`.work_order, `tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''), min(`tabJob Card Time Log`.from_time) as from_time, max(`tabJob Card Time Log`.to_time) as to_time FROM `tabJob Card` , `tabJob Card Time Log` WHERE - `tabJob Card`.name = `tabJob Card Time Log`.parent {0} - group by `tabJob Card`.name""".format( - conditions - ), + `tabJob Card`.name = `tabJob Card Time Log`.parent {conditions} + group by `tabJob Card`.name""", as_dict=1, ) diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js index 9e320853514..7b32a6ee289 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js @@ -1,31 +1,31 @@ frappe.views.calendar["Job Card"] = { field_map: { - "start": "from_time", - "end": "to_time", - "id": "name", - "title": "subject", - "color": "color", - "allDay": "allDay", - "progress": "progress" + start: "from_time", + end: "to_time", + id: "name", + title: "subject", + color: "color", + allDay: "allDay", + progress: "progress", }, gantt: { field_map: { - "start": "expected_start_date", - "end": "expected_end_date", - "id": "name", - "title": "subject", - "color": "color", - "allDay": "allDay", - "progress": "progress" - } + start: "expected_start_date", + end: "expected_end_date", + id: "name", + title: "subject", + color: "color", + allDay: "allDay", + progress: "progress", + }, }, filters: [ { - "fieldtype": "Link", - "fieldname": "employee", - "options": "Employee", - "label": __("Employee") - } + fieldtype: "Link", + fieldname: "employee", + options: "Employee", + label: __("Employee"), + }, ], - get_events_method: "erpnext.manufacturing.doctype.job_card.job_card.get_job_details" + get_events_method: "erpnext.manufacturing.doctype.job_card.job_card.get_job_details", }; diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js index 99fca9570f7..e417b7f576d 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_list.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js @@ -1,17 +1,17 @@ -frappe.listview_settings['Job Card'] = { +frappe.listview_settings["Job Card"] = { has_indicator_for_draft: true, add_fields: ["expected_start_date", "expected_end_date"], - get_indicator: function(doc) { + get_indicator: function (doc) { const status_colors = { "Work In Progress": "orange", - "Completed": "green", - "Cancelled": "red", + Completed: "green", + Cancelled: "red", "Material Transferred": "blue", - "Open": "red", + Open: "red", }; const status = doc.status || "Open"; const color = status_colors[status] || "blue"; return [__(status), color, `status,=,${status}`]; - } + }, }; diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index e7fbcda7ab0..d9a2046ea59 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -59,7 +59,6 @@ class TestJobCard(FrappeTestCase): frappe.db.rollback() def test_job_card_operations(self): - job_cards = frappe.get_all( "Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"] ) @@ -252,7 +251,6 @@ class TestJobCard(FrappeTestCase): @change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0}) def test_job_card_excess_material_transfer_block(self): - self.transfer_material_against = "Job Card" self.source_warehouse = "Stores - _TC" @@ -276,7 +274,6 @@ class TestJobCard(FrappeTestCase): @change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0}) def test_job_card_excess_material_transfer_with_no_reference(self): - self.transfer_material_against = "Job Card" self.source_warehouse = "Stores - _TC" diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js index a0122a47385..f54478a1c10 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js @@ -1,33 +1,40 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Manufacturing Settings', { -}); +frappe.ui.form.on("Manufacturing Settings", {}); frappe.tour["Manufacturing Settings"] = [ { fieldname: "material_consumption", title: __("Allow Multiple Material Consumption"), - description: __("If ticked, multiple materials can be used for a single Work Order. This is useful if one or more time consuming products are being manufactured.") + description: __( + "If ticked, multiple materials can be used for a single Work Order. This is useful if one or more time consuming products are being manufactured." + ), }, { fieldname: "backflush_raw_materials_based_on", title: __("Backflush Raw Materials"), - description: __("The Stock Entry of type 'Manufacture' is known as backflush. Raw materials being consumed to manufacture finished goods is known as backflushing.

    When creating Manufacture Entry, raw-material items are backflushed based on BOM of production item. If you want raw-material items to be backflushed based on Material Transfer entry made against that Work Order instead, then you can set it under this field.") + description: __( + "The Stock Entry of type 'Manufacture' is known as backflush. Raw materials being consumed to manufacture finished goods is known as backflushing.

    When creating Manufacture Entry, raw-material items are backflushed based on BOM of production item. If you want raw-material items to be backflushed based on Material Transfer entry made against that Work Order instead, then you can set it under this field." + ), }, { fieldname: "default_wip_warehouse", title: __("Work In Progress Warehouse"), - description: __("This Warehouse will be auto-updated in the Work In Progress Warehouse field of Work Orders.") + description: __( + "This Warehouse will be auto-updated in the Work In Progress Warehouse field of Work Orders." + ), }, { fieldname: "default_fg_warehouse", title: __("Finished Goods Warehouse"), - description: __("This Warehouse will be auto-updated in the Target Warehouse field of Work Order.") + description: __("This Warehouse will be auto-updated in the Target Warehouse field of Work Order."), }, { fieldname: "update_bom_costs_automatically", title: __("Update BOM Cost Automatically"), - description: __("If ticked, the BOM cost will be automatically updated based on Valuation Rate / Price List Rate / last purchase rate of raw materials.") - } + description: __( + "If ticked, the BOM cost will be automatically updated based on Valuation Rate / Price List Rate / last purchase rate of raw materials." + ), + }, ]; diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 01647d56c91..d3ad51f7236 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -31,6 +31,7 @@ "job_card_excess_transfer", "other_settings_section", "update_bom_costs_automatically", + "set_op_cost_and_scrape_from_sub_assemblies", "column_break_23", "make_serial_no_batch_from_work_order" ], @@ -194,13 +195,20 @@ "fieldname": "job_card_excess_transfer", "fieldtype": "Check", "label": "Allow Excess Material Transfer" + }, + { + "default": "0", + "description": "In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.", + "fieldname": "set_op_cost_and_scrape_from_sub_assemblies", + "fieldtype": "Check", + "label": "Set Operating Cost / Scrape Items From Sub-assemblies" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-13 22:09:09.401559", + "modified": "2023-12-28 16:37:44.874096", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", @@ -216,5 +224,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py index 730a8575247..fc885760ef5 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -14,8 +14,7 @@ class ManufacturingSettings(Document): def get_mins_between_operations(): return relativedelta( - minutes=cint(frappe.db.get_single_value("Manufacturing Settings", "mins_between_operations")) - or 10 + minutes=cint(frappe.db.get_single_value("Manufacturing Settings", "mins_between_operations")) or 10 ) diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.js b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.js index 61c0a997a42..7a9e2b90120 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.js +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.js @@ -1,8 +1,6 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Material Request Plan Item', { - refresh: function() { - - } +frappe.ui.form.on("Material Request Plan Item", { + refresh: function () {}, }); diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index d07bf0fa66b..06c1b497551 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -38,7 +38,8 @@ "in_list_view": 1, "label": "Item Code", "options": "Item", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "fieldname": "item_name", @@ -53,7 +54,8 @@ "in_standard_filter": 1, "label": "For Warehouse", "options": "Warehouse", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "columns": 1, @@ -141,7 +143,8 @@ "fieldname": "from_warehouse", "fieldtype": "Link", "label": "From Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "search_index": 1 }, { "fetch_from": "item_code.safety_stock", @@ -199,7 +202,7 @@ ], "istable": 1, "links": [], - "modified": "2023-09-12 12:09:08.358326", + "modified": "2024-02-11 16:21:11.977018", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js index ea73fd6e273..c51ea72c8a4 100644 --- a/erpnext/manufacturing/doctype/operation/operation.js +++ b/erpnext/manufacturing/doctype/operation/operation.js @@ -1,32 +1,34 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Operation', { - setup: function(frm) { - frm.set_query('operation', 'sub_operations', function() { +frappe.ui.form.on("Operation", { + setup: function (frm) { + frm.set_query("operation", "sub_operations", function () { return { filters: { - 'name': ['not in', [frm.doc.name]] - } + name: ["not in", [frm.doc.name]], + }, }; }); - } + }, }); -frappe.tour['Operation'] = [ +frappe.tour["Operation"] = [ { fieldname: "__newname", title: "Operation Name", - description: __("Enter a name for the Operation, for example, Cutting.") + description: __("Enter a name for the Operation, for example, Cutting."), }, { fieldname: "workstation", title: "Default Workstation", - description: __("Select the Default Workstation where the Operation will be performed. This will be fetched in BOMs and Work Orders.") + description: __( + "Select the Default Workstation where the Operation will be performed. This will be fetched in BOMs and Work Orders." + ), }, { fieldname: "sub_operations", title: "Sub Operations", - description: __("If an operation is divided into sub operations, they can be added here.") - } + description: __("If an operation is divided into sub operations, they can be added here."), + }, ]; diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index dd102b0fae0..54d1414c814 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -1,11 +1,10 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Production Plan', { - +frappe.ui.form.on("Production Plan", { before_save(frm) { // preserve temporary names on production plan item to re-link sub-assembly items - frm.doc.po_items.forEach(item => { + frm.doc.po_items.forEach((item) => { item.temporary_name = item.name; }); }, @@ -14,8 +13,8 @@ frappe.ui.form.on('Production Plan', { frm.trigger("setup_queries"); frm.custom_make_buttons = { - 'Work Order': 'Work Order / Subcontract PO', - 'Material Request': 'Material Request', + "Work Order": "Work Order / Subcontract PO", + "Material Request": "Material Request", }; }, @@ -25,36 +24,36 @@ frappe.ui.form.on('Production Plan', { query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query", filters: { company: frm.doc.company, - } - } + }, + }; }); - frm.set_query('for_warehouse', function(doc) { + frm.set_query("for_warehouse", function (doc) { return { filters: { company: doc.company, - is_group: 0 - } - } + is_group: 0, + }, + }; }); - frm.set_query('material_request', 'material_requests', function() { + frm.set_query("material_request", "material_requests", function () { return { filters: { material_request_type: "Manufacture", docstatus: 1, status: ["!=", "Stopped"], - } + }, }; }); frm.set_query("item_code", "po_items", (doc, cdt, cdn) => { return { query: "erpnext.controllers.queries.item_query", - filters:{ - 'is_stock_item': 1, - } - } + filters: { + is_stock_item: 1, + }, + }; }); frm.set_query("bom_no", "po_items", (doc, cdt, cdn) => { @@ -62,25 +61,25 @@ frappe.ui.form.on('Production Plan', { if (d.item_code) { return { query: "erpnext.controllers.queries.bom", - filters:{'item': d.item_code, 'docstatus': 1} - } + filters: { item: d.item_code, docstatus: 1 }, + }; } else frappe.msgprint(__("Please enter Item first")); }); frm.set_query("warehouse", "mr_items", (doc) => { return { filters: { - company: doc.company - } - } + company: doc.company, + }, + }; }); frm.set_query("warehouse", "po_items", (doc) => { return { filters: { - company: doc.company - } - } + company: doc.company, + }, + }; }); }, @@ -89,36 +88,62 @@ frappe.ui.form.on('Production Plan', { frm.trigger("show_progress"); if (frm.doc.status !== "Completed") { - frm.add_custom_button(__("Production Plan Summary"), ()=> { - frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name}); - }, __('View')); + frm.add_custom_button( + __("Production Plan Summary"), + () => { + frappe.set_route("query-report", "Production Plan Summary", { + production_plan: frm.doc.name, + }); + }, + __("View") + ); - if (frm.doc.status === "Closed") { - frm.add_custom_button(__("Re-open"), function() { - frm.events.close_open_production_plan(frm, false); - }, __("Status")); + if (frm.doc.status === "Closed") { + frm.add_custom_button( + __("Re-open"), + function () { + frm.events.close_open_production_plan(frm, false); + }, + __("Status") + ); } else { - frm.add_custom_button(__("Close"), function() { - frm.events.close_open_production_plan(frm, true); - }, __("Status")); + frm.add_custom_button( + __("Close"), + function () { + frm.events.close_open_production_plan(frm, true); + }, + __("Status") + ); } if (frm.doc.po_items && frm.doc.status !== "Closed") { - frm.add_custom_button(__("Work Order / Subcontract PO"), ()=> { - frm.trigger("make_work_order"); - }, __('Create')); + frm.add_custom_button( + __("Work Order / Subcontract PO"), + () => { + frm.trigger("make_work_order"); + }, + __("Create") + ); } - if (frm.doc.mr_items && frm.doc.mr_items.length && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { - frm.add_custom_button(__("Material Request"), ()=> { - frm.trigger("make_material_request"); - }, __('Create')); + if ( + frm.doc.mr_items && + frm.doc.mr_items.length && + !in_list(["Material Requested", "Closed"], frm.doc.status) + ) { + frm.add_custom_button( + __("Material Request"), + () => { + frm.trigger("make_material_request"); + }, + __("Create") + ); } } } if (frm.doc.status !== "Closed") { - frm.page.set_inner_btn_group_as_primary(__('Create')); + frm.page.set_inner_btn_group_as_primary(__("Create")); } frm.trigger("material_requirement"); @@ -145,19 +170,19 @@ frappe.ui.form.on('Production Plan', { ${__("Planned Qty: Quantity, for which, Work Order has been raised, but is pending to be manufactured.")}
  • - ${__('Requested Qty: Quantity requested for purchase, but not ordered.')} + ${__("Requested Qty: Quantity requested for purchase, but not ordered.")}
  • - ${__('Ordered Qty: Quantity ordered for purchase, but not received.')} + ${__("Ordered Qty: Quantity ordered for purchase, but not received.")}
  • ${__("Reserved Qty: Quantity ordered for sale, but not delivered.")}
  • - ${__('Reserved Qty for Production: Raw materials quantity to make manufacturing items.')} + ${__("Reserved Qty for Production: Raw materials quantity to make manufacturing items.")}
  • - ${__('Reserved Qty for Subcontract: Raw materials quantity to make subcontracted items.')} + ${__("Reserved Qty for Subcontract: Raw materials quantity to make subcontracted items.")}
  • @@ -168,15 +193,15 @@ frappe.ui.form.on('Production Plan', { set_field_options("projected_qty_formula", projected_qty_formula); }, - close_open_production_plan(frm, close=false) { + close_open_production_plan(frm, close = false) { frappe.call({ method: "set_status", freeze: true, doc: frm.doc, - args: {close : close}, - callback: function() { + args: { close: close, update_bin: true }, + callback: function () { frm.reload_doc(); - } + }, }); }, @@ -185,19 +210,19 @@ frappe.ui.form.on('Production Plan', { method: "make_work_order", freeze: true, doc: frm.doc, - callback: function() { + callback: function () { frm.reload_doc(); - } + }, }); }, make_material_request(frm) { - - frappe.confirm(__("Do you want to submit the material request"), - function() { + frappe.confirm( + __("Do you want to submit the material request"), + function () { frm.events.create_material_request(frm, 1); }, - function() { + function () { frm.events.create_material_request(frm, 0); } ); @@ -210,9 +235,9 @@ frappe.ui.form.on('Production Plan', { method: "make_material_request", freeze: true, doc: frm.doc, - callback: function(r) { + callback: function (r) { frm.reload_doc(); - } + }, }); }, @@ -220,9 +245,9 @@ frappe.ui.form.on('Production Plan', { frappe.call({ method: "get_open_sales_orders", doc: frm.doc, - callback: function(r) { + callback: function (r) { refresh_field("sales_orders"); - } + }, }); }, @@ -230,22 +255,22 @@ frappe.ui.form.on('Production Plan', { frappe.call({ method: "get_pending_material_requests", doc: frm.doc, - callback: function() { - refresh_field('material_requests'); - } + callback: function () { + refresh_field("material_requests"); + }, }); }, get_items(frm) { - frm.clear_table('prod_plan_references'); + frm.clear_table("prod_plan_references"); frappe.call({ method: "get_items", freeze: true, doc: frm.doc, callback: function () { - refresh_field('po_items'); - } + refresh_field("po_items"); + }, }); }, combine_items(frm) { @@ -255,12 +280,12 @@ frappe.ui.form.on('Production Plan', { method: "get_items", freeze: true, doc: frm.doc, - callback: function() { + callback: function () { frm.refresh_field("po_items"); if (frm.doc.sub_assembly_items.length > 0) { frm.trigger("get_sub_assembly_items"); } - } + }, }); }, @@ -278,9 +303,9 @@ frappe.ui.form.on('Production Plan', { method: "get_sub_assembly_items", freeze: true, doc: frm.doc, - callback: function() { + callback: function () { refresh_field("sub_assembly_items"); - } + }, }); }, @@ -294,9 +319,11 @@ frappe.ui.form.on('Production Plan', { frappe.throw(__("Select the Warehouse")); } - frm.events.get_items_for_material_requests(frm, [{ - warehouse: frm.doc.for_warehouse - }]); + frm.events.get_items_for_material_requests(frm, [ + { + warehouse: frm.doc.for_warehouse, + }, + ]); }, transfer_materials(frm) { @@ -305,6 +332,8 @@ frappe.ui.form.on('Production Plan', { frappe.throw(__("Select the Warehouse")); } + frm.set_value("consider_minimum_order_qty", 0); + if (frm.doc.ignore_existing_ordered_qty) { frm.events.get_items_for_material_requests(frm); } else { @@ -313,26 +342,26 @@ frappe.ui.form.on('Production Plan', { title: title, fields: [ { - 'label': __('Transfer From Warehouses'), - 'fieldtype': 'Table MultiSelect', - 'fieldname': 'warehouses', - 'options': 'Production Plan Material Request Warehouse', + label: __("Transfer From Warehouses"), + fieldtype: "Table MultiSelect", + fieldname: "warehouses", + options: "Production Plan Material Request Warehouse", get_query: function () { return { filters: { - company: frm.doc.company - } + company: frm.doc.company, + }, }; }, }, { - 'label': __('For Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'target_warehouse', - 'read_only': true, - 'default': frm.doc.for_warehouse - } - ] + label: __("For Warehouse"), + fieldtype: "Link", + fieldname: "target_warehouse", + read_only: true, + default: frm.doc.for_warehouse, + }, + ], }); dialog.show(); @@ -351,82 +380,90 @@ frappe.ui.form.on('Production Plan', { freeze: true, args: { doc: frm.doc, - warehouses: warehouses || [] + warehouses: warehouses || [], }, - callback: function(r) { - if(r.message) { - frm.set_value('mr_items', []); - r.message.forEach(row => { - let d = frm.add_child('mr_items'); + callback: function (r) { + if (r.message) { + frm.set_value("mr_items", []); + r.message.forEach((row) => { + let d = frm.add_child("mr_items"); for (let field in row) { - if (field !== 'name') { + if (field !== "name") { d[field] = row[field]; } } }); } - refresh_field('mr_items'); - } + refresh_field("mr_items"); + }, }); }, download_materials_required(frm) { - const fields = [{ - fieldname: 'warehouses', - fieldtype: 'Table MultiSelect', - label: __('Warehouses'), - default: frm.doc.from_warehouse, - options: "Production Plan Material Request Warehouse", - get_query: function () { - return { - filters: { - company: frm.doc.company - } - }; + const fields = [ + { + fieldname: "warehouses", + fieldtype: "Table MultiSelect", + label: __("Warehouses"), + default: frm.doc.from_warehouse, + options: "Production Plan Material Request Warehouse", + get_query: function () { + return { + filters: { + company: frm.doc.company, + }, + }; + }, }, - }]; + ]; - frappe.prompt(fields, (row) => { - let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; - open_url_post(frappe.request.url, { - cmd: get_template_url, - doc: frm.doc, - warehouses: row.warehouses - }); - }, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock')); + frappe.prompt( + fields, + (row) => { + let get_template_url = + "erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials"; + open_url_post(frappe.request.url, { + cmd: get_template_url, + doc: frm.doc, + warehouses: row.warehouses, + }); + }, + __("Select Warehouses to get Stock for Materials Planning"), + __("Get Stock") + ); }, show_progress(frm) { var bars = []; - var message = ''; - var title = ''; + var message = ""; + var title = ""; // produced qty let item_wise_qty = {}; frm.doc.po_items.forEach((data) => { - if(!item_wise_qty[data.item_code]) { + if (!item_wise_qty[data.item_code]) { item_wise_qty[data.item_code] = data.produced_qty; } else { item_wise_qty[data.item_code] += data.produced_qty; } - }) + }); if (item_wise_qty) { for (var key in item_wise_qty) { - title += __('Item {0}: {1} qty produced. ', [key, item_wise_qty[key]]); + title += __("Item {0}: {1} qty produced. ", [key, item_wise_qty[key]]); } } bars.push({ - 'title': title, - 'width': (frm.doc.total_produced_qty / frm.doc.total_planned_qty * 100) + '%', - 'progress_class': 'progress-bar-success' + title: title, + width: (frm.doc.total_produced_qty / frm.doc.total_planned_qty) * 100 + "%", + progress_class: "progress-bar-success", }); - if (bars[0].width == '0%') { - bars[0].width = '0.5%'; + if (bars[0].width == "0%") { + bars[0].width = "0.5%"; } message = title; - frm.dashboard.add_progress(__('Status'), bars, message); + frm.dashboard.add_progress(__("Status"), bars, message); }, }); @@ -437,13 +474,13 @@ frappe.ui.form.on("Production Plan Item", { frappe.call({ method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_item_data", args: { - item_code: row.item_code + item_code: row.item_code, }, - callback: function(r) { + callback: function (r) { for (let key in r.message) { frappe.model.set_value(cdt, cdn, key, r.message[key]); } - } + }, }); } }, @@ -458,30 +495,29 @@ frappe.ui.form.on("Material Request Plan Item", { args: { row: row, company: frm.doc.company, - for_warehouse: row.warehouse + for_warehouse: row.warehouse, }, - callback: function(r) { + callback: function (r) { if (r.message) { - let {projected_qty, actual_qty} = r.message[0]; + let { projected_qty, actual_qty } = r.message[0]; frappe.model.set_value(cdt, cdn, { - 'projected_qty': projected_qty, - 'actual_qty': actual_qty + projected_qty: projected_qty, + actual_qty: actual_qty, }); } - } - }) + }, + }); } }, material_request_type(frm, cdt, cdn) { let row = locals[cdt][cdn]; - if (row.from_warehouse && - row.material_request_type !== "Material Transfer") { - frappe.model.set_value(cdt, cdn, 'from_warehouse', ''); + if (row.from_warehouse && row.material_request_type !== "Material Transfer") { + frappe.model.set_value(cdt, cdn, "from_warehouse", ""); } - } + }, }); frappe.ui.form.on("Production Plan Sales Order", { @@ -504,47 +540,61 @@ frappe.ui.form.on("Production Plan Sales Order", { method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details", args: { sales_order }, callback(r) { - const {transaction_date, customer, grand_total} = r.message; - frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date); - frappe.model.set_value(cdt, cdn, 'customer', customer); - frappe.model.set_value(cdt, cdn, 'grand_total', grand_total); - } + const { transaction_date, customer, grand_total } = r.message; + frappe.model.set_value(cdt, cdn, "sales_order_date", transaction_date); + frappe.model.set_value(cdt, cdn, "customer", customer); + frappe.model.set_value(cdt, cdn, "grand_total", grand_total); + }, }); - } + }, }); } - } + }, }); -frappe.tour['Production Plan'] = [ +frappe.ui.form.on("Production Plan Sub Assembly Item", { + fg_warehouse(frm, cdt, cdn) { + erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "sub_assembly_items", "fg_warehouse"); + }, +}); + +frappe.tour["Production Plan"] = [ { fieldname: "get_items_from", title: "Get Items From", - description: __("Select whether to get items from a Sales Order or a Material Request. For now select Sales Order.\n A Production Plan can also be created manually where you can select the Items to manufacture.") + description: __( + "Select whether to get items from a Sales Order or a Material Request. For now select Sales Order.\n A Production Plan can also be created manually where you can select the Items to manufacture." + ), }, { fieldname: "get_sales_orders", title: "Get Sales Orders", - description: __("Click on Get Sales Orders to fetch sales orders based on the above filters.") + description: __("Click on Get Sales Orders to fetch sales orders based on the above filters."), }, { fieldname: "get_items", title: "Get Finished Goods for Manufacture", - description: __("Click on 'Get Finished Goods for Manufacture' to fetch the items from the above Sales Orders. Items only for which a BOM is present will be fetched.") + description: __( + "Click on 'Get Finished Goods for Manufacture' to fetch the items from the above Sales Orders. Items only for which a BOM is present will be fetched." + ), }, { fieldname: "po_items", title: "Finished Goods", - description: __("On expanding a row in the Items to Manufacture table, you'll see an option to 'Include Exploded Items'. Ticking this includes raw materials of the sub-assembly items in the production process.") + description: __( + "On expanding a row in the Items to Manufacture table, you'll see an option to 'Include Exploded Items'. Ticking this includes raw materials of the sub-assembly items in the production process." + ), }, { fieldname: "include_non_stock_items", title: "Include Non Stock Items", - description: __("To include non-stock items in the material request planning. i.e. Items for which 'Maintain Stock' checkbox is unticked.") + description: __( + "To include non-stock items in the material request planning. i.e. Items for which 'Maintain Stock' checkbox is unticked." + ), }, { fieldname: "include_subcontracted_items", title: "Include Subcontracted Items", - description: __("To add subcontracted Item's raw materials if include exploded items is disabled.") - } + description: __("To add subcontracted Item's raw materials if include exploded items is disabled."), + }, ]; diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 49386c4ebc4..84bbad58c38 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -48,6 +48,7 @@ "material_request_planning", "include_non_stock_items", "include_subcontracted_items", + "consider_minimum_order_qty", "include_safety_stock", "ignore_existing_ordered_qty", "column_break_25", @@ -297,7 +298,8 @@ "no_copy": 1, "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nClosed\nCancelled\nMaterial Requested", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "amended_from", @@ -419,17 +421,25 @@ "fieldtype": "Column Break" }, { + "description": "When a parent warehouse is chosen, the system conducts stock checks against the associated child warehouses", "fieldname": "sub_assembly_warehouse", "fieldtype": "Link", "label": "Sub Assembly Warehouse", + "mandatory_depends_on": "eval:doc.skip_available_sub_assembly_item === 1", "options": "Warehouse" + }, + { + "default": "0", + "fieldname": "consider_minimum_order_qty", + "fieldtype": "Check", + "label": "Consider Minimum Order Qty" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-03 14:08:11.928027", + "modified": "2024-02-27 13:34:20.692211", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 7778f060146..f4822fe583a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -236,9 +236,10 @@ class ProductionPlan(Document): so_item.parent, so_item.item_code, so_item.warehouse, - ( - (so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor - ).as_("pending_qty"), + so_item.qty, + so_item.work_order_qty, + so_item.delivered_qty, + so_item.conversion_factor, so_item.description, so_item.name, so_item.bom_no, @@ -261,6 +262,11 @@ class ProductionPlan(Document): items = items_query.run(as_dict=True) + for item in items: + item.pending_qty = ( + flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0) * item.conversion_factor + ) + pi = frappe.qb.DocType("Packed Item") packed_items_query = ( @@ -305,9 +311,7 @@ class ProductionPlan(Document): if not self.get("material_requests") or not self.get_so_mr_list( "material_request", "material_requests" ): - frappe.throw( - _("Please fill the Material Requests table"), title=_("Material Requests Required") - ) + frappe.throw(_("Please fill the Material Requests table"), title=_("Material Requests Required")) mr_list = self.get_so_mr_list("material_request", "material_requests") @@ -503,11 +507,12 @@ class ProductionPlan(Document): frappe.delete_doc("Work Order", d.name) @frappe.whitelist() - def set_status(self, close=None): + def set_status(self, close=None, update_bin=False): self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus) if close: self.db_set("status", "Closed") + self.update_bin_qty() return if self.total_produced_qty > 0: @@ -522,6 +527,9 @@ class ProductionPlan(Document): if close is not None: self.db_set("status", self.status) + if update_bin and self.docstatus == 1 and self.status != "Completed": + self.update_bin_qty() + def update_ordered_status(self): update_status = False for d in self.po_items: @@ -566,6 +574,13 @@ class ProductionPlan(Document): "project": self.project, } + key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse) + if self.combine_items: + key = (d.item_code, d.sales_order, d.warehouse) + + if not d.sales_order: + key = (d.name, d.item_code, d.warehouse) + if not item_details["project"] and d.sales_order: item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project") @@ -575,11 +590,11 @@ class ProductionPlan(Document): else: item_details.update( { - "qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse), {}).get("qty")) + "qty": flt(item_dict.get(key, {}).get("qty")) + (flt(d.planned_qty) - flt(d.ordered_qty)) } ) - item_dict[(d.item_code, d.sales_order, d.warehouse)] = item_details + item_dict[key] = item_details return item_dict @@ -603,7 +618,7 @@ class ProductionPlan(Document): def make_work_order_for_finished_goods(self, wo_list, default_warehouses): items_data = self.get_production_items() - for key, item in items_data.items(): + for _key, item in items_data.items(): if self.sub_assembly_items: item["use_multi_level_bom"] = 0 @@ -739,7 +754,7 @@ class ProductionPlan(Document): key = "{}:{}:{}".format(item.sales_order, material_request_type, item_doc.customer or "") schedule_date = item.schedule_date or add_days(nowdate(), cint(item_doc.lead_time_days)) - if not key in material_request_map: + if key not in material_request_map: # make a new MR for the combination material_request_map[key] = frappe.new_doc("Material Request") material_request = material_request_map[key] @@ -789,7 +804,7 @@ class ProductionPlan(Document): if material_request_list: material_request_list = [ - """{1}""".format(m.name, m.name) + f"""{m.name}""" for m in material_request_list ] msgprint(_("{0} created").format(comma_and(material_request_list))) @@ -803,8 +818,8 @@ class ProductionPlan(Document): sub_assembly_items_store = [] # temporary store to process all subassembly items for row in self.po_items: - if self.skip_available_sub_assembly_item and not row.warehouse: - frappe.throw(_("Row #{0}: Please select the FG Warehouse in Assembly Items").format(row.idx)) + if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse: + frappe.throw(_("Row #{0}: Please select the Sub Assembly Warehouse").format(row.idx)) if not row.item_code: frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) @@ -814,15 +829,24 @@ class ProductionPlan(Document): bom_data = [] - warehouse = ( - (self.sub_assembly_warehouse or row.warehouse) - if self.skip_available_sub_assembly_item - else None - ) + warehouse = (self.sub_assembly_warehouse) if self.skip_available_sub_assembly_item else None get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) sub_assembly_items_store.extend(bom_data) + if not sub_assembly_items_store and self.skip_available_sub_assembly_item: + message = ( + _( + "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}." + ).format(self.sub_assembly_warehouse) + + "

    " + ) + message += _( + "If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox." + ) + + frappe.msgprint(message, title=_("Note")) + if self.combine_sub_items: # Combine subassembly items sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store) @@ -835,15 +859,19 @@ class ProductionPlan(Document): def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): "Modify bom_data, set additional details." + is_group_warehouse = frappe.db.get_value("Warehouse", self.sub_assembly_warehouse, "is_group") + for data in bom_data: data.qty = data.stock_qty data.production_plan_item = row.name - data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse data.schedule_date = row.planned_start_date data.type_of_manufacturing = manufacturing_type or ( "Subcontract" if data.is_sub_contracted_item else "In House" ) + if not is_group_warehouse: + data.fg_warehouse = self.sub_assembly_warehouse + def set_default_supplier_for_subcontracting_order(self): items = [ d.production_item for d in self.sub_assembly_items if d.type_of_manufacturing == "Subcontract" @@ -902,9 +930,7 @@ class ProductionPlan(Document): return sub_assembly_items_store def all_items_completed(self): - all_items_produced = all( - flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 for d in self.po_items - ) + all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 for d in self.po_items) if not all_items_produced: return False @@ -947,9 +973,7 @@ def download_raw_materials(doc, warehouses=None): doc.warehouse = None frappe.flags.show_qty_in_stock_uom = 1 - items = get_items_for_material_requests( - doc, warehouses=warehouses, get_parent_warehouse_data=True - ) + items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True) for d in items: item_list.append( @@ -993,9 +1017,7 @@ def download_raw_materials(doc, warehouses=None): build_csv_response(item_list, doc.name) -def get_exploded_items( - item_details, company, bom_no, include_non_stock_items, planned_qty=1, doc=None -): +def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1, doc=None): bei = frappe.qb.DocType("BOM Explosion Item") bom = frappe.qb.DocType("BOM") item = frappe.qb.DocType("Item") @@ -1131,7 +1153,14 @@ def get_subitems( def get_material_request_items( - row, sales_order, company, ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict + doc, + row, + sales_order, + company, + ignore_existing_ordered_qty, + include_safety_stock, + warehouse, + bin_dict, ): total_qty = row["qty"] @@ -1140,8 +1169,10 @@ def get_material_request_items( required_qty = total_qty elif total_qty > bin_dict.get("projected_qty", 0): required_qty = total_qty - bin_dict.get("projected_qty", 0) - if required_qty > 0 and required_qty < row["min_order_qty"]: + + if doc.get("consider_minimum_order_qty") and required_qty > 0 and required_qty < row["min_order_qty"]: required_qty = row["min_order_qty"] + item_group_defaults = get_item_group_defaults(row.item_code, company) if not row["purchase_uom"]: @@ -1163,9 +1194,7 @@ def get_material_request_items( if include_safety_stock: required_qty += flt(row["safety_stock"]) - item_details = frappe.get_cached_value( - "Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1 - ) + item_details = frappe.get_cached_value("Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1) conversion_factor = 1.0 if ( @@ -1218,7 +1247,9 @@ def get_sales_orders(self): & (pi.parent_item == so_item.item_code) & ( ExistsCriterion( - frappe.qb.from_(bom).select(bom.name).where((bom.item == pi.item_code) & (bom.is_active == 1)) + frappe.qb.from_(bom) + .select(bom.name) + .where((bom.item == pi.item_code) & (bom.is_active == 1)) ) ) ) @@ -1261,7 +1292,7 @@ def get_sales_orders(self): ) open_so_query = open_so_query.where( - (ExistsCriterion(open_so_subquery1) | ExistsCriterion(open_so_subquery2)) + ExistsCriterion(open_so_subquery1) | ExistsCriterion(open_so_subquery2) ) open_so = open_so_query.run(as_dict=True) @@ -1374,7 +1405,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d so_item_details = frappe._dict() sub_assembly_items = {} - if doc.get("skip_available_sub_assembly_item"): + if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"): for d in doc.get("sub_assembly_items"): sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty")) @@ -1383,9 +1414,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d data["include_exploded_items"] = 1 planned_qty = data.get("required_qty") or data.get("planned_qty") - ignore_existing_ordered_qty = ( - data.get("ignore_existing_ordered_qty") or ignore_existing_ordered_qty - ) + ignore_existing_ordered_qty = data.get("ignore_existing_ordered_qty") or ignore_existing_ordered_qty warehouse = doc.get("for_warehouse") item_details = {} @@ -1403,24 +1432,27 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) if bom_no: - if ( - data.get("include_exploded_items") - and doc.get("sub_assembly_items") - and doc.get("skip_available_sub_assembly_item") - ): - item_details = get_raw_materials_of_sub_assembly_items( - item_details, - company, - bom_no, - include_non_stock_items, - sub_assembly_items, - planned_qty=planned_qty, - ) + if data.get("include_exploded_items") and doc.get("skip_available_sub_assembly_item"): + item_details = {} + if doc.get("sub_assembly_items"): + item_details = get_raw_materials_of_sub_assembly_items( + item_details, + company, + bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=planned_qty, + ) elif data.get("include_exploded_items") and include_subcontracted_items: # fetch exploded items from BOM item_details = get_exploded_items( - item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty, doc=doc + item_details, + company, + bom_no, + include_non_stock_items, + planned_qty=planned_qty, + doc=doc, ) else: item_details = get_subitems( @@ -1471,7 +1503,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d so_item_details[sales_order][item_code] = details mr_items = [] - for sales_order, item_code in so_item_details.items(): + for sales_order in so_item_details: item_dict = so_item_details[sales_order] for details in item_dict.values(): bin_dict = get_bin_details(details, doc.company, warehouse) @@ -1479,6 +1511,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d if details.qty > 0: items = get_material_request_items( + doc, details, sales_order, company, @@ -1574,7 +1607,7 @@ def get_item_data(item_code): return { "bom_no": item_details.get("bom_no"), - "stock_uom": item_details.get("stock_uom") + "stock_uom": item_details.get("stock_uom"), # "description": item_details.get("description") } @@ -1587,34 +1620,39 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse= stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) if warehouse: - bin_dict = get_bin_details(d, company, for_warehouse=warehouse) + bin_details = get_bin_details(d, company, for_warehouse=warehouse) - if bin_dict and bin_dict[0].projected_qty > 0: - if bin_dict[0].projected_qty > stock_qty: - continue - else: - stock_qty = stock_qty - bin_dict[0].projected_qty + for _bin_dict in bin_details: + if _bin_dict.projected_qty > 0: + if _bin_dict.projected_qty > stock_qty: + stock_qty = 0 + continue + else: + stock_qty = stock_qty - _bin_dict.projected_qty - bom_data.append( - frappe._dict( - { - "parent_item_code": parent_item_code, - "description": d.description, - "production_item": d.item_code, - "item_name": d.item_name, - "stock_uom": d.stock_uom, - "uom": d.stock_uom, - "bom_no": d.value, - "is_sub_contracted_item": d.is_sub_contracted_item, - "bom_level": indent, - "indent": indent, - "stock_qty": stock_qty, - } + if stock_qty > 0: + bom_data.append( + frappe._dict( + { + "parent_item_code": parent_item_code, + "description": d.description, + "production_item": d.item_code, + "item_name": d.item_name, + "stock_uom": d.stock_uom, + "uom": d.stock_uom, + "bom_no": d.value, + "is_sub_contracted_item": d.is_sub_contracted_item, + "bom_level": indent, + "indent": indent, + "stock_qty": stock_qty, + } + ) ) - ) - if d.value: - get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1) + if d.value: + get_sub_assembly_items( + d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1 + ) def set_default_warehouses(row, default_warehouses): @@ -1666,23 +1704,23 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): return reserved_qty_for_production_plan - reserved_qty_for_production +@frappe.request_cache def get_non_completed_production_plans(): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Production Plan Item") - query = ( + return ( frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) .select(table.name) + .distinct() .where( (table.docstatus == 1) & (table.status.notin(["Completed", "Closed"])) & (child.planned_qty > child.ordered_qty) ) - ).run(as_dict=True) - - return list(set([d.name for d in query])) + ).run(pluck="name") def get_raw_materials_of_sub_assembly_items( @@ -1756,9 +1794,7 @@ def get_raw_materials_of_sub_assembly_items( @frappe.whitelist() -def sales_order_query( - doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None -): +def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None): frappe.has_permission("Production Plan", throw=True) if not filters: diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_list.js b/erpnext/manufacturing/doctype/production_plan/production_plan_list.js index 8f946866247..bfef6e1220f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan_list.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan_list.js @@ -1,4 +1,4 @@ -frappe.listview_settings['Production Plan'] = { +frappe.listview_settings["Production Plan"] = { hide_name_column: true, add_fields: ["status"], filters: [["status", "!=", "Closed"]], @@ -6,14 +6,18 @@ frappe.listview_settings['Production Plan'] = { if (doc.status === "Submitted") { return [__("Not Started"), "orange", "status,=,Submitted"]; } else { - return [__(doc.status), { - "Draft": "red", - "In Process": "orange", - "Completed": "green", - "Material Requested": "yellow", - "Cancelled": "gray", - "Closed": "grey" - }[doc.status], "status,=," + doc.status]; + return [ + __(doc.status), + { + Draft: "red", + "In Process": "orange", + Completed: "green", + "Material Requested": "yellow", + Cancelled: "gray", + Closed: "grey", + }[doc.status], + "status,=," + doc.status, + ]; } - } + }, }; diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 6a50a10d07a..31f34a83762 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -110,9 +110,7 @@ class TestProductionPlan(FrappeTestCase): def test_production_plan_start_date(self): "Test if Work Order has same Planned Start Date as Prod Plan." planned_date = add_to_date(date=None, days=3) - plan = create_production_plan( - item_code="Test Production Item 1", planned_start_date=planned_date - ) + plan = create_production_plan(item_code="Test Production Item 1", planned_start_date=planned_date) plan.make_work_order() work_orders = frappe.get_all( @@ -213,9 +211,7 @@ class TestProductionPlan(FrappeTestCase): ) wo_doc = frappe.get_doc("Work Order", work_order) - wo_doc.update( - {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"} - ) + wo_doc.update({"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"}) wo_doc.submit() so_wo_qty = frappe.db.get_value("Sales Order Item", sales_order_item, "work_order_qty") @@ -400,7 +396,7 @@ class TestProductionPlan(FrappeTestCase): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom bom_tree_1 = {"Test Laptop": {"Test Motherboard": {"Test Motherboard Wires": {}}}} - bom = create_nested_bom(bom_tree_1, prefix="") + create_nested_bom(bom_tree_1, prefix="") item_doc = frappe.get_doc("Item", "Test Motherboard") company = "_Test Company" @@ -504,14 +500,10 @@ class TestProductionPlan(FrappeTestCase): def test_pp_to_mr_customer_provided(self): "Test Material Request from Production Plan for Customer Provided Item." - create_item( - "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 - ) + create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0) create_item("Production Item CUST") - for item, raw_materials in { - "Production Item CUST": ["Raw Material Item 1", "CUST-0987"] - }.items(): + for item, raw_materials in {"Production Item CUST": ["Raw Material Item 1", "CUST-0987"]}.items(): if not frappe.db.get_value("BOM", {"item": item}): make_bom(item=item, raw_materials=raw_materials) production_plan = create_production_plan(item_code="Production Item CUST") @@ -661,10 +653,10 @@ class TestProductionPlan(FrappeTestCase): items_data = pln.get_production_items() # Update qty - items_data[(item, None, None)]["qty"] = qty + items_data[(pln.po_items[0].name, item, None)]["qty"] = qty # Create and Submit Work Order for each item in items_data - for key, item in items_data.items(): + for _key, item in items_data.items(): if pln.sub_assembly_items: item["use_multi_level_bom"] = 0 @@ -833,7 +825,6 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(pln.po_items[0].planned_qty, 0.55) def test_temporary_name_relinking(self): - pp = frappe.new_doc("Production Plan") # this can not be unittested so mocking data that would be expected @@ -849,7 +840,7 @@ class TestProductionPlan(FrappeTestCase): pp.append("sub_assembly_items", {"production_plan_item": po_item.temporary_name}) pp._rename_temporary_references() - for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items): + for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items, strict=False): self.assertEqual(po_item.name, subassy_item.production_plan_item) # bad links should be erased @@ -860,7 +851,7 @@ class TestProductionPlan(FrappeTestCase): # reattempting on same doc shouldn't change anything pp._rename_temporary_references() - for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items): + for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items, strict=False): self.assertEqual(po_item.name, subassy_item.production_plan_item) def test_produced_qty_for_multi_level_bom_item(self): @@ -1137,9 +1128,7 @@ class TestProductionPlan(FrappeTestCase): 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") - ) + 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_from_warehouse_for_purchase_material_request(self): @@ -1147,7 +1136,7 @@ class TestProductionPlan(FrappeTestCase): from erpnext.stock.utils import get_or_make_bin create_item("RM-TEST-123 For Purchase", valuation_rate=100) - bin_name = get_or_make_bin("RM-TEST-123 For Purchase", "_Test Warehouse - _TC") + get_or_make_bin("RM-TEST-123 For Purchase", "_Test Warehouse - _TC") t_warehouse = create_warehouse("_Test Store - _TC") make_stock_entry( item_code="Raw Material Item 1", @@ -1157,9 +1146,7 @@ class TestProductionPlan(FrappeTestCase): ) plan = create_production_plan(item_code="Test Production Item 1", do_not_save=1) - mr_items = get_items_for_material_requests( - plan.as_dict(), warehouses=[{"warehouse": t_warehouse}] - ) + mr_items = get_items_for_material_requests(plan.as_dict(), warehouses=[{"warehouse": t_warehouse}]) for d in mr_items: plan.append("mr_items", d) @@ -1194,6 +1181,7 @@ class TestProductionPlan(FrappeTestCase): ignore_existing_ordered_qty=1, do_not_submit=1, skip_available_sub_assembly_item=1, + sub_assembly_warehouse="_Test Warehouse - _TC", warehouse="_Test Warehouse - _TC", ) @@ -1221,6 +1209,35 @@ class TestProductionPlan(FrappeTestCase): if row.item_code == "SubAssembly2 For SUB Test": self.assertEqual(row.quantity, 10) + def test_sub_assembly_and_their_raw_materials_exists(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + bom_tree = { + "FG1 For SUB Test": { + "SAB1 For SUB Test": {"CP1 For SUB Test": {}}, + "SAB2 For SUB Test": {}, + } + } + + parent_bom = create_nested_bom(bom_tree, prefix="") + for item in ["SAB1 For SUB Test", "SAB2 For SUB Test"]: + make_stock_entry(item_code=item, qty=10, rate=100, target="_Test Warehouse - _TC") + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=10, + ignore_existing_ordered_qty=1, + do_not_submit=1, + skip_available_sub_assembly_item=1, + warehouse="_Test Warehouse - _TC", + ) + + items = get_items_for_material_requests( + plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}] + ) + + self.assertFalse(items) + def test_transfer_and_purchase_mrp_for_purchase_uom(self): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -1298,6 +1315,7 @@ class TestProductionPlan(FrappeTestCase): ignore_existing_ordered_qty=1, do_not_submit=1, skip_available_sub_assembly_item=1, + sub_assembly_warehouse="_Test Warehouse - _TC", warehouse="_Test Warehouse - _TC", ) @@ -1426,9 +1444,7 @@ class TestProductionPlan(FrappeTestCase): ) pln.for_warehouse = rm_warehouse - items = get_items_for_material_requests( - pln.as_dict(), warehouses=[{"warehouse": store_warehouse}] - ) + items = get_items_for_material_requests(pln.as_dict(), warehouses=[{"warehouse": store_warehouse}]) for row in items: self.assertEqual(row.get("quantity"), 10.0) @@ -1437,9 +1453,7 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(row.get("from_warehouse"), store_warehouse) self.assertEqual(row.get("conversion_factor"), 1.0) - items = get_items_for_material_requests( - pln.as_dict(), warehouses=[{"warehouse": pln.for_warehouse}] - ) + items = get_items_for_material_requests(pln.as_dict(), warehouses=[{"warehouse": pln.for_warehouse}]) for row in items: self.assertEqual(row.get("quantity"), 1.0) @@ -1447,6 +1461,147 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(row.get("uom"), "Nos") self.assertEqual(row.get("conversion_factor"), 10.0) + def test_unreserve_qty_on_closing_of_pp(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + from erpnext.stock.utils import get_or_make_bin + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + rm_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + + create_warehouse("Store Warehouse", company="_Test Company") + rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company") + + make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan( + item_code=fg_item, planned_qty=10, stock_uom="_Test UOM 1", do_not_submit=1 + ) + + pln.for_warehouse = rm_warehouse + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + pln.append("mr_items", d) + + pln.save() + pln.submit() + + bin_name = get_or_make_bin(rm_item, rm_warehouse) + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln.reload() + pln.set_status(close=True, update_bin=True) + + bin_name = get_or_make_bin(rm_item, rm_warehouse) + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + self.assertAlmostEqual(after_qty, before_qty - 10) + + pln.reload() + pln.set_status(close=False, update_bin=True) + + bin_name = get_or_make_bin(rm_item, rm_warehouse) + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + self.assertAlmostEqual(after_qty, before_qty) + + def test_min_order_qty_in_pp(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item = make_item(properties={"is_stock_item": 1, "min_order_qty": 1000}).name + + rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company") + + make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan(item_code=fg_item, planned_qty=10, do_not_submit=1) + + pln.for_warehouse = rm_warehouse + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + self.assertEqual(d.get("quantity"), 10.0) + + pln.consider_minimum_order_qty = 1 + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + self.assertEqual(d.get("quantity"), 1000.0) + + def test_fg_item_quantity(self): + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item = make_item(properties={"is_stock_item": 1}).name + + make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan(item_code=fg_item, planned_qty=10, do_not_submit=1) + + pln.append( + "po_items", + { + "item_code": rm_item, + "planned_qty": 20, + "bom_no": pln.po_items[0].bom_no, + "warehouse": pln.po_items[0].warehouse, + "planned_start_date": add_to_date(nowdate(), days=1), + }, + ) + pln.submit() + wo_qty = {} + + for row in pln.po_items: + wo_qty[row.name] = row.planned_qty + + pln.make_work_order() + + work_orders = frappe.get_all( + "Work Order", + fields=["qty", "production_plan_item as name"], + filters={"production_plan": pln.name}, + ) + self.assertEqual(len(work_orders), 2) + + for row in work_orders: + self.assertEqual(row.qty, wo_qty[row.name]) + + def test_parent_warehouse_for_sub_assembly_items(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + parent_warehouse = "_Test Warehouse Group - _TC" + sub_warehouse = create_warehouse("Sub Warehouse", company="_Test Company") + + fg_item = make_item(properties={"is_stock_item": 1}).name + sf_item = make_item(properties={"is_stock_item": 1}).name + rm_item = make_item(properties={"is_stock_item": 1}).name + + bom_tree = {fg_item: {sf_item: {rm_item: {}}}} + create_nested_bom(bom_tree, prefix="") + + pln = create_production_plan( + item_code=fg_item, + planned_qty=10, + warehouse="_Test Warehouse - _TC", + sub_assembly_warehouse=parent_warehouse, + skip_available_sub_assembly_item=1, + do_not_submit=1, + skip_getting_mr_items=1, + ) + + pln.get_sub_assembly_items() + + for row in pln.sub_assembly_items: + self.assertFalse(row.fg_warehouse) + self.assertEqual(row.production_item, sf_item) + self.assertEqual(row.qty, 10.0) + + make_stock_entry(item_code=sf_item, qty=5, target=sub_warehouse, rate=100) + + pln.sub_assembly_items = [] + pln.get_sub_assembly_items() + + self.assertEqual(pln.sub_assembly_warehouse, parent_warehouse) + for row in pln.sub_assembly_items: + self.assertFalse(row.fg_warehouse) + self.assertEqual(row.production_item, sf_item) + self.assertEqual(row.qty, 5.0) + def create_production_plan(**args): """ @@ -1527,6 +1682,10 @@ def make_bom(**args): } ) + if args.operating_cost_per_bom_quantity: + bom.fg_based_operating_cost = 1 + bom.operating_cost_per_bom_quantity = args.operating_cost_per_bom_quantity + for item in args.raw_materials: item_doc = frappe.get_doc("Item", item) bom.append( diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json index 0688278e091..78a389760a7 100644 --- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json +++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json @@ -11,6 +11,7 @@ "bom_no", "column_break_6", "planned_qty", + "stock_uom", "warehouse", "planned_start_date", "section_break_9", @@ -18,7 +19,6 @@ "ordered_qty", "column_break_17", "description", - "stock_uom", "produced_qty", "reference_section", "sales_order", @@ -65,6 +65,7 @@ "width": "100px" }, { + "columns": 1, "fieldname": "planned_qty", "fieldtype": "Float", "in_list_view": 1, @@ -80,6 +81,7 @@ "fieldtype": "Column Break" }, { + "columns": 2, "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, @@ -141,8 +143,10 @@ "width": "200px" }, { + "columns": 1, "fieldname": "stock_uom", "fieldtype": "Link", + "in_list_view": 1, "label": "UOM", "oldfieldname": "stock_uom", "oldfieldtype": "Data", @@ -216,7 +220,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-11-25 14:15:40.061514", + "modified": "2024-02-27 13:24:43.571844", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.js b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.js index 53f87582d10..d4e74fa3ec7 100644 --- a/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.js +++ b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.js @@ -1,8 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Production Plan Material Request Warehouse', { +frappe.ui.form.on("Production Plan Material Request Warehouse", { // refresh: function(frm) { - // } }); diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json index aff740b732e..7965965d2b6 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -101,7 +101,6 @@ "columns": 1, "fieldname": "bom_level", "fieldtype": "Int", - "in_list_view": 1, "label": "Level (BOM)", "read_only": 1 }, @@ -149,8 +148,10 @@ "label": "Indent" }, { + "columns": 2, "fieldname": "fg_warehouse", "fieldtype": "Link", + "in_list_view": 1, "label": "Target Warehouse", "options": "Warehouse" }, @@ -170,6 +171,7 @@ "options": "Supplier" }, { + "columns": 1, "fieldname": "schedule_date", "fieldtype": "Datetime", "in_list_view": 1, @@ -207,7 +209,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-03 13:33:42.959387", + "modified": "2024-02-27 13:45:17.422435", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js index b480c70ad56..67e1bc713d0 100644 --- a/erpnext/manufacturing/doctype/routing/routing.js +++ b/erpnext/manufacturing/doctype/routing/routing.js @@ -1,38 +1,39 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Routing', { - refresh: function(frm) { +frappe.ui.form.on("Routing", { + refresh: function (frm) { frm.trigger("display_sequence_id_column"); }, - onload: function(frm) { + onload: function (frm) { frm.trigger("display_sequence_id_column"); }, - display_sequence_id_column: function(frm) { - frm.fields_dict.operations.grid.update_docfield_property( - 'sequence_id', 'in_list_view', 1 + display_sequence_id_column: function (frm) { + frm.fields_dict.operations.grid.update_docfield_property("sequence_id", "in_list_view", 1); + }, + + calculate_operating_cost: function (frm, child) { + const operating_cost = flt( + (flt(child.hour_rate) * flt(child.time_in_mins)) / 60, + precision("operating_cost", child) ); - }, - - calculate_operating_cost: function(frm, child) { - const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, precision("operating_cost", child)); frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost); - } + }, }); -frappe.ui.form.on('BOM Operation', { - operation: function(frm, cdt, cdn) { +frappe.ui.form.on("BOM Operation", { + operation: function (frm, cdt, cdn) { const d = locals[cdt][cdn]; - if(!d.operation) return; + if (!d.operation) return; frappe.call({ - "method": "frappe.client.get", + method: "frappe.client.get", args: { doctype: "Operation", - name: d.operation + name: d.operation, }, callback: function (data) { if (data.message.description) { @@ -44,41 +45,43 @@ frappe.ui.form.on('BOM Operation', { } frm.events.calculate_operating_cost(frm, d); - } + }, }); }, - workstation: function(frm, cdt, cdn) { + workstation: function (frm, cdt, cdn) { const d = locals[cdt][cdn]; frappe.call({ - "method": "frappe.client.get", + method: "frappe.client.get", args: { doctype: "Workstation", - name: d.workstation + name: d.workstation, }, callback: function (data) { frappe.model.set_value(d.doctype, d.name, "hour_rate", data.message.hour_rate); frm.events.calculate_operating_cost(frm, d); - } + }, }); }, - time_in_mins: function(frm, cdt, cdn) { + time_in_mins: function (frm, cdt, cdn) { const d = locals[cdt][cdn]; frm.events.calculate_operating_cost(frm, d); - } + }, }); -frappe.tour['Routing'] = [ +frappe.tour["Routing"] = [ { fieldname: "routing_name", title: "Routing Name", - description: __("Enter a name for Routing.") + description: __("Enter a name for Routing."), }, { fieldname: "operations", title: "BOM Operations", - description: __("Enter the Operation, the table will fetch the Operation details like Hourly Rate, Workstation automatically.\n\n After that, set the Operation Time in minutes and the table will calculate the Operation Costs based on the Hourly Rate and Operation Time.") - } + description: __( + "Enter the Operation, the table will fetch the Operation details like Hourly Rate, Workstation automatically.\n\n After that, set the Operation Time in minutes and the table will calculate the Operation Costs based on the Hourly Rate and Operation Time." + ), + }, ]; diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py index d4c37cf79e7..701eaeb9ef3 100644 --- a/erpnext/manufacturing/doctype/routing/routing.py +++ b/erpnext/manufacturing/doctype/routing/routing.py @@ -32,9 +32,9 @@ class Routing(Document): row.sequence_id = sequence_id + 1 elif sequence_id and row.sequence_id and cint(sequence_id) > cint(row.sequence_id): frappe.throw( - _("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}").format( - row.idx, row.sequence_id, sequence_id - ) + _( + "At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}" + ).format(row.idx, row.sequence_id, sequence_id) ) sequence_id = row.sequence_id diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.js b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js index be9db6a4089..8d6d3ddb879 100644 --- a/erpnext/manufacturing/doctype/sub_operation/sub_operation.js +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js @@ -1,8 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Sub Operation', { +frappe.ui.form.on("Sub Operation", { // refresh: function(frm) { - // } }); diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index b77e4e08735..0d4539c5287 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -40,7 +40,6 @@ class TestWorkOrder(FrappeTestCase): frappe.db.rollback() def check_planned_qty(self): - planned0 = ( frappe.db.get_value( "Bin", {"item_code": "_Test FG Item", "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty" @@ -129,9 +128,7 @@ class TestWorkOrder(FrappeTestCase): # reserved qty for production is updated self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission) - test_stock_entry.make_stock_entry( - item_code="_Test Item", target=warehouse, qty=100, basic_rate=100 - ) + test_stock_entry.make_stock_entry(item_code="_Test Item", target=warehouse, qty=100, basic_rate=100) test_stock_entry.make_stock_entry( item_code="_Test Item Home Desktop 100", target=warehouse, qty=100, basic_rate=100 ) @@ -141,9 +138,7 @@ class TestWorkOrder(FrappeTestCase): bin1_at_completion = get_bin(item, warehouse) - self.assertEqual( - cint(bin1_at_completion.reserved_qty_for_production), reserved_qty_on_submission - 1 - ) + self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production), reserved_qty_on_submission - 1) def test_production_item(self): wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True) @@ -178,9 +173,7 @@ class TestWorkOrder(FrappeTestCase): cint(self.bin1_at_start.reserved_qty_for_production) + 2, cint(self.bin1_on_submit.reserved_qty_for_production), ) - self.assertEqual( - cint(self.bin1_at_start.projected_qty), cint(self.bin1_on_submit.projected_qty) + 2 - ) + self.assertEqual(cint(self.bin1_at_start.projected_qty), cint(self.bin1_on_submit.projected_qty) + 2) def test_reserved_qty_for_production_cancel(self): self.test_reserved_qty_for_production_submit() @@ -234,7 +227,6 @@ class TestWorkOrder(FrappeTestCase): ) def test_reserved_qty_for_production_closed(self): - wo1 = make_wo_order_test_record(item="_Test FG Item", qty=2, source_warehouse=self.warehouse) item = wo1.required_items[0].item_code bin_before = get_bin(item, self.warehouse) @@ -366,7 +358,9 @@ class TestWorkOrder(FrappeTestCase): for item in s.items: if item.bom_no and item.item_code in scrap_item_details: self.assertEqual(wo_order_details.scrap_warehouse, item.t_warehouse) - self.assertEqual(flt(wo_order_details.qty) * flt(scrap_item_details[item.item_code]), item.qty) + self.assertEqual( + flt(wo_order_details.qty) * flt(scrap_item_details[item.item_code]), item.qty + ) def test_allow_overproduction(self): allow_overproduction("overproduction_percentage_for_work_order", 0) @@ -480,7 +474,7 @@ class TestWorkOrder(FrappeTestCase): ) self.assertEqual(len(job_cards), len(bom.operations)) - for i, job_card in enumerate(job_cards): + for _i, job_card in enumerate(job_cards): doc = frappe.get_doc("Job Card", job_card) doc.time_logs[0].completed_qty = 1 doc.submit() @@ -571,9 +565,7 @@ class TestWorkOrder(FrappeTestCase): for item in ["Test Batch Size Item For BOM", "Test Batch Size Item RM 1 For BOM"]: make_item(item, {"include_item_in_manufacturing": 1, "is_stock_item": 1}) - bom_name = frappe.db.get_value( - "BOM", {"item": fg_item, "is_active": 1, "with_operations": 1}, "name" - ) + bom_name = frappe.db.get_value("BOM", {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") if not bom_name: bom = make_bom(item=fg_item, rate=1000, raw_materials=[rm1], do_not_save=True) @@ -625,9 +617,7 @@ class TestWorkOrder(FrappeTestCase): make_item(item, item_args) - bom_name = frappe.db.get_value( - "BOM", {"item": fg_item, "is_active": 1, "with_operations": 1}, "name" - ) + bom_name = frappe.db.get_value("BOM", {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") if not bom_name: bom = make_bom(item=fg_item, rate=1000, raw_materials=[rm1], do_not_save=True) @@ -847,7 +837,6 @@ class TestWorkOrder(FrappeTestCase): ) qty = 10 - scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG source_warehouse = "Stores - _TC" wip_warehouse = "_Test Warehouse - _TC" fg_item_non_whole, _, bom_item = create_process_loss_bom_items() @@ -1189,7 +1178,9 @@ class TestWorkOrder(FrappeTestCase): for row in stock_entry.items: if row.item_code == fg_item: self.assertTrue(row.serial_no) - self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos))) + self.assertEqual( + sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos)) + ) except frappe.MandatoryError: self.fail("Batch generation causing failing in Work Order") @@ -1663,9 +1654,7 @@ class TestWorkOrder(FrappeTestCase): job_card2 = frappe.copy_doc(job_card_doc) self.assertRaises(frappe.ValidationError, job_card2.save) - frappe.db.set_single_value( - "Manufacturing Settings", "overproduction_percentage_for_work_order", 100 - ) + frappe.db.set_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order", 100) job_card2 = frappe.copy_doc(job_card_doc) job_card2.time_logs = [] @@ -1726,6 +1715,194 @@ class TestWorkOrder(FrappeTestCase): frappe.db.set_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order", 0) + def test_op_cost_and_scrap_based_on_sub_assemblies(self): + # Make Sub Assembly BOM 1 + + frappe.db.set_single_value("Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 1) + + items = { + "Test Final FG Item": 0, + "Test Final SF Item 1": 0, + "Test Final SF Item 2": 0, + "Test Final RM Item 1": 100, + "Test Final RM Item 2": 200, + "Test Final Scrap Item 1": 50, + "Test Final Scrap Item 2": 60, + } + + for item in items: + if not frappe.db.exists("Item", item): + item_properties = {"is_stock_item": 1, "valuation_rate": items[item]} + + (make_item(item_code=item, properties=item_properties),) + + prepare_boms_for_sub_assembly_test() + + wo_order = make_wo_order_test_record( + production_item="Test Final FG Item", + qty=10, + use_multi_level_bom=1, + skip_transfer=1, + from_wip_warehouse=1, + ) + + se_doc = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + se_doc.save() + + self.assertTrue(se_doc.additional_costs) + scrap_items = [] + for item in se_doc.items: + if item.is_scrap_item: + scrap_items.append(item.item_code) + + self.assertEqual(sorted(scrap_items), sorted(["Test Final Scrap Item 1", "Test Final Scrap Item 2"])) + for row in se_doc.additional_costs: + self.assertEqual(row.amount, 3000) + + frappe.db.set_single_value("Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0) + + def test_capcity_planning_for_workstation(self): + frappe.db.set_single_value( + "Manufacturing Settings", + { + "disable_capacity_planning": 0, + "capacity_planning_for_days": 1, + "mins_between_operations": 10, + }, + ) + + properties = {"is_stock_item": 1, "valuation_rate": 100} + fg_item = make_item("Test FG Item For Capacity Planning", properties).name + + rm_item = make_item("Test RM Item For Capacity Planning", properties).name + + workstation = "Test Workstation For Capacity Planning" + if not frappe.db.exists("Workstation", workstation): + make_workstation(workstation=workstation, production_capacity=1) + + operation = "Test Operation For Capacity Planning" + if not frappe.db.exists("Operation", operation): + make_operation(operation=operation, workstation=workstation) + + bom_doc = make_bom( + item=fg_item, + source_warehouse="Stores - _TC", + raw_materials=[rm_item], + with_operations=1, + do_not_submit=True, + ) + + bom_doc.append( + "operations", + {"operation": operation, "time_in_mins": 1420, "hour_rate": 100, "workstation": workstation}, + ) + bom_doc.submit() + + # 1st Work Order, + # Capacity to run parallel the operation 'Test Operation For Capacity Planning' is 2 + wo_doc = make_wo_order_test_record( + production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1 + ) + + wo_doc.submit() + job_cards = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name}, + ) + + self.assertEqual(len(job_cards), 1) + + # 2nd Work Order, + wo_doc = make_wo_order_test_record( + production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1 + ) + + wo_doc.submit() + job_cards = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name}, + ) + + self.assertEqual(len(job_cards), 1) + + # 3rd Work Order, capacity is full + wo_doc = make_wo_order_test_record( + production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1 + ) + + self.assertRaises(CapacityError, wo_doc.submit) + + frappe.db.set_single_value( + "Manufacturing Settings", {"disable_capacity_planning": 1, "mins_between_operations": 0} + ) + + +def make_operation(**kwargs): + kwargs = frappe._dict(kwargs) + + operation_doc = frappe.get_doc( + { + "doctype": "Operation", + "name": kwargs.operation, + "workstation": kwargs.workstation, + } + ) + operation_doc.insert() + + return operation_doc + + +def make_workstation(**kwargs): + kwargs = frappe._dict(kwargs) + + workstation_doc = frappe.get_doc( + { + "doctype": "Workstation", + "workstation_name": kwargs.workstation, + "workstation_type": kwargs.workstation_type, + "production_capacity": kwargs.production_capacity or 0, + "hour_rate": kwargs.hour_rate or 100, + } + ) + workstation_doc.insert() + + return workstation_doc + + +def prepare_boms_for_sub_assembly_test(): + if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}): + bom = make_bom( + item="Test Final SF Item 1", + source_warehouse="Stores - _TC", + raw_materials=["Test Final RM Item 1"], + operating_cost_per_bom_quantity=100, + do_not_submit=True, + ) + + bom.append("scrap_items", {"item_code": "Test Final Scrap Item 1", "qty": 1}) + + bom.submit() + + if not frappe.db.exists("BOM", {"item": "Test Final SF Item 2"}): + bom = make_bom( + item="Test Final SF Item 2", + source_warehouse="Stores - _TC", + raw_materials=["Test Final RM Item 2"], + operating_cost_per_bom_quantity=200, + do_not_submit=True, + ) + + bom.append("scrap_items", {"item_code": "Test Final Scrap Item 2", "qty": 1}) + + bom.submit() + + if not frappe.db.exists("BOM", {"item": "Test Final FG Item"}): + bom = make_bom( + item="Test Final FG Item", + source_warehouse="Stores - _TC", + raw_materials=["Test Final SF Item 1", "Test Final SF Item 2"], + ) + def prepare_data_for_workstation_type_check(): from erpnext.manufacturing.doctype.operation.test_operation import make_operation @@ -1955,7 +2132,7 @@ def make_wo_order_test_record(**args): wo_order.sales_order = args.sales_order or None wo_order.planned_start_date = args.planned_start_date or now() wo_order.transfer_material_against = args.transfer_material_against or "Work Order" - wo_order.from_wip_warehouse = args.from_wip_warehouse or None + wo_order.from_wip_warehouse = args.from_wip_warehouse or 0 if args.source_warehouse: for item in wo_order.get("required_items"): diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 88747b8f7a6..5e386035514 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -2,70 +2,70 @@ // For license information, please see license.txt frappe.ui.form.on("Work Order", { - setup: function(frm) { + setup: function (frm) { frm.custom_make_buttons = { - 'Stock Entry': 'Start', - 'Pick List': 'Create Pick List', - 'Job Card': 'Create Job Card' + "Stock Entry": "Start", + "Pick List": "Create Pick List", + "Job Card": "Create Job Card", }; // Set query for warehouses - frm.set_query("wip_warehouse", function() { + frm.set_query("wip_warehouse", function () { return { filters: { - 'company': frm.doc.company, - } + company: frm.doc.company, + }, }; }); - frm.set_query("source_warehouse", function() { + frm.set_query("source_warehouse", function () { return { filters: { - 'company': frm.doc.company, - } + company: frm.doc.company, + }, }; }); - frm.set_query("source_warehouse", "required_items", function() { + frm.set_query("source_warehouse", "required_items", function () { return { filters: { - 'company': frm.doc.company, - } + company: frm.doc.company, + }, }; }); - frm.set_query("sales_order", function() { + frm.set_query("sales_order", function () { return { filters: { - "status": ["not in", ["Closed", "On Hold"]] - } + status: ["not in", ["Closed", "On Hold"]], + }, }; }); - frm.set_query("fg_warehouse", function() { + frm.set_query("fg_warehouse", function () { return { filters: { - 'company': frm.doc.company, - 'is_group': 0 - } + company: frm.doc.company, + is_group: 0, + }, }; }); - frm.set_query("scrap_warehouse", function() { + frm.set_query("scrap_warehouse", function () { return { filters: { - 'company': frm.doc.company, - 'is_group': 0 - } + company: frm.doc.company, + is_group: 0, + }, }; }); // Set query for BOM - frm.set_query("bom_no", function() { + frm.set_query("bom_no", function () { if (frm.doc.production_item) { return { query: "erpnext.controllers.queries.bom", - filters: {item: cstr(frm.doc.production_item)} + filters: { item: cstr(frm.doc.production_item) }, }; } else { frappe.msgprint(__("Please enter Production Item first")); @@ -73,60 +73,62 @@ frappe.ui.form.on("Work Order", { }); // Set query for FG Item - frm.set_query("production_item", function() { + frm.set_query("production_item", function () { return { query: "erpnext.controllers.queries.item_query", filters: { - "is_stock_item": 1, - } + is_stock_item: 1, + }, }; }); // Set query for FG Item - frm.set_query("project", function() { - return{ - filters:[ - ['Project', 'status', 'not in', 'Completed, Cancelled'] - ] + frm.set_query("project", function () { + return { + filters: [["Project", "status", "not in", "Completed, Cancelled"]], }; }); - frm.set_query("operation", "required_items", function() { + frm.set_query("operation", "required_items", function () { return { query: "erpnext.manufacturing.doctype.work_order.work_order.get_bom_operations", filters: { - 'parent': frm.doc.bom_no, - 'parenttype': 'BOM' - } + parent: frm.doc.bom_no, + parenttype: "BOM", + }, }; }); // formatter for work order operation - frm.set_indicator_formatter('operation', - function(doc) { return (frm.doc.qty==doc.completed_qty) ? "green" : "orange"; }); + frm.set_indicator_formatter("operation", function (doc) { + return frm.doc.qty == doc.completed_qty ? "green" : "orange"; + }); }, - onload: function(frm) { - if (!frm.doc.status) - frm.doc.status = 'Draft'; + onload: function (frm) { + if (!frm.doc.status) frm.doc.status = "Draft"; frm.add_fetch("sales_order", "project", "project"); - if(frm.doc.__islocal) { + if (frm.doc.__islocal) { frm.set_value({ - "actual_start_date": "", - "actual_end_date": "" + actual_start_date: "", + actual_end_date: "", }); erpnext.work_order.set_default_warehouse(frm); } }, - source_warehouse: function(frm) { + source_warehouse: function (frm) { let transaction_controller = new erpnext.TransactionController(); - transaction_controller.autofill_warehouse(frm.doc.required_items, "source_warehouse", frm.doc.source_warehouse); + transaction_controller.autofill_warehouse( + frm.doc.required_items, + "source_warehouse", + frm.doc.source_warehouse + ); }, - refresh: function(frm) { + refresh: function (frm) { erpnext.toggle_naming_series(); erpnext.work_order.set_custom_buttons(frm); frm.set_intro(""); @@ -139,27 +141,30 @@ frappe.ui.form.on("Work Order", { } if (frm.doc.status != "Closed") { - if (frm.doc.docstatus === 1 && frm.doc.status !== "Completed" - && frm.doc.operations && frm.doc.operations.length) { - - const not_completed = frm.doc.operations.filter(d => { - if (d.status != 'Completed') { + if ( + frm.doc.docstatus === 1 && + frm.doc.status !== "Completed" && + frm.doc.operations && + frm.doc.operations.length + ) { + const not_completed = frm.doc.operations.filter((d) => { + if (d.status != "Completed") { return true; } }); if (not_completed && not_completed.length) { - frm.add_custom_button(__('Create Job Card'), () => { + frm.add_custom_button(__("Create Job Card"), () => { frm.trigger("make_job_card"); - }).addClass('btn-primary'); + }).addClass("btn-primary"); } } } - if(frm.doc.required_items && frm.doc.allow_alternative_item) { - const has_alternative = frm.doc.required_items.find(i => i.allow_alternative_item === 1); + if (frm.doc.required_items && frm.doc.allow_alternative_item) { + const has_alternative = frm.doc.required_items.find((i) => i.allow_alternative_item === 1); if (frm.doc.docstatus == 0 && has_alternative) { - frm.add_custom_button(__('Alternate Item'), () => { + frm.add_custom_button(__("Alternate Item"), () => { erpnext.utils.select_alternate_items({ frm: frm, child_docname: "required_items", @@ -167,16 +172,20 @@ frappe.ui.form.on("Work Order", { child_doctype: "Work Order Item", original_item_field: "original_item", condition: (d) => { - if (d.allow_alternative_item) {return true;} - } + if (d.allow_alternative_item) { + return true; + } + }, }); }); } } - if (frm.doc.status == "Completed" && - frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") { - frm.add_custom_button(__('Create BOM'), () => { + if ( + frm.doc.status == "Completed" && + frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture" + ) { + frm.add_custom_button(__("Create BOM"), () => { frm.trigger("make_bom"); }); } @@ -184,120 +193,128 @@ frappe.ui.form.on("Work Order", { frm.trigger("add_custom_button_to_return_components"); }, - add_custom_button_to_return_components: function(frm) { + add_custom_button_to_return_components: function (frm) { if (frm.doc.docstatus === 1 && in_list(["Closed", "Completed"], frm.doc.status)) { - let non_consumed_items = frm.doc.required_items.filter(d =>{ - return flt(d.consumed_qty) < flt(d.transferred_qty - d.returned_qty) + let non_consumed_items = frm.doc.required_items.filter((d) => { + return flt(d.consumed_qty) < flt(d.transferred_qty - d.returned_qty); }); if (non_consumed_items && non_consumed_items.length) { - frm.add_custom_button(__("Return Components"), function() { + frm.add_custom_button(__("Return Components"), function () { frm.trigger("create_stock_return_entry"); }).addClass("btn-primary"); } } }, - create_stock_return_entry: function(frm) { + create_stock_return_entry: function (frm) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.make_stock_return_entry", args: { - "work_order": frm.doc.name, + work_order: frm.doc.name, }, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { let doc = frappe.model.sync(r.message); frappe.set_route("Form", doc[0].doctype, doc[0].name); } - } + }, }); }, - make_job_card: function(frm) { + make_job_card: function (frm) { let qty = 0; let operations_data = []; - const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'), - fields: [ - { - fieldtype: 'Link', - fieldname: 'operation', - label: __('Operation'), - read_only: 1, - in_list_view: 1 + const dialog = frappe.prompt( + { + fieldname: "operations", + fieldtype: "Table", + label: __("Operations"), + fields: [ + { + fieldtype: "Link", + fieldname: "operation", + label: __("Operation"), + read_only: 1, + in_list_view: 1, + }, + { + fieldtype: "Link", + fieldname: "workstation", + label: __("Workstation"), + read_only: 1, + in_list_view: 1, + }, + { + fieldtype: "Data", + fieldname: "name", + label: __("Operation Id"), + }, + { + fieldtype: "Float", + fieldname: "pending_qty", + label: __("Pending Qty"), + }, + { + fieldtype: "Float", + fieldname: "qty", + label: __("Quantity to Manufacture"), + read_only: 0, + in_list_view: 1, + }, + { + fieldtype: "Float", + fieldname: "batch_size", + label: __("Batch Size"), + read_only: 1, + }, + { + fieldtype: "Int", + fieldname: "sequence_id", + label: __("Sequence Id"), + read_only: 1, + }, + ], + data: operations_data, + in_place_edit: true, + get_data: function () { + return operations_data; }, - { - fieldtype: 'Link', - fieldname: 'workstation', - label: __('Workstation'), - read_only: 1, - in_list_view: 1 - }, - { - fieldtype: 'Data', - fieldname: 'name', - label: __('Operation Id') - }, - { - fieldtype: 'Float', - fieldname: 'pending_qty', - label: __('Pending Qty'), - }, - { - fieldtype: 'Float', - fieldname: 'qty', - label: __('Quantity to Manufacture'), - read_only: 0, - in_list_view: 1, - }, - { - fieldtype: 'Float', - fieldname: 'batch_size', - label: __('Batch Size'), - read_only: 1 - }, - { - fieldtype: 'Int', - fieldname: 'sequence_id', - label: __('Sequence Id'), - read_only: 1 - }, - ], - data: operations_data, - in_place_edit: true, - get_data: function() { - return operations_data; - } - }, function(data) { - frappe.call({ - method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", - freeze: true, - args: { - work_order: frm.doc.name, - operations: data.operations, - }, - callback: function() { - frm.reload_doc(); - } - }); - }, __("Job Card"), __("Create")); + }, + function (data) { + frappe.call({ + method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", + freeze: true, + args: { + work_order: frm.doc.name, + operations: data.operations, + }, + callback: function () { + frm.reload_doc(); + }, + }); + }, + __("Job Card"), + __("Create") + ); - dialog.fields_dict["operations"].grid.wrapper.find('.grid-add-row').hide(); + dialog.fields_dict["operations"].grid.wrapper.find(".grid-add-row").hide(); var pending_qty = 0; - frm.doc.operations.forEach(data => { - if(data.completed_qty + data.process_loss_qty != frm.doc.qty) { + frm.doc.operations.forEach((data) => { + if (data.completed_qty + data.process_loss_qty != frm.doc.qty) { pending_qty = frm.doc.qty - flt(data.completed_qty) - flt(data.process_loss_qty); if (pending_qty) { dialog.fields_dict.operations.df.data.push({ - 'name': data.name, - 'operation': data.operation, - 'workstation': data.workstation, - 'batch_size': data.batch_size, - 'qty': pending_qty, - 'pending_qty': pending_qty, - 'sequence_id': data.sequence_id + name: data.name, + operation: data.operation, + workstation: data.workstation, + batch_size: data.batch_size, + qty: pending_qty, + pending_qty: pending_qty, + sequence_id: data.sequence_id, }); } } @@ -305,202 +322,212 @@ frappe.ui.form.on("Work Order", { dialog.fields_dict.operations.grid.refresh(); }, - make_bom: function(frm) { + make_bom: function (frm) { frappe.call({ method: "make_bom", doc: frm.doc, - callback: function(r){ + callback: function (r) { if (r.message) { var doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); } - } + }, }); }, - show_progress_for_items: function(frm) { + show_progress_for_items: function (frm) { var bars = []; - var message = ''; + var message = ""; var added_min = false; // produced qty - var title = __('{0} items produced', [frm.doc.produced_qty]); + var title = __("{0} items produced", [frm.doc.produced_qty]); bars.push({ - 'title': title, - 'width': (frm.doc.produced_qty / frm.doc.qty * 100) + '%', - 'progress_class': 'progress-bar-success' + title: title, + width: (frm.doc.produced_qty / frm.doc.qty) * 100 + "%", + progress_class: "progress-bar-success", }); - if (bars[0].width == '0%') { - bars[0].width = '0.5%'; + if (bars[0].width == "0%") { + bars[0].width = "0.5%"; added_min = 0.5; } message = title; // pending qty - if(!frm.doc.skip_transfer){ + if (!frm.doc.skip_transfer) { var pending_complete = frm.doc.material_transferred_for_manufacturing - frm.doc.produced_qty; - if(pending_complete) { - var width = ((pending_complete / frm.doc.qty * 100) - added_min); - title = __('{0} items in progress', [pending_complete]); + if (pending_complete) { + var width = (pending_complete / frm.doc.qty) * 100 - added_min; + title = __("{0} items in progress", [pending_complete]); bars.push({ - 'title': title, - 'width': (width > 100 ? "99.5" : width) + '%', - 'progress_class': 'progress-bar-warning' + title: title, + width: (width > 100 ? "99.5" : width) + "%", + progress_class: "progress-bar-warning", }); - message = message + '. ' + title; + message = message + ". " + title; } } - frm.dashboard.add_progress(__('Status'), bars, message); + frm.dashboard.add_progress(__("Status"), bars, message); }, - show_progress_for_operations: function(frm) { + show_progress_for_operations: function (frm) { if (frm.doc.operations && frm.doc.operations.length) { - let progress_class = { "Work in Progress": "progress-bar-warning", - "Completed": "progress-bar-success" + Completed: "progress-bar-success", }; let bars = []; - let message = ''; - let title = ''; + let message = ""; + let title = ""; let status_wise_oprtation_data = {}; let total_completed_qty = frm.doc.qty * frm.doc.operations.length; - frm.doc.operations.forEach(d => { + frm.doc.operations.forEach((d) => { if (!status_wise_oprtation_data[d.status]) { status_wise_oprtation_data[d.status] = [d.completed_qty, d.operation]; } else { status_wise_oprtation_data[d.status][0] += d.completed_qty; - status_wise_oprtation_data[d.status][1] += ', ' + d.operation; + status_wise_oprtation_data[d.status][1] += ", " + d.operation; } }); for (let key in status_wise_oprtation_data) { title = __("{0} Operations: {1}", [key, status_wise_oprtation_data[key][1].bold()]); bars.push({ - 'title': title, - 'width': status_wise_oprtation_data[key][0] / total_completed_qty * 100 + '%', - 'progress_class': progress_class[key] + title: title, + width: (status_wise_oprtation_data[key][0] / total_completed_qty) * 100 + "%", + progress_class: progress_class[key], }); - message += title + '. '; + message += title + ". "; } - frm.dashboard.add_progress(__('Status'), bars, message); + frm.dashboard.add_progress(__("Status"), bars, message); } }, - production_item: function(frm) { + production_item: function (frm) { if (frm.doc.production_item) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.get_item_details", args: { item: frm.doc.production_item, - project: frm.doc.project + project: frm.doc.project, }, freeze: true, - callback: function(r) { - if(r.message) { - frm.set_value('sales_order', ""); - frm.trigger('set_sales_order'); + callback: function (r) { + if (r.message) { + frm.set_value("sales_order", ""); + frm.trigger("set_sales_order"); erpnext.in_production_item_onchange = true; - $.each(["description", "stock_uom", "project", "bom_no", "allow_alternative_item", - "transfer_material_against", "item_name"], function(i, field) { - frm.set_value(field, r.message[field]); - }); + $.each( + [ + "description", + "stock_uom", + "project", + "bom_no", + "allow_alternative_item", + "transfer_material_against", + "item_name", + ], + function (i, field) { + frm.set_value(field, r.message[field]); + } + ); - if(r.message["set_scrap_wh_mandatory"]){ + if (r.message["set_scrap_wh_mandatory"]) { frm.toggle_reqd("scrap_warehouse", true); } erpnext.in_production_item_onchange = false; } - } + }, }); } }, - project: function(frm) { - if(!erpnext.in_production_item_onchange && !frm.doc.bom_no) { + project: function (frm) { + if (!erpnext.in_production_item_onchange && !frm.doc.bom_no) { frm.trigger("production_item"); } }, - bom_no: function(frm) { + bom_no: function (frm) { return frm.call({ doc: frm.doc, method: "get_items_and_operations_from_bom", freeze: true, - callback: function(r) { - if(r.message["set_scrap_wh_mandatory"]){ + callback: function (r) { + if (r.message["set_scrap_wh_mandatory"]) { frm.toggle_reqd("scrap_warehouse", true); } - } + }, }); }, - use_multi_level_bom: function(frm) { - if(frm.doc.bom_no) { + use_multi_level_bom: function (frm) { + if (frm.doc.bom_no) { frm.trigger("bom_no"); } }, - qty: function(frm) { - frm.trigger('bom_no'); + qty: function (frm) { + frm.trigger("bom_no"); }, - before_submit: function(frm) { + before_submit: function (frm) { frm.fields_dict.required_items.grid.toggle_reqd("source_warehouse", true); - frm.toggle_reqd("transfer_material_against", - frm.doc.operations && frm.doc.operations.length > 0); + frm.toggle_reqd("transfer_material_against", frm.doc.operations && frm.doc.operations.length > 0); }, - set_sales_order: function(frm) { - if(frm.doc.production_item) { + set_sales_order: function (frm) { + if (frm.doc.production_item) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.query_sales_order", args: { production_item: frm.doc.production_item }, - callback: function(r) { - frm.set_query("sales_order", function() { + callback: function (r) { + frm.set_query("sales_order", function () { erpnext.in_production_item_onchange = true; return { - filters: [ - ["Sales Order","name", "in", r.message] - ] + filters: [["Sales Order", "name", "in", r.message]], }; }); - } + }, }); } }, - additional_operating_cost: function(frm) { + additional_operating_cost: function (frm) { erpnext.work_order.calculate_cost(frm.doc); erpnext.work_order.calculate_total_cost(frm); }, }); frappe.ui.form.on("Work Order Item", { - source_warehouse: function(frm, cdt, cdn) { + source_warehouse: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; - if(!row.item_code) { + if (!row.item_code) { frappe.throw(__("Please set the Item Code first")); - } else if(row.source_warehouse) { + } else if (row.source_warehouse) { frappe.call({ - "method": "erpnext.stock.utils.get_latest_stock_qty", + method: "erpnext.stock.utils.get_latest_stock_qty", args: { item_code: row.item_code, - warehouse: row.source_warehouse + warehouse: row.source_warehouse, }, callback: function (r) { - frappe.model.set_value(row.doctype, row.name, - "available_qty_at_source_warehouse", r.message); - } + frappe.model.set_value( + row.doctype, + row.name, + "available_qty_at_source_warehouse", + r.message + ); + }, }); } }, - item_code: function(frm, cdt, cdn) { + item_code: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; if (row.item_code) { @@ -508,94 +535,104 @@ frappe.ui.form.on("Work Order Item", { method: "erpnext.stock.doctype.item.item.get_item_details", args: { item_code: row.item_code, - company: frm.doc.company + company: frm.doc.company, }, - callback: function(r) { + callback: function (r) { if (r.message) { frappe.model.set_value(cdt, cdn, { - "required_qty": row.required_qty || 1, - "item_name": r.message.item_name, - "description": r.message.description, - "source_warehouse": r.message.default_warehouse, - "allow_alternative_item": r.message.allow_alternative_item, - "include_item_in_manufacturing": r.message.include_item_in_manufacturing + required_qty: row.required_qty || 1, + item_name: r.message.item_name, + description: r.message.description, + source_warehouse: r.message.default_warehouse, + allow_alternative_item: r.message.allow_alternative_item, + include_item_in_manufacturing: r.message.include_item_in_manufacturing, }); } - } + }, }); } - } + }, }); frappe.ui.form.on("Work Order Operation", { - workstation: function(frm, cdt, cdn) { + workstation: function (frm, cdt, cdn) { var d = locals[cdt][cdn]; if (d.workstation) { frappe.call({ - "method": "frappe.client.get", + method: "frappe.client.get", args: { doctype: "Workstation", - name: d.workstation + name: d.workstation, }, callback: function (data) { frappe.model.set_value(d.doctype, d.name, "hour_rate", data.message.hour_rate); erpnext.work_order.calculate_cost(frm.doc); erpnext.work_order.calculate_total_cost(frm); - } + }, }); } }, - time_in_mins: function(frm, cdt, cdn) { + time_in_mins: function (frm, cdt, cdn) { erpnext.work_order.calculate_cost(frm.doc); erpnext.work_order.calculate_total_cost(frm); }, }); erpnext.work_order = { - set_custom_buttons: function(frm) { + set_custom_buttons: function (frm) { var doc = frm.doc; if (doc.status !== "Closed") { - frm.add_custom_button(__('Close'), function() { - frappe.confirm(__("Once the Work Order is Closed. It can't be resumed."), - () => { + frm.add_custom_button( + __("Close"), + function () { + frappe.confirm(__("Once the Work Order is Closed. It can't be resumed."), () => { erpnext.work_order.change_work_order_status(frm, "Closed"); - } - ); - }, __("Status")); + }); + }, + __("Status") + ); } if (doc.docstatus === 1 && !in_list(["Closed", "Completed"], doc.status)) { - if (doc.status != 'Stopped' && doc.status != 'Completed') { - frm.add_custom_button(__('Stop'), function() { - erpnext.work_order.change_work_order_status(frm, "Stopped"); - }, __("Status")); - } else if (doc.status == 'Stopped') { - frm.add_custom_button(__('Re-open'), function() { - erpnext.work_order.change_work_order_status(frm, "Resumed"); - }, __("Status")); + if (doc.status != "Stopped" && doc.status != "Completed") { + frm.add_custom_button( + __("Stop"), + function () { + erpnext.work_order.change_work_order_status(frm, "Stopped"); + }, + __("Status") + ); + } else if (doc.status == "Stopped") { + frm.add_custom_button( + __("Re-open"), + function () { + erpnext.work_order.change_work_order_status(frm, "Resumed"); + }, + __("Status") + ); } - const show_start_btn = (frm.doc.skip_transfer - || frm.doc.transfer_material_against == 'Job Card') ? 0 : 1; + const show_start_btn = + frm.doc.skip_transfer || frm.doc.transfer_material_against == "Job Card" ? 0 : 1; if (show_start_btn) { let pending_to_transfer = frm.doc.required_items.some( - item => flt(item.transferred_qty) < flt(item.required_qty) + (item) => flt(item.transferred_qty) < flt(item.required_qty) ); - if (pending_to_transfer && frm.doc.status != 'Stopped') { + if (pending_to_transfer && frm.doc.status != "Stopped") { frm.has_start_btn = true; - frm.add_custom_button(__('Create Pick List'), function() { + frm.add_custom_button(__("Create Pick List"), function () { erpnext.work_order.create_pick_list(frm); }); - var start_btn = frm.add_custom_button(__('Start'), function() { - erpnext.work_order.make_se(frm, 'Material Transfer for Manufacture'); + var start_btn = frm.add_custom_button(__("Start"), function () { + erpnext.work_order.make_se(frm, "Material Transfer for Manufacture"); }); - start_btn.addClass('btn-primary'); + start_btn.addClass("btn-primary"); } } - if (frm.doc.status != 'Stopped') { + if (frm.doc.status != "Stopped") { // If "Material Consumption is check in Manufacturing Settings, allow Material Consumption if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) { if (flt(doc.material_transferred_for_manufacturing) > 0 || frm.doc.skip_transfer) { @@ -604,89 +641,102 @@ erpnext.work_order = { var tbl = frm.doc.required_items || []; var tbl_lenght = tbl.length; for (var i = 0, len = tbl_lenght; i < len; i++) { - let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty; + let wo_item_qty = + frm.doc.required_items[i].transferred_qty || + frm.doc.required_items[i].required_qty; if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) { counter += 1; } } if (counter > 0) { - var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() { - const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on; - erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on); - }); - consumption_btn.addClass('btn-primary'); + var consumption_btn = frm.add_custom_button( + __("Material Consumption"), + function () { + const backflush_raw_materials_based_on = + frm.doc.__onload.backflush_raw_materials_based_on; + erpnext.work_order.make_consumption_se( + frm, + backflush_raw_materials_based_on + ); + } + ); + consumption_btn.addClass("btn-primary"); } } } - if(!frm.doc.skip_transfer){ + if (!frm.doc.skip_transfer) { if (flt(doc.material_transferred_for_manufacturing) > 0) { - if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) { + if (flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing)) { frm.has_finish_btn = true; - var finish_btn = frm.add_custom_button(__('Finish'), function() { - erpnext.work_order.make_se(frm, 'Manufacture'); + var finish_btn = frm.add_custom_button(__("Finish"), function () { + erpnext.work_order.make_se(frm, "Manufacture"); }); - if(doc.material_transferred_for_manufacturing>=doc.qty) { + if (doc.material_transferred_for_manufacturing >= doc.qty) { // all materials transferred for manufacturing, make this primary - finish_btn.addClass('btn-primary'); + finish_btn.addClass("btn-primary"); } } else if (frm.doc.__onload && frm.doc.__onload.overproduction_percentage) { let allowance_percentage = frm.doc.__onload.overproduction_percentage; if (allowance_percentage > 0) { - let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty); + let allowed_qty = frm.doc.qty + (allowance_percentage / 100) * frm.doc.qty; - if ((flt(doc.produced_qty) < allowed_qty)) { - frm.add_custom_button(__('Finish'), function() { - erpnext.work_order.make_se(frm, 'Manufacture'); + if (flt(doc.produced_qty) < allowed_qty) { + frm.add_custom_button(__("Finish"), function () { + erpnext.work_order.make_se(frm, "Manufacture"); }); } } } } } else { - if ((flt(doc.produced_qty) < flt(doc.qty))) { - var finish_btn = frm.add_custom_button(__('Finish'), function() { - erpnext.work_order.make_se(frm, 'Manufacture'); + if (flt(doc.produced_qty) < flt(doc.qty)) { + var finish_btn = frm.add_custom_button(__("Finish"), function () { + erpnext.work_order.make_se(frm, "Manufacture"); }); - finish_btn.addClass('btn-primary'); + finish_btn.addClass("btn-primary"); } } } } }, - calculate_cost: function(doc) { - if (doc.operations){ + calculate_cost: function (doc) { + if (doc.operations) { var op = doc.operations; doc.planned_operating_cost = 0.0; - for(var i=0;i { - frappe.prompt({ - fieldtype: 'Float', - label: __('Qty for {0}', [__(purpose)]), - fieldname: 'qty', - description: __('Max: {0}', [max]), - default: max - }, data => { - max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; + frappe.prompt( + { + fieldtype: "Float", + label: __("Qty for {0}", [__(purpose)]), + fieldname: "qty", + description: __("Max: {0}", [max]), + default: max, + }, + (data) => { + max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; - if (data.qty > max) { - frappe.msgprint(__('Quantity must not be more than {0}', [max])); - reject(); - } - data.purpose = purpose; - resolve(data); - }, __('Select Quantity'), __('Create')); + if (data.qty > max) { + frappe.msgprint(__("Quantity must not be more than {0}", [max])); + reject(); + } + data.purpose = purpose; + resolve(data); + }, + __("Select Quantity"), + __("Create") + ); }); }, - make_se: function(frm, purpose) { + make_se: function (frm, purpose) { this.show_prompt_for_qty_input(frm, purpose) - .then(data => { - return frappe.xcall('erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry', { - 'work_order_id': frm.doc.name, - 'purpose': purpose, - 'qty': data.qty + .then((data) => { + return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", { + work_order_id: frm.doc.name, + purpose: purpose, + qty: data.qty, }); - }).then(stock_entry => { + }) + .then((stock_entry) => { frappe.model.sync(stock_entry); - frappe.set_route('Form', stock_entry.doctype, stock_entry.name); + frappe.set_route("Form", stock_entry.doctype, stock_entry.name); }); - }, - create_pick_list: function(frm, purpose='Material Transfer for Manufacture') { + create_pick_list: function (frm, purpose = "Material Transfer for Manufacture") { this.show_prompt_for_qty_input(frm, purpose) - .then(data => { - return frappe.xcall('erpnext.manufacturing.doctype.work_order.work_order.create_pick_list', { - 'source_name': frm.doc.name, - 'for_qty': data.qty + .then((data) => { + return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.create_pick_list", { + source_name: frm.doc.name, + for_qty: data.qty, }); - }).then(pick_list => { + }) + .then((pick_list) => { frappe.model.sync(pick_list); - frappe.set_route('Form', pick_list.doctype, pick_list.name); + frappe.set_route("Form", pick_list.doctype, pick_list.name); }); }, - make_consumption_se: function(frm, backflush_raw_materials_based_on) { - if(!frm.doc.skip_transfer){ - var max = (backflush_raw_materials_based_on === "Material Transferred for Manufacture") ? - flt(frm.doc.material_transferred_for_manufacturing) - flt(frm.doc.produced_qty) : - flt(frm.doc.qty) - flt(frm.doc.produced_qty); - // flt(frm.doc.qty) - flt(frm.doc.material_transferred_for_manufacturing); + make_consumption_se: function (frm, backflush_raw_materials_based_on) { + if (!frm.doc.skip_transfer) { + var max = + backflush_raw_materials_based_on === "Material Transferred for Manufacture" + ? flt(frm.doc.material_transferred_for_manufacturing) - flt(frm.doc.produced_qty) + : flt(frm.doc.qty) - flt(frm.doc.produced_qty); + // flt(frm.doc.qty) - flt(frm.doc.material_transferred_for_manufacturing); } else { var max = flt(frm.doc.qty) - flt(frm.doc.produced_qty); } frappe.call({ - method:"erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", + method: "erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", args: { - "work_order_id": frm.doc.name, - "purpose": "Material Consumption for Manufacture", - "qty": max + work_order_id: frm.doc.name, + purpose: "Material Consumption for Manufacture", + qty: max, }, - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } + }, }); }, - change_work_order_status: function(frm, status) { - let method_name = status=="Closed" ? "close_work_order" : "stop_unstop"; + change_work_order_status: function (frm, status) { + let method_name = status == "Closed" ? "close_work_order" : "stop_unstop"; frappe.call({ method: `erpnext.manufacturing.doctype.work_order.work_order.${method_name}`, freeze: true, freeze_message: __("Updating Work Order status"), args: { work_order: frm.doc.name, - status: status + status: status, }, - callback: function(r) { - if(r.message) { + callback: function (r) { + if (r.message) { frm.set_value("status", r.message); frm.reload_doc(); } - } + }, }); - } + }, }; -frappe.tour['Work Order'] = [ +frappe.tour["Work Order"] = [ { fieldname: "production_item", title: "Item to Manufacture", - description: __("Select the Item to be manufactured.") + description: __("Select the Item to be manufactured."), }, { fieldname: "bom_no", title: "BOM No", - description: __("The default BOM for that item will be fetched by the system. You can also change the BOM.") + description: __( + "The default BOM for that item will be fetched by the system. You can also change the BOM." + ), }, { fieldname: "qty", title: "Qty to Manufacture", - description: __("Enter the quantity to manufacture. Raw material Items will be fetched only when this is set.") + description: __( + "Enter the quantity to manufacture. Raw material Items will be fetched only when this is set." + ), }, { fieldname: "use_multi_level_bom", title: "Use Multi-Level BOM", - description: __("This is enabled by default. If you want to plan materials for sub-assemblies of the Item you're manufacturing leave this enabled. If you plan and manufacture the sub-assemblies separately, you can disable this checkbox.") + description: __( + "This is enabled by default. If you want to plan materials for sub-assemblies of the Item you're manufacturing leave this enabled. If you plan and manufacture the sub-assemblies separately, you can disable this checkbox." + ), }, { fieldname: "source_warehouse", title: "Source Warehouse", - description: __("The warehouse where you store your raw materials. Each required item can have a separate source warehouse. Group warehouse also can be selected as source warehouse. On submission of the Work Order, the raw materials will be reserved in these warehouses for production usage.") + description: __( + "The warehouse where you store your raw materials. Each required item can have a separate source warehouse. Group warehouse also can be selected as source warehouse. On submission of the Work Order, the raw materials will be reserved in these warehouses for production usage." + ), }, { fieldname: "fg_warehouse", title: "Target Warehouse", - description: __("The warehouse where you store finished Items before they are shipped.") + description: __("The warehouse where you store finished Items before they are shipped."), }, { fieldname: "wip_warehouse", title: "Work-in-Progress Warehouse", - description: __("The warehouse where your Items will be transferred when you begin production. Group Warehouse can also be selected as a Work in Progress warehouse.") + description: __( + "The warehouse where your Items will be transferred when you begin production. Group Warehouse can also be selected as a Work in Progress warehouse." + ), }, { fieldname: "scrap_warehouse", title: "Scrap Warehouse", - description: __("If the BOM results in Scrap material, the Scrap Warehouse needs to be selected.") + description: __("If the BOM results in Scrap material, the Scrap Warehouse needs to be selected."), }, { fieldname: "required_items", title: "Required Items", - description: __("All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table.") + description: __( + "All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table." + ), }, { fieldname: "planned_start_date", title: "Planned Start Date", - description: __("Set the Planned Start Date (an Estimated Date at which you want the Production to begin)") + description: __( + "Set the Planned Start Date (an Estimated Date at which you want the Production to begin)" + ), }, { fieldname: "operations", title: "Operations", - description: __("If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed.") + description: __( + "If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed." + ), }, - - ]; diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index fb44dfdffbb..1c9a1492798 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -448,7 +448,8 @@ "no_copy": 1, "options": "Production Plan", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "production_plan_item", @@ -600,7 +601,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-08-11 18:35:49.852069", + "modified": "2024-02-11 15:47:13.454422", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index f95b4f66a33..6bb03310389 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -154,9 +154,7 @@ class WorkOrder(Document): def set_default_warehouse(self): if not self.wip_warehouse and not self.skip_transfer: - self.wip_warehouse = frappe.db.get_single_value( - "Manufacturing Settings", "default_wip_warehouse" - ) + self.wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") if not self.fg_warehouse: self.fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_fg_warehouse") @@ -172,8 +170,12 @@ class WorkOrder(Document): def calculate_operating_cost(self): self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0 for d in self.get("operations"): - d.planned_operating_cost = flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0) - d.actual_operating_cost = flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0) + d.planned_operating_cost = flt( + flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0), d.precision("planned_operating_cost") + ) + d.actual_operating_cost = flt( + flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0), d.precision("actual_operating_cost") + ) self.planned_operating_cost += flt(d.planned_operating_cost) self.actual_operating_cost += flt(d.actual_operating_cost) @@ -212,9 +214,7 @@ class WorkOrder(Document): so_qty = flt(so_item_qty) + flt(dnpi_qty) allowance_percentage = flt( - frappe.db.get_single_value( - "Manufacturing Settings", "overproduction_percentage_for_sales_order" - ) + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_sales_order") ) if total_qty > so_qty + (allowance_percentage / 100 * so_qty): @@ -256,6 +256,13 @@ class WorkOrder(Document): else: status = "Cancelled" + if ( + self.skip_transfer + and self.produced_qty + and self.qty > (flt(self.produced_qty) + flt(self.process_loss_qty)) + ): + status = "In Process" + return status def update_work_order_qty(self): @@ -343,9 +350,7 @@ class WorkOrder(Document): produced_qty = total_qty[0][0] if total_qty else 0 self.update_status() - production_plan.run_method( - "update_produced_pending_qty", produced_qty, self.production_plan_item - ) + production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item) def before_submit(self): self.create_serial_no_batch_no() @@ -489,7 +494,6 @@ class WorkOrder(Document): def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning): self.set_operation_start_end_time(index, row) - original_start_time = row.planned_start_time job_card_doc = create_job_card( self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning ) @@ -498,11 +502,15 @@ class WorkOrder(Document): row.planned_start_time = job_card_doc.time_logs[-1].from_time row.planned_end_time = job_card_doc.time_logs[-1].to_time - if date_diff(row.planned_start_time, original_start_time) > plan_days: + if date_diff(row.planned_end_time, self.planned_start_date) > plan_days: frappe.message_log.pop() frappe.throw( - _("Unable to find the time slot in the next {0} days for the operation {1}.").format( - plan_days, row.operation + _( + "Unable to find the time slot in the next {0} days for the operation {1}. Please increase the 'Capacity Planning For (Days)' in the {2}." + ).format( + plan_days, + row.operation, + get_link_to_form("Manufacturing Settings", "Manufacturing Settings"), ), CapacityError, ) @@ -520,9 +528,7 @@ class WorkOrder(Document): get_datetime(self.operations[idx - 1].planned_end_time) + get_mins_between_operations() ) - row.planned_end_time = get_datetime(row.planned_start_time) + relativedelta( - minutes=row.time_in_mins - ) + row.planned_end_time = get_datetime(row.planned_start_time) + relativedelta(minutes=row.time_in_mins) if row.planned_start_time == row.planned_end_time: frappe.throw(_("Capacity Planning Error, planned start time can not be same as end time")) @@ -589,11 +595,7 @@ class WorkOrder(Document): ) def update_ordered_qty(self): - if ( - self.production_plan - and self.production_plan_item - and not self.production_plan_sub_assembly_item - ): + if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item: table = frappe.qb.DocType("Work Order") query = ( @@ -633,11 +635,9 @@ class WorkOrder(Document): cond = "product_bundle_item = %s" if self.product_bundle_item else "production_item = %s" qty = frappe.db.sql( - """ select sum(qty) from - `tabWork Order` where sales_order = %s and docstatus = 1 and {0} - """.format( - cond - ), + f""" select sum(qty) from + `tabWork Order` where sales_order = %s and docstatus = 1 and {cond} + """, (self.sales_order, (self.product_bundle_item or self.production_item)), as_list=1, ) @@ -787,9 +787,7 @@ class WorkOrder(Document): def set_actual_dates(self): if self.get("operations"): - actual_start_dates = [ - d.actual_start_time for d in self.get("operations") if d.actual_start_time - ] + actual_start_dates = [d.actual_start_time for d in self.get("operations") if d.actual_start_time] if actual_start_dates: self.actual_start_date = min(actual_start_dates) @@ -834,11 +832,7 @@ class WorkOrder(Document): if not self.qty > 0: frappe.throw(_("Quantity to Manufacture must be greater than 0.")) - if ( - self.production_plan - and self.production_plan_item - and not self.production_plan_sub_assembly_item - ): + if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item: qty_dict = frappe.db.get_value( "Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1 ) @@ -974,7 +968,7 @@ class WorkOrder(Document): query = ( frappe.qb.from_(ste) .inner_join(ste_child) - .on((ste_child.parent == ste.name)) + .on(ste_child.parent == ste.name) .select( ste_child.item_code, ste_child.original_item, @@ -1004,7 +998,7 @@ class WorkOrder(Document): query = ( frappe.qb.from_(ste) .inner_join(ste_child) - .on((ste_child.parent == ste.name)) + .on(ste_child.parent == ste.name) .select( ste_child.item_code, ste_child.original_item, @@ -1272,9 +1266,7 @@ def make_stock_entry(work_order_id, purpose, qty=None): ) if work_order.bom_no: - stock_entry.inspection_required = frappe.db.get_value( - "BOM", work_order.bom_no, "inspection_required" - ) + stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required") if purpose == "Material Transfer for Manufacture": stock_entry.to_warehouse = wip_warehouse @@ -1380,9 +1372,7 @@ def close_work_order(work_order, status): def split_qty_based_on_batch_size(wo_doc, row, qty): - if not cint( - frappe.db.get_value("Operation", row.operation, "create_job_card_based_on_batch_size") - ): + if not cint(frappe.db.get_value("Operation", row.operation, "create_job_card_based_on_batch_size")): row.batch_size = row.get("qty") or wo_doc.qty row.job_card_qty = row.batch_size @@ -1462,9 +1452,7 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create doc.schedule_time_logs(row) doc.insert() - frappe.msgprint( - _("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True - ) + frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True) if enable_capacity_planning: # automatically added scheduling rows shouldn't change status to WIP @@ -1527,7 +1515,7 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): def get_reserved_qty_for_production( item_code: str, warehouse: str, - non_completed_production_plans: list = None, + non_completed_production_plans: list | None = None, check_production_plan: bool = False, ) -> float: """Get total reserved quantity for any item in specified warehouse""" diff --git a/erpnext/manufacturing/doctype/work_order/work_order_calendar.js b/erpnext/manufacturing/doctype/work_order/work_order_calendar.js index 7ce05e9b881..90ce74ce232 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_calendar.js +++ b/erpnext/manufacturing/doctype/work_order/work_order_calendar.js @@ -2,23 +2,23 @@ // For license information, please see license.txt frappe.views.calendar["Work Order"] = { - fields: ["planned_start_date", "planned_end_date", "status", "produced_qty", "qty", "name", "name"], + fields: ["planned_start_date", "planned_end_date", "status", "produced_qty", "qty", "name", "name"], field_map: { - "start": "planned_start_date", - "end": "planned_end_date", - "id": "name", - "title": "name", - "status": "status", - "allDay": "allDay", - "progress": function(data) { - return flt(data.produced_qty) / data.qty * 100; - } + start: "planned_start_date", + end: "planned_end_date", + id: "name", + title: "name", + status: "status", + allDay: "allDay", + progress: function (data) { + return (flt(data.produced_qty) / data.qty) * 100; + }, }, gantt: true, - get_css_class: function(data) { - if(data.status==="Completed") { + get_css_class: function (data) { + if (data.status === "Completed") { return "success"; - } else if(data.status==="In Process") { + } else if (data.status === "In Process") { return "warning"; } else { return "danger"; @@ -26,23 +26,23 @@ frappe.views.calendar["Work Order"] = { }, filters: [ { - "fieldtype": "Link", - "fieldname": "sales_order", - "options": "Sales Order", - "label": __("Sales Order") + fieldtype: "Link", + fieldname: "sales_order", + options: "Sales Order", + label: __("Sales Order"), }, { - "fieldtype": "Link", - "fieldname": "production_item", - "options": "Item", - "label": __("Production Item") + fieldtype: "Link", + fieldname: "production_item", + options: "Item", + label: __("Production Item"), }, { - "fieldtype": "Link", - "fieldname": "wip_warehouse", - "options": "Warehouse", - "label": __("WIP Warehouse") - } + fieldtype: "Link", + fieldname: "wip_warehouse", + options: "Warehouse", + label: __("WIP Warehouse"), + }, ], - get_events_method: "frappe.desk.calendar.get_events" -} + get_events_method: "frappe.desk.calendar.get_events", +}; diff --git a/erpnext/manufacturing/doctype/work_order/work_order_list.js b/erpnext/manufacturing/doctype/work_order/work_order_list.js index 81c23bb7104..1e1e5661fe5 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_list.js +++ b/erpnext/manufacturing/doctype/work_order/work_order_list.js @@ -1,19 +1,31 @@ -frappe.listview_settings['Work Order'] = { - add_fields: ["bom_no", "status", "sales_order", "qty", - "produced_qty", "expected_delivery_date", "planned_start_date", "planned_end_date"], +frappe.listview_settings["Work Order"] = { + add_fields: [ + "bom_no", + "status", + "sales_order", + "qty", + "produced_qty", + "expected_delivery_date", + "planned_start_date", + "planned_end_date", + ], filters: [["status", "!=", "Stopped"]], - get_indicator: function(doc) { - if(doc.status==="Submitted") { + get_indicator: function (doc) { + if (doc.status === "Submitted") { return [__("Not Started"), "orange", "status,=,Submitted"]; } else { - return [__(doc.status), { - "Draft": "red", - "Stopped": "red", - "Not Started": "red", - "In Process": "orange", - "Completed": "green", - "Cancelled": "gray" - }[doc.status], "status,=," + doc.status]; + return [ + __(doc.status), + { + Draft: "red", + Stopped: "red", + "Not Started": "red", + "In Process": "orange", + Completed: "green", + Cancelled: "gray", + }[doc.status], + "status,=," + doc.status, + ]; } - } + }, }; diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index f354d45381c..0f4d693544e 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -36,7 +36,8 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item Code", - "options": "Item" + "options": "Item", + "search_index": 1 }, { "fieldname": "source_warehouse", @@ -141,7 +142,7 @@ ], "istable": 1, "links": [], - "modified": "2022-09-28 10:50:43.512562", + "modified": "2024-02-11 15:45:32.318374", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", diff --git a/erpnext/manufacturing/doctype/workstation/_test_workstation.js b/erpnext/manufacturing/doctype/workstation/_test_workstation.js index 0f09bd1c61f..44050d0a85a 100644 --- a/erpnext/manufacturing/doctype/workstation/_test_workstation.js +++ b/erpnext/manufacturing/doctype/workstation/_test_workstation.js @@ -8,16 +8,16 @@ QUnit.test("test: Workstation", function (assert) { // number of asserts assert.expect(1); - frappe.run_serially('Workstation', [ + frappe.run_serially("Workstation", [ // insert a new Workstation - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), + () => + frappe.tests.make([ + // values to be set + { key: "value" }, + ]), () => { - assert.equal(cur_frm.doc.key, 'value'); + assert.equal(cur_frm.doc.key, "value"); }, - () => done() + () => done(), ]); - }); diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js index f830b170ed0..5381e440270 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.js +++ b/erpnext/manufacturing/doctype/workstation/workstation.js @@ -3,17 +3,16 @@ frappe.ui.form.on("Workstation", { onload(frm) { - if(frm.is_new()) - { + if (frm.is_new()) { frappe.call({ - type:"GET", - method:"erpnext.manufacturing.doctype.workstation.workstation.get_default_holiday_list", - callback: function(r) { - if(!r.exe && r.message){ + type: "GET", + method: "erpnext.manufacturing.doctype.workstation.workstation.get_default_holiday_list", + callback: function (r) { + if (!r.exe && r.message) { cur_frm.set_value("holiday_list", r.message); } - } - }) + }, + }); } }, @@ -22,35 +21,39 @@ frappe.ui.form.on("Workstation", { frm.call({ method: "set_data_based_on_workstation_type", doc: frm.doc, - callback: function(r) { + callback: function (r) { frm.refresh_fields(); - } - }) + }, + }); } - } + }, }); -frappe.tour['Workstation'] = [ +frappe.tour["Workstation"] = [ { fieldname: "workstation_name", title: "Workstation Name", - description: __("You can set it as a machine name or operation type. For example, stiching machine 12") + description: __( + "You can set it as a machine name or operation type. For example, stiching machine 12" + ), }, { fieldname: "production_capacity", title: "Production Capacity", - description: __("No. of parallel job cards which can be allowed on this workstation. Example: 2 would mean this workstation can process production for two Work Orders at a time.") + description: __( + "No. of parallel job cards which can be allowed on this workstation. Example: 2 would mean this workstation can process production for two Work Orders at a time." + ), }, { fieldname: "holiday_list", title: "Holiday List", - description: __("A Holiday List can be added to exclude counting these days for the Workstation.") + description: __("A Holiday List can be added to exclude counting these days for the Workstation."), }, { fieldname: "working_hours", title: "Working Hours", - description: __("Under Working Hours table, you can add start and end times for a Workstation. For example, a Workstation may be active from 9 am to 1 pm, then 2 pm to 5 pm. You can also specify the working hours based on shifts. While scheduling a Work Order, the system will check for the availability of the Workstation based on the working hours specified.") + description: __( + "Under Working Hours table, you can add start and end times for a Workstation. For example, a Workstation may be active from 9 am to 1 pm, then 2 pm to 5 pm. You can also specify the working hours based on shifts. While scheduling a Work Order, the system will check for the availability of the Workstation based on the working hours specified." + ), }, - - ]; diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 0eb9906a2ab..fe2cbc89b48 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -88,7 +88,8 @@ class Workstation(Document): if existing: frappe.throw( - _("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), OverlapError + _("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), + OverlapError, ) def update_bom_operation(self): @@ -128,10 +129,7 @@ def get_default_holiday_list(): def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime): if from_datetime and to_datetime: - - if not cint( - frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays") - ): + if not cint(frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")): check_workstation_for_holiday(workstation, from_datetime, to_datetime) if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")): @@ -174,7 +172,9 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime): if applicable_holidays: frappe.throw( - _("Workstation is closed on the following dates as per Holiday List: {0}").format(holiday_list) + _("Workstation is closed on the following dates as per Holiday List: {0}").format( + holiday_list + ) + "\n" + "\n".join(applicable_holidays), WorkstationHolidayError, diff --git a/erpnext/manufacturing/doctype/workstation/workstation_list.js b/erpnext/manufacturing/doctype/workstation/workstation_list.js index 6a89d21e1ef..77af49acf19 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_list.js +++ b/erpnext/manufacturing/doctype/workstation/workstation_list.js @@ -1,5 +1,5 @@ /* eslint-disable */ -frappe.listview_settings['Workstation'] = { +frappe.listview_settings["Workstation"] = { // add_fields: ["status"], // filters:[["status","=", "Open"]] }; diff --git a/erpnext/manufacturing/doctype/workstation_type/workstation_type.js b/erpnext/manufacturing/doctype/workstation_type/workstation_type.js index 419fa6c10ad..f0b3ed36a5e 100644 --- a/erpnext/manufacturing/doctype/workstation_type/workstation_type.js +++ b/erpnext/manufacturing/doctype/workstation_type/workstation_type.js @@ -1,8 +1,7 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Workstation Type', { +frappe.ui.form.on("Workstation Type", { // refresh: function(frm) { - // } }); diff --git a/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js b/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js index 1bcb1efdaad..fcb7e884ecc 100644 --- a/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js +++ b/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js @@ -1,12 +1,12 @@ -frappe.pages['bom-comparison-tool'].on_page_load = function(wrapper) { +frappe.pages["bom-comparison-tool"].on_page_load = function (wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, - title: __('BOM Comparison Tool'), - single_column: true + title: __("BOM Comparison Tool"), + single_column: true, }); new erpnext.BOMComparisonTool(page); -} +}; erpnext.BOMComparisonTool = class BOMComparisonTool { constructor(page) { @@ -18,45 +18,45 @@ erpnext.BOMComparisonTool = class BOMComparisonTool { this.form = new frappe.ui.FieldGroup({ fields: [ { - label: __('BOM 1'), - fieldname: 'name1', - fieldtype: 'Link', - options: 'BOM', + label: __("BOM 1"), + fieldname: "name1", + fieldtype: "Link", + options: "BOM", change: () => this.fetch_and_render(), get_query: () => { return { filters: { - "name": ["not in", [this.form.get_value("name2") || ""]] - } - } - } + name: ["not in", [this.form.get_value("name2") || ""]], + }, + }; + }, }, { - fieldtype: 'Column Break' + fieldtype: "Column Break", }, { - label: __('BOM 2'), - fieldname: 'name2', - fieldtype: 'Link', - options: 'BOM', + label: __("BOM 2"), + fieldname: "name2", + fieldtype: "Link", + options: "BOM", change: () => this.fetch_and_render(), get_query: () => { return { filters: { - "name": ["not in", [this.form.get_value("name1") || ""]] - } - } - } + name: ["not in", [this.form.get_value("name1") || ""]], + }, + }; + }, }, { - fieldtype: 'Section Break' + fieldtype: "Section Break", }, { - fieldtype: 'HTML', - fieldname: 'preview' - } + fieldtype: "HTML", + fieldname: "preview", + }, ], - body: this.page.body + body: this.page.body, }); this.form.make(); } @@ -64,33 +64,34 @@ erpnext.BOMComparisonTool = class BOMComparisonTool { fetch_and_render() { let { name1, name2 } = this.form.get_values(); if (!(name1 && name2)) { - this.form.get_field('preview').html(''); + this.form.get_field("preview").html(""); return; } // set working state - this.form.get_field('preview').html(` + this.form.get_field("preview").html(`
    ${__("Fetching...")}
    `); - frappe.call('erpnext.manufacturing.doctype.bom.bom.get_bom_diff', { - bom1: name1, - bom2: name2 - }).then(r => { - let diff = r.message; - frappe.model.with_doctype('BOM', () => { - this.render('BOM', name1, name2, diff); + frappe + .call("erpnext.manufacturing.doctype.bom.bom.get_bom_diff", { + bom1: name1, + bom2: name2, + }) + .then((r) => { + let diff = r.message; + frappe.model.with_doctype("BOM", () => { + this.render("BOM", name1, name2, diff); + }); }); - }); } render(doctype, name1, name2, diff) { - let change_html = (title, doctype, changed) => { let values_changed = this.get_changed_values(doctype, changed) - .map(change => { + .map((change) => { let [fieldname, value1, value2] = change; return `
    @@ -100,14 +101,14 @@ erpnext.BOMComparisonTool = class BOMComparisonTool { `; }) - .join(''); + .join(""); return `

    ${title}

    - ${__('Notes')} + ${__("Notes")}

    • - ${__("Loyalty Points will be calculated from the spent done (via the Sales Invoice), based on collection factor mentioned.")} + ${__( + "Loyalty Points will be calculated from the spent done (via the Sales Invoice), based on collection factor mentioned." + )}
    • - ${__("There can be multiple tiered collection factor based on the total spent. But the conversion factor for redemption will always be same for all the tier.")} + ${__( + "There can be multiple tiered collection factor based on the total spent. But the conversion factor for redemption will always be same for all the tier." + )}
    • - ${__("In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent")} + ${__( + "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent" + )}
    • ${__("If unlimited expiry for the Loyalty Points, keep the Expiry Duration empty or 0.")}
    • - ${__("If Auto Opt In is checked, then the customers will be automatically linked with the concerned Loyalty Program (on save)")} + ${__( + "If Auto Opt In is checked, then the customers will be automatically linked with the concerned Loyalty Program (on save)" + )}
    • ${__("One customer can be part of only single Loyalty Program.")} @@ -37,14 +44,14 @@ frappe.ui.form.on('Loyalty Program', { set_field_options("loyalty_program_help", help_content); }, - onload: function(frm) { - frm.set_query("expense_account", function(doc) { + onload: function (frm) { + frm.set_query("expense_account", function (doc) { return { filters: { - "root_type": "Expense", - 'is_group': 0, - 'company': doc.company - } + root_type: "Expense", + is_group: 0, + company: doc.company, + }, }; }); @@ -52,13 +59,15 @@ frappe.ui.form.on('Loyalty Program', { erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) { - frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules.")); + frappe.throw( + __("Please select the Multiple Tier Program type for more than one collection rules.") + ); } }, - company: function(frm) { + company: function (frm) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); - } + }, }); diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py index a134f746635..8da5a3f909f 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py @@ -25,13 +25,11 @@ def get_loyalty_details( condition += " and expiry_date>='%s' " % expiry_date loyalty_point_details = frappe.db.sql( - """select sum(loyalty_points) as loyalty_points, + f"""select sum(loyalty_points) as loyalty_points, sum(purchase_amount) as total_spent from `tabLoyalty Point Entry` where customer=%s and loyalty_program=%s and posting_date <= %s {condition} - group by customer""".format( - condition=condition - ), + group by customer""", (customer, loyalty_program, expiry_date), as_dict=1, ) @@ -52,9 +50,7 @@ def get_loyalty_program_details_with_points( include_expired_entry=False, current_transaction_amount=0, ): - lp_details = get_loyalty_program_details( - customer, loyalty_program, company=company, silent=silent - ) + lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent) loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program) lp_details.update( get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry) diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py index 3641ac4428f..4d21fb69806 100644 --- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py @@ -19,9 +19,7 @@ class TestLoyaltyProgram(unittest.TestCase): create_records() def test_loyalty_points_earned_single_tier(self): - frappe.db.set_value( - "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty" - ) + frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") # create a new sales invoice si_original = create_sales_invoice_record() si_original.insert() @@ -69,9 +67,7 @@ class TestLoyaltyProgram(unittest.TestCase): d.cancel() def test_loyalty_points_earned_multiple_tier(self): - frappe.db.set_value( - "Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty" - ) + frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty") # assign multiple tier program to the customer customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"}) customer.loyalty_program = frappe.get_doc( @@ -128,9 +124,7 @@ class TestLoyaltyProgram(unittest.TestCase): def test_cancel_sales_invoice(self): """cancelling the sales invoice should cancel the earned points""" - frappe.db.set_value( - "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty" - ) + frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") # create a new sales invoice si = create_sales_invoice_record() si.insert() @@ -140,7 +134,7 @@ class TestLoyaltyProgram(unittest.TestCase): "Loyalty Point Entry", {"invoice_type": "Sales Invoice", "invoice": si.name, "customer": si.customer}, ) - self.assertEqual(True, not (lpe is None)) + self.assertEqual(True, lpe is not None) # cancelling sales invoice si.cancel() @@ -148,9 +142,7 @@ class TestLoyaltyProgram(unittest.TestCase): self.assertEqual(True, (lpe is None)) def test_sales_invoice_return(self): - frappe.db.set_value( - "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty" - ) + frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") # create a new sales invoice si_original = create_sales_invoice_record(2) si_original.conversion_rate = flt(1) @@ -346,9 +338,7 @@ def create_records(): ).insert() # create item price - if not frappe.db.exists( - "Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"} - ): + if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}): frappe.get_doc( { "doctype": "Item Price", diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js index 103fa96d02d..25d437f154f 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js @@ -1,16 +1,16 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Mode of Payment', { - setup: function(frm) { - frm.set_query("default_account", "accounts", function(doc, cdt, cdn) { +frappe.ui.form.on("Mode of Payment", { + setup: function (frm) { + frm.set_query("default_account", "accounts", function (doc, cdt, cdn) { let d = locals[cdt][cdn]; return { filters: [ - ['Account', 'account_type', 'in', 'Bank, Cash, Receivable'], - ['Account', 'is_group', '=', 0], - ['Account', 'company', '=', d.company] - ] + ["Account", "account_type", "in", "Bank, Cash, Receivable"], + ["Account", "is_group", "=", 0], + ["Account", "company", "=", d.company], + ], }; }); }, diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.js b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.js index 569f0084c6a..3383534f59e 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.js +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.js @@ -1,16 +1,16 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Monthly Distribution', { +frappe.ui.form.on("Monthly Distribution", { onload(frm) { - if(frm.doc.__islocal) { - return frm.call('get_months').then(() => { - frm.refresh_field('percentages'); + if (frm.doc.__islocal) { + return frm.call("get_months").then(() => { + frm.refresh_field("percentages"); }); } }, refresh(frm) { - frm.toggle_display('distribution_id', frm.doc.__islocal); - } + frm.toggle_display("distribution_id", frm.doc.__islocal); + }, }); diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py index 1d19708eddf..e5b2e74b4a4 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py @@ -37,9 +37,7 @@ class MonthlyDistribution(Document): total = sum(flt(d.percentage_allocation) for d in self.get("percentages")) if flt(total, 2) != 100.0: - frappe.throw( - _("Percentage Allocation should be equal to 100%") + " ({0}%)".format(str(flt(total, 2))) - ) + frappe.throw(_("Percentage Allocation should be equal to 100%") + f" ({flt(total, 2)!s}%)") def get_periodwise_distribution_data(distribution_id, period_list, periodicity): diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 88867d11bb8..f1efba8a954 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -1,48 +1,52 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Opening Invoice Creation Tool', { - setup: function(frm) { - frm.set_query('party_type', 'invoices', function(doc, cdt, cdn) { +frappe.ui.form.on("Opening Invoice Creation Tool", { + setup: function (frm) { + frm.set_query("party_type", "invoices", function (doc, cdt, cdn) { return { filters: { - 'name': ['in', 'Customer, Supplier'] - } + name: ["in", "Customer, Supplier"], + }, }; }); if (frm.doc.company) { - frm.trigger('setup_company_filters'); + frm.trigger("setup_company_filters"); } - frappe.realtime.on('opening_invoice_creation_progress', data => { + frappe.realtime.on("opening_invoice_creation_progress", (data) => { if (!frm.doc.import_in_progress) { frm.dashboard.reset(); frm.doc.import_in_progress = true; } if (data.count == data.total) { - setTimeout(() => { - frm.doc.import_in_progress = false; - frm.clear_table("invoices"); - frm.refresh_fields(); - frm.page.clear_indicator(); - frm.dashboard.hide_progress(); - frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type])); - }, 1500, data.title); + setTimeout( + () => { + frm.doc.import_in_progress = false; + frm.clear_table("invoices"); + frm.refresh_fields(); + frm.page.clear_indicator(); + frm.dashboard.hide_progress(); + frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type])); + }, + 1500, + data.title + ); return; } frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message); - frm.page.set_indicator(__('In Progress'), 'orange'); + frm.page.set_indicator(__("In Progress"), "orange"); }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, - refresh: function(frm) { + refresh: function (frm) { frm.disable_save(); !frm.doc.import_in_progress && frm.trigger("make_dashboard"); - frm.page.set_primary_action(__('Create Invoices'), () => { + frm.page.set_primary_action(__("Create Invoices"), () => { let btn_primary = frm.page.btn_primary.get(0); return frm.call({ doc: frm.doc, @@ -58,100 +62,98 @@ frappe.ui.form.on('Opening Invoice Creation Tool', { } }, - setup_company_filters: function(frm) { - frm.set_query('cost_center', 'invoices', function(doc, cdt, cdn) { + setup_company_filters: function (frm) { + frm.set_query("cost_center", "invoices", function (doc, cdt, cdn) { return { filters: { - 'company': doc.company - } + company: doc.company, + }, }; }); - frm.set_query('cost_center', function(doc) { + frm.set_query("cost_center", function (doc) { return { filters: { - 'company': doc.company - } + company: doc.company, + }, }; }); - frm.set_query('temporary_opening_account', 'invoices', function(doc, cdt, cdn) { + frm.set_query("temporary_opening_account", "invoices", function (doc, cdt, cdn) { return { filters: { - 'company': doc.company - } - } + company: doc.company, + }, + }; }); }, - company: function(frm) { + company: function (frm) { if (frm.doc.company) { - - frm.trigger('setup_company_filters'); + frm.trigger("setup_company_filters"); frappe.call({ - method: 'erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool.get_temporary_opening_account', + method: "erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool.get_temporary_opening_account", args: { - company: frm.doc.company + company: frm.doc.company, }, callback: (r) => { if (r.message) { frm.doc.__onload.temporary_opening_account = r.message; - frm.trigger('update_invoice_table'); + frm.trigger("update_invoice_table"); } - } - }) + }, + }); } erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, - invoice_type: function(frm) { + invoice_type: function (frm) { $.each(frm.doc.invoices, (idx, row) => { - row.party_type = frm.doc.invoice_type == "Sales"? "Customer": "Supplier"; + row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; row.party = ""; }); frm.refresh_fields(); }, - make_dashboard: function(frm) { + make_dashboard: function (frm) { let max_count = frm.doc.__onload.max_count; let opening_invoices_summary = frm.doc.__onload.opening_invoices_summary; - if(!$.isEmptyObject(opening_invoices_summary)) { + if (!$.isEmptyObject(opening_invoices_summary)) { let section = frm.dashboard.add_section( - frappe.render_template('opening_invoice_creation_tool_dashboard', { + frappe.render_template("opening_invoice_creation_tool_dashboard", { data: opening_invoices_summary, - max_count: max_count + max_count: max_count, }), __("Opening Invoices Summary") ); - section.on('click', '.invoice-link', function() { - let doctype = $(this).attr('data-type'); - let company = $(this).attr('data-company'); - frappe.set_route('List', doctype, - {'is_opening': 'Yes', 'company': company, 'docstatus': 1}); + section.on("click", ".invoice-link", function () { + let doctype = $(this).attr("data-type"); + let company = $(this).attr("data-company"); + frappe.set_route("List", doctype, { is_opening: "Yes", company: company, docstatus: 1 }); }); frm.dashboard.show(); } }, - update_invoice_table: function(frm) { + update_invoice_table: function (frm) { $.each(frm.doc.invoices, (idx, row) => { if (!row.temporary_opening_account) { row.temporary_opening_account = frm.doc.__onload.temporary_opening_account; } - if(!row.cost_center) { + if (!row.cost_center) { row.cost_center = frm.doc.cost_center; } - row.party_type = frm.doc.invoice_type == "Sales"? "Customer": "Supplier"; + row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; }); - } + }, }); -frappe.ui.form.on('Opening Invoice Creation Tool Item', { +frappe.ui.form.on("Opening Invoice Creation Tool Item", { invoices_add: (frm) => { - frm.trigger('update_invoice_table'); - } + frm.trigger("update_invoice_table"); + }, }); diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 1e22c64c8f2..d365959c2cd 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -83,9 +83,7 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase): company = "_Test Opening Invoice Company" party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") - old_default_receivable_account = frappe.db.get_value( - "Company", company, "default_receivable_account" - ) + old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account") frappe.db.set_value("Company", company, "default_receivable_account", "") if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"): @@ -121,9 +119,7 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase): self.assertTrue(error_log) # teardown - frappe.db.set_value( - "Company", company, "default_receivable_account", old_default_receivable_account - ) + frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account) def test_renaming_of_invoice_using_invoice_number_field(self): company = "_Test Opening Invoice Company" @@ -169,7 +165,7 @@ def get_opening_invoice_creation_dict(**args): { "qty": 1.0, "outstanding_amount": 300, - "party": args.get("party_1") or "_Test {0}".format(party), + "party": args.get("party_1") or f"_Test {party}", "item_name": "Opening Item", "due_date": "2016-09-10", "posting_date": "2016-09-05", @@ -179,7 +175,7 @@ def get_opening_invoice_creation_dict(**args): { "qty": 2.0, "outstanding_amount": 250, - "party": args.get("party_2") or "_Test {0} 1".format(party), + "party": args.get("party_2") or f"_Test {party} 1", "item_name": "Opening Item", "due_date": "2016-09-10", "posting_date": "2016-09-05", diff --git a/erpnext/accounts/doctype/party_link/party_link.js b/erpnext/accounts/doctype/party_link/party_link.js index 6da9291d64d..dfa02c4be9a 100644 --- a/erpnext/accounts/doctype/party_link/party_link.js +++ b/erpnext/accounts/doctype/party_link/party_link.js @@ -1,33 +1,34 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Party Link', { - refresh: function(frm) { - frm.set_query('primary_role', () => { +frappe.ui.form.on("Party Link", { + refresh: function (frm) { + frm.set_query("primary_role", () => { return { filters: { - name: ['in', ['Customer', 'Supplier']] - } + name: ["in", ["Customer", "Supplier"]], + }, }; }); - frm.set_query('secondary_role', () => { - let party_types = Object.keys(frappe.boot.party_account_types) - .filter(p => p != frm.doc.primary_role); + frm.set_query("secondary_role", () => { + let party_types = Object.keys(frappe.boot.party_account_types).filter( + (p) => p != frm.doc.primary_role + ); return { filters: { - name: ['in', party_types] - } + name: ["in", party_types], + }, }; }); }, primary_role(frm) { - frm.set_value('primary_party', ''); - frm.set_value('secondary_role', ''); + frm.set_value("primary_party", ""); + frm.set_value("secondary_role", ""); }, secondary_role(frm) { - frm.set_value('secondary_party', ''); - } + frm.set_value("secondary_party", ""); + }, }); diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py index 312cfd2c0a1..d3148c3c455 100644 --- a/erpnext/accounts/doctype/party_link/party_link.py +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -24,7 +24,10 @@ class PartyLink(Document): if existing_party_link: frappe.throw( _("{} {} is already linked with {} {}").format( - self.primary_role, bold(self.primary_party), self.secondary_role, bold(self.secondary_party) + self.primary_role, + bold(self.primary_party), + self.secondary_role, + bold(self.secondary_party), ) ) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d5eeae5ab34..309141fe4c2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries', 'Bank Transaction']; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); @@ -631,7 +631,7 @@ frappe.ui.form.on('Payment Entry', { get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) { const today = frappe.datetime.get_today(); - const fields = [ + let fields = [ {fieldtype:"Section Break", label: __("Posting Date")}, {fieldtype:"Date", label: __("From Date"), fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)}, @@ -646,18 +646,29 @@ frappe.ui.form.on('Payment Entry', { fieldname:"outstanding_amt_greater_than", default: 0}, {fieldtype:"Column Break"}, {fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"}, - {fieldtype:"Section Break"}, - {fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center", - "get_query": function() { - return { - "filters": {"company": frm.doc.company} - } + ]; + + if (frm.dimension_filters) { + let column_break_insertion_point = Math.ceil((frm.dimension_filters.length)/2); + + fields.push({fieldtype:"Section Break"}); + frm.dimension_filters.map((elem, idx)=>{ + fields.push({ + fieldtype: "Link", + label: elem.document_type == "Cost Center" ? "Cost Center" : elem.label, + options: elem.document_type, + fieldname: elem.fieldname || elem.document_type + }); + if(idx+1 == column_break_insertion_point) { + fields.push({fieldtype:"Column Break"}); } - }, - {fieldtype:"Column Break"}, + }); + } + + fields = fields.concat([ {fieldtype:"Section Break"}, {fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1}, - ]; + ]); let btn_text = ""; @@ -919,7 +930,7 @@ frappe.ui.form.on('Payment Entry', { if(frm.doc.payment_type == "Receive" && frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions && frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) { - unallocated_amount = (frm.doc.base_received_amount + total_deductions + flt(frm.doc.base_total_taxes_and_charges) + unallocated_amount = (frm.doc.base_received_amount + total_deductions - flt(frm.doc.base_total_taxes_and_charges) - frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate; } else if (frm.doc.payment_type == "Pay" && frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index bd43fb77c23..d3db789c336 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -576,6 +576,7 @@ "fieldtype": "Select", "hidden": 1, "label": "Payment Order Status", + "no_copy": 1, "options": "Initiated\nPayment Ordered", "read_only": 1 }, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a3ebf045fc9..370e1deaa40 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -13,6 +13,7 @@ from pypika import Case from pypika.functions import Coalesce, Sum import erpnext +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.doctype.bank_account.bank_account import ( get_bank_account_details, get_party_bank_account, @@ -46,7 +47,7 @@ class InvalidPaymentEntry(ValidationError): class PaymentEntry(AccountsController): def __init__(self, *args, **kwargs): - super(PaymentEntry, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if not self.is_new(): self.setup_party_account_field() @@ -112,7 +113,7 @@ class PaymentEntry(AccountsController): "Unreconcile Payment", "Unreconcile Payment Entries", ) - super(PaymentEntry, self).on_cancel() + super().on_cancel() self.make_gl_entries(cancel=1) self.update_outstanding_amounts() self.update_advance_paid() @@ -220,9 +221,7 @@ class PaymentEntry(AccountsController): # If term based allocation is enabled, throw if ( d.payment_term is None or d.payment_term == "" - ) and self.term_based_allocation_enabled_for_reference( - d.reference_doctype, d.reference_name - ): + ) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name): frappe.throw( _( "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" @@ -234,7 +233,9 @@ class PaymentEntry(AccountsController): # The reference has already been fully paid if not latest: frappe.throw( - _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) + _("{0} {1} has already been fully paid.").format( + _(d.reference_doctype), d.reference_name + ) ) # The reference has already been partly paid elif ( @@ -258,14 +259,14 @@ class PaymentEntry(AccountsController): and latest.payment_term_outstanding and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding)) ) - and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) + and self.term_based_allocation_enabled_for_reference( + d.reference_doctype, d.reference_name + ) ): 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 - ) + ).format(d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term) ) if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): @@ -334,12 +335,15 @@ class PaymentEntry(AccountsController): ) def set_missing_ref_details( - self, force: bool = False, update_ref_details_only_for: list | None = None + self, + force: bool = False, + update_ref_details_only_for: list | None = None, + reference_exchange_details: dict | None = None, ) -> None: for d in self.get("references"): if d.allocated_amount: if update_ref_details_only_for and ( - not (d.reference_doctype, d.reference_name) in update_ref_details_only_for + (d.reference_doctype, d.reference_name) not in update_ref_details_only_for ): continue @@ -347,6 +351,14 @@ class PaymentEntry(AccountsController): d.reference_doctype, d.reference_name, self.party_account_currency ) + # Only update exchange rate when the reference is Journal Entry + if ( + reference_exchange_details + and d.reference_doctype == reference_exchange_details.reference_doctype + and d.reference_name == reference_exchange_details.reference_name + ): + ref_details.update({"exchange_rate": reference_exchange_details.exchange_rate}) + for field, value in ref_details.items(): if d.exchange_gain_loss: # for cases where gain/loss is booked into invoice @@ -378,7 +390,9 @@ class PaymentEntry(AccountsController): else: if ref_doc: if self.paid_from_account_currency == ref_doc.currency: - self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate") + self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get( + "conversion_rate" + ) if not self.source_exchange_rate: self.source_exchange_rate = get_exchange_rate( @@ -415,7 +429,7 @@ class PaymentEntry(AccountsController): if d.reference_doctype not in valid_reference_doctypes: frappe.throw( _("Reference Doctype must be one of {0}").format( - comma_or((_(d) for d in valid_reference_doctypes)) + comma_or(_(d) for d in valid_reference_doctypes) ) ) @@ -438,7 +452,8 @@ class PaymentEntry(AccountsController): if d.reference_doctype in frappe.get_hooks("invoice_doctypes"): if self.party_type == "Customer": ref_party_account = ( - get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to + get_party_account_based_on_invoice_discounting(d.reference_name) + or ref_doc.debit_to ) elif self.party_type == "Supplier": ref_party_account = ref_doc.credit_to @@ -448,7 +463,10 @@ class PaymentEntry(AccountsController): if ref_party_account != self.party_account: frappe.throw( _("{0} {1} is associated with {2}, but Party Account is {3}").format( - _(d.reference_doctype), d.reference_name, ref_party_account, self.party_account + _(d.reference_doctype), + d.reference_name, + ref_party_account, + self.party_account, ) ) @@ -459,7 +477,9 @@ class PaymentEntry(AccountsController): ) if ref_doc.docstatus != 1: - frappe.throw(_("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name)) + frappe.throw( + _("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name) + ) def get_valid_reference_doctypes(self): if self.party_type == "Customer": @@ -630,9 +650,7 @@ class PaymentEntry(AccountsController): if not (is_single_currency and reference_is_multi_currency): return allocated_amount - allocated_amount = flt( - allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount") - ) + allocated_amount = flt(allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")) return allocated_amount @@ -680,7 +698,6 @@ class PaymentEntry(AccountsController): accounts = [] for d in self.taxes: if d.account_head == tax_withholding_details.get("account_head"): - # Preserve user updated included in paid amount if d.included_in_paid_amount: tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount}) @@ -800,7 +817,6 @@ class PaymentEntry(AccountsController): flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount") ) else: - # Use source/target exchange rate, so no difference amount is calculated. # then update exchange gain/loss amount in reference table # if there is an exchange gain/loss amount in reference table, submit a JE for that @@ -873,19 +889,19 @@ class PaymentEntry(AccountsController): ) base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) - - if self.payment_type == "Receive": - self.difference_amount = base_party_amount - self.base_received_amount - elif self.payment_type == "Pay": - self.difference_amount = self.base_paid_amount - base_party_amount - else: - self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - - total_deductions = sum(flt(d.amount) for d in self.get("deductions")) included_taxes = self.get_included_taxes() + if self.payment_type == "Receive": + self.difference_amount = base_party_amount - self.base_received_amount + included_taxes + elif self.payment_type == "Pay": + self.difference_amount = self.base_paid_amount - base_party_amount - included_taxes + else: + self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - included_taxes + + total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + self.difference_amount = flt( - self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount") + self.difference_amount - total_deductions, self.precision("difference_amount") ) def get_included_taxes(self): @@ -917,7 +933,9 @@ class PaymentEntry(AccountsController): total_negative_outstanding = flt( sum( - abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 + abs(flt(d.outstanding_amount)) + for d in self.get("references") + if flt(d.outstanding_amount) < 0 ), self.references[0].precision("outstanding_amount") if self.references else None, ) @@ -970,7 +988,6 @@ class PaymentEntry(AccountsController): ) ] else: - remarks = [ _("Amount {0} {1} {2} {3}").format( self.party_account_currency, @@ -990,14 +1007,19 @@ class PaymentEntry(AccountsController): if d.allocated_amount: remarks.append( _("Amount {0} {1} against {2} {3}").format( - self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name + self.party_account_currency, + d.allocated_amount, + d.reference_doctype, + d.reference_name, ) ) for d in self.get("deductions"): if d.amount: remarks.append( - _("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account) + _("Amount {0} {1} deducted against {2}").format( + self.company_currency, d.amount, d.account + ) ) self.set("remarks", "\n".join(remarks)) @@ -1432,7 +1454,8 @@ def get_outstanding_reference_documents(args): return [] elif supplier_status["hold_type"] == "Payments": if ( - not supplier_status["release_date"] or getdate(nowdate()) <= supplier_status["release_date"] + not supplier_status["release_date"] + or getdate(nowdate()) <= supplier_status["release_date"] ): return [] @@ -1442,7 +1465,7 @@ def get_outstanding_reference_documents(args): # Get positive outstanding sales /purchase invoices condition = "" if args.get("voucher_type") and args.get("voucher_no"): - condition = " and voucher_type={0} and voucher_no={1}".format( + condition = " and voucher_type={} and voucher_no={}".format( frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"]) ) common_filter.append(ple.voucher_type == args["voucher_type"]) @@ -1453,6 +1476,13 @@ def get_outstanding_reference_documents(args): condition += " and cost_center='%s'" % args.get("cost_center") accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center")) + # dynamic dimension filters + active_dimensions = get_dimensions()[0] + for dim in active_dimensions: + if args.get(dim.fieldname): + condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'" + accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname)) + date_fields_dict = { "posting_date": ["from_posting_date", "to_posting_date"], "due_date": ["from_due_date", "to_due_date"], @@ -1460,21 +1490,21 @@ def get_outstanding_reference_documents(args): for fieldname, date_fields in date_fields_dict.items(): if args.get(date_fields[0]) and args.get(date_fields[1]): - condition += " and {0} between '{1}' and '{2}'".format( + condition += " and {} between '{}' and '{}'".format( fieldname, args.get(date_fields[0]), args.get(date_fields[1]) ) posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])]) elif args.get(date_fields[0]): # if only from date is supplied - condition += " and {0} >= '{1}'".format(fieldname, args.get(date_fields[0])) + condition += f" and {fieldname} >= '{args.get(date_fields[0])}'" posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0]))) elif args.get(date_fields[1]): # if only to date is supplied - condition += " and {0} <= '{1}'".format(fieldname, args.get(date_fields[1])) + condition += f" and {fieldname} <= '{args.get(date_fields[1])}'" posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1]))) if args.get("company"): - condition += " and company = {0}".format(frappe.db.escape(args.get("company"))) + condition += " and company = {}".format(frappe.db.escape(args.get("company"))) common_filter.append(ple.company == args.get("company")) outstanding_invoices = [] @@ -1546,9 +1576,7 @@ def get_outstanding_reference_documents(args): frappe.msgprint( _( "No outstanding {0} found for the {1} {2} which qualify the filters you have specified." - ).format( - _(ref_document_type), _(args.get("party_type")).lower(), frappe.bold(args.get("party")) - ) + ).format(_(ref_document_type), _(args.get("party_type")).lower(), frappe.bold(args.get("party"))) ) return data @@ -1584,12 +1612,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list return outstanding_invoices_after_split -def get_currency_data(outstanding_invoices: list, company: str = None) -> dict: +def get_currency_data(outstanding_invoices: list, company: str | None = None) -> dict: """Get currency and conversion data for a list of invoices.""" exc_rates = frappe._dict() - company_currency = ( - frappe.db.get_value("Company", company, "default_currency") if company else None - ) + company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None for doctype in ["Sales Invoice", "Purchase Invoice"]: invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] @@ -1680,6 +1706,12 @@ def get_orders_to_be_billed( if doc and hasattr(doc, "cost_center") and doc.cost_center: condition = " and cost_center='%s'" % cost_center + # dynamic dimension filters + active_dimensions = get_dimensions()[0] + for dim in active_dimensions: + if filters.get(dim.fieldname): + condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'" + if party_account_currency == company_currency: grand_total_field = "base_grand_total" rounded_total_field = "base_rounded_total" @@ -1827,18 +1859,14 @@ def get_account_details(account, date, cost_center=None): frappe.has_permission("Payment Entry", throw=True) # to check if the passed account is accessible under reference doctype Payment Entry - account_list = frappe.get_list( - "Account", {"name": account}, reference_doctype="Payment Entry", limit=1 - ) + account_list = frappe.get_list("Account", {"name": account}, reference_doctype="Payment Entry", limit=1) # There might be some user permissions which will allow account under certain doctypes # except for Payment Entry, only in such case we should throw permission error if not account_list: frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account)) - account_balance = get_balance_on( - account, date, cost_center=cost_center, ignore_account_permission=True - ) + account_balance = get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True) return frappe._dict( { @@ -1885,9 +1913,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre total_amount = outstanding_amount = exchange_rate = None ref_doc = frappe.get_doc(reference_doctype, reference_name) - company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency( - ref_doc.company - ) + company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company) if reference_doctype == "Dunning": total_amount = outstanding_amount = ref_doc.get("dunning_amount") @@ -1896,9 +1922,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: total_amount = ref_doc.get("total_amount") if ref_doc.multi_currency: - exchange_rate = get_exchange_rate( - party_account_currency, company_currency, ref_doc.posting_date - ) + exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) else: exchange_rate = 1 outstanding_amount = get_outstanding_on_journal_entry(reference_name) @@ -1956,9 +1980,7 @@ def get_payment_entry( ): doc = frappe.get_doc(dt, dn) over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") - if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= ( - 100.0 + over_billing_allowance - ): + if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (100.0 + over_billing_allowance): frappe.throw(_("Can only make payment against unbilled {0}").format(_(dt))) if not party_type: @@ -2011,9 +2033,7 @@ def get_payment_entry( pe.paid_from_account_currency = ( party_account_currency if payment_type == "Receive" else bank.account_currency ) - pe.paid_to_account_currency = ( - party_account_currency if payment_type == "Pay" else bank.account_currency - ) + pe.paid_to_account_currency = party_account_currency if payment_type == "Pay" else bank.account_currency pe.paid_amount = paid_amount pe.received_amount = received_amount pe.letter_head = doc.get("letter_head") @@ -2042,7 +2062,6 @@ def get_payment_entry( {"name": doc.payment_terms_template}, "allocate_payment_based_on_payment_terms", ): - for reference in get_reference_as_per_payment_terms( doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency ): @@ -2163,9 +2182,9 @@ 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): + if (dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0)) or ( + dt == "Purchase Invoice" and doc.outstanding_amount < 0 + ): payment_type = "Receive" else: payment_type = "Pay" @@ -2225,9 +2244,7 @@ def set_paid_amount_and_received_amount( return paid_amount, received_amount -def apply_early_payment_discount( - paid_amount, received_amount, doc, party_account_currency, reference_date -): +def apply_early_payment_discount(paid_amount, received_amount, doc, party_account_currency, reference_date): total_discount = 0 valid_discounts = [] eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] @@ -2237,7 +2254,6 @@ def apply_early_payment_discount( if doc.doctype in eligible_for_payments and has_payment_schedule: for term in doc.payment_schedule: if not term.discounted_amount and term.discount and reference_date <= term.discount_date: - if term.discount_type == "Percentage": grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total") discount_amount = flt(grand_total) * (term.discount / 100) @@ -2266,9 +2282,7 @@ def apply_early_payment_discount( return paid_amount, received_amount, total_discount, valid_discounts -def set_pending_discount_loss( - pe, doc, discount_amount, base_total_discount_loss, party_account_currency -): +def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss, party_account_currency): # If multi-currency, get base discount amount to adjust with base currency deductions/losses if party_account_currency != doc.company_currency: discount_amount = discount_amount * doc.get("conversion_rate", 1) @@ -2288,7 +2302,8 @@ def set_pending_discount_loss( pe.set_gain_or_loss( account_details={ "account": frappe.get_cached_value("Company", pe.company, account_type), - "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), + "cost_center": pe.cost_center + or frappe.get_cached_value("Company", pe.company, "cost_center"), "amount": discount_amount * positive_negative, } ) @@ -2311,9 +2326,7 @@ def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float: def get_total_discount_percent(doc, valid_discounts) -> float: """Get total percentage and amount discount applied as a percentage.""" total_discount_percent = ( - sum( - discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage" - ) + sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage") or 0.0 ) @@ -2356,9 +2369,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: # The same account head could be used more than once for tax in doc.get("taxes", []): - base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * ( - total_discount_percentage / 100 - ) + base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (total_discount_percentage / 100) account = tax.get("account_head") if not tax_discount_loss.get(account): @@ -2375,7 +2386,8 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: "deductions", { "account": account, - "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), + "cost_center": pe.cost_center + or frappe.get_cached_value("Company", pe.company, "cost_center"), "amount": flt(loss, precision), }, ) @@ -2398,7 +2410,8 @@ def get_reference_as_per_payment_terms( if not is_multi_currency_acc: # If accounting is done in company currency for multi-currency transaction payment_term_outstanding = flt( - payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount") + payment_term_outstanding * doc.get("conversion_rate"), + payment_term.precision("payment_amount"), ) if payment_term_outstanding: @@ -2426,7 +2439,7 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date): dr_or_cr = "debit_in_account_currency - credit_in_account_currency" paid_amount = frappe.db.sql( - """ + f""" select ifnull(sum({dr_or_cr}), 0) as paid_amount from `tabGL Entry` where against_voucher_type = %s @@ -2436,9 +2449,7 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date): and account = %s and due_date = %s and {dr_or_cr} > 0 - """.format( - dr_or_cr=dr_or_cr - ), + """, (dt, dn, party_type, party, account, due_date), ) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry_list.js b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js index 2d76fe69ef9..6974e58c78c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry_list.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js @@ -1,14 +1,13 @@ -frappe.listview_settings['Payment Entry'] = { - - onload: function(listview) { +frappe.listview_settings["Payment Entry"] = { + onload: function (listview) { if (listview.page.fields_dict.party_type) { - listview.page.fields_dict.party_type.get_query = function() { + listview.page.fields_dict.party_type.get_query = function () { return { - "filters": { - "name": ["in", Object.keys(frappe.boot.party_account_types)], - } + filters: { + name: ["in", Object.keys(frappe.boot.party_account_types)], + }, }; }; } - } + }, }; diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index a7f1f08cbc2..c600198999e 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -2,7 +2,6 @@ # See license.txt import json -import unittest import frappe from frappe import qb @@ -163,7 +162,7 @@ class TestPaymentEntry(FrappeTestCase): supplier.on_hold = 0 supplier.save() - except: + except Exception: pass else: raise Exception @@ -470,9 +469,7 @@ class TestPaymentEntry(FrappeTestCase): si.save() si.submit() - pe = get_payment_entry( - "Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700 - ) + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700) pe.reference_no = si.name pe.reference_date = nowdate() @@ -639,9 +636,7 @@ class TestPaymentEntry(FrappeTestCase): pe.set_exchange_rate() pe.set_amounts() - self.assertEqual( - pe.source_exchange_rate, 65.1, "{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1) - ) + self.assertEqual(pe.source_exchange_rate, 65.1, f"{pe.source_exchange_rate} is not equal to {65.1}") def test_internal_transfer_usd_to_inr(self): pe = frappe.new_doc("Payment Entry") @@ -705,7 +700,50 @@ class TestPaymentEntry(FrappeTestCase): pe2.submit() # create return entry against si1 - create_sales_invoice(is_return=1, return_against=si1.name, qty=-1) + cr_note = create_sales_invoice(is_return=1, return_against=si1.name, qty=-1) + si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount") + + # create JE(credit note) manually against si1 and cr_note + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": si1.company, + "voucher_type": "Credit Note", + "posting_date": nowdate(), + } + ) + je.append( + "accounts", + { + "account": si1.debit_to, + "party_type": "Customer", + "party": si1.customer, + "debit": 0, + "credit": 100, + "debit_in_account_currency": 0, + "credit_in_account_currency": 100, + "reference_type": si1.doctype, + "reference_name": si1.name, + "cost_center": si1.items[0].cost_center, + }, + ) + je.append( + "accounts", + { + "account": cr_note.debit_to, + "party_type": "Customer", + "party": cr_note.customer, + "debit": 100, + "credit": 0, + "debit_in_account_currency": 100, + "credit_in_account_currency": 0, + "reference_type": cr_note.doctype, + "reference_name": cr_note.name, + "cost_center": cr_note.items[0].cost_center, + }, + ) + je.save().submit() + si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount") self.assertEqual(si1_outstanding, -100) @@ -867,9 +905,7 @@ class TestPaymentEntry(FrappeTestCase): cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - pi = make_purchase_invoice_against_cost_center( - cost_center=cost_center, credit_to="Creditors - _TC" - ) + pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC") pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") self.assertEqual(pe.cost_center, pi.cost_center) @@ -910,9 +946,7 @@ class TestPaymentEntry(FrappeTestCase): si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=si.cost_center) - party_balance = get_balance_on( - party_type="Customer", party=si.customer, cost_center=si.cost_center - ) + party_balance = get_balance_on(party_type="Customer", party=si.customer, cost_center=si.cost_center) party_account_balance = get_balance_on(si.debit_to, cost_center=si.cost_center) pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") @@ -1171,7 +1205,7 @@ class TestPaymentEntry(FrappeTestCase): Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled """ - customer = create_customer() + create_customer() create_payment_terms_template() template = frappe.get_doc("Payment Terms Template", "Test Receivable Template") @@ -1230,9 +1264,7 @@ class TestPaymentEntry(FrappeTestCase): create_payment_terms_template() # SI has an earlier due date and SI2 has a later due date - si = create_sales_invoice( - qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4) - ) + si = create_sales_invoice(qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4)) si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer) si2.payment_terms_template = "Test Receivable Template" si2.submit() @@ -1358,12 +1390,11 @@ def create_payment_entry(**args): def create_payment_terms_template(): - create_payment_term("Basic Amount Receivable") create_payment_term("Tax Receivable") if not frappe.db.exists("Payment Terms Template", "Test Receivable Template"): - payment_term_template = frappe.get_doc( + frappe.get_doc( { "doctype": "Payment Terms Template", "template_name": "Test Receivable Template", diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js index aff067eab89..f15c2bdab21 100644 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js +++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js @@ -1,11 +1,11 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Payment Gateway Account', { +frappe.ui.form.on("Payment Gateway Account", { refresh(frm) { erpnext.utils.check_payments_app(); - if(!frm.doc.__islocal) { - frm.set_df_property('payment_gateway', 'read_only', 1); + if (!frm.doc.__islocal) { + frm.set_df_property("payment_gateway", "read_only", 1); } - } + }, }); diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js index 5a7be8e5ab2..07fe83177ba 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js @@ -1,8 +1,7 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Payment Ledger Entry', { +frappe.ui.form.on("Payment Ledger Entry", { // refresh: function(frm) { - // } }); diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py index bcbcb670faf..8be3d76474f 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py @@ -108,9 +108,9 @@ class PaymentLedgerEntry(Document): ): if not self.get(dimension.fieldname): frappe.throw( - _("Accounting Dimension {0} is required for 'Profit and Loss' account {1}.").format( - dimension.label, self.account - ) + _( + "Accounting Dimension {0} is required for 'Profit and Loss' account {1}." + ).format(dimension.label, self.account) ) if ( @@ -121,9 +121,9 @@ class PaymentLedgerEntry(Document): ): if not self.get(dimension.fieldname): frappe.throw( - _("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.").format( - dimension.label, self.account - ) + _( + "Accounting Dimension {0} is required for 'Balance Sheet' account {1}." + ).format(dimension.label, self.account) ) def validate(self): @@ -132,11 +132,12 @@ class PaymentLedgerEntry(Document): def on_update(self): adv_adj = self.flags.adv_adj if not self.flags.from_repost: - self.validate_account_details() - self.validate_dimensions_for_pl_and_bs() - self.validate_allowed_dimensions() - validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) + if not self.delinked: + self.validate_account_details() + self.validate_dimensions_for_pl_and_bs() + self.validate_allowed_dimensions() + validate_balance_type(self.account, adv_adj) # update outstanding amount if ( diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py index fc6dbba7e7f..3eac98d7910 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py @@ -84,11 +84,14 @@ class TestPaymentLedgerEntry(FrappeTestCase): self.customer = customer.name def create_sales_invoice( - self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False ): """ Helper function to populate default values in sales invoice """ + if posting_date is None: + posting_date = nowdate() + sinv = create_sales_invoice( qty=qty, rate=rate, @@ -112,10 +115,12 @@ class TestPaymentLedgerEntry(FrappeTestCase): ) return sinv - def create_payment_entry(self, amount=100, posting_date=nowdate()): + def create_payment_entry(self, amount=100, posting_date=None): """ Helper function to populate default values in payment entry """ + if posting_date is None: + posting_date = nowdate() payment = create_payment_entry( company=self.company, payment_type="Receive", @@ -128,9 +133,10 @@ class TestPaymentLedgerEntry(FrappeTestCase): payment.posting_date = posting_date return payment - def create_sales_order( - self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False - ): + def create_sales_order(self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False): + if posting_date is None: + posting_date = nowdate() + so = make_sales_order( company=self.company, transaction_date=posting_date, @@ -159,9 +165,7 @@ class TestPaymentLedgerEntry(FrappeTestCase): for doctype in doctype_list: qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() - def create_journal_entry( - self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None - ): + def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None): je = frappe.new_doc("Journal Entry") je.posting_date = posting_date or nowdate() je.company = self.company @@ -294,7 +298,7 @@ class TestPaymentLedgerEntry(FrappeTestCase): cr_note1.return_against = si3.name cr_note1 = cr_note1.save().submit() - pl_entries = ( + pl_entries_si3 = ( qb.from_(ple) .select( ple.voucher_type, @@ -309,7 +313,22 @@ class TestPaymentLedgerEntry(FrappeTestCase): .run(as_dict=True) ) - expected_values = [ + pl_entries_cr_note1 = ( + qb.from_(ple) + .select( + ple.voucher_type, + ple.voucher_no, + ple.against_voucher_type, + ple.against_voucher_no, + ple.amount, + ple.delinked, + ) + .where((ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name)) + .orderby(ple.creation) + .run(as_dict=True) + ) + + expected_values_for_si3 = [ { "voucher_type": si3.doctype, "voucher_no": si3.name, @@ -317,18 +336,21 @@ class TestPaymentLedgerEntry(FrappeTestCase): "against_voucher_no": si3.name, "amount": amount, "delinked": 0, - }, + } + ] + # credit/debit notes post ledger entries against itself + expected_values_for_cr_note1 = [ { "voucher_type": cr_note1.doctype, "voucher_no": cr_note1.name, - "against_voucher_type": si3.doctype, - "against_voucher_no": si3.name, + "against_voucher_type": cr_note1.doctype, + "against_voucher_no": cr_note1.name, "amount": -amount, "delinked": 0, }, ] - self.assertEqual(pl_entries[0], expected_values[0]) - self.assertEqual(pl_entries[1], expected_values[1]) + self.assertEqual(pl_entries_si3, expected_values_for_si3) + self.assertEqual(pl_entries_cr_note1, expected_values_for_cr_note1) def test_je_against_inv_and_note(self): ple = self.ple @@ -342,9 +364,7 @@ class TestPaymentLedgerEntry(FrappeTestCase): ) cr_note2.is_return = 1 cr_note2 = cr_note2.save().submit() - je1 = self.create_journal_entry( - self.debit_to, self.debit_to, amount, posting_date=transaction_date - ) + je1 = self.create_journal_entry(self.debit_to, self.debit_to, amount, posting_date=transaction_date) je1.get("accounts")[0].party_type = je1.get("accounts")[1].party_type = "Customer" je1.get("accounts")[0].party = je1.get("accounts")[1].party = self.customer je1.get("accounts")[0].reference_type = cr_note2.doctype @@ -399,9 +419,7 @@ class TestPaymentLedgerEntry(FrappeTestCase): ple.amount, ple.delinked, ) - .where( - (ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name) - ) + .where((ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name)) .orderby(ple.creation) .run(as_dict=True) ) @@ -498,7 +516,7 @@ class TestPaymentLedgerEntry(FrappeTestCase): amount = 100 so = self.create_sales_order(qty=1, rate=amount, posting_date=transaction_date).save().submit() - pe = get_payment_entry(so.doctype, so.name).save().submit() + get_payment_entry(so.doctype, so.name).save().submit() so.reload() so.cancel() diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js index 7d85d89c452..a041f290639 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.js +++ b/erpnext/accounts/doctype/payment_order/payment_order.js @@ -1,61 +1,69 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Payment Order', { - setup: function(frm) { - frm.set_query("company_bank_account", function() { +frappe.ui.form.on("Payment Order", { + setup: function (frm) { + frm.set_query("company_bank_account", function () { return { filters: { - "is_company_account":1 - } - } + is_company_account: 1, + }, + }; }); - frm.set_df_property('references', 'cannot_add_rows', true); + frm.set_df_property("references", "cannot_add_rows", true); }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.docstatus == 0) { - frm.add_custom_button(__('Payment Request'), function() { - frm.trigger("get_from_payment_request"); - }, __("Get Payments from")); + frm.add_custom_button( + __("Payment Request"), + function () { + frm.trigger("get_from_payment_request"); + }, + __("Get Payments from") + ); - frm.add_custom_button(__('Payment Entry'), function() { - frm.trigger("get_from_payment_entry"); - }, __("Get Payments from")); + frm.add_custom_button( + __("Payment Entry"), + function () { + frm.trigger("get_from_payment_entry"); + }, + __("Get Payments from") + ); - frm.trigger('remove_button'); + frm.trigger("remove_button"); } // payment Entry - if (frm.doc.docstatus===1 && frm.doc.payment_order_type==='Payment Request') { - frm.add_custom_button(__('Create Payment Entries'), function() { + if (frm.doc.docstatus === 1 && frm.doc.payment_order_type === "Payment Request") { + frm.add_custom_button(__("Create Payment Entries"), function () { frm.trigger("make_payment_records"); }); } }, - remove_row_if_empty: function(frm) { + remove_row_if_empty: function (frm) { // remove if first row is empty if (frm.doc.references.length > 0 && !frm.doc.references[0].reference_name) { frm.doc.references = []; } }, - remove_button: function(frm) { + remove_button: function (frm) { // remove custom button of order type that is not imported let label = ["Payment Request", "Payment Entry"]; if (frm.doc.references.length > 0 && frm.doc.payment_order_type) { - label = label.reduce(x => { - x!= frm.doc.payment_order_type; + label = label.reduce((x) => { + x != frm.doc.payment_order_type; return x; }); frm.remove_custom_button(label, "Get from"); } }, - get_from_payment_entry: function(frm) { + get_from_payment_entry: function (frm) { frm.trigger("remove_row_if_empty"); erpnext.utils.map_current_doc({ method: "erpnext.accounts.doctype.payment_entry.payment_entry.make_payment_order", @@ -63,7 +71,8 @@ frappe.ui.form.on('Payment Order', { target: frm, date_field: "posting_date", setters: { - party: frm.doc.supplier || "" + party_type: "Supplier", + party: frm.doc.supplier || "", }, get_query_filters: { bank: frm.doc.bank, @@ -71,70 +80,80 @@ frappe.ui.form.on('Payment Order', { payment_type: ["!=", "Receive"], bank_account: frm.doc.company_bank_account, paid_from: frm.doc.account, - payment_order_status: ["=", "Initiated"] - } + payment_order_status: ["=", "Initiated"], + }, }); }, - get_from_payment_request: function(frm) { + get_from_payment_request: function (frm) { frm.trigger("remove_row_if_empty"); erpnext.utils.map_current_doc({ method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_order", source_doctype: "Payment Request", target: frm, setters: { - party: frm.doc.supplier || "" + party_type: "Supplier", + party: frm.doc.supplier || "", }, get_query_filters: { bank: frm.doc.bank, docstatus: 1, status: ["=", "Initiated"], - } + }, }); }, - make_payment_records: function(frm){ + make_payment_records: function (frm) { var dialog = new frappe.ui.Dialog({ title: __("For Supplier"), fields: [ - {"fieldtype": "Link", "label": __("Supplier"), "fieldname": "supplier", "options":"Supplier", - "get_query": function () { + { + fieldtype: "Link", + label: __("Supplier"), + fieldname: "supplier", + options: "Supplier", + get_query: function () { return { - query:"erpnext.accounts.doctype.payment_order.payment_order.get_supplier_query", - filters: {'parent': frm.doc.name} - } - }, "reqd": 1 + query: "erpnext.accounts.doctype.payment_order.payment_order.get_supplier_query", + filters: { parent: frm.doc.name }, + }; + }, + reqd: 1, }, - {"fieldtype": "Link", "label": __("Mode of Payment"), "fieldname": "mode_of_payment", "options":"Mode of Payment", - "get_query": function () { + { + fieldtype: "Link", + label: __("Mode of Payment"), + fieldname: "mode_of_payment", + options: "Mode of Payment", + get_query: function () { return { - query:"erpnext.accounts.doctype.payment_order.payment_order.get_mop_query", - filters: {'parent': frm.doc.name} - } - } - } - ] + query: "erpnext.accounts.doctype.payment_order.payment_order.get_mop_query", + filters: { parent: frm.doc.name }, + }; + }, + }, + ], }); - dialog.set_primary_action(__("Submit"), function() { + dialog.set_primary_action(__("Submit"), function () { var args = dialog.get_values(); - if(!args) return; + if (!args) return; return frappe.call({ method: "erpnext.accounts.doctype.payment_order.payment_order.make_payment_records", args: { - "name": me.frm.doc.name, - "supplier": args.supplier, - "mode_of_payment": args.mode_of_payment + name: me.frm.doc.name, + supplier: args.supplier, + mode_of_payment: args.mode_of_payment, }, freeze: true, - callback: function(r) { + callback: function (r) { dialog.hide(); frm.refresh(); - } - }) - }) + }, + }); + }); dialog.show(); }, diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py index ff9615d14fc..dde9b094c01 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.py +++ b/erpnext/accounts/doctype/payment_order/payment_order.py @@ -66,9 +66,7 @@ def make_journal_entry(doc, supplier, mode_of_payment=None): je = frappe.new_doc("Journal Entry") je.payment_order = doc.name je.posting_date = nowdate() - mode_of_payment_type = frappe._dict( - frappe.get_all("Mode of Payment", fields=["name", "type"], as_list=1) - ) + mode_of_payment_type = frappe._dict(frappe.get_all("Mode of Payment", fields=["name", "type"], as_list=1)) je.voucher_type = "Bank Entry" if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == "Cash": diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index 0dcb1794b9a..7af096647ca 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -1,12 +1,15 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import getdate -from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account +from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import ( + create_bank_account, + create_gl_account, +) from erpnext.accounts.doctype.payment_entry.payment_entry import ( get_payment_entry, make_payment_order, @@ -14,28 +17,30 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import ( from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -class TestPaymentOrder(unittest.TestCase): +class TestPaymentOrder(FrappeTestCase): def setUp(self): - create_bank_account() + # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error + uniq_identifier = frappe.generate_hash(length=10) + self.gl_account = create_gl_account("_Test Bank " + uniq_identifier) + self.bank_account = create_bank_account( + gl_account=self.gl_account, bank_account_name="Checking Account " + uniq_identifier + ) def tearDown(self): - for bt in frappe.get_all("Payment Order"): - doc = frappe.get_doc("Payment Order", bt.name) - doc.cancel() - doc.delete() + frappe.db.rollback() def test_payment_order_creation_against_payment_entry(self): purchase_invoice = make_purchase_invoice() payment_entry = get_payment_entry( - "Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC" + "Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account ) payment_entry.reference_no = "_Test_Payment_Order" payment_entry.reference_date = getdate() - payment_entry.party_bank_account = "Checking Account - Citi Bank" + payment_entry.party_bank_account = self.bank_account payment_entry.insert() payment_entry.submit() - doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry") + doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry", self.bank_account) reference_doc = doc.get("references")[0] self.assertEqual(reference_doc.reference_name, payment_entry.name) self.assertEqual(reference_doc.reference_doctype, "Payment Entry") @@ -43,13 +48,13 @@ class TestPaymentOrder(unittest.TestCase): self.assertEqual(reference_doc.amount, 250) -def create_payment_order_against_payment_entry(ref_doc, order_type): +def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account): payment_order = frappe.get_doc( dict( doctype="Payment Order", company="_Test Company", payment_order_type=order_type, - company_bank_account="Checking Account - Citi Bank", + company_bank_account=bank_account, ) ) doc = make_payment_order(ref_doc.name, payment_order) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 5cdedb73c09..1e37f08bc77 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -2,129 +2,155 @@ // For license information, please see license.txt frappe.provide("erpnext.accounts"); -erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationController extends frappe.ui.form.Controller { +erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationController extends ( + frappe.ui.form.Controller +) { onload() { - const default_company = frappe.defaults.get_default('company'); - this.frm.set_value('company', default_company); + const default_company = frappe.defaults.get_default("company"); + this.frm.set_value("company", default_company); - this.frm.set_value('party_type', ''); - this.frm.set_value('party', ''); - this.frm.set_value('receivable_payable_account', ''); + this.frm.set_value("party_type", ""); + this.frm.set_value("party", ""); + this.frm.set_value("receivable_payable_account", ""); this.frm.set_query("party_type", () => { - return { - "filters": { - "name": ["in", Object.keys(frappe.boot.party_account_types)], - } - } - }); - - this.frm.set_query('receivable_payable_account', () => { return { filters: { - "company": this.frm.doc.company, - "is_group": 0, - "account_type": frappe.boot.party_account_types[this.frm.doc.party_type] - } + name: ["in", Object.keys(frappe.boot.party_account_types)], + }, }; }); - this.frm.set_query('bank_cash_account', () => { + this.frm.set_query("receivable_payable_account", () => { return { - filters:[ - ['Account', 'company', '=', this.frm.doc.company], - ['Account', 'is_group', '=', 0], - ['Account', 'account_type', 'in', ['Bank', 'Cash']] - ] + filters: { + company: this.frm.doc.company, + is_group: 0, + account_type: frappe.boot.party_account_types[this.frm.doc.party_type], + }, + }; + }); + + this.frm.set_query("bank_cash_account", () => { + return { + filters: [ + ["Account", "company", "=", this.frm.doc.company], + ["Account", "is_group", "=", 0], + ["Account", "account_type", "in", ["Bank", "Cash"]], + ], }; }); this.frm.set_query("cost_center", () => { return { - "filters": { - "company": this.frm.doc.company, - "is_group": 0 - } - } + filters: { + company: this.frm.doc.company, + is_group: 0, + }, + }; }); } refresh() { this.frm.disable_save(); - this.frm.set_df_property('invoices', 'cannot_delete_rows', true); - this.frm.set_df_property('payments', 'cannot_delete_rows', true); - this.frm.set_df_property('allocation', 'cannot_delete_rows', true); - - this.frm.set_df_property('invoices', 'cannot_add_rows', true); - this.frm.set_df_property('payments', 'cannot_add_rows', true); - this.frm.set_df_property('allocation', 'cannot_add_rows', true); + this.frm.set_df_property("invoices", "cannot_delete_rows", true); + this.frm.set_df_property("payments", "cannot_delete_rows", true); + this.frm.set_df_property("allocation", "cannot_delete_rows", true); + this.frm.set_df_property("invoices", "cannot_add_rows", true); + this.frm.set_df_property("payments", "cannot_add_rows", true); + this.frm.set_df_property("allocation", "cannot_add_rows", true); if (this.frm.doc.receivable_payable_account) { - this.frm.add_custom_button(__('Get Unreconciled Entries'), () => + this.frm.add_custom_button(__("Get Unreconciled Entries"), () => this.frm.trigger("get_unreconciled_entries") ); - this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary'); + this.frm.change_custom_button_type("Get Unreconciled Entries", null, "primary"); } if (this.frm.doc.invoices.length && this.frm.doc.payments.length) { - this.frm.add_custom_button(__('Allocate'), () => - this.frm.trigger("allocate") - ); - this.frm.change_custom_button_type('Allocate', null, 'primary'); - this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default'); + this.frm.add_custom_button(__("Allocate"), () => this.frm.trigger("allocate")); + this.frm.change_custom_button_type("Allocate", null, "primary"); + this.frm.change_custom_button_type("Get Unreconciled Entries", null, "default"); } if (this.frm.doc.allocation.length) { - this.frm.add_custom_button(__('Reconcile'), () => - this.frm.trigger("reconcile") - ); - this.frm.change_custom_button_type('Reconcile', null, 'primary'); - this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default'); - this.frm.change_custom_button_type('Allocate', null, 'default'); + this.frm.add_custom_button(__("Reconcile"), () => this.frm.trigger("reconcile")); + this.frm.change_custom_button_type("Reconcile", null, "primary"); + this.frm.change_custom_button_type("Get Unreconciled Entries", null, "default"); + this.frm.change_custom_button_type("Allocate", null, "default"); } + this.frm.trigger("set_query_for_dimension_filters"); + // check for any running reconciliation jobs if (this.frm.doc.receivable_payable_account) { this.frm.call({ doc: this.frm.doc, - method: 'is_auto_process_enabled', + method: "is_auto_process_enabled", callback: (r) => { if (r.message) { - this.frm.call({ - 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running", - "args": { - for_filter: { - company: this.frm.doc.company, - party_type: this.frm.doc.party_type, - party: this.frm.doc.party, - receivable_payable_account: this.frm.doc.receivable_payable_account + this.frm + .call({ + method: "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running", + args: { + for_filter: { + company: this.frm.doc.company, + party_type: this.frm.doc.party_type, + party: this.frm.doc.party, + receivable_payable_account: this.frm.doc.receivable_payable_account, + }, + }, + }) + .then((r) => { + if (r.message) { + let doc_link = frappe.utils.get_form_link( + "Process Payment Reconciliation", + r.message, + true + ); + let msg = __( + "Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", + [doc_link] + ); + this.frm.dashboard.add_comment(msg, "yellow"); } - } - }).then(r => { - if (r.message) { - let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true); - let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]); - this.frm.dashboard.add_comment(msg, "yellow"); - } - }); + }); } - } + }, }); } - + } + set_query_for_dimension_filters() { + frappe.call({ + method: "erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation.get_queries_for_dimension_filters", + args: { + company: this.frm.doc.company, + }, + callback: (r) => { + if (!r.exc && r.message) { + r.message.forEach((x) => { + this.frm.set_query(x.fieldname, () => { + return { + filters: x.filters, + }; + }); + }); + } + }, + }); } company() { - this.frm.set_value('party', ''); - this.frm.set_value('receivable_payable_account', ''); + this.frm.set_value("party", ""); + this.frm.set_value("receivable_payable_account", ""); } party_type() { - this.frm.set_value('party', ''); + this.frm.set_value("party", ""); } party() { - this.frm.set_value('receivable_payable_account', ''); + this.frm.set_value("receivable_payable_account", ""); this.frm.trigger("clear_child_tables"); if (!this.frm.doc.receivable_payable_account && this.frm.doc.party_type && this.frm.doc.party) { @@ -133,15 +159,14 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo args: { company: this.frm.doc.company, party_type: this.frm.doc.party_type, - party: this.frm.doc.party + party: this.frm.doc.party, }, callback: (r) => { if (!r.exc && r.message) { this.frm.set_value("receivable_payable_account", r.message); } this.frm.refresh(); - - } + }, }); } } @@ -159,7 +184,6 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.frm.trigger("get_unreconciled_entries"); } - clear_child_tables() { this.frm.clear_table("invoices"); this.frm.clear_table("payments"); @@ -171,52 +195,52 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.frm.clear_table("allocation"); return this.frm.call({ doc: this.frm.doc, - method: 'get_unreconciled_entries', + method: "get_unreconciled_entries", callback: () => { if (!(this.frm.doc.payments.length || this.frm.doc.invoices.length)) { - frappe.throw({message: __("No Unreconciled Invoices and Payments found for this party and account")}); - } else if (!(this.frm.doc.invoices.length)) { - frappe.throw({message: __("No Outstanding Invoices found for this party")}); - } else if (!(this.frm.doc.payments.length)) { - frappe.throw({message: __("No Unreconciled Payments found for this party")}); + frappe.throw({ + message: __("No Unreconciled Invoices and Payments found for this party and account"), + }); + } else if (!this.frm.doc.invoices.length) { + frappe.throw({ message: __("No Outstanding Invoices found for this party") }); + } else if (!this.frm.doc.payments.length) { + frappe.throw({ message: __("No Unreconciled Payments found for this party") }); } this.frm.refresh(); - } + }, }); - } allocate() { let payments = this.frm.fields_dict.payments.grid.get_selected_children(); - if (!(payments.length)) { + if (!payments.length) { payments = this.frm.doc.payments; } let invoices = this.frm.fields_dict.invoices.grid.get_selected_children(); - if (!(invoices.length)) { + if (!invoices.length) { invoices = this.frm.doc.invoices; } return this.frm.call({ doc: this.frm.doc, - method: 'allocate_entries', + method: "allocate_entries", args: { payments: payments, - invoices: invoices + invoices: invoices, }, callback: () => { this.frm.refresh(); - } + }, }); } reconcile() { - var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount); + var show_dialog = this.frm.doc.allocation.filter((d) => d.difference_amount); if (show_dialog && show_dialog.length) { - this.data = []; const dialog = new frappe.ui.Dialog({ title: __("Select Difference Account"), - size: 'extra-large', + size: "extra-large", fields: [ { fieldname: "allocation", @@ -228,77 +252,89 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo get_data: () => { return this.data; }, - fields: [{ - fieldtype:'Data', - fieldname:"docname", - in_list_view: 1, - hidden: 1 - }, { - fieldtype:'Data', - fieldname:"reference_name", - label: __("Voucher No"), - in_list_view: 1, - read_only: 1 - }, { - fieldtype:'Date', - fieldname:"gain_loss_posting_date", - label: __("Posting Date"), - in_list_view: 1, - reqd: 1, - }, { - - fieldtype:'Link', - options: 'Account', - in_list_view: 1, - label: __("Difference Account"), - fieldname: 'difference_account', - reqd: 1, - get_query: () => { - return { - filters: { - company: this.frm.doc.company, - is_group: 0 - } - } - } - }, { - fieldtype:'Currency', - in_list_view: 1, - label: __("Difference Amount"), - fieldname: 'difference_amount', - read_only: 1 - }] + fields: [ + { + fieldtype: "Data", + fieldname: "docname", + in_list_view: 1, + hidden: 1, + }, + { + fieldtype: "Data", + fieldname: "reference_name", + label: __("Voucher No"), + in_list_view: 1, + read_only: 1, + }, + { + fieldtype: "Date", + fieldname: "gain_loss_posting_date", + label: __("Posting Date"), + in_list_view: 1, + reqd: 1, + }, + { + fieldtype: "Link", + options: "Account", + in_list_view: 1, + label: __("Difference Account"), + fieldname: "difference_account", + reqd: 1, + get_query: () => { + return { + filters: { + company: this.frm.doc.company, + is_group: 0, + }, + }; + }, + }, + { + fieldtype: "Currency", + in_list_view: 1, + label: __("Difference Amount"), + fieldname: "difference_amount", + read_only: 1, + }, + ], }, { - fieldtype: 'HTML', - options: " New Journal Entry will be posted for the difference amount " - } + fieldtype: "HTML", + options: " New Journal Entry will be posted for the difference amount ", + }, ], primary_action: () => { const args = dialog.get_values()["allocation"]; - args.forEach(d => { - frappe.model.set_value("Payment Reconciliation Allocation", d.docname, - "difference_account", d.difference_account); - frappe.model.set_value("Payment Reconciliation Allocation", d.docname, - "gain_loss_posting_date", d.gain_loss_posting_date); - + args.forEach((d) => { + frappe.model.set_value( + "Payment Reconciliation Allocation", + d.docname, + "difference_account", + d.difference_account + ); + frappe.model.set_value( + "Payment Reconciliation Allocation", + d.docname, + "gain_loss_posting_date", + d.gain_loss_posting_date + ); }); this.reconcile_payment_entries(); dialog.hide(); }, - primary_action_label: __('Reconcile Entries') + primary_action_label: __("Reconcile Entries"), }); - this.frm.doc.allocation.forEach(d => { + this.frm.doc.allocation.forEach((d) => { if (d.difference_amount) { dialog.fields_dict.allocation.df.data.push({ - 'docname': d.name, - 'reference_name': d.reference_name, - 'difference_amount': d.difference_amount, - 'difference_account': d.difference_account, - 'gain_loss_posting_date': d.gain_loss_posting_date + docname: d.name, + reference_name: d.reference_name, + difference_amount: d.difference_amount, + difference_account: d.difference_account, + gain_loss_posting_date: d.gain_loss_posting_date, }); } }); @@ -314,41 +350,39 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo reconcile_payment_entries() { return this.frm.call({ doc: this.frm.doc, - method: 'reconcile', + method: "reconcile", callback: () => { this.frm.clear_table("allocation"); this.frm.refresh(); - } + }, }); } }; -frappe.ui.form.on('Payment Reconciliation Allocation', { - allocated_amount: function(frm, cdt, cdn) { +frappe.ui.form.on("Payment Reconciliation Allocation", { + allocated_amount: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; // filter invoice - let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number)); + let invoice = frm.doc.invoices.filter((x) => x.invoice_number == row.invoice_number); // filter payment - let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name)); + let payment = frm.doc.payments.filter((x) => x.reference_name == row.reference_name); frm.call({ doc: frm.doc, - method: 'calculate_difference_on_allocation_change', + method: "calculate_difference_on_allocation_change", args: { payment_entry: payment, invoice: invoice, - allocated_amount: row.allocated_amount + allocated_amount: row.allocated_amount, }, callback: (r) => { if (r.message) { row.difference_amount = r.message; frm.refresh(); } - } + }, }); - } + }, }); - - -extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm})); +extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({ frm: cur_frm })); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index e8985de4e1e..948bae3dfe6 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -24,7 +24,9 @@ "invoice_limit", "payment_limit", "bank_cash_account", + "accounting_dimensions_section", "cost_center", + "dimension_col_break", "sec_break1", "invoice_name", "invoices", @@ -199,6 +201,18 @@ "fieldname": "payment_name", "fieldtype": "Data", "label": "Filter on Payment" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: doc.invoices.length == 0", + "depends_on": "eval:doc.receivable_payable_account", + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions Filter" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, @@ -206,7 +220,7 @@ "is_virtual": 1, "issingle": 1, "links": [], - "modified": "2023-11-17 17:33:55.701726", + "modified": "2023-12-14 13:38:16.264013", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 2b98baf2210..b0347514905 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today import erpnext +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import ( is_any_doc_running, ) @@ -24,10 +25,11 @@ from erpnext.controllers.accounts_controller import get_advance_payment_entries_ class PaymentReconciliation(Document): def __init__(self, *args, **kwargs): - super(PaymentReconciliation, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.common_filter_conditions = [] self.accounting_dimension_filter_conditions = [] self.ple_posting_date_filter = [] + self.dimensions = get_dimensions()[0] def load_from_db(self): # 'modified' attribute is required for `run_doc_method` to work properly. @@ -110,7 +112,7 @@ class PaymentReconciliation(Document): def get_payment_entries(self): order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" - condition = self.get_conditions(get_payments=True) + condition = self.get_payment_entry_conditions() payment_entries = get_advance_payment_entries_for_regional( self.party_type, @@ -126,66 +128,67 @@ class PaymentReconciliation(Document): return payment_entries def get_jv_entries(self): - condition = self.get_conditions() + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + conditions = self.get_journal_filter_conditions() + + # Dimension filters + for x in self.dimensions: + dimension = x.fieldname + if self.get(dimension): + conditions.append(jea[dimension] == self.get(dimension)) if self.payment_name: - condition += f" and t1.name like '%%{self.payment_name}%%'" + conditions.append(je.name.like(f"%%{self.payment_name}%%")) if self.get("cost_center"): - condition += f" and t2.cost_center = '{self.cost_center}' " + conditions.append(jea.cost_center == self.cost_center) dr_or_cr = ( "credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit_in_account_currency" ) + conditions.append(jea[dr_or_cr].gt(0)) - bank_account_condition = ( - "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" + if self.bank_cash_account: + conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%")) + + journal_query = ( + qb.from_(je) + .inner_join(jea) + .on(jea.parent == je.name) + .select( + ConstantColumn("Journal Entry").as_("reference_type"), + je.name.as_("reference_name"), + je.posting_date, + je.remark.as_("remarks"), + jea.name.as_("reference_row"), + jea[dr_or_cr].as_("amount"), + jea.is_advance, + jea.exchange_rate, + jea.account_currency.as_("currency"), + jea.cost_center.as_("cost_center"), + ) + .where( + (je.docstatus == 1) + & (jea.party_type == self.party_type) + & (jea.party == self.party) + & (jea.account == self.receivable_payable_account) + & ( + (jea.reference_type == "") + | (jea.reference_type.isnull()) + | (jea.reference_type.isin(("Sales Order", "Purchase Order"))) + ) + ) + .where(Criterion.all(conditions)) + .orderby(je.posting_date) ) - limit = f"limit {self.payment_limit}" if self.payment_limit else " " + if self.payment_limit: + journal_query = journal_query.limit(self.payment_limit) - # nosemgrep - journal_entries = frappe.db.sql( - """ - select - "Journal Entry" as reference_type, t1.name as reference_name, - t1.posting_date, t1.remark as remarks, t2.name as reference_row, - {dr_or_cr} as amount, t2.is_advance, t2.exchange_rate, - t2.account_currency as currency, t2.cost_center as cost_center - from - `tabJournal Entry` t1, `tabJournal Entry Account` t2 - where - t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1 - and t2.party_type = %(party_type)s and t2.party = %(party)s - and t2.account = %(account)s and {dr_or_cr} > 0 {condition} - and (t2.reference_type is null or t2.reference_type = '' or - (t2.reference_type in ('Sales Order', 'Purchase Order') - and t2.reference_name is not null and t2.reference_name != '')) - and (CASE - WHEN t1.voucher_type in ('Debit Note', 'Credit Note') - THEN 1=1 - ELSE {bank_account_condition} - END) - order by t1.posting_date - {limit} - """.format( - **{ - "dr_or_cr": dr_or_cr, - "bank_account_condition": bank_account_condition, - "condition": condition, - "limit": limit, - } - ), - { - "party_type": self.party_type, - "party": self.party, - "account": self.receivable_payable_account, - "bank_cash_account": "%%%s%%" % self.bank_cash_account, - }, - as_dict=1, - ) + journal_entries = journal_query.run(as_dict=True) return list(journal_entries) @@ -216,7 +219,6 @@ class PaymentReconciliation(Document): self.return_invoices = self.return_invoices_query.run(as_dict=True) def get_dr_or_cr_notes(self): - self.build_qb_filter_conditions(get_return_invoices=True) ple = qb.DocType("Payment Ledger Entry") @@ -228,20 +230,18 @@ class PaymentReconciliation(Document): self.common_filter_conditions.append(ple.account == self.receivable_payable_account) self.get_return_invoices() - return_invoices = [ - x for x in self.return_invoices if x.return_against == None or x.return_against == "" - ] outstanding_dr_or_cr = [] - if return_invoices: + if self.return_invoices: ple_query = QueryPaymentLedger() return_outstanding = ple_query.get_voucher_outstandings( - vouchers=return_invoices, + vouchers=self.return_invoices, common_filter=self.common_filter_conditions, posting_date=self.ple_posting_date_filter, min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None, max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None, get_payments=True, + accounting_dimensions=self.accounting_dimension_filter_conditions, ) for inv in return_outstanding: @@ -339,9 +339,7 @@ class PaymentReconciliation(Document): payment_entry[0].get("reference_name") ) - new_difference_amount = self.get_difference_amount( - payment_entry[0], invoice[0], allocated_amount - ) + new_difference_amount = self.get_difference_amount(payment_entry[0], invoice[0], allocated_amount) return new_difference_amount @frappe.whitelist() @@ -391,8 +389,15 @@ class PaymentReconciliation(Document): row = self.append("allocation", {}) row.update(entry) + def update_dimension_values_in_allocated_entries(self, res): + for x in self.dimensions: + dimension = x.fieldname + if self.get(dimension): + res[dimension] = self.get(dimension) + return res + def get_allocated_entry(self, pay, inv, allocated_amount): - return frappe._dict( + res = frappe._dict( { "reference_type": pay.get("reference_type"), "reference_name": pay.get("reference_name"), @@ -408,6 +413,9 @@ class PaymentReconciliation(Document): } ) + res = self.update_dimension_values_in_allocated_entries(res) + return res + def reconcile_allocations(self, skip_ref_details_update_for_pe=False): adjust_allocations_for_taxes(self) dr_or_cr = ( @@ -430,10 +438,10 @@ class PaymentReconciliation(Document): reconciled_entry.append(payment_details) if entry_list: - reconcile_against_document(entry_list, skip_ref_details_update_for_pe) + reconcile_against_document(entry_list, skip_ref_details_update_for_pe, self.dimensions) if dr_or_cr_notes: - reconcile_dr_cr_note(dr_or_cr_notes, self.company) + reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions) @frappe.whitelist() def reconcile(self): @@ -449,9 +457,9 @@ class PaymentReconciliation(Document): if running_doc: frappe.throw( - _("A Reconciliation Job {0} is running for the same filters. Cannot reconcile now").format( - get_link_to_form("Auto Reconcile", running_doc) - ) + _( + "A Reconciliation Job {0} is running for the same filters. Cannot reconcile now" + ).format(get_link_to_form("Auto Reconcile", running_doc)) ) return @@ -462,7 +470,7 @@ class PaymentReconciliation(Document): self.get_unreconciled_entries() def get_payment_details(self, row, dr_or_cr): - return frappe._dict( + payment_details = frappe._dict( { "voucher_type": row.get("reference_type"), "voucher_no": row.get("reference_name"), @@ -485,6 +493,12 @@ class PaymentReconciliation(Document): } ) + for x in self.dimensions: + if row.get(x.fieldname): + payment_details[x.fieldname] = row.get(x.fieldname) + + return payment_details + def check_mandatory_to_fetch(self): for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: if not self.get(fieldname): @@ -538,9 +552,7 @@ class PaymentReconciliation(Document): invoice_exchange_map.update(purchase_invoice_map) - journals = [ - d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry" - ] + journals = [d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"] journals.extend( [d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"] ) @@ -549,7 +561,12 @@ class PaymentReconciliation(Document): journals_map = frappe._dict( frappe.db.get_all( "Journal Entry Account", - filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])}, + filters={ + "parent": ("in", journals), + "account": ("in", [self.receivable_payable_account]), + "party_type": self.party_type, + "party": self.party, + }, fields=[ "parent as `name`", "exchange_rate", @@ -592,6 +609,13 @@ class PaymentReconciliation(Document): if not invoices_to_reconcile: frappe.throw(_("No records found in Allocation table")) + def build_dimensions_filter_conditions(self): + ple = qb.DocType("Payment Ledger Entry") + for x in self.dimensions: + dimension = x.fieldname + if self.get(dimension): + self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension)) + def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): self.common_filter_conditions.clear() self.accounting_dimension_filter_conditions.clear() @@ -615,40 +639,58 @@ class PaymentReconciliation(Document): if self.to_payment_date: self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date)) - def get_conditions(self, get_payments=False): - condition = " and company = '{0}' ".format(self.company) + self.build_dimensions_filter_conditions() - if self.get("cost_center") and get_payments: - condition = " and cost_center = '{0}' ".format(self.cost_center) + def get_payment_entry_conditions(self): + conditions = [] + pe = qb.DocType("Payment Entry") + conditions.append(pe.company == self.company) - condition += ( - " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) - if self.from_payment_date - else "" - ) - condition += ( - " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) - if self.to_payment_date - else "" - ) + if self.get("cost_center"): + conditions.append(pe.cost_center == self.cost_center) + + if self.from_payment_date: + conditions.append(pe.posting_date.gte(self.from_payment_date)) + + if self.to_payment_date: + conditions.append(pe.posting_date.lte(self.to_payment_date)) if self.minimum_payment_amount: - condition += ( - " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) - if get_payments - else " and total_debit >= {0}".format(flt(self.minimum_payment_amount)) - ) + conditions.append(pe.unallocated_amount.gte(flt(self.minimum_payment_amount))) + if self.maximum_payment_amount: - condition += ( - " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) - if get_payments - else " and total_debit <= {0}".format(flt(self.maximum_payment_amount)) - ) + conditions.append(pe.unallocated_amount.lte(flt(self.maximum_payment_amount))) - return condition + # pass dynamic dimension filter values to payment query + for x in self.dimensions: + dimension = x.fieldname + if self.get(dimension): + conditions.append(pe[dimension] == self.get(dimension)) + + return conditions + + def get_journal_filter_conditions(self): + conditions = [] + je = qb.DocType("Journal Entry") + qb.DocType("Journal Entry Account") + conditions.append(je.company == self.company) + + if self.from_payment_date: + conditions.append(je.posting_date.gte(self.from_payment_date)) + + if self.to_payment_date: + conditions.append(je.posting_date.lte(self.to_payment_date)) + + if self.minimum_payment_amount: + conditions.append(je.total_debit.gte(self.minimum_payment_amount)) + + if self.maximum_payment_amount: + conditions.append(je.total_debit.lte(self.maximum_payment_amount)) + + return conditions -def reconcile_dr_cr_note(dr_cr_notes, company): +def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None): for inv in dr_cr_notes: voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" @@ -698,6 +740,15 @@ def reconcile_dr_cr_note(dr_cr_notes, company): } ) + # Credit Note(JE) will inherit the same dimension values as payment + dimensions_dict = frappe._dict() + if active_dimensions: + for dim in active_dimensions: + dimensions_dict[dim.fieldname] = inv.get(dim.fieldname) + + jv.accounts[0].update(dimensions_dict) + jv.accounts[1].update(dimensions_dict) + jv.flags.ignore_mandatory = True jv.flags.skip_remarks_creation = True jv.flags.ignore_exchange_rate = True @@ -731,9 +782,27 @@ def reconcile_dr_cr_note(dr_cr_notes, company): inv.against_voucher, None, inv.cost_center, + dimensions_dict, ) @erpnext.allow_regional def adjust_allocations_for_taxes(doc): pass + + +@frappe.whitelist() +def get_queries_for_dimension_filters(company: str | None = None): + dimensions_with_filters = [] + for d in get_dimensions()[0]: + filters = {} + meta = frappe.get_meta(d.document_type) + if meta.has_field("company") and company: + filters.update({"company": company}) + + if meta.is_tree: + filters.update({"is_group": 0}) + + dimensions_with_filters.append({"fieldname": d.fieldname, "filters": filters}) + + return dimensions_with_filters diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index d7a73f0ce71..22f05957a79 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -1,7 +1,6 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe from frappe import qb @@ -56,6 +55,7 @@ class TestPaymentReconciliation(FrappeTestCase): self.expense_account = "Cost of Goods Sold - _PR" self.debit_to = "Debtors - _PR" self.creditors = "Creditors - _PR" + self.cash = "Cash - _PR" # create bank account if frappe.db.exists("Account", "HDFC - _PR"): @@ -126,11 +126,14 @@ class TestPaymentReconciliation(FrappeTestCase): setattr(self, x.attribute, acc.name) def create_sales_invoice( - self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False ): """ Helper function to populate default values in sales invoice """ + if posting_date is None: + posting_date = nowdate() + sinv = create_sales_invoice( qty=qty, rate=rate, @@ -154,10 +157,13 @@ class TestPaymentReconciliation(FrappeTestCase): ) return sinv - def create_payment_entry(self, amount=100, posting_date=nowdate(), customer=None): + def create_payment_entry(self, amount=100, posting_date=None, customer=None): """ Helper function to populate default values in payment entry """ + if posting_date is None: + posting_date = nowdate() + payment = create_payment_entry( company=self.company, payment_type="Receive", @@ -171,11 +177,14 @@ class TestPaymentReconciliation(FrappeTestCase): return payment def create_purchase_invoice( - self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False ): """ Helper function to populate default values in sales invoice """ + if posting_date is None: + posting_date = nowdate() + pinv = make_purchase_invoice( qty=qty, rate=rate, @@ -200,11 +209,14 @@ class TestPaymentReconciliation(FrappeTestCase): return pinv def create_purchase_order( - self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False ): """ Helper function to populate default values in sales invoice """ + if posting_date is None: + posting_date = nowdate() + pord = create_purchase_order( qty=qty, rate=rate, @@ -249,9 +261,7 @@ class TestPaymentReconciliation(FrappeTestCase): pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() return pr - def create_journal_entry( - self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None - ): + def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None): je = frappe.new_doc("Journal Entry") je.posting_date = posting_date or nowdate() je.company = self.company @@ -401,7 +411,7 @@ class TestPaymentReconciliation(FrappeTestCase): rate = 100 invoices = [] payments = [] - for i in range(5): + for _i in range(5): invoices.append(self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date)) pe = self.create_payment_entry(amount=rate, posting_date=transaction_date).save().submit() payments.append(pe) @@ -486,6 +496,91 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(len(pr.get("invoices")), 0) self.assertEqual(len(pr.get("payments")), 0) + def test_payment_against_foreign_currency_journal(self): + transaction_date = nowdate() + + self.supplier = "_Test Supplier USD" + self.supplier2 = make_supplier("_Test Supplier2 USD", "USD") + amount = 100 + exc_rate1 = 80 + exc_rate2 = 83 + + je = frappe.new_doc("Journal Entry") + je.posting_date = transaction_date + je.company = self.company + je.user_remark = "test" + je.multi_currency = 1 + je.set( + "accounts", + [ + { + "account": self.creditors_usd, + "party_type": "Supplier", + "party": self.supplier, + "exchange_rate": exc_rate1, + "cost_center": self.cost_center, + "credit": amount * exc_rate1, + "credit_in_account_currency": amount, + }, + { + "account": self.creditors_usd, + "party_type": "Supplier", + "party": self.supplier2, + "exchange_rate": exc_rate2, + "cost_center": self.cost_center, + "credit": amount * exc_rate2, + "credit_in_account_currency": amount, + }, + { + "account": self.expense_account, + "cost_center": self.cost_center, + "debit": (amount * exc_rate1) + (amount * exc_rate2), + "debit_in_account_currency": (amount * exc_rate1) + (amount * exc_rate2), + }, + ], + ) + je.save().submit() + + pe = self.create_payment_entry(amount=amount, posting_date=transaction_date) + pe.payment_type = "Pay" + pe.party_type = "Supplier" + pe.party = self.supplier + pe.paid_to = self.creditors_usd + pe.paid_from = self.cash + pe.paid_amount = 8000 + pe.received_amount = 100 + pe.target_exchange_rate = exc_rate1 + pe.paid_to_account_currency = "USD" + pe.save().submit() + + pr = self.create_payment_reconciliation(party_is_customer=False) + pr.receivable_payable_account = self.creditors_usd + pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount + pr.from_invoice_date = pr.to_invoice_date = transaction_date + pr.from_payment_date = pr.to_payment_date = transaction_date + + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # There should no difference_amount as the Journal and Payment have same exchange rate - 'exc_rate1' + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + + pr.reconcile() + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 0) + self.assertEqual(len(pr.get("payments")), 0) + + journals = frappe.db.get_all( + "Journal Entry Account", + filters={"reference_type": je.doctype, "reference_name": je.name, "docstatus": 1}, + fields=["parent"], + ) + self.assertEqual([], journals) + def test_journal_against_invoice(self): transaction_date = nowdate() amount = 100 @@ -591,6 +686,70 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(si.status, "Paid") self.assertEqual(si.outstanding_amount, 0) + def test_invoice_status_after_cr_note_cancellation(self): + # This test case is made after the 'always standalone Credit/Debit notes' feature is introduced + transaction_date = nowdate() + amount = 100 + + si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + + cr_note = self.create_sales_invoice( + qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True + ) + cr_note.is_return = 1 + cr_note.return_against = si.name + cr_note = cr_note.save().submit() + + pr = self.create_payment_reconciliation() + + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + pr.get_unreconciled_entries() + self.assertEqual(pr.get("invoices"), []) + self.assertEqual(pr.get("payments"), []) + + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "docstatus": 1, + "voucher_type": "Credit Note", + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="name", + ) + self.assertEqual(len(journals), 1) + + # assert status and outstanding + si.reload() + self.assertEqual(si.status, "Credit Note Issued") + self.assertEqual(si.outstanding_amount, 0) + + cr_note.reload() + cr_note.cancel() + # 'Credit Note' Journal should be auto cancelled + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "docstatus": 1, + "voucher_type": "Credit Note", + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="name", + ) + self.assertEqual(len(journals), 0) + # assert status and outstanding + si.reload() + self.assertEqual(si.status, "Unpaid") + self.assertEqual(si.outstanding_amount, 100) + def test_cr_note_partial_against_invoice(self): transaction_date = nowdate() amount = 100 @@ -671,9 +830,7 @@ class TestPaymentReconciliation(FrappeTestCase): cr_note.cancel() - pay = self.create_payment_entry( - amount=amount, posting_date=transaction_date, customer=self.customer3 - ) + pay = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=self.customer3) pay.paid_from = self.debtors_eur pay.paid_from_account_currency = "EUR" pay.source_exchange_rate = exchange_rate @@ -875,9 +1032,7 @@ class TestPaymentReconciliation(FrappeTestCase): rate = 100 # 'Main - PR' Cost Center - si1 = self.create_sales_invoice( - qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True - ) + si1 = self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True) si1.cost_center = self.main_cc.name si1.submit() @@ -893,9 +1048,7 @@ class TestPaymentReconciliation(FrappeTestCase): je1 = je1.save().submit() # 'Sub - PR' Cost Center - si2 = self.create_sales_invoice( - qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True - ) + si2 = self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True) si2.cost_center = self.sub_cc.name si2.submit() @@ -980,6 +1133,17 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(pr.allocation[0].allocated_amount, 85) self.assertEqual(pr.allocation[0].difference_amount, 0) + pr.reconcile() + si.reload() + self.assertEqual(si.outstanding_amount, 0) + # No Exchange Gain/Loss journal should be generated + exc_gain_loss_journals = frappe.db.get_all( + "Journal Entry Account", + filters={"reference_type": si.doctype, "reference_name": si.name, "docstatus": 1}, + fields=["parent"], + ) + self.assertEqual(exc_gain_loss_journals, []) + def test_reconciliation_purchase_invoice_against_return(self): self.supplier = "_Test Supplier USD" pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True) @@ -1184,3 +1348,17 @@ def make_customer(customer_name, currency=None): return customer.name else: return customer_name + + +def make_supplier(supplier_name, currency=None): + if not frappe.db.exists("Supplier", supplier_name): + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = supplier_name + supplier.type = "Individual" + supplier.supplier_group = "Local" + if currency: + supplier.default_currency = currency + supplier.save() + return supplier.name + else: + return supplier_name diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 491c67818df..3f85b213500 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -24,7 +24,9 @@ "difference_account", "exchange_rate", "currency", - "cost_center" + "accounting_dimensions_section", + "cost_center", + "dimension_col_break" ], "fields": [ { @@ -157,12 +159,21 @@ "fieldname": "gain_loss_posting_date", "fieldtype": "Date", "label": "Difference Posting Date" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-11-17 17:33:38.612615", + "modified": "2023-12-14 13:38:26.104150", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index e9139120283..e5a6040c735 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -1,83 +1,95 @@ -cur_frm.add_fetch("payment_gateway_account", "payment_account", "payment_account") -cur_frm.add_fetch("payment_gateway_account", "payment_gateway", "payment_gateway") -cur_frm.add_fetch("payment_gateway_account", "message", "message") +cur_frm.add_fetch("payment_gateway_account", "payment_account", "payment_account"); +cur_frm.add_fetch("payment_gateway_account", "payment_gateway", "payment_gateway"); +cur_frm.add_fetch("payment_gateway_account", "message", "message"); frappe.ui.form.on("Payment Request", { - setup: function(frm) { - frm.set_query("party_type", function() { + setup: function (frm) { + frm.set_query("party_type", function () { return { query: "erpnext.setup.doctype.party_type.party_type.get_party_type", }; }); - } -}) + }, +}); -frappe.ui.form.on("Payment Request", "onload", function(frm, dt, dn){ +frappe.ui.form.on("Payment Request", "onload", function (frm, dt, dn) { if (frm.doc.reference_doctype) { frappe.call({ - method:"erpnext.accounts.doctype.payment_request.payment_request.get_print_format_list", - args: {"ref_doctype": frm.doc.reference_doctype}, - callback:function(r){ - set_field_options("print_format", r.message["print_format"]) - } - }) + method: "erpnext.accounts.doctype.payment_request.payment_request.get_print_format_list", + args: { ref_doctype: frm.doc.reference_doctype }, + callback: function (r) { + set_field_options("print_format", r.message["print_format"]); + }, + }); } -}) +}); -frappe.ui.form.on("Payment Request", "refresh", function(frm) { - if(frm.doc.payment_request_type == 'Inward' && frm.doc.payment_channel !== "Phone" && - !in_list(["Initiated", "Paid"], frm.doc.status) && !frm.doc.__islocal && frm.doc.docstatus==1){ - frm.add_custom_button(__('Resend Payment Email'), function(){ +frappe.ui.form.on("Payment Request", "refresh", function (frm) { + if ( + frm.doc.payment_request_type == "Inward" && + frm.doc.payment_channel !== "Phone" && + !in_list(["Initiated", "Paid"], frm.doc.status) && + !frm.doc.__islocal && + frm.doc.docstatus == 1 + ) { + frm.add_custom_button(__("Resend Payment Email"), function () { frappe.call({ method: "erpnext.accounts.doctype.payment_request.payment_request.resend_payment_email", - args: {"docname": frm.doc.name}, + args: { docname: frm.doc.name }, freeze: true, freeze_message: __("Sending"), - callback: function(r){ - if(!r.exc) { + callback: function (r) { + if (!r.exc) { frappe.msgprint(__("Message Sent")); } - } + }, }); }); } - if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") { - frm.add_custom_button(__('Create Payment Entry'), function(){ + if ( + (!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && + frm.doc.status == "Initiated" + ) { + frm.add_custom_button(__("Create Payment Entry"), function () { frappe.call({ method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry", - args: {"docname": frm.doc.name}, + args: { docname: frm.doc.name }, freeze: true, - callback: function(r){ - if(!r.exc) { + callback: function (r) { + if (!r.exc) { var doc = frappe.model.sync(r.message); frappe.set_route("Form", r.message.doctype, r.message.name); } - } + }, }); }).addClass("btn-primary"); } }); -frappe.ui.form.on("Payment Request", "is_a_subscription", function(frm) { +frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) { frm.toggle_reqd("payment_gateway_account", frm.doc.is_a_subscription); frm.toggle_reqd("subscription_plans", frm.doc.is_a_subscription); if (frm.doc.is_a_subscription && frm.doc.reference_doctype && frm.doc.reference_name) { frappe.call({ method: "erpnext.accounts.doctype.payment_request.payment_request.get_subscription_details", - args: {"reference_doctype": frm.doc.reference_doctype, "reference_name": frm.doc.reference_name}, + args: { reference_doctype: frm.doc.reference_doctype, reference_name: frm.doc.reference_name }, freeze: true, - callback: function(data){ - if(!data.exc) { - $.each(data.message || [], function(i, v){ - var d = frappe.model.add_child(frm.doc, "Subscription Plan Detail", "subscription_plans"); + callback: function (data) { + if (!data.exc) { + $.each(data.message || [], function (i, v) { + var d = frappe.model.add_child( + frm.doc, + "Subscription Plan Detail", + "subscription_plans" + ); d.qty = v.qty; d.plan = v.plan; }); frm.refresh_field("subscription_plans"); } - } + }, }); } }); diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index f6653f87f0f..583735b1cc6 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -37,7 +37,7 @@ class PaymentRequest(Document): self.status = "Draft" self.validate_reference_document() self.validate_payment_request_amount() - self.validate_currency() + # self.validate_currency() self.validate_subscription_details() def validate_reference_document(self): @@ -50,7 +50,7 @@ class PaymentRequest(Document): ) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart": + if not hasattr(ref_doc, "order_type") or ref_doc.order_type != "Shopping Cart": ref_amount = get_amount(ref_doc, self.payment_account) if existing_payment_request_amount + flt(self.grand_total) > ref_amount: @@ -103,7 +103,7 @@ class PaymentRequest(Document): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) if ( - hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart" + hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart" ) or self.flags.mute_email: send_mail = False @@ -155,7 +155,7 @@ class PaymentRequest(Document): def make_invoice(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart": + if hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart": from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice si = make_sales_invoice(self.reference_name, ignore_permissions=True) @@ -241,14 +241,10 @@ class PaymentRequest(Document): else: party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company) - party_account_currency = ref_doc.get("party_account_currency") or get_account_currency( - party_account - ) + party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account) bank_amount = self.grand_total - if ( - party_account_currency == ref_doc.company_currency and party_account_currency != self.currency - ): + if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") else: party_amount = self.grand_total @@ -266,7 +262,7 @@ class PaymentRequest(Document): "mode_of_payment": self.mode_of_payment, "reference_no": self.name, "reference_date": nowdate(), - "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format( + "remarks": "Payment Entry against {} {} via Payment Request {}".format( self.reference_doctype, self.reference_name, self.name ), } @@ -280,21 +276,17 @@ class PaymentRequest(Document): } ) + if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: + amount = payment_entry.base_paid_amount + else: + amount = self.grand_total + + payment_entry.received_amount = amount + payment_entry.get("references")[0].allocated_amount = amount + for dimension in get_accounting_dimensions(): payment_entry.update({dimension: self.get(dimension)}) - if payment_entry.difference_amount: - company_details = get_company_defaults(ref_doc.company) - - payment_entry.append( - "deductions", - { - "account": company_details.exchange_gain_loss_account, - "cost_center": company_details.cost_center, - "amount": payment_entry.difference_amount, - }, - ) - if submit: payment_entry.insert(ignore_permissions=True) payment_entry.submit() @@ -380,14 +372,13 @@ class PaymentRequest(Document): and hasattr(frappe.local, "session") and frappe.local.session.user != "Guest" ) and self.payment_channel != "Phone": - success_url = shopping_cart_settings.payment_success_url if success_url: redirect_to = ({"Orders": "/orders", "Invoices": "/invoices", "My Account": "/me"}).get( success_url, "/me" ) else: - redirect_to = get_url("/orders/{0}".format(self.reference_name)) + redirect_to = get_url(f"/orders/{self.reference_name}") return redirect_to @@ -413,15 +404,11 @@ def make_payment_request(**args): frappe.db.set_value( "Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False ) - frappe.db.set_value( - "Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False - ) + frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False) grand_total = grand_total - loyalty_amount bank_account = ( - get_party_bank_account(args.get("party_type"), args.get("party")) - if args.get("party_type") - else "" + get_party_bank_account(args.get("party_type"), args.get("party")) if args.get("party_type") else "" ) draft_payment_request = frappe.db.get_value( @@ -441,6 +428,12 @@ def make_payment_request(**args): pr = frappe.get_doc("Payment Request", draft_payment_request) else: pr = frappe.new_doc("Payment Request") + + if not args.get("payment_request_type"): + args["payment_request_type"] = ( + "Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward" + ) + pr.update( { "payment_gateway_account": gateway_account.get("name"), @@ -499,9 +492,9 @@ def get_amount(ref_doc, payment_account=None): elif dt in ["Sales Invoice", "Purchase Invoice"]: if not ref_doc.get("is_pos"): if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.outstanding_amount) + grand_total = flt(ref_doc.grand_total) else: - grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate + grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate elif dt == "Sales Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: diff --git a/erpnext/accounts/doctype/payment_request/payment_request_list.js b/erpnext/accounts/doctype/payment_request/payment_request_list.js index 85d729cd61c..183ca7c4584 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request_list.js +++ b/erpnext/accounts/doctype/payment_request/payment_request_list.js @@ -1,23 +1,19 @@ -frappe.listview_settings['Payment Request'] = { +frappe.listview_settings["Payment Request"] = { add_fields: ["status"], - get_indicator: function(doc) { - if(doc.status == "Draft") { + get_indicator: function (doc) { + if (doc.status == "Draft") { return [__("Draft"), "gray", "status,=,Draft"]; } - if(doc.status == "Requested") { + if (doc.status == "Requested") { return [__("Requested"), "green", "status,=,Requested"]; - } - else if(doc.status == "Initiated") { + } else if (doc.status == "Initiated") { return [__("Initiated"), "green", "status,=,Initiated"]; - } - else if(doc.status == "Partially Paid") { + } else if (doc.status == "Partially Paid") { return [__("Partially Paid"), "orange", "status,=,Partially Paid"]; - } - else if(doc.status == "Paid") { + } else if (doc.status == "Paid") { return [__("Paid"), "blue", "status,=,Paid"]; - } - else if(doc.status == "Cancelled") { + } else if (doc.status == "Cancelled") { return [__("Cancelled"), "red", "status,=,Cancelled"]; } - } -} + }, +}; diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index feb2fdffc95..932060895b0 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -86,6 +86,8 @@ class TestPaymentRequest(unittest.TestCase): pr = make_payment_request( dt="Purchase Invoice", dn=si_usd.name, + party_type="Supplier", + party="_Test Supplier USD", recipient_id="user@example.com", mute_email=1, payment_gateway_account="_Test Gateway - USD", @@ -93,11 +95,56 @@ class TestPaymentRequest(unittest.TestCase): return_doc=1, ) - pe = pr.create_payment_entry() + pr.create_payment_entry() pr.load_from_db() self.assertEqual(pr.status, "Paid") + def test_multiple_payment_entry_against_purchase_invoice(self): + purchase_invoice = make_purchase_invoice( + customer="_Test Supplier USD", + debit_to="_Test Payable USD - _TC", + currency="USD", + conversion_rate=50, + ) + + pr = make_payment_request( + dt="Purchase Invoice", + party_type="Supplier", + party="_Test Supplier USD", + dn=purchase_invoice.name, + recipient_id="user@example.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + return_doc=1, + ) + + pr.grand_total = pr.grand_total / 2 + + pr.submit() + pr.create_payment_entry() + + purchase_invoice.load_from_db() + self.assertEqual(purchase_invoice.status, "Partly Paid") + + pr = make_payment_request( + dt="Purchase Invoice", + party_type="Supplier", + party="_Test Supplier USD", + dn=purchase_invoice.name, + recipient_id="user@example.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + return_doc=1, + ) + + pr.save() + pr.submit() + pr.create_payment_entry() + + purchase_invoice.load_from_db() + self.assertEqual(purchase_invoice.status, "Paid") + def test_payment_entry(self): frappe.db.set_value( "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" @@ -158,7 +205,7 @@ class TestPaymentRequest(unittest.TestCase): self.assertTrue(gl_entries) - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[gle.account][0], gle.account) self.assertEqual(expected_gle[gle.account][1], gle.debit) self.assertEqual(expected_gle[gle.account][2], gle.credit) diff --git a/erpnext/accounts/doctype/payment_term/payment_term.js b/erpnext/accounts/doctype/payment_term/payment_term.js index feecf93484c..c1c9d7c3258 100644 --- a/erpnext/accounts/doctype/payment_term/payment_term.js +++ b/erpnext/accounts/doctype/payment_term/payment_term.js @@ -1,22 +1,24 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Payment Term', { +frappe.ui.form.on("Payment Term", { onload(frm) { - frm.trigger('set_dynamic_description'); + frm.trigger("set_dynamic_description"); }, discount(frm) { - frm.trigger('set_dynamic_description'); + frm.trigger("set_dynamic_description"); }, discount_type(frm) { - frm.trigger('set_dynamic_description'); + frm.trigger("set_dynamic_description"); }, set_dynamic_description(frm) { if (frm.doc.discount) { - let description = __("{0}% of total invoice value will be given as discount.", [frm.doc.discount]); - if (frm.doc.discount_type == 'Amount') { + let description = __("{0}% of total invoice value will be given as discount.", [ + frm.doc.discount, + ]); + if (frm.doc.discount_type == "Amount") { description = __("{0} will be given as discount.", [fmt_money(frm.doc.discount)]); } frm.set_df_property("discount", "description", description); } - } + }, }); diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js index 6046c13e146..b766c3b412b 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js @@ -1,12 +1,18 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Payment Terms Template', { - refresh: function(frm) { - frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms); +frappe.ui.form.on("Payment Terms Template", { + refresh: function (frm) { + frm.fields_dict.terms.grid.toggle_reqd( + "payment_term", + frm.doc.allocate_payment_based_on_payment_terms + ); }, - allocate_payment_based_on_payment_terms: function(frm) { - frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms); - } + allocate_payment_based_on_payment_terms: function (frm) { + frm.fields_dict.terms.grid.toggle_reqd( + "payment_term", + frm.doc.allocate_payment_based_on_payment_terms + ); + }, }); diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py index 7b04a68e89a..3cb1d87f13a 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py @@ -19,9 +19,7 @@ class PaymentTermsTemplate(Document): total_portion += flt(term.get("invoice_portion", 0)) if flt(total_portion, 2) != 100.00: - frappe.msgprint( - _("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red" - ) + frappe.msgprint(_("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red") def validate_terms(self): terms = [] diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js index e923d4ed5e6..82d8cb37fe7 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js @@ -1,38 +1,41 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Period Closing Voucher', { - onload: function(frm) { +frappe.ui.form.on("Period Closing Voucher", { + onload: function (frm) { if (!frm.doc.transaction_date) frm.doc.transaction_date = frappe.datetime.obj_to_str(new Date()); }, - setup: function(frm) { - frm.set_query("closing_account_head", function() { + setup: function (frm) { + frm.set_query("closing_account_head", function () { return { filters: [ - ['Account', 'company', '=', frm.doc.company], - ['Account', 'is_group', '=', '0'], - ['Account', 'freeze_account', '=', 'No'], - ['Account', 'root_type', 'in', 'Liability, Equity'] - ] - } + ["Account", "company", "=", frm.doc.company], + ["Account", "is_group", "=", "0"], + ["Account", "freeze_account", "=", "No"], + ["Account", "root_type", "in", "Liability, Equity"], + ], + }; }); }, - refresh: function(frm) { - 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": moment(frm.doc.modified).format('YYYY-MM-DD'), - "company": frm.doc.company, - "group_by": "", - "show_cancelled_entries": frm.doc.docstatus === 2 - }; - frappe.set_route("query-report", "General Ledger"); - }, "fa fa-table"); + refresh: function (frm) { + 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: moment(frm.doc.modified).format("YYYY-MM-DD"), + company: frm.doc.company, + group_by: "", + show_cancelled_entries: frm.doc.docstatus === 2, + }; + frappe.set_route("query-report", "General Ledger"); + }, + "fa fa-table" + ); } - } - -}) + }, +}); 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 674db6c2e43..c6ccb52f86d 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -47,7 +47,8 @@ class PeriodClosingVoucher(AccountsController): enqueue_after_commit=True, ) frappe.msgprint( - _("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True + _("The GL Entries will be cancelled in the background, it can take a few minutes."), + alert=True, ) else: make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name) @@ -89,9 +90,7 @@ class PeriodClosingVoucher(AccountsController): self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self ) - self.year_start_date = get_fiscal_year( - self.posting_date, self.fiscal_year, company=self.company - )[1] + self.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1] self.check_if_previous_year_closed() @@ -121,7 +120,8 @@ class PeriodClosingVoucher(AccountsController): previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True) if previous_fiscal_year and not frappe.db.exists( - "GL Entry", {"posting_date": ("<=", last_year_closing), "company": self.company} + "GL Entry", + {"posting_date": ("<=", last_year_closing), "company": self.company, "is_cancelled": 0}, ): return @@ -204,7 +204,9 @@ class PeriodClosingVoucher(AccountsController): "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, - "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, + "credit": abs(flt(acc.bal_in_company_currency)) + if flt(acc.bal_in_company_currency) > 0 + else 0, "is_period_closing_voucher_entry": 1, }, item=acc, @@ -228,7 +230,9 @@ class PeriodClosingVoucher(AccountsController): "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, - "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, + "credit": abs(flt(acc.bal_in_company_currency)) + if flt(acc.bal_in_company_currency) < 0 + else 0, "is_period_closing_voucher_entry": 1, }, item=acc, diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 91e71e90dd8..715800a7c16 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -1,36 +1,37 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('POS Closing Entry', { - onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log']; - frm.set_query("pos_profile", function(doc) { +frappe.ui.form.on("POS Closing Entry", { + onload: function (frm) { + frm.ignore_doctypes_on_cancel_all = ["POS Invoice Merge Log"]; + frm.set_query("pos_profile", function (doc) { return { - filters: { 'user': doc.user } + filters: { user: doc.user }, }; }); - frm.set_query("user", function(doc) { + frm.set_query("user", function (doc) { return { query: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_cashiers", - filters: { 'parent': doc.pos_profile } + filters: { parent: doc.pos_profile }, }; }); - frm.set_query("pos_opening_entry", function(doc) { - return { filters: { 'status': 'Open', 'docstatus': 1 } }; + frm.set_query("pos_opening_entry", function (doc) { + return { filters: { status: "Open", docstatus: 1 } }; }); - if (frm.doc.docstatus === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime()); + if (frm.doc.docstatus === 0 && !frm.doc.amended_from) + frm.set_value("period_end_date", frappe.datetime.now_datetime()); - frappe.realtime.on('closing_process_complete', async function(data) { + frappe.realtime.on("closing_process_complete", async function (data) { await frm.reload_doc(); - if (frm.doc.status == 'Failed' && frm.doc.error_message) { + if (frm.doc.status == "Failed" && frm.doc.error_message) { frappe.msgprint({ - title: __('POS Closing Failed'), + title: __("POS Closing Failed"), message: frm.doc.error_message, - indicator: 'orange', - clear: true + indicator: "orange", + clear: true, }); } }); @@ -47,23 +48,23 @@ frappe.ui.form.on('POS Closing Entry', { } }, - refresh: function(frm) { - if (frm.doc.docstatus == 1 && frm.doc.status == 'Failed') { + refresh: function (frm) { + if (frm.doc.docstatus == 1 && frm.doc.status == "Failed") { const issue = 'issue'; frm.dashboard.set_headline( - __('POS Closing failed while running in a background process. You can resolve the {0} and retry the process again.', [issue])); + __( + "POS Closing failed while running in a background process. You can resolve the {0} and retry the process again.", + [issue] + ) + ); - $('#jump_to_error').on('click', (e) => { + $("#jump_to_error").on("click", (e) => { e.preventDefault(); - frappe.utils.scroll_to( - cur_frm.get_field("error_message").$wrapper, - true, - 30 - ); + frappe.utils.scroll_to(cur_frm.get_field("error_message").$wrapper, true, 30); }); - frm.add_custom_button(__('Retry'), function () { - frm.call('retry', {}, () => { + frm.add_custom_button(__("Retry"), function () { + frm.call("retry", {}, () => { frm.reload_doc(); }); }); @@ -71,48 +72,54 @@ frappe.ui.form.on('POS Closing Entry', { }, pos_opening_entry(frm) { - if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) { + if ( + frm.doc.pos_opening_entry && + frm.doc.period_start_date && + frm.doc.period_end_date && + frm.doc.user + ) { reset_values(frm); frappe.run_serially([ () => frm.trigger("set_opening_amounts"), - () => frm.trigger("get_pos_invoices") + () => frm.trigger("get_pos_invoices"), ]); } }, set_opening_amounts(frm) { - return frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry) + return frappe.db + .get_doc("POS Opening Entry", frm.doc.pos_opening_entry) .then(({ balance_details }) => { - balance_details.forEach(detail => { + balance_details.forEach((detail) => { frm.add_child("payment_reconciliation", { mode_of_payment: detail.mode_of_payment, opening_amount: detail.opening_amount, - expected_amount: detail.opening_amount + expected_amount: detail.opening_amount, }); - }) + }); }); }, get_pos_invoices(frm) { return frappe.call({ - method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices', + method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices", args: { start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), pos_profile: frm.doc.pos_profile, - user: frm.doc.user + user: frm.doc.user, }, callback: (r) => { let pos_docs = r.message; set_form_data(pos_docs, frm); refresh_fields(frm); set_html_data(frm); - } + }, }); }, - before_save: async function(frm) { - frappe.dom.freeze(__('Processing Sales! Please Wait...')); + before_save: async function (frm) { + frappe.dom.freeze(__("Processing Sales! Please Wait...")); frm.set_value("grand_total", 0); frm.set_value("net_total", 0); @@ -125,12 +132,12 @@ frappe.ui.form.on('POS Closing Entry', { await Promise.all([ frappe.call({ - method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices', + method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices", args: { start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), pos_profile: frm.doc.pos_profile, - user: frm.doc.user + user: frm.doc.user, }, callback: (r) => { let pos_invoices = r.message; @@ -143,22 +150,22 @@ frappe.ui.form.on('POS Closing Entry', { refresh_fields(frm); set_html_data(frm); } - } - }) - ]) + }, + }), + ]); frappe.dom.unfreeze(); - } + }, }); -frappe.ui.form.on('POS Closing Entry Detail', { +frappe.ui.form.on("POS Closing Entry Detail", { closing_amount: (frm, cdt, cdn) => { const row = locals[cdt][cdn]; frappe.model.set_value(cdt, cdn, "difference", flt(row.closing_amount - row.expected_amount)); - } -}) + }, +}); function set_form_data(data, frm) { - data.forEach(d => { + data.forEach((d) => { add_to_pos_transaction(d, frm); frm.doc.grand_total += flt(d.grand_total); frm.doc.net_total += flt(d.net_total); @@ -173,13 +180,15 @@ function add_to_pos_transaction(d, frm) { pos_invoice: d.name, posting_date: d.posting_date, grand_total: d.grand_total, - customer: d.customer - }) + customer: d.customer, + }); } function refresh_payments(d, frm) { - d.payments.forEach(p => { - const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); + d.payments.forEach((p) => { + const payment = frm.doc.payment_reconciliation.find( + (pay) => pay.mode_of_payment === p.mode_of_payment + ); if (p.account == d.account_for_change_amount) { p.amount -= flt(d.change_amount); } @@ -191,25 +200,25 @@ function refresh_payments(d, frm) { mode_of_payment: p.mode_of_payment, opening_amount: 0, expected_amount: p.amount, - closing_amount: 0 - }) + closing_amount: 0, + }); } - }) + }); } function refresh_taxes(d, frm) { - d.taxes.forEach(t => { - const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate); + d.taxes.forEach((t) => { + const tax = frm.doc.taxes.find((tx) => tx.account_head === t.account_head && tx.rate === t.rate); if (tax) { tax.amount += flt(t.tax_amount); } else { frm.add_child("taxes", { account_head: t.account_head, rate: t.rate, - amount: t.tax_amount - }) + amount: t.tax_amount, + }); } - }) + }); } function reset_values(frm) { @@ -231,13 +240,13 @@ function refresh_fields(frm) { } function set_html_data(frm) { - if (frm.doc.docstatus === 1 && frm.doc.status == 'Submitted') { + if (frm.doc.docstatus === 1 && frm.doc.status == "Submitted") { frappe.call({ method: "get_payment_reconciliation_details", doc: frm.doc, callback: (r) => { frm.get_field("payment_reconciliation_details").$wrapper.html(r.message); - } + }, }); } } diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index 115b415eeda..f61bfdb58ab 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -33,7 +33,7 @@ class POSClosingEntry(StatusUpdater): for key, value in pos_occurences.items(): if len(value) > 1: error_list.append( - _("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value))) + _(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}") ) if error_list: @@ -128,9 +128,7 @@ def get_pos_invoices(start, end, pos_profile, user): as_dict=1, ) - data = list( - filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data) - ) + data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)) # need to get taxes and payments so can't avoid get_doc data = [frappe.get_doc("POS Invoice", d.name).as_dict() for d in data] @@ -201,7 +199,11 @@ def make_closing_entry_from_opening(opening_entry): else: payments.append( frappe._dict( - {"mode_of_payment": p.mode_of_payment, "opening_amount": 0, "expected_amount": p.amount} + { + "mode_of_payment": p.mode_of_payment, + "opening_amount": 0, + "expected_amount": p.amount, + } ) ) diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js index cffeb4d5351..29f00fbff7b 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js @@ -2,16 +2,15 @@ // License: GNU General Public License v3. See license.txt // render -frappe.listview_settings['POS Closing Entry'] = { - get_indicator: function(doc) { +frappe.listview_settings["POS Closing Entry"] = { + get_indicator: function (doc) { var status_color = { - "Draft": "red", - "Submitted": "blue", - "Queued": "orange", - "Failed": "red", - "Cancelled": "red" - + Draft: "red", + Submitted: "blue", + Queued: "orange", + Failed: "red", + Cancelled: "red", }; - return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; - } + return [__(doc.status), status_color[doc.status], "status,=," + doc.status]; + }, }; diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index de333cb9e8d..854523f1009 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -774,7 +774,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1562,7 +1562,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2023-11-20 12:27:12.848149", + "modified": "2024-03-20 16:00:34.268756", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index df5e221e195..4abe91680e8 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -13,7 +13,6 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_lo from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( SalesInvoice, - get_bank_cash_account, get_mode_of_payment_info, update_multi_mode_option, ) @@ -28,7 +27,7 @@ from erpnext.stock.doctype.serial_no.serial_no import ( class POSInvoice(SalesInvoice): def __init__(self, *args, **kwargs): - super(POSInvoice, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate(self): if not cint(self.is_pos): @@ -52,7 +51,6 @@ class POSInvoice(SalesInvoice): self.validate_stock_availablility() self.validate_return_items_qty() self.set_status() - self.set_account_for_mode_of_payment() self.validate_pos() self.validate_payment_amount() self.validate_loyalty_transaction() @@ -131,7 +129,9 @@ class POSInvoice(SalesInvoice): ) if paid_amt and pay.amount != paid_amt: - return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment)) + return frappe.throw( + _("Payment related to {0} is not completed").format(pay.mode_of_payment) + ) def validate_pos_reserved_serial_nos(self, item): serial_nos = get_serial_nos(item.serial_no) @@ -166,7 +166,7 @@ class POSInvoice(SalesInvoice): serial_nos = row.serial_no.split("\n") if serial_nos: - for key, value in collections.Counter(serial_nos).items(): + for _key, value in collections.Counter(serial_nos).items(): if value > 1: frappe.throw(_("Duplicate Serial No {0} found").format("key")) @@ -193,9 +193,7 @@ class POSInvoice(SalesInvoice): frappe.throw( _( "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" - ).format( - item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed - ), + ).format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"), ) @@ -251,7 +249,7 @@ class POSInvoice(SalesInvoice): available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse) - item_code, warehouse, qty = ( + item_code, warehouse, _qty = ( frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty), @@ -584,11 +582,6 @@ class POSInvoice(SalesInvoice): update_multi_mode_option(self, pos_profile) self.paid_amount = 0 - def set_account_for_mode_of_payment(self): - for pay in self.payments: - if not pay.account: - pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") - @frappe.whitelist() def create_payment_request(self): for pay in self.payments: @@ -705,7 +698,7 @@ def get_pos_reserved_qty(item_code, warehouse): reserved_qty = ( frappe.qb.from_(p_inv) .from_(p_item) - .select(Sum(p_item.qty).as_("qty")) + .select(Sum(p_item.stock_qty).as_("stock_qty")) .where( (p_inv.name == p_item.parent) & (IfNull(p_inv.consolidated_invoice, "") == "") @@ -716,7 +709,7 @@ def get_pos_reserved_qty(item_code, warehouse): ) ).run(as_dict=True) - return reserved_qty[0].qty or 0 if reserved_qty else 0 + return flt(reserved_qty[0].stock_qty) if reserved_qty else 0 @frappe.whitelist() diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js index 2dbf2a4fcd3..0379932bb7a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js @@ -2,40 +2,47 @@ // License: GNU General Public License v3. See license.txt // render -frappe.listview_settings['POS Invoice'] = { - add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company", - "currency", "is_return"], - get_indicator: function(doc) { +frappe.listview_settings["POS Invoice"] = { + add_fields: [ + "customer", + "customer_name", + "base_grand_total", + "outstanding_amount", + "due_date", + "company", + "currency", + "is_return", + ], + get_indicator: function (doc) { var status_color = { - "Draft": "red", - "Unpaid": "orange", - "Paid": "green", - "Submitted": "blue", - "Consolidated": "green", - "Return": "darkgrey", + Draft: "red", + Unpaid: "orange", + Paid: "green", + Submitted: "blue", + Consolidated: "green", + Return: "darkgrey", "Unpaid and Discounted": "orange", "Overdue and Discounted": "red", - "Overdue": "red" - + Overdue: "red", }; - return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + return [__(doc.status), status_color[doc.status], "status,=," + doc.status]; }, right_column: "grand_total", - onload: function(me) { - me.page.add_action_item('Make Merge Log', function() { + onload: function (me) { + me.page.add_action_item("Make Merge Log", function () { const invoices = me.get_checked_items(); frappe.call({ method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_merge_log", freeze: true, - args:{ - "invoices": invoices + args: { + invoices: invoices, }, callback: function (r) { if (r.message) { var doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); } - } + }, }); }); }, diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index bd2fee3b0ad..186c0ea7fd4 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -338,9 +338,7 @@ class TestPOSInvoice(unittest.TestCase): ) pos.set("payments", []) - pos.append( - "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 50} - ) + pos.append("payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 50}) pos.append( "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60, "default": 1} ) @@ -594,9 +592,7 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records create_records() - frappe.db.set_value( - "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty" - ) + frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") before_lp_details = get_loyalty_program_details_with_points( "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" ) @@ -670,9 +666,7 @@ class TestPOSInvoice(unittest.TestCase): consolidate_pos_invoices() pos_inv.load_from_db() - rounded_total = frappe.db.get_value( - "Sales Invoice", pos_inv.consolidated_invoice, "rounded_total" - ) + rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 3470) def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self): @@ -719,9 +713,7 @@ class TestPOSInvoice(unittest.TestCase): consolidate_pos_invoices() pos_inv.load_from_db() - rounded_total = frappe.db.get_value( - "Sales Invoice", pos_inv.consolidated_invoice, "rounded_total" - ) + rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 840) def test_merging_with_validate_selling_price(self): @@ -773,9 +765,7 @@ class TestPOSInvoice(unittest.TestCase): consolidate_pos_invoices() pos_inv2.load_from_db() - rounded_total = frappe.db.get_value( - "Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total" - ) + rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 400) def test_pos_batch_item_qty_validation(self): @@ -839,19 +829,19 @@ class TestPOSInvoice(unittest.TestCase): pos_inv = create_pos_invoice(qty=1, do_not_submit=1) pos_inv.items[0].rate = 300 pos_inv.save() - self.assertEquals(pos_inv.items[0].discount_percentage, 10) + self.assertEqual(pos_inv.items[0].discount_percentage, 10) # rate shouldn't change - self.assertEquals(pos_inv.items[0].rate, 405) + self.assertEqual(pos_inv.items[0].rate, 405) pos_inv.ignore_pricing_rule = 1 pos_inv.save() - self.assertEquals(pos_inv.ignore_pricing_rule, 1) + self.assertEqual(pos_inv.ignore_pricing_rule, 1) # rate should reset since pricing rules are ignored - self.assertEquals(pos_inv.items[0].rate, 450) + self.assertEqual(pos_inv.items[0].rate, 450) pos_inv.items[0].rate = 300 pos_inv.save() - self.assertEquals(pos_inv.items[0].rate, 300) + self.assertEqual(pos_inv.items[0].rate, 300) finally: item_price.delete() @@ -874,7 +864,7 @@ class TestPOSInvoice(unittest.TestCase): dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no") - self.assertEquals(delivery_document_no, dn.name) + self.assertEqual(delivery_document_no, dn.name) init_user_and_profile() diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js index 73c6290d7b0..8423987447e 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js @@ -1,21 +1,21 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('POS Invoice Merge Log', { - setup: function(frm) { - frm.set_query("pos_invoice", "pos_invoices", doc => { +frappe.ui.form.on("POS Invoice Merge Log", { + setup: function (frm) { + frm.set_query("pos_invoice", "pos_invoices", (doc) => { return { filters: { - 'docstatus': 1, - 'customer': doc.customer, - 'consolidated_invoice': '' - } - } + docstatus: 1, + customer: doc.customer, + consolidated_invoice: "", + }, + }; }); }, - merge_invoices_based_on: function(frm) { - frm.set_value('customer', ''); - frm.set_value('customer_group', ''); - } + merge_invoices_based_on: function (frm) { + frm.set_value("customer", ""); + frm.set_value("customer_group", ""); + }, }); diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 438ff9f3c4c..1302a0c386c 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -29,7 +29,7 @@ class POSInvoiceMergeLog(Document): for key, value in pos_occurences.items(): if len(value) > 1: error_list.append( - _("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value))) + _(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}") ) if error_list: @@ -56,7 +56,9 @@ class POSInvoiceMergeLog(Document): bold_pos_invoice = frappe.bold(d.pos_invoice) bold_status = frappe.bold(status) if docstatus != 1: - frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice)) + frappe.throw( + _("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice) + ) if status == "Consolidated": frappe.throw( _("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status) @@ -75,15 +77,17 @@ class POSInvoiceMergeLog(Document): d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated ) msg += " " - msg += _("Original invoice should be consolidated before or along with the return invoice.") + msg += _( + "Original invoice should be consolidated before or along with the return invoice." + ) msg += "

      " - msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against) + msg += _("You can add original invoice {} manually to proceed.").format( + bold_return_against + ) frappe.throw(msg) def on_submit(self): - pos_invoice_docs = [ - frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices - ] + pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] returns = [d for d in pos_invoice_docs if d.get("is_return") == 1] sales = [d for d in pos_invoice_docs if d.get("is_return") == 0] @@ -100,9 +104,7 @@ class POSInvoiceMergeLog(Document): self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note) def on_cancel(self): - pos_invoice_docs = [ - frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices - ] + pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] self.update_pos_invoices(pos_invoice_docs) self.cancel_linked_invoices() @@ -192,7 +194,9 @@ class POSInvoiceMergeLog(Document): for t in taxes: if t.account_head == tax.account_head and t.cost_center == tax.cost_center: t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount) - t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount) + t.base_tax_amount = flt(t.base_tax_amount) + flt( + tax.base_tax_amount_after_discount_amount + ) update_item_wise_tax_detail(t, tax) found = True if not found: @@ -292,9 +296,7 @@ def update_item_wise_tax_detail(consolidate_tax_row, tax_row): else: consolidated_tax_detail.update({item_code: [tax_data[0], tax_data[1]]}) - consolidate_tax_row.item_wise_tax_detail = json.dumps( - consolidated_tax_detail, separators=(",", ":") - ) + consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(",", ":")) def get_all_unconsolidated_invoices(): @@ -339,9 +341,7 @@ def consolidate_pos_invoices(pos_invoices=None, closing_entry=None): if len(invoices) >= 10 and closing_entry: closing_entry.set_status(update=True, status="Queued") - enqueue_job( - create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry - ) + enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry) else: create_merge_logs(invoice_by_customer, closing_entry) @@ -389,9 +389,7 @@ def split_invoices(invoices): if not item.serial_no: continue - return_against_is_added = any( - d for d in _invoices if d.pos_invoice == pos_invoice.return_against - ) + return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against) if return_against_is_added: break @@ -442,7 +440,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): if closing_entry: closing_entry.set_status(update=True, status="Failed") - if type(error_message) == list: + if isinstance(error_message, list): error_message = frappe.json.dumps(error_message) closing_entry.db_set("error_message", error_message) raise @@ -493,7 +491,7 @@ def enqueue_job(job, **kwargs): timeout=10000, event="processing_merge_logs", job_name=job_name, - now=frappe.conf.developer_mode or frappe.flags.in_test + now=frappe.conf.developer_mode or frappe.flags.in_test, ) if job == create_merge_logs: diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 9e696f18b6a..144523624e8 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -28,15 +28,11 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append( - "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200} - ) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) pos_inv2.submit() pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) - pos_inv3.append( - "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300} - ) + pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) pos_inv3.submit() consolidate_pos_invoices() @@ -65,15 +61,11 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append( - "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200} - ) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) pos_inv2.submit() pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) - pos_inv3.append( - "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300} - ) + pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) pos_inv3.submit() pos_inv_cn = make_sales_return(pos_inv.name) @@ -309,7 +301,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): init_user_and_profile() item_rates = [69, 59, 29] - for i in [1, 2]: + for _i in [1, 2]: inv = create_pos_invoice(is_return=1, do_not_save=1) inv.items = [] for rate in item_rates: diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js index d23f348f04e..6a316d55e6a 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js +++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js @@ -1,56 +1,55 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('POS Opening Entry', { +frappe.ui.form.on("POS Opening Entry", { setup(frm) { if (frm.doc.docstatus == 0) { - frm.trigger('set_posting_date_read_only'); - frm.set_value('period_start_date', frappe.datetime.now_datetime()); - frm.set_value('user', frappe.session.user); + frm.trigger("set_posting_date_read_only"); + frm.set_value("period_start_date", frappe.datetime.now_datetime()); + frm.set_value("user", frappe.session.user); } - frm.set_query("user", function(doc) { + frm.set_query("user", function (doc) { return { query: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_cashiers", - filters: { 'parent': doc.pos_profile } + filters: { parent: doc.pos_profile }, }; }); }, refresh(frm) { // set default posting date / time - if(frm.doc.docstatus == 0) { - if(!frm.doc.posting_date) { - frm.set_value('posting_date', frappe.datetime.nowdate()); + if (frm.doc.docstatus == 0) { + if (!frm.doc.posting_date) { + frm.set_value("posting_date", frappe.datetime.nowdate()); } - frm.trigger('set_posting_date_read_only'); + frm.trigger("set_posting_date_read_only"); } }, set_posting_date_read_only(frm) { - if(frm.doc.docstatus == 0 && frm.doc.set_posting_date) { - frm.set_df_property('posting_date', 'read_only', 0); + if (frm.doc.docstatus == 0 && frm.doc.set_posting_date) { + frm.set_df_property("posting_date", "read_only", 0); } else { - frm.set_df_property('posting_date', 'read_only', 1); + frm.set_df_property("posting_date", "read_only", 1); } }, set_posting_date(frm) { - frm.trigger('set_posting_date_read_only'); + frm.trigger("set_posting_date_read_only"); }, pos_profile: (frm) => { if (frm.doc.pos_profile) { - frappe.db.get_doc("POS Profile", frm.doc.pos_profile) - .then(({ payments }) => { - if (payments.length) { - frm.doc.balance_details = []; - payments.forEach(({ mode_of_payment }) => { - frm.add_child("balance_details", { mode_of_payment }); - }) - frm.refresh_field("balance_details"); - } - }); + frappe.db.get_doc("POS Profile", frm.doc.pos_profile).then(({ payments }) => { + if (payments.length) { + frm.doc.balance_details = []; + payments.forEach(({ mode_of_payment }) => { + frm.add_child("balance_details", { mode_of_payment }); + }); + frm.refresh_field("balance_details"); + } + }); } - } + }, }); diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js index 1ad3c919b71..7ff9c4bbbc0 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js +++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js @@ -2,15 +2,14 @@ // License: GNU General Public License v3. See license.txt // render -frappe.listview_settings['POS Opening Entry'] = { - get_indicator: function(doc) { +frappe.listview_settings["POS Opening Entry"] = { + get_indicator: function (doc) { var status_color = { - "Draft": "red", - "Open": "orange", - "Closed": "green", - "Cancelled": "red" - + Draft: "red", + Open: "orange", + Closed: "green", + Cancelled: "red", }; - return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; - } + return [__(doc.status), status_color[doc.status], "status,=," + doc.status]; + }, }; diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index e8aee737f29..2c032dd718b 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -114,10 +114,8 @@ class POSProfile(Document): condition = " where pfu.default = 1 " pos_view_users = frappe.db.sql_list( - """select pfu.user - from `tabPOS Profile User` as pfu {0}""".format( - condition - ) + f"""select pfu.user + from `tabPOS Profile User` as pfu {condition}""" ) for user in pos_view_users: @@ -144,10 +142,8 @@ def get_item_groups(pos_profile): def get_child_nodes(group_type, root): lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"]) return frappe.db.sql( - """ Select name, lft, rgt from `tab{tab}` where - lft >= {lft} and rgt <= {rgt} order by lft""".format( - tab=group_type, lft=lft, rgt=rgt - ), + f""" Select name, lft, rgt from `tab{group_type}` where + lft >= {lft} and rgt <= {rgt} order by lft""", as_dict=1, ) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index 788aa62701d..e03714bdffe 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -52,11 +52,9 @@ def get_customers_list(pos_profile=None): return ( frappe.db.sql( - """ select name, customer_name, customer_group, + f""" select name, customer_name, customer_group, territory, customer_pos_id from tabCustomer where disabled = 0 - and {cond}""".format( - cond=cond - ), + and {cond}""", tuple(customer_groups), as_dict=1, ) @@ -75,7 +73,7 @@ def get_items_list(pos_profile, company): cond = "and i.item_group in (%s)" % (", ".join(["%s"] * len(args_list))) return frappe.db.sql( - """ + f""" select i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no, i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image, @@ -88,10 +86,8 @@ def get_items_list(pos_profile, company): where i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 and i.is_fixed_asset = 0 {cond} - """.format( - cond=cond - ), - tuple([company] + args_list), + """, + tuple([company, *args_list]), as_dict=1, ) diff --git a/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.js b/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.js index f0884ebef5e..63416745394 100644 --- a/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.js +++ b/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.js @@ -1,6 +1,4 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('POS Profile User', { - -}); +frappe.ui.form.on("POS Profile User", {}); diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.js b/erpnext/accounts/doctype/pos_settings/pos_settings.js index 7d8f3562c8c..a2e5a57e5f7 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.js +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.js @@ -1,57 +1,91 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -let search_fields_datatypes = ['Data', 'Link', 'Dynamic Link', 'Long Text', 'Select', 'Small Text', 'Text', 'Text Editor']; -let do_not_include_fields = ["naming_series", "item_code", "item_name", "stock_uom", "asset_naming_series", - "default_material_request_type", "valuation_method", "warranty_period", "weight_uom", "batch_number_series", - "serial_no_series", "purchase_uom", "customs_tariff_number", "sales_uom", "deferred_revenue_account", - "deferred_expense_account", "quality_inspection_template", "route", "slideshow", "website_image_alt", "thumbnail", - "web_long_description"] +let search_fields_datatypes = [ + "Data", + "Link", + "Dynamic Link", + "Long Text", + "Select", + "Small Text", + "Text", + "Text Editor", +]; +let do_not_include_fields = [ + "naming_series", + "item_code", + "item_name", + "stock_uom", + "asset_naming_series", + "default_material_request_type", + "valuation_method", + "warranty_period", + "weight_uom", + "batch_number_series", + "serial_no_series", + "purchase_uom", + "customs_tariff_number", + "sales_uom", + "deferred_revenue_account", + "deferred_expense_account", + "quality_inspection_template", + "route", + "slideshow", + "website_image_alt", + "thumbnail", + "web_long_description", +]; -frappe.ui.form.on('POS Settings', { - onload: function(frm) { +frappe.ui.form.on("POS Settings", { + onload: function (frm) { frm.trigger("get_invoice_fields"); frm.trigger("add_search_options"); }, - get_invoice_fields: function(frm) { + get_invoice_fields: function (frm) { frappe.model.with_doctype("POS Invoice", () => { - var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) { - if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || ['Button'].includes(d.fieldtype)) { - return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; + var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function (d) { + if ( + frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || + ["Button"].includes(d.fieldtype) + ) { + return { label: d.label + " (" + d.fieldtype + ")", value: d.fieldname }; } else { return null; } }); frm.fields_dict.invoice_fields.grid.update_docfield_property( - 'fieldname', 'options', [""].concat(fields) + "fieldname", + "options", + [""].concat(fields) ); }); - }, - add_search_options: function(frm) { + add_search_options: function (frm) { frappe.model.with_doctype("Item", () => { - var fields = $.map(frappe.get_doc("DocType", "Item").fields, function(d) { - if (search_fields_datatypes.includes(d.fieldtype) && !(do_not_include_fields.includes(d.fieldname))) { + var fields = $.map(frappe.get_doc("DocType", "Item").fields, function (d) { + if ( + search_fields_datatypes.includes(d.fieldtype) && + !do_not_include_fields.includes(d.fieldname) + ) { return [d.label]; } else { return null; } }); - fields.unshift(''); - frm.fields_dict.pos_search_fields.grid.update_docfield_property('field', 'options', fields); + fields.unshift(""); + frm.fields_dict.pos_search_fields.grid.update_docfield_property("field", "options", fields); }); - - } + }, }); frappe.ui.form.on("POS Search Fields", { - field: function(frm, doctype, name) { + field: function (frm, doctype, name) { var doc = frappe.get_doc(doctype, name); - var df = $.map(frappe.get_doc("DocType", "Item").fields, function(d) { + var df = $.map(frappe.get_doc("DocType", "Item").fields, function (d) { if (doc.field == d.label && search_fields_datatypes.includes(d.fieldtype)) { return d; } else { @@ -61,13 +95,13 @@ frappe.ui.form.on("POS Search Fields", { doc.fieldname = df.fieldname; frm.refresh_field("fields"); - } + }, }); frappe.ui.form.on("POS Field", { - fieldname: function(frm, doctype, name) { + fieldname: function (frm, doctype, name) { var doc = frappe.get_doc(doctype, name); - var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) { + var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function (d) { return doc.fieldname == d.fieldname ? d : null; })[0]; @@ -77,5 +111,5 @@ frappe.ui.form.on("POS Field", { doc.fieldtype = df.fieldtype; doc.default_value = df.default; frm.refresh_field("fields"); - } + }, }); diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js index 826758245a3..cd5b00e6ad0 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js @@ -1,44 +1,43 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Pricing Rule', { - setup: function(frm) { - frm.fields_dict["for_price_list"].get_query = function(doc){ +frappe.ui.form.on("Pricing Rule", { + setup: function (frm) { + frm.fields_dict["for_price_list"].get_query = function (doc) { return { filters: { - 'selling': doc.selling, - 'buying': doc.buying, - 'currency': doc.currency - } + selling: doc.selling, + buying: doc.buying, + currency: doc.currency, + }, }; }; - ['items', 'item_groups', 'brands'].forEach(d => { - frm.fields_dict[d].grid.get_field('uom').get_query = function(doc, cdt, cdn){ + ["items", "item_groups", "brands"].forEach((d) => { + frm.fields_dict[d].grid.get_field("uom").get_query = function (doc, cdt, cdn) { var row = locals[cdt][cdn]; return { - query:"erpnext.accounts.doctype.pricing_rule.pricing_rule.get_item_uoms", - filters: {'value': row[frappe.scrub(doc.apply_on)], apply_on: doc.apply_on} - } + query: "erpnext.accounts.doctype.pricing_rule.pricing_rule.get_item_uoms", + filters: { value: row[frappe.scrub(doc.apply_on)], apply_on: doc.apply_on }, + }; }; - }) + }); }, - onload: function(frm) { - if(frm.doc.__islocal && !frm.doc.applicable_for && (frm.doc.customer || frm.doc.supplier)) { - if(frm.doc.customer) { + onload: function (frm) { + if (frm.doc.__islocal && !frm.doc.applicable_for && (frm.doc.customer || frm.doc.supplier)) { + if (frm.doc.customer) { frm.doc.applicable_for = "Customer"; - frm.doc.selling = 1 + frm.doc.selling = 1; } else { frm.doc.applicable_for = "Supplier"; - frm.doc.buying = 1 + frm.doc.buying = 1; } } }, - refresh: function(frm) { - var help_content = - ` + refresh: function (frm) { + var help_content = `

      @@ -97,61 +96,70 @@ frappe.ui.form.on('Pricing Rule', {

      `; - frm.set_df_property('pricing_rule_help', 'options', help_content); + frm.set_df_property("pricing_rule_help", "options", help_content); frm.events.set_options_for_applicable_for(frm); frm.trigger("toggle_reqd_apply_on"); }, - apply_on: function(frm) { + apply_on: function (frm) { frm.trigger("toggle_reqd_apply_on"); }, - toggle_reqd_apply_on: function(frm) { + toggle_reqd_apply_on: function (frm) { const fields = { - 'Item Code': 'items', - 'Item Group': 'item_groups', - 'Brand': 'brands' - } + "Item Code": "items", + "Item Group": "item_groups", + Brand: "brands", + }; for (var key in fields) { - frm.toggle_reqd(fields[key], - frm.doc.apply_on === key ? 1 : 0); + frm.toggle_reqd(fields[key], frm.doc.apply_on === key ? 1 : 0); } }, - rate_or_discount: function(frm) { - if(frm.doc.rate_or_discount == 'Rate') { - frm.set_value('for_price_list', ""); + rate_or_discount: function (frm) { + if (frm.doc.rate_or_discount == "Rate") { + frm.set_value("for_price_list", ""); } }, - selling: function(frm) { + selling: function (frm) { frm.events.set_options_for_applicable_for(frm); }, - buying: function(frm) { + buying: function (frm) { frm.events.set_options_for_applicable_for(frm); }, //Dynamically change the description based on type of margin - margin_type: function(frm){ - frm.set_df_property('margin_rate_or_amount', 'description', frm.doc.margin_type=='Percentage'?'In Percentage %':'In Amount'); + margin_type: function (frm) { + frm.set_df_property( + "margin_rate_or_amount", + "description", + frm.doc.margin_type == "Percentage" ? "In Percentage %" : "In Amount" + ); }, - set_options_for_applicable_for: function(frm) { + set_options_for_applicable_for: function (frm) { var options = [""]; var applicable_for = frm.doc.applicable_for; - if(frm.doc.selling) { - options = $.merge(options, ["Customer", "Customer Group", "Territory", "Sales Partner", "Campaign"]); + if (frm.doc.selling) { + options = $.merge(options, [ + "Customer", + "Customer Group", + "Territory", + "Sales Partner", + "Campaign", + ]); } - if(frm.doc.buying) { + if (frm.doc.buying) { $.merge(options, ["Supplier", "Supplier Group"]); } set_field_options("applicable_for", options.join("\n")); - if(!in_list(options, applicable_for)) applicable_for = null; + if (!in_list(options, applicable_for)) applicable_for = null; frm.set_value("applicable_for", applicable_for); - } + }, }); diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index e2b015bf021..f39aa026b47 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -77,9 +77,9 @@ class PricingRule(Document): if self.priority and cint(self.priority) == 1: throw( - _("As the field {0} is enabled, the value of the field {1} should be more than 1.").format( - frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority") - ) + _( + "As the field {0} is enabled, the value of the field {1} should be more than 1." + ).format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")) ) def validate_applicable_for_selling_or_buying(self): @@ -273,9 +273,7 @@ def apply_pricing_rule(args, doc=None): def get_serial_no_for_item(args): from erpnext.stock.get_item_details import get_serial_no - item_details = frappe._dict( - {"doctype": args.doctype, "name": args.name, "serial_no": args.serial_no} - ) + item_details = frappe._dict({"doctype": args.doctype, "name": args.name, "serial_no": args.serial_no}) if args.get("parenttype") in ("Sales Invoice", "Delivery Note") and flt(args.stock_qty) > 0: item_details.serial_no = get_serial_no(args) return item_details @@ -373,9 +371,11 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False): ) if pricing_rule.apply_rule_on_other_items: - item_details["apply_rule_on_other_items"] = json.dumps(pricing_rule.apply_rule_on_other_items) + item_details["apply_rule_on_other_items"] = json.dumps( + pricing_rule.apply_rule_on_other_items + ) - if pricing_rule.coupon_code_based == 1 and args.coupon_code == None: + if pricing_rule.coupon_code_based == 1 and args.coupon_code is None: return item_details if not pricing_rule.validate_applied_rule: @@ -419,7 +419,6 @@ def update_args_for_pricing_rule(args): if args.transaction_type == "selling": if args.customer and not (args.customer_group and args.territory): - if args.quotation_to and args.quotation_to != "Customer": customer = frappe._dict() else: @@ -450,9 +449,9 @@ def get_pricing_rule_details(args, pricing_rule): def apply_price_discount_rule(pricing_rule, item_details, args): item_details.pricing_rule_for = pricing_rule.rate_or_discount - if ( - pricing_rule.margin_type in ["Amount", "Percentage"] and pricing_rule.currency == args.currency - ) or (pricing_rule.margin_type == "Percentage"): + if (pricing_rule.margin_type in ["Amount", "Percentage"] and pricing_rule.currency == args.currency) or ( + pricing_rule.margin_type == "Percentage" + ): item_details.margin_type = pricing_rule.margin_type item_details.has_margin = True @@ -595,7 +594,7 @@ def get_item_uoms(doctype, txt, searchfield, start, page_len, filters): return frappe.get_all( "UOM Conversion Detail", - filters={"parent": ("in", items), "uom": ("like", "{0}%".format(txt))}, + filters={"parent": ("in", items), "uom": ("like", f"{txt}%")}, fields=["distinct uom"], as_list=1, ) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 7f8cc4c63e0..ab52d6c9729 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -103,8 +103,6 @@ class TestPricingRule(unittest.TestCase): self.assertEqual(details.get("discount_percentage"), 15) def test_pricing_rule_for_margin(self): - from frappe import MandatoryError - from erpnext.stock.get_item_details import get_item_details test_record = { @@ -205,8 +203,6 @@ class TestPricingRule(unittest.TestCase): self.assertEqual(details.get("discount_percentage"), 10) def test_pricing_rule_for_variants(self): - from frappe import MandatoryError - from erpnext.stock.get_item_details import get_item_details if not frappe.db.exists("Item", "Test Variant PRT"): @@ -1055,8 +1051,7 @@ def delete_existing_pricing_rules(): "Pricing Rule Item Group", "Pricing Rule Brand", ]: - - frappe.db.sql("delete from `tab{0}`".format(doctype)) + frappe.db.sql(f"delete from `tab{doctype}`") def make_item_price(item, price_list_name, item_price): diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 65d4595fcd5..ce96a3d1240 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -101,14 +101,12 @@ def _get_pricing_rules(apply_on, args, values): if not args.get(apply_on_field): return [] - child_doc = "`tabPricing Rule {0}`".format(apply_on) + child_doc = f"`tabPricing Rule {apply_on}`" conditions = item_variant_condition = item_conditions = "" values[apply_on_field] = args.get(apply_on_field) if apply_on_field in ["item_code", "brand"]: - item_conditions = "{child_doc}.{apply_on_field}= %({apply_on_field})s".format( - child_doc=child_doc, apply_on_field=apply_on_field - ) + item_conditions = f"{child_doc}.{apply_on_field}= %({apply_on_field})s" if apply_on_field == "item_code": if args.get("uom", None): @@ -121,9 +119,7 @@ def _get_pricing_rules(apply_on, args, values): args.variant_of = frappe.get_cached_value("Item", args.item_code, "variant_of") if args.variant_of: - item_variant_condition = " or {child_doc}.item_code=%(variant_of)s ".format( - child_doc=child_doc - ) + item_variant_condition = f" or {child_doc}.item_code=%(variant_of)s " values["variant_of"] = args.variant_of elif apply_on_field == "item_group": item_conditions = _get_tree_conditions(args, "Item Group", child_doc, False) @@ -131,7 +127,7 @@ def _get_pricing_rules(apply_on, args, values): conditions += get_other_conditions(conditions, values, args) warehouse_conditions = _get_tree_conditions(args, "Warehouse", "`tabPricing Rule`") if warehouse_conditions: - warehouse_conditions = " and {0}".format(warehouse_conditions) + warehouse_conditions = f" and {warehouse_conditions}" if not args.price_list: args.price_list = None @@ -157,7 +153,7 @@ def _get_pricing_rules(apply_on, args, values): item_variant_condition=item_variant_condition, transaction_type=args.transaction_type, warehouse_cond=warehouse_conditions, - apply_on_other_field="other_{0}".format(apply_on_field), + apply_on_other_field=f"other_{apply_on_field}", conditions=conditions, ), values, @@ -196,14 +192,13 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): frappe.throw(_("Invalid {0}").format(args.get(field))) parent_groups = frappe.db.sql_list( - """select name from `tab%s` - where lft<=%s and rgt>=%s""" - % (parenttype, "%s", "%s"), + """select name from `tab{}` + where lft<={} and rgt>={}""".format(parenttype, "%s", "%s"), (lft, rgt), ) if parenttype in ["Customer Group", "Item Group", "Territory"]: - parent_field = "parent_{0}".format(frappe.scrub(parenttype)) + parent_field = f"parent_{frappe.scrub(parenttype)}" root_name = frappe.db.get_list( parenttype, {"is_group": 1, parent_field: ("is", "not set")}, @@ -229,10 +224,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): def get_other_conditions(conditions, values, args): for field in ["company", "customer", "supplier", "campaign", "sales_partner"]: if args.get(field): - conditions += " and ifnull(`tabPricing Rule`.{0}, '') in (%({1})s, '')".format(field, field) + conditions += f" and ifnull(`tabPricing Rule`.{field}, '') in (%({field})s, '')" values[field] = args.get(field) else: - conditions += " and ifnull(`tabPricing Rule`.{0}, '') = ''".format(field) + conditions += f" and ifnull(`tabPricing Rule`.{field}, '') = ''" for parenttype in ["Customer Group", "Territory", "Supplier Group"]: group_condition = _get_tree_conditions(args, parenttype, "`tabPricing Rule`") @@ -504,7 +499,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None): "transaction_date" if frappe.get_meta(doctype).has_field("transaction_date") else "posting_date" ) - child_doctype = "{0} Item".format(doctype) + child_doctype = f"{doctype} Item" apply_on = frappe.scrub(pr_doc.get("apply_on")) values = [pr_doc.valid_from, pr_doc.valid_upto] @@ -514,30 +509,26 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None): warehouses = get_child_warehouses(pr_doc.warehouse) condition += """ and `tab{child_doc}`.warehouse in ({warehouses}) - """.format( - child_doc=child_doctype, warehouses=",".join(["%s"] * len(warehouses)) - ) + """.format(child_doc=child_doctype, warehouses=",".join(["%s"] * len(warehouses))) values.extend(warehouses) if items: - condition = " and `tab{child_doc}`.{apply_on} in ({items})".format( + condition += " and `tab{child_doc}`.{apply_on} in ({items})".format( child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items)) ) values.extend(items) data_set = frappe.db.sql( - """ SELECT `tab{child_doc}`.stock_qty, - `tab{child_doc}`.amount - FROM `tab{child_doc}`, `tab{parent_doc}` + f""" SELECT `tab{child_doctype}`.stock_qty, + `tab{child_doctype}`.amount + FROM `tab{child_doctype}`, `tab{doctype}` WHERE - `tab{child_doc}`.parent = `tab{parent_doc}`.name and `tab{parent_doc}`.{date_field} - between %s and %s and `tab{parent_doc}`.docstatus = 1 - {condition} group by `tab{child_doc}`.name - """.format( - parent_doc=doctype, child_doc=child_doctype, condition=condition, date_field=date_field - ), + `tab{child_doctype}`.parent = `tab{doctype}`.name and `tab{doctype}`.{date_field} + between %s and %s and `tab{doctype}`.docstatus = 1 + {condition} group by `tab{child_doctype}`.name + """, tuple(values), as_dict=1, ) @@ -556,11 +547,9 @@ def apply_pricing_rule_on_transaction(doc): conditions = get_other_conditions(conditions, values, doc) pricing_rules = frappe.db.sql( - """ Select `tabPricing Rule`.* from `tabPricing Rule` + f""" Select `tabPricing Rule`.* from `tabPricing Rule` where {conditions} and `tabPricing Rule`.disable = 0 - """.format( - conditions=conditions - ), + """, values, as_dict=1, ) @@ -583,7 +572,9 @@ def apply_pricing_rule_on_transaction(doc): continue if ( - d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field) + d.validate_applied_rule + and doc.get(field) is not None + and doc.get(field) < d.get(pr_field) ): frappe.msgprint(_("User has not applied rule on the invoice {0}").format(doc.name)) else: @@ -643,9 +634,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): qty = pricing_rule.free_qty or 1 if pricing_rule.is_recursive: - transaction_qty = ( - args.get("qty") if args else doc.total_qty - ) - pricing_rule.apply_recursion_over + transaction_qty = (args.get("qty") if args else doc.total_qty) - pricing_rule.apply_recursion_over if transaction_qty: qty = flt(transaction_qty) * qty / pricing_rule.recurse_for if pricing_rule.round_free_qty: diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js index 1ec6805ae0c..92a6c6259f0 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js @@ -1,53 +1,58 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Process Deferred Accounting', { - setup: function(frm) { - frm.set_query("document_type", function() { +frappe.ui.form.on("Process Deferred Accounting", { + setup: function (frm) { + frm.set_query("document_type", function () { return { filters: { - 'name': ['in', ['Sales Invoice', 'Purchase Invoice']] - } + name: ["in", ["Sales Invoice", "Purchase Invoice"]], + }, }; }); }, - type: function(frm) { + type: function (frm) { if (frm.doc.company && frm.doc.type) { - frm.set_query("account", function() { + frm.set_query("account", function () { return { filters: { - 'company': frm.doc.company, - 'root_type': frm.doc.type === 'Income' ? 'Liability' : 'Asset', - 'is_group': 0 - } + company: frm.doc.company, + root_type: frm.doc.type === "Income" ? "Liability" : "Asset", + is_group: 0, + }, }; }); } }, - validate: function() { + validate: function () { return new Promise((resolve) => { - return frappe.db.get_single_value('Accounts Settings', 'automatically_process_deferred_accounting_entry') - .then(value => { - if(value) { - frappe.throw(__('Manual entry cannot be created! Disable automatic entry for deferred accounting in accounts settings and try again')); + return frappe.db + .get_single_value("Accounts Settings", "automatically_process_deferred_accounting_entry") + .then((value) => { + if (value) { + frappe.throw( + __( + "Manual entry cannot be created! Disable automatic entry for deferred accounting in accounts settings and try again" + ) + ); } resolve(value); }); }); }, - end_date: function(frm) { + end_date: function (frm) { if (frm.doc.end_date && frm.doc.end_date < frm.doc.start_date) { frappe.throw(__("End date cannot be before start date")); } }, - onload: function(frm) { + onload: function (frm) { if (frm.doc.posting_date && frm.doc.docstatus === 0) { - frm.set_value('start_date', frappe.datetime.add_months(frm.doc.posting_date, -1)); - frm.set_value('end_date', frm.doc.posting_date); + frm.set_value("start_date", frappe.datetime.add_months(frm.doc.posting_date, -1)); + frm.set_value("end_date", frm.doc.posting_date); } - } + }, }); diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index 263621dcf4d..fddd9f83926 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -40,7 +40,7 @@ class TestProcessDeferredAccounting(unittest.TestCase): si.save() si.submit() - process_deferred_accounting = doc = frappe.get_doc( + process_deferred_accounting = frappe.get_doc( dict( doctype="Process Deferred Accounting", posting_date="2023-07-01", diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js index dd601bfc451..0f52a4d998e 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js @@ -2,129 +2,128 @@ // For license information, please see license.txt frappe.ui.form.on("Process Payment Reconciliation", { - onload: function(frm) { + onload: function (frm) { // set queries - frm.set_query("party_type", function() { - return { - "filters": { - "name": ["in", Object.keys(frappe.boot.party_account_types)], - } - } - }); - frm.set_query('receivable_payable_account', function(doc) { + frm.set_query("party_type", function () { return { filters: { - "company": doc.company, - "is_group": 0, - "account_type": frappe.boot.party_account_types[doc.party_type] - } + name: ["in", Object.keys(frappe.boot.party_account_types)], + }, }; }); - frm.set_query('cost_center', function(doc) { + frm.set_query("receivable_payable_account", function (doc) { return { filters: { - "company": doc.company, - "is_group": 0, - } + company: doc.company, + is_group: 0, + account_type: frappe.boot.party_account_types[doc.party_type], + }, }; }); - frm.set_query('bank_cash_account', function(doc) { + frm.set_query("cost_center", function (doc) { return { - filters:[ - ['Account', 'company', '=', doc.company], - ['Account', 'is_group', '=', 0], - ['Account', 'account_type', 'in', ['Bank', 'Cash']] - ] + filters: { + company: doc.company, + is_group: 0, + }, + }; + }); + frm.set_query("bank_cash_account", function (doc) { + return { + filters: [ + ["Account", "company", "=", doc.company], + ["Account", "is_group", "=", 0], + ["Account", "account_type", "in", ["Bank", "Cash"]], + ], }; }); - }, - refresh: function(frm) { - if (frm.doc.docstatus==1 && ['Queued', 'Paused'].find(x => x == frm.doc.status)) { - let execute_btn = __("Start / Resume") + refresh: function (frm) { + if (frm.doc.docstatus == 1 && ["Queued", "Paused"].find((x) => x == frm.doc.status)) { + let execute_btn = __("Start / Resume"); frm.add_custom_button(execute_btn, () => { frm.call({ - method: 'erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_job_for_doc', + method: "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_job_for_doc", args: { - docname: frm.doc.name - } - }).then(r => { - if(!r.exc) { + docname: frm.doc.name, + }, + }).then((r) => { + if (!r.exc) { frappe.show_alert(__("Job Started")); frm.reload_doc(); } }); }); } - if (frm.doc.docstatus==1 && ['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) { + if ( + frm.doc.docstatus == 1 && + ["Completed", "Running", "Paused", "Partially Reconciled"].find((x) => x == frm.doc.status) + ) { frm.call({ - 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.get_reconciled_count", + method: "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.get_reconciled_count", args: { - "docname": frm.docname, - } - }).then(r => { + docname: frm.docname, + }, + }).then((r) => { if (r.message) { let progress = 0; let description = ""; if (r.message.processed) { - progress = (r.message.processed/r.message.total) * 100; - description = r.message.processed + "/" + r.message.total + " processed"; + progress = (r.message.processed / r.message.total) * 100; + description = r.message.processed + "/" + r.message.total + " processed"; } else if (r.message.total == 0 && frm.doc.status == "Completed") { progress = 100; } - - frm.dashboard.add_progress('Reconciliation Progress', progress, description); + frm.dashboard.add_progress("Reconciliation Progress", progress, description); } - }) + }); } - if (frm.doc.docstatus==1 && frm.doc.status == 'Running') { - let execute_btn = __("Pause") + if (frm.doc.docstatus == 1 && frm.doc.status == "Running") { + let execute_btn = __("Pause"); frm.add_custom_button(execute_btn, () => { frm.call({ - 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.pause_job_for_doc", + method: "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.pause_job_for_doc", args: { - "docname": frm.docname, - } - }).then(r => { + docname: frm.docname, + }, + }).then((r) => { if (!r.exc) { frappe.show_alert(__("Job Paused")); - frm.reload_doc() + frm.reload_doc(); } }); - }); } }, company(frm) { - frm.set_value('party', ''); - frm.set_value('receivable_payable_account', ''); + frm.set_value("party", ""); + frm.set_value("receivable_payable_account", ""); }, party_type(frm) { - frm.set_value('party', ''); + frm.set_value("party", ""); }, party(frm) { - frm.set_value('receivable_payable_account', ''); + frm.set_value("receivable_payable_account", ""); if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) { return frappe.call({ method: "erpnext.accounts.party.get_party_account", args: { company: frm.doc.company, party_type: frm.doc.party_type, - party: frm.doc.party + party: frm.doc.party, }, callback: (r) => { if (!r.exc && r.message) { frm.set_value("receivable_payable_account", r.message); } frm.refresh(); - - } + }, }); } - } + }, }); diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py index 31660306db0..c7535e76d7e 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -41,9 +41,7 @@ class ProcessPaymentReconciliation(Document): def on_cancel(self): self.db_set("status", "Cancelled") - log = frappe.db.get_value( - "Process Payment Reconciliation Log", filters={"process_pr": self.name} - ) + log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": self.name}) if log: frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Cancelled") @@ -129,7 +127,7 @@ def trigger_job_for_doc(docname: str | None = None): frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running") job_name = f"start_processing_{docname}" if not is_job_running(job_name): - job = frappe.enqueue( + frappe.enqueue( method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters", queue="long", is_async=True, @@ -147,7 +145,7 @@ def trigger_job_for_doc(docname: str | None = None): # Resume tasks for running doc job_name = f"start_processing_{docname}" if not is_job_running(job_name): - job = frappe.enqueue( + frappe.enqueue( method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters", queue="long", is_async=True, @@ -224,7 +222,7 @@ def reconcile_based_on_filters(doc: None | str = None) -> None: job_name = f"process_{doc}_fetch_and_allocate" if not is_job_running(job_name): - job = frappe.enqueue( + frappe.enqueue( method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate", queue="long", timeout="3600", @@ -245,7 +243,7 @@ def reconcile_based_on_filters(doc: None | str = None) -> None: if not allocated: job_name = f"process__{doc}_fetch_and_allocate" if not is_job_running(job_name): - job = frappe.enqueue( + frappe.enqueue( method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate", queue="long", timeout="3600", @@ -263,7 +261,7 @@ def reconcile_based_on_filters(doc: None | str = None) -> None: else: reconcile_job_name = f"process_{doc}_reconcile" if not is_job_running(reconcile_job_name): - job = frappe.enqueue( + frappe.enqueue( method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile", queue="long", timeout="3600", @@ -350,7 +348,7 @@ def fetch_and_allocate(doc: str) -> None: reconcile_job_name = f"process_{doc}_reconcile" if not is_job_running(reconcile_job_name): - job = frappe.enqueue( + frappe.enqueue( method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile", queue="long", timeout="3600", @@ -391,7 +389,6 @@ def reconcile(doc: None | str = None) -> None: # If Payment Entry, update details only for newly linked references # This is for performance if allocations[0].reference_type == "Payment Entry": - references = [(x.invoice_type, x.invoice_number) for x in allocations] pe = frappe.get_doc(allocations[0].reference_type, allocations[0].reference_name) pe.flags.ignore_validate_update_after_submit = True @@ -405,17 +402,18 @@ def reconcile(doc: None | str = None) -> None: # Update reconciled count reconciled_count = frappe.db.count( - "Process Payment Reconciliation Log Allocations", filters={"parent": log, "reconciled": True} + "Process Payment Reconciliation Log Allocations", + filters={"parent": log, "reconciled": True}, ) frappe.db.set_value( "Process Payment Reconciliation Log", log, "reconciled_entries", reconciled_count ) - except Exception as err: + except Exception: # Update the parent doc about the exception frappe.db.rollback() - traceback = frappe.get_traceback() + traceback = frappe.get_traceback(with_context=True) if traceback: message = "Traceback:
      " + traceback frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message) @@ -449,20 +447,19 @@ def reconcile(doc: None | str = None) -> None: frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True) frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed") else: - - if not (frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused"): + if not ( + frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused" + ): # trigger next batch in job # generate reconcile job name allocation = get_next_allocation(log) if allocation: - reconcile_job_name = ( - f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}" - ) + reconcile_job_name = f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}" else: reconcile_job_name = f"process_{doc}_reconcile" if not is_job_running(reconcile_job_name): - job = frappe.enqueue( + frappe.enqueue( method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile", queue="long", timeout="3600", @@ -481,7 +478,7 @@ def reconcile(doc: None | str = None) -> None: def is_any_doc_running(for_filter: str | dict | None = None) -> str | None: running_doc = None if for_filter: - if type(for_filter) == str: + if isinstance(for_filter, str): for_filter = frappe.json.loads(for_filter) running_doc = frappe.db.get_value( diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js index 8012d6e0374..ed182adde1b 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js @@ -1,15 +1,15 @@ -frappe.listview_settings['Process Payment Reconciliation'] = { +frappe.listview_settings["Process Payment Reconciliation"] = { add_fields: ["status"], - get_indicator: function(doc) { + get_indicator: function (doc) { let colors = { - 'Queued': 'orange', - 'Paused': 'orange', - 'Completed': 'green', - 'Partially Reconciled': 'orange', - 'Running': 'blue', - 'Failed': 'red', + Queued: "orange", + Paused: "orange", + Completed: "green", + "Partially Reconciled": "orange", + Running: "blue", + Failed: "red", }; let status = doc.status; - return [__(status), colors[status], 'status,=,'+status]; + return [__(status), colors[status], "status,=," + status]; }, }; diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js index 2468f10bccf..f483d0039e3 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js @@ -3,15 +3,14 @@ frappe.ui.form.on("Process Payment Reconciliation Log", { refresh(frm) { - if (['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) { + if (["Completed", "Running", "Paused", "Partially Reconciled"].find((x) => x == frm.doc.status)) { let progress = 0; if (frm.doc.reconciled_entries != 0) { - progress = frm.doc.reconciled_entries / frm.doc.total_allocations * 100; - } else if(frm.doc.total_allocations == 0 && frm.doc.status == "Completed"){ + progress = (frm.doc.reconciled_entries / frm.doc.total_allocations) * 100; + } else if (frm.doc.total_allocations == 0 && frm.doc.status == "Completed") { progress = 100; } - frm.dashboard.add_progress(__('Reconciliation Progress'), progress); + frm.dashboard.add_progress(__("Reconciliation Progress"), progress); } - }, }); diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js index 5a652048a23..19c73b249dc 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js @@ -1,15 +1,15 @@ -frappe.listview_settings['Process Payment Reconciliation Log'] = { +frappe.listview_settings["Process Payment Reconciliation Log"] = { add_fields: ["status"], - get_indicator: function(doc) { + get_indicator: function (doc) { var colors = { - 'Partially Reconciled': 'orange', - 'Paused': 'orange', - 'Reconciled': 'green', - 'Failed': 'red', - 'Cancelled': 'red', - 'Running': 'blue', + "Partially Reconciled": "orange", + Paused: "orange", + Reconciled: "green", + Failed: "red", + Cancelled: "red", + Running: "blue", }; let status = doc.status; - return [__(status), colors[status], "status,=,"+status]; + return [__(status), colors[status], "status,=," + status]; }, }; diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 5307ccb1931..81ebf9744c4 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -89,10 +89,11 @@ - - - - + + + + + @@ -101,6 +102,7 @@ +
      30 Days60 Days90 Days120 Days0 - 30 Days30 - 60 Days60 - 90 Days90 - 120 DaysAbove 120 Days
      {{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }} {{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }} {{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}
      diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js index c5908b783ee..be3579e2d64 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -1,153 +1,148 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Process Statement Of Accounts', { - view_properties: function(frm) { - frappe.route_options = {doc_type: 'Customer'}; +frappe.ui.form.on("Process Statement Of Accounts", { + view_properties: function (frm) { + frappe.route_options = { doc_type: "Customer" }; frappe.set_route("Form", "Customize Form"); }, - refresh: function(frm){ - if(!frm.doc.__islocal) { - frm.add_custom_button(__('Send Emails'), function(){ + refresh: function (frm) { + if (!frm.doc.__islocal) { + frm.add_custom_button(__("Send Emails"), function () { frappe.call({ method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails", args: { - "document_name": frm.doc.name, + document_name: frm.doc.name, }, - callback: function(r) { - if(r && r.message) { - frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'}); + callback: function (r) { + if (r && r.message) { + frappe.show_alert({ message: __("Emails Queued"), indicator: "blue" }); + } else { + frappe.msgprint(__("No Records for these settings.")); } - else{ - frappe.msgprint(__('No Records for these settings.')) - } - } + }, }); }); - frm.add_custom_button(__('Download'), function(){ + frm.add_custom_button(__("Download"), function () { var url = frappe.urllib.get_full_url( - '/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?' - + 'document_name='+encodeURIComponent(frm.doc.name)) + "/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?" + + "document_name=" + + encodeURIComponent(frm.doc.name) + ); $.ajax({ url: url, - type: 'GET', - success: function(result) { - if(jQuery.isEmptyObject(result)){ - frappe.msgprint(__('No Records for these settings.')); - } - else{ + type: "GET", + success: function (result) { + if (jQuery.isEmptyObject(result)) { + frappe.msgprint(__("No Records for these settings.")); + } else { window.location = url; } - } + }, }); }); } }, - onload: function(frm) { - frm.set_query('currency', function(){ + onload: function (frm) { + frm.set_query("currency", function () { return { filters: { - 'enabled': 1 - } - } - }); - frm.set_query("account", function() { - return { - filters: { - 'company': frm.doc.company - } + enabled: 1, + }, }; }); - if(frm.doc.__islocal){ - frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1)); - frm.set_value('to_date', frappe.datetime.get_today()); + frm.set_query("account", function () { + return { + filters: { + company: frm.doc.company, + }, + }; + }); + if (frm.doc.__islocal) { + frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1)); + frm.set_value("to_date", frappe.datetime.get_today()); } }, - report: function(frm){ + report: function (frm) { let filters = { - 'company': frm.doc.company, + company: frm.doc.company, + }; + if (frm.doc.report == "Accounts Receivable") { + filters["account_type"] = "Receivable"; } - if(frm.doc.report == 'Accounts Receivable'){ - filters['account_type'] = 'Receivable'; - } - frm.set_query("account", function() { + frm.set_query("account", function () { return { - filters: filters + filters: filters, }; }); - }, - customer_collection: function(frm){ - frm.set_value('collection_name', ''); - if(frm.doc.customer_collection){ - frm.get_field('collection_name').set_label(frm.doc.customer_collection); + customer_collection: function (frm) { + frm.set_value("collection_name", ""); + if (frm.doc.customer_collection) { + frm.get_field("collection_name").set_label(frm.doc.customer_collection); } }, - frequency: function(frm){ - if(frm.doc.frequency != ''){ - frm.set_value('start_date', frappe.datetime.get_today()); - } - else{ - frm.set_value('start_date', ''); + frequency: function (frm) { + if (frm.doc.frequency != "") { + frm.set_value("start_date", frappe.datetime.get_today()); + } else { + frm.set_value("start_date", ""); } }, - fetch_customers: function(frm){ - if(frm.doc.collection_name){ + fetch_customers: function (frm) { + if (frm.doc.collection_name) { frappe.call({ method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.fetch_customers", args: { - 'customer_collection': frm.doc.customer_collection, - 'collection_name': frm.doc.collection_name, - 'primary_mandatory': frm.doc.primary_mandatory + customer_collection: frm.doc.customer_collection, + collection_name: frm.doc.collection_name, + primary_mandatory: frm.doc.primary_mandatory, }, - callback: function(r) { - if(!r.exc) { - if(r.message.length){ - frm.clear_table('customers'); - for (const customer of r.message){ - var row = frm.add_child('customers'); + callback: function (r) { + if (!r.exc) { + if (r.message.length) { + frm.clear_table("customers"); + for (const customer of r.message) { + var row = frm.add_child("customers"); row.customer = customer.name; row.primary_email = customer.primary_email; row.billing_email = customer.billing_email; } - frm.refresh_field('customers'); - } - else{ - frappe.throw(__('No Customers found with selected options.')); + frm.refresh_field("customers"); + } else { + frappe.throw(__("No Customers found with selected options.")); } } - } + }, }); + } else { + frappe.throw("Enter " + frm.doc.customer_collection + " name."); } - else { - frappe.throw('Enter ' + frm.doc.customer_collection + ' name.'); - } - } + }, }); -frappe.ui.form.on('Process Statement Of Accounts Customer', { - customer: function(frm, cdt, cdn){ +frappe.ui.form.on("Process Statement Of Accounts Customer", { + customer: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; - if (!row.customer){ + if (!row.customer) { return; } frappe.call({ - method: 'erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.get_customer_emails', + method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.get_customer_emails", args: { - 'customer_name': row.customer, - 'primary_mandatory': frm.doc.primary_mandatory + customer_name: row.customer, + primary_mandatory: frm.doc.primary_mandatory, }, - callback: function(r){ - if(!r.exe){ - if(r.message.length){ - frappe.model.set_value(cdt, cdn, "primary_email", r.message[0]) - frappe.model.set_value(cdt, cdn, "billing_email", r.message[1]) - } - else { - return + callback: function (r) { + if (!r.exe) { + if (r.message.length) { + frappe.model.set_value(cdt, cdn, "primary_email", r.message[0]); + frappe.model.set_value(cdt, cdn, "billing_email", r.message[1]); + } else { + return; } } - } - }) - } + }, + }); + }, }); diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index d4b4b37b4ee..0c266ce7947 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -64,18 +64,6 @@ def get_statement_dict(doc, get_statement_dict=False): statement_dict = {} ageing = "" - err_journals = None - if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals: - err_journals = frappe.db.get_all( - "Journal Entry", - filters={ - "company": doc.company, - "docstatus": 1, - "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), - }, - as_list=True, - ) - for entry in doc.customers: if doc.include_ageing: ageing = set_ageing(doc, entry) @@ -88,8 +76,8 @@ def get_statement_dict(doc, get_statement_dict=False): ) filters = get_common_filters(doc) - if err_journals: - filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) + if doc.ignore_exchange_rate_revaluation_journals: + filters.update({"ignore_err": True}) if doc.report == "General Ledger": filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) @@ -409,11 +397,16 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): subject = frappe.render_template(doc.subject, context) message = frappe.render_template(doc.body, context) + if doc.sender: + sender_email = frappe.db.get_value("Email Account", doc.sender, "email_id") + else: + sender_email = frappe.session.user + frappe.enqueue( queue="short", method=frappe.sendmail, recipients=recipients, - sender=doc.sender or frappe.session.user, + sender=sender_email, cc=cc, subject=subject, message=message, @@ -430,9 +423,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): else: new_to_date = add_months(new_to_date, 1 if doc.frequency == "Monthly" else 3) new_from_date = add_months(new_to_date, -1 * doc.filter_duration) - doc.add_comment( - "Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now()) - ) + doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())) if doc.report == "General Ledger": doc.db_set("to_date", new_to_date, commit=True) doc.db_set("from_date", new_from_date, commit=True) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py index a3a74df4029..92dbb5ef273 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py @@ -1,7 +1,6 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe from frappe.tests.utils import FrappeTestCase diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js index e840c79cd75..7a26c07d01f 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js @@ -1,51 +1,56 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Promotional Scheme', { - refresh: function(frm) { +frappe.ui.form.on("Promotional Scheme", { + refresh: function (frm) { frm.trigger("set_options_for_applicable_for"); frm.trigger("toggle_reqd_apply_on"); }, - selling: function(frm) { + selling: function (frm) { frm.trigger("set_options_for_applicable_for"); }, - buying: function(frm) { + buying: function (frm) { frm.trigger("set_options_for_applicable_for"); }, - set_options_for_applicable_for: function(frm) { + set_options_for_applicable_for: function (frm) { var options = [""]; var applicable_for = frm.doc.applicable_for; - if(frm.doc.selling) { - options = $.merge(options, ["Customer", "Customer Group", "Territory", "Sales Partner", "Campaign"]); + if (frm.doc.selling) { + options = $.merge(options, [ + "Customer", + "Customer Group", + "Territory", + "Sales Partner", + "Campaign", + ]); } - if(frm.doc.buying) { + if (frm.doc.buying) { $.merge(options, ["Supplier", "Supplier Group"]); } set_field_options("applicable_for", options.join("\n")); - if(!in_list(options, applicable_for)) applicable_for = null; + if (!in_list(options, applicable_for)) applicable_for = null; frm.set_value("applicable_for", applicable_for); }, - apply_on: function(frm) { + apply_on: function (frm) { frm.trigger("toggle_reqd_apply_on"); }, - toggle_reqd_apply_on: function(frm) { + toggle_reqd_apply_on: function (frm) { const fields = { - 'Item Code': 'items', - 'Item Group': 'item_groups', - 'Brand': 'brands' + "Item Code": "items", + "Item Group": "item_groups", + Brand: "brands", }; for (var key in fields) { - frm.toggle_reqd(fields[key], - frm.doc.apply_on === key ? 1 : 0); + frm.toggle_reqd(fields[key], frm.doc.apply_on === key ? 1 : 0); } - } + }, }); diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 4d28d106604..e3278098f1a 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -100,9 +100,7 @@ class PromotionalScheme(Document): docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name}) for docname in docnames: - if frappe.db.exists( - "Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)} - ): + if frappe.db.exists("Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)}): raise_for_transaction_exists(self.name) if docnames and not transaction_exists: @@ -177,7 +175,7 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): args = get_args_for_pricing_rule(doc) applicable_for = frappe.scrub(doc.get("applicable_for")) - for idx, d in enumerate(doc.get(child_doc)): + for _idx, d in enumerate(doc.get(child_doc)): if d.name in rules: if not args.get(applicable_for): docname = get_pricing_rule_docname(d) @@ -187,7 +185,14 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): for applicable_for_value in args.get(applicable_for): docname = get_pricing_rule_docname(d, applicable_for, applicable_for_value) pr = prepare_pricing_rule( - args, doc, child_doc, discount_fields, d, docname, applicable_for, applicable_for_value + args, + doc, + child_doc, + discount_fields, + d, + docname, + applicable_for, + applicable_for_value, ) new_doc.append(pr) @@ -213,7 +218,7 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): def get_pricing_rule_docname( - row: dict, applicable_for: str = None, applicable_for_value: str = None + row: dict, applicable_for: str | None = None, applicable_for_value: str | None = None ) -> str: fields = ["promotional_scheme_id", "name"] filters = {"promotional_scheme_id": row.name} diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index c2aa1d936e5..665fc6edcc9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"]; if(!this.frm.doc.__islocal) { // show credit_to in print format @@ -99,8 +99,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } } - if(doc.docstatus == 1 && doc.outstanding_amount != 0 - && !(doc.is_return && doc.return_against) && !doc.on_hold) { + if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), @@ -164,6 +163,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } }) }, __("Get Items From")); + + if (!this.frm.doc.is_return) { + frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => { + if (value) { + this.frm.doc.items.forEach((item) => { + this.frm.fields_dict.items.grid.update_docfield_property( + "rate", "read_only", (item.purchase_receipt && item.pr_detail) + ); + }); + } + }); + } } this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 643047e982d..6b0ec8e8c85 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -22,6 +22,9 @@ "is_paid", "is_return", "return_against", + "update_outstanding_for_self", + "update_billed_amount_in_purchase_order", + "update_billed_amount_in_purchase_receipt", "apply_tds", "tax_withholding_category", "amended_from", @@ -410,6 +413,20 @@ "read_only": 1, "search_index": 1 }, + { + "default": "0", + "depends_on": "eval: doc.is_return", + "fieldname": "update_billed_amount_in_purchase_order", + "fieldtype": "Check", + "label": "Update Billed Amount in Purchase Order" + }, + { + "default": "1", + "depends_on": "eval: doc.is_return", + "fieldname": "update_billed_amount_in_purchase_receipt", + "fieldtype": "Check", + "label": "Update Billed Amount in Purchase Receipt" + }, { "fieldname": "section_addresses", "fieldtype": "Section Break", @@ -739,7 +756,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1588,13 +1605,21 @@ "fieldtype": "Check", "label": "Use Transaction Date Exchange Rate", "read_only": 1 + }, + { + "default": "1", + "depends_on": "eval: doc.is_return && doc.return_against", + "description": "Debit Note will update it's own outstanding amount, even if \"Return Against\" is specified.", + "fieldname": "update_outstanding_for_self", + "fieldtype": "Check", + "label": "Update Outstanding for Self" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-11-03 15:47:30.319200", + "modified": "2024-03-20 15:57:00.736868", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 944a8fb2364..b53627a682f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -3,7 +3,7 @@ import frappe -from frappe import _, throw +from frappe import _, qb, throw from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate @@ -55,7 +55,7 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class PurchaseInvoice(BuyingController): def __init__(self, *args, **kwargs): - super(PurchaseInvoice, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.status_updater = [ { "source_dt": "Purchase Invoice Item", @@ -72,7 +72,7 @@ class PurchaseInvoice(BuyingController): ] def onload(self): - super(PurchaseInvoice, self).onload() + super().onload() supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") self.set_onload("supplier_tds", supplier_tds) @@ -92,7 +92,7 @@ class PurchaseInvoice(BuyingController): self.validate_posting_time() - super(PurchaseInvoice, self).validate() + super().validate() if not self.is_return: self.po_required() @@ -131,6 +131,18 @@ class PurchaseInvoice(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + self.set_percentage_received() + + def set_percentage_received(self): + total_billed_qty = 0.0 + total_received_qty = 0.0 + for row in self.items: + if row.purchase_receipt and row.pr_detail and row.received_qty: + total_billed_qty += row.qty + total_received_qty += row.received_qty + + if total_billed_qty and total_received_qty: + self.per_received = total_received_qty / total_billed_qty * 100 def validate_release_date(self): if self.release_date and getdate(nowdate()) >= getdate(self.release_date): @@ -143,7 +155,6 @@ class PurchaseInvoice(BuyingController): if flt(self.paid_amount) + flt(self.write_off_amount) - flt( self.get("rounded_total") or self.grand_total ) > 1 / (10 ** (self.precision("base_grand_total") + 1)): - frappe.throw(_("""Paid amount + Write Off Amount can not be greater than Grand Total""")) def create_remarks(self): @@ -172,7 +183,7 @@ class PurchaseInvoice(BuyingController): self.tax_withholding_category = tds_category self.set_onload("supplier_tds", tds_category) - super(PurchaseInvoice, self).set_missing_values(for_validate) + super().set_missing_values(for_validate) def validate_credit_to_acc(self): if not self.credit_to: @@ -206,12 +217,12 @@ class PurchaseInvoice(BuyingController): check_list = [] for d in self.get("items"): - if d.purchase_order and not d.purchase_order in check_list and not d.purchase_receipt: + if d.purchase_order and d.purchase_order not in check_list and not d.purchase_receipt: check_list.append(d.purchase_order) check_on_hold_or_closed_status("Purchase Order", d.purchase_order) def validate_with_previous_doc(self): - super(PurchaseInvoice, self).validate_with_previous_doc( + super().validate_with_previous_doc( { "Purchase Order": { "ref_dn_field": "purchase_order", @@ -259,7 +270,7 @@ class PurchaseInvoice(BuyingController): exc=WarehouseMissingError, ) - super(PurchaseInvoice, self).validate_warehouse() + super().validate_warehouse() def validate_item_code(self): for d in self.get("items"): @@ -295,7 +306,6 @@ class PurchaseInvoice(BuyingController): or not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier") ) ): - if self.update_stock and item.warehouse and (not item.from_warehouse): if ( for_validate @@ -323,12 +333,16 @@ class PurchaseInvoice(BuyingController): if negative_expense_booked_in_pr: if ( - for_validate and item.expense_account and item.expense_account != stock_not_billed_account + for_validate + and item.expense_account + and item.expense_account != stock_not_billed_account ): msg = _( "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}" ).format( - item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt) + item.idx, + frappe.bold(stock_not_billed_account), + frappe.bold(item.purchase_receipt), ) frappe.msgprint(msg, title=_("Expense Head Changed")) @@ -337,7 +351,9 @@ class PurchaseInvoice(BuyingController): # If no purchase receipt present then book expense in 'Stock Received But Not Billed' # This is done in cases when Purchase Invoice is created before Purchase Receipt if ( - for_validate and item.expense_account and item.expense_account != stock_not_billed_account + for_validate + and item.expense_account + and item.expense_account != stock_not_billed_account ): msg = _( "Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}." @@ -388,7 +404,6 @@ class PurchaseInvoice(BuyingController): def po_required(self): if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes": - if frappe.get_value( "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" ): @@ -398,7 +413,9 @@ class PurchaseInvoice(BuyingController): if not d.purchase_order: msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code)) msg += "

      " - msg += _("To submit the invoice without purchase order please set {0} as {1} in {2}").format( + msg += _( + "To submit the invoice without purchase order please set {0} as {1} in {2}" + ).format( frappe.bold(_("Purchase Order Required")), frappe.bold("No"), get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"), @@ -408,7 +425,6 @@ class PurchaseInvoice(BuyingController): def pr_required(self): stock_items = self.get_stock_items() if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes": - if frappe.get_value( "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt" ): @@ -441,7 +457,8 @@ class PurchaseInvoice(BuyingController): frappe.throw(_("Purchase Order {0} is not submitted").format(d.purchase_order)) if d.purchase_receipt: submitted = frappe.db.sql( - "select name from `tabPurchase Receipt` where docstatus = 1 and name = %s", d.purchase_receipt + "select name from `tabPurchase Receipt` where docstatus = 1 and name = %s", + d.purchase_receipt, ) if not submitted: frappe.throw(_("Purchase Receipt {0} is not submitted").format(d.purchase_receipt)) @@ -489,7 +506,9 @@ class PurchaseInvoice(BuyingController): for item in self.get("items"): if item.purchase_receipt: frappe.throw( - _("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt) + _("Stock cannot be updated against Purchase Receipt {0}").format( + item.purchase_receipt + ) ) def validate_for_repost(self): @@ -499,9 +518,14 @@ class PurchaseInvoice(BuyingController): validate_docs_for_deferred_accounting([], [self.name]) def on_submit(self): - super(PurchaseInvoice, self).on_submit() + super().on_submit() self.check_prev_docstatus() + + if self.is_return and not self.update_billed_amount_in_purchase_order: + # NOTE status updating bypassed for is_return + self.status_updater = [] + self.update_status_updater_args() self.update_prevdoc_status() @@ -534,9 +558,7 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.repost_future_sle_and_gle() - if ( - frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction" - ): + if frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction": self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) @@ -558,13 +580,12 @@ class PurchaseInvoice(BuyingController): self.db_set("repost_required", self.needs_repost) def make_gl_entries(self, gl_entries=None, from_repost=False): - if not gl_entries: - gl_entries = self.get_gl_entries() + update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" + if self.docstatus == 1: + if not gl_entries: + gl_entries = self.get_gl_entries() - if gl_entries: - update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" - - if self.docstatus == 1: + if gl_entries: make_gl_entries( gl_entries, update_outstanding=update_outstanding, @@ -572,29 +593,43 @@ class PurchaseInvoice(BuyingController): from_repost=from_repost, ) self.make_exchange_gain_loss_journal() - elif self.docstatus == 2: - provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"] - make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - if provisional_entries: - for entry in provisional_entries: - frappe.db.set_value( - "GL Entry", - {"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no}, - "is_cancelled", - 1, - ) - - if update_outstanding == "No": - update_outstanding_amt( - self.credit_to, - "Supplier", - self.supplier, - self.doctype, - self.return_against if cint(self.is_return) and self.return_against else self.name, - ) - - elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: + elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + self.cancel_provisional_entries() + + self.update_supplier_outstanding(update_outstanding) + + def cancel_provisional_entries(self): + rows = set() + purchase_receipts = set() + for d in self.items: + if d.purchase_receipt: + purchase_receipts.add(d.purchase_receipt) + rows.add(d.name) + + if rows: + # cancel gl entries + gle = qb.DocType("GL Entry") + gle_update_query = ( + qb.update(gle) + .set(gle.is_cancelled, 1) + .where( + (gle.voucher_type == "Purchase Receipt") + & (gle.voucher_no.isin(purchase_receipts)) + & (gle.voucher_detail_no.isin(rows)) + ) + ) + gle_update_query.run() + + def update_supplier_outstanding(self, update_outstanding): + if update_outstanding == "No": + update_outstanding_amt( + self.credit_to, + "Supplier", + self.supplier, + self.doctype, + self.return_against if cint(self.is_return) and self.return_against else self.name, + ) def get_gl_entries(self, warehouse_account=None): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) @@ -646,6 +681,10 @@ class PurchaseInvoice(BuyingController): ) if grand_total and not self.is_internal_transfer(): + against_voucher = self.name + if self.is_return and self.return_against and not self.update_outstanding_for_self: + against_voucher = self.return_against + # Did not use base_grand_total to book rounding loss gle gl_entries.append( self.get_gl_dict( @@ -659,9 +698,7 @@ class PurchaseInvoice(BuyingController): "credit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": against_voucher, "against_voucher_type": self.doctype, "project": self.project, "cost_center": self.cost_center, @@ -701,18 +738,16 @@ class PurchaseInvoice(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) provisional_accounting_for_non_stock_items = cint( - frappe.db.get_value( - "Company", self.company, "enable_provisional_accounting_for_non_stock_items" - ) + frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items") ) - - purchase_receipt_doc_map = {} + if provisional_accounting_for_non_stock_items: + self.get_provisional_accounts() for item in self.get("items"): if flt(item.base_net_amount): account_currency = get_account_currency(item.expense_account) if item.item_code: - asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") + frappe.get_cached_value("Item", item.item_code, "asset_category") if ( self.update_stock @@ -817,7 +852,9 @@ class PurchaseInvoice(BuyingController): if flt(item.rm_supp_cost): supplier_warehouse_account = warehouse_account[self.supplier_warehouse]["account"] if not supplier_warehouse_account: - frappe.throw(_("Please set account in Warehouse {0}").format(self.supplier_warehouse)) + frappe.throw( + _("Please set account in Warehouse {0}").format(self.supplier_warehouse) + ) gl_entries.append( self.get_gl_dict( { @@ -843,34 +880,7 @@ class PurchaseInvoice(BuyingController): dummy, amount = self.get_amount_and_base_amount(item, None) if provisional_accounting_for_non_stock_items: - if item.purchase_receipt: - provisional_account = frappe.db.get_value( - "Purchase Receipt Item", item.pr_detail, "provisional_expense_account" - ) or self.get_company_default("default_provisional_account") - purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) - - if not purchase_receipt_doc: - purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt) - purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc - - # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - expense_booked_in_pr = frappe.db.get_value( - "GL Entry", - { - "is_cancelled": 0, - "voucher_type": "Purchase Receipt", - "voucher_no": item.purchase_receipt, - "voucher_detail_no": item.pr_detail, - "account": provisional_account, - }, - ["name"], - ) - - if expense_booked_in_pr: - # Intentionally passing purchase invoice item to handle partial billing - purchase_receipt_doc.add_provisional_gl_entry( - item, gl_entries, self.posting_date, provisional_account, reverse=1 - ) + self.make_provisional_gl_entry(gl_entries, item) if not self.is_internal_transfer(): gl_entries.append( @@ -894,10 +904,9 @@ class PurchaseInvoice(BuyingController): and self.conversion_rate != exchange_rate_map[item.purchase_receipt] and item.net_rate == net_rate_map[item.pr_detail] ): - - discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * ( - exchange_rate_map[item.purchase_receipt] - self.conversion_rate - ) + discrepancy_caused_by_exchange_rate_difference = ( + item.qty * item.net_rate + ) * (exchange_rate_map[item.purchase_receipt] - self.conversion_rate) gl_entries.append( self.get_gl_dict( @@ -925,17 +934,6 @@ class PurchaseInvoice(BuyingController): item=item, ) ) - - # update gross amount of asset bought through this document - assets = frappe.db.get_all( - "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} - ) - for asset in assets: - frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) - frappe.db.set_value( - "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) - ) - if ( self.auto_accounting_for_stock and self.is_opening == "No" @@ -975,16 +973,80 @@ class PurchaseInvoice(BuyingController): item.item_tax_amount, item.precision("item_tax_amount") ) + if item.is_fixed_asset and item.landed_cost_voucher_amount: + self.update_gross_purchase_amount_for_linked_assets(item) + + def get_provisional_accounts(self): + self.provisional_accounts = frappe._dict() + linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt]) + pr_items = frappe.get_all( + "Purchase Receipt Item", + filters={"parent": ("in", linked_purchase_receipts)}, + fields=["name", "provisional_expense_account", "qty", "base_rate"], + ) + default_provisional_account = self.get_company_default("default_provisional_account") + provisional_accounts = set( + [ + d.provisional_expense_account + if d.provisional_expense_account + else default_provisional_account + for d in pr_items + ] + ) + + provisional_gl_entries = frappe.get_all( + "GL Entry", + filters={ + "voucher_type": "Purchase Receipt", + "voucher_no": ("in", linked_purchase_receipts), + "account": ("in", provisional_accounts), + "is_cancelled": 0, + }, + fields=["voucher_detail_no"], + ) + rows_with_provisional_entries = [d.voucher_detail_no for d in provisional_gl_entries] + for item in pr_items: + self.provisional_accounts[item.name] = { + "provisional_account": item.provisional_expense_account or default_provisional_account, + "qty": item.qty, + "base_rate": item.base_rate, + "has_provisional_entry": item.name in rows_with_provisional_entries, + } + + def make_provisional_gl_entry(self, gl_entries, item): + if item.purchase_receipt: + pr_item = self.provisional_accounts.get(item.pr_detail, {}) + if pr_item.get("has_provisional_entry"): + purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt) + + # Intentionally passing purchase invoice item to handle partial billing + purchase_receipt_doc.add_provisional_gl_entry( + item, + gl_entries, + self.posting_date, + pr_item.get("provisional_account"), + reverse=1, + item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")), + ) + + def update_gross_purchase_amount_for_linked_assets(self, item): assets = frappe.db.get_all( - "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} + "Asset", + filters={"purchase_invoice": self.name, "item_code": item.item_code}, + fields=["name", "asset_quantity"], ) for asset in assets: - frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) - frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) + purchase_amount = flt(item.valuation_rate) * asset.asset_quantity + frappe.db.set_value( + "Asset", + asset.name, + { + "gross_purchase_amount": purchase_amount, + "purchase_receipt_amount": purchase_amount, + }, + ) - def make_stock_adjustment_entry( - self, gl_entries, item, voucher_wise_stock_value, account_currency - ): + def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency): net_amt_precision = item.precision("base_net_amount") val_rate_db_precision = 6 if cint(item.precision("valuation_rate")) <= 6 else 9 @@ -1000,7 +1062,6 @@ class PurchaseInvoice(BuyingController): and warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) ): - cost_of_goods_sold_account = self.get_company_default("default_expense_account") stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) stock_adjustment_amt = warehouse_debit_amount - stock_amount @@ -1223,9 +1284,7 @@ class PurchaseInvoice(BuyingController): # base_rounding_adjustment may become zero due to small precision # eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2 # then base_rounding_adjustment becomes zero and error is thrown in GL Entry - if ( - not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment - ): + if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment: round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center ) @@ -1248,10 +1307,14 @@ class PurchaseInvoice(BuyingController): def on_cancel(self): check_if_return_invoice_linked_with_payment_entry(self) - super(PurchaseInvoice, self).on_cancel() + super().on_cancel() self.check_on_hold_or_closed_status() + if self.is_return and not self.update_billed_amount_in_purchase_order: + # NOTE status updating bypassed for is_return + self.status_updater = [] + self.update_status_updater_args() self.update_prevdoc_status() @@ -1275,9 +1338,7 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.repost_future_sle_and_gle() - if ( - frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction" - ): + if frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction": self.update_project() self.db_set("status", "Cancelled") @@ -1308,9 +1369,7 @@ class PurchaseInvoice(BuyingController): pj = frappe.qb.DocType("Project") for proj, value in projects.items(): - res = ( - frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run() - ) + res = frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run() current_purchase_cost = res and res[0][0] or 0 frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value) @@ -1345,6 +1404,9 @@ class PurchaseInvoice(BuyingController): frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi)) def update_billing_status_in_pr(self, update_modified=True): + if self.is_return and not self.update_billed_amount_in_purchase_receipt: + return + updated_pr = [] po_details = [] @@ -1526,12 +1588,8 @@ class PurchaseInvoice(BuyingController): elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" # Check if outstanding amount is 0 due to debit note issued against invoice - elif ( - outstanding_amount <= 0 - and self.is_return == 0 - and frappe.db.get_value( - "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} - ) + elif self.is_return == 0 and frappe.db.get_value( + "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} ): self.status = "Debit Note Issued" elif self.is_return == 1: @@ -1579,9 +1637,7 @@ def get_purchase_document_details(doc): ) net_rate_map = frappe._dict( - frappe.get_all( - child_doctype, filters={"name": ("in", items)}, fields=["name", "net_rate"], as_list=1 - ) + frappe.get_all(child_doctype, filters={"name": ("in", items)}, fields=["name", "net_rate"], as_list=1) ) return exchange_rate_map, net_rate_map @@ -1660,10 +1716,6 @@ def make_inter_company_sales_invoice(source_name, target_doc=None): return make_inter_company_transaction("Purchase Invoice", source_name, target_doc) -def on_doctype_update(): - frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"]) - - @frappe.whitelist() def make_purchase_receipt(source_name, target_doc=None): def update_item(obj, target, source_parent): diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index e1c37c60013..4350ba11148 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -22,48 +22,35 @@ frappe.listview_settings["Purchase Invoice"] = { return [__(doc.status), "darkgrey", "status,=," + doc.status]; } - if ( - flt(doc.outstanding_amount) > 0 && - doc.docstatus == 1 && - cint(doc.on_hold) - ) { + if (flt(doc.outstanding_amount) > 0 && doc.docstatus == 1 && cint(doc.on_hold)) { if (!doc.release_date) { return [__("On Hold"), "darkgrey"]; - } else if ( - frappe.datetime.get_diff( - doc.release_date, - frappe.datetime.nowdate() - ) > 0 - ) { + } else if (frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) { return [__("Temporarily on Hold"), "darkgrey"]; } } const status_colors = { - "Unpaid": "orange", - "Paid": "green", - "Return": "gray", - "Overdue": "red", + Unpaid: "orange", + Paid: "green", + Return: "gray", + Overdue: "red", "Partly Paid": "yellow", "Internal Transfer": "darkgrey", }; if (status_colors[doc.status]) { - return [ - __(doc.status), - status_colors[doc.status], - "status,=," + doc.status, - ]; + return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; } }, - onload: function(listview) { - listview.page.add_action_item(__("Purchase Receipt"), ()=>{ + onload: function (listview) { + listview.page.add_action_item(__("Purchase Receipt"), () => { erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt"); }); - listview.page.add_action_item(__("Payment"), ()=>{ + listview.page.add_action_item(__("Payment"), () => { erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry"); }); - } + }, }; diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index dc2b37291e8..22e28e6ce36 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2,8 +2,6 @@ # License: GNU General Public License v3. See license.txt -import unittest - import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, flt, getdate, nowdate, today @@ -218,7 +216,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): supplier.on_hold = 0 supplier.save() - except: + except Exception: pass else: raise Exception @@ -252,7 +250,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(pi.on_hold, 0) def test_gl_entries_with_perpetual_inventory_against_pr(self): - pr = make_purchase_receipt( company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", @@ -303,7 +300,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): ] ) - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(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) @@ -327,9 +324,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): pi.submit() # Get exchnage gain and loss account - exchange_gain_loss_account = frappe.db.get_value( - "Company", pi.company, "exchange_gain_loss_account" - ) + exchange_gain_loss_account = frappe.db.get_value("Company", pi.company, "exchange_gain_loss_account") # fetching the latest GL Entry with exchange gain and loss account account amount = frappe.db.get_value( @@ -545,12 +540,10 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"}) existing_purchase_cost = frappe.db.sql( - """select sum(base_net_amount) + f"""select sum(base_net_amount) from `tabPurchase Invoice Item` - where project = '{0}' - and docstatus=1""".format( - project.name - ) + where project = '{project.name}' + and docstatus=1""" ) existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0 @@ -725,7 +718,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): "credit", "credit_in_account_currency", ): - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][field], gle[field]) # Check for valid currency @@ -747,7 +740,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertFalse(gle) def test_purchase_invoice_update_stock_gl_entry_with_perpetual_inventory(self): - pi = make_purchase_invoice( update_stock=1, posting_date=frappe.utils.nowdate(), @@ -776,13 +768,12 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): (d[0], d) for d in [[pi.credit_to, 0.0, 250.0], [stock_in_hand_account, 250.0, 0.0]] ) - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_gl_entries[gle.account][0], gle.account) self.assertEqual(expected_gl_entries[gle.account][1], gle.debit) self.assertEqual(expected_gl_entries[gle.account][2], gle.credit) def test_purchase_invoice_for_is_paid_and_update_stock_gl_entry_with_perpetual_inventory(self): - pi = make_purchase_invoice( update_stock=1, posting_date=frappe.utils.nowdate(), @@ -817,7 +808,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): ] ) - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_gl_entries[gle.account][0], gle.account) self.assertEqual(expected_gl_entries[gle.account][1], gle.debit) self.assertEqual(expected_gl_entries[gle.account][2], gle.credit) @@ -1015,12 +1006,8 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): def test_duplicate_due_date_in_terms(self): pi = make_purchase_invoice(do_not_save=1) - pi.append( - "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) - ) - pi.append( - "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) - ) + pi.append("payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50)) + pi.append("payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50)) self.assertRaises(frappe.ValidationError, pi.insert) @@ -1058,9 +1045,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - pi = make_purchase_invoice_against_cost_center( - cost_center=cost_center, credit_to="Creditors - _TC" - ) + pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC") self.assertEqual(pi.cost_center, cost_center) expected_values = { @@ -1520,22 +1505,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) def test_provisional_accounting_entry(self): - create_item("_Test Non Stock Item", is_stock_item=0) + setup_provisional_accounting() - provisional_account = create_account( - account_name="Provision Account", - parent_account="Current Liabilities - _TC", - company="_Test Company", - ) - - company = frappe.get_doc("Company", "_Test Company") - company.enable_provisional_accounting_for_non_stock_items = 1 - company.default_provisional_account = provisional_account - company.save() - - pr = make_purchase_receipt( - item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2) - ) + pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2)) pi = create_purchase_invoice_from_receipt(pr.name) pi.set_posting_time = 1 @@ -1544,7 +1516,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): pi.save() pi.submit() - self.assertEquals(pr.items[0].provisional_expense_account, "Provision Account - _TC") + self.assertEqual(pr.items[0].provisional_expense_account, "Provision Account - _TC") # Check GLE for Purchase Invoice expected_gle = [ @@ -1571,19 +1543,102 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date], ] - check_gl_entries( - self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date + check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date) + + toggle_provisional_accounting_setting() + + def test_provisional_accounting_entry_for_over_billing(self): + setup_provisional_accounting() + + # Configure Buying Settings to allow rate change + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + # Create PR: rate = 1000, qty = 5 + pr = make_purchase_receipt( + item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2) ) - company.enable_provisional_accounting_for_non_stock_items = 0 - company.save() + # Overbill PR: rate = 2000, qty = 10 + pi = create_purchase_invoice_from_receipt(pr.name) + pi.set_posting_time = 1 + pi.posting_date = add_days(pr.posting_date, -1) + pi.items[0].qty = 10 + pi.items[0].rate = 2000 + pi.items[0].expense_account = "Cost of Goods Sold - _TC" + pi.save() + pi.submit() + + expected_gle = [ + ["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, -1)], + ["Creditors - _TC", 0, 20000, add_days(pr.posting_date, -1)], + ] + + check_gl_entries(self, pi.name, expected_gle, pi.posting_date) + + expected_gle_for_purchase_receipt = [ + ["Provision Account - _TC", 5000, 0, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date], + ["Provision Account - _TC", 0, 5000, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date], + ] + + check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + + # Cancel purchase invoice to check reverse provisional entry cancellation + pi.cancel() + + expected_gle_for_purchase_receipt_post_pi_cancel = [ + ["Provision Account - _TC", 0, 5000, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date], + ] + + check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date) + + toggle_provisional_accounting_setting() + + def test_provisional_accounting_entry_for_partial_billing(self): + setup_provisional_accounting() + + # Configure Buying Settings to allow rate change + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + # Create PR: rate = 1000, qty = 5 + pr = make_purchase_receipt( + item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2) + ) + + # Partially bill PR: rate = 500, qty = 2 + pi = create_purchase_invoice_from_receipt(pr.name) + pi.set_posting_time = 1 + pi.posting_date = add_days(pr.posting_date, -1) + pi.items[0].qty = 2 + pi.items[0].rate = 500 + pi.items[0].expense_account = "Cost of Goods Sold - _TC" + pi.save() + pi.submit() + + expected_gle = [ + ["Cost of Goods Sold - _TC", 1000, 0, add_days(pr.posting_date, -1)], + ["Creditors - _TC", 0, 1000, add_days(pr.posting_date, -1)], + ] + + check_gl_entries(self, pi.name, expected_gle, pi.posting_date) + + expected_gle_for_purchase_receipt = [ + ["Provision Account - _TC", 5000, 0, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date], + ["Provision Account - _TC", 0, 1000, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 1000, 0, pi.posting_date], + ] + + check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + + toggle_provisional_accounting_setting() def test_adjust_incoming_rate(self): frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) - frappe.db.set_single_value( - "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1 - ) + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) # Increase the cost of the item @@ -1635,9 +1690,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): ) self.assertEqual(stock_value_difference, 50) - frappe.db.set_single_value( - "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0 - ) + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0) # Don't adjust incoming rate @@ -1667,7 +1720,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) def test_item_less_defaults(self): - pi = frappe.new_doc("Purchase Invoice") pi.supplier = "_Test Supplier" pi.company = "_Test Company" @@ -1893,6 +1945,21 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC") + def test_debit_note_with_account_mismatch(self): + new_creditors = create_account( + parent_account="Accounts Payable - _TC", + account_name="Creditors 2", + company="_Test Company", + account_type="Payable", + ) + pi = make_purchase_invoice(qty=1, rate=1000) + dr_note = make_purchase_invoice( + qty=-1, rate=1000, is_return=1, return_against=pi.name, do_not_save=True + ) + dr_note.credit_to = new_creditors + + self.assertRaises(frappe.ValidationError, dr_note.save) + def check_gl_entries( doc, @@ -2076,4 +2143,24 @@ def make_purchase_invoice_against_cost_center(**args): return pi +def setup_provisional_accounting(**args): + args = frappe._dict(args) + create_item("_Test Non Stock Item", is_stock_item=0) + company = args.company or "_Test Company" + provisional_account = create_account( + account_name=args.account_name or "Provision Account", + parent_account=args.parent_account or "Current Liabilities - _TC", + company=company, + ) + toggle_provisional_accounting_setting(enable=1, company=company, provisional_account=provisional_account) + + +def toggle_provisional_accounting_setting(**args): + args = frappe._dict(args) + company = frappe.get_doc("Company", args.company or "_Test Company") + company.enable_provisional_accounting_for_non_stock_items = args.enable or 0 + company.default_provisional_account = args.provisional_account + company.save() + + test_records = frappe.get_test_records("Purchase Invoice") diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index bc56132c60f..cafdc0e12c6 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -286,7 +286,6 @@ "oldfieldname": "import_rate", "oldfieldtype": "Currency", "options": "currency", - "read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)", "reqd": 1 }, { @@ -741,6 +740,7 @@ "fieldtype": "Currency", "label": "Landed Cost Voucher Amount", "no_copy": 1, + "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 }, @@ -894,7 +894,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-30 16:26:05.629780", + "modified": "2024-03-19 19:09:47.210965", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -904,4 +904,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 347cae05b72..adab54b3756 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -126,7 +126,7 @@ "fieldname": "rate", "fieldtype": "Float", "in_list_view": 1, - "label": "Rate", + "label": "Tax Rate", "oldfieldname": "rate", "oldfieldtype": "Currency" }, @@ -230,7 +230,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-05 20:04:36.618240", + "modified": "2024-01-14 10:04:36.618240", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", @@ -239,4 +239,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py index 70d29bfda25..7c54b53120a 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py @@ -17,4 +17,4 @@ class PurchaseTaxesandChargesTemplate(Document): def autoname(self): if self.company and self.title: abbr = frappe.get_cached_value("Company", self.company, "abbr") - self.name = "{0} - {1}".format(self.title, abbr) + self.name = f"{self.title} - {abbr}" diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js index c7b7a148cfb..c304c7f17eb 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js @@ -2,47 +2,47 @@ // For license information, please see license.txt frappe.ui.form.on("Repost Accounting Ledger", { - setup: function(frm) { - frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) { + setup: function (frm) { + frm.fields_dict["vouchers"].grid.get_field("voucher_type").get_query = function (doc) { return { - query: "erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.get_repost_allowed_types" - } - } + query: "erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.get_repost_allowed_types", + }; + }; - frm.fields_dict['vouchers'].grid.get_field('voucher_no').get_query = function(doc) { + frm.fields_dict["vouchers"].grid.get_field("voucher_no").get_query = function (doc) { if (doc.company) { return { filters: { company: doc.company, - docstatus: 1 - } - } + docstatus: 1, + }, + }; } - } + }; }, - refresh: function(frm) { - frm.add_custom_button(__('Show Preview'), () => { + refresh: function (frm) { + frm.add_custom_button(__("Show Preview"), () => { frm.call({ - method: 'generate_preview', + method: "generate_preview", doc: frm.doc, freeze: true, - freeze_message: __('Generating Preview'), - callback: function(r) { + freeze_message: __("Generating Preview"), + callback: function (r) { if (r && r.message) { let content = r.message; let opts = { title: "Preview", subtitle: "preview", content: content, - print_settings: {orientation: "landscape"}, + print_settings: { orientation: "landscape" }, columns: [], data: [], - } + }; frappe.render_grid(opts); } - } + }, }); }); - } + }, }); diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 9211b286c7d..15478ab8633 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -9,7 +9,7 @@ from frappe.utils.data import comma_and class RepostAccountingLedger(Document): def __init__(self, *args, **kwargs): - super(RepostAccountingLedger, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._allowed_types = get_allowed_types_from_settings() def validate(self): @@ -70,6 +70,7 @@ class RepostAccountingLedger(Document): ).append(gle.update({"old": True})) def generate_preview_data(self): + frappe.flags.through_repost_accounting_ledger = True self.gl_entries = [] self.get_existing_ledger_entries() for x in self.vouchers: @@ -123,6 +124,7 @@ class RepostAccountingLedger(Document): @frappe.whitelist() def start_repost(account_repost_doc=str) -> None: + frappe.flags.through_repost_accounting_ledger = True if account_repost_doc: repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc) @@ -134,7 +136,9 @@ def start_repost(account_repost_doc=str) -> None: doc = frappe.get_doc(x.voucher_type, x.voucher_no) if repost_doc.delete_cancelled_entries: - frappe.db.delete("GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}) + frappe.db.delete( + "GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name} + ) frappe.db.delete( "Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name} ) @@ -180,7 +184,9 @@ def validate_docs_for_deferred_accounting(sales_docs, purchase_docs): if docs_with_deferred_revenue or docs_with_deferred_expense: frappe.throw( _("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format( - frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])) + frappe.bold( + comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]) + ) ) ) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py index d6f7096132f..f631ef437d6 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py @@ -9,7 +9,6 @@ from frappe.utils import add_days, nowdate, today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request -from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import start_repost from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.utils import get_fiscal_year diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js index 6801408c7b3..b9621c06eb8 100644 --- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js @@ -1,53 +1,53 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Repost Payment Ledger', { - setup: function(frm) { +frappe.ui.form.on("Repost Payment Ledger", { + setup: function (frm) { frm.set_query("voucher_type", () => { return { filters: { - name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']] - } + name: ["in", ["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]], + }, }; }); - frm.fields_dict['repost_vouchers'].grid.get_field('voucher_type').get_query = function(doc) { + frm.fields_dict["repost_vouchers"].grid.get_field("voucher_type").get_query = function (doc) { return { filters: { - name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']] - } - } - } + name: ["in", ["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]], + }, + }; + }; - frm.fields_dict['repost_vouchers'].grid.get_field('voucher_no').get_query = function(doc) { + frm.fields_dict["repost_vouchers"].grid.get_field("voucher_no").get_query = function (doc) { if (doc.company) { return { filters: { company: doc.company, - docstatus: 1 - } - } + docstatus: 1, + }, + }; } - } - + }; }, - refresh: function(frm) { - - if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.repost_status)) { - frm.set_intro(__("Use 'Repost in background' button to trigger background job. Job can only be triggered when document is in Queued or Failed status.")); - var btn_label = __("Repost in background") + refresh: function (frm) { + if (frm.doc.docstatus == 1 && ["Queued", "Failed"].find((x) => x == frm.doc.repost_status)) { + frm.set_intro( + __( + "Use 'Repost in background' button to trigger background job. Job can only be triggered when document is in Queued or Failed status." + ) + ); + var btn_label = __("Repost in background"); frm.add_custom_button(btn_label, () => { frappe.call({ - method: 'erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.execute_repost_payment_ledger', + method: "erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.execute_repost_payment_ledger", args: { docname: frm.doc.name, - } + }, }); - frappe.msgprint(__('Reposting in the background.')); + frappe.msgprint(__("Reposting in the background.")); }); } - - } + }, }); - diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py index 209cad4f905..d383b870b2c 100644 --- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py @@ -40,10 +40,10 @@ def start_payment_ledger_repost(docname=None): frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", "") frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Completed") - except Exception as e: + except Exception: frappe.db.rollback() - traceback = frappe.get_traceback() + traceback = frappe.get_traceback(with_context=True) if traceback: message = "Traceback:
      " + traceback frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message) @@ -53,7 +53,7 @@ def start_payment_ledger_repost(docname=None): class RepostPaymentLedger(Document): def __init__(self, *args, **kwargs): - super(RepostPaymentLedger, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.vouchers = [] def before_validate(self): diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger_list.js b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger_list.js index e0451845ced..76d17fc6860 100644 --- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger_list.js +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger_list.js @@ -1,12 +1,12 @@ frappe.listview_settings["Repost Payment Ledger"] = { add_fields: ["repost_status"], - get_indicator: function(doc) { + get_indicator: function (doc) { var colors = { - 'Queued': 'orange', - 'Completed': 'green', - 'Failed': 'red', + Queued: "orange", + Completed: "green", + Failed: "red", }; let status = doc.repost_status; - return [__(status), colors[status], 'status,=,'+status]; + return [__(status), colors[status], "status,=," + status]; }, }; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 1a04841fd52..b2672424af9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -11,30 +11,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.setup(doc); } company() { + super.company(); erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); - - let me = this; - if (this.frm.doc.company) { - frappe.call({ - method: - "erpnext.accounts.party.get_party_account", - args: { - party_type: 'Customer', - party: this.frm.doc.customer, - company: this.frm.doc.company - }, - callback: (response) => { - if (response) me.frm.set_value("debit_to", response.message); - }, - }); - } } onload() { var me = this; super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format @@ -91,8 +76,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e if(doc.update_stock) this.show_stock_ledger(); - if (doc.docstatus == 1 && doc.outstanding_amount!=0 - && !(cint(doc.is_return) && doc.return_against)) { + if (doc.docstatus == 1 && doc.outstanding_amount!=0) { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), @@ -890,8 +874,8 @@ frappe.ui.form.on('Sales Invoice', { frm.events.append_time_log(frm, timesheet, 1.0); } }); - frm.refresh_field("timesheets"); frm.trigger("calculate_timesheet_totals"); + frm.refresh(); }, async get_exchange_rate(frm, from_currency, to_currency) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 0c337aa62ec..25f11b65e49 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -25,6 +25,7 @@ "is_consolidated", "is_return", "return_against", + "update_outstanding_for_self", "update_billed_amount_in_sales_order", "update_billed_amount_in_delivery_note", "is_debit_note", @@ -138,6 +139,7 @@ "loyalty_amount", "column_break_77", "loyalty_program", + "dont_create_loyalty_points", "loyalty_redemption_account", "loyalty_redemption_cost_center", "contact_and_address_tab", @@ -941,7 +943,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "hide_days": 1, "hide_seconds": 1, "label": "Taxes and Charges Calculation", @@ -1039,8 +1041,7 @@ "label": "Loyalty Program", "no_copy": 1, "options": "Loyalty Program", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -2153,6 +2154,24 @@ "fieldname": "update_billed_amount_in_delivery_note", "fieldtype": "Check", "label": "Update Billed Amount in Delivery Note" + }, + { + "default": "0", + "depends_on": "loyalty_program", + "fieldname": "dont_create_loyalty_points", + "fieldtype": "Check", + "label": "Don't Create Loyalty Points", + "no_copy": 1 + }, + { + "default": "1", + "depends_on": "eval: doc.is_return && doc.return_against", + "description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.", + "fieldname": "update_outstanding_for_self", + "fieldtype": "Check", + "label": "Update Outstanding for Self", + "no_copy": 1, + "print_hide": 1 } ], "icon": "fa fa-file-text", @@ -2165,7 +2184,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-11-23 16:56:29.679499", + "modified": "2024-03-22 17:50:34.395602", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2220,4 +2239,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3d18a860361..3ea4b91641c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -50,7 +50,7 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class SalesInvoice(SellingController): def __init__(self, *args, **kwargs): - super(SalesInvoice, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.status_updater = [ { "source_dt": "Sales Invoice Item", @@ -87,10 +87,10 @@ class SalesInvoice(SellingController): self.indicator_title = _("Paid") def validate(self): - super(SalesInvoice, self).validate() + super().validate() self.validate_auto_set_posting_time() - if not self.is_pos: + if not (self.is_pos or self.is_debit_note): self.so_dn_required() self.set_tax_withholding() @@ -245,7 +245,8 @@ class SalesInvoice(SellingController): self.calculate_taxes_and_totals() def before_save(self): - set_account_for_mode_of_payment(self) + self.set_account_for_mode_of_payment() + self.set_paid_amount() def on_submit(self): self.validate_pos_paid_amount() @@ -292,19 +293,20 @@ class SalesInvoice(SellingController): self.update_time_sheet(self.name) - if ( - frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" - ): + if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction": update_company_current_month_sales(self.company) self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) # create the loyalty point ledger entry if the customer is enrolled in any loyalty program - if not self.is_return and not self.is_consolidated and self.loyalty_program: - self.make_loyalty_point_entry() - elif ( - self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program + if ( + not self.is_return + and not self.is_consolidated + and self.loyalty_program + and not self.dont_create_loyalty_points ): + self.make_loyalty_point_entry() + elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program: against_si_doc = frappe.get_doc("Sales Invoice", self.return_against) against_si_doc.delete_loyalty_point_entry() against_si_doc.make_loyalty_point_entry() @@ -333,11 +335,11 @@ class SalesInvoice(SellingController): def check_if_consolidated_invoice(self): # since POS Invoice extends Sales Invoice, we explicitly check if doctype is Sales Invoice if self.doctype == "Sales Invoice" and self.is_consolidated: - invoice_or_credit_note = ( - "consolidated_credit_note" if self.is_return else "consolidated_invoice" - ) + invoice_or_credit_note = "consolidated_credit_note" if self.is_return else "consolidated_invoice" pos_closing_entry = frappe.get_all( - "POS Invoice Merge Log", filters={invoice_or_credit_note: self.name}, pluck="pos_closing_entry" + "POS Invoice Merge Log", + filters={invoice_or_credit_note: self.name}, + pluck="pos_closing_entry", ) if pos_closing_entry and pos_closing_entry[0]: msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format( @@ -349,13 +351,13 @@ class SalesInvoice(SellingController): def before_cancel(self): self.check_if_consolidated_invoice() - super(SalesInvoice, self).before_cancel() + super().before_cancel() self.update_time_sheet(None) def on_cancel(self): check_if_return_invoice_linked_with_payment_entry(self) - super(SalesInvoice, self).on_cancel() + super().on_cancel() self.check_sales_order_on_hold_or_close("sales_order") @@ -385,16 +387,12 @@ class SalesInvoice(SellingController): self.db_set("status", "Cancelled") self.db_set("repost_required", 0) - if ( - frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" - ): + if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction": update_company_current_month_sales(self.company) self.update_project() if not self.is_return and not self.is_consolidated and self.loyalty_program: self.delete_loyalty_point_entry() - elif ( - self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program - ): + elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program: against_si_doc = frappe.get_doc("Sales Invoice", self.return_against) against_si_doc.delete_loyalty_point_entry() against_si_doc.make_loyalty_point_entry() @@ -498,7 +496,7 @@ class SalesInvoice(SellingController): if not self.due_date and self.customer: self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company) - super(SalesInvoice, self).set_missing_values(for_validate) + super().set_missing_values(for_validate) print_format = pos.get("print_format") if pos else None if not print_format and not cint(frappe.db.get_value("Print Format", "POS Invoice", "disabled")): @@ -533,9 +531,6 @@ class SalesInvoice(SellingController): ): data.sales_invoice = sales_invoice - def on_update(self): - self.set_paid_amount() - def on_update_after_submit(self): if hasattr(self, "repost_required"): fields_to_check = [ @@ -566,6 +561,11 @@ class SalesInvoice(SellingController): self.paid_amount = paid_amount self.base_paid_amount = base_paid_amount + def set_account_for_mode_of_payment(self): + for payment in self.payments: + if not payment.account: + payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account") + def validate_time_sheets_are_submitted(self): for data in self.timesheets: if data.time_sheet: @@ -686,7 +686,8 @@ class SalesInvoice(SellingController): if account.report_type != "Balance Sheet": msg = ( - _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " " + _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + + " " ) msg += _( "You can change the parent account to a Balance Sheet account or select a different account." @@ -715,11 +716,16 @@ class SalesInvoice(SellingController): ) def validate_with_previous_doc(self): - super(SalesInvoice, self).validate_with_previous_doc( + super().validate_with_previous_doc( { "Sales Order": { "ref_dn_field": "sales_order", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + "compare_fields": [ + ["customer", "="], + ["company", "="], + ["project", "="], + ["currency", "="], + ], }, "Sales Order Item": { "ref_dn_field": "so_detail", @@ -729,7 +735,12 @@ class SalesInvoice(SellingController): }, "Delivery Note": { "ref_dn_field": "delivery_note", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + "compare_fields": [ + ["customer", "="], + ["company", "="], + ["project", "="], + ["currency", "="], + ], }, "Delivery Note Item": { "ref_dn_field": "dn_detail", @@ -784,13 +795,14 @@ class SalesInvoice(SellingController): } for key, value in prev_doc_field_map.items(): if frappe.db.get_single_value("Selling Settings", value[0]) == "Yes": - if frappe.get_value("Customer", self.customer, value[0]): continue for d in self.get("items"): if d.item_code and not d.get(key.lower().replace(" ", "_")) and not self.get(value[1]): - msgprint(_("{0} is mandatory for Item {1}").format(key, d.item_code), raise_exception=1) + msgprint( + _("{0} is mandatory for Item {1}").format(key, d.item_code), raise_exception=1 + ) def validate_proj_cust(self): """check for does customer belong to same project as entered..""" @@ -817,7 +829,7 @@ class SalesInvoice(SellingController): msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True) def validate_warehouse(self): - super(SalesInvoice, self).validate_warehouse() + super().validate_warehouse() for d in self.get_item_list(): if ( @@ -1051,6 +1063,10 @@ class SalesInvoice(SellingController): ) if grand_total and not self.is_internal_transfer(): + against_voucher = self.name + if self.is_return and self.return_against and not self.update_outstanding_for_self: + against_voucher = self.return_against + # Did not use base_grand_total to book rounding loss gle gl_entries.append( self.get_gl_dict( @@ -1064,9 +1080,7 @@ class SalesInvoice(SellingController): "debit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": against_voucher, "against_voucher_type": self.doctype, "cost_center": self.cost_center, "project": self.project, @@ -1144,7 +1158,9 @@ class SalesInvoice(SellingController): asset.db_set("disposal_date", None) if asset.calculate_depreciation: - posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") + posting_date = frappe.db.get_value( + "Sales Invoice", self.return_against, "posting_date" + ) reverse_depreciation_entry_made_after_disposal(asset, posting_date) reset_depreciation_schedule(asset, self.posting_date) @@ -1178,7 +1194,9 @@ class SalesInvoice(SellingController): else item.deferred_revenue_account ) - amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting) + amount, base_amount = self.get_amount_and_base_amount( + item, enable_discount_accounting + ) account_currency = get_account_currency(income_account) gl_entries.append( @@ -1202,7 +1220,7 @@ class SalesInvoice(SellingController): # expense account gl entries if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company): - gl_entries += super(SalesInvoice, self).get_gl_entries() + gl_entries += super().get_gl_entries() def get_asset(self, item): if item.get("asset"): @@ -1265,7 +1283,6 @@ class SalesInvoice(SellingController): def make_pos_gl_entries(self, gl_entries): if cint(self.is_pos): - skip_change_gl_entries = not cint( frappe.db.get_single_value("Accounts Settings", "post_change_gl_entries") ) @@ -1287,9 +1304,7 @@ class SalesInvoice(SellingController): "credit_in_account_currency": payment_mode.base_amount if self.party_account_currency == self.company_currency else payment_mode.amount, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center, }, @@ -1426,7 +1441,9 @@ class SalesInvoice(SellingController): "credit_in_account_currency": flt( self.rounding_adjustment, self.precision("rounding_adjustment") ), - "credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")), + "credit": flt( + self.base_rounding_adjustment, self.precision("base_rounding_adjustment") + ), "cost_center": round_off_cost_center if self.use_company_roundoff_cost_center else (self.cost_center or round_off_cost_center), @@ -1448,7 +1465,11 @@ class SalesInvoice(SellingController): ) billed_amt = billed_amt and billed_amt[0][0] or 0 frappe.db.set_value( - "Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified + "Delivery Note Item", + d.dn_detail, + "billed_amt", + billed_amt, + update_modified=update_modified, ) updated_delivery_notes.append(d.delivery_note) elif d.so_detail: @@ -1556,7 +1577,6 @@ class SalesInvoice(SellingController): and getdate(lp_details.from_date) <= getdate(self.posting_date) and (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)) ): - collection_factor = lp_details.collection_factor if lp_details.collection_factor else 1.0 points_earned = cint(eligible_amount / collection_factor) @@ -1693,12 +1713,8 @@ class SalesInvoice(SellingController): elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" # Check if outstanding amount is 0 due to credit note issued against invoice - elif ( - outstanding_amount <= 0 - and self.is_return == 0 - and frappe.db.get_value( - "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} - ) + elif self.is_return == 0 and frappe.db.get_value( + "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} ): self.status = "Credit Note Issued" elif self.is_return == 1: @@ -1811,16 +1827,15 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc filters={"parenttype": partytype, "parent": party}, ) companies = [d.company for d in companies] - if not company in companies: + if company not in companies: frappe.throw( - _("{0} not allowed to transact with {1}. Please change the Company.").format( - partytype, company - ) + _( + "{0} not allowed to transact with {1}. Please change the Company or add the Company in the 'Allowed To Transact With'-Section in the Customer record." + ).format(_(partytype), company) ) def update_linked_doc(doctype, name, inter_company_reference): - if doctype in ["Sales Invoice", "Purchase Invoice"]: ref_field = "inter_company_invoice_reference" else: @@ -1831,7 +1846,6 @@ def update_linked_doc(doctype, name, inter_company_reference): def unlink_inter_company_doc(doctype, name, inter_company_reference): - if doctype in ["Sales Invoice", "Purchase Invoice"]: ref_doc = "Purchase Invoice" if doctype == "Sales Invoice" else "Sales Invoice" ref_field = "inter_company_invoice_reference" @@ -1944,12 +1958,6 @@ def make_sales_return(source_name, target_doc=None): return make_return_doc("Sales Invoice", source_name, target_doc) -def set_account_for_mode_of_payment(self): - for data in self.payments: - if not data.account: - data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account") - - def get_inter_company_details(doc, doctype): if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]: parties = frappe.db.get_all( @@ -2012,16 +2020,13 @@ def get_internal_party(parties, link_doctype, doc): def validate_inter_company_transaction(doc, doctype): - details = get_inter_company_details(doc, doctype) price_list = ( doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list ) - valid_price_list = frappe.db.get_value( - "Price List", {"name": price_list, "buying": 1, "selling": 1} - ) + valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1}) if not valid_price_list and not doc.is_internal_transfer(): frappe.throw(_("Selected Price List should have buying and selling fields checked.")) @@ -2282,9 +2287,7 @@ def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, wa for item in doc.get("items"): item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item)) if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"): - item.warehouse = frappe.db.get_value( - "Purchase Order Item", item.purchase_order_item, "warehouse" - ) + item.warehouse = frappe.db.get_value("Purchase Order Item", item.purchase_order_item, "warehouse") def get_delivery_note_details(internal_reference): @@ -2388,10 +2391,6 @@ def get_loyalty_programs(customer): return lp_details -def on_doctype_update(): - frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"]) - - @frappe.whitelist() def create_invoice_discounting(source_name, target_doc=None): invoice = frappe.get_doc("Sales Invoice", source_name) @@ -2540,9 +2539,7 @@ def check_if_return_invoice_linked_with_payment_entry(self): # If a Return invoice is linked with payment entry along with other invoices, # the cancellation of the Return causes allocated amount to be greater than paid - if not frappe.db.get_single_value( - "Accounts Settings", "unlink_payment_on_cancellation_of_invoice" - ): + if not frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"): return payment_entries = [] diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index 1605b151a14..f971f68a454 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -2,34 +2,42 @@ // License: GNU General Public License v3. See license.txt // render -frappe.listview_settings['Sales Invoice'] = { - add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company", - "currency", "is_return"], - get_indicator: function(doc) { +frappe.listview_settings["Sales Invoice"] = { + add_fields: [ + "customer", + "customer_name", + "base_grand_total", + "outstanding_amount", + "due_date", + "company", + "currency", + "is_return", + ], + get_indicator: function (doc) { const status_colors = { - "Draft": "grey", - "Unpaid": "orange", - "Paid": "green", - "Return": "gray", + Draft: "grey", + Unpaid: "orange", + Paid: "green", + Return: "gray", "Credit Note Issued": "gray", "Unpaid and Discounted": "orange", "Partly Paid and Discounted": "yellow", "Overdue and Discounted": "red", - "Overdue": "red", + Overdue: "red", "Partly Paid": "yellow", - "Internal Transfer": "darkgrey" + "Internal Transfer": "darkgrey", }; - return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status]; + return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; }, right_column: "grand_total", - onload: function(listview) { - listview.page.add_action_item(__("Delivery Note"), ()=>{ + onload: function (listview) { + listview.page.add_action_item(__("Delivery Note"), () => { erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note"); }); - listview.page.add_action_item(__("Payment"), ()=>{ + listview.page.add_action_item(__("Payment"), () => { erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry"); }); - } + }, }; diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index d050299912d..87d297f4aa1 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt import copy -import unittest import frappe from frappe.model.dynamic_links import get_dynamic_link_map @@ -901,7 +900,7 @@ class TestSalesInvoice(FrappeTestCase): ] ) - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(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) @@ -927,7 +926,7 @@ class TestSalesInvoice(FrappeTestCase): write_off_account="_Test Write Off - TCP1", ) - pr = make_purchase_receipt( + make_purchase_receipt( company="_Test Company with perpetual inventory", item_code="_Test FG Item", warehouse="Stores - TCP1", @@ -1081,6 +1080,44 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(pos.grand_total, 100.0) self.assertEqual(pos.write_off_amount, 10) + def test_ledger_entries_of_return_pos_invoice(self): + make_pos_profile() + + pos = create_sales_invoice(do_not_save=True) + pos.is_pos = 1 + pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos.save().submit() + self.assertEqual(pos.outstanding_amount, 0.0) + self.assertEqual(pos.status, "Paid") + + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + + pos_return = make_sales_return(pos.name) + pos_return.save().submit() + pos_return.reload() + pos.reload() + self.assertEqual(pos_return.is_return, 1) + self.assertEqual(pos_return.return_against, pos.name) + self.assertEqual(pos_return.outstanding_amount, 0.0) + self.assertEqual(pos_return.status, "Return") + self.assertEqual(pos.outstanding_amount, 0.0) + self.assertEqual(pos.status, "Credit Note Issued") + + expected = ( + ("Cash - _TC", 0.0, 100.0, pos_return.name, None), + ("Debtors - _TC", 0.0, 100.0, pos_return.name, pos_return.name), + ("Debtors - _TC", 100.0, 0.0, pos_return.name, pos_return.name), + ("Sales - _TC", 100.0, 0.0, pos_return.name, None), + ) + res = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pos_return.name, "is_cancelled": 0}, + fields=["account", "debit", "credit", "voucher_no", "against_voucher"], + order_by="account, debit, credit", + as_list=1, + ) + self.assertEqual(expected, res) + def test_pos_with_no_gl_entry_for_change_amount(self): frappe.db.set_value("Accounts Settings", None, "post_change_gl_entries", 0) @@ -1270,7 +1307,7 @@ class TestSalesInvoice(FrappeTestCase): expected_values = dict( (d[0], d) for d in [["Debtors - TCP1", 100.0, 0.0], ["Sales - TCP1", 0.0, 100.0]] ) - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(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) @@ -1294,7 +1331,7 @@ class TestSalesInvoice(FrappeTestCase): [test_records[1]["items"][0]["income_account"], 0.0, 100.0], ] ) - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(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) @@ -1384,9 +1421,7 @@ class TestSalesInvoice(FrappeTestCase): si.submit() self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse")) - self.assertEqual( - frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name - ) + self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name) return si @@ -1528,8 +1563,27 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(party_credited, 1000) # Check outstanding amount - self.assertFalse(si1.outstanding_amount) - self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) + self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000) + self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500) + + def test_zero_qty_return_invoice_with_stock_effect(self): + cr_note = create_sales_invoice(qty=-1, rate=300, is_return=1, do_not_submit=True) + cr_note.update_stock = True + cr_note.items[0].qty = 0 + self.assertRaises(frappe.ValidationError, cr_note.save) + + def test_return_invoice_with_account_mismatch(self): + debtors2 = create_account( + parent_account="Accounts Receivable - _TC", + account_name="Debtors 2", + company="_Test Company", + account_type="Receivable", + ) + si = create_sales_invoice(qty=1, rate=1000) + cr_note = create_sales_invoice( + qty=-1, rate=1000, is_return=1, return_against=si.name, debit_to=debtors2, do_not_save=True + ) + self.assertRaises(frappe.ValidationError, cr_note.save) def test_gle_made_when_asset_is_returned(self): create_asset_data() @@ -1695,7 +1749,7 @@ class TestSalesInvoice(FrappeTestCase): "credit", "credit_in_account_currency", ): - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][field], gle[field]) # cancel @@ -2237,12 +2291,8 @@ class TestSalesInvoice(FrappeTestCase): def test_duplicate_due_date_in_terms(self): si = create_sales_invoice(do_not_save=1) - si.append( - "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) - ) - si.append( - "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) - ) + si.append("payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50)) + si.append("payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50)) self.assertRaises(frappe.ValidationError, si.insert) @@ -2436,9 +2486,7 @@ class TestSalesInvoice(FrappeTestCase): item.no_of_months = 12 item.save() - si = create_sales_invoice( - item=item.name, posting_date="2019-01-16", rate=50000, do_not_submit=True - ) + si = create_sales_invoice(item=item.name, posting_date="2019-01-16", rate=50000, do_not_submit=True) si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2019-01-16" si.items[0].service_end_date = "2019-03-31" @@ -2758,21 +2806,16 @@ class TestSalesInvoice(FrappeTestCase): item.save() sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True) - self.assertEqual( - sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC" - ) + self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC") # Apply discount sales_invoice.apply_discount_on = "Net Total" sales_invoice.discount_amount = 300 sales_invoice.save() - self.assertEqual( - sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC" - ) + self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC") @change_settings("Selling Settings", {"enable_discount_accounting": 1}) def test_sales_invoice_with_discount_accounting_enabled(self): - discount_account = create_account( account_name="Discount Account", parent_account="Indirect Expenses - _TC", @@ -2790,7 +2833,6 @@ class TestSalesInvoice(FrappeTestCase): @change_settings("Selling Settings", {"enable_discount_accounting": 1}) def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): - from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import ( update_repost_settings, ) @@ -2803,7 +2845,7 @@ class TestSalesInvoice(FrappeTestCase): company="_Test Company", ) - tds_payable_account = create_account( + create_account( account_name="TDS Payable", account_type="Tax", parent_account="Duties and Taxes - _TC", @@ -3125,9 +3167,7 @@ class TestSalesInvoice(FrappeTestCase): """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - over_billing_allowance = frappe.db.get_single_value( - "Accounts Settings", "over_billing_allowance" - ) + over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) dn = create_delivery_note() @@ -3319,7 +3359,7 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(len(journals), 1) je_type = frappe.get_cached_value("Journal Entry", journals[0], "voucher_type") self.assertEqual(je_type, "Exchange Gain Or Loss") - ledger_outstanding = frappe.db.get_all( + frappe.db.get_all( "Payment Ledger Entry", filters={"against_voucher_no": si.name, "delinked": 0}, fields=["sum(amount), sum(amount_in_account_currency)"], @@ -3381,40 +3421,104 @@ class TestSalesInvoice(FrappeTestCase): si.items[0].rate = 10 si.save() + def test_taxes_merging_from_delivery_note(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note -def get_sales_invoice_for_e_invoice(): - si = make_sales_invoice_for_ewaybill() - si.naming_series = "INV-2020-.#####" - si.items = [] - si.append( - "items", - { - "item_code": "_Test Item", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 2000, - "rate": 12, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }, - ) + dn1 = create_delivery_note(do_not_submit=1) + dn1.items[0].qty = 10 + dn1.items[0].rate = 100 + dn1.append( + "taxes", + { + "charge_type": "Actual", + "account_head": "Freight and Forwarding Charges - _TC", + "description": "movement charges", + "tax_amount": 100, + }, + ) + dn1.append( + "taxes", + { + "charge_type": "Actual", + "account_head": "Marketing Expenses - _TC", + "description": "marketing", + "tax_amount": 150, + }, + ) + dn1.save().submit() - si.append( - "items", - { - "item_code": "_Test Item 2", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 420, - "rate": 15, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }, - ) + dn2 = create_delivery_note(do_not_submit=1) + dn2.items[0].qty = 5 + dn2.items[0].rate = 100 + dn2.append( + "taxes", + { + "charge_type": "Actual", + "account_head": "Freight and Forwarding Charges - _TC", + "description": "movement charges", + "tax_amount": 20, + }, + ) + dn2.append( + "taxes", + { + "charge_type": "Actual", + "account_head": "Miscellaneous Expenses - _TC", + "description": "marketing", + "tax_amount": 60, + }, + ) + dn2.save().submit() - return si + # si = make_sales_invoice(dn1.name) + si = create_sales_invoice(do_not_submit=True) + si.customer = dn1.customer + si.items.clear() + + from frappe.model.mapper import map_docs + + map_docs( + method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice", + source_names=frappe.json.dumps([dn1.name, dn2.name]), + target_doc=si, + args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}), + ) + si.save().submit() + + expected = [ + { + "charge_type": "Actual", + "account_head": "Freight and Forwarding Charges - _TC", + "tax_amount": 120.0, + "total": 1520.0, + "base_total": 1520.0, + }, + { + "charge_type": "Actual", + "account_head": "Marketing Expenses - _TC", + "tax_amount": 150.0, + "total": 1670.0, + "base_total": 1670.0, + }, + { + "charge_type": "Actual", + "account_head": "Miscellaneous Expenses - _TC", + "tax_amount": 60.0, + "total": 1610.0, + "base_total": 1610.0, + }, + ] + actual = [ + dict( + charge_type=x.charge_type, + account_head=x.account_head, + tax_amount=x.tax_amount, + total=x.total, + base_total=x.base_total, + ) + for x in si.taxes + ] + self.assertEqual(expected, actual) def check_gl_entries(doc, voucher_no, expected_gle, posting_date): diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index e236577e118..9e0a7983b74 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -108,7 +108,7 @@ "fieldname": "rate", "fieldtype": "Float", "in_list_view": 1, - "label": "Rate", + "label": "Tax Rate", "oldfieldname": "rate", "oldfieldtype": "Currency" }, @@ -218,7 +218,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-10-17 13:08:17.776528", + "modified": "2024-01-14 10:08:17.776528", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", @@ -227,4 +227,4 @@ "sort_field": "modified", "sort_order": "ASC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index d9009bae4c0..1bedf2c743e 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -22,7 +22,7 @@ class SalesTaxesandChargesTemplate(Document): def autoname(self): if self.company and self.title: abbr = frappe.get_cached_value("Company", self.company, "abbr") - self.name = "{0} - {1}".format(self.title, abbr) + self.name = f"{self.title} - {abbr}" def set_missing_values(self): for data in self.taxes: @@ -37,10 +37,8 @@ def valdiate_taxes_and_charges_template(doc): if doc.is_default == 1: frappe.db.sql( - """update `tab{0}` set is_default = 0 - where is_default = 1 and name != %s and company = %s""".format( - doc.doctype - ), + f"""update `tab{doc.doctype}` set is_default = 0 + where is_default = 1 and name != %s and company = %s""", (doc.name, doc.company), ) diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.js b/erpnext/accounts/doctype/share_transfer/share_transfer.js index 6317c9c8c0d..dd9d9628aa8 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.js +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.js @@ -3,21 +3,19 @@ frappe.provide("erpnext.share_transfer"); -frappe.ui.form.on('Share Transfer', { - refresh: function(frm) { +frappe.ui.form.on("Share Transfer", { + refresh: function (frm) { // Don't show Parties which are a Company - let shareholders = ['from_shareholder', 'to_shareholder']; + let shareholders = ["from_shareholder", "to_shareholder"]; shareholders.forEach((shareholder) => { - frm.fields_dict[shareholder].get_query = function() { + frm.fields_dict[shareholder].get_query = function () { return { - filters: [ - ["Shareholder", "is_company", "=", 0] - ] + filters: [["Shareholder", "is_company", "=", 0]], }; }; }); if (frm.doc.docstatus == 1 && frm.doc.equity_or_liability_account && frm.doc.asset_account) { - frm.add_custom_button(__('Create Journal Entry'), function () { + frm.add_custom_button(__("Create Journal Entry"), function () { erpnext.share_transfer.make_jv(frm); }); } @@ -25,54 +23,59 @@ frappe.ui.form.on('Share Transfer', { frm.toggle_reqd("asset_account", frm.doc.transfer_type != "Transfer"); }, no_of_shares: (frm) => { - if (frm.doc.rate != undefined || frm.doc.rate != null){ + if (frm.doc.rate != undefined || frm.doc.rate != null) { erpnext.share_transfer.update_amount(frm); } }, rate: (frm) => { - if (frm.doc.no_of_shares != undefined || frm.doc.no_of_shares != null){ + if (frm.doc.no_of_shares != undefined || frm.doc.no_of_shares != null) { erpnext.share_transfer.update_amount(frm); } }, - company: async function(frm) { + company: async function (frm) { if (frm.doc.company) { - let currency = (await frappe.db.get_value("Company", frm.doc.company, "default_currency")).message.default_currency; - frm.set_query("equity_or_liability_account", function() { + let currency = (await frappe.db.get_value("Company", frm.doc.company, "default_currency")).message + .default_currency; + frm.set_query("equity_or_liability_account", function () { return { filters: { - "is_group":0, - "root_type": ["in",["Equity","Liability"]], - "company": frm.doc.company, - "account_currency": currency - } + is_group: 0, + root_type: ["in", ["Equity", "Liability"]], + company: frm.doc.company, + account_currency: currency, + }, }; }); - frm.set_query("asset_account", function() { + frm.set_query("asset_account", function () { return { filters: { - "is_group":0, - "root_type":"Asset", - "company": frm.doc.company, - "account_currency": currency - } + is_group: 0, + root_type: "Asset", + company: frm.doc.company, + account_currency: currency, + }, }; }); } }, - transfer_type: function(frm) { + transfer_type: function (frm) { frm.toggle_reqd("asset_account", frm.doc.transfer_type != "Transfer"); - } + }, }); -erpnext.share_transfer.update_amount = function(frm) { +erpnext.share_transfer.update_amount = function (frm) { frm.doc.amount = frm.doc.no_of_shares * frm.doc.rate; frm.refresh_field("amount"); }; erpnext.share_transfer.make_jv = function (frm) { - var account, payment_account, credit_applicant_type, credit_applicant, - debit_applicant_type, debit_applicant; + var account, + payment_account, + credit_applicant_type, + credit_applicant, + debit_applicant_type, + debit_applicant; if (frm.doc.transfer_type == "Transfer") { account = frm.doc.equity_or_liability_account; @@ -81,16 +84,14 @@ erpnext.share_transfer.make_jv = function (frm) { credit_applicant = frm.doc.to_shareholder; debit_applicant_type = "Shareholder"; debit_applicant = frm.doc.from_shareholder; - } - else if (frm.doc.transfer_type == "Issue") { + } else if (frm.doc.transfer_type == "Issue") { account = frm.doc.asset_account; payment_account = frm.doc.equity_or_liability_account; credit_applicant_type = "Shareholder"; credit_applicant = frm.doc.to_shareholder; debit_applicant_type = ""; debit_applicant = ""; - } - else { + } else { account = frm.doc.equity_or_liability_account; payment_account = frm.doc.asset_account; credit_applicant_type = ""; @@ -100,19 +101,19 @@ erpnext.share_transfer.make_jv = function (frm) { } frappe.call({ args: { - "company": frm.doc.company, - "account": account, - "amount": frm.doc.amount, - "payment_account": payment_account, - "credit_applicant_type": credit_applicant_type, - "credit_applicant": credit_applicant, - "debit_applicant_type": debit_applicant_type, - "debit_applicant": debit_applicant + company: frm.doc.company, + account: account, + amount: frm.doc.amount, + payment_account: payment_account, + credit_applicant_type: credit_applicant_type, + credit_applicant: credit_applicant, + debit_applicant_type: debit_applicant_type, + debit_applicant: debit_applicant, }, method: "erpnext.accounts.doctype.share_transfer.share_transfer.make_jv_entry", callback: function (r) { var doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); - } + }, }); }; diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.py b/erpnext/accounts/doctype/share_transfer/share_transfer.py index 4f49843c1eb..21f9b1c2595 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.py @@ -178,7 +178,9 @@ class ShareTransfer(Document): doc = self.get_shareholder_doc(shareholder) for entry in doc.share_balance: if ( - entry.share_type != self.share_type or entry.from_no > self.to_no or entry.to_no < self.from_no + entry.share_type != self.share_type + or entry.from_no > self.to_no + or entry.to_no < self.from_no ): continue # since query lies outside bounds elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: # both inside @@ -230,7 +232,9 @@ class ShareTransfer(Document): for entry in current_entries: # use spaceage logic here if ( - entry.share_type != self.share_type or entry.from_no > self.to_no or entry.to_no < self.from_no + entry.share_type != self.share_type + or entry.from_no > self.to_no + or entry.to_no < self.from_no ): new_entries.append(entry) continue # since query lies outside bounds @@ -240,7 +244,9 @@ class ShareTransfer(Document): if entry.to_no == self.to_no: pass # nothing to append else: - new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate)) + new_entries.append( + self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate) + ) else: if entry.to_no == self.to_no: new_entries.append( @@ -250,7 +256,9 @@ class ShareTransfer(Document): new_entries.append( self.return_share_balance_entry(entry.from_no, self.from_no - 1, entry.rate) ) - new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate)) + new_entries.append( + self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate) + ) elif entry.from_no >= self.from_no and entry.to_no <= self.to_no: # split and check pass # nothing to append @@ -282,7 +290,7 @@ class ShareTransfer(Document): def get_shareholder_doc(self, shareholder): # Get Shareholder doc based on the Shareholder name if shareholder: - query_filters = {"name": shareholder} + pass name = frappe.db.get_value("Shareholder", {"name": shareholder}, "name") diff --git a/erpnext/accounts/doctype/share_type/share_type.js b/erpnext/accounts/doctype/share_type/share_type.js index 1ae85e3a57e..5dfb7607fed 100644 --- a/erpnext/accounts/doctype/share_type/share_type.js +++ b/erpnext/accounts/doctype/share_type/share_type.js @@ -1,8 +1,6 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Share Type', { - refresh: function(frm) { - - } +frappe.ui.form.on("Share Type", { + refresh: function (frm) {}, }); diff --git a/erpnext/accounts/doctype/shareholder/shareholder.js b/erpnext/accounts/doctype/shareholder/shareholder.js index c6f101e7f31..2a31b805ab3 100644 --- a/erpnext/accounts/doctype/shareholder/shareholder.js +++ b/erpnext/accounts/doctype/shareholder/shareholder.js @@ -1,39 +1,38 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Shareholder', { - refresh: function(frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Shareholder' }; +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); + frm.toggle_display(["contact_html"], !frm.doc.__islocal); if (frm.doc.__islocal) { - hide_field(['contact_html']); + hide_field(["contact_html"]); frappe.contacts.clear_address_and_contact(frm); - } - else { - if (frm.doc.is_company){ - hide_field(['company']); + } else { + if (frm.doc.is_company) { + hide_field(["company"]); } else { - unhide_field(['contact_html']); + unhide_field(["contact_html"]); frappe.contacts.render_address_and_contact(frm); } } - if (frm.doc.folio_no != undefined){ - frm.add_custom_button(__("Share Balance"), function(){ + if (frm.doc.folio_no != undefined) { + frm.add_custom_button(__("Share Balance"), function () { frappe.route_options = { - "shareholder": frm.doc.name, + shareholder: frm.doc.name, }; frappe.set_route("query-report", "Share Balance"); }); - frm.add_custom_button(__("Share Ledger"), function(){ + frm.add_custom_button(__("Share Ledger"), function () { frappe.route_options = { - "shareholder": frm.doc.name, + shareholder: frm.doc.name, }; frappe.set_route("query-report", "Share Ledger"); }); - let fields = ['title', 'folio_no', 'company']; + let fields = ["title", "folio_no", "company"]; fields.forEach((fieldname) => { frm.fields_dict[fieldname].df.read_only = 1; frm.refresh_fields(fieldname); @@ -44,11 +43,11 @@ frappe.ui.form.on('Shareholder', { }, validate: (frm) => { let contact_list = { - contacts: [] + contacts: [], }; - $('div[data-fieldname=contact_html] > .address-box').each( (index, ele) => { - contact_list.contacts.push(ele.innerText.replace(' Edit', '')); + $("div[data-fieldname=contact_html] > .address-box").each((index, ele) => { + contact_list.contacts.push(ele.innerText.replace(" Edit", "")); }); frm.doc.contact_list = JSON.stringify(contact_list); - } + }, }); diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js index 8e4b806f02d..1ece3e6c3dd 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js @@ -1,33 +1,33 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.provide('erpnext.accounts.dimensions'); +frappe.provide("erpnext.accounts.dimensions"); -frappe.ui.form.on('Shipping Rule', { - onload: function(frm) { +frappe.ui.form.on("Shipping Rule", { + onload: function (frm) { erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, - company: function(frm) { + company: function (frm) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, - refresh: function(frm) { - frm.set_query("account", function() { + refresh: function (frm) { + frm.set_query("account", function () { return { filters: { - company: frm.doc.company - } - } - }) + company: frm.doc.company, + }, + }; + }); - frm.trigger('toggle_reqd'); + frm.trigger("toggle_reqd"); }, - calculate_based_on: function(frm) { - frm.trigger('toggle_reqd'); + calculate_based_on: function (frm) { + frm.trigger("toggle_reqd"); + }, + toggle_reqd: function (frm) { + frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === "Fixed"); + frm.toggle_reqd("conditions", frm.doc.calculate_based_on !== "Fixed"); }, - toggle_reqd: function(frm) { - frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === 'Fixed'); - frm.toggle_reqd("conditions", frm.doc.calculate_based_on !== 'Fixed'); - } }); diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index 1d79503a05e..2c6d5eb5f11 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -44,7 +44,8 @@ class ShippingRule(Document): zero_to_values.append(d) elif d.from_value >= d.to_value: throw( - _("From value must be less than to value in row {0}").format(d.idx), FromGreaterThanToError + _("From value must be less than to value in row {0}").format(d.idx), + FromGreaterThanToError, ) # check if more than two or more rows has To Value = 0 @@ -87,9 +88,7 @@ class ShippingRule(Document): def get_shipping_amount_from_rules(self, value): for condition in self.get("conditions"): - if not condition.to_value or ( - flt(condition.from_value) <= flt(value) <= flt(condition.to_value) - ): + if not condition.to_value or (flt(condition.from_value) <= flt(value) <= flt(condition.to_value)): return condition.shipping_amount return 0.0 @@ -104,7 +103,9 @@ class ShippingRule(Document): ) if shipping_country not in [d.country for d in self.countries]: frappe.throw( - _("Shipping rule not applicable for country {0} in Shipping Address").format(shipping_country) + _("Shipping rule not applicable for country {0} in Shipping Address").format( + shipping_country + ) ) def add_shipping_rule_to_tax_table(self, doc, shipping_amount): @@ -172,11 +173,9 @@ class ShippingRule(Document): messages = [] for d1, d2 in overlaps: messages.append( - "%s-%s = %s " - % (d1.from_value, d1.to_value, fmt_money(d1.shipping_amount, currency=company_currency)) + f"{d1.from_value}-{d1.to_value} = {fmt_money(d1.shipping_amount, currency=company_currency)} " + _("and") - + " %s-%s = %s" - % (d2.from_value, d2.to_value, fmt_money(d2.shipping_amount, currency=company_currency)) + + f" {d2.from_value}-{d2.to_value} = {fmt_money(d2.shipping_amount, currency=company_currency)}" ) msgprint("\n".join(messages), raise_exception=OverlappingConditionError) diff --git a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py index a24e834c572..a5a0ada8ba5 100644 --- a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py @@ -45,7 +45,6 @@ class TestShippingRule(unittest.TestCase): def create_shipping_rule(shipping_rule_type, shipping_rule_name): - if frappe.db.exists("Shipping Rule", shipping_rule_name): return frappe.get_doc("Shipping Rule", shipping_rule_name) diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js index 4b351f9d764..60981f4b1d1 100644 --- a/erpnext/accounts/doctype/subscription/subscription.js +++ b/erpnext/accounts/doctype/subscription/subscription.js @@ -1,104 +1,99 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Subscription', { - setup: function(frm) { - frm.set_query('party_type', function() { - return { - filters : { - name: ['in', ['Customer', 'Supplier']] - } - } - }); - - frm.set_query('cost_center', function() { +frappe.ui.form.on("Subscription", { + setup: function (frm) { + frm.set_query("party_type", function () { return { filters: { - company: frm.doc.company - } + name: ["in", ["Customer", "Supplier"]], + }, }; }); - frm.set_query('sales_tax_template', function () { + frm.set_query("cost_center", function () { return { filters: { - company: frm.doc.company - } + company: frm.doc.company, + }, + }; + }); + + frm.set_query("sales_tax_template", function () { + return { + filters: { + company: frm.doc.company, + }, }; }); }, - refresh: function(frm) { - if(!frm.is_new()){ - if(frm.doc.status !== 'Cancelled'){ - frm.add_custom_button( - __('Cancel Subscription'), - () => frm.events.cancel_this_subscription(frm) + refresh: function (frm) { + if (!frm.is_new()) { + if (frm.doc.status !== "Cancelled") { + frm.add_custom_button(__("Cancel Subscription"), () => + frm.events.cancel_this_subscription(frm) ); - frm.add_custom_button( - __('Fetch Subscription Updates'), - () => frm.events.get_subscription_updates(frm) + frm.add_custom_button(__("Fetch Subscription Updates"), () => + frm.events.get_subscription_updates(frm) ); - } - else if(frm.doc.status === 'Cancelled'){ - frm.add_custom_button( - __('Restart Subscription'), - () => frm.events.renew_this_subscription(frm) + } else if (frm.doc.status === "Cancelled") { + frm.add_custom_button(__("Restart Subscription"), () => + frm.events.renew_this_subscription(frm) ); } } }, - cancel_this_subscription: function(frm) { + cancel_this_subscription: function (frm) { const doc = frm.doc; frappe.confirm( - __('This action will stop future billing. Are you sure you want to cancel this subscription?'), - function() { + __("This action will stop future billing. Are you sure you want to cancel this subscription?"), + function () { frappe.call({ - method: - "erpnext.accounts.doctype.subscription.subscription.cancel_subscription", - args: {name: doc.name}, - callback: function(data){ - if(!data.exc){ + method: "erpnext.accounts.doctype.subscription.subscription.cancel_subscription", + args: { name: doc.name }, + callback: function (data) { + if (!data.exc) { frm.reload_doc(); } - } + }, }); } ); }, - renew_this_subscription: function(frm) { + renew_this_subscription: function (frm) { const doc = frm.doc; frappe.confirm( - __('You will lose records of previously generated invoices. Are you sure you want to restart this subscription?'), - function() { + __( + "You will lose records of previously generated invoices. Are you sure you want to restart this subscription?" + ), + function () { frappe.call({ - method: - "erpnext.accounts.doctype.subscription.subscription.restart_subscription", - args: {name: doc.name}, - callback: function(data){ - if(!data.exc){ + method: "erpnext.accounts.doctype.subscription.subscription.restart_subscription", + args: { name: doc.name }, + callback: function (data) { + if (!data.exc) { frm.reload_doc(); } - } + }, }); } ); }, - get_subscription_updates: function(frm) { + get_subscription_updates: function (frm) { const doc = frm.doc; frappe.call({ - method: - "erpnext.accounts.doctype.subscription.subscription.get_subscription_updates", - args: {name: doc.name}, + method: "erpnext.accounts.doctype.subscription.subscription.get_subscription_updates", + args: { name: doc.name }, freeze: true, - callback: function(data){ - if(!data.exc){ + callback: function (data) { + if (!data.exc) { frm.reload_doc(); } - } + }, }); - } + }, }); diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 9dab4e91fba..fea2ae9bf8e 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -349,15 +349,15 @@ class Subscription(Document): company = self.get("company") or get_default_company() if not company: frappe.throw( - _("Company is mandatory was generating invoice. Please set default company in Global Defaults") + _( + "Company is mandatory for generating an invoice. Please set a default company in Global Defaults." + ) ) invoice.company = company invoice.set_posting_time = 1 invoice.posting_date = ( - self.current_invoice_start - if self.generate_invoice_at_period_start - else self.current_invoice_end + self.current_invoice_start if self.generate_invoice_at_period_start else self.current_invoice_end ) invoice.cost_center = self.cost_center @@ -559,10 +559,9 @@ class Subscription(Document): 3. Change the `Subscription` status to 'Cancelled' """ - if not self.is_current_invoice_generated( - self.current_invoice_start, self.current_invoice_end - ) and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): - + if not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) and ( + self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice() + ): prorate = frappe.db.get_single_value("Subscription Settings", "prorate") self.generate_invoice(prorate) @@ -594,7 +593,7 @@ class Subscription(Document): """ current_invoice = self.get_current_invoice() if not current_invoice: - frappe.throw(_("Current invoice {0} is missing").format(current_invoice.invoice)) + frappe.throw(_("Subscription {0}: Current invoice is missing.").format(frappe.bold(self.name))) else: if not self.has_outstanding_invoice(): self.status = "Active" @@ -607,10 +606,11 @@ class Subscription(Document): # Generate invoices periodically even if current invoice are unpaid if ( self.generate_new_invoices_past_due_date - and not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) + and not self.is_current_invoice_generated( + self.current_invoice_start, self.current_invoice_end + ) and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()) ): - prorate = frappe.db.get_single_value("Subscription Settings", "prorate") self.generate_invoice(prorate) @@ -626,7 +626,7 @@ class Subscription(Document): Returns `True` if the most recent invoice for the `Subscription` is not paid """ doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" - current_invoice = self.get_current_invoice() + self.get_current_invoice() invoice_list = [d.invoice for d in self.invoices] outstanding_invoices = frappe.get_all( diff --git a/erpnext/accounts/doctype/subscription/subscription_list.js b/erpnext/accounts/doctype/subscription/subscription_list.js index 6490ff3776e..55430a15c5d 100644 --- a/erpnext/accounts/doctype/subscription/subscription_list.js +++ b/erpnext/accounts/doctype/subscription/subscription_list.js @@ -1,17 +1,17 @@ -frappe.listview_settings['Subscription'] = { - get_indicator: function(doc) { - if(doc.status === 'Trialling') { +frappe.listview_settings["Subscription"] = { + get_indicator: function (doc) { + if (doc.status === "Trialling") { return [__("Trialling"), "green"]; - } else if(doc.status === 'Active') { + } else if (doc.status === "Active") { return [__("Active"), "green"]; - } else if(doc.status === 'Completed') { - return [__("Completed"), "green"]; - } else if(doc.status === 'Past Due Date') { + } else if (doc.status === "Completed") { + return [__("Completed"), "green"]; + } else if (doc.status === "Past Due Date") { return [__("Past Due Date"), "orange"]; - } else if(doc.status === 'Unpaid') { + } else if (doc.status === "Unpaid") { return [__("Unpaid"), "red"]; - } else if(doc.status === 'Cancelled') { + } else if (doc.status === "Cancelled") { return [__("Cancelled"), "gray"]; } - } + }, }; diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 89ba0c8055e..2f0b87e9ea2 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -1,7 +1,6 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe from frappe.tests.utils import FrappeTestCase @@ -85,9 +84,7 @@ def create_parties(): customer = frappe.new_doc("Customer") customer.customer_name = "_Test Subscription Customer" customer.billing_currency = "USD" - customer.append( - "accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"} - ) + customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"}) customer.insert() @@ -358,9 +355,7 @@ class TestSubscription(FrappeTestCase): invoice = subscription.get_current_invoice() diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt( - date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 - ) + plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) prorate_factor = flt(diff / plan_days) self.assertEqual( @@ -417,9 +412,7 @@ class TestSubscription(FrappeTestCase): invoice = subscription.get_current_invoice() diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt( - date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 - ) + plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) prorate_factor = flt(diff / plan_days) self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js index 00727f103f9..125dc7dd9ae 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js @@ -1,10 +1,10 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Subscription Plan', { - price_determination: function(frm) { - frm.toggle_reqd("cost", frm.doc.price_determination === 'Fixed rate'); - frm.toggle_reqd("price_list", frm.doc.price_determination === 'Based on price list'); +frappe.ui.form.on("Subscription Plan", { + price_determination: function (frm) { + frm.toggle_reqd("cost", frm.doc.price_determination === "Fixed rate"); + frm.toggle_reqd("price_list", frm.doc.price_determination === "Based on price list"); }, subscription_plan: function (frm) { diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index 75223c2ccca..4c4a812dd77 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -21,9 +21,7 @@ class SubscriptionPlan(Document): @frappe.whitelist() -def get_plan_rate( - plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1 -): +def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1): plan = frappe.get_doc("Subscription Plan", plan) if plan.price_determination == "Fixed Rate": return plan.cost * prorate_factor diff --git a/erpnext/accounts/doctype/tax_category/tax_category.js b/erpnext/accounts/doctype/tax_category/tax_category.js index 4b63edbde2d..fe60e037b16 100644 --- a/erpnext/accounts/doctype/tax_category/tax_category.js +++ b/erpnext/accounts/doctype/tax_category/tax_category.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Tax Category', { - refresh: function(frm) { - - } +frappe.ui.form.on("Tax Category", { + refresh: function (frm) {}, }); diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.js b/erpnext/accounts/doctype/tax_rule/tax_rule.js index bc497163e8b..b8c68e8d81d 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.js +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.js @@ -1,40 +1,40 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on("Tax Rule", "customer", function(frm) { - if(frm.doc.customer) { +frappe.ui.form.on("Tax Rule", "customer", function (frm) { + if (frm.doc.customer) { frappe.call({ - method:"erpnext.accounts.doctype.tax_rule.tax_rule.get_party_details", + method: "erpnext.accounts.doctype.tax_rule.tax_rule.get_party_details", args: { - "party": frm.doc.customer, - "party_type": "customer" + party: frm.doc.customer, + party_type: "customer", }, - callback: function(r) { - if(!r.exc) { - $.each(r.message, function(k, v) { + callback: function (r) { + if (!r.exc) { + $.each(r.message, function (k, v) { frm.set_value(k, v); }); } - } + }, }); } }); -frappe.ui.form.on("Tax Rule", "supplier", function(frm) { - if(frm.doc.supplier) { +frappe.ui.form.on("Tax Rule", "supplier", function (frm) { + if (frm.doc.supplier) { frappe.call({ - method:"erpnext.accounts.doctype.tax_rule.tax_rule.get_party_details", + method: "erpnext.accounts.doctype.tax_rule.tax_rule.get_party_details", args: { - "party": frm.doc.supplier, - "party_type": "supplier" + party: frm.doc.supplier, + party_type: "supplier", }, - callback: function(r) { - if(!r.exc) { - $.each(r.message, function(k, v) { + callback: function (r) { + if (!r.exc) { + $.each(r.message, function (k, v) { frm.set_value(k, v); }); } - } + }, }); } }); diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index 4d201292ed2..959c3b57456 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -82,27 +82,23 @@ class TaxRule(Document): for d in filters: if conds: conds += " and " - conds += """ifnull({0}, '') = {1}""".format(d, frappe.db.escape(cstr(filters[d]))) + conds += f"""ifnull({d}, '') = {frappe.db.escape(cstr(filters[d]))}""" if self.from_date and self.to_date: - conds += """ and ((from_date > '{from_date}' and from_date < '{to_date}') or - (to_date > '{from_date}' and to_date < '{to_date}') or - ('{from_date}' > from_date and '{from_date}' < to_date) or - ('{from_date}' = from_date and '{to_date}' = to_date))""".format( - from_date=self.from_date, to_date=self.to_date - ) + conds += f""" and ((from_date > '{self.from_date}' and from_date < '{self.to_date}') or + (to_date > '{self.from_date}' and to_date < '{self.to_date}') or + ('{self.from_date}' > from_date and '{self.from_date}' < to_date) or + ('{self.from_date}' = from_date and '{self.to_date}' = to_date))""" elif self.from_date and not self.to_date: - conds += """ and to_date > '{from_date}'""".format(from_date=self.from_date) + conds += f""" and to_date > '{self.from_date}'""" elif self.to_date and not self.from_date: - conds += """ and from_date < '{to_date}'""".format(to_date=self.to_date) + conds += f""" and from_date < '{self.to_date}'""" tax_rule = frappe.db.sql( - "select name, priority \ - from `tabTax Rule` where {0} and name != '{1}'".format( - conds, self.name - ), + f"select name, priority \ + from `tabTax Rule` where {conds} and name != '{self.name}'", as_dict=1, ) @@ -117,7 +113,6 @@ class TaxRule(Document): and cint(frappe.db.get_single_value("E Commerce Settings", "enabled")) and not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1, "name": ["!=", self.name]}) ): - self.use_for_shopping_cart = 1 frappe.msgprint( _( @@ -174,27 +169,25 @@ def get_tax_template(posting_date, args): conditions.append("(from_date is null) and (to_date is null)") conditions.append( - "ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category")))) + "ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")))) ) if "tax_category" in args.keys(): del args["tax_category"] for key, value in args.items(): if key == "use_for_shopping_cart": - conditions.append("use_for_shopping_cart = {0}".format(1 if value else 0)) + conditions.append(f"use_for_shopping_cart = {1 if value else 0}") elif key == "customer_group": if not value: value = get_root_of("Customer Group") customer_group_condition = get_customer_group_condition(value) - conditions.append("ifnull({0}, '') in ('', {1})".format(key, customer_group_condition)) + conditions.append(f"ifnull({key}, '') in ('', {customer_group_condition})") else: - conditions.append("ifnull({0}, '') in ('', {1})".format(key, frappe.db.escape(cstr(value)))) + conditions.append(f"ifnull({key}, '') in ('', {frappe.db.escape(cstr(value))})") tax_rule = frappe.db.sql( """select * from `tabTax Rule` - where {0}""".format( - " and ".join(conditions) - ), + where {}""".format(" and ".join(conditions)), as_dict=True, ) @@ -219,7 +212,7 @@ def get_tax_template(posting_date, args): )[0] tax_template = rule.sales_tax_template or rule.purchase_tax_template - doctype = "{0} Taxes and Charges Template".format(rule.tax_type) + doctype = f"{rule.tax_type} Taxes and Charges Template" if frappe.db.get_value(doctype, tax_template, "disabled") == 1: return None @@ -229,9 +222,7 @@ def get_tax_template(posting_date, args): def get_customer_group_condition(customer_group): condition = "" - customer_groups = [ - "%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group) - ] + customer_groups = ["%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)] if customer_groups: condition = ",".join(["%s"] * len(customer_groups)) % (tuple(customer_groups)) return condition diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.js b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.js index 7b479749465..4f4f32c4747 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.js +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.js @@ -1,18 +1,18 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Tax Withholding Category', { - setup: function(frm) { - frm.set_query("account", "accounts", function(doc, cdt, cdn) { +frappe.ui.form.on("Tax Withholding Category", { + setup: function (frm) { + frm.set_query("account", "accounts", function (doc, cdt, cdn) { var child = locals[cdt][cdn]; if (child.company) { return { filters: { - 'company': child.company, - 'root_type': ['in', ['Asset', 'Liability']] - } + company: child.company, + root_type: ["in", ["Asset", "Liability"]], + }, }; } }); - } + }, }); diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 943c0057f99..58ce2840b4b 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -36,13 +36,11 @@ class TaxWithholdingCategory(Document): def validate_thresholds(self): for d in self.get("rates"): - if ( - d.cumulative_threshold and d.single_threshold and d.cumulative_threshold < d.single_threshold - ): + if d.cumulative_threshold and d.single_threshold and d.cumulative_threshold < d.single_threshold: frappe.throw( - _("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format( - d.idx - ) + _( + "Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold" + ).format(d.idx) ) @@ -295,9 +293,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" field = ( - "base_tax_withholding_net_total as base_net_total" - if party_type == "Supplier" - else "base_net_total" + "base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total" ) voucher_wise_amount = {} vouchers = [] @@ -351,9 +347,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): return vouchers, voucher_wise_amount -def get_advance_vouchers( - parties, company=None, from_date=None, to_date=None, party_type="Supplier" -): +def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type="Supplier"): """ Use Payment Ledger to fetch unallocated Advance Payments """ @@ -374,9 +368,7 @@ def get_advance_vouchers( if from_date and to_date: conditions.append(ple.posting_date[from_date:to_date]) - advances = ( - qb.from_(ple).select(ple.voucher_no).distinct().where(Criterion.all(conditions)).run(as_list=1) - ) + advances = qb.from_(ple).select(ple.voucher_no).distinct().where(Criterion.all(conditions)).run(as_list=1) if advances: advances = [x[0] for x in advances] @@ -522,6 +514,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): "GL Entry", { "is_cancelled": 0, + "party_type": "Customer", "party": ["in", parties], "company": inv.company, "voucher_no": ["in", vouchers], @@ -536,13 +529,12 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): conditions = [] conditions.append(ple.amount.lt(0)) conditions.append(ple.delinked == 0) + conditions.append(ple.party_type == "Customer") conditions.append(ple.party.isin(parties)) conditions.append(ple.voucher_no == ple.against_voucher_no) conditions.append(ple.company == inv.company) - advances = ( - qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1) - ) + (qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1)) advance_amt = ( qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0 @@ -555,6 +547,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): { "is_cancelled": 0, "credit": [">", 0], + "party_type": "Customer", "party": ["in", parties], "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], "company": inv.company, @@ -600,9 +593,7 @@ def get_limit_consumed(ldc, parties): return limit_consumed -def get_lower_deduction_amount( - current_amount, limit_consumed, certificate_limit, rate, tax_details -): +def get_lower_deduction_amount(current_amount, limit_consumed, certificate_limit, rate, tax_details): if certificate_limit - flt(limit_consumed) - flt(current_amount) >= 0: return current_amount * rate / 100 else: @@ -614,9 +605,7 @@ def get_lower_deduction_amount( def is_valid_certificate(ldc, posting_date, limit_consumed): available_amount = flt(ldc.certificate_limit) - flt(limit_consumed) - if ( - getdate(ldc.valid_from) <= getdate(posting_date) <= getdate(ldc.valid_upto) - ) and available_amount > 0: + if (getdate(ldc.valid_from) <= getdate(posting_date) <= getdate(ldc.valid_upto)) and available_amount > 0: return True return False diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 0a749f96652..a5509619583 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -96,9 +96,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): def test_tax_withholding_category_checks(self): invoices = [] - frappe.db.set_value( - "Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category" - ) + frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category") # First Invoice with no tds check pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000, do_not_save=True) @@ -453,7 +451,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): pe3.save() pe3.submit() - self.assertEquals(pe3.get("taxes")[0].tax_amount, 1200) + self.assertEqual(pe3.get("taxes")[0].tax_amount, 1200) pe1.cancel() pe2.cancel() pe3.cancel() @@ -850,9 +848,7 @@ def create_tax_withholding_category( ).insert() -def create_lower_deduction_certificate( - supplier, tax_withholding_category, tax_rate, certificate_no, limit -): +def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_rate, certificate_no, limit): fiscal_year = get_fiscal_year(today(), company="_Test Company") if not frappe.db.exists("Lower Deduction Certificate", certificate_no): frappe.get_doc( diff --git a/erpnext/accounts/doctype/transaction_deletion_record_details/__init__.py b/erpnext/accounts/doctype/transaction_deletion_record_details/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json new file mode 100644 index 00000000000..fe4b0852ac1 --- /dev/null +++ b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json @@ -0,0 +1,58 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-02-04 10:53:32.307930", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "doctype_name", + "docfield_name", + "no_of_docs", + "done" + ], + "fields": [ + { + "fieldname": "doctype_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "DocType", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "docfield_name", + "fieldtype": "Data", + "label": "DocField", + "read_only": 1 + }, + { + "fieldname": "no_of_docs", + "fieldtype": "Int", + "in_list_view": 1, + "label": "No of Docs", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "done", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Done", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-02-05 17:35:09.556054", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Transaction Deletion Record Details", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.py b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.py new file mode 100644 index 00000000000..bc5b5c41fdd --- /dev/null +++ b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class TransactionDeletionRecordDetails(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + docfield_name: DF.Data | None + doctype_name: DF.Link + done: DF.Check + no_of_docs: DF.Int + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + # end: auto-generated types + + pass diff --git a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index f404d9981a3..882dd1d6dab 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -8,6 +8,7 @@ from frappe.utils import today from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): @@ -49,6 +50,16 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): ) return pe + def create_sales_order(self): + so = make_sales_order( + company=self.company, + customer=self.customer, + item=self.item, + rate=100, + transaction_date=today(), + ) + return so + def test_01_unreconcile_invoice(self): si1 = self.create_sales_invoice() si2 = self.create_sales_invoice() @@ -82,7 +93,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): unreconcile.add_references() self.assertEqual(len(unreconcile.allocations), 2) allocations = [x.reference_name for x in unreconcile.allocations] - self.assertEquals([si1.name, si2.name], allocations) + self.assertEqual([si1.name, si2.name], allocations) # unreconcile si1 for x in unreconcile.allocations: if x.reference_name != si1.name: @@ -147,7 +158,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): unreconcile.add_references() self.assertEqual(len(unreconcile.allocations), 2) allocations = [x.reference_name for x in unreconcile.allocations] - self.assertEquals([si1.name, si2.name], allocations) + self.assertEqual([si1.name, si2.name], allocations) # unreconcile si1 from pe2 for x in unreconcile.allocations: if x.reference_name != si1.name: @@ -205,7 +216,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): unreconcile.add_references() self.assertEqual(len(unreconcile.allocations), 2) allocations = [x.reference_name for x in unreconcile.allocations] - self.assertEquals([si1.name, si2.name], allocations) + self.assertEqual([si1.name, si2.name], allocations) # unreconcile si1 from pe for x in unreconcile.allocations: if x.reference_name != si1.name: @@ -290,7 +301,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): unreconcile.add_references() self.assertEqual(len(unreconcile.allocations), 2) allocations = [x.reference_name for x in unreconcile.allocations] - self.assertEquals([si1.name, si2.name], allocations) + self.assertEqual([si1.name, si2.name], allocations) # unreconcile si1 from pe2 for x in unreconcile.allocations: if x.reference_name != si1.name: @@ -314,3 +325,41 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): ), 1, ) + + def test_05_unreconcile_order(self): + so = self.create_sales_order() + + pe = self.create_payment_entry() + # Allocation payment against Sales Order + pe.paid_amount = 100 + pe.append( + "references", + {"reference_doctype": so.doctype, "reference_name": so.name, "allocated_amount": 100}, + ) + pe.save().submit() + + # Assert 'Advance Paid' + so.reload() + self.assertEqual(so.advance_paid, 100) + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payment", + "company": self.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 1) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEqual([so.name], allocations) + # unreconcile so + unreconcile.save().submit() + + # Assert 'Advance Paid' + so.reload() + pe.reload() + self.assertEqual(so.advance_paid, 0) + self.assertEqual(len(pe.references), 0) + self.assertEqual(pe.unallocated_amount, 100) diff --git a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js index 70cefb13b57..63ae30d9d20 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js @@ -3,39 +3,36 @@ frappe.ui.form.on("Unreconcile Payment", { refresh(frm) { - frm.set_query("voucher_type", function() { + frm.set_query("voucher_type", function () { return { filters: { - name: ["in", ["Payment Entry", "Journal Entry"]] - } - } + name: ["in", ["Payment Entry", "Journal Entry"]], + }, + }; }); - - frm.set_query("voucher_no", function(doc) { + frm.set_query("voucher_no", function (doc) { return { filters: { company: doc.company, - docstatus: 1 - } - } + docstatus: 1, + }, + }; }); - }, - get_allocations: function(frm) { + get_allocations: function (frm) { frm.clear_table("allocations"); frappe.call({ method: "get_allocations_from_payment", doc: frm.doc, - callback: function(r) { + callback: function (r) { if (r.message) { - r.message.forEach(x => { - frm.add_child("allocations", x) - }) + r.message.forEach((x) => { + frm.add_child("allocations", x); + }); frm.refresh_fields(); } - } - }) - - } + }, + }); + }, }); diff --git a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py index 77906a78332..091bccf5099 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py @@ -18,7 +18,7 @@ from erpnext.accounts.utils import ( class UnreconcilePayment(Document): def validate(self): self.supported_types = ["Payment Entry", "Journal Entry"] - if not self.voucher_type in self.supported_types: + if self.voucher_type not in self.supported_types: frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types))) @frappe.whitelist() @@ -63,11 +63,14 @@ class UnreconcilePayment(Document): update_voucher_outstanding( alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) + if doc.doctype in frappe.get_hooks("advance_payment_doctypes"): + doc.set_total_advance_paid() + frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) @frappe.whitelist() -def doc_has_references(doctype: str = None, docname: str = None): +def doc_has_references(doctype: str | None = None, docname: str | None = None): if doctype in ["Sales Invoice", "Purchase Invoice"]: return frappe.db.count( "Payment Ledger Entry", @@ -82,7 +85,7 @@ def doc_has_references(doctype: str = None, docname: str = None): @frappe.whitelist() def get_linked_payments_for_doc( - company: str = None, doctype: str = None, docname: str = None + company: str | None = None, doctype: str | None = None, docname: str | None = None ) -> list: if company and doctype and docname: _dt = doctype diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 134ddddf9e0..58601f3b08d 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -7,15 +7,19 @@ import copy import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cint, cstr, flt, formatdate, getdate, now +from frappe.utils import cint, flt, formatdate, getdate, now import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) +from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( + get_dimension_filter_map, +) 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 +from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError def make_gl_entries( @@ -74,7 +78,7 @@ def make_acc_dimensions_offsetting_entry(gl_map): "credit": credit, "debit_in_account_currency": debit, "credit_in_account_currency": credit, - "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name), + "remarks": _("Offsetting for Accounting Dimension") + f" - {dimension.name}", "against_voucher": None, } ) @@ -175,9 +179,7 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None): - cost_center_allocation = get_cost_center_allocation_data( - gl_map[0]["company"], gl_map[0]["posting_date"] - ) + cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"]) if not cost_center_allocation: return gl_map @@ -186,9 +188,7 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None): cost_center = d.get("cost_center") # Validate budget against main cost center - validate_expense_against_budget( - d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision) - ) + validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)) if cost_center and cost_center_allocation.get(cost_center): for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items(): @@ -220,9 +220,7 @@ def get_cost_center_allocation_data(company, posting_date): cc_allocation = frappe._dict() for d in records: - cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault( - d.cost_center, d.percentage - ) + cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(d.cost_center, d.percentage) return cc_allocation @@ -230,11 +228,13 @@ def get_cost_center_allocation_data(company, posting_date): def merge_similar_entries(gl_map, precision=None): merged_gl_map = [] accounting_dimensions = get_accounting_dimensions() + merge_properties = get_merge_properties(accounting_dimensions) for entry in gl_map: + entry.merge_key = get_merge_key(entry, merge_properties) # if there is already an entry in this account then just add it # to that entry - same_head = check_if_in_list(entry, merged_gl_map, accounting_dimensions) + same_head = check_if_in_list(entry, merged_gl_map) if same_head: same_head.debit = flt(same_head.debit) + flt(entry.debit) same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt( @@ -269,33 +269,34 @@ def merge_similar_entries(gl_map, precision=None): return merged_gl_map -def check_if_in_list(gle, gl_map, dimensions=None): - account_head_fieldnames = [ - "voucher_detail_no", - "party", - "against_voucher", +def get_merge_properties(dimensions=None): + merge_properties = [ + "account", "cost_center", - "against_voucher_type", + "party", "party_type", + "voucher_detail_no", + "against_voucher", + "against_voucher_type", "project", "finance_book", ] - if dimensions: - account_head_fieldnames = account_head_fieldnames + dimensions + merge_properties.extend(dimensions) + return merge_properties + +def get_merge_key(entry, merge_properties): + merge_key = [] + for fieldname in merge_properties: + merge_key.append(entry.get(fieldname, "")) + + return tuple(merge_key) + + +def check_if_in_list(gle, gl_map): for e in gl_map: - same_head = True - if e.account != gle.account: - same_head = False - continue - - for fieldname in account_head_fieldnames: - if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)): - same_head = False - break - - if same_head: + if e.merge_key == gle.merge_key: return e @@ -354,6 +355,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): process_debit_credit_difference(gl_map) + dimension_filter_map = get_dimension_filter_map() if gl_map: check_freezing_date(gl_map[0]["posting_date"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_map) @@ -361,6 +363,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"]) for entry in gl_map: + validate_allowed_dimensions(entry, dimension_filter_map) make_entry(entry, adv_adj, update_outstanding, from_repost) @@ -531,9 +534,7 @@ def update_accounting_dimensions(round_off_gle): round_off_gle[dimension] = dimension_values.get(dimension) -def get_round_off_account_and_cost_center( - company, voucher_type, voucher_no, use_company_default=False -): +def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False): round_off_account, round_off_cost_center = frappe.get_cached_value( "Company", company, ["round_off_account", "round_off_cost_center"] ) or [None, None] @@ -641,9 +642,7 @@ def check_freezing_date(posting_date, adv_adj=False): def validate_against_pcv(is_opening, posting_date, company): - if is_opening and frappe.db.exists( - "Period Closing Voucher", {"docstatus": 1, "company": company} - ): + if is_opening and frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}): frappe.throw( _("Opening Entry can not be created after Period Closing Voucher is created."), title=_("Invalid Opening Entry"), @@ -654,9 +653,7 @@ def validate_against_pcv(is_opening, posting_date, company): ) if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date): - message = _("Books have been closed till the period ending on {0}").format( - formatdate(last_pcv_date) - ) + message = _("Books have been closed till the period ending on {0}").format(formatdate(last_pcv_date)) message += "
      " message += _("You cannot create/amend any accounting entries till this date.") frappe.throw(message, title=_("Period Closed")) @@ -672,3 +669,39 @@ def set_as_cancel(voucher_type, voucher_no): where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", (now(), frappe.session.user, voucher_type, voucher_no), ) + + +def validate_allowed_dimensions(gl_entry, dimension_filter_map): + for key, value in dimension_filter_map.items(): + dimension = key[0] + account = key[1] + + if gl_entry.account == account: + if value["is_mandatory"] and not gl_entry.get(dimension): + frappe.throw( + _("{0} is mandatory for account {1}").format( + frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account) + ), + MandatoryAccountDimensionError, + ) + + if value["allow_or_restrict"] == "Allow": + if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]: + frappe.throw( + _("Invalid value {0} for {1} against account {2}").format( + frappe.bold(gl_entry.get(dimension)), + frappe.bold(frappe.unscrub(dimension)), + frappe.bold(gl_entry.account), + ), + InvalidAccountDimensionError, + ) + else: + if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]: + frappe.throw( + _("Invalid value {0} for {1} against account {2}").format( + frappe.bold(gl_entry.get(dimension)), + frappe.bold(frappe.unscrub(dimension)), + frappe.bold(gl_entry.account), + ), + InvalidAccountDimensionError, + ) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index c24d28b3249..2b95e16f356 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -2,14 +2,12 @@ # License: GNU General Public License v3. See license.txt -from typing import Optional - import frappe from frappe import _, msgprint, qb, scrub from frappe.contacts.doctype.address.address import get_company_address, get_default_address from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values -from frappe.query_builder.functions import Abs, Date, Sum +from frappe.query_builder.functions import Abs, Count, Date, Sum from frappe.utils import ( add_days, add_months, @@ -69,7 +67,6 @@ def get_party_details( shipping_address=None, pos_profile=None, ): - if not party: return {} if not frappe.db.exists(party_type, party): @@ -114,14 +111,12 @@ def _get_party_details( set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype) ) party = party_details[party_type.lower()] - - if not ignore_permissions and not ( - frappe.has_permission(party_type, "read", party) - or frappe.has_permission(party_type, "select", party) - ): - frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError) - party = frappe.get_doc(party_type, party) + + if not ignore_permissions: + ptype = "select" if frappe.only_has_select_perm(party_type) else "read" + frappe.has_permission(party_type, ptype, party, throw=True) + currency = party.get("default_currency") or currency or get_company_currency(company) party_address, shipping_address = set_address_details( @@ -155,9 +150,7 @@ def _get_party_details( party_details["taxes_and_charges"] = tax_template if cint(fetch_payment_terms_template): - party_details["payment_terms_template"] = get_payment_terms_template( - party.name, party_type, company - ) + party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) if not party_details.get("currency"): party_details["currency"] = currency @@ -175,9 +168,7 @@ def _get_party_details( # supplier tax withholding category if party_type == "Supplier" and party: - party_details["supplier_tds"] = frappe.get_value( - party_type, party.name, "tax_withholding_category" - ) + party_details["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category") if not party_details.get("tax_category") and pos_profile: party_details["tax_category"] = frappe.get_value("POS Profile", pos_profile, "tax_category") @@ -197,12 +188,8 @@ def set_address_details( *, ignore_permissions=False, ): - billing_address_field = ( - "customer_address" if party_type == "Lead" else party_type.lower() + "_address" - ) - party_details[billing_address_field] = party_address or get_default_address( - party_type, party.name - ) + billing_address_field = "customer_address" if party_type == "Lead" else party_type.lower() + "_address" + party_details[billing_address_field] = party_address or get_default_address(party_type, party.name) if doctype: party_details.update( get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]) @@ -312,9 +299,7 @@ def set_contact_details(party_details, party, party_type): "department as contact_department", ] - contact_details = frappe.db.get_value( - "Contact", party_details.contact_person, fields, as_dict=True - ) + contact_details = frappe.db.get_value("Contact", party_details.contact_person, fields, as_dict=True) contact_details.contact_display = " ".join( filter( @@ -340,9 +325,7 @@ def set_other_values(party_details, party, party_type): party_details[f] = party.get(f) # fields prepended with default in Customer doctype - for f in ["currency"] + ( - ["sales_partner", "commission_rate"] if party_type == "Customer" else [] - ): + for f in ["currency"] + (["sales_partner", "commission_rate"] if party_type == "Customer" else []): if party.get("default_" + f): party_details[f] = party.get("default_" + f) @@ -379,14 +362,10 @@ def set_price_list(party_details, party, party_type, given_price_list, pos=None) "Price List", price_list, "currency", cache=True ) - party_details[ - "selling_price_list" if party.doctype == "Customer" else "buying_price_list" - ] = price_list + party_details["selling_price_list" if party.doctype == "Customer" else "buying_price_list"] = price_list -def set_account_and_due_date( - party, account, party_type, company, posting_date, bill_date, doctype -): +def set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype): if doctype not in ["POS Invoice", "Sales Invoice", "Purchase Invoice"]: # not an invoice return {party_type.lower(): party} @@ -451,9 +430,7 @@ def get_party_account(party_type, party=None, company=None): @frappe.whitelist() def get_party_bank_account(party_type, party): - return frappe.db.get_value( - "Bank Account", {"party_type": party_type, "party": party, "is_default": 1} - ) + return frappe.db.get_value("Bank Account", {"party_type": party_type, "party": party, "is_default": 1}) def get_party_account_currency(party_type, party, company): @@ -575,9 +552,7 @@ def get_due_date(posting_date, party_type, party, company=None, bill_date=None): template_name = get_payment_terms_template(party, party_type, company) if template_name: - due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime( - "%Y-%m-%d" - ) + due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") else: if party_type == "Supplier": supplier_group = frappe.get_cached_value(party_type, party, "supplier_group") @@ -737,7 +712,6 @@ def get_payment_terms_template(party_name, party_type, company=None): def validate_party_frozen_disabled(party_type, party_name): - if frappe.flags.ignore_party_validation: return @@ -750,7 +724,7 @@ def validate_party_frozen_disabled(party_type, party_name): frozen_accounts_modifier = frappe.db.get_single_value( "Accounts Settings", "frozen_accounts_modifier" ) - if not frozen_accounts_modifier in frappe.get_roles(): + if frozen_accounts_modifier not in frappe.get_roles(): frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen) elif party_type == "Employee": @@ -763,34 +737,37 @@ def get_timeline_data(doctype, name): from frappe.desk.form.load import get_communication_data out = {} - fields = "creation, count(*)" after = add_years(None, -1).strftime("%Y-%m-%d") - group_by = "group by Date(creation)" data = get_communication_data( doctype, name, after=after, - group_by="group by creation", - fields="C.creation as creation, count(C.name)", + group_by="group by communication_date", + fields="C.communication_date as communication_date, count(C.name)", as_dict=False, ) # fetch and append data from Activity Log - data += frappe.db.sql( - """select {fields} - from `tabActivity Log` - where (reference_doctype=%(doctype)s and reference_name=%(name)s) - or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s) - or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s) - and status!='Success' and creation > {after} - {group_by} order by creation desc - """.format( - fields=fields, group_by=group_by, after=after - ), - {"doctype": doctype, "name": name}, - as_dict=False, - ) + activity_log = frappe.qb.DocType("Activity Log") + data += ( + frappe.qb.from_(activity_log) + .select(activity_log.communication_date, Count(activity_log.name)) + .where( + ( + ((activity_log.reference_doctype == doctype) & (activity_log.reference_name == name)) + | ((activity_log.timeline_doctype == doctype) & (activity_log.timeline_name == name)) + | ( + (activity_log.reference_doctype.isin(["Quotation", "Opportunity"])) + & (activity_log.timeline_name == name) + ) + ) + & (activity_log.status != "Success") + & (activity_log.creation > after) + ) + .groupby(activity_log.communication_date) + .orderby(activity_log.communication_date, order=frappe.qb.desc) + ).run() timeline_items = dict(data) @@ -870,9 +847,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): party_account_currency = get_party_account_currency(party_type, party, d.company) if party_account_currency == company_default_currency: - billing_this_year = flt( - company_wise_billing_this_year.get(d.company, {}).get("base_grand_total") - ) + billing_this_year = flt(company_wise_billing_this_year.get(d.company, {}).get("base_grand_total")) else: billing_this_year = flt(company_wise_billing_this_year.get(d.company, {}).get("grand_total")) @@ -898,7 +873,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): return company_wise_info -def get_party_shipping_address(doctype: str, name: str) -> Optional[str]: +def get_party_shipping_address(doctype: str, name: str) -> str | None: """ Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true. and/or `is_shipping_address = 1`. @@ -964,7 +939,7 @@ def get_partywise_advanced_payment_amount( return frappe._dict(data) -def get_default_contact(doctype: str, name: str) -> Optional[str]: +def get_default_contact(doctype: str, name: str) -> str | None: """ Returns contact name only if there is a primary contact for given doctype and name. diff --git a/erpnext/accounts/report/account_balance/account_balance.js b/erpnext/accounts/report/account_balance/account_balance.js index bb66951cdcd..52775739b8f 100644 --- a/erpnext/accounts/report/account_balance/account_balance.js +++ b/erpnext/accounts/report/account_balance/account_balance.js @@ -3,31 +3,31 @@ /* eslint-disable */ frappe.query_reports["Account Balance"] = { - "filters": [ + filters: [ { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", - default: frappe.defaults.get_user_default("Company") + default: frappe.defaults.get_user_default("Company"), }, { - fieldname:"report_date", + fieldname: "report_date", label: __("Date"), fieldtype: "Date", default: frappe.datetime.get_today(), - reqd: 1 + reqd: 1, }, { fieldname: "root_type", label: __("Root Type"), fieldtype: "Select", options: [ - { "value": "Asset", "label": __("Asset") }, - { "value": "Liability", "label": __("Liability") }, - { "value": "Income", "label": __("Income") }, - { "value": "Expense", "label": __("Expense") }, - { "value": "Equity", "label": __("Equity") } + { value: "Asset", label: __("Asset") }, + { value: "Liability", label: __("Liability") }, + { value: "Income", label: __("Income") }, + { value: "Expense", label: __("Expense") }, + { value: "Equity", label: __("Equity") }, ], }, { @@ -35,30 +35,32 @@ frappe.query_reports["Account Balance"] = { label: __("Account Type"), fieldtype: "Select", options: [ - { "value": "Accumulated Depreciation", "label": __("Accumulated Depreciation") }, - { "value": "Asset Received But Not Billed", "label": __("Asset Received But Not Billed") }, - { "value": "Bank", "label": __("Bank") }, - { "value": "Cash", "label": __("Cash") }, - { "value": "Chargeble", "label": __("Chargeble") }, - { "value": "Capital Work in Progress", "label": __("Capital Work in Progress") }, - { "value": "Cost of Goods Sold", "label": __("Cost of Goods Sold") }, - { "value": "Depreciation", "label": __("Depreciation") }, - { "value": "Equity", "label": __("Equity") }, - { "value": "Expense Account", "label": __("Expense Account") }, - { "value": "Expenses Included In Asset Valuation", "label": __("Expenses Included In Asset Valuation") }, - { "value": "Expenses Included In Valuation", "label": __("Expenses Included In Valuation") }, - { "value": "Fixed Asset", "label": __("Fixed Asset") }, - { "value": "Income Account", "label": __("Income Account") }, - { "value": "Payable", "label": __("Payable") }, - { "value": "Receivable", "label": __("Receivable") }, - { "value": "Round Off", "label": __("Round Off") }, - { "value": "Stock", "label": __("Stock") }, - { "value": "Stock Adjustment", "label": __("Stock Adjustment") }, - { "value": "Stock Received But Not Billed", "label": __("Stock Received But Not Billed") }, - { "value": "Tax", "label": __("Tax") }, - { "value": "Temporary", "label": __("Temporary") }, + { value: "Accumulated Depreciation", label: __("Accumulated Depreciation") }, + { value: "Asset Received But Not Billed", label: __("Asset Received But Not Billed") }, + { value: "Bank", label: __("Bank") }, + { value: "Cash", label: __("Cash") }, + { value: "Chargeble", label: __("Chargeble") }, + { value: "Capital Work in Progress", label: __("Capital Work in Progress") }, + { value: "Cost of Goods Sold", label: __("Cost of Goods Sold") }, + { value: "Depreciation", label: __("Depreciation") }, + { value: "Equity", label: __("Equity") }, + { value: "Expense Account", label: __("Expense Account") }, + { + value: "Expenses Included In Asset Valuation", + label: __("Expenses Included In Asset Valuation"), + }, + { value: "Expenses Included In Valuation", label: __("Expenses Included In Valuation") }, + { value: "Fixed Asset", label: __("Fixed Asset") }, + { value: "Income Account", label: __("Income Account") }, + { value: "Payable", label: __("Payable") }, + { value: "Receivable", label: __("Receivable") }, + { value: "Round Off", label: __("Round Off") }, + { value: "Stock", label: __("Stock") }, + { value: "Stock Adjustment", label: __("Stock Adjustment") }, + { value: "Stock Received But Not Billed", label: __("Stock Received But Not Billed") }, + { value: "Tax", label: __("Tax") }, + { value: "Temporary", label: __("Temporary") }, ], }, - - ] -} + ], +}; diff --git a/erpnext/accounts/report/account_balance/account_balance.py b/erpnext/accounts/report/account_balance/account_balance.py index 824a965cdcf..6f9f7ebcb8d 100644 --- a/erpnext/accounts/report/account_balance/account_balance.py +++ b/erpnext/accounts/report/account_balance/account_balance.py @@ -61,7 +61,6 @@ def get_conditions(filters): def get_data(filters): - data = [] conditions = get_conditions(filters) accounts = frappe.db.get_all( diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 0979cffbf3c..61a3a96d5fe 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -2,188 +2,194 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Accounts Payable"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname": "report_date", - "label": __("Posting Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "report_date", + label: __("Posting Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, { - "fieldname": "finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book" + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname": "cost_center", - "label": __("Cost Center"), - "fieldtype": "Link", - "options": "Cost Center", + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "Link", + options: "Cost Center", get_query: () => { - var company = frappe.query_report.get_filter_value('company'); + var company = frappe.query_report.get_filter_value("company"); return { filters: { - 'company': company - } - } - } - }, - { - "fieldname": "party_account", - "label": __("Payable Account"), - "fieldtype": "Link", - "options": "Account", - get_query: () => { - var company = frappe.query_report.get_filter_value('company'); - return { - filters: { - 'company': company, - 'account_type': 'Payable', - 'is_group': 0 - } + company: company, + }, }; - } + }, }, { - "fieldname": "ageing_based_on", - "label": __("Ageing Based On"), - "fieldtype": "Select", - "options": 'Posting Date\nDue Date\nSupplier Invoice Date', - "default": "Due Date" + fieldname: "party_account", + label: __("Payable Account"), + fieldtype: "Link", + options: "Account", + get_query: () => { + var company = frappe.query_report.get_filter_value("company"); + return { + filters: { + company: company, + account_type: "Payable", + is_group: 0, + }, + }; + }, }, { - "fieldname": "range1", - "label": __("Ageing Range 1"), - "fieldtype": "Int", - "default": "30", - "reqd": 1 + fieldname: "ageing_based_on", + label: __("Ageing Based On"), + fieldtype: "Select", + options: "Posting Date\nDue Date\nSupplier Invoice Date", + default: "Due Date", }, { - "fieldname": "range2", - "label": __("Ageing Range 2"), - "fieldtype": "Int", - "default": "60", - "reqd": 1 + fieldname: "range1", + label: __("Ageing Range 1"), + fieldtype: "Int", + default: "30", + reqd: 1, }, { - "fieldname": "range3", - "label": __("Ageing Range 3"), - "fieldtype": "Int", - "default": "90", - "reqd": 1 + fieldname: "range2", + label: __("Ageing Range 2"), + fieldtype: "Int", + default: "60", + reqd: 1, }, { - "fieldname": "range4", - "label": __("Ageing Range 4"), - "fieldtype": "Int", - "default": "120", - "reqd": 1 + fieldname: "range3", + label: __("Ageing Range 3"), + fieldtype: "Int", + default: "90", + reqd: 1, }, { - "fieldname": "payment_terms_template", - "label": __("Payment Terms Template"), - "fieldtype": "Link", - "options": "Payment Terms Template" + fieldname: "range4", + label: __("Ageing Range 4"), + fieldtype: "Int", + default: "120", + reqd: 1, }, { - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Autocomplete", + fieldname: "payment_terms_template", + label: __("Payment Terms Template"), + fieldtype: "Link", + options: "Payment Terms Template", + }, + { + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Autocomplete", options: get_party_type_options(), - on_change: function() { - frappe.query_report.set_filter_value('party', ""); - frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier"); - } + on_change: function () { + frappe.query_report.set_filter_value("party", ""); + frappe.query_report.toggle_filter_display( + "supplier_group", + frappe.query_report.get_filter_value("party_type") !== "Supplier" + ); + }, }, { - "fieldname":"party", - "label": __("Party"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { + fieldname: "party", + label: __("Party"), + fieldtype: "MultiSelectList", + get_data: function (txt) { if (!frappe.query_report.filters) return; - let party_type = frappe.query_report.get_filter_value('party_type'); + let party_type = frappe.query_report.get_filter_value("party_type"); if (!party_type) return; return frappe.db.get_link_options(party_type, txt); }, }, { - "fieldname": "supplier_group", - "label": __("Supplier Group"), - "fieldtype": "Link", - "options": "Supplier Group", - "hidden": 1 + fieldname: "supplier_group", + label: __("Supplier Group"), + fieldtype: "Link", + options: "Supplier Group", + hidden: 1, }, { - "fieldname": "group_by_party", - "label": __("Group By Supplier"), - "fieldtype": "Check" + fieldname: "group_by_party", + label: __("Group By Supplier"), + fieldtype: "Check", }, { - "fieldname": "based_on_payment_terms", - "label": __("Based On Payment Terms"), - "fieldtype": "Check", + fieldname: "based_on_payment_terms", + label: __("Based On Payment Terms"), + fieldtype: "Check", }, { - "fieldname": "show_remarks", - "label": __("Show Remarks"), - "fieldtype": "Check", + fieldname: "show_remarks", + label: __("Show Remarks"), + fieldtype: "Check", }, { - "fieldname": "show_future_payments", - "label": __("Show Future Payments"), - "fieldtype": "Check", + fieldname: "show_future_payments", + label: __("Show Future Payments"), + fieldtype: "Check", }, { - "fieldname": "for_revaluation_journals", - "label": __("Revaluation Journals"), - "fieldtype": "Check", + fieldname: "for_revaluation_journals", + label: __("Revaluation Journals"), + fieldtype: "Check", }, { - "fieldname": "ignore_accounts", - "label": __("Group by Voucher"), - "fieldtype": "Check", - } - + fieldname: "in_party_currency", + label: __("In Party Currency"), + fieldtype: "Check", + }, + { + fieldname: "ignore_accounts", + label: __("Group by Voucher"), + fieldtype: "Check", + }, ], - "formatter": function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (data && data.bold) { value = value.bold(); - } return value; }, - onload: function(report) { - report.page.add_inner_button(__("Accounts Payable Summary"), function() { + onload: function (report) { + report.page.add_inner_button(__("Accounts Payable Summary"), function () { var filters = report.get_values(); - frappe.set_route('query-report', 'Accounts Payable Summary', {company: filters.company}); + frappe.set_route("query-report", "Accounts Payable Summary", { company: filters.company }); }); - } -} + }, +}; -erpnext.utils.add_dimensions('Accounts Payable', 9); +erpnext.utils.add_dimensions("Accounts Payable", 9); function get_party_type_options() { let options = []; - frappe.db.get_list( - "Party Type", {filters:{"account_type": "Payable"}, fields:['name']} - ).then((res) => { - res.forEach((party_type) => { - options.push(party_type.name); + frappe.db + .get_list("Party Type", { filters: { account_type: "Payable" }, fields: ["name"] }) + .then((res) => { + res.forEach((party_type) => { + options.push(party_type.name); + }); }); - }); return options; } diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index 9f03d92cd50..f5c9d16073e 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -1,16 +1,10 @@ -import unittest - import frappe -from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, getdate, today +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today -from erpnext import get_default_cost_center -from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.accounts_payable.accounts_payable import execute from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): @@ -40,6 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "range2": 60, "range3": 90, "range4": 120, + "in_party_currency": 1, } data = execute(filters) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 0f206b1cf42..92ea9e8f598 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -2,140 +2,143 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Accounts Payable Summary"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"report_date", - "label": __("Posting Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "report_date", + label: __("Posting Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, { - "fieldname":"ageing_based_on", - "label": __("Ageing Based On"), - "fieldtype": "Select", - "options": 'Posting Date\nDue Date', - "default": "Due Date" + fieldname: "ageing_based_on", + label: __("Ageing Based On"), + fieldtype: "Select", + options: "Posting Date\nDue Date", + default: "Due Date", }, { - "fieldname":"range1", - "label": __("Ageing Range 1"), - "fieldtype": "Int", - "default": "30", - "reqd": 1 + fieldname: "range1", + label: __("Ageing Range 1"), + fieldtype: "Int", + default: "30", + reqd: 1, }, { - "fieldname":"range2", - "label": __("Ageing Range 2"), - "fieldtype": "Int", - "default": "60", - "reqd": 1 + fieldname: "range2", + label: __("Ageing Range 2"), + fieldtype: "Int", + default: "60", + reqd: 1, }, { - "fieldname":"range3", - "label": __("Ageing Range 3"), - "fieldtype": "Int", - "default": "90", - "reqd": 1 + fieldname: "range3", + label: __("Ageing Range 3"), + fieldtype: "Int", + default: "90", + reqd: 1, }, { - "fieldname":"range4", - "label": __("Ageing Range 4"), - "fieldtype": "Int", - "default": "120", - "reqd": 1 + fieldname: "range4", + label: __("Ageing Range 4"), + fieldtype: "Int", + default: "120", + reqd: 1, }, { - "fieldname":"finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book" + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname":"cost_center", - "label": __("Cost Center"), - "fieldtype": "Link", - "options": "Cost Center", + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "Link", + options: "Cost Center", get_query: () => { - var company = frappe.query_report.get_filter_value('company'); + var company = frappe.query_report.get_filter_value("company"); return { filters: { - 'company': company - } - } - } + company: company, + }, + }; + }, }, { - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Autocomplete", + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Autocomplete", options: get_party_type_options(), - on_change: function() { - frappe.query_report.set_filter_value('party', ""); - frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier"); - } + on_change: function () { + frappe.query_report.set_filter_value("party", ""); + frappe.query_report.toggle_filter_display( + "supplier_group", + frappe.query_report.get_filter_value("party_type") !== "Supplier" + ); + }, }, { - "fieldname":"party", - "label": __("Party"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { + fieldname: "party", + label: __("Party"), + fieldtype: "MultiSelectList", + get_data: function (txt) { if (!frappe.query_report.filters) return; - let party_type = frappe.query_report.get_filter_value('party_type'); + let party_type = frappe.query_report.get_filter_value("party_type"); if (!party_type) return; return frappe.db.get_link_options(party_type, txt); }, }, { - "fieldname":"payment_terms_template", - "label": __("Payment Terms Template"), - "fieldtype": "Link", - "options": "Payment Terms Template" + fieldname: "payment_terms_template", + label: __("Payment Terms Template"), + fieldtype: "Link", + options: "Payment Terms Template", }, { - "fieldname":"supplier_group", - "label": __("Supplier Group"), - "fieldtype": "Link", - "options": "Supplier Group" + fieldname: "supplier_group", + label: __("Supplier Group"), + fieldtype: "Link", + options: "Supplier Group", }, { - "fieldname":"based_on_payment_terms", - "label": __("Based On Payment Terms"), - "fieldtype": "Check", + fieldname: "based_on_payment_terms", + label: __("Based On Payment Terms"), + fieldtype: "Check", }, { - "fieldname": "for_revaluation_journals", - "label": __("Revaluation Journals"), - "fieldtype": "Check", - } + fieldname: "for_revaluation_journals", + label: __("Revaluation Journals"), + fieldtype: "Check", + }, ], - onload: function(report) { - report.page.add_inner_button(__("Accounts Payable"), function() { + onload: function (report) { + report.page.add_inner_button(__("Accounts Payable"), function () { var filters = report.get_values(); - frappe.set_route('query-report', 'Accounts Payable', {company: filters.company}); + frappe.set_route("query-report", "Accounts Payable", { company: filters.company }); }); - } -} + }, +}; -erpnext.utils.add_dimensions('Accounts Payable Summary', 9); +erpnext.utils.add_dimensions("Accounts Payable Summary", 9); function get_party_type_options() { let options = []; - frappe.db.get_list( - "Party Type", {filters:{"account_type": "Payable"}, fields:['name']} - ).then((res) => { - res.forEach((party_type) => { - options.push(party_type.name); + frappe.db + .get_list("Party Type", { filters: { account_type: "Payable" }, fields: ["name"] }) + .then((res) => { + res.forEach((party_type) => { + options.push(party_type.name); + }); }); - }); return options; -} \ No newline at end of file +} diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index ed3b9915591..7d8d33c46b4 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -10,10 +10,8 @@

      {%= __(report.report_name) %}

      - {% if (filters.customer_name) { %} - {%= filters.customer_name %} - {% } else { %} - {%= filters.customer || filters.supplier %} + {% if (filters.party) { %} + {%= __(filters.party) %} {% } %}

      @@ -141,7 +139,7 @@
    {%= __("Reference") %}{%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %}{%= (filters.party) ? __("Remarks"): __("Party") %}{%= __("Invoiced Amount") %}{%= __("Remaining Balance") %}{%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %}{%= (filters.party) ? __("Remarks"): __("Party") %} {%= __("Total Invoiced Amount") %} {%= __("Total Paid Amount") %} {%= report.report_name === "Accounts Receivable Summary" ? __('Credit Note Amount') : __('Debit Note Amount') %} - {% if(!(filters.customer || filters.supplier)) { %} + {% if(!(filters.party)) { %} {%= data[i]["party"] %} {% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
    {%= data[i]["customer_name"] %} @@ -260,7 +258,7 @@ {% if(data[i]["party"]|| " ") { %} {% if(!data[i]["is_total_row"]) { %}
    - {% if(!(filters.customer || filters.supplier)) { %} + {% if(!(filters.party)) { %} {%= data[i]["party"] %} {% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
    {%= data[i]["customer_name"] %} diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index d6e3098e171..7e4563ee85e 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -4,220 +4,224 @@ frappe.provide("erpnext.utils"); frappe.query_reports["Accounts Receivable"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname": "report_date", - "label": __("Posting Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "report_date", + label: __("Posting Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, { - "fieldname": "finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book" + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname": "cost_center", - "label": __("Cost Center"), - "fieldtype": "Link", - "options": "Cost Center", + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "Link", + options: "Cost Center", get_query: () => { - var company = frappe.query_report.get_filter_value('company'); + var company = frappe.query_report.get_filter_value("company"); return { filters: { - 'company': company - } + company: company, + }, }; - } + }, }, { - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Autocomplete", + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Autocomplete", options: get_party_type_options(), - on_change: function() { - frappe.query_report.set_filter_value('party', ""); - frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer"); - } + on_change: function () { + frappe.query_report.set_filter_value("party", ""); + frappe.query_report.toggle_filter_display( + "customer_group", + frappe.query_report.get_filter_value("party_type") !== "Customer" + ); + }, }, { - "fieldname":"party", - "label": __("Party"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { + fieldname: "party", + label: __("Party"), + fieldtype: "MultiSelectList", + get_data: function (txt) { if (!frappe.query_report.filters) return; - let party_type = frappe.query_report.get_filter_value('party_type'); + let party_type = frappe.query_report.get_filter_value("party_type"); if (!party_type) return; return frappe.db.get_link_options(party_type, txt); }, }, { - "fieldname": "party_account", - "label": __("Receivable Account"), - "fieldtype": "Link", - "options": "Account", + fieldname: "party_account", + label: __("Receivable Account"), + fieldtype: "Link", + options: "Account", get_query: () => { - var company = frappe.query_report.get_filter_value('company'); + var company = frappe.query_report.get_filter_value("company"); return { filters: { - 'company': company, - 'account_type': 'Receivable', - 'is_group': 0 - } + company: company, + account_type: "Receivable", + is_group: 0, + }, }; - } + }, }, { - "fieldname": "ageing_based_on", - "label": __("Ageing Based On"), - "fieldtype": "Select", - "options": 'Posting Date\nDue Date', - "default": "Due Date" + fieldname: "ageing_based_on", + label: __("Ageing Based On"), + fieldtype: "Select", + options: "Posting Date\nDue Date", + default: "Due Date", }, { - "fieldname": "range1", - "label": __("Ageing Range 1"), - "fieldtype": "Int", - "default": "30", - "reqd": 1 + fieldname: "range1", + label: __("Ageing Range 1"), + fieldtype: "Int", + default: "30", + reqd: 1, }, { - "fieldname": "range2", - "label": __("Ageing Range 2"), - "fieldtype": "Int", - "default": "60", - "reqd": 1 + fieldname: "range2", + label: __("Ageing Range 2"), + fieldtype: "Int", + default: "60", + reqd: 1, }, { - "fieldname": "range3", - "label": __("Ageing Range 3"), - "fieldtype": "Int", - "default": "90", - "reqd": 1 + fieldname: "range3", + label: __("Ageing Range 3"), + fieldtype: "Int", + default: "90", + reqd: 1, }, { - "fieldname": "range4", - "label": __("Ageing Range 4"), - "fieldtype": "Int", - "default": "120", - "reqd": 1 + fieldname: "range4", + label: __("Ageing Range 4"), + fieldtype: "Int", + default: "120", + reqd: 1, }, { - "fieldname":"customer_group", - "label": __("Customer Group"), - "fieldtype": "MultiSelectList", - "options": "Customer Group", - get_data: function(txt) { - return frappe.db.get_link_options('Customer Group', txt); - } + fieldname: "customer_group", + label: __("Customer Group"), + fieldtype: "MultiSelectList", + options: "Customer Group", + get_data: function (txt) { + return frappe.db.get_link_options("Customer Group", txt); + }, }, { - "fieldname": "payment_terms_template", - "label": __("Payment Terms Template"), - "fieldtype": "Link", - "options": "Payment Terms Template" + fieldname: "payment_terms_template", + label: __("Payment Terms Template"), + fieldtype: "Link", + options: "Payment Terms Template", }, { - "fieldname": "sales_partner", - "label": __("Sales Partner"), - "fieldtype": "Link", - "options": "Sales Partner" + fieldname: "sales_partner", + label: __("Sales Partner"), + fieldtype: "Link", + options: "Sales Partner", }, { - "fieldname": "sales_person", - "label": __("Sales Person"), - "fieldtype": "Link", - "options": "Sales Person" + fieldname: "sales_person", + label: __("Sales Person"), + fieldtype: "Link", + options: "Sales Person", }, { - "fieldname": "territory", - "label": __("Territory"), - "fieldtype": "Link", - "options": "Territory" + fieldname: "territory", + label: __("Territory"), + fieldtype: "Link", + options: "Territory", }, { - "fieldname": "group_by_party", - "label": __("Group By Customer"), - "fieldtype": "Check" + fieldname: "group_by_party", + label: __("Group By Customer"), + fieldtype: "Check", }, { - "fieldname": "based_on_payment_terms", - "label": __("Based On Payment Terms"), - "fieldtype": "Check", + fieldname: "based_on_payment_terms", + label: __("Based On Payment Terms"), + fieldtype: "Check", }, { - "fieldname": "show_future_payments", - "label": __("Show Future Payments"), - "fieldtype": "Check", + fieldname: "show_future_payments", + label: __("Show Future Payments"), + fieldtype: "Check", }, { - "fieldname": "show_delivery_notes", - "label": __("Show Linked Delivery Notes"), - "fieldtype": "Check", + fieldname: "show_delivery_notes", + label: __("Show Linked Delivery Notes"), + fieldtype: "Check", }, { - "fieldname": "show_sales_person", - "label": __("Show Sales Person"), - "fieldtype": "Check", + fieldname: "show_sales_person", + label: __("Show Sales Person"), + fieldtype: "Check", }, { - "fieldname": "show_remarks", - "label": __("Show Remarks"), - "fieldtype": "Check", + fieldname: "show_remarks", + label: __("Show Remarks"), + fieldtype: "Check", }, { - "fieldname": "for_revaluation_journals", - "label": __("Revaluation Journals"), - "fieldtype": "Check", + fieldname: "for_revaluation_journals", + label: __("Revaluation Journals"), + fieldtype: "Check", }, { - "fieldname": "ignore_accounts", - "label": __("Group by Voucher"), - "fieldtype": "Check", - } - - + fieldname: "in_party_currency", + label: __("In Party Currency"), + fieldtype: "Check", + }, + { + fieldname: "ignore_accounts", + label: __("Group by Voucher"), + fieldtype: "Check", + }, ], - "formatter": function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (data && data.bold) { value = value.bold(); - } return value; }, - onload: function(report) { - report.page.add_inner_button(__("Accounts Receivable Summary"), function() { + onload: function (report) { + report.page.add_inner_button(__("Accounts Receivable Summary"), function () { var filters = report.get_values(); - frappe.set_route('query-report', 'Accounts Receivable Summary', {company: filters.company}); + frappe.set_route("query-report", "Accounts Receivable Summary", { company: filters.company }); }); - } -} - -erpnext.utils.add_dimensions('Accounts Receivable', 9); + }, +}; +erpnext.utils.add_dimensions("Accounts Receivable", 9); function get_party_type_options() { let options = []; - frappe.db.get_list( - "Party Type", {filters:{"account_type": "Receivable"}, fields:['name']} - ).then((res) => { - res.forEach((party_type) => { - options.push(party_type.name); + frappe.db + .get_list("Party Type", { filters: { account_type: "Receivable" }, fields: ["name"] }) + .then((res) => { + res.forEach((party_type) => { + options.push(party_type.name); + }); }); - }); return options; } diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py old mode 100755 new mode 100644 index e4d5938c0b5..dcfd85afddb --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -5,7 +5,7 @@ from collections import OrderedDict import frappe -from frappe import _, qb, scrub +from frappe import _, qb, query_builder, scrub from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate @@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision # 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters # 7. For overpayment against an invoice with payment terms, there will be an additional row # 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated -# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party -# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable" +# 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency +# 10. This report is based on Payment Ledger Entries def execute(filters=None): @@ -40,24 +40,20 @@ def execute(filters=None): return ReceivablePayableReport(filters).run(args) -class ReceivablePayableReport(object): +class ReceivablePayableReport: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.qb_selection_filter = [] self.ple = qb.DocType("Payment Ledger Entry") self.filters.report_date = getdate(self.filters.report_date or nowdate()) self.age_as_on = ( - getdate(nowdate()) - if self.filters.report_date > getdate(nowdate()) - else self.filters.report_date + getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date ) def run(self, args): self.filters.update(args) self.set_defaults() - self.party_naming_by = frappe.db.get_value( - args.get("naming_by")[0], None, args.get("naming_by")[1] - ) + self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) self.get_columns() self.get_data() self.get_chart_data() @@ -72,9 +68,7 @@ class ReceivablePayableReport(object): self.currency_precision = get_currency_precision() or 2 self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit" self.account_type = self.filters.account_type - self.party_type = frappe.db.get_all( - "Party Type", {"account_type": self.account_type}, pluck="name" - ) + self.party_type = frappe.db.get_all("Party Type", {"account_type": self.account_type}, pluck="name") self.party_details = {} self.invoices = set() self.skip_total_row = 0 @@ -84,6 +78,12 @@ class ReceivablePayableReport(object): self.total_row_map = {} self.skip_total_row = 1 + if self.filters.get("in_party_currency"): + if self.filters.get("party") and len(self.filters.get("party")) == 1: + self.skip_total_row = 0 + else: + self.skip_total_row = 1 + def get_data(self): self.get_ple_entries() self.get_sales_invoices_or_customers_based_on_sales_person() @@ -122,7 +122,7 @@ class ReceivablePayableReport(object): else: key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) - if not key in self.voucher_balance: + if key not in self.voucher_balance: self.voucher_balance[key] = frappe._dict( voucher_type=ple.voucher_type, voucher_no=ple.voucher_no, @@ -145,7 +145,7 @@ class ReceivablePayableReport(object): if self.filters.get("group_by_party"): self.init_subtotal_row(ple.party) - if self.filters.get("group_by_party"): + if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"): self.init_subtotal_row("Total") def get_invoices(self, ple): @@ -224,8 +224,7 @@ class ReceivablePayableReport(object): if not row: return - # amount in "Party Currency", if its supplied. If not, amount in company currency - if self.filters.get("party_type") and self.filters.get("party"): + if self.filters.get("in_party_currency") or self.filters.get("party_account"): amount = ple.amount_in_account_currency else: amount = ple.amount @@ -260,8 +259,10 @@ class ReceivablePayableReport(object): def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) - for field in self.get_currency_fields(): - total_row[field] += row.get(field, 0.0) + if total_row: + for field in self.get_currency_fields(): + total_row[field] += row.get(field, 0.0) + total_row["currency"] = row.get("currency", "") def append_subtotal_row(self, party): sub_total_row = self.total_row_map.get(party) @@ -274,7 +275,7 @@ class ReceivablePayableReport(object): def build_data(self): # set outstanding for all the accumulated balances # as we can use this to filter out invoices without outstanding - for key, row in self.voucher_balance.items(): + for _key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) row.outstanding_in_account_currency = flt( row.invoiced_in_account_currency @@ -288,7 +289,7 @@ class ReceivablePayableReport(object): must_consider = False if self.filters.get("for_revaluation_journals"): if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or ( - (abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision) + abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision ): must_consider = True else: @@ -322,7 +323,7 @@ class ReceivablePayableReport(object): if self.filters.get("group_by_party"): self.append_subtotal_row(self.previous_party) if self.data: - self.data.append(self.total_row_map.get("Total")) + self.data.append(self.total_row_map.get("Total", {})) def append_row(self, row): self.allocate_future_payments(row) @@ -453,7 +454,7 @@ class ReceivablePayableReport(object): party_details = self.get_party_details(row.party) or {} row.update(party_details) - if self.filters.get("party_type") and self.filters.get("party"): + if self.filters.get("in_party_currency") or self.filters.get("party_account"): row.currency = row.account_currency else: row.currency = self.company_currency @@ -474,19 +475,17 @@ class ReceivablePayableReport(object): def get_payment_terms(self, row): # build payment_terms for row payment_terms_details = frappe.db.sql( - """ + f""" select si.name, si.party_account_currency, si.currency, si.conversion_rate, si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount, ps.description, ps.paid_amount, ps.discounted_amount - from `tab{0}` si, `tabPayment Schedule` ps + from `tab{row.voucher_type}` si, `tabPayment Schedule` ps where si.name = ps.parent and si.name = %s order by ps.paid_amount desc, due_date - """.format( - row.voucher_type - ), + """, row.voucher_no, as_dict=1, ) @@ -574,6 +573,8 @@ class ReceivablePayableReport(object): def get_future_payments_from_payment_entry(self): pe = frappe.qb.DocType("Payment Entry") pe_ref = frappe.qb.DocType("Payment Entry Reference") + ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) + return ( frappe.qb.from_(pe) .inner_join(pe_ref) @@ -585,6 +586,11 @@ class ReceivablePayableReport(object): (pe.posting_date).as_("future_date"), (pe_ref.allocated_amount).as_("future_amount"), (pe.reference_no).as_("future_ref"), + ifelse( + pe.payment_type == "Receive", + pe.source_exchange_rate * pe_ref.allocated_amount, + pe.target_exchange_rate * pe_ref.allocated_amount, + ).as_("future_amount_in_base_currency"), ) .where( (pe.docstatus < 2) @@ -621,13 +627,24 @@ class ReceivablePayableReport(object): query = query.select( Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount") ) + query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency")) else: query = query.select( Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount") ) + query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency")) else: query = query.select( - Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount") + Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_( + "future_amount_in_base_currency" + ) + ) + query = query.select( + Sum( + jea.debit_in_account_currency + if self.account_type == "Payable" + else jea.credit_in_account_currency + ).as_("future_amount") ) query = query.having(qb.Field("future_amount") > 0) @@ -643,14 +660,19 @@ class ReceivablePayableReport(object): row.remaining_balance = row.outstanding row.future_amount = 0.0 for future in self.future_payments.get((row.voucher_no, row.party), []): - if row.remaining_balance > 0 and future.future_amount: - if future.future_amount > row.outstanding: + if self.filters.in_party_currency: + future_amount_field = "future_amount" + else: + future_amount_field = "future_amount_in_base_currency" + + if row.remaining_balance != 0 and future.get(future_amount_field): + if future.get(future_amount_field) > row.outstanding: row.future_amount = row.outstanding - future.future_amount = future.future_amount - row.outstanding + future[future_amount_field] = future.get(future_amount_field) - row.outstanding row.remaining_balance = 0 else: - row.future_amount += future.future_amount - future.future_amount = 0 + row.future_amount += future.get(future_amount_field) + future[future_amount_field] = 0 row.remaining_balance = row.outstanding - row.future_amount row.setdefault("future_ref", []).append( @@ -662,7 +684,12 @@ class ReceivablePayableReport(object): def get_return_entries(self): doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice" - filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company} + filters = { + "is_return": 1, + "docstatus": 1, + "company": self.filters.company, + "update_outstanding_for_self": 0, + } or_filters = {} for party_type in self.party_type: party_field = scrub(party_type) @@ -702,9 +729,7 @@ class ReceivablePayableReport(object): row.age = (getdate(self.age_as_on) - getdate(entry_date)).days or 0 index = None - if not ( - self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4 - ): + if not (self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4): self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = ( 30, 60, @@ -730,12 +755,10 @@ class ReceivablePayableReport(object): if self.filters.show_future_payments: self.qb_selection_filter.append( - ( - self.ple.posting_date.lte(self.filters.report_date) - | ( - (self.ple.voucher_no == self.ple.against_voucher_no) - & (Date(self.ple.creation).lte(self.filters.report_date)) - ) + self.ple.posting_date.lte(self.filters.report_date) + | ( + (self.ple.voucher_no == self.ple.against_voucher_no) + & (Date(self.ple.creation).lte(self.filters.report_date)) ) ) else: @@ -803,7 +826,7 @@ class ReceivablePayableReport(object): self.qb_selection_filter = [] self.or_filters = [] - for party_type in self.party_type: + for _party_type in self.party_type: self.add_common_filters() if self.account_type == "Receivable": @@ -940,7 +963,7 @@ class ReceivablePayableReport(object): return True def get_party_details(self, party): - if not party in self.party_details: + if party not in self.party_details: if self.account_type == "Receivable": fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"] diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 77f8c6eaaa9..a65e424173c 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,11 +1,8 @@ -import unittest - import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, getdate, today -from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute @@ -62,7 +59,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): pe.insert() pe.submit() - def create_credit_note(self, docname): + def create_credit_note(self, docname, do_not_submit=False): credit_note = create_sales_invoice( company=self.company, customer=self.customer, @@ -72,6 +69,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): cost_center=self.cost_center, is_return=1, return_against=docname, + do_not_submit=do_not_submit, ) return credit_note @@ -125,7 +123,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): # check invoice grand total and invoiced column's value for 3 payment terms si = self.create_sales_invoice() - name = si.name report = execute(filters) @@ -149,7 +146,9 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): ) # check invoice grand total, invoiced, paid and outstanding column's value after credit note - self.create_credit_note(si.name) + cr_note = self.create_credit_note(si.name, do_not_submit=True) + cr_note.update_outstanding_for_self = False + cr_note.save().submit() report = execute(filters) expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to] @@ -167,6 +166,82 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): ], ) + def test_cr_note_flag_to_update_self(self): + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "show_remarks": True, + } + + # check invoice grand total and invoiced column's value for 3 payment terms + si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si.set_posting_time = True + si.posting_date = add_days(today(), -1) + si.save().submit() + + report = execute(filters) + + expected_data = [100, 100, "No Remarks"] + + self.assertEqual(len(report[1]), 1) + row = report[1][0] + self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) + + # check invoice grand total, invoiced, paid and outstanding column's value after payment + self.create_payment_entry(si.name) + report = execute(filters) + + expected_data_after_payment = [100, 100, 40, 60] + self.assertEqual(len(report[1]), 1) + row = report[1][0] + self.assertEqual( + expected_data_after_payment, + [row.invoice_grand_total, row.invoiced, row.paid, row.outstanding], + ) + + # check invoice grand total, invoiced, paid and outstanding column's value after credit note + cr_note = self.create_credit_note(si.name, do_not_submit=True) + cr_note.update_outstanding_for_self = True + cr_note.save().submit() + report = execute(filters) + + expected_data_after_credit_note = [ + [100.0, 100.0, 40.0, 0.0, 60.0, si.name], + [0, 0, 100.0, 0.0, -100.0, cr_note.name], + ] + self.assertEqual(len(report[1]), 2) + si_row = next( + [ + row.invoice_grand_total, + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + row.voucher_no, + ] + for row in report[1] + if row.voucher_no == si.name + ) + + cr_note_row = next( + [ + row.invoice_grand_total, + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + row.voucher_no, + ] + for row in report[1] + if row.voucher_no == cr_note.name + ) + self.assertEqual(expected_data_after_credit_note[0], si_row) + self.assertEqual(expected_data_after_credit_note[1], cr_note_row) + def test_payment_againt_po_in_receivable_report(self): """ Payments made against Purchase Order will show up as outstanding amount @@ -238,9 +313,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): err.extend("accounts", accounts) err.accounts[0].new_exchange_rate = 85 row = err.accounts[0] - row.new_balance_in_base_currency = flt( - row.new_exchange_rate * flt(row.balance_in_account_currency) - ) + row.new_balance_in_base_currency = flt(row.new_exchange_rate * flt(row.balance_in_account_currency)) row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) err.set_total_gain_loss() err = err.save().submit() @@ -261,7 +334,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): report = execute(filters) expected_data_for_err = [0, -500, 0, 500] - row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0] + row = next(x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name) self.assertEqual( expected_data_for_err, [ @@ -390,11 +463,30 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): ) def test_future_payments(self): + sr = self.create_sales_invoice(do_not_submit=True) + sr.is_return = 1 + sr.items[0].qty = -1 + sr.items[0].rate = 10 + sr.calculate_taxes_and_totals() + sr.submit() + si = self.create_sales_invoice() pe = get_payment_entry(si.doctype, si.name) + pe.append( + "references", + { + "reference_doctype": sr.doctype, + "reference_name": sr.name, + "due_date": sr.due_date, + "total_amount": sr.grand_total, + "outstanding_amount": sr.outstanding_amount, + "allocated_amount": sr.outstanding_amount, + }, + ) + pe.posting_date = add_days(today(), 1) - pe.paid_amount = 90.0 - pe.references[0].allocated_amount = 90.0 + pe.paid_amount = 80 + pe.references[0].allocated_amount = 90.0 # pe.paid_amount + sr.grand_total pe.save().submit() filters = { "company": self.company, @@ -406,16 +498,21 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "show_future_payments": True, } report = execute(filters)[1] - self.assertEqual(len(report), 1) + self.assertEqual(len(report), 2) - expected_data = [100.0, 100.0, 10.0, 90.0] + expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]} - row = report[0] - self.assertEqual( - expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] - ) + rows = report[:2] + for row in rows: + self.assertEqual( + expected_data[row.voucher_no], + [row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount], + ) pe.cancel() + sr.load_from_db() # Outstanding amount is updated so a updated timestamp is needed. + sr.cancel() + # full payment in future date pe = get_payment_entry(si.doctype, si.name) pe.posting_date = add_days(today(), 1) @@ -472,7 +569,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person]) def test_cost_center_filter(self): - si = self.create_sales_invoice() + self.create_sales_invoice() filters = { "company": self.company, "report_date": today(), @@ -489,7 +586,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center]) def test_customer_group_filter(self): - si = self.create_sales_invoice() + self.create_sales_invoice() cus_group = frappe.db.get_value("Customer", self.customer, "customer_group") filters = { "company": self.company, @@ -511,7 +608,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): self.assertEqual(len(report), 0) def test_multi_customer_group_filter(self): - si = self.create_sales_invoice() + self.create_sales_invoice() cus_group = frappe.db.get_value("Customer", self.customer, "customer_group") # Create a list of customer groups, e.g., ["Group1", "Group2"] cus_groups_list = [cus_group, "_Test Customer Group 1"] @@ -579,7 +676,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): filters.update({"party_account": self.debtors_usd}) report = execute(filters)[1] self.assertEqual(len(report), 1) - expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency] + expected_data = [100.0, 100.0, self.debtors_usd, si2.currency] row = report[0] self.assertEqual( expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency] @@ -616,6 +713,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "range2": 60, "range3": 90, "range4": 120, + "in_party_currency": 1, } si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) @@ -623,7 +721,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): si.conversion_rate = 80 si.debit_to = self.debtors_usd si.save().submit() - name = si.name # check invoice grand total and invoiced column's value for 3 payment terms report = execute(filters) @@ -683,9 +780,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): def test_report_output_if_party_is_missing(self): acc_name = "Additional Debtors" - if not frappe.db.get_value( - "Account", filters={"account_name": acc_name, "company": self.company} - ): + if not frappe.db.get_value("Account", filters={"account_name": acc_name, "company": self.company}): additional_receivable_acc = frappe.get_doc( { "doctype": "Account", @@ -771,3 +866,92 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): # post sorting output should be [[Additional Debtors, ...], [Debtors, ...]] report_output = sorted(report_output, key=lambda x: x[0]) self.assertEqual(expected_data, report_output) + + def test_future_payments_on_foreign_currency(self): + self.customer2 = ( + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "Jane Doe", + "type": "Individual", + "default_currency": "USD", + } + ) + .insert() + .submit() + ) + + si = self.create_sales_invoice(do_not_submit=True) + si.posting_date = add_days(today(), -1) + si.customer = self.customer2 + si.currency = "USD" + si.conversion_rate = 80 + si.debit_to = self.debtors_usd + si.save().submit() + + # full payment in USD + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.base_received_amount = 7500 + pe.received_amount = 7500 + pe.source_exchange_rate = 75 + pe.save().submit() + + filters = frappe._dict( + { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "show_future_payments": True, + "in_party_currency": False, + } + ) + report = execute(filters)[1] + self.assertEqual(len(report), 1) + + expected_data = [8000.0, 8000.0, 500.0, 7500.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + filters.in_party_currency = True + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, 0.0, 100.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + pe.cancel() + # partial payment in USD on a future date + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.base_received_amount = 6750 + pe.received_amount = 6750 + pe.source_exchange_rate = 75 + pe.paid_amount = 90 # in USD + pe.references[0].allocated_amount = 90 + pe.save().submit() + + filters.in_party_currency = False + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [8000.0, 8000.0, 1250.0, 6750.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + filters.in_party_currency = True + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, 10.0, 90.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 2f6d2582b33..964abc23747 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -2,168 +2,171 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Accounts Receivable Summary"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"report_date", - "label": __("Posting Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "report_date", + label: __("Posting Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, { - "fieldname":"ageing_based_on", - "label": __("Ageing Based On"), - "fieldtype": "Select", - "options": 'Posting Date\nDue Date', - "default": "Due Date" + fieldname: "ageing_based_on", + label: __("Ageing Based On"), + fieldtype: "Select", + options: "Posting Date\nDue Date", + default: "Due Date", }, { - "fieldname":"range1", - "label": __("Ageing Range 1"), - "fieldtype": "Int", - "default": "30", - "reqd": 1 + fieldname: "range1", + label: __("Ageing Range 1"), + fieldtype: "Int", + default: "30", + reqd: 1, }, { - "fieldname":"range2", - "label": __("Ageing Range 2"), - "fieldtype": "Int", - "default": "60", - "reqd": 1 + fieldname: "range2", + label: __("Ageing Range 2"), + fieldtype: "Int", + default: "60", + reqd: 1, }, { - "fieldname":"range3", - "label": __("Ageing Range 3"), - "fieldtype": "Int", - "default": "90", - "reqd": 1 + fieldname: "range3", + label: __("Ageing Range 3"), + fieldtype: "Int", + default: "90", + reqd: 1, }, { - "fieldname":"range4", - "label": __("Ageing Range 4"), - "fieldtype": "Int", - "default": "120", - "reqd": 1 + fieldname: "range4", + label: __("Ageing Range 4"), + fieldtype: "Int", + default: "120", + reqd: 1, }, { - "fieldname":"finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book" + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname":"cost_center", - "label": __("Cost Center"), - "fieldtype": "Link", - "options": "Cost Center", + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "Link", + options: "Cost Center", get_query: () => { - var company = frappe.query_report.get_filter_value('company'); + var company = frappe.query_report.get_filter_value("company"); return { filters: { - 'company': company - } - } - } + company: company, + }, + }; + }, }, { - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Autocomplete", + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Autocomplete", options: get_party_type_options(), - on_change: function() { - frappe.query_report.set_filter_value('party', ""); - frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer"); - } + on_change: function () { + frappe.query_report.set_filter_value("party", ""); + frappe.query_report.toggle_filter_display( + "customer_group", + frappe.query_report.get_filter_value("party_type") !== "Customer" + ); + }, }, { - "fieldname":"party", - "label": __("Party"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { + fieldname: "party", + label: __("Party"), + fieldtype: "MultiSelectList", + get_data: function (txt) { if (!frappe.query_report.filters) return; - let party_type = frappe.query_report.get_filter_value('party_type'); + let party_type = frappe.query_report.get_filter_value("party_type"); if (!party_type) return; return frappe.db.get_link_options(party_type, txt); }, }, { - "fieldname":"customer_group", - "label": __("Customer Group"), - "fieldtype": "Link", - "options": "Customer Group" + fieldname: "customer_group", + label: __("Customer Group"), + fieldtype: "Link", + options: "Customer Group", }, { - "fieldname":"payment_terms_template", - "label": __("Payment Terms Template"), - "fieldtype": "Link", - "options": "Payment Terms Template" + fieldname: "payment_terms_template", + label: __("Payment Terms Template"), + fieldtype: "Link", + options: "Payment Terms Template", }, { - "fieldname":"territory", - "label": __("Territory"), - "fieldtype": "Link", - "options": "Territory" + fieldname: "territory", + label: __("Territory"), + fieldtype: "Link", + options: "Territory", }, { - "fieldname":"sales_partner", - "label": __("Sales Partner"), - "fieldtype": "Link", - "options": "Sales Partner" + fieldname: "sales_partner", + label: __("Sales Partner"), + fieldtype: "Link", + options: "Sales Partner", }, { - "fieldname":"sales_person", - "label": __("Sales Person"), - "fieldtype": "Link", - "options": "Sales Person" + fieldname: "sales_person", + label: __("Sales Person"), + fieldtype: "Link", + options: "Sales Person", }, { - "fieldname":"based_on_payment_terms", - "label": __("Based On Payment Terms"), - "fieldtype": "Check", + fieldname: "based_on_payment_terms", + label: __("Based On Payment Terms"), + fieldtype: "Check", }, { - "fieldname":"show_future_payments", - "label": __("Show Future Payments"), - "fieldtype": "Check", + fieldname: "show_future_payments", + label: __("Show Future Payments"), + fieldtype: "Check", }, { - "fieldname":"show_gl_balance", - "label": __("Show GL Balance"), - "fieldtype": "Check", + fieldname: "show_gl_balance", + label: __("Show GL Balance"), + fieldtype: "Check", }, { - "fieldname": "for_revaluation_journals", - "label": __("Revaluation Journals"), - "fieldtype": "Check", - } + fieldname: "for_revaluation_journals", + label: __("Revaluation Journals"), + fieldtype: "Check", + }, ], - onload: function(report) { - report.page.add_inner_button(__("Accounts Receivable"), function() { + onload: function (report) { + report.page.add_inner_button(__("Accounts Receivable"), function () { var filters = report.get_values(); - frappe.set_route('query-report', 'Accounts Receivable', { company: filters.company }); + frappe.set_route("query-report", "Accounts Receivable", { company: filters.company }); }); - } -} + }, +}; -erpnext.utils.add_dimensions('Accounts Receivable Summary', 9); +erpnext.utils.add_dimensions("Accounts Receivable Summary", 9); function get_party_type_options() { let options = []; - frappe.db.get_list( - "Party Type", {filters:{"account_type": "Receivable"}, fields:['name']} - ).then((res) => { - res.forEach((party_type) => { - options.push(party_type.name); + frappe.db + .get_list("Party Type", { filters: { account_type: "Receivable" }, fields: ["name"] }) + .then((res) => { + res.forEach((party_type) => { + options.push(party_type.name); + }); }); - }); return options; -} \ No newline at end of file +} diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index a92f960fdf0..b1c02b38452 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -23,12 +23,8 @@ def execute(filters=None): class AccountsReceivableSummary(ReceivablePayableReport): def run(self, args): self.account_type = args.get("account_type") - self.party_type = frappe.db.get_all( - "Party Type", {"account_type": self.account_type}, pluck="name" - ) - self.party_naming_by = frappe.db.get_value( - args.get("naming_by")[0], None, args.get("naming_by")[1] - ) + self.party_type = frappe.db.get_all("Party Type", {"account_type": self.account_type}, pluck="name") + self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) self.get_columns() self.get_data(args) return self.columns, self.data diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py index 3ee35a114d1..4ef607bab28 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py @@ -1,5 +1,3 @@ -import unittest - import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import today diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js index 12b94347e00..db49ccb79a8 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js @@ -2,58 +2,58 @@ // For license information, please see license.txt frappe.query_reports["Asset Depreciation Ledger"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "reqd": 1 + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1 + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"asset", - "label": __("Asset"), - "fieldtype": "Link", - "options": "Asset" + fieldname: "asset", + label: __("Asset"), + fieldtype: "Link", + options: "Asset", }, { - "fieldname":"asset_category", - "label": __("Asset Category"), - "fieldtype": "Link", - "options": "Asset Category" + fieldname: "asset_category", + label: __("Asset Category"), + fieldtype: "Link", + options: "Asset Category", }, { - "fieldname":"cost_center", - "label": __("Cost Center"), - "fieldtype": "Link", - "options": "Cost Center" + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "Link", + options: "Cost Center", }, { - "fieldname":"finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book" + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname": "include_default_book_assets", - "label": __("Include Default FB Assets"), - "fieldtype": "Check", - "default": 1 + fieldname: "include_default_book_assets", + label: __("Include Default FB Assets"), + fieldtype: "Check", + default: 1, }, - ] -} + ], +}; diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js index 5f78b779342..88ec0881585 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js @@ -2,34 +2,34 @@ // For license information, please see license.txt frappe.query_reports["Asset Depreciations and Balances"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], - "reqd": 1 + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + reqd: 1, }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], - "reqd": 1 + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + reqd: 1, }, { - "fieldname":"asset_category", - "label": __("Asset Category"), - "fieldtype": "Link", - "options": "Asset Category" - } - ] -} + fieldname: "asset_category", + label: __("Asset Category"), + fieldtype: "Link", + options: "Asset Category", + }, + ], +}; diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index bdc8d8504f8..1754780e346 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -62,7 +62,7 @@ def get_asset_categories(filters): if filters.get("asset_category"): condition += " and asset_category = %(asset_category)s" return frappe.db.sql( - """ + f""" SELECT asset_category, ifnull(sum(case when purchase_date < %(from_date)s then case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then @@ -101,11 +101,9 @@ def get_asset_categories(filters): 0 end), 0) as cost_of_scrapped_asset from `tabAsset` - where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {} + where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition} group by asset_category - """.format( - condition - ), + """, { "to_date": filters.to_date, "from_date": filters.from_date, @@ -170,9 +168,7 @@ def get_assets(filters): where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} group by a.asset_category) as results group by results.asset_category - """.format( - condition - ), + """.format(condition), {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1, ) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js index f1f8e5f6e7c..605dff8b608 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.js +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js @@ -1,22 +1,33 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.require("assets/erpnext/js/financial_statements.js", function() { +frappe.require("assets/erpnext/js/financial_statements.js", function () { frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statements); - erpnext.utils.add_dimensions('Balance Sheet', 10); + erpnext.utils.add_dimensions("Balance Sheet", 10); frappe.query_reports["Balance Sheet"]["filters"].push({ - "fieldname": "accumulated_values", - "label": __("Accumulated Values"), - "fieldtype": "Check", - "default": 1 + fieldname: "selected_view", + label: __("Select View"), + fieldtype: "Select", + options: [ + { value: "Report", label: __("Report View") }, + { value: "Growth", label: __("Growth View") }, + ], + default: "Report", + reqd: 1, + }); + frappe.query_reports["Balance Sheet"]["filters"].push({ + fieldname: "accumulated_values", + label: __("Accumulated Values"), + fieldtype: "Check", + default: 1, }); frappe.query_reports["Balance Sheet"]["filters"].push({ - "fieldname": "include_default_book_entries", - "label": __("Include Default FB Entries"), - "fieldtype": "Check", - "default": 1 + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, }); }); diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index b225aac7b56..2106451bd1a 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -97,11 +97,11 @@ def execute(filters=None): chart = get_chart_data(filters, columns, asset, liability, equity) - report_summary = get_report_summary( + report_summary, primitive_summary = get_report_summary( period_list, asset, liability, equity, provisional_profit_loss, currency, filters ) - return columns, data, message, chart, report_summary + return columns, data, message, chart, report_summary, primitive_summary def get_provisional_profit_loss( @@ -180,7 +180,6 @@ def get_report_summary( filters, consolidated=False, ): - net_asset, net_liability, net_equity, net_provisional_profit_loss = 0.0, 0.0, 0.0, 0.0 if filters.get("accumulated_values"): @@ -217,7 +216,7 @@ def get_report_summary( "datatype": "Currency", "currency": currency, }, - ] + ], (net_asset - net_liability + net_equity) def get_chart_data(filters, columns, asset, liability, equity): diff --git a/erpnext/accounts/report/balance_sheet/test_balance_sheet.py b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py index 3cb6efebee3..ca357ece1d1 100644 --- a/erpnext/accounts/report/balance_sheet/test_balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py @@ -13,15 +13,13 @@ class TestBalanceSheet(FrappeTestCase): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( create_sales_invoice, - make_sales_invoice, ) - from erpnext.accounts.utils import get_fiscal_year frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'") frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 6'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'") - pi = make_purchase_invoice( + make_purchase_invoice( company="_Test Company 6", warehouse="Finished Goods - _TC6", expense_account="Cost of Goods Sold - _TC6", @@ -29,7 +27,7 @@ class TestBalanceSheet(FrappeTestCase): qty=10, rate=100, ) - si = create_sales_invoice( + create_sales_invoice( company="_Test Company 6", debit_to="Debtors - _TC6", income_account="Sales - _TC6", diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js index 8a7d071a474..e8ae2a1dabe 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js @@ -2,37 +2,38 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Bank Clearance Summary"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], - "width": "80" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + width: "80", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, { - "fieldname":"account", - "label": __("Bank Account"), - "fieldtype": "Link", - "options": "Account", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company")? - locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "", - "get_query": function() { + fieldname: "account", + label: __("Bank Account"), + fieldtype: "Link", + options: "Account", + reqd: 1, + default: frappe.defaults.get_user_default("Company") + ? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"] + : "", + get_query: function () { return { - "query": "erpnext.controllers.queries.get_account_list", - "filters": [ - ['Account', 'account_type', 'in', 'Bank, Cash'], - ['Account', 'is_group', '=', 0], - ] - } - } + query: "erpnext.controllers.queries.get_account_list", + filters: [ + ["Account", "account_type", "in", "Bank, Cash"], + ["Account", "is_group", "=", 0], + ], + }; + }, }, - ] -} + ], +}; diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index cb7445546f1..ab8bdd7afbd 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -63,31 +63,27 @@ def get_conditions(filters): def get_entries(filters): conditions = get_conditions(filters) journal_entries = frappe.db.sql( - """SELECT + f"""SELECT "Journal Entry", jv.name, jv.posting_date, jv.cheque_no, jv.clearance_date, jvd.against_account, jvd.debit - jvd.credit FROM `tabJournal Entry Account` jvd, `tabJournal Entry` jv WHERE - jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {0} - order by posting_date DESC, jv.name DESC""".format( - conditions - ), + jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {conditions} + order by posting_date DESC, jv.name DESC""", filters, as_list=1, ) payment_entries = frappe.db.sql( - """SELECT + f"""SELECT "Payment Entry", name, posting_date, reference_no, clearance_date, party, if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount) FROM `tabPayment Entry` WHERE - docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {0} - order by posting_date DESC, name DESC""".format( - conditions - ), + docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {conditions} + order by posting_date DESC, name DESC""", filters, as_list=1, ) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js index 9bb6a14c677..efcfa7a5ee5 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js @@ -2,47 +2,48 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Bank Reconciliation Statement"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"account", - "label": __("Bank Account"), - "fieldtype": "Link", - "options": "Account", - "default": frappe.defaults.get_user_default("Company")? - locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "", - "reqd": 1, - "get_query": function() { - var company = frappe.query_report.get_filter_value('company') + fieldname: "account", + label: __("Bank Account"), + fieldtype: "Link", + options: "Account", + default: frappe.defaults.get_user_default("Company") + ? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"] + : "", + reqd: 1, + get_query: function () { + var company = frappe.query_report.get_filter_value("company"); return { - "query": "erpnext.controllers.queries.get_account_list", - "filters": [ - ['Account', 'account_type', 'in', 'Bank, Cash'], - ['Account', 'is_group', '=', 0], - ['Account', 'disabled', '=', 0], - ['Account', 'company', '=', company], - ] - } - } + query: "erpnext.controllers.queries.get_account_list", + filters: [ + ["Account", "account_type", "in", "Bank, Cash"], + ["Account", "is_group", "=", 0], + ["Account", "disabled", "=", 0], + ["Account", "company", "=", company], + ], + }; + }, }, { - "fieldname":"report_date", - "label": __("Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1 + fieldname: "report_date", + label: __("Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"include_pos_transactions", - "label": __("Include POS Transactions"), - "fieldtype": "Check" + fieldname: "include_pos_transactions", + label: __("Include POS Transactions"), + fieldtype: "Check", }, - ] -} + ], +}; diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index fbc1a69ddc5..7c2389d8dd5 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -35,10 +35,7 @@ def execute(filters=None): amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters) bank_bal = ( - flt(balance_as_per_system) - - flt(total_debit) - + flt(total_credit) - + amounts_not_reflected_in_system + flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system ) data += [ @@ -220,7 +217,7 @@ def get_loan_entries(filters): ) if doctype == "Loan Repayment" and frappe.db.has_column("Loan Repayment", "repay_from_salary"): - query = query.where((loan_doc.repay_from_salary == 0)) + query = query.where(loan_doc.repay_from_salary == 0) entries = query.run(as_dict=1) loan_docs.extend(entries) @@ -282,7 +279,7 @@ def get_loan_amount(filters): ) if doctype == "Loan Repayment" and frappe.db.has_column("Loan Repayment", "repay_from_salary"): - query = query.where((loan_doc.repay_from_salary == 0)) + query = query.where(loan_doc.repay_from_salary == 0) amount = query.run()[0][0] total_amount += flt(amount) diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.js b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.js index e1fccb6e720..65c8e822227 100644 --- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.js +++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.js @@ -2,28 +2,28 @@ // For license information, please see license.txt /* eslint-disable */ -frappe.query_reports['Billed Items To Be Received'] = { - 'filters': [ +frappe.query_reports["Billed Items To Be Received"] = { + filters: [ { - 'label': __('Company'), - 'fieldname': 'company', - 'fieldtype': 'Link', - 'options': 'Company', - 'reqd': 1, - 'default': frappe.defaults.get_default('Company') + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_default("Company"), }, { - 'label': __('As on Date'), - 'fieldname': 'posting_date', - 'fieldtype': 'Date', - 'reqd': 1, - 'default': get_today() + label: __("As on Date"), + fieldname: "posting_date", + fieldtype: "Date", + reqd: 1, + default: get_today(), }, { - 'label': __('Purchase Invoice'), - 'fieldname': 'purchase_invoice', - 'fieldtype': 'Link', - 'options': 'Purchase Invoice' - } - ] + label: __("Purchase Invoice"), + fieldname: "purchase_invoice", + fieldtype: "Link", + options: "Purchase Invoice", + }, + ], }; diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py index 62bee82590b..f6efc8a685c 100644 --- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py +++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py @@ -30,9 +30,7 @@ def get_report_filters(report_filters): ] if report_filters.get("purchase_invoice"): - filters.append( - ["Purchase Invoice", "per_received", "in", [report_filters.get("purchase_invoice")]] - ) + filters.append(["Purchase Invoice", "per_received", "in", [report_filters.get("purchase_invoice")]]) return filters @@ -40,10 +38,10 @@ def get_report_filters(report_filters): def get_report_fields(): fields = [] for p_field in ["name", "supplier", "company", "posting_date", "currency"]: - fields.append("`tabPurchase Invoice`.`{}`".format(p_field)) + fields.append(f"`tabPurchase Invoice`.`{p_field}`") for c_field in ["item_code", "item_name", "uom", "qty", "received_qty", "rate", "amount"]: - fields.append("`tabPurchase Invoice Item`.`{}`".format(c_field)) + fields.append(f"`tabPurchase Invoice Item`.`{c_field}`") return fields diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 5955c2e0fc9..156ae8f2f2a 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -2,14 +2,49 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Budget Variance Report"] = { - "filters": [ + filters: get_filters(), + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes(__("variance"))) { + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + }, +}; +function get_filters() { + function get_dimensions() { + let result = []; + frappe.call({ + method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + args: { + with_cost_center_and_project: true, + }, + async: false, + callback: function (r) { + if (!r.exc) { + result = r.message[0].map((elem) => elem.document_type); + } + }, + }); + return result; + } + + let budget_against_options = get_dimensions(); + + let filters = [ { fieldname: "from_fiscal_year", label: __("From Fiscal Year"), fieldtype: "Link", options: "Fiscal Year", default: frappe.sys_defaults.fiscal_year, - reqd: 1 + reqd: 1, }, { fieldname: "to_fiscal_year", @@ -17,20 +52,20 @@ frappe.query_reports["Budget Variance Report"] = { fieldtype: "Link", options: "Fiscal Year", default: frappe.sys_defaults.fiscal_year, - reqd: 1 + reqd: 1, }, { fieldname: "period", label: __("Period"), fieldtype: "Select", options: [ - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Half-Yearly", "label": __("Half-Yearly") }, - { "value": "Yearly", "label": __("Yearly") } + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, ], default: "Yearly", - reqd: 1 + reqd: 1, }, { fieldname: "company", @@ -38,57 +73,40 @@ frappe.query_reports["Budget Variance Report"] = { fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { fieldname: "budget_against", label: __("Budget Against"), fieldtype: "Select", - options: ["Cost Center", "Project"], + options: budget_against_options, default: "Cost Center", reqd: 1, - on_change: function() { + on_change: function () { frappe.query_report.set_filter_value("budget_against_filter", []); frappe.query_report.refresh(); - } + }, }, { - fieldname:"budget_against_filter", - label: __('Dimension Filter'), + fieldname: "budget_against_filter", + label: __("Dimension Filter"), fieldtype: "MultiSelectList", - get_data: function(txt) { + get_data: function (txt) { if (!frappe.query_report.filters) return; - let budget_against = frappe.query_report.get_filter_value('budget_against'); + let budget_against = frappe.query_report.get_filter_value("budget_against"); if (!budget_against) return; return frappe.db.get_link_options(budget_against, txt); - } + }, }, { - fieldname:"show_cumulative", + fieldname: "show_cumulative", label: __("Show Cumulative Amount"), fieldtype: "Check", default: 0, }, - ], - "formatter": function (value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data); + ]; - if (column.fieldname.includes(__("variance"))) { - - if (data[column.fieldname] < 0) { - value = "" + value + ""; - } - else if (data[column.fieldname] > 0) { - value = "" + value + ""; - } - } - - return value; - } + return filters; } - -erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]); -}); diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 96cfab9f11f..e540aa9993c 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -112,7 +112,9 @@ def get_columns(filters): ]: if group_months: label = label % ( - formatdate(from_date, format_string="MMM") + "-" + formatdate(to_date, format_string="MMM") + formatdate(from_date, format_string="MMM") + + "-" + + formatdate(to_date, format_string="MMM") ) else: label = label % formatdate(from_date, format_string="MMM") @@ -147,9 +149,7 @@ def get_cost_centers(filters): where company = %s {order_by} - """.format( - tab=filters.get("budget_against"), order_by=order_by - ), + """.format(tab=filters.get("budget_against"), order_by=order_by), filters.get("company"), ) else: @@ -159,9 +159,7 @@ def get_cost_centers(filters): name from `tab{tab}` - """.format( - tab=filters.get("budget_against") - ) + """.format(tab=filters.get("budget_against")) ) # nosec @@ -170,12 +168,12 @@ def get_dimension_target_details(filters): budget_against = frappe.scrub(filters.get("budget_against")) cond = "" if filters.get("budget_against_filter"): - cond += """ and b.{budget_against} in (%s)""".format(budget_against=budget_against) % ", ".join( + cond += f""" and b.{budget_against} in (%s)""" % ", ".join( ["%s"] * len(filters.get("budget_against_filter")) ) return frappe.db.sql( - """ + f""" select b.{budget_against} as budget_against, b.monthly_distribution, @@ -194,10 +192,7 @@ def get_dimension_target_details(filters): {cond} order by b.fiscal_year - """.format( - budget_against=budget_against, - cond=cond, - ), + """, tuple( [ filters.from_fiscal_year, @@ -244,15 +239,13 @@ def get_actual_details(name, filters): if filters.get("budget_against") == "Cost Center": cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) - cond = """ - and lft >= "{lft}" - and rgt <= "{rgt}" - """.format( - lft=cc_lft, rgt=cc_rgt - ) + cond = f""" + and lft >= "{cc_lft}" + and rgt <= "{cc_rgt}" + """ ac_details = frappe.db.sql( - """ + f""" select gl.account, gl.debit, @@ -275,7 +268,7 @@ def get_actual_details(name, filters): select name from - `tab{tab}` + `tab{filters.budget_against}` where name = gl.{budget_against} {cond} @@ -283,9 +276,7 @@ def get_actual_details(name, filters): group by gl.name order by gl.fiscal_year - """.format( - tab=filters.budget_against, budget_against=budget_against, cond=cond - ), + """, (filters.from_fiscal_year, filters.to_fiscal_year, name), as_dict=1, ) @@ -314,7 +305,9 @@ def get_dimension_account_month_map(filters): tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] month_percentage = ( - tdd.get(ccd.monthly_distribution, {}).get(month, 0) if ccd.monthly_distribution else 100.0 / 12 + tdd.get(ccd.monthly_distribution, {}).get(month, 0) + if ccd.monthly_distribution + else 100.0 / 12 ) tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 @@ -327,7 +320,6 @@ def get_dimension_account_month_map(filters): def get_fiscal_years(filters): - fiscal_year = frappe.db.sql( """ select @@ -344,7 +336,6 @@ def get_fiscal_years(filters): def get_chart_data(filters, columns, data): - if not data: return None @@ -360,7 +351,9 @@ def get_chart_data(filters, columns, data): else: if group_months: label = ( - formatdate(from_date, format_string="MMM") + "-" + formatdate(to_date, format_string="MMM") + formatdate(from_date, format_string="MMM") + + "-" + + formatdate(to_date, format_string="MMM") ) labels.append(label) else: diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js index b9c62b52861..c3560c8d3f5 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -1,11 +1,10 @@ // Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.require("assets/erpnext/js/financial_statements.js", function() { - frappe.query_reports["Cash Flow"] = $.extend({}, - erpnext.financial_statements); +frappe.require("assets/erpnext/js/financial_statements.js", function () { + frappe.query_reports["Cash Flow"] = $.extend({}, erpnext.financial_statements); - erpnext.utils.add_dimensions('Cash Flow', 10); + erpnext.utils.add_dimensions("Cash Flow", 10); // The last item in the array is the definition for Presentation Currency // filter. It won't be used in cash flow for now so we pop it. Please take @@ -13,12 +12,10 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Cash Flow"]["filters"].splice(8, 1); - frappe.query_reports["Cash Flow"]["filters"].push( - { - "fieldname": "include_default_book_entries", - "label": __("Include Default FB Entries"), - "fieldtype": "Check", - "default": 1 - } - ); + frappe.query_reports["Cash Flow"]["filters"].push({ + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, + }); }); diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index af706811ab2..87bde218dae 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -114,9 +114,7 @@ def execute(filters=None): add_total_row_account( data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters ) - columns = get_columns( - filters.periodicity, period_list, filters.accumulated_values, filters.company - ) + columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) chart = get_chart_data(columns, data) @@ -183,8 +181,8 @@ def get_account_type_based_gl_data(company, filters=None): if filters.include_default_book_entries: company_fb = frappe.db.get_value("Company", company, "default_finance_book") - cond = """ AND (finance_book in (%s, %s, '') OR finance_book IS NULL) - """ % ( + cond = """ AND (finance_book in ({}, {}, '') OR finance_book IS NULL) + """.format( frappe.db.escape(filters.finance_book), frappe.db.escape(company_fb), ) @@ -198,15 +196,13 @@ def get_account_type_based_gl_data(company, filters=None): cond += " and cost_center in %(cost_center)s" gl_sum = frappe.db.sql_list( - """ + f""" select sum(credit) - sum(debit) from `tabGL Entry` where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s and voucher_type != 'Period Closing Voucher' and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond} - """.format( - cond=cond - ), + """, filters, ) @@ -224,9 +220,7 @@ def get_start_date(period, accumulated_values, company): return start_date -def add_total_row_account( - out, data, label, period_list, currency, summary_data, filters, consolidated=False -): +def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False): total_row = { "account_name": "'" + _("{0}").format(label) + "'", "account": "'" + _("{0}").format(label) + "'", @@ -258,9 +252,7 @@ def get_report_summary(summary_data, currency): report_summary = [] for label, value in summary_data.items(): - report_summary.append( - {"value": value, "label": label, "datatype": "Currency", "currency": currency} - ) + report_summary.append({"value": value, "label": label, "datatype": "Currency", "currency": currency}) return report_summary @@ -273,7 +265,7 @@ def get_chart_data(columns, data): "values": [account.get(d.get("fieldname")) for d in columns[2:]], } for account in data - if account.get("parent_account") == None and account.get("currency") + if account.get("parent_account") is None and account.get("currency") ] datasets = datasets[:-1] diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py index 24e585e07f6..ea79917045d 100644 --- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py +++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py @@ -131,7 +131,12 @@ def setup_mappers(mappers): account_types_labels = sorted( set( - (d["label"], d["is_working_capital"], d["is_income_tax_liability"], d["is_income_tax_expense"]) + ( + d["label"], + d["is_working_capital"], + d["is_income_tax_liability"], + d["is_income_tax_expense"], + ) for d in account_types ), key=lambda x: x[1], @@ -319,14 +324,10 @@ def add_data_for_operating_activities( data.append(interest_paid) section_data.append(interest_paid) - _add_total_row_account( - data, section_data, mapper["section_footer"], period_list, company_currency - ) + _add_total_row_account(data, section_data, mapper["section_footer"], period_list, company_currency) -def calculate_adjustment( - filters, non_expense_mapper, expense_mapper, use_accumulated_values, period_list -): +def calculate_adjustment(filters, non_expense_mapper, expense_mapper, use_accumulated_values, period_list): liability_accounts = [d["names"] for d in non_expense_mapper] expense_accounts = [d["names"] for d in expense_mapper] @@ -388,9 +389,7 @@ def add_data_for_other_activities( data.append(account_data) section_data.append(account_data) - _add_total_row_account( - data, section_data, mapper["section_footer"], period_list, company_currency - ) + _add_total_row_account(data, section_data, mapper["section_footer"], period_list, company_currency) def compute_data(filters, company_currency, profit_data, period_list, light_mappers, full_mapper): @@ -465,22 +464,16 @@ def execute(filters=None): company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") - data = compute_data( - filters, company_currency, net_profit_loss, period_list, mappers, cash_flow_accounts - ) + data = compute_data(filters, company_currency, net_profit_loss, period_list, mappers, cash_flow_accounts) _add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency) - columns = get_columns( - filters.periodicity, period_list, filters.accumulated_values, filters.company - ) + columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) return columns, data -def _get_account_type_based_data( - filters, account_names, period_list, accumulated_values, opening_balances=0 -): - if not account_names or not account_names[0] or not type(account_names[0]) == str: +def _get_account_type_based_data(filters, account_names, period_list, accumulated_values, opening_balances=0): + if not account_names or not account_names[0] or not isinstance(account_names[0], str): # only proceed if account_names is a list of account names return {} 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 b7d25c41982..f700cb7759d 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -2,125 +2,153 @@ // For license information, please see license.txt /* eslint-disable */ -frappe.require("assets/erpnext/js/financial_statements.js", function() { +frappe.require("assets/erpnext/js/financial_statements.js", function () { frappe.query_reports["Consolidated Financial Statement"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"filter_based_on", - "label": __("Filter Based On"), - "fieldtype": "Select", - "options": ["Fiscal Year", "Date Range"], - "default": ["Fiscal Year"], - "reqd": 1, - on_change: function() { - let filter_based_on = frappe.query_report.get_filter_value('filter_based_on'); - frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range'); - frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range'); - frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year'); - frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year'); + fieldname: "filter_based_on", + label: __("Filter Based On"), + fieldtype: "Select", + options: ["Fiscal Year", "Date Range"], + default: ["Fiscal Year"], + reqd: 1, + on_change: function () { + let filter_based_on = frappe.query_report.get_filter_value("filter_based_on"); + frappe.query_report.toggle_filter_display( + "from_fiscal_year", + filter_based_on === "Date Range" + ); + frappe.query_report.toggle_filter_display( + "to_fiscal_year", + filter_based_on === "Date Range" + ); + frappe.query_report.toggle_filter_display( + "period_start_date", + filter_based_on === "Fiscal Year" + ); + frappe.query_report.toggle_filter_display( + "period_end_date", + filter_based_on === "Fiscal Year" + ); frappe.query_report.refresh(); - } + }, }, { - "fieldname":"period_start_date", - "label": __("Start Date"), - "fieldtype": "Date", - "hidden": 1, - "reqd": 1 + fieldname: "period_start_date", + label: __("Start Date"), + fieldtype: "Date", + hidden: 1, + reqd: 1, }, { - "fieldname":"period_end_date", - "label": __("End Date"), - "fieldtype": "Date", - "hidden": 1, - "reqd": 1 + fieldname: "period_end_date", + label: __("End Date"), + fieldtype: "Date", + hidden: 1, + reqd: 1, }, { - "fieldname":"from_fiscal_year", - "label": __("Start Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1, + fieldname: "from_fiscal_year", + label: __("Start Year"), + fieldtype: "Link", + options: "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) { - let year_start_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), "year_start_date"); - frappe.query_report.set_filter_value({ - period_start_date: year_start_date - }); - }); - } + frappe.model.with_doc( + "Fiscal Year", + frappe.query_report.get_filter_value("from_fiscal_year"), + function (r) { + let year_start_date = frappe.model.get_value( + "Fiscal Year", + frappe.query_report.get_filter_value("from_fiscal_year"), + "year_start_date" + ); + frappe.query_report.set_filter_value({ + period_start_date: year_start_date, + }); + } + ); + }, }, { - "fieldname":"to_fiscal_year", - "label": __("End Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1, + fieldname: "to_fiscal_year", + label: __("End Year"), + fieldtype: "Link", + options: "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) { - let year_end_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), "year_end_date"); - frappe.query_report.set_filter_value({ - period_end_date: year_end_date - }); - }); - } + frappe.model.with_doc( + "Fiscal Year", + frappe.query_report.get_filter_value("to_fiscal_year"), + function (r) { + let year_end_date = frappe.model.get_value( + "Fiscal Year", + frappe.query_report.get_filter_value("to_fiscal_year"), + "year_end_date" + ); + frappe.query_report.set_filter_value({ + period_end_date: year_end_date, + }); + } + ); + }, }, { - "fieldname":"finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book" + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname":"report", - "label": __("Report"), - "fieldtype": "Select", - "options": ["Profit and Loss Statement", "Balance Sheet", "Cash Flow"], - "default": "Balance Sheet", - "reqd": 1 + fieldname: "report", + label: __("Report"), + fieldtype: "Select", + options: ["Profit and Loss Statement", "Balance Sheet", "Cash Flow"], + default: "Balance Sheet", + reqd: 1, }, { - "fieldname": "presentation_currency", - "label": __("Currency"), - "fieldtype": "Select", - "options": erpnext.get_presentation_currency_list(), - "default": frappe.defaults.get_user_default("Currency") + fieldname: "presentation_currency", + label: __("Currency"), + fieldtype: "Select", + options: erpnext.get_presentation_currency_list(), + default: frappe.defaults.get_user_default("Currency"), }, { - "fieldname":"accumulated_in_group_company", - "label": __("Accumulated Values in Group Company"), - "fieldtype": "Check", - "default": 0 + fieldname: "accumulated_in_group_company", + label: __("Accumulated Values in Group Company"), + fieldtype: "Check", + default: 0, }, { - "fieldname": "include_default_book_entries", - "label": __("Include Default FB Entries"), - "fieldtype": "Check", - "default": 1 + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, }, { - "fieldname": "show_zero_values", - "label": __("Show zero values"), - "fieldtype": "Check" - } + fieldname: "show_zero_values", + label: __("Show zero values"), + fieldtype: "Check", + }, ], - "formatter": function(value, row, column, data, default_formatter) { - if (data && column.fieldname=="account") { + formatter: function (value, row, column, data, default_formatter) { + if (data && column.fieldname == "account") { value = data.account_name || value; column.link_onclick = - "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; + "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; column.is_tree = true; } @@ -129,7 +157,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { } value = default_formatter(value, row, column, data); - if (!data.parent_account) { + if (data && !data.parent_account) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); @@ -138,16 +166,16 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { } return value; }, - onload: function() { + onload: function () { let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); - frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); frappe.query_report.set_filter_value({ period_start_date: fy.year_start_date, - period_end_date: fy.year_end_date + period_end_date: fy.year_end_date, }); }); - } - } + }, + }; }); diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index fe4b6c71ebc..794721b6c92 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -55,9 +55,7 @@ def execute(filters=None): fiscal_year, companies, columns, filters ) elif filters.get("report") == "Profit and Loss Statement": - data, message, chart, report_summary = get_profit_loss_data( - fiscal_year, companies, columns, filters - ) + data, message, chart, report_summary = get_profit_loss_data(fiscal_year, companies, columns, filters) else: if cint(frappe.db.get_single_value("Accounts Settings", "use_custom_cash_flow")): from erpnext.accounts.report.cash_flow.custom_cash_flow import execute as execute_custom @@ -86,9 +84,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters): asset, liability, equity, companies, filters.get("company"), company_currency, True ) - message, opening_balance = prepare_companywise_opening_balance( - asset, liability, equity, companies - ) + message, opening_balance = prepare_companywise_opening_balance(asset, liability, equity, companies) if opening_balance: unclosed = { @@ -197,9 +193,7 @@ def get_income_expense_data(companies, fiscal_year, filters): expense = get_data(companies, "Expense", "Debit", fiscal_year, filters, True) - net_profit_loss = get_net_profit_loss( - income, expense, companies, filters.company, company_currency, True - ) + net_profit_loss = get_net_profit_loss(income, expense, companies, filters.company, company_currency, True) return income, expense, net_profit_loss @@ -328,9 +322,7 @@ def get_columns(companies, filters): return columns -def get_data( - companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False -): +def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False): accounts, accounts_by_name, parent_children_map = get_account_heads(root_type, companies, filters) if not accounts: @@ -354,7 +346,6 @@ def get_data( root_type, as_dict=1, ): - set_gl_entries_by_account( start_date, end_date, @@ -371,9 +362,7 @@ def get_data( calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year) accumulate_values_into_parents(accounts, accounts_by_name, companies) - out = prepare_data( - accounts, start_date, end_date, balance_must_be, companies, company_currency, filters - ) + out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters) out = filter_out_zero_value_rows( out, parent_children_map, show_zero_values=filters.get("show_zero_values") @@ -393,9 +382,7 @@ def get_company_currency(filters=None): def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year): start_date = ( - fiscal_year.year_start_date - if filters.filter_based_on == "Fiscal Year" - else filters.period_start_date + fiscal_year.year_start_date if filters.filter_based_on == "Fiscal Year" else filters.period_start_date ) for entries in gl_entries_by_account.values(): @@ -427,8 +414,12 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters and parent_company_currency != child_company_currency and filters.get("accumulated_in_group_company") ): - debit = convert(debit, parent_company_currency, child_company_currency, filters.end_date) - credit = convert(credit, parent_company_currency, child_company_currency, filters.end_date) + debit = convert( + debit, parent_company_currency, child_company_currency, filters.end_date + ) + credit = convert( + credit, parent_company_currency, child_company_currency, filters.end_date + ) d[company] = d.get(company, 0.0) + flt(debit) - flt(credit) @@ -512,10 +503,8 @@ def get_subsidiary_companies(company): lft, rgt = frappe.get_cached_value("Company", company, ["lft", "rgt"]) return frappe.db.sql_list( - """select name from `tabCompany` - where lft >= {0} and rgt <= {1} order by lft, rgt""".format( - lft, rgt - ) + f"""select name from `tabCompany` + where lft >= {lft} and rgt <= {rgt} order by lft, rgt""" ) @@ -552,9 +541,7 @@ def get_accounts(root_type, companies): return accounts -def prepare_data( - accounts, start_date, end_date, balance_must_be, companies, company_currency, filters -): +def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters): data = [] for d in accounts: @@ -564,9 +551,7 @@ def prepare_data( row = frappe._dict( { "account_name": ( - "%s - %s" % (_(d.account_number), _(d.account_name)) - if d.account_number - else _(d.account_name) + f"{_(d.account_number)} - {_(d.account_name)}" if d.account_number else _(d.account_name) ), "account": _(d.name), "parent_account": _(d.parent_account), @@ -614,9 +599,7 @@ def set_gl_entries_by_account( ): """Returns a dict like { "account": [gl entries], ... }""" - company_lft, company_rgt = frappe.get_cached_value( - "Company", filters.get("company"), ["lft", "rgt"] - ) + company_lft, company_rgt = frappe.get_cached_value("Company", filters.get("company"), ["lft", "rgt"]) companies = frappe.db.sql( """ select name, default_currency from `tabCompany` @@ -744,7 +727,7 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters, d): additional_conditions = [] if ignore_closing_entries: - additional_conditions.append((gle.voucher_type != "Period Closing Voucher")) + additional_conditions.append(gle.voucher_type != "Period Closing Voucher") if from_date: additional_conditions.append(gle.posting_date >= from_date) @@ -807,7 +790,7 @@ def filter_accounts(accounts, depth=10): def add_to_list(parent, level): if level < depth: children = parent_children_map.get(parent) or [] - sort_accounts(children, is_root=True if parent == None else False) + sort_accounts(children, is_root=True if parent is None else False) for child in children: child.indent = level diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js index a1236316638..dec2ebd2520 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js @@ -3,95 +3,95 @@ /* eslint-disable */ frappe.query_reports["Customer Ledger Summary"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + 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), - "reqd": 1, - "width": "60px" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, + width: "60px", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1, - "width": "60px" + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + width: "60px", }, { - "fieldname":"finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book" + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname":"party", - "label": __("Customer"), - "fieldtype": "Link", - "options": "Customer", + fieldname: "party", + label: __("Customer"), + fieldtype: "Link", + options: "Customer", on_change: () => { - var party = frappe.query_report.get_filter_value('party'); + var party = frappe.query_report.get_filter_value("party"); if (party) { - frappe.db.get_value('Customer', party, ["tax_id", "customer_name"], function(value) { - frappe.query_report.set_filter_value('tax_id', value["tax_id"]); - frappe.query_report.set_filter_value('customer_name', value["customer_name"]); + frappe.db.get_value("Customer", party, ["tax_id", "customer_name"], function (value) { + frappe.query_report.set_filter_value("tax_id", value["tax_id"]); + frappe.query_report.set_filter_value("customer_name", value["customer_name"]); }); } else { - frappe.query_report.set_filter_value('tax_id', ""); - frappe.query_report.set_filter_value('customer_name', ""); + frappe.query_report.set_filter_value("tax_id", ""); + frappe.query_report.set_filter_value("customer_name", ""); } - } + }, }, { - "fieldname":"customer_group", - "label": __("Customer Group"), - "fieldtype": "Link", - "options": "Customer Group" + fieldname: "customer_group", + label: __("Customer Group"), + fieldtype: "Link", + options: "Customer Group", }, { - "fieldname":"payment_terms_template", - "label": __("Payment Terms Template"), - "fieldtype": "Link", - "options": "Payment Terms Template" + fieldname: "payment_terms_template", + label: __("Payment Terms Template"), + fieldtype: "Link", + options: "Payment Terms Template", }, { - "fieldname":"territory", - "label": __("Territory"), - "fieldtype": "Link", - "options": "Territory" + fieldname: "territory", + label: __("Territory"), + fieldtype: "Link", + options: "Territory", }, { - "fieldname":"sales_partner", - "label": __("Sales Partner"), - "fieldtype": "Link", - "options": "Sales Partner" + fieldname: "sales_partner", + label: __("Sales Partner"), + fieldtype: "Link", + options: "Sales Partner", }, { - "fieldname":"sales_person", - "label": __("Sales Person"), - "fieldtype": "Link", - "options": "Sales Person" + fieldname: "sales_person", + label: __("Sales Person"), + fieldtype: "Link", + options: "Sales Person", }, { - "fieldname":"tax_id", - "label": __("Tax Id"), - "fieldtype": "Data", - "hidden": 1 + fieldname: "tax_id", + label: __("Tax Id"), + fieldtype: "Data", + hidden: 1, }, { - "fieldname":"customer_name", - "label": __("Customer Name"), - "fieldtype": "Data", - "hidden": 1 - } - ] + fieldname: "customer_name", + label: __("Customer Name"), + fieldtype: "Data", + hidden: 1, + }, + ], }; diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index d870a1aaf83..108f9f617df 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -3,11 +3,11 @@ import frappe -from frappe import _, scrub +from frappe import _, qb, scrub from frappe.utils import getdate, nowdate -class PartyLedgerSummaryReport(object): +class PartyLedgerSummaryReport: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.filters.from_date = getdate(self.filters.from_date or nowdate()) @@ -21,9 +21,7 @@ class PartyLedgerSummaryReport(object): frappe.throw(_("From Date must be before To Date")) self.filters.party_type = args.get("party_type") - self.party_naming_by = frappe.db.get_value( - args.get("naming_by")[0], None, args.get("naming_by")[1] - ) + self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) self.get_gl_entries() self.get_additional_columns() @@ -38,7 +36,6 @@ class PartyLedgerSummaryReport(object): """ Additional Columns for 'User Permission' based access control """ - from frappe import qb if self.filters.party_type == "Customer": self.territories = frappe._dict({}) @@ -50,7 +47,7 @@ class PartyLedgerSummaryReport(object): .select( customer.name, customer.territory, customer.customer_group, customer.default_sales_partner ) - .where((customer.disabled == 0)) + .where(customer.disabled == 0) .run(as_dict=True) ) @@ -63,7 +60,7 @@ class PartyLedgerSummaryReport(object): result = ( frappe.qb.from_(supplier) .select(supplier.name, supplier.supplier_group) - .where((supplier.disabled == 0)) + .where(supplier.disabled == 0) .run(as_dict=True) ) @@ -185,9 +182,7 @@ class PartyLedgerSummaryReport(object): return columns def get_data(self): - company_currency = frappe.get_cached_value( - "Company", self.filters.get("company"), "default_currency" - ) + company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency") invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit" @@ -261,7 +256,7 @@ class PartyLedgerSummaryReport(object): join = "left join `tabSupplier` p on gle.party = p.name" self.gl_entries = frappe.db.sql( - """ + f""" select gle.posting_date, gle.party, gle.voucher_type, gle.voucher_no, gle.against_voucher_type, gle.against_voucher, gle.debit, gle.credit, gle.is_opening {join_field} @@ -271,9 +266,7 @@ class PartyLedgerSummaryReport(object): gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' and gle.posting_date <= %(to_date)s {conditions} order by gle.posting_date - """.format( - join=join, join_field=join_field, conditions=conditions - ), + """, self.filters, as_dict=True, ) @@ -297,22 +290,18 @@ class PartyLedgerSummaryReport(object): ) conditions.append( - """party in (select name from tabCustomer - where exists(select name from `tabCustomer Group` where lft >= {0} and rgt <= {1} - and name=tabCustomer.customer_group))""".format( - lft, rgt - ) + f"""party in (select name from tabCustomer + where exists(select name from `tabCustomer Group` where lft >= {lft} and rgt <= {rgt} + and name=tabCustomer.customer_group))""" ) if self.filters.get("territory"): lft, rgt = frappe.db.get_value("Territory", self.filters.get("territory"), ["lft", "rgt"]) conditions.append( - """party in (select name from tabCustomer - where exists(select name from `tabTerritory` where lft >= {0} and rgt <= {1} - and name=tabCustomer.territory))""".format( - lft, rgt - ) + f"""party in (select name from tabCustomer + where exists(select name from `tabTerritory` where lft >= {lft} and rgt <= {rgt} + and name=tabCustomer.territory))""" ) if self.filters.get("payment_terms_template"): @@ -332,12 +321,10 @@ class PartyLedgerSummaryReport(object): conditions.append( """exists(select name from `tabSales Team` steam where - steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1}) + steam.sales_person in (select name from `tabSales Person` where lft >= {} and rgt <= {}) and ((steam.parent = voucher_no and steam.parenttype = voucher_type) or (steam.parent = against_voucher and steam.parenttype = against_voucher_type) - or (steam.parent = party and steam.parenttype = 'Customer')))""".format( - lft, rgt - ) + or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt) ) if self.filters.party_type == "Supplier": @@ -365,15 +352,35 @@ class PartyLedgerSummaryReport(object): def get_party_adjustment_amounts(self): conditions = self.prepare_conditions() - income_or_expense = ( - "Expense Account" if self.filters.party_type == "Customer" else "Income Account" + account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account" + income_or_expense_accounts = frappe.db.get_all( + "Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name" ) invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit" round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account") + gl = qb.DocType("GL Entry") + if not income_or_expense_accounts: + # prevent empty 'in' condition + income_or_expense_accounts.append("") + else: + # escape '%' in account name + # ignoring frappe.db.escape as it replaces single quotes with double quotes + income_or_expense_accounts = [x.replace("%", "%%") for x in income_or_expense_accounts] + + accounts_query = ( + qb.from_(gl) + .select(gl.voucher_type, gl.voucher_no) + .where( + (gl.account.isin(income_or_expense_accounts)) + & (gl.posting_date.gte(self.filters.from_date)) + & (gl.posting_date.lte(self.filters.to_date)) + ) + ) + gl_entries = frappe.db.sql( - """ + f""" select posting_date, account, party, voucher_type, voucher_no, debit, credit from @@ -381,17 +388,13 @@ class PartyLedgerSummaryReport(object): where docstatus < 2 and is_cancelled = 0 and (voucher_type, voucher_no) in ( - select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc - where acc.name = gle.account and acc.account_type = '{income_or_expense}' - and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 + {accounts_query} ) and (voucher_type, voucher_no) in ( select voucher_type, voucher_no from `tabGL Entry` gle where gle.party_type=%(party_type)s and ifnull(party, '') != '' and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions} ) - """.format( - conditions=conditions, income_or_expense=income_or_expense - ), + """, self.filters, as_dict=True, ) @@ -414,7 +417,7 @@ class PartyLedgerSummaryReport(object): elif gle.party: parties.setdefault(gle.party, 0) parties[gle.party] += gle.get(reverse_dr_or_cr) - gle.get(invoice_dr_or_cr) - elif frappe.get_cached_value("Account", gle.account, "account_type") == income_or_expense: + elif frappe.get_cached_value("Account", gle.account, "account_type") == account_type: accounts.setdefault(gle.account, 0) accounts[gle.account] += gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr) else: @@ -422,14 +425,14 @@ class PartyLedgerSummaryReport(object): if parties and accounts: if len(parties) == 1: - party = list(parties.keys())[0] + party = next(iter(parties.keys())) for account, amount in accounts.items(): self.party_adjustment_accounts.add(account) self.party_adjustment_details.setdefault(party, {}) self.party_adjustment_details[party].setdefault(account, 0) self.party_adjustment_details[party][account] += amount elif len(accounts) == 1 and not has_irrelevant_entry: - account = list(accounts.keys())[0] + account = next(iter(accounts.keys())) self.party_adjustment_accounts.add(account) for party, amount in parties.items(): self.party_adjustment_details.setdefault(party, {}) 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 96e0c844ca5..63f1062fd33 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 @@ -5,110 +5,118 @@ function get_filters() { let filters = [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"filter_based_on", - "label": __("Filter Based On"), - "fieldtype": "Select", - "options": ["Fiscal Year", "Date Range"], - "default": ["Fiscal Year"], - "reqd": 1, - on_change: function() { - let filter_based_on = frappe.query_report.get_filter_value('filter_based_on'); - frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range'); - frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range'); - frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year'); - frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year'); + fieldname: "filter_based_on", + label: __("Filter Based On"), + fieldtype: "Select", + options: ["Fiscal Year", "Date Range"], + default: ["Fiscal Year"], + reqd: 1, + on_change: function () { + let filter_based_on = frappe.query_report.get_filter_value("filter_based_on"); + frappe.query_report.toggle_filter_display( + "from_fiscal_year", + filter_based_on === "Date Range" + ); + frappe.query_report.toggle_filter_display("to_fiscal_year", filter_based_on === "Date Range"); + frappe.query_report.toggle_filter_display( + "period_start_date", + filter_based_on === "Fiscal Year" + ); + frappe.query_report.toggle_filter_display( + "period_end_date", + filter_based_on === "Fiscal Year" + ); frappe.query_report.refresh(); - } + }, }, { - "fieldname":"period_start_date", - "label": __("Start Date"), - "fieldtype": "Date", - "hidden": 1, - "reqd": 1 + fieldname: "period_start_date", + label: __("Start Date"), + fieldtype: "Date", + hidden: 1, + reqd: 1, }, { - "fieldname":"period_end_date", - "label": __("End Date"), - "fieldtype": "Date", - "hidden": 1, - "reqd": 1 + fieldname: "period_end_date", + label: __("End Date"), + fieldtype: "Date", + hidden: 1, + reqd: 1, }, { - "fieldname":"from_fiscal_year", - "label": __("Start Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1 + fieldname: "from_fiscal_year", + label: __("Start Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, }, { - "fieldname":"to_fiscal_year", - "label": __("End Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1 + fieldname: "to_fiscal_year", + label: __("End Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, }, { - "fieldname": "periodicity", - "label": __("Periodicity"), - "fieldtype": "Select", - "options": [ - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Half-Yearly", "label": __("Half-Yearly") }, - { "value": "Yearly", "label": __("Yearly") } + fieldname: "periodicity", + label: __("Periodicity"), + fieldtype: "Select", + options: [ + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, ], - "default": "Monthly", - "reqd": 1 + default: "Monthly", + reqd: 1, }, { - "fieldname": "type", - "label": __("Invoice Type"), - "fieldtype": "Select", - "options": [ - { "value": "Revenue", "label": __("Revenue") }, - { "value": "Expense", "label": __("Expense") } + fieldname: "type", + label: __("Invoice Type"), + fieldtype: "Select", + options: [ + { value: "Revenue", label: __("Revenue") }, + { value: "Expense", label: __("Expense") }, ], - "default": "Revenue", - "reqd": 1 + default: "Revenue", + reqd: 1, }, { - "fieldname" : "with_upcoming_postings", - "label": __("Show with upcoming revenue/expense"), - "fieldtype": "Check", - "default": 1 - } - ] + fieldname: "with_upcoming_postings", + label: __("Show with upcoming revenue/expense"), + fieldtype: "Check", + default: 1, + }, + ]; return filters; } frappe.query_reports["Deferred Revenue and Expense"] = { - "filters": get_filters(), - "formatter": function(value, row, column, data, default_formatter){ + filters: get_filters(), + formatter: function (value, row, column, data, default_formatter) { return default_formatter(value, row, column, data); }, - onload: function(report){ + onload: function (report) { let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); - frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); frappe.query_report.set_filter_value({ period_start_date: fy.year_start_date, - period_end_date: fy.year_end_date + period_end_date: fy.year_end_date, }); }); - } + }, }; - 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 cad5325c6e9..8999ef710f0 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 @@ -10,7 +10,7 @@ from erpnext.accounts.report.financial_statements import get_period_list from erpnext.accounts.utils import get_fiscal_year -class Deferred_Item(object): +class Deferred_Item: """ Helper class for processing items with deferred revenue/expense """ @@ -152,13 +152,11 @@ class Deferred_Item(object): if posting.posted == "posted": actual += self.get_amount(posting) - self.period_total.append( - frappe._dict({"key": period.key, "total": period_sum, "actual": actual}) - ) + self.period_total.append(frappe._dict({"key": period.key, "total": period_sum, "actual": actual})) return self.period_total -class Deferred_Invoice(object): +class Deferred_Invoice: def __init__(self, invoice, items, filters, period_list): """ Helper class for processing invoices with deferred revenue/expense items @@ -194,7 +192,7 @@ class Deferred_Invoice(object): for item in self.items: item_total = item.calculate_item_revenue_expense_for_period() # update invoice total - for idx, period in enumerate(self.period_list, 0): + for idx in range(len(self.period_list)): self.period_total[idx].total += item_total[idx].total self.period_total[idx].actual += item_total[idx].actual return self.period_total @@ -219,7 +217,7 @@ class Deferred_Invoice(object): return ret_data -class Deferred_Revenue_and_Expense_Report(object): +class Deferred_Revenue_and_Expense_Report: def __init__(self, filters=None): """ Initialize deferred revenue/expense report with user provided filters or system defaults, if none is provided @@ -348,7 +346,7 @@ class Deferred_Revenue_and_Expense_Report(object): for inv in self.deferred_invoices: inv_total = inv.calculate_invoice_revenue_expense_for_period() # calculate total for whole report - for idx, period in enumerate(self.period_list, 0): + for idx in range(len(self.period_list)): self.period_total[idx].total += inv_total[idx].total self.period_total[idx].actual += inv_total[idx].actual 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 7b1a9027780..f8a965b699c 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 @@ -1,5 +1,3 @@ -import unittest - import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings @@ -13,8 +11,6 @@ from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_e ) from erpnext.accounts.test.accounts_mixin import AccountsTestMixin 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 class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin): diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.js b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.js index 1454b2c73c8..f6051d7e04f 100644 --- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.js +++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.js @@ -2,7 +2,5 @@ // For license information, please see license.txt frappe.query_reports["Delivered Items To Be Billed"] = { - "filters": [ - - ] -} + filters: [], +}; 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 33d51066dac..149b258248c 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 @@ -2,83 +2,81 @@ // For license information, please see license.txt /* eslint-disable */ -frappe.require("assets/erpnext/js/financial_statements.js", function() { +frappe.require("assets/erpnext/js/financial_statements.js", function () { frappe.query_reports["Dimension-wise Accounts Balance Report"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname": "fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1, - "on_change": function(query_report) { + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "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; if (!fiscal_year) { return; } - frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); frappe.query_report.set_filter_value({ from_date: fy.year_start_date, - to_date: fy.year_end_date + to_date: fy.year_end_date, }); }); - } + }, }, { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], - "reqd": 1 + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + reqd: 1, }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], - "reqd": 1 + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + reqd: 1, }, { - "fieldname": "finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book", + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname": "dimension", - "label": __("Select Dimension"), - "fieldtype": "Select", - "default": "Cost Center", - "options": get_accounting_dimension_options(), - "reqd": 1, + fieldname: "dimension", + label: __("Select Dimension"), + fieldtype: "Select", + default: "Cost Center", + options: get_accounting_dimension_options(), + reqd: 1, }, ], - "formatter": erpnext.financial_statements.formatter, - "tree": true, - "name_field": "account", - "parent_field": "parent_account", - "initial_depth": 3 - } - + formatter: erpnext.financial_statements.formatter, + tree: true, + name_field: "account", + parent_field: "parent_account", + initial_depth: 3, + }; }); function get_accounting_dimension_options() { - let options =["Cost Center", "Project"]; - frappe.db.get_list('Accounting Dimension', - {fields:['document_type']}).then((res) => { - res.forEach((dimension) => { - options.push(dimension.document_type); - }); + let options = ["Cost Center", "Project"]; + frappe.db.get_list("Accounting Dimension", { fields: ["document_type"] }).then((res) => { + res.forEach((dimension) => { + options.push(dimension.document_type); }); - return options + }); + return options; } diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index ecad9f104fa..6c1e73d5edc 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -15,7 +15,6 @@ from erpnext.accounts.report.trial_balance.trial_balance import validate_filters def execute(filters=None): - validate_filters(filters) dimension_list = get_dimensions(filters) @@ -90,9 +89,7 @@ def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_ac gl_filters["dimensions"] = set(dimension_list) if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.db.get_value( - "Company", filters.company, "default_finance_book" - ) + gl_filters["company_fb"] = frappe.db.get_value("Company", filters.company, "default_finance_book") gl_entries = frappe.db.sql( """ @@ -119,7 +116,6 @@ def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_ac def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, dimension_type): - for entries in gl_entries_by_account.values(): for entry in entries: d = accounts_by_name.get(entry.account) @@ -151,7 +147,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list): "to_date": filters.to_date, "currency": company_currency, "account_name": ( - "{} - {}".format(d.account_number, d.account_name) if d.account_number else d.account_name + f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name ), } @@ -183,7 +179,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list): def get_condition(dimension): conditions = [] - conditions.append("{0} in %(dimensions)s".format(frappe.scrub(dimension))) + conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s") return " and {}".format(" and ".join(conditions)) if conditions else "" diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 7355c4b8a16..56f3bd1caa4 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -8,17 +8,7 @@ import re import frappe from frappe import _ -from frappe.utils import ( - add_days, - add_months, - cint, - cstr, - flt, - formatdate, - get_first_day, - getdate, - today, -) +from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, @@ -53,8 +43,6 @@ def get_period_list( year_start_date = getdate(period_start_date) year_end_date = getdate(period_end_date) - year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date - months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity] period_list = [] @@ -174,7 +162,6 @@ def get_data( ignore_accumulated_values_for_fy=False, total=True, ): - accounts = get_accounts(company, root_type) if not accounts: return None @@ -190,7 +177,6 @@ def get_data( root_type, as_dict=1, ): - set_gl_entries_by_account( company, period_list[0]["year_start_date"] if only_current_fiscal_year else None, @@ -248,7 +234,8 @@ def calculate_values( if entry.posting_date <= period.to_date: if (accumulated_values or entry.posting_date >= period.from_date) and ( - not ignore_accumulated_values_for_fy or entry.fiscal_year == period.to_date_fiscal_year + not ignore_accumulated_values_for_fy + or entry.fiscal_year == period.to_date_fiscal_year ): d[period.key] = d.get(period.key, 0.0) + flt(entry.debit) - flt(entry.credit) @@ -292,9 +279,7 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency): "is_group": d.is_group, "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1), "account_name": ( - "%s - %s" % (_(d.account_number), _(d.account_name)) - if d.account_number - else _(d.account_name) + f"{_(d.account_number)} - {_(d.account_name)}" if d.account_number else _(d.account_name) ), } ) @@ -382,7 +367,7 @@ def filter_accounts(accounts, depth=20): def add_to_list(parent, level): if level < depth: children = parent_children_map.get(parent) or [] - sort_accounts(children, is_root=True if parent == None else False) + sort_accounts(children, is_root=True if parent is None else False) for child in children: child.indent = level @@ -573,7 +558,9 @@ def apply_additional_conditions(doctype, query, from_date, ignore_closing_entrie company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb): - frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'")) + frappe.throw( + _("To use a different finance book, please uncheck 'Include Default FB Entries'") + ) query = query.where( (gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""])) diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js index 7e6b0537e87..4eadf342be8 100644 --- a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js @@ -4,49 +4,49 @@ function get_filters() { let filters = [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"period_start_date", - "label": __("Start Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + fieldname: "period_start_date", + label: __("Start Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - "fieldname":"period_end_date", - "label": __("End Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.get_today() + fieldname: "period_end_date", + label: __("End Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.get_today(), }, { - "fieldname":"account", - "label": __("Account"), - "fieldtype": "MultiSelectList", - "options": "Account", - get_data: function(txt) { - return frappe.db.get_link_options('Account', txt, { + fieldname: "account", + label: __("Account"), + fieldtype: "MultiSelectList", + options: "Account", + get_data: function (txt) { + return frappe.db.get_link_options("Account", txt, { company: frappe.query_report.get_filter_value("company"), - account_type: ['in', ["Receivable", "Payable"]] + account_type: ["in", ["Receivable", "Payable"]], }); - } + }, }, { - "fieldname":"voucher_no", - "label": __("Voucher No"), - "fieldtype": "Data", - "width": 100, + fieldname: "voucher_no", + label: __("Voucher No"), + fieldtype: "Data", + width: 100, }, - ] + ]; return filters; } frappe.query_reports["General and Payment Ledger Comparison"] = { - "filters": get_filters() + filters: get_filters(), }; diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py index 696a03b0a70..89cf7e504f0 100644 --- a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py @@ -7,7 +7,7 @@ from frappe.query_builder import Criterion from frappe.query_builder.functions import Sum -class General_Payment_Ledger_Comparison(object): +class General_Payment_Ledger_Comparison: """ A Utility report to compare Voucher-wise balance between General and Payment Ledger """ @@ -58,10 +58,9 @@ class General_Payment_Ledger_Comparison(object): for acc_type, val in self.account_types.items(): if val.accounts: - filter_criterion = [] if self.filters.voucher_no: - filter_criterion.append((gle.voucher_no == self.filters.voucher_no)) + filter_criterion.append(gle.voucher_no == self.filters.voucher_no) if self.filters.period_start_date: filter_criterion.append(gle.posting_date.gte(self.filters.period_start_date)) @@ -102,10 +101,9 @@ class General_Payment_Ledger_Comparison(object): for acc_type, val in self.account_types.items(): if val.accounts: - filter_criterion = [] if self.filters.voucher_no: - filter_criterion.append((ple.voucher_no == self.filters.voucher_no)) + filter_criterion.append(ple.voucher_no == self.filters.voucher_no) if self.filters.period_start_date: filter_criterion.append(ple.posting_date.gte(self.filters.period_start_date)) @@ -141,7 +139,7 @@ class General_Payment_Ledger_Comparison(object): self.ple_balances = set() # consolidate both receivable and payable balances in one set - for acc_type, val in self.account_types.items(): + for _acc_type, val in self.account_types.items(): self.gle_balances = set(val.gle) | self.gle_balances self.ple_balances = set(val.ple) | self.ple_balances @@ -177,7 +175,6 @@ class General_Payment_Ledger_Comparison(object): def get_columns(self): self.columns = [] - options = None self.columns.append( dict( label=_("Company"), diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py index 59e906ba332..afa81b83cfc 100644 --- a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py @@ -1,5 +1,3 @@ -import unittest - import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase @@ -40,9 +38,7 @@ class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin): ) # manually edit the payment ledger entry - ple = frappe.db.get_all( - "Payment Ledger Entry", filters={"voucher_no": sinv.name, "delinked": 0} - )[0] + ple = frappe.db.get_all("Payment Ledger Entry", filters={"voucher_no": sinv.name, "delinked": 0})[0] frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", sinv.grand_total - 1) filters = frappe._dict({"company": self.company}) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index 2d5ca497654..3c4e1a05c97 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -55,10 +55,10 @@
    - {%= format_currency(data[i].debit, filters.presentation_currency) %} + {%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %} - {%= format_currency(data[i].credit, filters.presentation_currency) %} + {%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
    And ${hidden_logs.length} more others
    ${__("Meta Data")}${__("Unresolve")}${__("Unresolve")}${__("Error Message")}${__("Create")}${__("Create")}
    - + @@ -115,28 +116,30 @@ erpnext.BOMComparisonTool = class BOMComparisonTool {
    ${__('Field')}${__("Field")} ${name1} ${name2}
    `; - } + }; - let value_changes = change_html(__('Values Changed'), doctype, diff.changed); + let value_changes = change_html(__("Values Changed"), doctype, diff.changed); - let row_changes_by_fieldname = group_items(diff.row_changed, change => change[0]); + let row_changes_by_fieldname = group_items(diff.row_changed, (change) => change[0]); - let table_changes = Object.keys(row_changes_by_fieldname).map(fieldname => { - let changes = row_changes_by_fieldname[fieldname]; - let df = frappe.meta.get_docfield(doctype, fieldname); - - let html = changes.map(change => { - let [fieldname,, item_code, changes] = change; + let table_changes = Object.keys(row_changes_by_fieldname) + .map((fieldname) => { + let changes = row_changes_by_fieldname[fieldname]; let df = frappe.meta.get_docfield(doctype, fieldname); - let child_doctype = df.options; - let values_changed = this.get_changed_values(child_doctype, changes); - return values_changed.map((change, i) => { - let [fieldname, value1, value2] = change; - let th = i === 0 - ? `${item_code}` - : ''; - return ` + let html = changes + .map((change) => { + let [fieldname, , item_code, changes] = change; + let df = frappe.meta.get_docfield(doctype, fieldname); + let child_doctype = df.options; + let values_changed = this.get_changed_values(child_doctype, changes); + + return values_changed + .map((change, i) => { + let [fieldname, value1, value2] = change; + let th = + i === 0 ? `${item_code}` : ""; + return ` ${th} ${frappe.meta.get_label(child_doctype, fieldname)} @@ -144,54 +147,58 @@ erpnext.BOMComparisonTool = class BOMComparisonTool { ${value2} `; - }).join(''); - }).join(''); + }) + .join(""); + }) + .join(""); - return ` -

    ${__('Changes in {0}', [df.label])}

    + return ` +

    ${__("Changes in {0}", [df.label])}

    - - + + ${html}
    ${__('Item Code')}${__('Field')}${__("Item Code")}${__("Field")} ${name1} ${name2}
    `; - }).join(''); + }) + .join(""); let get_added_removed_html = (title, grouped_items) => { - return Object.keys(grouped_items).map(fieldname => { - let rows = grouped_items[fieldname]; - let df = frappe.meta.get_docfield(doctype, fieldname); - let fields = frappe.meta.get_docfields(df.options) - .filter(df => df.in_list_view); + return Object.keys(grouped_items) + .map((fieldname) => { + let rows = grouped_items[fieldname]; + let df = frappe.meta.get_docfield(doctype, fieldname); + let fields = frappe.meta.get_docfields(df.options).filter((df) => df.in_list_view); - let html = rows.map(row => { - let [, doc] = row; - let cells = fields - .map(df => `${doc[df.fieldname]}`) - .join(''); - return `${cells}`; - }).join(''); + let html = rows + .map((row) => { + let [, doc] = row; + let cells = fields.map((df) => `${doc[df.fieldname]}`).join(""); + return `${cells}`; + }) + .join(""); - let header = fields.map(df => `${df.label}`).join(''); - return ` + let header = fields.map((df) => `${df.label}`).join(""); + return `

    ${$.format(title, [df.label])}

    ${header} ${html}
    `; - }).join(''); + }) + .join(""); }; - let added_by_fieldname = group_items(diff.added, change => change[0]); - let removed_by_fieldname = group_items(diff.removed, change => change[0]); + let added_by_fieldname = group_items(diff.added, (change) => change[0]); + let removed_by_fieldname = group_items(diff.removed, (change) => change[0]); - let added_html = get_added_removed_html(__('Rows Added in {0}'), added_by_fieldname); - let removed_html = get_added_removed_html(__('Rows Removed in {0}'), removed_by_fieldname); + let added_html = get_added_removed_html(__("Rows Added in {0}"), added_by_fieldname); + let removed_html = get_added_removed_html(__("Rows Removed in {0}"), removed_by_fieldname); let html = ` ${value_changes} @@ -200,14 +207,14 @@ erpnext.BOMComparisonTool = class BOMComparisonTool { ${removed_html} `; - this.form.get_field('preview').html(html); + this.form.get_field("preview").html(html); } get_changed_values(doctype, changed) { - return changed.filter(change => { + return changed.filter((change) => { let [fieldname, value1, value2] = change; - if (!value1) value1 = ''; - if (!value2) value2 = ''; + if (!value1) value1 = ""; + if (!value2) value2 = ""; if (value1 === value2) return false; let df = frappe.meta.get_docfield(doctype, fieldname); if (!df) return false; diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.js b/erpnext/manufacturing/report/bom_explorer/bom_explorer.js index b94d3f37704..073b670e8c1 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.js +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.js @@ -3,13 +3,13 @@ /* eslint-disable */ frappe.query_reports["BOM Explorer"] = { - "filters": [ + filters: [ { fieldname: "bom", label: __("BOM"), fieldtype: "Link", options: "BOM", - reqd: 1 + reqd: 1, }, - ] + ], }; diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js index 30974f170c4..ce93208243f 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js @@ -3,40 +3,40 @@ /* eslint-disable */ frappe.query_reports["BOM Operations Time"] = { - "filters": [ + filters: [ { - "fieldname": "item_code", - "label": __("Item Code"), - "fieldtype": "Link", - "width": "100", - "options": "Item", - "get_query": () =>{ + fieldname: "item_code", + label: __("Item Code"), + fieldtype: "Link", + width: "100", + options: "Item", + get_query: () => { return { - filters: { "is_stock_item": 1 } - } - } + filters: { is_stock_item: 1 }, + }; + }, }, { - "fieldname": "bom_id", - "label": __("BOM ID"), - "fieldtype": "MultiSelectList", - "width": "100", - "options": "BOM", - "get_data": function(txt) { + fieldname: "bom_id", + label: __("BOM ID"), + fieldtype: "MultiSelectList", + width: "100", + options: "BOM", + get_data: function (txt) { return frappe.db.get_link_options("BOM", txt); }, - "get_query": () =>{ + get_query: () => { return { - filters: { "docstatus": 1, "is_active": 1, "with_operations": 1 } - } - } + filters: { docstatus: 1, is_active: 1, with_operations: 1 }, + }; + }, }, { - "fieldname": "workstation", - "label": __("Workstation"), - "fieldtype": "Link", - "width": "100", - "options": "Workstation" + fieldname: "workstation", + label: __("Workstation"), + fieldtype: "Link", + width: "100", + options: "Workstation", }, - ] + ], }; diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js index a0fd91e866f..d6b86595ef4 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js @@ -3,32 +3,32 @@ /* eslint-disable */ frappe.query_reports["BOM Stock Calculated"] = { - "filters": [ + filters: [ { - "fieldname": "bom", - "label": __("BOM"), - "fieldtype": "Link", - "options": "BOM", - "reqd": 1 + fieldname: "bom", + label: __("BOM"), + fieldtype: "Link", + options: "BOM", + reqd: 1, }, { - "fieldname": "warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse", + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", }, { - "fieldname": "qty_to_make", - "label": __("Quantity to Make"), - "fieldtype": "Float", - "default": "1.0", - "reqd": 1 + fieldname: "qty_to_make", + label: __("Quantity to Make"), + fieldtype: "Float", + default: "1.0", + reqd: 1, }, { - "fieldname": "show_exploded_view", - "label": __("Show exploded view"), - "fieldtype": "Check", - "default": false, - } - ] -} + fieldname: "show_exploded_view", + label: __("Show exploded view"), + fieldtype: "Check", + default: false, + }, + ], +}; diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index 550445c1f77..6bc05a468f1 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -33,9 +33,7 @@ def get_report_data(last_purchase_rate, required_qty, row, manufacture_details): row.item_code, row.description, comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False), - comma_and( - manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False - ), + comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False), qty_per_unit, row.actual_qty, required_qty, diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js index e7f67caf249..91d73d0101c 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js @@ -1,38 +1,41 @@ frappe.query_reports["BOM Stock Report"] = { - "filters": [ + filters: [ { - "fieldname": "bom", - "label": __("BOM"), - "fieldtype": "Link", - "options": "BOM", - "reqd": 1 - }, { - "fieldname": "warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse", - "reqd": 1 - }, { - "fieldname": "show_exploded_view", - "label": __("Show exploded view"), - "fieldtype": "Check" - }, { - "fieldname": "qty_to_produce", - "label": __("Quantity to Produce"), - "fieldtype": "Int", - "default": "1" - }, + fieldname: "bom", + label: __("BOM"), + fieldtype: "Link", + options: "BOM", + reqd: 1, + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + reqd: 1, + }, + { + fieldname: "show_exploded_view", + label: __("Show exploded view"), + fieldtype: "Check", + }, + { + fieldname: "qty_to_produce", + label: __("Quantity to Produce"), + fieldtype: "Int", + default: "1", + }, ], - "formatter": function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (column.id == "item") { if (data["in_stock_qty"] >= data["required_qty"]) { - value = `${data['item']}`; + value = `${data["item"]}`; } else { - value = `${data['item']}`; + value = `${data["item"]}`; } } - return value - } -} + return value; + }, +}; diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 3573a3a93d8..48ffbac5820 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -44,9 +44,7 @@ def get_bom_stock(filters): else: bom_item_table = "BOM Item" - warehouse_details = frappe.db.get_value( - "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 - ) + warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) BOM = frappe.qb.DocType("BOM") BOM_ITEM = frappe.qb.DocType(bom_item_table) diff --git a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.js b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.js index c6ecaef2fa3..fb3a29538b8 100644 --- a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.js +++ b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.js @@ -3,27 +3,27 @@ /* eslint-disable */ frappe.query_reports["BOM Variance Report"] = { - "filters": [ + filters: [ { - "fieldname":"bom_no", - "label": __("BOM No"), - "fieldtype": "Link", - "options": "BOM" + fieldname: "bom_no", + label: __("BOM No"), + fieldtype: "Link", + options: "BOM", }, { - "fieldname":"work_order", - "label": __("Work Order"), - "fieldtype": "Link", - "options": "Work Order", - "get_query": function() { - var bom_no = frappe.query_report.get_filter_value('bom_no'); - return{ + fieldname: "work_order", + label: __("Work Order"), + fieldtype: "Link", + options: "Work Order", + get_query: function () { + var bom_no = frappe.query_report.get_filter_value("bom_no"); + return { query: "erpnext.manufacturing.report.bom_variance_report.bom_variance_report.get_work_orders", filters: { - 'bom_no': bom_no - } - } - } + bom_no: bom_no, + }, + }; + }, }, - ] -} + ], +}; diff --git a/erpnext/manufacturing/report/completed_work_orders/completed_work_orders.json b/erpnext/manufacturing/report/completed_work_orders/completed_work_orders.json index be50e93f1ba..7925b8a8ab8 100644 --- a/erpnext/manufacturing/report/completed_work_orders/completed_work_orders.json +++ b/erpnext/manufacturing/report/completed_work_orders/completed_work_orders.json @@ -1,25 +1,28 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-08-12 12:44:27", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2018-02-13 04:58:51.549413", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Completed Work Orders", - "owner": "Administrator", - "query": "SELECT\n `tabWork Order`.name as \"Work Order:Link/Work Order:200\",\n `tabWork Order`.creation as \"Date:Date:120\",\n `tabWork Order`.production_item as \"Item:Link/Item:150\",\n `tabWork Order`.qty as \"To Produce:Int:100\",\n `tabWork Order`.produced_qty as \"Produced:Int:100\",\n `tabWork Order`.company as \"Company:Link/Company:\"\nFROM\n `tabWork Order`\nWHERE\n `tabWork Order`.docstatus=1\n AND ifnull(`tabWork Order`.produced_qty,0) = `tabWork Order`.qty", - "ref_doctype": "Work Order", - "report_name": "Completed Work Orders", - "report_type": "Query Report", + "add_total_row": 0, + "columns": [], + "creation": "2013-08-12 12:44:27", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2024-02-21 14:35:14.301848", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Completed Work Orders", + "owner": "Administrator", + "prepared_report": 0, + "query": "SELECT\n `tabWork Order`.name as \"Work Order:Link/Work Order:200\",\n `tabWork Order`.creation as \"Date:Date:120\",\n `tabWork Order`.production_item as \"Item:Link/Item:150\",\n `tabWork Order`.qty as \"To Produce:Int:100\",\n `tabWork Order`.produced_qty as \"Produced:Int:100\",\n `tabWork Order`.company as \"Company:Link/Company:\"\nFROM\n `tabWork Order`\nWHERE\n `tabWork Order`.docstatus=1\n AND ifnull(`tabWork Order`.produced_qty,0) >= `tabWork Order`.qty", + "ref_doctype": "Work Order", + "report_name": "Completed Work Orders", + "report_type": "Query Report", "roles": [ { "role": "Manufacturing User" - }, + }, { "role": "Stock User" } diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js index 72eed5e0d7c..6092b39b6c5 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js @@ -3,24 +3,26 @@ /* eslint-disable */ frappe.query_reports["Cost of Poor Quality Report"] = { - "filters": [ + filters: [ { label: __("Company"), fieldname: "company", fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { label: __("From Date"), - fieldname:"from_date", + fieldname: "from_date", fieldtype: "Datetime", - default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)), + default: frappe.datetime.convert_to_system_tz( + frappe.datetime.add_months(frappe.datetime.now_datetime(), -1) + ), }, { label: __("To Date"), - fieldname:"to_date", + fieldname: "to_date", fieldtype: "Datetime", default: frappe.datetime.now_datetime(), }, @@ -29,45 +31,45 @@ frappe.query_reports["Cost of Poor Quality Report"] = { fieldname: "name", fieldtype: "Link", options: "Job Card", - get_query: function() { + get_query: function () { return { filters: { is_corrective_job_card: 1, - docstatus: 1 - } - } - } + docstatus: 1, + }, + }; + }, }, { label: __("Work Order"), fieldname: "work_order", fieldtype: "Link", - options: "Work Order" + options: "Work Order", }, { label: __("Operation"), fieldname: "operation", fieldtype: "Link", options: "Operation", - get_query: function() { + get_query: function () { return { filters: { - is_corrective_operation: 1 - } - } - } + is_corrective_operation: 1, + }, + }; + }, }, { label: __("Workstation"), fieldname: "workstation", fieldtype: "Link", - options: "Workstation" + options: "Workstation", }, { label: __("Item"), fieldname: "production_item", fieldtype: "Link", - options: "Item" + options: "Item", }, { label: __("Serial No"), @@ -75,14 +77,14 @@ frappe.query_reports["Cost of Poor Quality Report"] = { fieldtype: "Link", options: "Serial No", depends_on: "eval: doc.production_item", - get_query: function() { - var item_code = frappe.query_report.get_filter_value('production_item'); + get_query: function () { + var item_code = frappe.query_report.get_filter_value("production_item"); return { filters: { - item_code: item_code - } - } - } + item_code: item_code, + }, + }; + }, }, { label: __("Batch No"), @@ -90,14 +92,14 @@ frappe.query_reports["Cost of Poor Quality Report"] = { fieldtype: "Link", options: "Batch No", depends_on: "eval: doc.production_item", - get_query: function() { - var item_code = frappe.query_report.get_filter_value('production_item'); + get_query: function () { + var item_code = frappe.query_report.get_filter_value("production_item"); return { filters: { - item: item_code - } - } - } + item: item_code, + }, + }; + }, }, - ] + ], }; diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 481fe51d739..a86df319441 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -20,9 +20,7 @@ def get_data(report_filters): job_card = frappe.qb.DocType("Job Card") - operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_( - "operating_cost" - ) + operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_("operating_cost") item_code = (job_card.production_item).as_("item_code") query = ( @@ -64,7 +62,7 @@ def append_filters(query, report_filters, operations, job_card): ): if report_filters.get(field): if field == "serial_no": - query = query.where(job_card[field].like("%{}%".format(report_filters.get(field)))) + query = query.where(job_card[field].like(f"%{report_filters.get(field)}%")) elif field == "operation": query = query.where(job_card[field].isin(operations)) else: diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js index f6486743aa3..22cf59c572a 100644 --- a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js @@ -3,17 +3,19 @@ /* eslint-disable */ frappe.query_reports["Downtime Analysis"] = { - "filters": [ + filters: [ { label: __("From Date"), - fieldname:"from_date", + fieldname: "from_date", fieldtype: "Datetime", - default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)), - reqd: 1 + default: frappe.datetime.convert_to_system_tz( + frappe.datetime.add_months(frappe.datetime.now_datetime(), -1) + ), + reqd: 1, }, { label: __("To Date"), - fieldname:"to_date", + fieldname: "to_date", fieldtype: "Datetime", default: frappe.datetime.now_datetime(), reqd: 1, @@ -22,7 +24,7 @@ frappe.query_reports["Downtime Analysis"] = { label: __("Machine"), fieldname: "workstation", fieldtype: "Link", - options: "Workstation" - } - ] + options: "Workstation", + }, + ], }; diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js index 123a82a3882..f9e2b6cfcb0 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js @@ -3,95 +3,95 @@ /* eslint-disable */ frappe.query_reports["Exponential Smoothing Forecasting"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1 + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), 12), - "reqd": 1 + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), 12), + reqd: 1, }, { - "fieldname":"based_on_document", - "label": __("Based On Document"), - "fieldtype": "Select", - "options": ["Sales Order", "Delivery Note", "Quotation"], - "default": "Sales Order", - "reqd": 1 + fieldname: "based_on_document", + label: __("Based On Document"), + fieldtype: "Select", + options: ["Sales Order", "Delivery Note", "Quotation"], + default: "Sales Order", + reqd: 1, }, { - "fieldname":"based_on_field", - "label": __("Based On"), - "fieldtype": "Select", - "options": ["Qty", "Amount"], - "default": "Qty", - "reqd": 1 + fieldname: "based_on_field", + label: __("Based On"), + fieldtype: "Select", + options: ["Qty", "Amount"], + default: "Qty", + reqd: 1, }, { - "fieldname":"no_of_years", - "label": __("Based On Data ( in years )"), - "fieldtype": "Select", - "options": [3, 6, 9], - "default": 3, - "reqd": 1 + fieldname: "no_of_years", + label: __("Based On Data ( in years )"), + fieldtype: "Select", + options: [3, 6, 9], + default: 3, + reqd: 1, }, { - "fieldname": "periodicity", - "label": __("Periodicity"), - "fieldtype": "Select", - "options": [ - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Half-Yearly", "label": __("Half-Yearly") }, - { "value": "Yearly", "label": __("Yearly") } + fieldname: "periodicity", + label: __("Periodicity"), + fieldtype: "Select", + options: [ + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, ], - "default": "Yearly", - "reqd": 1 + default: "Yearly", + reqd: 1, }, { - "fieldname":"smoothing_constant", - "label": __("Smoothing Constant"), - "fieldtype": "Select", - "options": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], - "reqd": 1, - "default": 0.3 + fieldname: "smoothing_constant", + label: __("Smoothing Constant"), + fieldtype: "Select", + options: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], + reqd: 1, + default: 0.3, }, { - "fieldname":"item_code", - "label": __("Item Code"), - "fieldtype": "Link", - "options": "Item" + fieldname: "item_code", + label: __("Item Code"), + fieldtype: "Link", + options: "Item", }, { - "fieldname":"warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse", + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", get_query: () => { - var company = frappe.query_report.get_filter_value('company'); + var company = frappe.query_report.get_filter_value("company"); if (company) { return { filters: { - 'company': company - } + company: company, + }, }; } - } - } - ] + }, + }, + ], }; diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py index d3bce831551..0155b19d14c 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -15,9 +15,9 @@ def execute(filters=None): return ForecastingReport(filters).execute_report() -class ExponentialSmoothingForecast(object): +class ExponentialSmoothingForecast: def forecast_future_data(self): - for key, value in self.period_wise_data.items(): + for _key, value in self.period_wise_data.items(): forecast_data = [] for period in self.period_list: forecast_key = "forecast_" + period.key @@ -87,7 +87,7 @@ class ForecastingReport(ExponentialSmoothingForecast): entry.get(self.based_on_field) ) - for key, value in self.period_wise_data.items(): + for value in self.period_wise_data.values(): list_of_period_value = [value.get(p.key, 0) for p in self.period_list] if list_of_period_value: @@ -183,7 +183,6 @@ class ForecastingReport(ExponentialSmoothingForecast): "Half-Yearly", "Quarterly", ] or period.from_date >= getdate(self.filters.from_date): - forecast_key = period.key label = _(period.label) if period.from_date >= getdate(self.filters.from_date): 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 9f1098d46fc..aac687c1413 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -3,14 +3,14 @@ /* eslint-disable */ frappe.query_reports["Job Card Summary"] = { - "filters": [ + filters: [ { label: __("Company"), fieldname: "company", fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { fieldname: "fiscal_year", @@ -19,30 +19,30 @@ frappe.query_reports["Job Card Summary"] = { options: "Fiscal Year", default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), reqd: 1, - on_change: function(query_report) { + on_change: function (query_report) { var fiscal_year = query_report.get_values().fiscal_year; if (!fiscal_year) { return; } - frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); frappe.query_report.set_filter_value({ from_date: fy.year_start_date, - to_date: fy.year_end_date + to_date: fy.year_end_date, }); }); - } + }, }, { label: __("From Posting Date"), - fieldname:"from_date", + fieldname: "from_date", fieldtype: "Date", default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], - reqd: 1 + reqd: 1, }, { label: __("To Posting Date"), - fieldname:"to_date", + fieldname: "to_date", fieldtype: "Date", default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], reqd: 1, @@ -51,35 +51,35 @@ frappe.query_reports["Job Card Summary"] = { label: __("Status"), fieldname: "status", fieldtype: "Select", - options: ["", "Open", "Work In Progress", "Completed", "On Hold"] + options: ["", "Open", "Work In Progress", "Completed", "On Hold"], }, { label: __("Work Orders"), fieldname: "work_order", fieldtype: "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Work Order', txt); - } + get_data: function (txt) { + return frappe.db.get_link_options("Work Order", txt); + }, }, { label: __("Production Item"), fieldname: "production_item", fieldtype: "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Item', txt); - } + get_data: function (txt) { + return frappe.db.get_link_options("Item", txt); + }, }, { label: __("Workstation"), fieldname: "workstation", fieldtype: "Link", - options: "Workstation" + options: "Workstation", }, { label: __("Operation"), fieldname: "operation", fieldtype: "Link", - options: "Operation" - } - ] + options: "Operation", + }, + ], }; diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.js b/erpnext/manufacturing/report/process_loss_report/process_loss_report.js index b0c2b94a254..ab6f64dc5c2 100644 --- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.js +++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.js @@ -4,41 +4,41 @@ frappe.query_reports["Process Loss Report"] = { filters: [ - { - label: __("Company"), - fieldname: "company", - fieldtype: "Link", - options: "Company", - mandatory: true, - default: frappe.defaults.get_user_default("Company"), - }, { - label: __("Item"), - fieldname: "item", - fieldtype: "Link", - options: "Item", - mandatory: false, + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + mandatory: true, + default: frappe.defaults.get_user_default("Company"), }, { - label: __("Work Order"), - fieldname: "work_order", - fieldtype: "Link", - options: "Work Order", - mandatory: false, + label: __("Item"), + fieldname: "item", + fieldtype: "Link", + options: "Item", + mandatory: false, }, - { - label: __("From Date"), - fieldname: "from_date", - fieldtype: "Date", - mandatory: true, - default: frappe.datetime.year_start(), - }, - { - label: __("To Date"), - fieldname: "to_date", - fieldtype: "Date", - mandatory: true, - default: frappe.datetime.get_today(), - }, - ] + { + label: __("Work Order"), + fieldname: "work_order", + fieldtype: "Link", + options: "Work Order", + mandatory: false, + }, + { + label: __("From Date"), + fieldname: "from_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.year_start(), + }, + { + label: __("To Date"), + fieldname: "to_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.get_today(), + }, + ], }; diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py index c3dd9cf9b1a..51efc6e655f 100644 --- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py +++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from typing import Dict, List, Tuple import frappe from frappe import _ @@ -9,12 +8,12 @@ from frappe.query_builder.functions import Sum Filters = frappe._dict Row = frappe._dict -Data = List[Row] -Columns = List[Dict[str, str]] -QueryArgs = Dict[str, str] +Data = list[Row] +Columns = list[dict[str, str]] +QueryArgs = dict[str, str] -def execute(filters: Filters) -> Tuple[Columns, Data]: +def execute(filters: Filters) -> tuple[Columns, Data]: filters = frappe._dict(filters or {}) columns = get_columns() data = get_data(filters) diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.js b/erpnext/manufacturing/report/production_analytics/production_analytics.js index 7c71b2eac65..da8e4926a45 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.js +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.js @@ -3,41 +3,41 @@ /* eslint-disable */ frappe.query_reports["Production Analytics"] = { - "filters": [ + filters: [ { fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], - reqd: 1 + reqd: 1, }, { fieldname: "range", label: __("Range"), fieldtype: "Select", options: [ - { "value": "Weekly", "label": __("Weekly") }, - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Yearly", "label": __("Yearly") } + { value: "Weekly", label: __("Weekly") }, + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Yearly", label: __("Yearly") }, ], default: "Monthly", - reqd: 1 - } - ] -} + reqd: 1, + }, + ], +}; diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index 12b5d19ba87..c02c1e6fcd3 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -20,13 +20,10 @@ def get_columns(filters): ranges = get_period_date_ranges(filters) - for dummy, end_date in ranges: - + for _dummy, end_date in ranges: period = get_period(end_date, filters) - columns.append( - {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} - ) + columns.append({"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120}) return columns @@ -110,7 +107,7 @@ def get_data(filters, columns): for label in labels: work = {} work["Status"] = label - for dummy, end_date in ranges: + for _dummy, end_date in ranges: period = get_period(end_date, filters) if periodic_data.get(label).get(period): work[scrub(period)] = periodic_data.get(label).get(period) diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js index a4d7fae4560..567e8ebc6c3 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js @@ -3,28 +3,28 @@ /* eslint-disable */ frappe.query_reports["Production Plan Summary"] = { - "filters": [ + filters: [ { fieldname: "production_plan", label: __("Production Plan"), fieldtype: "Link", options: "Production Plan", reqd: 1, - get_query: function() { + get_query: function () { return { filters: { - "docstatus": 1 - } + docstatus: 1, + }, }; - } - } + }, + }, ], - "formatter": function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (column.fieldname == "item_code") { - var color = data.pending_qty > 0 ? 'red': 'green'; - value = `${data['item_code']}`; + var color = data.pending_qty > 0 ? "red" : "green"; + value = `${data["item_code"]}`; } return value; diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index 076690ff090..5bc9236c1d5 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -56,14 +56,10 @@ def get_production_plan_item_details(filters, data, order_details): } ) - get_production_plan_sub_assembly_item_details( - filters, row, production_plan_doc, data, order_details - ) + get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details) -def get_production_plan_sub_assembly_item_details( - filters, row, production_plan_doc, data, order_details -): +def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details): for item in production_plan_doc.sub_assembly_items: if row.name == item.production_plan_item: subcontracted_item = item.type_of_manufacturing == "Subcontract" @@ -76,7 +72,9 @@ def get_production_plan_sub_assembly_item_details( ) else: docname = frappe.get_value( - "Work Order", {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name" + "Work Order", + {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, + "name", ) data.append( @@ -88,7 +86,9 @@ def get_production_plan_sub_assembly_item_details( "document_type": "Work Order" if not subcontracted_item else "Purchase Order", "document_name": docname or "", "bom_level": item.bom_level, - "produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0), + "produced_qty": order_details.get((docname, item.production_item), {}).get( + "produced_qty", 0 + ), "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0)), } diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js index 675b8a11008..bde90504e67 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js @@ -3,106 +3,110 @@ /* eslint-disable */ frappe.query_reports["Production Planning Report"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"based_on", - "label": __("Based On"), - "fieldtype": "Select", - "options": ["Sales Order", "Material Request", "Work Order"], - "default": "Sales Order", - "reqd": 1, - on_change: function() { + fieldname: "based_on", + label: __("Based On"), + fieldtype: "Select", + options: ["Sales Order", "Material Request", "Work Order"], + default: "Sales Order", + reqd: 1, + on_change: function () { let filters = frappe.query_report.filters; - let based_on = frappe.query_report.get_filter_value('based_on'); + let based_on = frappe.query_report.get_filter_value("based_on"); let options = { "Sales Order": ["Delivery Date", "Total Amount"], "Material Request": ["Required Date"], - "Work Order": ["Planned Start Date"] - } + "Work Order": ["Planned Start Date"], + }; - filters.forEach(d => { + filters.forEach((d) => { if (d.fieldname == "order_by") { d.df.options = options[based_on]; - d.set_input(d.df.options) + d.set_input(d.df.options); } }); frappe.query_report.refresh(); - } + }, }, { - "fieldname":"docnames", - "label": __("Document Name"), - "fieldtype": "MultiSelectList", - "options": "Sales Order", - "get_data": function(txt) { + fieldname: "docnames", + label: __("Document Name"), + fieldtype: "MultiSelectList", + options: "Sales Order", + get_data: function (txt) { if (!frappe.query_report.filters) return; - let based_on = frappe.query_report.get_filter_value('based_on'); + let based_on = frappe.query_report.get_filter_value("based_on"); if (!based_on) return; return frappe.db.get_link_options(based_on, txt); }, - "get_query": function() { - var company = frappe.query_report.get_filter_value('company'); + get_query: function () { + var company = frappe.query_report.get_filter_value("company"); return { filters: { - "docstatus": 1, - "company": company - } + docstatus: 1, + company: company, + }, }; - } + }, }, { - "fieldname":"raw_material_warehouse", - "label": __("Raw Material Warehouse"), - "fieldtype": "Link", - "options": "Warehouse", - "depends_on": "eval: doc.based_on != 'Work Order'", - "get_query": function() { - var company = frappe.query_report.get_filter_value('company'); + fieldname: "raw_material_warehouse", + label: __("Raw Material Warehouse"), + fieldtype: "Link", + options: "Warehouse", + depends_on: "eval: doc.based_on != 'Work Order'", + get_query: function () { + var company = frappe.query_report.get_filter_value("company"); return { filters: { - "company": company - } + company: company, + }, }; - } + }, }, { - "fieldname":"order_by", - "label": __("Order By"), - "fieldtype": "Select", - "options": ["Delivery Date", "Total Amount"], - "default": "Delivery Date" + fieldname: "order_by", + label: __("Order By"), + fieldtype: "Select", + options: ["Delivery Date", "Total Amount"], + default: "Delivery Date", }, { - "fieldname":"include_subassembly_raw_materials", - "label": __("Include Sub-assembly Raw Materials"), - "fieldtype": "Check", - "depends_on": "eval: doc.based_on != 'Work Order'", - "default": 0 + fieldname: "include_subassembly_raw_materials", + label: __("Include Sub-assembly Raw Materials"), + fieldtype: "Check", + depends_on: "eval: doc.based_on != 'Work Order'", + default: 0, }, ], - "formatter": function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.fieldname == "production_item_name" && data && data.qty_to_manufacture > data.available_qty ) { + if ( + column.fieldname == "production_item_name" && + data && + data.qty_to_manufacture > data.available_qty + ) { value = `
    ${value}
    `; } - if (column.fieldname == "production_item" && !data.name ) { + if (column.fieldname == "production_item" && !data.name) { value = ""; } - if (column.fieldname == "raw_material_name" && data && data.required_qty > data.allotted_qty ) { + if (column.fieldname == "raw_material_name" && data && data.required_qty > data.allotted_qty) { value = `
    ${value}
    `; } diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index 109d9ab656b..63af3e5cbe6 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -13,7 +13,7 @@ def execute(filters=None): return ProductionPlanReport(filters).execute_report() -class ProductionPlanReport(object): +class ProductionPlanReport: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.raw_materials_dict = {} @@ -79,7 +79,10 @@ class ProductionPlanReport(object): query = query.where(child.parent.isin(self.filters.docnames)) if doctype == "Sales Order": - query = query.select(child.delivery_date, parent.base_grand_total,).where( + query = query.select( + child.delivery_date, + parent.base_grand_total, + ).where( (child.stock_qty > child.produced_qty) & (parent.per_delivered < 100.0) & (parent.status.notin(["Completed", "Closed"])) @@ -91,7 +94,9 @@ class ProductionPlanReport(object): query = query.orderby(parent.base_grand_total, order=Order.desc) elif doctype == "Material Request": - query = query.select(child.schedule_date,).where( + query = query.select( + child.schedule_date, + ).where( (parent.per_ordered < 100) & (parent.material_request_type == "Manufacture") & (parent.status != "Stopped") @@ -280,9 +285,7 @@ class ProductionPlanReport(object): d.remaining_qty = d.required_qty self.pick_materials_from_warehouses(d, data, warehouses) - if ( - d.remaining_qty and self.filters.raw_material_warehouse and d.remaining_qty != d.required_qty - ): + if d.remaining_qty and self.filters.raw_material_warehouse and d.remaining_qty != d.required_qty: row = self.get_args() d.warehouse = self.filters.raw_material_warehouse d.required_qty = d.remaining_qty diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js index d4587aa6619..1e90955ebb1 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js @@ -3,17 +3,17 @@ /* eslint-disable */ frappe.query_reports["Quality Inspection Summary"] = { - "filters": [ + filters: [ { label: __("From Date"), - fieldname:"from_date", + fieldname: "from_date", fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.get_today(), -12), - reqd: 1 + reqd: 1, }, { label: __("To Date"), - fieldname:"to_date", + fieldname: "to_date", fieldtype: "Date", default: frappe.datetime.get_today(), reqd: 1, @@ -22,19 +22,19 @@ frappe.query_reports["Quality Inspection Summary"] = { label: __("Status"), fieldname: "status", fieldtype: "Select", - options: ["", "Accepted", "Rejected"] + options: ["", "Accepted", "Rejected"], }, { label: __("Item Code"), fieldname: "item_code", fieldtype: "Link", - options: "Item" + options: "Item", }, { label: __("Inspected By"), fieldname: "inspected_by", fieldtype: "Link", - options: "User" - } - ] + options: "User", + }, + ], }; diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py index e436fdca646..3e20f310ff9 100644 --- a/erpnext/manufacturing/report/test_reports.py +++ b/erpnext/manufacturing/report/test_reports.py @@ -1,5 +1,4 @@ import unittest -from typing import List, Tuple import frappe @@ -13,7 +12,7 @@ DEFAULT_FILTERS = { } -REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ +REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}), ("BOM Operations Time", {}), ("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}), diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js index 2fb4ec67913..51654b90fa9 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js @@ -3,65 +3,65 @@ /* eslint-disable */ frappe.query_reports["Work Order Consumed Materials"] = { - "filters": [ + filters: [ { label: __("Company"), fieldname: "company", fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { label: __("From Date"), - fieldname:"from_date", + fieldname: "from_date", fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: frappe.datetime.get_today(), - reqd: 1 + reqd: 1, }, { label: __("Work Order"), fieldname: "name", fieldtype: "Link", options: "Work Order", - get_query: function() { + get_query: function () { return { filters: { - status: ["in", ["In Process", "Completed", "Stopped"]] - } - } - } + status: ["in", ["In Process", "Completed", "Stopped"]], + }, + }; + }, }, { label: __("Production Item"), fieldname: "production_item", fieldtype: "Link", depends_on: "eval: !doc.name", - options: "Item" + options: "Item", }, { label: __("Status"), fieldname: "status", fieldtype: "Select", - options: ["", "In Process", "Completed", "Stopped"] + options: ["", "In Process", "Completed", "Stopped"], }, { label: __("Excess Materials Consumed"), fieldname: "show_extra_consumed_materials", - fieldtype: "Check" - } + fieldtype: "Check", + }, ], - "formatter": function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.fieldname == "raw_material_name" && data && data.extra_consumed_qty > 0 ) { + if (column.fieldname == "raw_material_name" && data && data.extra_consumed_qty > 0) { value = `
    ${value}
    `; } diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py index 14e97d3dd78..64363e20e39 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -22,7 +22,7 @@ def get_data(report_filters): wo_items = {} work_orders = frappe.get_all("Work Order", filters=filters, fields=fields) - returned_materials = get_returned_materials(work_orders) + get_returned_materials(work_orders) for d in work_orders: d.extra_consumed_qty = 0.0 @@ -33,7 +33,7 @@ def get_data(report_filters): wo_items.setdefault((d.name, d.production_item), []).append(d) data = [] - for key, wo_data in wo_items.items(): + for _key, wo_data in wo_items.items(): for index, row in enumerate(wo_data): if index != 0: # If one work order has multiple raw materials then show parent data in the first row only diff --git a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.js b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.js index dbb7c234101..ec8651ddf7d 100644 --- a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.js +++ b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.js @@ -3,12 +3,12 @@ /* eslint-disable */ frappe.query_reports["Work Order Stock Report"] = { - "filters": [ + filters: [ { - "fieldname": "warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse" - } - ] -} + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + }, + ], +}; diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js index 67bd24dd805..67e6e706c59 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js @@ -3,32 +3,32 @@ /* eslint-disable */ frappe.query_reports["Work Order Summary"] = { - "filters": [ + filters: [ { label: __("Company"), fieldname: "company", fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { label: __("Based On"), - fieldname:"based_on", + fieldname: "based_on", fieldtype: "Select", options: "Creation Date\nPlanned Date\nActual Date", - default: "Creation Date" + default: "Creation Date", }, { label: __("From Posting Date"), - fieldname:"from_date", + fieldname: "from_date", fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.get_today(), -3), - reqd: 1 + reqd: 1, }, { label: __("To Posting Date"), - fieldname:"to_date", + fieldname: "to_date", fieldtype: "Date", default: frappe.datetime.get_today(), reqd: 1, @@ -37,36 +37,36 @@ frappe.query_reports["Work Order Summary"] = { label: __("Status"), fieldname: "status", fieldtype: "Select", - options: ["", "Not Started", "In Process", "Completed", "Stopped", "Closed"] + options: ["", "Not Started", "In Process", "Completed", "Stopped", "Closed"], }, { label: __("Sales Orders"), fieldname: "sales_order", fieldtype: "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Sales Order', txt); - } + get_data: function (txt) { + return frappe.db.get_link_options("Sales Order", txt); + }, }, { label: __("Production Item"), fieldname: "production_item", fieldtype: "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Item', txt); - } + get_data: function (txt) { + return frappe.db.get_link_options("Item", txt); + }, }, { label: __("Age"), - fieldname:"age", + fieldname: "age", fieldtype: "Int", - default: "0" + default: "0", }, { label: __("Charts Based On"), - fieldname:"charts_based_on", + fieldname: "charts_based_on", fieldtype: "Select", options: ["Status", "Age", "Quantity"], - default: "Status" + default: "Status", }, - ] + ], }; diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index 97f30ef62e9..8d3770805e6 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -58,7 +58,7 @@ def get_data(filters): query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")]) data = frappe.get_all( - "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1 + "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc" ) res = [] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d96f45fd676..078aa5e0a0f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -274,6 +274,7 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') +erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes #22-02-2024 erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v14_0.delete_healthcare_doctypes @@ -352,6 +353,13 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation erpnext.patches.v14_0.update_zero_asset_quantity_field execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") erpnext.patches.v14_0.clear_reconciliation_values_from_singles +erpnext.patches.v14_0.update_total_asset_cost_field +erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool +erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22 # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger -erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index +erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 +erpnext.patches.v14_0.set_maintain_stock_for_bom_item +execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1) +erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records +erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset \ No newline at end of file diff --git a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py index e2d0943d724..b98e7ab4bf6 100644 --- a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py +++ b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py @@ -12,9 +12,7 @@ def execute(): if "barcode" not in frappe.db.get_table_columns("Item"): return - items_barcode = frappe.db.sql( - "select name, barcode from tabItem where barcode is not null", as_dict=True - ) + items_barcode = frappe.db.sql("select name, barcode from tabItem where barcode is not null", as_dict=True) frappe.reload_doc("stock", "doctype", "item") for item in items_barcode: diff --git a/erpnext/patches/v10_0/set_currency_in_pricing_rule.py b/erpnext/patches/v10_0/set_currency_in_pricing_rule.py index d68148eec1c..08f5ed166ab 100644 --- a/erpnext/patches/v10_0/set_currency_in_pricing_rule.py +++ b/erpnext/patches/v10_0/set_currency_in_pricing_rule.py @@ -9,6 +9,4 @@ def execute(): if doc.company: currency = frappe.get_cached_value("Company", doc.company, "default_currency") - frappe.db.sql( - """update `tabPricing Rule` set currency = %s where name = %s""", (currency, doc.name) - ) + frappe.db.sql("""update `tabPricing Rule` set currency = %s where name = %s""", (currency, doc.name)) diff --git a/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py b/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py index 87151c102b1..375c0109fc8 100644 --- a/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py +++ b/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py @@ -52,7 +52,6 @@ def drop_columns_from_subscription(): "status", "amended_from", ]: - if field in frappe.db.get_table_columns("Subscription"): fields_to_drop["Subscription"].append(field) diff --git a/erpnext/patches/v11_0/create_department_records_for_each_company.py b/erpnext/patches/v11_0/create_department_records_for_each_company.py index 84be2bee9dc..a4ba154b3e5 100644 --- a/erpnext/patches/v11_0/create_department_records_for_each_company.py +++ b/erpnext/patches/v11_0/create_department_records_for_each_company.py @@ -53,11 +53,10 @@ def update_records(doctype, comp_dict): for department in records: when_then.append( - """ - WHEN company = "%s" and department = "%s" - THEN "%s" + f""" + WHEN company = "{company}" and department = "{department}" + THEN "{records[department]}" """ - % (company, department, records[department]) ) if not when_then: @@ -66,11 +65,10 @@ def update_records(doctype, comp_dict): frappe.db.sql( """ update - `tab%s` + `tab{}` set - department = CASE %s END - """ - % (doctype, " ".join(when_then)) + department = CASE {} END + """.format(doctype, " ".join(when_then)) ) @@ -83,11 +81,10 @@ def update_instructors(comp_dict): for department in records: when_then.append( - """ - WHEN employee = "%s" and department = "%s" - THEN "%s" + f""" + WHEN employee = "{employee.name}" and department = "{department}" + THEN "{records[department]}" """ - % (employee.name, department, records[department]) ) if not when_then: diff --git a/erpnext/patches/v11_0/make_italian_localization_fields.py b/erpnext/patches/v11_0/make_italian_localization_fields.py index 1b9793df80b..05c7b8a1e0c 100644 --- a/erpnext/patches/v11_0/make_italian_localization_fields.py +++ b/erpnext/patches/v11_0/make_italian_localization_fields.py @@ -20,19 +20,17 @@ def execute(): # Set state codes condition = "" for state, code in state_codes.items(): - condition += " when {0} then {1}".format(frappe.db.escape(state), frappe.db.escape(code)) + condition += f" when {frappe.db.escape(state)} then {frappe.db.escape(code)}" if condition: - condition = "state_code = (case state {0} end),".format(condition) + condition = f"state_code = (case state {condition} end)," frappe.db.sql( - """ + f""" UPDATE tabAddress set {condition} country_code = UPPER(ifnull((select code from `tabCountry` where name = `tabAddress`.country), '')) where country_code is null and state_code is null - """.format( - condition=condition - ) + """ ) frappe.db.sql( diff --git a/erpnext/patches/v11_0/merge_land_unit_with_location.py b/erpnext/patches/v11_0/merge_land_unit_with_location.py index c1afef67785..92d0bc90c58 100644 --- a/erpnext/patches/v11_0/merge_land_unit_with_location.py +++ b/erpnext/patches/v11_0/merge_land_unit_with_location.py @@ -29,9 +29,9 @@ def execute(): rename_field("Linked Location", "land_unit", "location") if not frappe.db.exists("Location", "All Land Units"): - frappe.get_doc( - {"doctype": "Location", "is_group": True, "location_name": "All Land Units"} - ).insert(ignore_permissions=True) + frappe.get_doc({"doctype": "Location", "is_group": True, "location_name": "All Land Units"}).insert( + ignore_permissions=True + ) if frappe.db.table_exists("Land Unit"): land_units = frappe.get_all("Land Unit", fields=["*"], order_by="lft") diff --git a/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py b/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py index 37c07799ddc..418bb1378d5 100644 --- a/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py +++ b/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py @@ -106,8 +106,6 @@ def execute(): `expense_account`, `income_account`, `buying_cost_center`, `selling_cost_center` ) VALUES {} - """.format( - ", ".join(["%s"] * len(to_insert_data)) - ), + """.format(", ".join(["%s"] * len(to_insert_data))), tuple(to_insert_data), ) diff --git a/erpnext/patches/v11_0/refactor_naming_series.py b/erpnext/patches/v11_0/refactor_naming_series.py index 9cfb082c1f2..53c293cae81 100644 --- a/erpnext/patches/v11_0/refactor_naming_series.py +++ b/erpnext/patches/v11_0/refactor_naming_series.py @@ -124,9 +124,7 @@ def get_series(): def get_series_to_preserve(doctype): series_to_preserve = frappe.db.sql_list( - """select distinct naming_series from `tab{doctype}` where ifnull(naming_series, '') != ''""".format( - doctype=doctype - ) + f"""select distinct naming_series from `tab{doctype}` where ifnull(naming_series, '') != ''""" ) series_to_preserve.sort() return series_to_preserve diff --git a/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py b/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py index c444c16a59b..206e7f5ebef 100644 --- a/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py +++ b/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py @@ -6,8 +6,6 @@ import frappe def execute(): - if frappe.db.table_exists("Asset Adjustment") and not frappe.db.table_exists( - "Asset Value Adjustment" - ): + if frappe.db.table_exists("Asset Adjustment") and not frappe.db.table_exists("Asset Value Adjustment"): frappe.rename_doc("DocType", "Asset Adjustment", "Asset Value Adjustment", force=True) frappe.reload_doc("assets", "doctype", "asset_value_adjustment") diff --git a/erpnext/patches/v11_0/rename_bom_wo_fields.py b/erpnext/patches/v11_0/rename_bom_wo_fields.py index fb25eeb6fcc..2132ff31917 100644 --- a/erpnext/patches/v11_0/rename_bom_wo_fields.py +++ b/erpnext/patches/v11_0/rename_bom_wo_fields.py @@ -9,9 +9,7 @@ from frappe.model.utils.rename_field import rename_field def execute(): # updating column value to handle field change from Data to Currency changed_field = "base_scrap_material_cost" - frappe.db.sql( - f"update `tabBOM` set {changed_field} = '0' where trim(coalesce({changed_field}, ''))= ''" - ) + frappe.db.sql(f"update `tabBOM` set {changed_field} = '0' where trim(coalesce({changed_field}, ''))= ''") for doctype in ["BOM Explosion Item", "BOM Item", "Work Order Item", "Item"]: if frappe.db.has_column(doctype, "allow_transfer_for_manufacture"): diff --git a/erpnext/patches/v11_0/rename_production_order_to_work_order.py b/erpnext/patches/v11_0/rename_production_order_to_work_order.py index b58ac4e72f1..31a9c88d4eb 100644 --- a/erpnext/patches/v11_0/rename_production_order_to_work_order.py +++ b/erpnext/patches/v11_0/rename_production_order_to_work_order.py @@ -21,14 +21,10 @@ def execute(): rename_field("Timesheet", "production_order", "work_order") rename_field("Stock Entry", "production_order", "work_order") - frappe.rename_doc( - "Report", "Production Orders in Progress", "Work Orders in Progress", force=True - ) + frappe.rename_doc("Report", "Production Orders in Progress", "Work Orders in Progress", force=True) frappe.rename_doc("Report", "Completed Production Orders", "Completed Work Orders", force=True) frappe.rename_doc("Report", "Open Production Orders", "Open Work Orders", force=True) frappe.rename_doc( "Report", "Issued Items Against Production Order", "Issued Items Against Work Order", force=True ) - frappe.rename_doc( - "Report", "Production Order Stock Report", "Work Order Stock Report", force=True - ) + frappe.rename_doc("Report", "Production Order Stock Report", "Work Order Stock Report", force=True) diff --git a/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py b/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py index 96daba7d368..9aa0d67804d 100644 --- a/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py +++ b/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py @@ -25,10 +25,8 @@ def execute(): def build_tree(): frappe.db.sql( - """update `tabSupplier Group` set parent_supplier_group = '{0}' - where is_group = 0""".format( - _("All Supplier Groups") - ) + """update `tabSupplier Group` set parent_supplier_group = '{}' + where is_group = 0""".format(_("All Supplier Groups")) ) if not frappe.db.exists("Supplier Group", _("All Supplier Groups")): diff --git a/erpnext/patches/v11_0/set_department_for_doctypes.py b/erpnext/patches/v11_0/set_department_for_doctypes.py index b784772576f..70e9c8cbba6 100644 --- a/erpnext/patches/v11_0/set_department_for_doctypes.py +++ b/erpnext/patches/v11_0/set_department_for_doctypes.py @@ -4,7 +4,6 @@ import frappe def execute(): - doctypes_to_update = { "projects": ["Activity Cost", "Timesheet"], "setup": ["Sales Person"], diff --git a/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py b/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py index 548a7cb158c..b6fc1a630a2 100644 --- a/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py +++ b/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py @@ -4,7 +4,6 @@ from frappe.model.workflow import get_workflow_name def execute(): for doctype in ["Expense Claim", "Leave Application"]: - active_workflow = get_workflow_name(doctype) if not active_workflow: continue diff --git a/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py b/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py index a7351d27595..3f59747391a 100644 --- a/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py +++ b/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py @@ -16,12 +16,10 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", frappe.scrub(doctype)) frappe.db.sql( - """ update `tab{0}` child, tabItem item + f""" update `tab{doctype}` child, tabItem item set child.include_item_in_manufacturing = 1 where child.item_code = item.name and ifnull(item.is_stock_item, 0) = 1 - """.format( - doctype - ) + """ ) diff --git a/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py b/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py index 51ba706dcf0..9d7d4548987 100644 --- a/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py +++ b/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py @@ -7,9 +7,7 @@ import frappe def execute(): frappe.reload_doc("buying", "doctype", "buying_settings") - frappe.db.set_value( - "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" - ) + frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") frappe.reload_doc("stock", "doctype", "stock_entry_detail") frappe.db.sql( diff --git a/erpnext/patches/v11_0/update_department_lft_rgt.py b/erpnext/patches/v11_0/update_department_lft_rgt.py index 778392ec5fc..9104e36ece8 100644 --- a/erpnext/patches/v11_0/update_department_lft_rgt.py +++ b/erpnext/patches/v11_0/update_department_lft_rgt.py @@ -12,10 +12,8 @@ def execute(): ).insert(ignore_permissions=True, ignore_mandatory=True) frappe.db.sql( - """update `tabDepartment` set parent_department = '{0}' - where is_group = 0""".format( - _("All Departments") - ) + """update `tabDepartment` set parent_department = '{}' + where is_group = 0""".format(_("All Departments")) ) rebuild_tree("Department", "parent_department") diff --git a/erpnext/patches/v11_0/update_sales_partner_type.py b/erpnext/patches/v11_0/update_sales_partner_type.py index 72fd424b245..ced77d97276 100644 --- a/erpnext/patches/v11_0/update_sales_partner_type.py +++ b/erpnext/patches/v11_0/update_sales_partner_type.py @@ -16,7 +16,7 @@ def execute(): # get partner type in existing forms (customized) # and create a document if not created for d in ["Sales Partner"]: - partner_type = frappe.db.sql_list("select distinct partner_type from `tab{0}`".format(d)) + partner_type = frappe.db.sql_list(f"select distinct partner_type from `tab{d}`") for s in partner_type: if s and s not in default_sales_partner_type: insert_sales_partner_type(s) diff --git a/erpnext/patches/v11_0/update_total_qty_field.py b/erpnext/patches/v11_0/update_total_qty_field.py index 09fcdb8723d..3ed7c834a46 100644 --- a/erpnext/patches/v11_0/update_total_qty_field.py +++ b/erpnext/patches/v11_0/update_total_qty_field.py @@ -24,16 +24,14 @@ def execute(): for doctype in doctypes: total_qty = frappe.db.sql( - """ + f""" SELECT parent, SUM(qty) as qty FROM - `tab{0} Item` - where parenttype = '{0}' + `tab{doctype} Item` + where parenttype = '{doctype}' GROUP BY parent - """.format( - doctype - ), + """, as_dict=True, ) @@ -53,13 +51,11 @@ def execute(): # This is probably never used anywhere else as of now, but should be values = [] for d in batch_transactions: - values.append("({0}, {1})".format(frappe.db.escape(d.parent), d.qty)) + values.append(f"({frappe.db.escape(d.parent)}, {d.qty})") conditions = ",".join(values) frappe.db.sql( - """ - INSERT INTO `tab{}` (name, total_qty) VALUES {} + f""" + INSERT INTO `tab{doctype}` (name, total_qty) VALUES {conditions} ON DUPLICATE KEY UPDATE name = VALUES(name), total_qty = VALUES(total_qty) - """.format( - doctype, conditions - ) + """ ) diff --git a/erpnext/patches/v11_1/make_job_card_time_logs.py b/erpnext/patches/v11_1/make_job_card_time_logs.py index beb2c4e5341..14bc4ee2a6d 100644 --- a/erpnext/patches/v11_1/make_job_card_time_logs.py +++ b/erpnext/patches/v11_1/make_job_card_time_logs.py @@ -8,9 +8,7 @@ import frappe def execute(): frappe.reload_doc("manufacturing", "doctype", "job_card_time_log") - if frappe.db.table_exists("Job Card") and frappe.get_meta("Job Card").has_field( - "actual_start_date" - ): + if frappe.db.table_exists("Job Card") and frappe.get_meta("Job Card").has_field("actual_start_date"): time_logs = [] for d in frappe.get_all( "Job Card", @@ -37,9 +35,7 @@ def execute(): `tabJob Card Time Log` (from_time, to_time, time_in_mins, completed_qty, parent, parenttype, parentfield, name) values {values} - """.format( - values=",".join(["%s"] * len(time_logs)) - ), + """.format(values=",".join(["%s"] * len(time_logs))), tuple(time_logs), ) diff --git a/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py b/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py index b681f25d84e..b4095ee4e84 100644 --- a/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py +++ b/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py @@ -8,14 +8,10 @@ import frappe def execute(): frappe.reload_doctype("Quotation") frappe.db.sql(""" UPDATE `tabQuotation` set party_name = lead WHERE quotation_to = 'Lead' """) - frappe.db.sql( - """ UPDATE `tabQuotation` set party_name = customer WHERE quotation_to = 'Customer' """ - ) + frappe.db.sql(""" UPDATE `tabQuotation` set party_name = customer WHERE quotation_to = 'Customer' """) frappe.reload_doctype("Opportunity") - frappe.db.sql( - """ UPDATE `tabOpportunity` set party_name = lead WHERE opportunity_from = 'Lead' """ - ) + frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = lead WHERE opportunity_from = 'Lead' """) frappe.db.sql( """ UPDATE `tabOpportunity` set party_name = customer WHERE opportunity_from = 'Customer' """ ) diff --git a/erpnext/patches/v11_1/set_missing_opportunity_from.py b/erpnext/patches/v11_1/set_missing_opportunity_from.py index ae5f6200145..fd3bf7ef445 100644 --- a/erpnext/patches/v11_1/set_missing_opportunity_from.py +++ b/erpnext/patches/v11_1/set_missing_opportunity_from.py @@ -2,7 +2,6 @@ import frappe def execute(): - frappe.reload_doctype("Opportunity") if frappe.db.has_column("Opportunity", "enquiry_from"): frappe.db.sql( @@ -10,9 +9,7 @@ def execute(): where ifnull(opportunity_from, '') = '' and ifnull(enquiry_from, '') != ''""" ) - if frappe.db.has_column("Opportunity", "lead") and frappe.db.has_column( - "Opportunity", "enquiry_from" - ): + if frappe.db.has_column("Opportunity", "lead") and frappe.db.has_column("Opportunity", "enquiry_from"): frappe.db.sql( """ UPDATE `tabOpportunity` set party_name = lead where enquiry_from = 'Lead' and ifnull(party_name, '') = '' and ifnull(lead, '') != ''""" diff --git a/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py b/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py index 284b616bbda..0029074da94 100644 --- a/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py +++ b/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py @@ -11,9 +11,7 @@ def execute(): if frappe.db.has_column("Company", "default_terms"): rename_field("Company", "default_terms", "default_selling_terms") - for company in frappe.get_all( - "Company", ["name", "default_selling_terms", "default_buying_terms"] - ): + for company in frappe.get_all("Company", ["name", "default_selling_terms", "default_buying_terms"]): if company.default_selling_terms and not company.default_buying_terms: frappe.db.set_value( "Company", company.name, "default_buying_terms", company.default_selling_terms diff --git a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py index 744ea1ccd8a..c3bf4a0f39e 100644 --- a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py +++ b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py @@ -3,7 +3,6 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field def execute(): - frappe.reload_doc("accounts", "doctype", "accounting_dimension") accounting_dimensions = frappe.db.sql( @@ -17,7 +16,6 @@ def execute(): count = 1 for d in accounting_dimensions: - if count % 2 == 0: insert_after_field = "dimension_col_break" else: @@ -31,7 +29,6 @@ def execute(): "Expense Claim Detail", "Expense Taxes and Charges", ]: - field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) if field: diff --git a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py index 80e9047cf06..8c7f6bd225f 100644 --- a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py +++ b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py @@ -4,7 +4,6 @@ from erpnext.regional.united_states.setup import make_custom_fields def execute(): - frappe.reload_doc("accounts", "doctype", "allowed_to_transact_with", force=True) frappe.reload_doc("accounts", "doctype", "pricing_rule_detail", force=True) frappe.reload_doc("crm", "doctype", "lost_reason_detail", force=True) diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py index 285183bfa25..5d9c8102481 100644 --- a/erpnext/patches/v12_0/fix_quotation_expired_status.py +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -16,9 +16,7 @@ def execute(): and qo.valid_till < so.transaction_date""" # check if SO was created after quotation expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and exists({invalid_so_against_quo})""".format( - cond=cond, invalid_so_against_quo=invalid_so_against_quo - ) + f"""UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and exists({invalid_so_against_quo})""" ) valid_so_against_quo = """ @@ -31,7 +29,5 @@ def execute(): and qo.valid_till >= so.transaction_date""" # check if SO was created before quotation expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and exists({valid_so_against_quo})""".format( - cond=cond, valid_so_against_quo=valid_so_against_quo - ) + f"""UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and exists({valid_so_against_quo})""" ) diff --git a/erpnext/patches/v12_0/make_item_manufacturer.py b/erpnext/patches/v12_0/make_item_manufacturer.py index 3f233659d01..67d6f974005 100644 --- a/erpnext/patches/v12_0/make_item_manufacturer.py +++ b/erpnext/patches/v12_0/make_item_manufacturer.py @@ -30,8 +30,6 @@ def execute(): """ INSERT INTO `tabItem Manufacturer` (`name`, `item_code`, `manufacturer`, `manufacturer_part_no`, `creation`, `owner`) - VALUES {}""".format( - ", ".join(["%s"] * len(item_manufacturer)) - ), + VALUES {}""".format(", ".join(["%s"] * len(item_manufacturer))), tuple(item_manufacturer), ) diff --git a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py index 671dfd116d2..e6f522b8c94 100644 --- a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py +++ b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py @@ -16,7 +16,7 @@ def execute(): SET b.swift_number = ba.swift_number WHERE b.name = ba.bank """ ) - except Exception as e: + except Exception: frappe.log_error("Bank to Bank Account patch migration failed") frappe.reload_doc("accounts", "doctype", "bank_account") diff --git a/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py b/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py index 17c1966624e..67fe5db516e 100644 --- a/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py +++ b/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py @@ -29,12 +29,10 @@ def move_credit_limit_to_child_table(): fields = ", bypass_credit_limit_check_at_sales_order" credit_limit_records = frappe.db.sql( - """ - SELECT name, credit_limit {0} - FROM `tab{1}` where credit_limit > 0 - """.format( - fields, doctype - ), + f""" + SELECT name, credit_limit {fields} + FROM `tab{doctype}` where credit_limit > 0 + """, as_dict=1, ) # nosec diff --git a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py index c4c3b692e23..8ac1991d443 100644 --- a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py +++ b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py @@ -80,11 +80,9 @@ def execute(): for dt in doctypes: for d in frappe.db.sql( - """select name, parenttype, parent, item_code, item_tax_rate from `tab{0} Item` + f"""select name, parenttype, parent, item_code, item_tax_rate from `tab{dt} Item` where ifnull(item_tax_rate, '') not in ('', '{{}}') - and item_tax_template is NULL""".format( - dt - ), + and item_tax_template is NULL""", as_dict=1, ): item_tax_map = json.loads(d.item_tax_rate) @@ -145,13 +143,23 @@ def get_item_tax_template( if not parent_account: parent_account = frappe.db.get_value( "Account", - filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, + filters={ + "account_type": "Tax", + "root_type": "Liability", + "is_group": 0, + "company": company, + }, fieldname="parent_account", ) if not parent_account: parent_account = frappe.db.get_value( "Account", - filters={"account_type": "Tax", "root_type": "Liability", "is_group": 1, "company": company}, + filters={ + "account_type": "Tax", + "root_type": "Liability", + "is_group": 1, + "company": company, + }, ) filters = { "account_name": account_name, diff --git a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py index 71926107bd5..ffd8ddcf308 100644 --- a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py +++ b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py @@ -18,9 +18,7 @@ def execute(): WHERE `tab{child_doc}`.parent = `tab{parent_doc}`.name and `tab{parent_doc}`.distribution_id is not null and `tab{parent_doc}`.distribution_id != '' - """.format( - parent_doc=d, child_doc="Target Detail" - ) + """.format(parent_doc=d, child_doc="Target Detail") ) frappe.delete_doc("Report", "Sales Partner-wise Transaction Summary") diff --git a/erpnext/patches/v12_0/purchase_receipt_status.py b/erpnext/patches/v12_0/purchase_receipt_status.py index 3b828d69cb2..2869e2bcd0a 100644 --- a/erpnext/patches/v12_0/purchase_receipt_status.py +++ b/erpnext/patches/v12_0/purchase_receipt_status.py @@ -17,16 +17,14 @@ def execute(): if not affected_purchase_receipts: return - logger.info( - "purchase_receipt_status: begin patch, PR count: {}".format(len(affected_purchase_receipts)) - ) + logger.info(f"purchase_receipt_status: begin patch, PR count: {len(affected_purchase_receipts)}") frappe.reload_doc("stock", "doctype", "Purchase Receipt") frappe.reload_doc("stock", "doctype", "Purchase Receipt Item") for pr in affected_purchase_receipts: pr_name = pr[0] - logger.info("purchase_receipt_status: patching PR - {}".format(pr_name)) + logger.info(f"purchase_receipt_status: patching PR - {pr_name}") pr_doc = frappe.get_doc("Purchase Receipt", pr_name) diff --git a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py index e2a3887b9ac..dfc23669e0e 100644 --- a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py +++ b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py @@ -6,12 +6,8 @@ import frappe def _rename_single_field(**kwargs): count = frappe.db.sql( - "SELECT COUNT(*) FROM tabSingles WHERE doctype='{doctype}' AND field='{new_name}';".format( - **kwargs - ) - )[0][ - 0 - ] # nosec + "SELECT COUNT(*) FROM tabSingles WHERE doctype='{doctype}' AND field='{new_name}';".format(**kwargs) + )[0][0] # nosec if count == 0: frappe.db.sql( "UPDATE tabSingles SET field='{new_name}' WHERE doctype='{doctype}' AND field='{old_name}';".format( @@ -22,7 +18,5 @@ def _rename_single_field(**kwargs): def execute(): _rename_single_field(doctype="Bank Clearance", old_name="bank_account", new_name="account") - _rename_single_field( - doctype="Bank Clearance", old_name="bank_account_no", new_name="bank_account" - ) + _rename_single_field(doctype="Bank Clearance", old_name="bank_account_no", new_name="bank_account") frappe.reload_doc("Accounts", "doctype", "Bank Clearance") diff --git a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py index a4a85871ea2..69ddb603d7d 100644 --- a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py +++ b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py @@ -41,35 +41,29 @@ def execute(): cond = " AND parent_doc.update_stock = 1" data = frappe.db.sql( - """ SELECT parent_doc.name as name, child_doc.name as child_name + f""" SELECT parent_doc.name as name, child_doc.name as child_name FROM `tab{doctype}` parent_doc, `tab{doctype} Item` child_doc WHERE parent_doc.name = child_doc.parent AND parent_doc.docstatus < 2 AND child_doc.target_warehouse is not null AND child_doc.target_warehouse != '' AND child_doc.creation > '2020-04-16' {cond} - """.format( - doctype=doctype, cond=cond - ), + """, as_dict=1, ) if data: names = [d.child_name for d in data] frappe.db.sql( - """ UPDATE `tab{0} Item` set target_warehouse = null - WHERE name in ({1}) """.format( - doctype, ",".join(["%s"] * len(names)) - ), + """ UPDATE `tab{} Item` set target_warehouse = null + WHERE name in ({}) """.format(doctype, ",".join(["%s"] * len(names))), tuple(names), ) frappe.db.sql( """ UPDATE `tabPacked Item` set target_warehouse = null - WHERE parenttype = '{0}' and parent_detail_docname in ({1}) - """.format( - doctype, ",".join(["%s"] * len(names)) - ), + WHERE parenttype = '{}' and parent_detail_docname in ({}) + """.format(doctype, ",".join(["%s"] * len(names))), tuple(names), ) diff --git a/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py b/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py index d88593b4984..5d1cb64bf6b 100644 --- a/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py +++ b/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py @@ -2,17 +2,14 @@ import frappe def execute(): - frappe.reload_doc("selling", "doctype", "sales_order_item", force=True) frappe.reload_doc("buying", "doctype", "purchase_order_item", force=True) for doctype in ("Sales Order Item", "Purchase Order Item"): frappe.db.sql( - """ - UPDATE `tab{0}` + f""" + UPDATE `tab{doctype}` SET against_blanket_order = 1 WHERE ifnull(blanket_order, '') != '' - """.format( - doctype - ) + """ ) diff --git a/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py b/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py index 37af989549f..8f29fc888e0 100644 --- a/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py +++ b/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py @@ -4,6 +4,4 @@ import frappe def execute(): frappe.reload_doc("accounts", "doctype", "accounts_settings") - frappe.db.set_value( - "Accounts Settings", None, "automatically_process_deferred_accounting_entry", 1 - ) + frappe.db.set_value("Accounts Settings", None, "automatically_process_deferred_accounting_entry", 1) diff --git a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py index 562ebed757b..a5b9f43da8f 100644 --- a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py +++ b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py @@ -12,6 +12,5 @@ def execute(): fields=["sales_order", "sales_order_item"], filters={"sales_order": ("!=", ""), "sales_order_item": ("!=", "")}, ): - # update produced qty in sales order update_produced_qty_in_so_item(d.sales_order, d.sales_order_item) diff --git a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py index 2edf0f54fcf..e8207316baf 100644 --- a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py +++ b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py @@ -4,7 +4,6 @@ import frappe def execute(): - frappe.reload_doc("stock", "doctype", "delivery_note_item", force=True) frappe.reload_doc("stock", "doctype", "purchase_receipt_item", force=True) @@ -50,14 +49,12 @@ def execute(): Format => { 'document' : ['return_document_1','return_document_2'] }""" return_against_documents = frappe.db.sql( - """ + f""" SELECT return_against as document, name as return_document FROM `tab{doctype}` WHERE - is_return = 1 and docstatus = 1""".format( - doctype=doctype - ), + is_return = 1 and docstatus = 1""", as_dict=1, ) # nosec @@ -72,7 +69,7 @@ def execute(): return_document_map = defaultdict(list) detail_field = "purchase_receipt_item" if doctype == "Purchase Receipt" else "dn_detail" - child_doc = frappe.scrub("{0} Item".format(doctype)) + child_doc = frappe.scrub(f"{doctype} Item") frappe.reload_doc("stock", "doctype", child_doc) return_document_map = make_return_document_map(doctype, return_document_map) @@ -89,7 +86,8 @@ def execute(): for return_item in return_doc_items: for doc_item in doc_items: if ( - row_is_mappable(doc_item, return_item, detail_field) and doc_item.get("name") not in mapped + row_is_mappable(doc_item, return_item, detail_field) + and doc_item.get("name") not in mapped ): map_rows(doc_item, return_item, detail_field, doctype) mapped.append(doc_item.get("name")) diff --git a/erpnext/patches/v12_0/set_quotation_status.py b/erpnext/patches/v12_0/set_quotation_status.py index bebedd3a498..edfb4a4544a 100644 --- a/erpnext/patches/v12_0/set_quotation_status.py +++ b/erpnext/patches/v12_0/set_quotation_status.py @@ -2,7 +2,6 @@ import frappe def execute(): - frappe.db.sql( """ UPDATE `tabQuotation` set status = 'Open' where docstatus = 1 and status = 'Submitted' """ diff --git a/erpnext/patches/v12_0/set_task_status.py b/erpnext/patches/v12_0/set_task_status.py index 1c6654e57ac..27810d7abe9 100644 --- a/erpnext/patches/v12_0/set_task_status.py +++ b/erpnext/patches/v12_0/set_task_status.py @@ -10,7 +10,7 @@ def execute(): ) if property_setter_name: property_setter = frappe.get_doc("Property Setter", property_setter_name) - if not "Completed" in property_setter.value: + if "Completed" not in property_setter.value: property_setter.value = property_setter.value + "\nCompleted" property_setter.save() diff --git a/erpnext/patches/v12_0/stock_entry_enhancements.py b/erpnext/patches/v12_0/stock_entry_enhancements.py index db099a304cf..75749a49972 100644 --- a/erpnext/patches/v12_0/stock_entry_enhancements.py +++ b/erpnext/patches/v12_0/stock_entry_enhancements.py @@ -28,7 +28,6 @@ def create_stock_entry_types(): "Repack", "Send to Subcontractor", ]: - ste_type = frappe.get_doc({"doctype": "Stock Entry Type", "name": purpose, "purpose": purpose}) try: diff --git a/erpnext/patches/v12_0/update_bom_in_so_mr.py b/erpnext/patches/v12_0/update_bom_in_so_mr.py index 114f65d100e..88ac90d9f97 100644 --- a/erpnext/patches/v12_0/update_bom_in_so_mr.py +++ b/erpnext/patches/v12_0/update_bom_in_so_mr.py @@ -11,14 +11,12 @@ def execute(): condition = " and doc.per_ordered < 100 and doc.material_request_type = 'Manufacture'" frappe.db.sql( - """ UPDATE `tab{doc}` as doc, `tab{doc} Item` as child_doc, tabItem as item + f""" UPDATE `tab{doctype}` as doc, `tab{doctype} Item` as child_doc, tabItem as item SET child_doc.bom_no = item.default_bom WHERE child_doc.item_code = item.name and child_doc.docstatus < 2 and child_doc.parent = doc.name - and item.default_bom is not null and item.default_bom != '' {cond} - """.format( - doc=doctype, cond=condition - ) + and item.default_bom is not null and item.default_bom != '' {condition} + """ ) diff --git a/erpnext/patches/v12_0/update_due_date_in_gle.py b/erpnext/patches/v12_0/update_due_date_in_gle.py index a1c4f51ad01..693311001de 100644 --- a/erpnext/patches/v12_0/update_due_date_in_gle.py +++ b/erpnext/patches/v12_0/update_due_date_in_gle.py @@ -8,13 +8,11 @@ def execute(): frappe.reload_doc("accounts", "doctype", frappe.scrub(doctype)) frappe.db.sql( - """ UPDATE `tabGL Entry`, `tab{doctype}` + f""" UPDATE `tabGL Entry`, `tab{doctype}` SET `tabGL Entry`.due_date = `tab{doctype}`.due_date WHERE `tabGL Entry`.voucher_no = `tab{doctype}`.name and `tabGL Entry`.party is not null and `tabGL Entry`.voucher_type in ('Sales Invoice', 'Purchase Invoice', 'Journal Entry') - and `tabGL Entry`.account in (select name from `tabAccount` where account_type in ('Receivable', 'Payable'))""".format( # nosec - doctype=doctype - ) + and `tabGL Entry`.account in (select name from `tabAccount` where account_type in ('Receivable', 'Payable'))""" ) diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py index 398dd700eda..ea9a06bdcd8 100644 --- a/erpnext/patches/v12_0/update_is_cancelled_field.py +++ b/erpnext/patches/v12_0/update_is_cancelled_field.py @@ -17,20 +17,16 @@ def execute(): continue frappe.db.sql( - """ + f""" UPDATE `tab{doctype}` SET is_cancelled = 0 - where is_cancelled in ('', 'No') or is_cancelled is NULL""".format( - doctype=doctype - ) + where is_cancelled in ('', 'No') or is_cancelled is NULL""" ) frappe.db.sql( - """ + f""" UPDATE `tab{doctype}` SET is_cancelled = 1 - where is_cancelled = 'Yes'""".format( - doctype=doctype - ) + where is_cancelled = 'Yes'""" ) frappe.reload_doc(module, "doctype", frappe.scrub(doctype)) diff --git a/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py b/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py index 7dc0af9a1aa..26db58dffe8 100644 --- a/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py +++ b/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py @@ -19,8 +19,7 @@ def execute(): """ UPDATE `tabCustom Field` SET owner = 'Administrator' - WHERE fieldname = %s - AND dt IN (%s)""" - % ("%s", ", ".join(["%s"] * len(doclist))), # nosec - tuple([dimension.fieldname] + doclist), + WHERE fieldname = {} + AND dt IN ({})""".format("%s", ", ".join(["%s"] * len(doclist))), # nosec + tuple([dimension.fieldname, *doclist]), ) diff --git a/erpnext/patches/v12_0/update_pricing_rule_fields.py b/erpnext/patches/v12_0/update_pricing_rule_fields.py index 8da06b0bda0..f4eee85dd25 100644 --- a/erpnext/patches/v12_0/update_pricing_rule_fields.py +++ b/erpnext/patches/v12_0/update_pricing_rule_fields.py @@ -8,7 +8,6 @@ parentfield = {"item_code": "items", "item_group": "item_groups", "brand": "bran def execute(): - if not frappe.get_all("Pricing Rule", limit=1): return @@ -33,20 +32,16 @@ def execute(): child_doctype = doctype + " Item" frappe.db.sql( - """ UPDATE `tab{child_doctype}` SET pricing_rules = pricing_rule + f""" UPDATE `tab{child_doctype}` SET pricing_rules = pricing_rule WHERE docstatus < 2 and pricing_rule is not null and pricing_rule != '' - """.format( - child_doctype=child_doctype - ) + """ ) data = frappe.db.sql( - """ SELECT pricing_rule, name, parent, + f""" SELECT pricing_rule, name, parent, parenttype, creation, modified, docstatus, modified_by, owner, name - FROM `tab{child_doc}` where docstatus < 2 and pricing_rule is not null - and pricing_rule != ''""".format( - child_doc=child_doctype - ), + FROM `tab{child_doctype}` where docstatus < 2 and pricing_rule is not null + and pricing_rule != ''""", as_dict=1, ) @@ -73,9 +68,7 @@ def execute(): """ INSERT INTO `tabPricing Rule Detail` (`pricing_rule`, `child_docname`, `parent`, `parentfield`, `parenttype`, `creation`, `modified`, `docstatus`, `modified_by`, `owner`, `name`) - VALUES {values} """.format( - values=", ".join(["%s"] * len(values)) - ), + VALUES {values} """.format(values=", ".join(["%s"] * len(values))), tuple(values), ) @@ -116,8 +109,6 @@ def execute(): """ INSERT INTO `tab{doctype}` ({field}, parent, parentfield, parenttype, creation, modified, owner, modified_by, name) - VALUES {values} """.format( - doctype=doctype, field=field, values=", ".join(["%s"] * len(values)) - ), + VALUES {values} """.format(doctype=doctype, field=field, values=", ".join(["%s"] * len(values))), tuple(values), ) diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py index 7ad2bec8598..92fc7024d8d 100644 --- a/erpnext/patches/v13_0/add_bin_unique_constraint.py +++ b/erpnext/patches/v13_0/add_bin_unique_constraint.py @@ -21,7 +21,6 @@ def delete_broken_bins(): def delete_and_patch_duplicate_bins(): - duplicate_bins = frappe.db.sql( """ SELECT diff --git a/erpnext/patches/v13_0/add_cost_center_in_loans.py b/erpnext/patches/v13_0/add_cost_center_in_loans.py index e293cf2874e..f4c29150865 100644 --- a/erpnext/patches/v13_0/add_cost_center_in_loans.py +++ b/erpnext/patches/v13_0/add_cost_center_in_loans.py @@ -7,6 +7,4 @@ def execute(): for company in frappe.get_all("Company", pluck="name"): default_cost_center = frappe.db.get_value("Company", company, "cost_center") - frappe.qb.update(loan).set(loan.cost_center, default_cost_center).where( - loan.company == company - ).run() + frappe.qb.update(loan).set(loan.cost_center, default_cost_center).where(loan.company == company).run() diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py index ddbb7fd0f1b..fb7540e1a00 100644 --- a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -13,9 +13,7 @@ def execute(): return frappe.reload_doc("manufacturing", "doctype", "manufacturing_settings") - if cint( - frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order") - ): + if cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): return frappe.reload_doc("manufacturing", "doctype", "work_order") @@ -76,9 +74,7 @@ def execute(): def set_expense_account(doc): for row in doc.items: if row.is_finished_item and not row.expense_account: - row.expense_account = frappe.get_cached_value( - "Company", doc.company, "stock_adjustment_account" - ) + row.expense_account = frappe.get_cached_value("Company", doc.company, "stock_adjustment_account") def repost_stock_entry(doc): diff --git a/erpnext/patches/v13_0/add_po_to_global_search.py b/erpnext/patches/v13_0/add_po_to_global_search.py index 514cd343900..c07b6d91181 100644 --- a/erpnext/patches/v13_0/add_po_to_global_search.py +++ b/erpnext/patches/v13_0/add_po_to_global_search.py @@ -4,9 +4,7 @@ import frappe def execute(): global_search_settings = frappe.get_single("Global Search Settings") - if "Purchase Order" in ( - dt.document_type for dt in global_search_settings.allowed_in_global_search - ): + if "Purchase Order" in (dt.document_type for dt in global_search_settings.allowed_in_global_search): return global_search_settings.append("allowed_in_global_search", {"document_type": "Purchase Order"}) diff --git a/erpnext/patches/v13_0/agriculture_deprecation_warning.py b/erpnext/patches/v13_0/agriculture_deprecation_warning.py index 512444ef657..053710bfc98 100644 --- a/erpnext/patches/v13_0/agriculture_deprecation_warning.py +++ b/erpnext/patches/v13_0/agriculture_deprecation_warning.py @@ -2,7 +2,6 @@ import click def execute(): - click.secho( "Agriculture Domain is moved to a separate app and will be removed from ERPNext in version-14.\n" "Please install the app to continue using the Agriculture domain: https://github.com/frappe/agriculture", diff --git a/erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py b/erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py index 0b00188e6a8..5baec0c67da 100644 --- a/erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py +++ b/erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py @@ -2,7 +2,6 @@ import frappe def execute(): - # Erase all default item manufacturers that dont exist. item = frappe.qb.DocType("Item") manufacturer = frappe.qb.DocType("Manufacturer") diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py index 08ddbbf3375..d0bde56cb05 100644 --- a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py +++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py @@ -6,9 +6,7 @@ def execute(): params = set() # get all parameters from QI readings table - for (p,) in frappe.db.get_all( - "Quality Inspection Reading", fields=["specification"], as_list=True - ): + for (p,) in frappe.db.get_all("Quality Inspection Reading", fields=["specification"], as_list=True): params.add(p.strip()) # get all parameters from QI Template as some may be unused in QI diff --git a/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py b/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py index 1bac0fdbf0b..b3e9593bd2c 100644 --- a/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py +++ b/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py @@ -1,5 +1,4 @@ import json -from typing import List, Union import frappe @@ -42,7 +41,7 @@ def execute(): ) -def generate_fields_to_edit() -> List: +def generate_fields_to_edit() -> list: fields = [] for i in range(1, 13): fields.append(f"card_{i}_item") # fields like 'card_1_item', etc. @@ -50,7 +49,7 @@ def generate_fields_to_edit() -> List: return fields -def make_new_website_item(item: str) -> Union[str, None]: +def make_new_website_item(item: str) -> str | None: try: doc = frappe.get_doc("Item", item) web_item = make_website_item(doc) # returns [website_item.name, item_name] diff --git a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py index e8d0b593e6f..59ca96bd0bd 100644 --- a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py +++ b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py @@ -84,11 +84,9 @@ def execute(): move_table_multiselect_data(df) else: frappe.db.sql( # nosemgrep - """ + f""" UPDATE `tabWebsite Item` wi, `tabItem` i - SET wi.{0} = i.{0} + SET wi.{row.fieldname} = i.{row.fieldname} WHERE wi.item_code = i.item_code - """.format( - row.fieldname - ) + """ ) diff --git a/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py b/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py index 8e6bce66fe4..84b17a5515e 100644 --- a/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py +++ b/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py @@ -12,14 +12,12 @@ def execute(): count = 1 for d in accounting_dimensions: - if count % 2 == 0: insert_after_field = "dimension_col_break" else: insert_after_field = "accounting_dimensions_section" for doctype in ["Purchase Order", "Purchase Receipt", "Sales Order"]: - field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) if field: @@ -36,7 +34,7 @@ def execute(): try: create_custom_field(doctype, df, ignore_validate=True) frappe.clear_cache(doctype=doctype) - except Exception as e: + except Exception: pass count += 1 diff --git a/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py b/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py index 51ab0e8b65c..b9e703373c5 100644 --- a/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py +++ b/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py @@ -15,14 +15,12 @@ def execute(): count = 1 for d in accounting_dimensions: - if count % 2 == 0: insert_after_field = "dimension_col_break" else: insert_after_field = "accounting_dimensions_section" for doctype in ["POS Invoice", "POS Invoice Item"]: - field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) if field: diff --git a/erpnext/patches/v13_0/create_custom_field_for_finance_book.py b/erpnext/patches/v13_0/create_custom_field_for_finance_book.py index 2b8666d21b6..e0292098e7a 100644 --- a/erpnext/patches/v13_0/create_custom_field_for_finance_book.py +++ b/erpnext/patches/v13_0/create_custom_field_for_finance_book.py @@ -14,7 +14,7 @@ def execute(): "label": "For Income Tax", "fieldtype": "Check", "insert_after": "finance_book_name", - "description": "If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.", + "description": "If the asset is put to use for less than 180 days in the first year, the first year's depreciation rate will be reduced by 50%.", } ] } diff --git a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py index 66aae9a30af..d28f8c95f6d 100644 --- a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py +++ b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py @@ -8,9 +8,7 @@ from erpnext.regional.united_arab_emirates.setup import make_custom_fields def execute(): - company = frappe.get_all( - "Company", filters={"country": ["in", ["Saudi Arabia", "United Arab Emirates"]]} - ) + company = frappe.get_all("Company", filters={"country": ["in", ["Saudi Arabia", "United Arab Emirates"]]}) if not company: return diff --git a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py index 5cbd0b5fcb5..e748f52521a 100644 --- a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py +++ b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py @@ -9,9 +9,7 @@ def execute(): if not company: return - TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value( - "TaxJar Settings", "taxjar_create_transactions" - ) + TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox") diff --git a/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py b/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py index c53eb794378..46946a8a0e7 100644 --- a/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py +++ b/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py @@ -6,9 +6,7 @@ import frappe def execute(): - if frappe.db.exists("DocType", "Bank Reconciliation Detail") and frappe.db.exists( "DocType", "Bank Clearance Detail" ): - frappe.delete_doc("DocType", "Bank Reconciliation Detail", force=1) diff --git a/erpnext/patches/v13_0/enable_provisional_accounting.py b/erpnext/patches/v13_0/enable_provisional_accounting.py index dd18167b718..6edb423cfc4 100644 --- a/erpnext/patches/v13_0/enable_provisional_accounting.py +++ b/erpnext/patches/v13_0/enable_provisional_accounting.py @@ -11,6 +11,4 @@ def execute(): company.enable_perpetual_inventory_for_non_stock_items, ).set(company.default_provisional_account, company.service_received_but_not_billed).where( company.enable_perpetual_inventory_for_non_stock_items == 1 - ).where( - company.service_received_but_not_billed.isnotnull() - ).run() + ).where(company.service_received_but_not_billed.isnotnull()).run() diff --git a/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py b/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py index aeb8d8eb588..e305b375c7f 100644 --- a/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py +++ b/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py @@ -1,4 +1,4 @@ -from typing import List, NewType +from typing import NewType import frappe @@ -13,7 +13,7 @@ def execute(): create_repost_item_valuation(patched_stock_entry) -def find_broken_stock_entries() -> List[StockEntryCode]: +def find_broken_stock_entries() -> list[StockEntryCode]: period_closing_date = frappe.db.get_value( "Period Closing Voucher", {"docstatus": 1}, "posting_date", order_by="posting_date desc" ) diff --git a/erpnext/patches/v13_0/fix_invoice_statuses.py b/erpnext/patches/v13_0/fix_invoice_statuses.py index 253b425c58b..dc15a019a38 100644 --- a/erpnext/patches/v13_0/fix_invoice_statuses.py +++ b/erpnext/patches/v13_0/fix_invoice_statuses.py @@ -41,7 +41,7 @@ def execute(): ("Overdue", "Overdue and Discounted", "Partly Paid", "Partly Paid and Discounted"), ), "outstanding_amount": (">", 0), - "modified": (">", "2021-01-01") + "modified": (">", "2021-01-01"), # an assumption is being made that only invoices modified # after 2021 got affected as incorrectly overdue. # required for performance reasons. diff --git a/erpnext/patches/v13_0/healthcare_deprecation_warning.py b/erpnext/patches/v13_0/healthcare_deprecation_warning.py index c6fba59371c..3fff235f446 100644 --- a/erpnext/patches/v13_0/healthcare_deprecation_warning.py +++ b/erpnext/patches/v13_0/healthcare_deprecation_warning.py @@ -2,7 +2,6 @@ import click def execute(): - click.secho( "Healthcare Module is moved to a separate app and will be removed from ERPNext in version-14.\n" "Please install the app to continue using the module: https://github.com/frappe/healthcare", diff --git a/erpnext/patches/v13_0/hospitality_deprecation_warning.py b/erpnext/patches/v13_0/hospitality_deprecation_warning.py index 2708b2ccd3b..64166514562 100644 --- a/erpnext/patches/v13_0/hospitality_deprecation_warning.py +++ b/erpnext/patches/v13_0/hospitality_deprecation_warning.py @@ -2,7 +2,6 @@ import click def execute(): - click.secho( "Hospitality domain is moved to a separate app and will be removed from ERPNext in version-14.\n" "When upgrading to ERPNext version-14, please install the app to continue using the Hospitality domain: https://github.com/frappe/hospitality", diff --git a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py index 0235a621ce0..d25da7accb1 100644 --- a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py +++ b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py @@ -4,7 +4,6 @@ from erpnext.utilities.naming import set_by_naming_series def execute(): - stock_settings = frappe.get_doc("Stock Settings") set_by_naming_series( diff --git a/erpnext/patches/v13_0/move_branch_code_to_bank_account.py b/erpnext/patches/v13_0/move_branch_code_to_bank_account.py index 24061271484..79c79ffb592 100644 --- a/erpnext/patches/v13_0/move_branch_code_to_bank_account.py +++ b/erpnext/patches/v13_0/move_branch_code_to_bank_account.py @@ -6,13 +6,10 @@ import frappe def execute(): - frappe.reload_doc("accounts", "doctype", "bank_account") frappe.reload_doc("accounts", "doctype", "bank") - if frappe.db.has_column("Bank", "branch_code") and frappe.db.has_column( - "Bank Account", "branch_code" - ): + if frappe.db.has_column("Bank", "branch_code") and frappe.db.has_column("Bank Account", "branch_code"): frappe.db.sql( """UPDATE `tabBank` b, `tabBank Account` ba SET ba.branch_code = b.branch_code diff --git a/erpnext/patches/v13_0/non_profit_deprecation_warning.py b/erpnext/patches/v13_0/non_profit_deprecation_warning.py index 5b54b25a5bc..173664625ad 100644 --- a/erpnext/patches/v13_0/non_profit_deprecation_warning.py +++ b/erpnext/patches/v13_0/non_profit_deprecation_warning.py @@ -2,7 +2,6 @@ import click def execute(): - click.secho( "Non Profit Domain is moved to a separate app and will be removed from ERPNext in version-14.\n" "When upgrading to ERPNext version-14, please install the app to continue using the Non Profit domain: https://github.com/frappe/non_profit", diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py index a9b6df70985..dfa76a78dc3 100644 --- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -57,7 +57,11 @@ def execute(): for entry in opportunities: mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, "Minutes") frappe.db.set_value( - "Opportunity", entry.name, "first_response_time", mins_to_first_response, update_modified=False + "Opportunity", + entry.name, + "first_response_time", + mins_to_first_response, + update_modified=False, ) # commit after every 100 updates count += 1 diff --git a/erpnext/patches/v13_0/requeue_failed_reposts.py b/erpnext/patches/v13_0/requeue_failed_reposts.py index 752490da304..c7dfaa256a3 100644 --- a/erpnext/patches/v13_0/requeue_failed_reposts.py +++ b/erpnext/patches/v13_0/requeue_failed_reposts.py @@ -3,7 +3,6 @@ from frappe.utils import cstr def execute(): - reposts = frappe.get_all( "Repost Item Valuation", {"status": "Failed", "modified": [">", "2021-10-05"]}, diff --git a/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py b/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py index a4d70124492..40df73ee14b 100644 --- a/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py +++ b/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py @@ -20,9 +20,9 @@ def execute(): .run(as_dict=True) ) - frappe.qb.update(dn_item).inner_join(dn).on(dn.name == dn_item.parent).set( - dn_item.returned_qty, 0 - ).where(dn.is_return == 1).where(dn_item.returned_qty > 0).run() + frappe.qb.update(dn_item).inner_join(dn).on(dn.name == dn_item.parent).set(dn_item.returned_qty, 0).where( + dn.is_return == 1 + ).where(dn_item.returned_qty > 0).run() for d in dn_list: dn_doc = frappe.get_doc("Delivery Note", d.get("name")) diff --git a/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py index 1adf0d84538..be30fb12515 100644 --- a/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py +++ b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py @@ -28,9 +28,7 @@ def execute(): results = query.run(as_dict=True) for row in results: - so_item = frappe.get_value( - "Material Request Item", row.material_request_item, "sales_order_item" - ) + so_item = frappe.get_value("Material Request Item", row.material_request_item, "sales_order_item") frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item) if so_item: diff --git a/erpnext/patches/v13_0/shopify_deprecation_warning.py b/erpnext/patches/v13_0/shopify_deprecation_warning.py index 245d1a96250..274c35faafe 100644 --- a/erpnext/patches/v13_0/shopify_deprecation_warning.py +++ b/erpnext/patches/v13_0/shopify_deprecation_warning.py @@ -2,7 +2,6 @@ import click def execute(): - click.secho( "Shopify Integration is moved to a separate app and will be removed from ERPNext in version-14.\n" "Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations", diff --git a/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py b/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py index 35710a9bb4a..41c3358e76b 100644 --- a/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py +++ b/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py @@ -3,7 +3,6 @@ import frappe def execute(): - frappe.delete_doc("DocType", "Shopping Cart Settings", ignore_missing=True) frappe.delete_doc("DocType", "Products Settings", ignore_missing=True) frappe.delete_doc("DocType", "Supplier Item Group", ignore_missing=True) @@ -13,7 +12,6 @@ def execute(): def notify_users(): - click.secho( "Shopping cart and Product settings are merged into E-commerce settings.\n" "Checkout the documentation to learn more:" diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py index b69a408e65b..182711afda5 100644 --- a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py +++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py @@ -38,9 +38,7 @@ def execute(): if correct_sr_no == sle.serial_no: continue - frappe.db.set_value( - "Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False - ) + frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False) broken_serial_nos.update(serial_no_list) if not broken_serial_nos: diff --git a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py index bf98e9ea420..c073ba4fb18 100644 --- a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py +++ b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py @@ -14,6 +14,4 @@ def execute(): lr.payment_account, loan.payment_account ).set(lr.loan_account, loan.loan_account).set( lr.penalty_income_account, loan.penalty_income_account - ).where( - lr.docstatus < 2 - ).run() + ).where(lr.docstatus < 2).run() diff --git a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py index 3c6c5b5b75f..2c41a2976e9 100644 --- a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py +++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py @@ -29,7 +29,7 @@ def execute(): ) if not doc.planned_end_date: - planned_end_date = add_to_date(doc.planned_start_date, minutes=doc.lead_time) + add_to_date(doc.planned_start_date, minutes=doc.lead_time) doc.db_set("planned_end_date", doc.actual_start_date, update_modified=False) frappe.db.sql( diff --git a/erpnext/patches/v13_0/update_disbursement_account.py b/erpnext/patches/v13_0/update_disbursement_account.py index d6aba4720ee..c980f63a40b 100644 --- a/erpnext/patches/v13_0/update_disbursement_account.py +++ b/erpnext/patches/v13_0/update_disbursement_account.py @@ -2,7 +2,6 @@ import frappe def execute(): - frappe.reload_doc("loan_management", "doctype", "loan_type") frappe.reload_doc("loan_management", "doctype", "loan") diff --git a/erpnext/patches/v13_0/update_job_card_status.py b/erpnext/patches/v13_0/update_job_card_status.py index f2d12da119a..0f5469261fc 100644 --- a/erpnext/patches/v13_0/update_job_card_status.py +++ b/erpnext/patches/v13_0/update_job_card_status.py @@ -5,7 +5,6 @@ import frappe def execute(): - job_card = frappe.qb.DocType("Job Card") ( frappe.qb.update(job_card) diff --git a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py index 136d34e6ff1..183cd790ae6 100644 --- a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py +++ b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py @@ -13,8 +13,6 @@ def execute(): frappe.qb.update(mv).join(mvp).on(mvp.parent == mv.name).set( mv.maintenance_schedule, Coalesce(mvp.prevdoc_docname, "") - ).where( - (mv.maintenance_type == "Scheduled") & (mvp.prevdoc_docname.notnull()) & (mv.docstatus < 2) - ).run( + ).where((mv.maintenance_type == "Scheduled") & (mvp.prevdoc_docname.notnull()) & (mv.docstatus < 2)).run( as_dict=1 ) diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index 30db996e736..638db8a1573 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -13,7 +13,6 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ def execute(): - # Create a penalty account for loan types frappe.reload_doc("loan_management", "doctype", "loan_type") @@ -111,9 +110,7 @@ def execute(): loan_type_name = create_loan_type(loan, loan_type_name, penalty_account) # update loan type in loan - frappe.db.sql( - "UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name, loan.name) - ) + frappe.db.sql("UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name, loan.name)) loan_type = loan_type_name if loan_type_name not in updated_loan_types: diff --git a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py index 72e77fe2161..4bba7b96c83 100644 --- a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py +++ b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py @@ -4,7 +4,6 @@ from erpnext.stock.utils import get_bin def execute(): - wo = frappe.qb.DocType("Work Order") wo_item = frappe.qb.DocType("Work Order Item") diff --git a/erpnext/patches/v13_0/update_sane_transfer_against.py b/erpnext/patches/v13_0/update_sane_transfer_against.py index 45691e2ded7..0f58775d9cb 100644 --- a/erpnext/patches/v13_0/update_sane_transfer_against.py +++ b/erpnext/patches/v13_0/update_sane_transfer_against.py @@ -5,7 +5,5 @@ def execute(): bom = frappe.qb.DocType("BOM") ( - frappe.qb.update(bom) - .set(bom.transfer_material_against, "Work Order") - .where(bom.with_operations == 0) + frappe.qb.update(bom).set(bom.transfer_material_against, "Work Order").where(bom.with_operations == 0) ).run() diff --git a/erpnext/patches/v13_0/update_schedule_type_in_loans.py b/erpnext/patches/v13_0/update_schedule_type_in_loans.py index e5b5f643604..757758b5bf9 100644 --- a/erpnext/patches/v13_0/update_schedule_type_in_loans.py +++ b/erpnext/patches/v13_0/update_schedule_type_in_loans.py @@ -9,6 +9,6 @@ def execute(): loan_type.repayment_schedule_type, "Monthly as per repayment start date" ).where(loan_type.is_term_loan == 1).run() - frappe.qb.update(loan).set( - loan.repayment_schedule_type, "Monthly as per repayment start date" - ).where(loan.is_term_loan == 1).run() + frappe.qb.update(loan).set(loan.repayment_schedule_type, "Monthly as per repayment start date").where( + loan.is_term_loan == 1 + ).run() diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py index 84c683acd2c..8464fa8acbc 100644 --- a/erpnext/patches/v13_0/update_sla_enhancements.py +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -39,14 +39,16 @@ def execute(): for priority in priorities: if priority.parenttype == "Service Level Agreement": response_time = convert_to_seconds(priority.response_time, priority.response_time_period) - resolution_time = convert_to_seconds(priority.resolution_time, priority.resolution_time_period) + resolution_time = convert_to_seconds( + priority.resolution_time, priority.resolution_time_period + ) frappe.db.set_value( "Service Level Priority", priority.name, {"response_time": response_time, "resolution_time": resolution_time}, ) if priority.parenttype == "Service Level": - if not priority.parent in priority_dict: + if priority.parent not in priority_dict: priority_dict[priority.parent] = [] priority_dict[priority.parent].append(priority) @@ -86,7 +88,9 @@ def execute(): { "priority": priority.priority, "default_priority": priority.default_priority, - "response_time": convert_to_seconds(priority.response_time, priority.response_time_period), + "response_time": convert_to_seconds( + priority.response_time, priority.response_time_period + ), "resolution_time": convert_to_seconds( priority.resolution_time, priority.resolution_time_period ), diff --git a/erpnext/patches/v13_0/update_subscription.py b/erpnext/patches/v13_0/update_subscription.py index 66b3def79c4..96bd14621da 100644 --- a/erpnext/patches/v13_0/update_subscription.py +++ b/erpnext/patches/v13_0/update_subscription.py @@ -6,7 +6,6 @@ import frappe def execute(): - frappe.reload_doc("accounts", "doctype", "subscription") frappe.reload_doc("accounts", "doctype", "subscription_invoice") frappe.reload_doc("accounts", "doctype", "subscription_plan") diff --git a/erpnext/patches/v13_0/update_timesheet_changes.py b/erpnext/patches/v13_0/update_timesheet_changes.py index 02654c11d30..c54b4cbe103 100644 --- a/erpnext/patches/v13_0/update_timesheet_changes.py +++ b/erpnext/patches/v13_0/update_timesheet_changes.py @@ -20,12 +20,10 @@ def execute(): ) frappe.db.sql( - """UPDATE `tabTimesheet` - SET currency = '{0}', + f"""UPDATE `tabTimesheet` + SET currency = '{base_currency}', exchange_rate = 1.0, base_total_billable_amount = total_billable_amount, base_total_billed_amount = total_billed_amount, - base_total_costing_amount = total_costing_amount""".format( - base_currency - ) + base_total_costing_amount = total_costing_amount""" ) diff --git a/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py index 693d06dc1a0..280635c41bc 100644 --- a/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py +++ b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py @@ -2,7 +2,6 @@ import frappe def execute(): - doctype = "Stock Reconciliation Item" if not frappe.db.has_column(doctype, "current_serial_no"): @@ -11,6 +10,4 @@ def execute(): sr_item = frappe.qb.DocType(doctype) - ( - frappe.qb.update(sr_item).set(sr_item.current_serial_no, None).where(sr_item.current_qty == 0) - ).run() + (frappe.qb.update(sr_item).set(sr_item.current_serial_no, None).where(sr_item.current_qty == 0)).run() diff --git a/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py b/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py index 9b07ba846f4..a044bb0d9ab 100644 --- a/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py +++ b/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py @@ -7,20 +7,16 @@ import frappe def execute(): for doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"]: frappe.db.sql( - """ + f""" UPDATE `tab{doctype}` SET is_subcontracted = 0 - where is_subcontracted in ('', 'No') or is_subcontracted is null""".format( - doctype=doctype - ) + where is_subcontracted in ('', 'No') or is_subcontracted is null""" ) frappe.db.sql( - """ + f""" UPDATE `tab{doctype}` SET is_subcontracted = 1 - where is_subcontracted = 'Yes'""".format( - doctype=doctype - ) + where is_subcontracted = 'Yes'""" ) frappe.reload_doc(frappe.get_meta(doctype).module, "doctype", frappe.scrub(doctype)) diff --git a/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py b/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py index 607ef69538e..40a408b96a6 100644 --- a/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py +++ b/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py @@ -7,6 +7,4 @@ import frappe def execute(): for doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: tab = frappe.qb.DocType(doctype).as_("tab") - frappe.qb.update(tab).set(tab.is_old_subcontracting_flow, 1).where( - tab.is_subcontracted == 1 - ).run() + frappe.qb.update(tab).set(tab.is_old_subcontracting_flow, 1).where(tab.is_subcontracted == 1).run() diff --git a/erpnext/patches/v14_0/correct_asset_value_if_je_with_workflow.py b/erpnext/patches/v14_0/correct_asset_value_if_je_with_workflow.py index aededa2287d..dba27b4044d 100644 --- a/erpnext/patches/v14_0/correct_asset_value_if_je_with_workflow.py +++ b/erpnext/patches/v14_0/correct_asset_value_if_je_with_workflow.py @@ -39,9 +39,7 @@ def correct_value_for_assets_with_manual_depr_entries(): asset.opening_accumulated_depreciation.as_("opening_accumulated_depreciation"), Sum(gle.debit).as_("depr_amount"), ) - .where( - gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account) - ) + .where(gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)) .where(gle.debit != 0) .where(gle.is_cancelled == 0) .where(asset.docstatus == 1) @@ -80,9 +78,7 @@ def correct_value_for_assets_with_auto_depr(fb_name=None): asset.opening_accumulated_depreciation.as_("opening_accumulated_depreciation"), Sum(gle.debit).as_("depr_amount"), ) - .where( - gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account) - ) + .where(gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)) .where(gle.debit != 0) .where(gle.is_cancelled == 0) .where(asset.docstatus == 1) diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_in_reconciliation_tool.py b/erpnext/patches/v14_0/create_accounting_dimensions_in_reconciliation_tool.py new file mode 100644 index 00000000000..4466eaace8d --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_in_reconciliation_tool.py @@ -0,0 +1,8 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + create_accounting_dimensions_for_doctype, +) + + +def execute(): + create_accounting_dimensions_for_doctype(doctype="Payment Reconciliation") + create_accounting_dimensions_for_doctype(doctype="Payment Reconciliation Allocation") diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_in_subcontracting_doctypes.py b/erpnext/patches/v14_0/create_accounting_dimensions_in_subcontracting_doctypes.py index b349c07f6df..aedf2f176fd 100644 --- a/erpnext/patches/v14_0/create_accounting_dimensions_in_subcontracting_doctypes.py +++ b/erpnext/patches/v14_0/create_accounting_dimensions_in_subcontracting_doctypes.py @@ -12,7 +12,6 @@ def execute(): count = 1 for d in accounting_dimensions: - if count % 2 == 0: insert_after_field = "dimension_col_break" else: @@ -24,7 +23,6 @@ def execute(): "Subcontracting Receipt", "Subcontracting Receipt Item", ]: - field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) if field: diff --git a/erpnext/patches/v14_0/crm_ux_cleanup.py b/erpnext/patches/v14_0/crm_ux_cleanup.py index b2df36ff351..186ccf8bb3b 100644 --- a/erpnext/patches/v14_0/crm_ux_cleanup.py +++ b/erpnext/patches/v14_0/crm_ux_cleanup.py @@ -45,7 +45,8 @@ def add_calendar_event_for_leads(): "owner": d.lead_owner, "subject": ("Contact " + cstr(d.lead_name)), "description": ( - ("Contact " + cstr(d.lead_name)) + (("
    By: " + cstr(d.contact_by)) if d.contact_by else "") + ("Contact " + cstr(d.lead_name)) + + (("
    By: " + cstr(d.contact_by)) if d.contact_by else "") ), "starts_on": d.contact_date, "ends_on": d.ends_on, @@ -87,8 +88,6 @@ def add_calendar_event_for_opportunities(): } ) - event.append( - "event_participants", {"reference_doctype": "Opportunity", "reference_docname": d.name} - ) + event.append("event_participants", {"reference_doctype": "Opportunity", "reference_docname": d.name}) event.insert(ignore_permissions=True) diff --git a/erpnext/patches/v14_0/delete_agriculture_doctypes.py b/erpnext/patches/v14_0/delete_agriculture_doctypes.py index 8ec0c33090d..c8f9c93d279 100644 --- a/erpnext/patches/v14_0/delete_agriculture_doctypes.py +++ b/erpnext/patches/v14_0/delete_agriculture_doctypes.py @@ -13,9 +13,7 @@ def execute(): for report in reports: frappe.delete_doc("Report", report, ignore_missing=True, force=True) - dashboards = frappe.get_all( - "Dashboard", {"module": "agriculture", "is_standard": 1}, pluck="name" - ) + dashboards = frappe.get_all("Dashboard", {"module": "agriculture", "is_standard": 1}, pluck="name") for dashboard in dashboards: frappe.delete_doc("Dashboard", dashboard, ignore_missing=True, force=True) diff --git a/erpnext/patches/v14_0/delete_education_doctypes.py b/erpnext/patches/v14_0/delete_education_doctypes.py index 55b64eaabd8..1d636f99a7b 100644 --- a/erpnext/patches/v14_0/delete_education_doctypes.py +++ b/erpnext/patches/v14_0/delete_education_doctypes.py @@ -16,9 +16,7 @@ def execute(): for report in reports: frappe.delete_doc("Report", report, ignore_missing=True, force=True) - print_formats = frappe.get_all( - "Print Format", {"module": "education", "standard": "Yes"}, pluck="name" - ) + print_formats = frappe.get_all("Print Format", {"module": "education", "standard": "Yes"}, pluck="name") for print_format in print_formats: frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True) @@ -31,9 +29,7 @@ def execute(): for dashboard in dashboards: frappe.delete_doc("Dashboard", dashboard, ignore_missing=True, force=True) - dashboards = frappe.get_all( - "Dashboard Chart", {"module": "education", "is_standard": 1}, pluck="name" - ) + dashboards = frappe.get_all("Dashboard Chart", {"module": "education", "is_standard": 1}, pluck="name") for dashboard in dashboards: frappe.delete_doc("Dashboard Chart", dashboard, ignore_missing=True, force=True) diff --git a/erpnext/patches/v14_0/delete_healthcare_doctypes.py b/erpnext/patches/v14_0/delete_healthcare_doctypes.py index 896a4409507..b4dde5b5899 100644 --- a/erpnext/patches/v14_0/delete_healthcare_doctypes.py +++ b/erpnext/patches/v14_0/delete_healthcare_doctypes.py @@ -15,9 +15,7 @@ def execute(): for report in reports: frappe.delete_doc("Report", report, ignore_missing=True, force=True) - print_formats = frappe.get_all( - "Print Format", {"module": "healthcare", "standard": "Yes"}, pluck="name" - ) + print_formats = frappe.get_all("Print Format", {"module": "healthcare", "standard": "Yes"}, pluck="name") for print_format in print_formats: frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True) @@ -30,9 +28,7 @@ def execute(): for dashboard in dashboards: frappe.delete_doc("Dashboard", dashboard, ignore_missing=True, force=True) - dashboards = frappe.get_all( - "Dashboard Chart", {"module": "healthcare", "is_standard": 1}, pluck="name" - ) + dashboards = frappe.get_all("Dashboard Chart", {"module": "healthcare", "is_standard": 1}, pluck="name") for dashboard in dashboards: frappe.delete_doc("Dashboard Chart", dashboard, ignore_missing=True, force=True) diff --git a/erpnext/patches/v14_0/delete_hub_doctypes.py b/erpnext/patches/v14_0/delete_hub_doctypes.py index 08f4445fe2e..451880d5b75 100644 --- a/erpnext/patches/v14_0/delete_hub_doctypes.py +++ b/erpnext/patches/v14_0/delete_hub_doctypes.py @@ -2,7 +2,6 @@ import frappe def execute(): - doctypes = frappe.get_all("DocType", {"module": "Hub Node", "custom": 0}, pluck="name") for doctype in doctypes: frappe.delete_doc("DocType", doctype, ignore_missing=True) diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py index e9ea8b19e12..74a73fdce90 100644 --- a/erpnext/patches/v14_0/delete_non_profit_doctypes.py +++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py @@ -6,9 +6,7 @@ def execute(): frappe.delete_doc("Workspace", "Non Profit", ignore_missing=True, force=True) - print_formats = frappe.get_all( - "Print Format", {"module": "Non Profit", "standard": "Yes"}, pluck="name" - ) + print_formats = frappe.get_all("Print Format", {"module": "Non Profit", "standard": "Yes"}, pluck="name") for print_format in print_formats: frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True) diff --git a/erpnext/patches/v14_0/delete_orphaned_asset_movement_item_records.py b/erpnext/patches/v14_0/delete_orphaned_asset_movement_item_records.py new file mode 100644 index 00000000000..a1d7dc9b3d4 --- /dev/null +++ b/erpnext/patches/v14_0/delete_orphaned_asset_movement_item_records.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + # nosemgrep + frappe.db.sql( + """ + DELETE FROM `tabAsset Movement Item` + WHERE parent NOT IN (SELECT name FROM `tabAsset Movement`) + """ + ) diff --git a/erpnext/patches/v14_0/fix_crm_no_of_employees.py b/erpnext/patches/v14_0/fix_crm_no_of_employees.py index 268eb95732e..29c8b05c79f 100644 --- a/erpnext/patches/v14_0/fix_crm_no_of_employees.py +++ b/erpnext/patches/v14_0/fix_crm_no_of_employees.py @@ -15,12 +15,10 @@ def execute(): frappe.reload_doctype(doctype) for key, value in options.items(): frappe.db.sql( - """ + f""" update `tab{doctype}` set no_of_employees = %s where no_of_employees = %s - """.format( - doctype=doctype - ), + """, (value, key), ) diff --git a/erpnext/patches/v14_0/migrate_cost_center_allocations.py b/erpnext/patches/v14_0/migrate_cost_center_allocations.py index 48f4e6d9893..03e2b0289dc 100644 --- a/erpnext/patches/v14_0/migrate_cost_center_allocations.py +++ b/erpnext/patches/v14_0/migrate_cost_center_allocations.py @@ -44,8 +44,6 @@ def get_existing_cost_center_allocations(): cc_allocations = frappe._dict() for d in records: - cc_allocations.setdefault(d.name, frappe._dict()).setdefault( - d.cost_center, d.percentage_allocation - ) + cc_allocations.setdefault(d.name, frappe._dict()).setdefault(d.cost_center, d.percentage_allocation) return cc_allocations diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py index 72c8c074d2c..56694065bcf 100644 --- a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py @@ -152,7 +152,8 @@ def execute(): gl.star, ConstantColumn(1).as_("docstatus"), IfNull( - ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type + ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), + gl.voucher_type, ).as_("against_voucher_type"), IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_( "against_voucher_no" diff --git a/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py b/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py new file mode 100644 index 00000000000..cb39a9280e4 --- /dev/null +++ b/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + cancelled_asset_capitalizations = frappe.get_all( + "Asset Capitalization", + filters={"docstatus": 2}, + fields=["name", "target_asset"], + ) + for asset_capitalization in cancelled_asset_capitalizations: + frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None) diff --git a/erpnext/patches/v14_0/remove_hr_and_payroll_modules.py b/erpnext/patches/v14_0/remove_hr_and_payroll_modules.py index 4d01fcb4216..3d01f1422e4 100644 --- a/erpnext/patches/v14_0/remove_hr_and_payroll_modules.py +++ b/erpnext/patches/v14_0/remove_hr_and_payroll_modules.py @@ -33,9 +33,7 @@ def execute(): ]: frappe.delete_doc("Report", report, ignore_missing=True, force=True) - doctypes = frappe.get_all( - "DocType", {"module": ("in", ["HR", "Payroll"]), "custom": 0}, pluck="name" - ) + doctypes = frappe.get_all("DocType", {"module": ("in", ["HR", "Payroll"]), "custom": 0}, pluck="name") for doctype in doctypes: frappe.delete_doc("DocType", doctype, ignore_missing=True, force=True) @@ -51,9 +49,7 @@ def execute(): frappe.delete_doc("User Type", "Employee Self Service", ignore_missing=True, force=True) for dt in ["Web Form", "Dashboard", "Dashboard Chart", "Number Card"]: - records = frappe.get_all( - dt, {"module": ("in", ["HR", "Payroll"]), "is_standard": 1}, pluck="name" - ) + records = frappe.get_all(dt, {"module": ("in", ["HR", "Payroll"]), "is_standard": 1}, pluck="name") for record in records: frappe.delete_doc(dt, record, ignore_missing=True, force=True) diff --git a/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py b/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py new file mode 100644 index 00000000000..f0b618f32d4 --- /dev/null +++ b/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + if not frappe.db.exists("BOM", {"docstatus": 1}): + return + + # Added is_stock_item to handle Read Only based on condition for the rate field + frappe.db.sql( + """ + UPDATE + `tabBOM Item` boi, + `tabItem` i + SET + boi.is_stock_item = i.is_stock_item + WHERE + boi.item_code = i.name + """ + ) diff --git a/erpnext/patches/v14_0/update_asset_value_for_manual_depr_entries.py b/erpnext/patches/v14_0/update_asset_value_for_manual_depr_entries.py index 5d7b5cf19c1..9bdcda09d3c 100644 --- a/erpnext/patches/v14_0/update_asset_value_for_manual_depr_entries.py +++ b/erpnext/patches/v14_0/update_asset_value_for_manual_depr_entries.py @@ -17,9 +17,7 @@ def execute(): .join(company) .on(company.name == asset.company) .select(Sum(gle.debit).as_("value"), asset.name.as_("asset_name")) - .where( - gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account) - ) + .where(gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)) .where(gle.debit != 0) .where(gle.is_cancelled == 0) .where(asset.docstatus == 1) @@ -31,8 +29,4 @@ def execute(): asset_total_depr_value_map.asset_name == asset.name ).set( asset.value_after_depreciation, asset.value_after_depreciation - asset_total_depr_value_map.value - ).where( - asset.docstatus == 1 - ).where( - asset.calculate_depreciation == 0 - ).run() + ).where(asset.docstatus == 1).where(asset.calculate_depreciation == 0).run() diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py index 2c842814839..cfc29c87fa1 100644 --- a/erpnext/patches/v14_0/update_closing_balances.py +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -22,7 +22,6 @@ def execute(): 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) diff --git a/erpnext/patches/v14_0/update_flag_for_return_invoices.py b/erpnext/patches/v14_0/update_flag_for_return_invoices.py new file mode 100644 index 00000000000..2136d55a786 --- /dev/null +++ b/erpnext/patches/v14_0/update_flag_for_return_invoices.py @@ -0,0 +1,70 @@ +from frappe import qb + + +def execute(): + # Set "update_outstanding_for_self" flag in Credit/Debit Notes + # Fetch Credit/Debit notes that does have 'return_against' but still post ledger entries against themselves. + + gle = qb.DocType("GL Entry") + + # Use hardcoded 'creation' date to isolate Credit/Debit notes created post v14 backport + # https://github.com/frappe/erpnext/pull/39497 + creation_date = "2024-01-25" + + si = qb.DocType("Sales Invoice") + + # unset flag, as migration would have set it for all records, as the field was introduced with default '1' + qb.update(si).set(si.update_outstanding_for_self, False).run() + + if cr_notes := ( + qb.from_(si) + .select(si.name) + .where( + (si.creation.gte(creation_date)) + & (si.docstatus == 1) + & (si.is_return.eq(True)) + & (si.return_against.notnull()) + ) + .run() + ): + cr_notes = [x[0] for x in cr_notes] + if docs_that_require_update := ( + qb.from_(gle) + .select(gle.voucher_no) + .distinct() + .where((gle.voucher_no.isin(cr_notes)) & (gle.voucher_no == gle.against_voucher)) + .run() + ): + docs_that_require_update = [x[0] for x in docs_that_require_update] + qb.update(si).set(si.update_outstanding_for_self, True).where( + si.name.isin(docs_that_require_update) + ).run() + + pi = qb.DocType("Purchase Invoice") + + # unset flag, as migration would have set it for all records, as the field was introduced with default '1' + qb.update(pi).set(pi.update_outstanding_for_self, False).run() + + if dr_notes := ( + qb.from_(pi) + .select(pi.name) + .where( + (pi.creation.gte(creation_date)) + & (pi.docstatus == 1) + & (pi.is_return.eq(True)) + & (pi.return_against.notnull()) + ) + .run() + ): + dr_notes = [x[0] for x in dr_notes] + if docs_that_require_update := ( + qb.from_(gle) + .select(gle.voucher_no) + .distinct() + .where((gle.voucher_no.isin(dr_notes)) & (gle.voucher_no == gle.against_voucher)) + .run() + ): + docs_that_require_update = [x[0] for x in docs_that_require_update] + qb.update(pi).set(pi.update_outstanding_for_self, True).where( + pi.name.isin(docs_that_require_update) + ).run() diff --git a/erpnext/patches/v14_0/update_partial_tds_fields.py b/erpnext/patches/v14_0/update_partial_tds_fields.py index 5ccc2dc3aa2..991201395f3 100644 --- a/erpnext/patches/v14_0/update_partial_tds_fields.py +++ b/erpnext/patches/v14_0/update_partial_tds_fields.py @@ -14,32 +14,20 @@ def execute(): frappe.qb.update(purchase_invoice).set( purchase_invoice.tax_withholding_net_total, purchase_invoice.net_total - ).set( - purchase_invoice.base_tax_withholding_net_total, purchase_invoice.base_net_total - ).where( + ).set(purchase_invoice.base_tax_withholding_net_total, purchase_invoice.base_net_total).where( purchase_invoice.company == company.name - ).where( - purchase_invoice.apply_tds == 1 - ).where( + ).where(purchase_invoice.apply_tds == 1).where( purchase_invoice.posting_date >= fiscal_year_details.year_start_date - ).where( - purchase_invoice.docstatus == 1 - ).run() + ).where(purchase_invoice.docstatus == 1).run() purchase_order = frappe.qb.DocType("Purchase Order") frappe.qb.update(purchase_order).set( purchase_order.tax_withholding_net_total, purchase_order.net_total - ).set( - purchase_order.base_tax_withholding_net_total, purchase_order.base_net_total - ).where( + ).set(purchase_order.base_tax_withholding_net_total, purchase_order.base_net_total).where( purchase_order.company == company.name - ).where( - purchase_order.apply_tds == 1 - ).where( + ).where(purchase_order.apply_tds == 1).where( purchase_order.transaction_date >= fiscal_year_details.year_start_date - ).where( - purchase_order.docstatus == 1 - ).run() + ).where(purchase_order.docstatus == 1).run() except FiscalYearError: pass diff --git a/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py b/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py new file mode 100644 index 00000000000..ca126a40a40 --- /dev/null +++ b/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + frappe.db.sql( + """ + UPDATE `tabStock Ledger Entry` + SET posting_datetime = DATE_FORMAT(timestamp(posting_date, posting_time), '%Y-%m-%d %H:%i:%s') + """ + ) + + drop_indexes() + + +def drop_indexes(): + if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"): + return + + frappe.db.sql_ddl("ALTER TABLE `tabStock Ledger Entry` DROP INDEX `posting_sort_index`") diff --git a/erpnext/patches/v14_0/update_total_asset_cost_field.py b/erpnext/patches/v14_0/update_total_asset_cost_field.py new file mode 100644 index 00000000000..57cf71b6134 --- /dev/null +++ b/erpnext/patches/v14_0/update_total_asset_cost_field.py @@ -0,0 +1,17 @@ +import frappe + + +def execute(): + asset = frappe.qb.DocType("Asset") + frappe.qb.update(asset).set(asset.total_asset_cost, asset.gross_purchase_amount).run() + + asset_repair_list = frappe.db.get_all( + "Asset Repair", + filters={"docstatus": 1, "repair_status": "Completed", "capitalize_repair_cost": 1}, + fields=["asset", "repair_cost"], + ) + + for asset_repair in asset_repair_list: + frappe.qb.update(asset).set( + asset.total_asset_cost, asset.total_asset_cost + asset_repair.repair_cost + ).where(asset.name == asset_repair.asset).run() diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js index 59f808a3158..e0d7af001c0 100644 --- a/erpnext/portal/doctype/homepage/homepage.js +++ b/erpnext/portal/doctype/homepage/homepage.js @@ -1,30 +1,30 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Homepage', { - setup: function(frm) { - frm.fields_dict["products"].grid.get_field("item").get_query = function() { +frappe.ui.form.on("Homepage", { + setup: function (frm) { + frm.fields_dict["products"].grid.get_field("item").get_query = function () { return { - filters: {'published': 1} - } - } + filters: { published: 1 }, + }; + }; }, - refresh: function(frm) { - frm.add_custom_button(__('Set Meta Tags'), () => { - frappe.utils.set_meta_tag('home'); + refresh: function (frm) { + frm.add_custom_button(__("Set Meta Tags"), () => { + frappe.utils.set_meta_tag("home"); }); - frm.add_custom_button(__('Customize Homepage Sections'), () => { - frappe.set_route('List', 'Homepage Section', 'List'); + frm.add_custom_button(__("Customize Homepage Sections"), () => { + frappe.set_route("List", "Homepage Section", "List"); }); }, }); -frappe.ui.form.on('Homepage Featured Product', { - view: function(frm, cdt, cdn) { - var child= locals[cdt][cdn]; +frappe.ui.form.on("Homepage Featured Product", { + view: function (frm, cdt, cdn) { + var child = locals[cdt][cdn]; if (child.item_code && child.route) { - window.open('/' + child.route, '_blank'); + window.open("/" + child.route, "_blank"); } - } + }, }); diff --git a/erpnext/portal/doctype/homepage/homepage.py b/erpnext/portal/doctype/homepage/homepage.py index 0d2e3607881..be90776cca3 100644 --- a/erpnext/portal/doctype/homepage/homepage.py +++ b/erpnext/portal/doctype/homepage/homepage.py @@ -20,7 +20,6 @@ class Homepage(Document): filters={"published": 1}, limit=3, ): - doc = frappe.get_doc("Website Item", d.name) if not doc.route: # set missing route diff --git a/erpnext/portal/doctype/homepage_section/homepage_section.js b/erpnext/portal/doctype/homepage_section/homepage_section.js index 68859eb10b6..270b73aafdf 100644 --- a/erpnext/portal/doctype/homepage_section/homepage_section.js +++ b/erpnext/portal/doctype/homepage_section/homepage_section.js @@ -1,6 +1,4 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Homepage Section', { - -}); +frappe.ui.form.on("Homepage Section", {}); diff --git a/erpnext/projects/doctype/activity_cost/activity_cost.js b/erpnext/projects/doctype/activity_cost/activity_cost.js index 2d22caad8e2..d47b574265f 100644 --- a/erpnext/projects/doctype/activity_cost/activity_cost.js +++ b/erpnext/projects/doctype/activity_cost/activity_cost.js @@ -1 +1 @@ -cur_frm.add_fetch('employee', 'employee_name', 'employee_name'); +cur_frm.add_fetch("employee", "employee_name", "employee_name"); diff --git a/erpnext/projects/doctype/activity_type/activity_type.js b/erpnext/projects/doctype/activity_type/activity_type.js index f1ba882812e..b620cdd6c64 100644 --- a/erpnext/projects/doctype/activity_type/activity_type.js +++ b/erpnext/projects/doctype/activity_type/activity_type.js @@ -1,12 +1,15 @@ frappe.ui.form.on("Activity Type", { - onload: function(frm) { - frm.set_currency_labels(["billing_rate", "costing_rate"], frappe.defaults.get_global_default('currency')); + onload: function (frm) { + frm.set_currency_labels( + ["billing_rate", "costing_rate"], + frappe.defaults.get_global_default("currency") + ); }, - refresh: function(frm) { - frm.add_custom_button(__("Activity Cost per Employee"), function() { - frappe.route_options = {"activity_type": frm.doc.name}; + refresh: function (frm) { + frm.add_custom_button(__("Activity Cost per Employee"), function () { + frappe.route_options = { activity_type: frm.doc.name }; frappe.set_route("List", "Activity Cost"); }); - } + }, }); diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 8f7437fef8a..3ea8189feec 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -3,16 +3,16 @@ frappe.ui.form.on("Project", { setup(frm) { frm.make_methods = { - 'Timesheet': () => { + Timesheet: () => { open_form(frm, "Timesheet", "Timesheet Detail", "time_logs"); }, - 'Purchase Order': () => { + "Purchase Order": () => { open_form(frm, "Purchase Order", "Purchase Order Item", "items"); }, - 'Purchase Receipt': () => { + "Purchase Receipt": () => { open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items"); }, - 'Purchase Invoice': () => { + "Purchase Invoice": () => { open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items"); }, }; @@ -22,23 +22,31 @@ frappe.ui.form.on("Project", { so.get_route_options_for_new_doc = () => { if (frm.is_new()) return; return { - "customer": frm.doc.customer, - "project_name": frm.doc.name + customer: frm.doc.customer, + project_name: frm.doc.name, }; }; - frm.set_query('customer', 'erpnext.controllers.queries.customer_query'); + frm.set_query("customer", "erpnext.controllers.queries.customer_query"); frm.set_query("user", "users", function () { return { - query: "erpnext.projects.doctype.project.project.get_users_for_project" + query: "erpnext.projects.doctype.project.project.get_users_for_project", + }; + }); + + frm.set_query("department", function (doc) { + return { + filters: { + company: doc.company, + }, }; }); // sales order - frm.set_query('sales_order', function () { + frm.set_query("sales_order", function () { var filters = { - 'project': ["in", frm.doc.__islocal ? [""] : [frm.doc.name, ""]] + project: ["in", frm.doc.__islocal ? [""] : [frm.doc.name, ""]], }; if (frm.doc.customer) { @@ -46,7 +54,7 @@ frappe.ui.form.on("Project", { } return { - filters: filters + filters: filters, }; }); }, @@ -57,108 +65,133 @@ frappe.ui.form.on("Project", { } else { frm.add_web_link("/projects?project=" + encodeURIComponent(frm.doc.name)); - frm.trigger('show_dashboard'); + frm.trigger("show_dashboard"); } frm.trigger("set_custom_buttons"); }, - set_custom_buttons: function(frm) { + set_custom_buttons: function (frm) { if (!frm.is_new()) { - frm.add_custom_button(__('Duplicate Project with Tasks'), () => { - frm.events.create_duplicate(frm); - }, __("Actions")); + frm.add_custom_button( + __("Duplicate Project with Tasks"), + () => { + frm.events.create_duplicate(frm); + }, + __("Actions") + ); - frm.add_custom_button(__('Update Total Purchase Cost'), () => { - frm.events.update_total_purchase_cost(frm); - }, __("Actions")); + frm.add_custom_button( + __("Update Total Purchase Cost"), + () => { + frm.events.update_total_purchase_cost(frm); + }, + __("Actions") + ); frm.trigger("set_project_status_button"); - if (frappe.model.can_read("Task")) { - frm.add_custom_button(__("Gantt Chart"), function () { - frappe.route_options = { - "project": frm.doc.name - }; - frappe.set_route("List", "Task", "Gantt"); - }, __("View")); + frm.add_custom_button( + __("Gantt Chart"), + function () { + frappe.route_options = { + project: frm.doc.name, + }; + frappe.set_route("List", "Task", "Gantt"); + }, + __("View") + ); - frm.add_custom_button(__("Kanban Board"), () => { - frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', { - project: frm.doc.name - }).then(() => { - frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name); - }); - }, __("View")); + frm.add_custom_button( + __("Kanban Board"), + () => { + frappe + .call( + "erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists", + { + project: frm.doc.name, + } + ) + .then(() => { + frappe.set_route("List", "Task", "Kanban", frm.doc.project_name); + }); + }, + __("View") + ); } } - - }, - update_total_purchase_cost: function(frm) { + update_total_purchase_cost: function (frm) { frappe.call({ method: "erpnext.projects.doctype.project.project.recalculate_project_total_purchase_cost", - args: {project: frm.doc.name}, + args: { project: frm.doc.name }, freeze: true, - freeze_message: __('Recalculating Purchase Cost against this Project...'), - callback: function(r) { + freeze_message: __("Recalculating Purchase Cost against this Project..."), + callback: function (r) { if (r && !r.exc) { - frappe.msgprint(__('Total Purchase Cost has been updated')); + frappe.msgprint(__("Total Purchase Cost has been updated")); frm.refresh(); } - } - + }, }); }, - set_project_status_button: function(frm) { - frm.add_custom_button(__('Set Project Status'), () => { - let d = new frappe.ui.Dialog({ - "title": __("Set Project Status"), - "fields": [ - { - "fieldname": "status", - "fieldtype": "Select", - "label": "Status", - "reqd": 1, - "options": "Completed\nCancelled", + set_project_status_button: function (frm) { + frm.add_custom_button( + __("Set Project Status"), + () => { + let d = new frappe.ui.Dialog({ + title: __("Set Project Status"), + fields: [ + { + fieldname: "status", + fieldtype: "Select", + label: "Status", + reqd: 1, + options: "Completed\nCancelled", + }, + ], + primary_action: function () { + frm.events.set_status(frm, d.get_values().status); + d.hide(); }, - ], - primary_action: function() { - frm.events.set_status(frm, d.get_values().status); - d.hide(); - }, - primary_action_label: __("Set Project Status") - }).show(); - }, __("Actions")); + primary_action_label: __("Set Project Status"), + }).show(); + }, + __("Actions") + ); }, - create_duplicate: function(frm) { - return new Promise(resolve => { - frappe.prompt('Project Name', (data) => { - frappe.xcall('erpnext.projects.doctype.project.project.create_duplicate_project', - { + create_duplicate: function (frm) { + return new Promise((resolve) => { + frappe.prompt("Project Name", (data) => { + frappe + .xcall("erpnext.projects.doctype.project.project.create_duplicate_project", { prev_doc: frm.doc, - project_name: data.value - }).then(() => { - frappe.set_route('Form', "Project", data.value); - frappe.show_alert(__("Duplicate project has been created")); - }); + project_name: data.value, + }) + .then(() => { + frappe.set_route("Form", "Project", data.value); + frappe.show_alert(__("Duplicate project has been created")); + }); resolve(); }); }); }, - set_status: function(frm, status) { - frappe.confirm(__('Set Project and all Tasks to status {0}?', [status.bold()]), () => { - frappe.xcall('erpnext.projects.doctype.project.project.set_project_status', - {project: frm.doc.name, status: status}).then(() => { - frm.reload_doc(); - }); + set_status: function (frm, status) { + frappe.confirm(__("Set Project and all Tasks to status {0}?", [status.bold()]), () => { + frappe + .xcall("erpnext.projects.doctype.project.project.set_project_status", { + project: frm.doc.name, + status: status, + }) + .then(() => { + frm.reload_doc(); + }); }); }, - }); function open_form(frm, doctype, child_doctype, parentfield) { @@ -176,5 +209,4 @@ function open_form(frm, doctype, child_doctype, parentfield) { frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); }); - } diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 8238af0ad16..2c74088a19e 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -452,7 +452,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2023-02-14 04:54:25.819620", + "modified": "2024-04-24 10:56:16.001032", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -487,6 +487,15 @@ "role": "Projects Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "report": 1, + "role": "Employee", + "select": 1, + "share": 1 } ], "quick_entry": 1, diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 748c755d377..29a288ef671 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -17,7 +17,7 @@ from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday class Project(Document): def get_feed(self): - return "{0}: {1}".format(_(self.status), frappe.safe_decode(self.project_name)) + return f"{_(self.status)}: {frappe.safe_decode(self.project_name)}" def onload(self): self.set_onload( @@ -49,7 +49,6 @@ class Project(Document): Copy tasks from template """ if self.project_template and not frappe.db.get_all("Task", dict(project=self.name), limit=1): - # has a template, and no loaded tasks, so lets create if not self.expected_start_date: # project starts today @@ -87,6 +86,7 @@ class Project(Document): is_group=task_details.is_group, color=task_details.color, template_task=task_details.name, + priority=task_details.priority, ) ).insert() @@ -119,7 +119,9 @@ class Project(Document): for child_task in template_task.get("depends_on"): if project_template_map and project_template_map.get(child_task.task): project_task.reload() # reload, as it might have been updated in the previous iteration - project_task.append("depends_on", {"task": project_template_map.get(child_task.task).name}) + project_task.append( + "depends_on", {"task": project_template_map.get(child_task.task).name} + ) project_task.save() def check_for_parent_tasks(self, template_task, project_task, project_tasks): @@ -273,7 +275,7 @@ class Project(Document): frappe.db.set_value("Project", new_name, "copied_from", new_name) def send_welcome_email(self): - url = get_url("/project/?name={0}".format(self.name)) + url = get_url(f"/project/?name={self.name}") messages = ( _("You have been invited to collaborate on the project: {0}").format(self.name), url, @@ -288,7 +290,9 @@ class Project(Document): for user in self.users: if user.welcome_email_sent == 0: frappe.sendmail( - user.user, subject=_("Project Collaboration Invitation"), content=content.format(*messages) + user.user, + subject=_("Project Collaboration Invitation"), + content=content.format(*messages), ) user.welcome_email_sent = 1 @@ -307,9 +311,7 @@ def get_timeline_data(doctype, name): ) -def get_project_list( - doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" -): +def get_project_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"): meta = frappe.get_meta(doctype) if not filters: filters = [] @@ -593,9 +595,7 @@ def update_project_sales_billing(): return # Else simply fallback to Daily - exists_query = ( - "(SELECT 1 from `tab{doctype}` where docstatus = 1 and project = `tabProject`.name)" - ) + exists_query = "(SELECT 1 from `tab{doctype}` where docstatus = 1 and project = `tabProject`.name)" project_map = {} for project_details in frappe.db.sql( """ @@ -638,7 +638,7 @@ def set_project_status(project, status): """ set status for project and all related tasks """ - if not status in ("Completed", "Cancelled"): + if status not in ("Completed", "Cancelled"): frappe.throw(_("Status must be Cancelled or Completed")) project = frappe.get_doc("Project", project) @@ -658,9 +658,7 @@ def get_holiday_list(company=None): holiday_list = frappe.get_cached_value("Company", company, "default_holiday_list") if not holiday_list: frappe.throw( - _("Please set a default Holiday List for Company {0}").format( - frappe.bold(get_default_company()) - ) + _("Please set a default Holiday List for Company {0}").format(frappe.bold(get_default_company())) ) return holiday_list diff --git a/erpnext/projects/doctype/project/project_list.js b/erpnext/projects/doctype/project/project_list.js index 5ad4bb7f93b..1503b1ee5d3 100644 --- a/erpnext/projects/doctype/project/project_list.js +++ b/erpnext/projects/doctype/project/project_list.js @@ -1,11 +1,11 @@ -frappe.listview_settings['Project'] = { +frappe.listview_settings["Project"] = { add_fields: ["status", "priority", "is_active", "percent_complete", "expected_end_date", "project_name"], - filters:[["status","=", "Open"]], - get_indicator: function(doc) { - if(doc.status=="Open" && doc.percent_complete) { + filters: [["status", "=", "Open"]], + get_indicator: function (doc) { + if (doc.status == "Open" && doc.percent_complete) { return [__("{0}%", [cint(doc.percent_complete)]), "orange", "percent_complete,>,0|status,=,Open"]; } else { return [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; } - } + }, }; diff --git a/erpnext/projects/doctype/project/project_timesheet.js b/erpnext/projects/doctype/project/project_timesheet.js index 32df04ff98b..bc6de3134da 100644 --- a/erpnext/projects/doctype/project/project_timesheet.js +++ b/erpnext/projects/doctype/project/project_timesheet.js @@ -1,53 +1,63 @@ - -QUnit.test("test project", function(assert) { +QUnit.test("test project", function (assert) { assert.expect(6); let done = assert.async(); - var task_title = ["Documentation","Implementation","Testing"]; + var task_title = ["Documentation", "Implementation", "Testing"]; // To create a timesheet with different tasks and costs - let timesheet = (title,start_time,end_time,bill_rate,cost_rate) => { + let timesheet = (title, start_time, end_time, bill_rate, cost_rate) => { return frappe.run_serially([ - () => frappe.db.get_value('Task', {'subject': title}, 'name'), + () => frappe.db.get_value("Task", { subject: title }, "name"), (task) => { // Creating timesheet for a project - return frappe.tests.make('Timesheet', [ - {time_logs:[ - [ - {activity_type: 'Communication'}, - {from_time: start_time}, - {to_time: end_time}, - {hours: 2}, - {project: 'Test App'}, - {task: task.name}, - {billable: '1'}, - {billing_rate: bill_rate}, - {costing_rate: cost_rate} - ] - ]} + return frappe.tests.make("Timesheet", [ + { + time_logs: [ + [ + { activity_type: "Communication" }, + { from_time: start_time }, + { to_time: end_time }, + { hours: 2 }, + { project: "Test App" }, + { task: task.name }, + { billable: "1" }, + { billing_rate: bill_rate }, + { costing_rate: cost_rate }, + ], + ], + }, ]); }, // To check if a correct billable and costing amount is calculated for every task () => { - if(title=== 'Documentation') - { - assert.ok(cur_frm.get_field('total_billable_amount').get_value()==20, - 'Billable amount for Documentation task is correctly calculated'); - assert.ok(cur_frm.get_field('total_costing_amount').get_value()==16, - 'Costing amount for Documentation task is correctly calculated'); + if (title === "Documentation") { + assert.ok( + cur_frm.get_field("total_billable_amount").get_value() == 20, + "Billable amount for Documentation task is correctly calculated" + ); + assert.ok( + cur_frm.get_field("total_costing_amount").get_value() == 16, + "Costing amount for Documentation task is correctly calculated" + ); } - if(title=== 'Implementation') - { - assert.ok(cur_frm.get_field('total_billable_amount').get_value()==40, - 'Billable amount for Implementation task is correctly calculated'); - assert.ok(cur_frm.get_field('total_costing_amount').get_value()==32, - 'Costing amount for Implementation task is correctly calculated'); + if (title === "Implementation") { + assert.ok( + cur_frm.get_field("total_billable_amount").get_value() == 40, + "Billable amount for Implementation task is correctly calculated" + ); + assert.ok( + cur_frm.get_field("total_costing_amount").get_value() == 32, + "Costing amount for Implementation task is correctly calculated" + ); } - if(title=== 'Testing') - { - assert.ok(cur_frm.get_field('total_billable_amount').get_value()==60, - 'Billable amount for Testing task correctly calculated'); - assert.ok(cur_frm.get_field('total_costing_amount').get_value()==50, - 'Costing amount for Testing task is correctly calculated'); + if (title === "Testing") { + assert.ok( + cur_frm.get_field("total_billable_amount").get_value() == 60, + "Billable amount for Testing task correctly calculated" + ); + assert.ok( + cur_frm.get_field("total_costing_amount").get_value() == 50, + "Costing amount for Testing task is correctly calculated" + ); } }, ]); @@ -55,37 +65,39 @@ QUnit.test("test project", function(assert) { frappe.run_serially([ () => { // Creating project with task - return frappe.tests.make('Project', [ - { project_name: 'Test App'}, - { expected_start_date: '2017-07-22'}, - { expected_end_date: '2017-09-22'}, - { estimated_costing: '10,000.00'}, - { tasks:[ - [ - {title: 'Documentation'}, - {start_date: '2017-07-24'}, - {end_date: '2017-07-31'}, - {description: 'To make a proper documentation defining requirements etc'} + return frappe.tests.make("Project", [ + { project_name: "Test App" }, + { expected_start_date: "2017-07-22" }, + { expected_end_date: "2017-09-22" }, + { estimated_costing: "10,000.00" }, + { + tasks: [ + [ + { title: "Documentation" }, + { start_date: "2017-07-24" }, + { end_date: "2017-07-31" }, + { description: "To make a proper documentation defining requirements etc" }, + ], + [ + { title: "Implementation" }, + { start_date: "2017-08-01" }, + { end_date: "2017-08-01" }, + { description: "Writing algorithms and to code the functionalities" }, + ], + [ + { title: "Testing" }, + { start_date: "2017-08-01" }, + { end_date: "2017-08-15" }, + { description: "To make the test cases and test the functionalities" }, + ], ], - [ - {title: 'Implementation'}, - {start_date: '2017-08-01'}, - {end_date: '2017-08-01'}, - {description: 'Writing algorithms and to code the functionalities'} - ], - [ - {title: 'Testing'}, - {start_date: '2017-08-01'}, - {end_date: '2017-08-15'}, - {description: 'To make the test cases and test the functionalities'} - ] - ]} + }, ]); }, // Creating Timesheet with different tasks - () => timesheet(task_title[0],'2017-07-24 13:00:00','2017-07-24 13:00:00',10,8), - () => timesheet(task_title[1],'2017-07-25 13:00:00','2017-07-25 15:00:00',20,16), - () => timesheet(task_title[2],'2017-07-26 13:00:00','2017-07-26 15:00:00',30,25), - () => done() + () => timesheet(task_title[0], "2017-07-24 13:00:00", "2017-07-24 13:00:00", 10, 8), + () => timesheet(task_title[1], "2017-07-25 13:00:00", "2017-07-25 15:00:00", 20, 16), + () => timesheet(task_title[2], "2017-07-26 13:00:00", "2017-07-26 15:00:00", 30, 25), + () => done(), ]); }); diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index e49fecd1f47..1b7460f7a2a 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -23,20 +23,23 @@ class TestProject(FrappeTestCase): task1 = task_exists("Test Template Task with No Parent and Dependency") if not task1: task1 = create_task( - subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3 + subject="Test Template Task with No Parent and Dependency", + is_template=1, + begin=5, + duration=3, + priority="High", ) - template = make_project_template( - "Test Project Template - No Parent and Dependend Tasks", [task1] - ) + template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1]) project = get_project(project_name, template) tasks = frappe.get_all( "Task", - ["subject", "exp_end_date", "depends_on_tasks"], + ["subject", "exp_end_date", "depends_on_tasks", "priority"], dict(project=project.name), order_by="creation asc", ) + self.assertEqual(tasks[0].priority, "High") self.assertEqual(tasks[0].subject, "Test Template Task with No Parent and Dependency") self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3)) self.assertEqual(len(tasks), 1) @@ -180,9 +183,7 @@ class TestProject(FrappeTestCase): template_parent_task3, template_task3, ] - project_template = make_project_template( - "Project template with common Task Subject", template_tasks - ) + project_template = make_project_template("Project template with common Task Subject", template_tasks) # Step - 4: Create Project against the Project Template project = get_project("Project with common Task Subject", project_template) @@ -191,7 +192,7 @@ class TestProject(FrappeTestCase): ) # Test - 1: No. of Project Tasks should be equal to No. of Template Tasks - self.assertEquals(len(project_tasks), len(template_tasks)) + self.assertEqual(len(project_tasks), len(template_tasks)) # Test - 2: All child Project Tasks should have Parent Task linked for pt in project_tasks: @@ -200,7 +201,6 @@ class TestProject(FrappeTestCase): def get_project(name, template): - project = frappe.get_doc( dict( doctype="Project", diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js index 3d3c15c6e05..a2d008af027 100644 --- a/erpnext/projects/doctype/project_template/project_template.js +++ b/erpnext/projects/doctype/project_template/project_template.js @@ -1,7 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Project Template', { +frappe.ui.form.on("Project Template", { // refresh: function(frm) { // } @@ -9,19 +9,19 @@ frappe.ui.form.on('Project Template', { frm.set_query("task", "tasks", function () { return { filters: { - "is_template": 1 - } + is_template: 1, + }, }; }); - } + }, }); -frappe.ui.form.on('Project Template Task', { +frappe.ui.form.on("Project Template Task", { task: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; frappe.db.get_value("Task", row.task, "subject", (value) => { row.subject = value.subject; refresh_field("tasks"); }); - } + }, }); diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index 4fd24bf78a2..382ffd5aa4c 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -12,7 +12,9 @@ class TestProjectTemplate(unittest.TestCase): pass -def make_project_template(project_template_name, project_tasks=[]): +def make_project_template(project_template_name, project_tasks=None): + if project_tasks is None: + project_tasks = [] if not frappe.db.exists("Project Template", project_template_name): project_tasks = project_tasks or [ create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), diff --git a/erpnext/projects/doctype/project_type/project_type.js b/erpnext/projects/doctype/project_type/project_type.js index e3dda5eccc5..8506c787ece 100644 --- a/erpnext/projects/doctype/project_type/project_type.js +++ b/erpnext/projects/doctype/project_type/project_type.js @@ -1,6 +1,4 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Project Type', { - -}); +frappe.ui.form.on("Project Type", {}); diff --git a/erpnext/projects/doctype/project_update/project_update.js b/erpnext/projects/doctype/project_update/project_update.js index 990c1afd9ab..4e3f0fa97d2 100644 --- a/erpnext/projects/doctype/project_update/project_update.js +++ b/erpnext/projects/doctype/project_update/project_update.js @@ -1,10 +1,8 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Project Update', { - refresh: function() { - - }, +frappe.ui.form.on("Project Update", { + refresh: function () {}, onload: function (frm) { frm.set_value("naming_series", "UPDATE-.project.-.YY.MM.DD.-.####"); @@ -13,5 +11,5 @@ frappe.ui.form.on('Project Update', { validate: function (frm) { frm.set_value("time", frappe.datetime.now_time()); frm.set_value("date", frappe.datetime.nowdate()); - } + }, }); diff --git a/erpnext/projects/doctype/project_update/project_update.py b/erpnext/projects/doctype/project_update/project_update.py index 175f787a303..7390621860d 100644 --- a/erpnext/projects/doctype/project_update/project_update.py +++ b/erpnext/projects/doctype/project_update/project_update.py @@ -34,13 +34,8 @@ def daily_reminder(): email_sending(project_name, frequency, date_start, date_end, progress, number_of_drafts, update) -def email_sending( - project_name, frequency, date_start, date_end, progress, number_of_drafts, update -): - - holiday = frappe.db.sql( - """SELECT holiday_date FROM `tabHoliday` where holiday_date = CURRENT_DATE;""" - ) +def email_sending(project_name, frequency, date_start, date_end, progress, number_of_drafts, update): + holiday = frappe.db.sql("""SELECT holiday_date FROM `tabHoliday` where holiday_date = CURRENT_DATE;""") msg = ( "

    Project Name: " + project_name @@ -87,8 +82,6 @@ def email_sending( if len(holiday) == 0: email = frappe.db.sql("""SELECT user from `tabProject User` WHERE parent = %s;""", project_name) for emails in email: - frappe.sendmail( - recipients=emails, subject=frappe._(project_name + " " + "Summary"), message=msg - ) + frappe.sendmail(recipients=emails, subject=frappe._(project_name + " " + "Summary"), message=msg) else: pass diff --git a/erpnext/projects/doctype/projects_settings/projects_settings.js b/erpnext/projects/doctype/projects_settings/projects_settings.js index 9902b834920..08f50a6d989 100644 --- a/erpnext/projects/doctype/projects_settings/projects_settings.js +++ b/erpnext/projects/doctype/projects_settings/projects_settings.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Projects Settings', { - refresh: function(frm) { - - } +frappe.ui.form.on("Projects Settings", { + refresh: function (frm) {}, }); diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js index 3cd92ee719d..c56c998a518 100644 --- a/erpnext/projects/doctype/task/task.js +++ b/erpnext/projects/doctype/task/task.js @@ -6,33 +6,34 @@ frappe.provide("erpnext.projects"); frappe.ui.form.on("Task", { setup: function (frm) { frm.make_methods = { - 'Timesheet': () => frappe.model.open_mapped_doc({ - method: 'erpnext.projects.doctype.task.task.make_timesheet', - frm: frm - }) - } + Timesheet: () => + frappe.model.open_mapped_doc({ + method: "erpnext.projects.doctype.task.task.make_timesheet", + frm: frm, + }), + }; }, onload: function (frm) { frm.set_query("task", "depends_on", function () { let filters = { - name: ["!=", frm.doc.name] + name: ["!=", frm.doc.name], }; if (frm.doc.project) filters["project"] = frm.doc.project; return { - filters: filters + filters: filters, }; - }) + }); frm.set_query("parent_task", function () { let filters = { - "is_group": 1, - "name": ["!=", frm.doc.name] + is_group: 1, + name: ["!=", frm.doc.name], }; if (frm.doc.project) filters["project"] = frm.doc.project; return { - filters: filters - } + filters: filters, + }; }); }, @@ -40,22 +41,22 @@ frappe.ui.form.on("Task", { frappe.call({ method: "erpnext.projects.doctype.task.task.check_if_child_exists", args: { - name: frm.doc.name + name: frm.doc.name, }, callback: function (r) { if (r.message.length > 0) { - let message = __('Cannot convert Task to non-group because the following child Tasks exist: {0}.', + let message = __( + "Cannot convert Task to non-group because the following child Tasks exist: {0}.", [r.message.join(", ")] ); frappe.msgprint(message); frm.reload_doc(); } - } - }) + }, + }); }, validate: function (frm) { - frm.doc.project && frappe.model.remove_from_locals("Project", - frm.doc.project); - } + frm.doc.project && frappe.model.remove_from_locals("Project", frm.doc.project); + }, }); diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 1a7834257ca..b051494a651 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -24,7 +24,7 @@ class Task(NestedSet): nsm_parent_field = "parent_task" def get_feed(self): - return "{0}: {1}".format(_(self.status), self.subject) + return f"{_(self.status)}: {self.subject}" def get_customer_details(self): cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer) @@ -118,14 +118,14 @@ class Task(NestedSet): def validate_parent_template_task(self): if self.parent_task: if not frappe.db.get_value("Task", self.parent_task, "is_template"): - parent_task_format = """{0}""".format(self.parent_task) + parent_task_format = f"""{self.parent_task}""" frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format)) def validate_depends_on_tasks(self): if self.depends_on: for task in self.depends_on: if not frappe.db.get_value("Task", task.task, "is_template"): - dependent_task_format = """{0}""".format(task.task) + dependent_task_format = f"""{task.task}""" frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format)) def validate_completed_on(self): @@ -184,7 +184,7 @@ class Task(NestedSet): task_list, count = [self.name], 0 while len(task_list) > count: tasks = frappe.db.sql( - " select %s from `tabTask Depends On` where %s = %s " % (d[0], d[1], "%s"), + " select {} from `tabTask Depends On` where {} = {} ".format(d[0], d[1], "%s"), cstr(task_list[count]), ) count = count + 1 @@ -276,14 +276,12 @@ def get_project(doctype, txt, searchfield, start, page_len, filters): search_cond = " or " + " or ".join(field + " like %(txt)s" for field in searchfields) return frappe.db.sql( - """ select name {search_columns} from `tabProject` + f""" select name {search_columns} from `tabProject` where %(key)s like %(txt)s %(mcond)s - {search_condition} + {search_cond} order by name - limit %(page_len)s offset %(start)s""".format( - search_columns=search_columns, search_condition=search_cond - ), + limit %(page_len)s offset %(start)s""", { "key": searchfield, "txt": "%" + txt + "%", @@ -343,7 +341,6 @@ def make_timesheet(source_name, target_doc=None, ignore_permissions=False): @frappe.whitelist() def get_children(doctype, parent, task=None, project=None, is_root=False): - filters = [["docstatus", "<", "2"]] if task: diff --git a/erpnext/projects/doctype/task/task_calendar.js b/erpnext/projects/doctype/task/task_calendar.js index 49dbb76a1a5..768eb15b157 100644 --- a/erpnext/projects/doctype/task/task_calendar.js +++ b/erpnext/projects/doctype/task/task_calendar.js @@ -3,21 +3,21 @@ frappe.views.calendar["Task"] = { field_map: { - "start": "exp_start_date", - "end": "exp_end_date", - "id": "name", - "title": "subject", - "allDay": "allDay", - "progress": "progress" + start: "exp_start_date", + end: "exp_end_date", + id: "name", + title: "subject", + allDay: "allDay", + progress: "progress", }, gantt: true, filters: [ { - "fieldtype": "Link", - "fieldname": "project", - "options": "Project", - "label": __("Project") - } + fieldtype: "Link", + fieldname: "project", + options: "Project", + label: __("Project"), + }, ], - get_events_method: "frappe.desk.calendar.get_events" -} + get_events_method: "frappe.desk.calendar.get_events", +}; diff --git a/erpnext/projects/doctype/task/task_list.js b/erpnext/projects/doctype/task/task_list.js index 5ab8bae2e1d..17b0ed2c7fa 100644 --- a/erpnext/projects/doctype/task/task_list.js +++ b/erpnext/projects/doctype/task/task_list.js @@ -1,28 +1,36 @@ -frappe.listview_settings['Task'] = { - add_fields: ["project", "status", "priority", "exp_start_date", - "exp_end_date", "subject", "progress", "depends_on_tasks"], +frappe.listview_settings["Task"] = { + add_fields: [ + "project", + "status", + "priority", + "exp_start_date", + "exp_end_date", + "subject", + "progress", + "depends_on_tasks", + ], filters: [["status", "=", "Open"]], - onload: function(listview) { + onload: function (listview) { var method = "erpnext.projects.doctype.task.task.set_multiple_status"; - listview.page.add_menu_item(__("Set as Open"), function() { - listview.call_for_selected_items(method, {"status": "Open"}); + listview.page.add_menu_item(__("Set as Open"), function () { + listview.call_for_selected_items(method, { status: "Open" }); }); - listview.page.add_menu_item(__("Set as Completed"), function() { - listview.call_for_selected_items(method, {"status": "Completed"}); + listview.page.add_menu_item(__("Set as Completed"), function () { + listview.call_for_selected_items(method, { status: "Completed" }); }); }, - get_indicator: function(doc) { + get_indicator: function (doc) { var colors = { - "Open": "orange", - "Overdue": "red", + Open: "orange", + Overdue: "red", "Pending Review": "orange", - "Working": "orange", - "Completed": "green", - "Cancelled": "dark grey", - "Template": "blue" - } + Working: "orange", + Completed: "green", + Cancelled: "dark grey", + Template: "blue", + }; return [__(doc.status), colors[doc.status], "status,=," + doc.status]; }, gantt_custom_popup_html: function (ganttobj, task) { diff --git a/erpnext/projects/doctype/task/task_tree.js b/erpnext/projects/doctype/task/task_tree.js index 9ebfcdd180c..fba1b309260 100644 --- a/erpnext/projects/doctype/task/task_tree.js +++ b/erpnext/projects/doctype/task/task_tree.js @@ -1,84 +1,88 @@ frappe.provide("frappe.treeview_settings"); -frappe.treeview_settings['Task'] = { +frappe.treeview_settings["Task"] = { get_tree_nodes: "erpnext.projects.doctype.task.task.get_children", add_tree_node: "erpnext.projects.doctype.task.task.add_node", filters: [ { fieldname: "project", - fieldtype:"Link", + fieldtype: "Link", options: "Project", label: __("Project"), }, { fieldname: "task", - fieldtype:"Link", + fieldtype: "Link", options: "Task", label: __("Task"), - get_query: function() { - var me = frappe.treeview_settings['Task']; + get_query: function () { + var me = frappe.treeview_settings["Task"]; var project = me.page.fields_dict.project.get_value(); - var args = [["Task", 'is_group', '=', 1]]; - if(project){ - args.push(["Task", 'project', "=", project]); + var args = [["Task", "is_group", "=", 1]]; + if (project) { + args.push(["Task", "project", "=", project]); } return { - filters: args + filters: args, }; - } - } + }, + }, ], breadcrumb: "Projects", get_tree_root: false, root_label: "All Tasks", ignore_fields: ["parent_task"], - onload: function(me) { - frappe.treeview_settings['Task'].page = {}; - $.extend(frappe.treeview_settings['Task'].page, me.page); + onload: function (me) { + frappe.treeview_settings["Task"].page = {}; + $.extend(frappe.treeview_settings["Task"].page, me.page); me.make_tree(); }, toolbar: [ { - label:__("Add Multiple"), - condition: function(node) { + label: __("Add Multiple"), + condition: function (node) { return node.expandable; }, - click: function(node) { + click: function (node) { this.data = []; const dialog = new frappe.ui.Dialog({ title: __("Add Multiple Tasks"), fields: [ { - fieldname: "multiple_tasks", fieldtype: "Table", - in_place_edit: true, data: this.data, + fieldname: "multiple_tasks", + fieldtype: "Table", + in_place_edit: true, + data: this.data, get_data: () => { return this.data; }, - fields: [{ - fieldtype:'Data', - fieldname:"subject", - in_list_view: 1, - reqd: 1, - label: __("Subject") - }] + fields: [ + { + fieldtype: "Data", + fieldname: "subject", + in_list_view: 1, + reqd: 1, + label: __("Subject"), + }, + ], }, ], - primary_action: function() { + primary_action: function () { dialog.hide(); return frappe.call({ method: "erpnext.projects.doctype.task.task.add_multiple_tasks", args: { data: dialog.get_values()["multiple_tasks"], - parent: node.data.value + parent: node.data.value, }, - callback: function() { } + callback: function () {}, }); }, - primary_action_label: __('Create') + primary_action_label: __("Create"), }); dialog.show(); - } - } + }, + }, ], - extend_toolbar: true + extend_toolbar: true, }; diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index c0333f8f590..b0194b08dfa 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -122,6 +122,7 @@ def create_task( begin=0, duration=0, save=True, + priority=None, ): if not frappe.db.exists("Task", subject): task = frappe.new_doc("Task") @@ -130,15 +131,14 @@ def create_task( task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() task.project = ( - project or None - if is_template - else frappe.get_value("Project", {"project_name": "_Test Project"}) + project or None if is_template else frappe.get_value("Project", {"project_name": "_Test Project"}) ) task.is_template = is_template task.start = begin task.duration = duration task.is_group = is_group task.parent_task = parent_task + task.priority = priority if save: task.save() else: diff --git a/erpnext/projects/doctype/task_type/task_type.js b/erpnext/projects/doctype/task_type/task_type.js index c1be5da4f64..9c6176dbf55 100644 --- a/erpnext/projects/doctype/task_type/task_type.js +++ b/erpnext/projects/doctype/task_type/task_type.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Task Type', { +frappe.ui.form.on("Task Type", { // refresh: function(frm) { - // } }); diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 828a55e7bc1..da042f36aef 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -5,7 +5,7 @@ import datetime import unittest import frappe -from frappe.utils import add_months, add_to_date, now_datetime, nowdate +from frappe.utils import add_to_date, now_datetime, nowdate from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.projects.doctype.timesheet.timesheet import OverlapError, make_sales_invoice @@ -40,9 +40,7 @@ class TestTimesheet(unittest.TestCase): emp = make_employee("test_employee_6@salary.com") timesheet = make_timesheet(emp, simulate=True, is_billable=1) - sales_invoice = make_sales_invoice( - timesheet.name, "_Test Item", "_Test Customer", currency="INR" - ) + sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer", currency="INR") sales_invoice.due_date = nowdate() sales_invoice.submit() timesheet = frappe.get_doc("Timesheet", timesheet.name) @@ -211,9 +209,7 @@ def make_timesheet( timesheet_detail.activity_type = activity_type timesheet_detail.from_time = now_datetime() timesheet_detail.hours = 2 - timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta( - hours=timesheet_detail.hours - ) + timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta(hours=timesheet_detail.hours) timesheet_detail.project = project timesheet_detail.task = task diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index d1d07a79d67..0d03d6085d9 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -2,39 +2,39 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Timesheet", { - setup: function(frm) { + setup: function (frm) { frappe.require("/assets/erpnext/js/projects/timer.js"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice']; + frm.ignore_doctypes_on_cancel_all = ["Sales Invoice"]; - frm.fields_dict.employee.get_query = function() { + frm.fields_dict.employee.get_query = function () { return { - filters:{ - 'status': 'Active' - } + filters: { + status: "Active", + }, }; }; - frm.fields_dict['time_logs'].grid.get_field('task').get_query = function(frm, cdt, cdn) { + frm.fields_dict["time_logs"].grid.get_field("task").get_query = function (frm, cdt, cdn) { var child = locals[cdt][cdn]; - return{ + return { filters: { - 'project': child.project, - 'status': ["!=", "Cancelled"] - } + project: child.project, + status: ["!=", "Cancelled"], + }, }; }; - frm.fields_dict['time_logs'].grid.get_field('project').get_query = function() { - return{ + frm.fields_dict["time_logs"].grid.get_field("project").get_query = function () { + return { filters: { - 'company': frm.doc.company - } + company: frm.doc.company, + }, }; }; }, - onload: function(frm) { + onload: function (frm) { if (frm.doc.__islocal && frm.doc.time_logs) { calculate_time_and_amount(frm); } @@ -44,33 +44,32 @@ frappe.ui.form.on("Timesheet", { } }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.docstatus == 1) { if ( - (frm.doc.per_billed < 100) - && (frm.doc.total_billable_hours) - && (frm.doc.total_billable_hours > frm.doc.total_billed_hours) + frm.doc.per_billed < 100 && + frm.doc.total_billable_hours && + frm.doc.total_billable_hours > frm.doc.total_billed_hours ) { - frm.add_custom_button(__("Create Sales Invoice"), function() { + frm.add_custom_button(__("Create Sales Invoice"), function () { frm.trigger("make_invoice"); }); } } if (frm.doc.docstatus < 1) { - - let button = 'Start Timer'; - $.each(frm.doc.time_logs || [], function(i, row) { - if ((row.from_time <= frappe.datetime.now_datetime()) && !row.completed) { - button = 'Resume Timer'; + let button = "Start Timer"; + $.each(frm.doc.time_logs || [], function (i, row) { + if (row.from_time <= frappe.datetime.now_datetime() && !row.completed) { + button = "Resume Timer"; } }); - frm.add_custom_button(__(button), function() { + frm.add_custom_button(__(button), function () { var flag = true; - $.each(frm.doc.time_logs || [], function(i, row) { + $.each(frm.doc.time_logs || [], function (i, row) { // Fetch the row for which from_time is not present - if (flag && row.activity_type && !row.from_time){ + if (flag && row.activity_type && !row.from_time) { erpnext.timesheet.timer(frm, row); row.from_time = frappe.datetime.now_datetime(); frm.refresh_fields("time_logs"); @@ -79,7 +78,10 @@ frappe.ui.form.on("Timesheet", { } // Fetch the row for timer where activity is not completed and from_time is before now_time if (flag && row.from_time <= frappe.datetime.now_datetime() && !row.completed) { - let timestamp = moment(frappe.datetime.now_datetime()).diff(moment(row.from_time),"seconds"); + let timestamp = moment(frappe.datetime.now_datetime()).diff( + moment(row.from_time), + "seconds" + ); erpnext.timesheet.timer(frm, row, timestamp); flag = false; } @@ -90,143 +92,168 @@ frappe.ui.form.on("Timesheet", { } }).addClass("btn-primary"); } - if(frm.doc.per_billed > 0) { + if (frm.doc.per_billed > 0) { frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false); frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false); } let filters = { - "status": "Open" + status: "Open", }; if (frm.doc.customer) { filters["customer"] = frm.doc.customer; } - frm.set_query('parent_project', function(doc) { + frm.set_query("parent_project", function (doc) { return { - filters: filters + filters: filters, }; }); - frm.trigger('setup_filters'); - frm.trigger('set_dynamic_field_label'); + frm.trigger("setup_filters"); + frm.trigger("set_dynamic_field_label"); }, - customer: function(frm) { - frm.set_query('project', 'time_logs', function(doc) { + customer: function (frm) { + frm.set_query("project", "time_logs", function (doc) { return { filters: { - "customer": doc.customer - } + customer: doc.customer, + }, }; }); frm.refresh(); }, - currency: function(frm) { - let base_currency = frappe.defaults.get_global_default('currency'); - if (frm.doc.currency && (base_currency != frm.doc.currency)) { + currency: function (frm) { + let base_currency = frappe.defaults.get_global_default("currency"); + if (frm.doc.currency && base_currency != frm.doc.currency) { frappe.call({ method: "erpnext.setup.utils.get_exchange_rate", args: { from_currency: frm.doc.currency, - to_currency: base_currency + to_currency: base_currency, }, - callback: function(r) { + callback: function (r) { if (r.message) { - frm.set_value('exchange_rate', flt(r.message)); - frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + " = [?] " + base_currency); + frm.set_value("exchange_rate", flt(r.message)); + frm.set_df_property( + "exchange_rate", + "description", + "1 " + frm.doc.currency + " = [?] " + base_currency + ); } - } + }, }); } - frm.trigger('set_dynamic_field_label'); + frm.trigger("set_dynamic_field_label"); }, - exchange_rate: function(frm) { - $.each(frm.doc.time_logs, function(i, d) { + exchange_rate: function (frm) { + $.each(frm.doc.time_logs, function (i, d) { calculate_billing_costing_amount(frm, d.doctype, d.name); }); calculate_time_and_amount(frm); }, - set_dynamic_field_label: function(frm) { - let base_currency = frappe.defaults.get_global_default('currency'); - frm.set_currency_labels(["base_total_costing_amount", "base_total_billable_amount", "base_total_billed_amount"], base_currency); - frm.set_currency_labels(["total_costing_amount", "total_billable_amount", "total_billed_amount"], frm.doc.currency); + set_dynamic_field_label: function (frm) { + let base_currency = frappe.defaults.get_global_default("currency"); + frm.set_currency_labels( + ["base_total_costing_amount", "base_total_billable_amount", "base_total_billed_amount"], + base_currency + ); + frm.set_currency_labels( + ["total_costing_amount", "total_billable_amount", "total_billed_amount"], + frm.doc.currency + ); - frm.toggle_display(["base_total_costing_amount", "base_total_billable_amount", "base_total_billed_amount"], - frm.doc.currency != base_currency); + frm.toggle_display( + ["base_total_costing_amount", "base_total_billable_amount", "base_total_billed_amount"], + frm.doc.currency != base_currency + ); if (frm.doc.time_logs.length > 0) { - frm.set_currency_labels(["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], base_currency, "time_logs"); - frm.set_currency_labels(["billing_rate", "billing_amount", "costing_rate", "costing_amount"], frm.doc.currency, "time_logs"); + frm.set_currency_labels( + ["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], + base_currency, + "time_logs" + ); + frm.set_currency_labels( + ["billing_rate", "billing_amount", "costing_rate", "costing_amount"], + frm.doc.currency, + "time_logs" + ); let time_logs_grid = frm.fields_dict.time_logs.grid; - $.each(["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], function(i, d) { - if (frappe.meta.get_docfield(time_logs_grid.doctype, d)) - time_logs_grid.set_column_disp(d, frm.doc.currency != base_currency); - }); + $.each( + ["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], + function (i, d) { + if (frappe.meta.get_docfield(time_logs_grid.doctype, d)) + time_logs_grid.set_column_disp(d, frm.doc.currency != base_currency); + } + ); } frm.refresh_fields(); }, - make_invoice: function(frm) { - let fields = [{ - "fieldtype": "Link", - "label": __("Item Code"), - "fieldname": "item_code", - "options": "Item" - }]; + make_invoice: function (frm) { + let fields = [ + { + fieldtype: "Link", + label: __("Item Code"), + fieldname: "item_code", + options: "Item", + }, + ]; if (!frm.doc.customer) { fields.push({ - "fieldtype": "Link", - "label": __("Customer"), - "fieldname": "customer", - "options": "Customer", - "default": frm.doc.customer + fieldtype: "Link", + label: __("Customer"), + fieldname: "customer", + options: "Customer", + default: frm.doc.customer, }); } let dialog = new frappe.ui.Dialog({ title: __("Create Sales Invoice"), - fields: fields + fields: fields, }); - dialog.set_primary_action(__('Create Sales Invoice'), () => { + dialog.set_primary_action(__("Create Sales Invoice"), () => { var args = dialog.get_values(); - if(!args) return; + if (!args) return; dialog.hide(); return frappe.call({ type: "GET", method: "erpnext.projects.doctype.timesheet.timesheet.make_sales_invoice", args: { - "source_name": frm.doc.name, - "item_code": args.item_code, - "customer": frm.doc.customer || args.customer, - "currency": frm.doc.currency + source_name: frm.doc.name, + item_code: args.item_code, + customer: frm.doc.customer || args.customer, + currency: frm.doc.currency, }, freeze: true, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { frappe.model.sync(r.message); frappe.set_route("Form", r.message.doctype, r.message.name); } - } + }, }); }); dialog.show(); }, - parent_project: function(frm) { + parent_project: function (frm) { set_project_in_timelog(frm); - } + }, }); frappe.ui.form.on("Timesheet Detail", { - time_logs_remove: function(frm) { + time_logs_remove: function (frm) { calculate_time_and_amount(frm); }, @@ -239,47 +266,47 @@ frappe.ui.form.on("Timesheet Detail", { } }, - from_time: function(frm, cdt, cdn) { + from_time: function (frm, cdt, cdn) { calculate_end_time(frm, cdt, cdn); }, - to_time: function(frm, cdt, cdn) { + to_time: function (frm, cdt, cdn) { var child = locals[cdt][cdn]; - if(frm._setting_hours) return; + if (frm._setting_hours) return; var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600; frappe.model.set_value(cdt, cdn, "hours", hours); }, - time_logs_add: function(frm, cdt, cdn) { - if(frm.doc.parent_project) { - frappe.model.set_value(cdt, cdn, 'project', frm.doc.parent_project); + time_logs_add: function (frm, cdt, cdn) { + if (frm.doc.parent_project) { + frappe.model.set_value(cdt, cdn, "project", frm.doc.parent_project); } }, - hours: function(frm, cdt, cdn) { + hours: function (frm, cdt, cdn) { calculate_end_time(frm, cdt, cdn); calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, - billing_hours: function(frm, cdt, cdn) { + billing_hours: function (frm, cdt, cdn) { calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, - billing_rate: function(frm, cdt, cdn) { + billing_rate: function (frm, cdt, cdn) { calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, - costing_rate: function(frm, cdt, cdn) { + costing_rate: function (frm, cdt, cdn) { calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, - is_billable: function(frm, cdt, cdn) { + is_billable: function (frm, cdt, cdn) { update_billing_hours(frm, cdt, cdn); update_time_rates(frm, cdt, cdn); calculate_billing_costing_amount(frm, cdt, cdn); @@ -294,7 +321,7 @@ frappe.ui.form.on("Timesheet Detail", { args: { employee: frm.doc.employee, activity_type: frm.selected_doc.activity_type, - currency: frm.doc.currency + currency: frm.doc.currency, }, callback: function (r) { if (r.message) { @@ -302,72 +329,71 @@ frappe.ui.form.on("Timesheet Detail", { frappe.model.set_value(cdt, cdn, "costing_rate", r.message["costing_rate"]); calculate_billing_costing_amount(frm, cdt, cdn); } - } + }, }); - } + }, }); -var calculate_end_time = function(frm, cdt, cdn) { +var calculate_end_time = function (frm, cdt, cdn) { let child = locals[cdt][cdn]; - if(!child.from_time) { + if (!child.from_time) { // if from_time value is not available then set the current datetime frappe.model.set_value(cdt, cdn, "from_time", frappe.datetime.get_datetime_as_string()); } let d = moment(child.from_time); - if(child.hours) { + if (child.hours) { d.add(child.hours, "hours"); frm._setting_hours = true; - frappe.model.set_value(cdt, cdn, "to_time", - d.format(frappe.defaultDatetimeFormat)).then(() => { + frappe.model.set_value(cdt, cdn, "to_time", d.format(frappe.defaultDatetimeFormat)).then(() => { frm._setting_hours = false; }); } }; -var update_billing_hours = function(frm, cdt, cdn) { +var update_billing_hours = function (frm, cdt, cdn) { let child = frappe.get_doc(cdt, cdn); if (!child.is_billable) { - frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0); + frappe.model.set_value(cdt, cdn, "billing_hours", 0.0); } else { // bill all hours by default frappe.model.set_value(cdt, cdn, "billing_hours", child.hours); } }; -var update_time_rates = function(frm, cdt, cdn) { +var update_time_rates = function (frm, cdt, cdn) { let child = frappe.get_doc(cdt, cdn); if (!child.is_billable) { - frappe.model.set_value(cdt, cdn, 'billing_rate', 0.0); + frappe.model.set_value(cdt, cdn, "billing_rate", 0.0); } }; -var calculate_billing_costing_amount = function(frm, cdt, cdn) { +var calculate_billing_costing_amount = function (frm, cdt, cdn) { let row = frappe.get_doc(cdt, cdn); let billing_amount = 0.0; let base_billing_amount = 0.0; let exchange_rate = flt(frm.doc.exchange_rate); - frappe.model.set_value(cdt, cdn, 'base_billing_rate', flt(row.billing_rate) * exchange_rate); - frappe.model.set_value(cdt, cdn, 'base_costing_rate', flt(row.costing_rate) * exchange_rate); + frappe.model.set_value(cdt, cdn, "base_billing_rate", flt(row.billing_rate) * exchange_rate); + frappe.model.set_value(cdt, cdn, "base_costing_rate", flt(row.costing_rate) * exchange_rate); if (row.billing_hours && row.is_billable) { base_billing_amount = flt(row.billing_hours) * flt(row.base_billing_rate); billing_amount = flt(row.billing_hours) * flt(row.billing_rate); } - frappe.model.set_value(cdt, cdn, 'base_billing_amount', base_billing_amount); - frappe.model.set_value(cdt, cdn, 'base_costing_amount', flt(row.base_costing_rate) * flt(row.hours)); - frappe.model.set_value(cdt, cdn, 'billing_amount', billing_amount); - frappe.model.set_value(cdt, cdn, 'costing_amount', flt(row.costing_rate) * flt(row.hours)); + frappe.model.set_value(cdt, cdn, "base_billing_amount", base_billing_amount); + frappe.model.set_value(cdt, cdn, "base_costing_amount", flt(row.base_costing_rate) * flt(row.hours)); + frappe.model.set_value(cdt, cdn, "billing_amount", billing_amount); + frappe.model.set_value(cdt, cdn, "costing_amount", flt(row.costing_rate) * flt(row.hours)); }; -var calculate_time_and_amount = function(frm) { +var calculate_time_and_amount = function (frm) { let tl = frm.doc.time_logs || []; let total_working_hr = 0; let total_billing_hr = 0; let total_billable_amount = 0; let total_costing_amount = 0; - for(var i=0; i { + const fields = ["name", "company"]; + frappe.db.get_value("Employee", options, fields).then(({ message }) => { if (message) { // there is an employee with the currently logged in user_id frm.set_value("employee", message.name); @@ -399,8 +425,8 @@ const set_employee_and_company = function(frm) { }; function set_project_in_timelog(frm) { - if(frm.doc.parent_project) { - $.each(frm.doc.time_logs || [], function(i, item) { + if (frm.doc.parent_project) { + $.each(frm.doc.time_logs || [], function (i, item) { frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project); }); } diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index e63ac144f64..e67841e0358 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -77,7 +77,7 @@ class Timesheet(Document): def set_status(self): self.status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[str(self.docstatus or 0)] - if self.per_billed == 100: + if flt(self.per_billed, self.precision("per_billed")) >= 100.0: self.status = "Billed" if self.sales_invoice: @@ -293,12 +293,10 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to @frappe.whitelist() def get_timesheet_detail_rate(timelog, currency): timelog_detail = frappe.db.sql( - """SELECT tsd.billing_amount as billing_amount, + f"""SELECT tsd.billing_amount as billing_amount, ts.currency as currency FROM `tabTimesheet Detail` tsd INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent - WHERE tsd.name = '{0}'""".format( - timelog - ), + WHERE tsd.name = '{timelog}'""", as_dict=1, )[0] @@ -320,14 +318,12 @@ def get_timesheet(doctype, txt, searchfield, start, page_len, filters): condition = "and tsd.project = %(project)s" return frappe.db.sql( - """select distinct tsd.parent from `tabTimesheet Detail` tsd, + f"""select distinct tsd.parent from `tabTimesheet Detail` tsd, `tabTimesheet` ts where ts.status in ('Submitted', 'Payslip') and tsd.parent = ts.name and tsd.docstatus = 1 and ts.total_billable_amount > 0 and tsd.parent LIKE %(txt)s {condition} - order by tsd.parent limit %(page_len)s offset %(start)s""".format( - condition=condition - ), + order by tsd.parent limit %(page_len)s offset %(start)s""", { "txt": "%" + txt + "%", "start": start, @@ -389,6 +385,9 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None "timesheets", { "time_sheet": timesheet.name, + "project_name": time_log.project_name, + "from_time": time_log.from_time, + "to_time": time_log.to_time, "billing_hours": time_log.billing_hours, "billing_amount": time_log.billing_amount, "timesheet_detail": time_log.name, @@ -449,18 +448,14 @@ def get_events(start, end, filters=None): where `tabTimesheet Detail`.parent = `tabTimesheet`.name and `tabTimesheet`.docstatus < 2 and (from_time <= %(end)s and to_time >= %(start)s) {conditions} {match_cond} - """.format( - conditions=conditions, match_cond=get_match_cond("Timesheet") - ), + """.format(conditions=conditions, match_cond=get_match_cond("Timesheet")), {"start": start, "end": end}, as_dict=True, update={"allDay": 0}, ) -def get_timesheets_list( - doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" -): +def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"): user = frappe.session.user # find customer name from contact. customer = "" @@ -479,7 +474,7 @@ def get_timesheets_list( projects = [d.name for d in frappe.get_all("Project", filters={"customer": customer})] # Return timesheet related data to web portal. timesheets = frappe.db.sql( - """ + f""" SELECT ts.name, tsd.activity_type, ts.status, ts.total_billable_hours, COALESCE(ts.sales_invoice, tsd.sales_invoice) AS sales_invoice, tsd.project @@ -491,10 +486,8 @@ def get_timesheets_list( tsd.project IN %(projects)s ) ORDER BY `end_date` ASC - LIMIT {1} offset {0} - """.format( - limit_start, limit_page_length - ), + LIMIT {limit_page_length} offset {limit_start} + """, dict(sales_invoices=sales_invoices, projects=projects), as_dict=True, ) # nosec diff --git a/erpnext/projects/doctype/timesheet/timesheet_calendar.js b/erpnext/projects/doctype/timesheet/timesheet_calendar.js index 80967ede1ce..27992a107cb 100644 --- a/erpnext/projects/doctype/timesheet/timesheet_calendar.js +++ b/erpnext/projects/doctype/timesheet/timesheet_calendar.js @@ -1,32 +1,32 @@ frappe.views.calendar["Timesheet"] = { field_map: { - "start": "start_date", - "end": "end_date", - "name": "parent", - "id": "name", - "allDay": "allDay", - "child_name": "name", - "title": "title" + start: "start_date", + end: "end_date", + name: "parent", + id: "name", + allDay: "allDay", + child_name: "name", + title: "title", }, style_map: { - "0": "info", - "1": "standard", - "2": "danger" + 0: "info", + 1: "standard", + 2: "danger", }, gantt: true, filters: [ { - "fieldtype": "Link", - "fieldname": "project", - "options": "Project", - "label": __("Project") + fieldtype: "Link", + fieldname: "project", + options: "Project", + label: __("Project"), }, { - "fieldtype": "Link", - "fieldname": "employee", - "options": "Employee", - "label": __("Employee") - } + fieldtype: "Link", + fieldname: "employee", + options: "Employee", + label: __("Employee"), + }, ], - get_events_method: "erpnext.projects.doctype.timesheet.timesheet.get_events" -} + get_events_method: "erpnext.projects.doctype.timesheet.timesheet.get_events", +}; diff --git a/erpnext/projects/doctype/timesheet/timesheet_list.js b/erpnext/projects/doctype/timesheet/timesheet_list.js index b59fdc96fe8..0de568ce589 100644 --- a/erpnext/projects/doctype/timesheet/timesheet_list.js +++ b/erpnext/projects/doctype/timesheet/timesheet_list.js @@ -1,16 +1,16 @@ -frappe.listview_settings['Timesheet'] = { +frappe.listview_settings["Timesheet"] = { add_fields: ["status", "total_hours", "start_date", "end_date"], - get_indicator: function(doc) { - if (doc.status== "Billed") { - return [__("Billed"), "green", "status,=," + "Billed"] + get_indicator: function (doc) { + if (doc.status == "Billed") { + return [__("Billed"), "green", "status,=," + "Billed"]; } - if (doc.status== "Payslip") { - return [__("Payslip"), "green", "status,=," + "Payslip"] + if (doc.status == "Payslip") { + return [__("Payslip"), "green", "status,=," + "Payslip"]; } - if (doc.status== "Completed") { - return [__("Completed"), "green", "status,=," + "Completed"] + if (doc.status == "Completed") { + return [__("Completed"), "green", "status,=," + "Completed"]; } - } + }, }; diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py index ac1524a49dd..ab8d3838921 100644 --- a/erpnext/projects/report/billing_summary.py +++ b/erpnext/projects/report/billing_summary.py @@ -50,9 +50,7 @@ def get_data(filters): timesheets = get_timesheets(filters) filters.from_date = frappe.utils.get_datetime(filters.from_date) - filters.to_date = frappe.utils.add_to_date( - frappe.utils.get_datetime(filters.to_date), days=1, seconds=-1 - ) + filters.to_date = frappe.utils.add_to_date(frappe.utils.get_datetime(filters.to_date), days=1, seconds=-1) timesheet_details = get_timesheet_details(filters, timesheets.keys()) diff --git a/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.js b/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.js index 93cb9402fa8..60525a1ae41 100644 --- a/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.js +++ b/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.js @@ -2,18 +2,18 @@ // For license information, please see license.txt frappe.query_reports["Daily Timesheet Summary"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, - ] -} + ], +}; diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js index 5aa44c0a8c9..983eb448cb5 100644 --- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js +++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js @@ -3,31 +3,31 @@ /* eslint-disable */ frappe.query_reports["Delayed Tasks Summary"] = { - "filters": [ + filters: [ { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date" + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", }, { - "fieldname": "priority", - "label": __("Priority"), - "fieldtype": "Select", - "options": ["", "Low", "Medium", "High", "Urgent"] + fieldname: "priority", + label: __("Priority"), + fieldtype: "Select", + options: ["", "Low", "Medium", "High", "Urgent"], }, { - "fieldname": "status", - "label": __("Status"), - "fieldtype": "Select", - "options": ["", "Open", "Working","Pending Review","Overdue","Completed"] + fieldname: "status", + label: __("Status"), + fieldtype: "Select", + options: ["", "Open", "Working", "Pending Review", "Overdue", "Completed"], }, ], - "formatter": function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (column.id == "delay") { if (data["delay"] > 0) { @@ -36,6 +36,6 @@ frappe.query_reports["Delayed Tasks Summary"] = { value = `

    ${value}

    `; } } - return value - } + return value; + }, }; diff --git a/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py index 91a0607b17c..c288125ac11 100644 --- a/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py @@ -31,14 +31,14 @@ class TestDelayedTasksSummary(unittest.TestCase): {"subject": "_Test Task 98", "status": "Completed", "priority": "Low", "delay": -1}, ] report = execute(filters) - data = list(filter(lambda x: x.subject == "_Test Task 99", report[1]))[0] + data = next(filter(lambda x: x.subject == "_Test Task 99", report[1])) for key in ["subject", "status", "priority", "delay"]: self.assertEqual(expected_data[0].get(key), data.get(key)) filters.status = "Completed" report = execute(filters) - data = list(filter(lambda x: x.subject == "_Test Task 98", report[1]))[0] + data = next(filter(lambda x: x.subject == "_Test Task 98", report[1])) for key in ["subject", "status", "priority", "delay"]: self.assertEqual(expected_data[1].get(key), data.get(key)) diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js index 9c904c57872..4bdbf659a5c 100644 --- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js +++ b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js @@ -3,32 +3,32 @@ /* eslint-disable */ frappe.query_reports["Employee Billing Summary"] = { - "filters": [ + filters: [ { fieldname: "employee", label: __("Employee"), fieldtype: "Link", options: "Employee", - reqd: 1 + reqd: 1, }, { - fieldname:"from_date", + fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.month_start(), -1), - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: frappe.datetime.add_days(frappe.datetime.month_start(), -1), - reqd: 1 + reqd: 1, }, { - fieldname:"include_draft_timesheets", + fieldname: "include_draft_timesheets", label: __("Include Timesheets in Draft Status"), fieldtype: "Check", }, - ] -} + ], +}; diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.js b/erpnext/projects/report/project_billing_summary/project_billing_summary.js index 6a6f3677e3f..86c54e519a5 100644 --- a/erpnext/projects/report/project_billing_summary/project_billing_summary.js +++ b/erpnext/projects/report/project_billing_summary/project_billing_summary.js @@ -3,32 +3,32 @@ /* eslint-disable */ frappe.query_reports["Project Billing Summary"] = { - "filters": [ + filters: [ { fieldname: "project", label: __("Project"), fieldtype: "Link", options: "Project", - reqd: 1 + reqd: 1, }, { - fieldname:"from_date", + fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.month_start(), -1), - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), - reqd: 1 + default: frappe.datetime.add_days(frappe.datetime.month_start(), -1), + reqd: 1, }, { - fieldname:"include_draft_timesheets", + fieldname: "include_draft_timesheets", label: __("Include Timesheets in Draft Status"), fieldtype: "Check", }, - ] -} + ], +}; diff --git a/erpnext/projects/report/project_summary/project_summary.js b/erpnext/projects/report/project_summary/project_summary.js index 414b7b206a1..b5bf4bedd1b 100644 --- a/erpnext/projects/report/project_summary/project_summary.js +++ b/erpnext/projects/report/project_summary/project_summary.js @@ -3,40 +3,40 @@ /* eslint-disable */ frappe.query_reports["Project Summary"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname": "is_active", - "label": __("Is Active"), - "fieldtype": "Select", - "options": "\nYes\nNo", - "default": "Yes", + fieldname: "is_active", + label: __("Is Active"), + fieldtype: "Select", + options: "\nYes\nNo", + default: "Yes", }, { - "fieldname": "status", - "label": __("Status"), - "fieldtype": "Select", - "options": "\nOpen\nCompleted\nCancelled", - "default": "Open" + fieldname: "status", + label: __("Status"), + fieldtype: "Select", + options: "\nOpen\nCompleted\nCancelled", + default: "Open", }, { - "fieldname": "project_type", - "label": __("Project Type"), - "fieldtype": "Link", - "options": "Project Type" + fieldname: "project_type", + label: __("Project Type"), + fieldtype: "Link", + options: "Project Type", }, { - "fieldname": "priority", - "label": __("Priority"), - "fieldtype": "Select", - "options": "\nLow\nMedium\nHigh" - } - ] + fieldname: "priority", + label: __("Priority"), + fieldtype: "Select", + options: "\nLow\nMedium\nHigh", + }, + ], }; diff --git a/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.js b/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.js index ccca26439d0..dcadcc0852e 100644 --- a/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.js +++ b/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.js @@ -2,7 +2,5 @@ // For license information, please see license.txt frappe.query_reports["Project wise Stock Tracking"] = { - "filters": [ - - ] -} + filters: [], +}; diff --git a/erpnext/projects/utils.py b/erpnext/projects/utils.py index 3cc4da4f07d..5046d015cb6 100644 --- a/erpnext/projects/utils.py +++ b/erpnext/projects/utils.py @@ -19,13 +19,14 @@ def query_task(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( """select name, subject from `tabTask` - where (`%s` like %s or `subject` like %s) %s + where (`{}` like {} or `subject` like {}) {} order by - case when `subject` like %s then 0 else 1 end, - case when `%s` like %s then 0 else 1 end, - `%s`, + case when `subject` like {} then 0 else 1 end, + case when `{}` like {} then 0 else 1 end, + `{}`, subject - limit %s offset %s""" - % (searchfield, "%s", "%s", match_conditions, "%s", searchfield, "%s", searchfield, "%s", "%s"), + limit {} offset {}""".format( + searchfield, "%s", "%s", match_conditions, "%s", searchfield, "%s", searchfield, "%s", "%s" + ), (search_string, search_string, order_by_string, order_by_string, page_len, start), ) diff --git a/erpnext/projects/web_form/tasks/tasks.js b/erpnext/projects/web_form/tasks/tasks.js index ffc5e984253..8f56ebb353d 100644 --- a/erpnext/projects/web_form/tasks/tasks.js +++ b/erpnext/projects/web_form/tasks/tasks.js @@ -1,3 +1,3 @@ -frappe.ready(function() { +frappe.ready(function () { // bind events here -}) +}); diff --git a/erpnext/public/js/account_tree_grid.js b/erpnext/public/js/account_tree_grid.js index 413a5ee9719..6b4cdf1177a 100644 --- a/erpnext/public/js/account_tree_grid.js +++ b/erpnext/public/js/account_tree_grid.js @@ -18,53 +18,96 @@ erpnext.AccountTreeGrid = class AccountTreeGrid extends frappe.views.TreeGridRep constructor(wrapper, title) { super({ title: title, - parent: $(wrapper).find('.layout-main'), + parent: $(wrapper).find(".layout-main"), page: wrapper.page, doctypes: ["Company", "Fiscal Year", "Account", "GL Entry", "Cost Center"], tree_grid: { show: true, parent_field: "parent_account", - formatter: function(item) { - return repl("\ - %(value)s", { + %(value)s", + { value: item.name, - }); - } + } + ); + }, }, }); this.filters = [ - {fieldtype: "Select", label: __("Company"), link:"Company", fieldname: "company", + { + fieldtype: "Select", + label: __("Company"), + link: "Company", + fieldname: "company", default_value: __("Select Company..."), - filter: function(val, item, opts, me) { + filter: function (val, item, opts, me) { if (item.company == val || val == opts.default_value) { return me.apply_zero_filter(val, item, opts, me); } return false; - }}, - {fieldtype: "Select", label: "Fiscal Year", link:"Fiscal Year", fieldname: "fiscal_year", - default_value: __("Select Fiscal Year...")}, - {fieldtype: "Date", label: __("From Date"), fieldname: "from_date"}, - {fieldtype: "Label", label: __("To")}, - {fieldtype: "Date", label: __("To Date"), fieldname: "to_date"} - ] + }, + }, + { + fieldtype: "Select", + label: "Fiscal Year", + link: "Fiscal Year", + fieldname: "fiscal_year", + default_value: __("Select Fiscal Year..."), + }, + { fieldtype: "Date", label: __("From Date"), fieldname: "from_date" }, + { fieldtype: "Label", label: __("To") }, + { fieldtype: "Date", label: __("To Date"), fieldname: "to_date" }, + ]; } setup_columns() { this.columns = [ - {id: "name", name: __("Account"), field: "name", width: 300, cssClass: "cell-title"}, - {id: "opening_dr", name: __("Opening (Dr)"), field: "opening_dr", width: 100, - formatter: this.currency_formatter}, - {id: "opening_cr", name: __("Opening (Cr)"), field: "opening_cr", width: 100, - formatter: this.currency_formatter}, - {id: "debit", name: __("Debit"), field: "debit", width: 100, - formatter: this.currency_formatter}, - {id: "credit", name: __("Credit"), field: "credit", width: 100, - formatter: this.currency_formatter}, - {id: "closing_dr", name: __("Closing (Dr)"), field: "closing_dr", width: 100, - formatter: this.currency_formatter}, - {id: "closing_cr", name: __("Closing (Cr)"), field: "closing_cr", width: 100, - formatter: this.currency_formatter} + { id: "name", name: __("Account"), field: "name", width: 300, cssClass: "cell-title" }, + { + id: "opening_dr", + name: __("Opening (Dr)"), + field: "opening_dr", + width: 100, + formatter: this.currency_formatter, + }, + { + id: "opening_cr", + name: __("Opening (Cr)"), + field: "opening_cr", + width: 100, + formatter: this.currency_formatter, + }, + { + id: "debit", + name: __("Debit"), + field: "debit", + width: 100, + formatter: this.currency_formatter, + }, + { + id: "credit", + name: __("Credit"), + field: "credit", + width: 100, + formatter: this.currency_formatter, + }, + { + id: "closing_dr", + name: __("Closing (Dr)"), + field: "closing_dr", + width: 100, + formatter: this.currency_formatter, + }, + { + id: "closing_cr", + name: __("Closing (Cr)"), + field: "closing_cr", + width: 100, + formatter: this.currency_formatter, + }, ]; } @@ -72,33 +115,33 @@ erpnext.AccountTreeGrid = class AccountTreeGrid extends frappe.views.TreeGridRep super.setup_filters(); var me = this; // default filters - this.filter_inputs.fiscal_year.change(function() { + this.filter_inputs.fiscal_year.change(function () { var fy = $(this).val(); - $.each(frappe.report_dump.data["Fiscal Year"], function(i, v) { - if (v.name==fy) { + $.each(frappe.report_dump.data["Fiscal Year"], function (i, v) { + if (v.name == fy) { me.filter_inputs.from_date.val(frappe.datetime.str_to_user(v.year_start_date)); me.filter_inputs.to_date.val(frappe.datetime.str_to_user(v.year_end_date)); } }); me.refresh(); }); - me.show_zero_check() - if(me.ignore_closing_entry) me.ignore_closing_entry(); + me.show_zero_check(); + if (me.ignore_closing_entry) me.ignore_closing_entry(); } prepare_data() { var me = this; - if(!this.primary_data) { + if (!this.primary_data) { // make accounts list me.data = []; me.parent_map = {}; me.item_by_name = {}; - $.each(frappe.report_dump.data["Account"], function(i, v) { + $.each(frappe.report_dump.data["Account"], function (i, v) { var d = copy_dict(v); me.data.push(d); me.item_by_name[d.name] = d; - if(d.parent_account) { + if (d.parent_account) { me.parent_map[d.name] = d.parent_account; } }); @@ -107,20 +150,19 @@ erpnext.AccountTreeGrid = class AccountTreeGrid extends frappe.views.TreeGridRep } me.data = [].concat(me.primary_data); - $.each(me.data, function(i, d) { + $.each(me.data, function (i, d) { me.init_account(d); }); this.set_indent(); this.prepare_balances(); - } init_account(d) { this.reset_item_values(d); } prepare_balances() { - var gl = frappe.report_dump.data['GL Entry']; + var gl = frappe.report_dump.data["GL Entry"]; var me = this; this.opening_date = frappe.datetime.user_to_obj(this.filter_inputs.from_date.val()); @@ -128,12 +170,11 @@ erpnext.AccountTreeGrid = class AccountTreeGrid extends frappe.views.TreeGridRep this.set_fiscal_year(); if (!this.fiscal_year) return; - $.each(this.data, function(i, v) { - v.opening_dr = v.opening_cr = v.debit - = v.credit = v.closing_dr = v.closing_cr = 0; + $.each(this.data, function (i, v) { + v.opening_dr = v.opening_cr = v.debit = v.credit = v.closing_dr = v.closing_cr = 0; }); - $.each(gl, function(i, v) { + $.each(gl, function (i, v) { var posting_date = frappe.datetime.str_to_obj(v.posting_date); var account = me.item_by_name[v.account]; me.update_balances(account, posting_date, v); @@ -144,13 +185,15 @@ erpnext.AccountTreeGrid = class AccountTreeGrid extends frappe.views.TreeGridRep update_balances(account, posting_date, v) { // opening if (posting_date < this.opening_date || v.is_opening === "Yes") { - if (account.report_type === "Profit and Loss" && - posting_date <= frappe.datetime.str_to_obj(this.fiscal_year[1])) { + if ( + account.report_type === "Profit and Loss" && + posting_date <= frappe.datetime.str_to_obj(this.fiscal_year[1]) + ) { // balance of previous fiscal_year should // not be part of opening of pl account balance } else { - var opening_bal = flt(account.opening_dr) - flt(account.opening_cr) + - flt(v.debit) - flt(v.credit); + var opening_bal = + flt(account.opening_dr) - flt(account.opening_cr) + flt(v.debit) - flt(v.credit); this.set_debit_or_credit(account, "opening", opening_bal); } } else if (this.opening_date <= posting_date && posting_date <= this.closing_date) { @@ -159,43 +202,47 @@ erpnext.AccountTreeGrid = class AccountTreeGrid extends frappe.views.TreeGridRep account.credit += flt(v.credit); } // closing - var closing_bal = flt(account.opening_dr) - flt(account.opening_cr) + - flt(account.debit) - flt(account.credit); + var closing_bal = + flt(account.opening_dr) - flt(account.opening_cr) + flt(account.debit) - flt(account.credit); this.set_debit_or_credit(account, "closing", closing_bal); } set_debit_or_credit(account, field, balance) { - if(balance > 0) { - account[field+"_dr"] = balance; - account[field+"_cr"] = 0; + if (balance > 0) { + account[field + "_dr"] = balance; + account[field + "_cr"] = 0; } else { - account[field+"_cr"] = Math.abs(balance); - account[field+"_dr"] = 0; + account[field + "_cr"] = Math.abs(balance); + account[field + "_dr"] = 0; } } update_groups() { // update groups - var me= this; - $.each(this.data, function(i, account) { + var me = this; + $.each(this.data, function (i, account) { // update groups - if((account.is_group == 0) || (account.rgt - account.lft == 1)) { + if (account.is_group == 0 || account.rgt - account.lft == 1) { var parent = me.parent_map[account.name]; - while(parent) { + while (parent) { var parent_account = me.item_by_name[parent]; - $.each(me.columns, function(c, col) { + $.each(me.columns, function (c, col) { if (col.formatter == me.currency_formatter) { - if(col.field=="opening_dr") { - var bal = flt(parent_account.opening_dr) - + if (col.field == "opening_dr") { + var bal = + flt(parent_account.opening_dr) - flt(parent_account.opening_cr) + - flt(account.opening_dr) - flt(account.opening_cr); + flt(account.opening_dr) - + flt(account.opening_cr); me.set_debit_or_credit(parent_account, "opening", bal); - } else if(col.field=="closing_dr") { - var bal = flt(parent_account.closing_dr) - + } else if (col.field == "closing_dr") { + var bal = + flt(parent_account.closing_dr) - flt(parent_account.closing_cr) + - flt(account.closing_dr) - flt(account.closing_cr); + flt(account.closing_dr) - + flt(account.closing_cr); me.set_debit_or_credit(parent_account, "closing", bal); - } else if(in_list(["debit", "credit"], col.field)) { - parent_account[col.field] = flt(parent_account[col.field]) + - flt(account[col.field]); + } else if (in_list(["debit", "credit"], col.field)) { + parent_account[col.field] = + flt(parent_account[col.field]) + flt(account[col.field]); } } }); @@ -213,9 +260,11 @@ erpnext.AccountTreeGrid = class AccountTreeGrid extends frappe.views.TreeGridRep this.fiscal_year = null; var me = this; - $.each(frappe.report_dump.data["Fiscal Year"], function(i, v) { - if (me.opening_date >= frappe.datetime.str_to_obj(v.year_start_date) && - me.closing_date <= frappe.datetime.str_to_obj(v.year_end_date)) { + $.each(frappe.report_dump.data["Fiscal Year"], function (i, v) { + if ( + me.opening_date >= frappe.datetime.str_to_obj(v.year_start_date) && + me.closing_date <= frappe.datetime.str_to_obj(v.year_end_date) + ) { me.fiscal_year = v; } }); @@ -231,7 +280,7 @@ erpnext.AccountTreeGrid = class AccountTreeGrid extends frappe.views.TreeGridRep account: account, company: this.company, from_date: this.from_date, - to_date: this.to_date + to_date: this.to_date, }; frappe.set_route("query-report", "General Ledger"); } diff --git a/erpnext/public/js/address.js b/erpnext/public/js/address.js index 57f7163bbb2..606e0b119fd 100644 --- a/erpnext/public/js/address.js +++ b/erpnext/public/js/address.js @@ -2,24 +2,23 @@ // For license information, please see license.txt frappe.ui.form.on("Address", { - is_your_company_address: function(frm) { - frm.clear_table('links'); - if(frm.doc.is_your_company_address) { - frm.add_child('links', { - link_doctype: 'Company', - link_name: frappe.defaults.get_user_default('Company') + is_your_company_address: function (frm) { + frm.clear_table("links"); + if (frm.doc.is_your_company_address) { + frm.add_child("links", { + link_doctype: "Company", + link_name: frappe.defaults.get_user_default("Company"), }); - frm.set_query('link_doctype', 'links', () => { + frm.set_query("link_doctype", "links", () => { return { filters: { - name: 'Company' - } + name: "Company", + }, }; }); - frm.refresh_field('links'); + frm.refresh_field("links"); + } else { + frm.trigger("refresh"); } - else { - frm.trigger('refresh'); - } - } + }, }); diff --git a/erpnext/public/js/agriculture/ternary_plot.js b/erpnext/public/js/agriculture/ternary_plot.js index b06a1fd7c8c..4fc1c4fc3f1 100644 --- a/erpnext/public/js/agriculture/ternary_plot.js +++ b/erpnext/public/js/agriculture/ternary_plot.js @@ -1,10 +1,10 @@ -frappe.provide('agriculture'); +frappe.provide("agriculture"); agriculture.TernaryPlot = class TernaryPlot { constructor(opts) { Object.assign(this, opts); - frappe.require('assets/frappe/js/lib/snap.svg-min.js', () => { + frappe.require("assets/frappe/js/lib/snap.svg-min.js", () => { this.make_svg(); this.init_snap(); this.init_config(); @@ -29,124 +29,170 @@ agriculture.TernaryPlot = class TernaryPlot { triangle_side: 300, spacing: 50, strokeWidth: 1, - stroke: frappe.ui.color.get('black') + stroke: frappe.ui.color.get("black"), }; this.config.scaling_factor = this.config.triangle_side / 100; let { triangle_side: t, spacing: s, scaling_factor: p } = this.config; this.coords = { sand: { - points: [ - s + t * Snap.cos(60), s, - s, s + t * Snap.cos(30), - s + t, s + t * Snap.cos(30) - ], - color: frappe.ui.color.get('peach') + points: [s + t * Snap.cos(60), s, s, s + t * Snap.cos(30), s + t, s + t * Snap.cos(30)], + color: frappe.ui.color.get("peach"), }, loamy_sand: { points: [ - s + 15 * p * Snap.cos(60), s + (100 - 15) * p * Snap.cos(30), - s + 10 * p * Snap.cos(60), s + (100 - 10) * p * Snap.cos(30), - s + (100 - 85) * p, s + t * Snap.cos(30), - s + (100 - 70) * p, s + t * Snap.cos(30) + s + 15 * p * Snap.cos(60), + s + (100 - 15) * p * Snap.cos(30), + s + 10 * p * Snap.cos(60), + s + (100 - 10) * p * Snap.cos(30), + s + (100 - 85) * p, + s + t * Snap.cos(30), + s + (100 - 70) * p, + s + t * Snap.cos(30), ], - color: frappe.ui.color.get('pink') + color: frappe.ui.color.get("pink"), }, sandy_loam: { points: [ - s + 20 * p * Snap.cos(60) + 27.5 * p, s + (100 - 20) * p * Snap.cos(30), - s + 20 * p * Snap.cos(60), s + (100 - 20) * p * Snap.cos(30), - s + 15 * p * Snap.cos(60), s + (100 - 15) * p * Snap.cos(30), - s + (100 - 75) * p, s + t * Snap.cos(30), - s + (100 - 50) * p, s + t * Snap.cos(30), - s + (100 - 50) * p + 7.5 * p * Snap.cos(60), s + t * Snap.cos(30) - 7.5 * p * Snap.cos(30), - s + (100 - 50) * p + 7.5 * p * Snap.cos(60) - 10 * p, s + t * Snap.cos(30) - 7.5 * p * Snap.cos(30) + s + 20 * p * Snap.cos(60) + 27.5 * p, + s + (100 - 20) * p * Snap.cos(30), + s + 20 * p * Snap.cos(60), + s + (100 - 20) * p * Snap.cos(30), + s + 15 * p * Snap.cos(60), + s + (100 - 15) * p * Snap.cos(30), + s + (100 - 75) * p, + s + t * Snap.cos(30), + s + (100 - 50) * p, + s + t * Snap.cos(30), + s + (100 - 50) * p + 7.5 * p * Snap.cos(60), + s + t * Snap.cos(30) - 7.5 * p * Snap.cos(30), + s + (100 - 50) * p + 7.5 * p * Snap.cos(60) - 10 * p, + s + t * Snap.cos(30) - 7.5 * p * Snap.cos(30), ], - color: frappe.ui.color.get('pink', 'light') + color: frappe.ui.color.get("pink", "light"), }, loam: { points: [ - s + (100 - 50) * p + 27.5 * p * Snap.cos(60), s + t * Snap.cos(30) - 27.5 * p * Snap.cos(30), - s + (100 - 50) * p + 27.5 * p * Snap.cos(60) - 22.5 * p, s + t * Snap.cos(30) - 27.5 * p * Snap.cos(30), - s + 20 * p * Snap.cos(60) + 27.5 * p, s + (100 - 20) * p * Snap.cos(30), - s + (100 - 50) * p + 7.5 * p * Snap.cos(60) - 10 * p, s + t * Snap.cos(30) - 7.5 * p * Snap.cos(30), - s + (100 - 50) * p + 7.5 * p * Snap.cos(60), s + t * Snap.cos(30) - 7.5 * p * Snap.cos(30) + s + (100 - 50) * p + 27.5 * p * Snap.cos(60), + s + t * Snap.cos(30) - 27.5 * p * Snap.cos(30), + s + (100 - 50) * p + 27.5 * p * Snap.cos(60) - 22.5 * p, + s + t * Snap.cos(30) - 27.5 * p * Snap.cos(30), + s + 20 * p * Snap.cos(60) + 27.5 * p, + s + (100 - 20) * p * Snap.cos(30), + s + (100 - 50) * p + 7.5 * p * Snap.cos(60) - 10 * p, + s + t * Snap.cos(30) - 7.5 * p * Snap.cos(30), + s + (100 - 50) * p + 7.5 * p * Snap.cos(60), + s + t * Snap.cos(30) - 7.5 * p * Snap.cos(30), ], - color: frappe.ui.color.get('brown') + color: frappe.ui.color.get("brown"), }, silt_loam: { points: [ - s + t - 27.5 * p * Snap.cos(60), s + 72.5 * p * Snap.cos(30), - s + (100 - 50) * p + 27.5 * p * Snap.cos(60), s + t * Snap.cos(30) - 27.5 * p * Snap.cos(30), - s + (100 - 50) * p, s + t * Snap.cos(30), - s + (100 - 20) * p, s + t * Snap.cos(30), - s + (100 - 20) * p + 12.5 * p * Snap.cos(60), s + 90 * p * Snap.cos(30), - s + t - 12.5 * p * Snap.cos(60), s + (100 - 12.5) * p * Snap.cos(30) + s + t - 27.5 * p * Snap.cos(60), + s + 72.5 * p * Snap.cos(30), + s + (100 - 50) * p + 27.5 * p * Snap.cos(60), + s + t * Snap.cos(30) - 27.5 * p * Snap.cos(30), + s + (100 - 50) * p, + s + t * Snap.cos(30), + s + (100 - 20) * p, + s + t * Snap.cos(30), + s + (100 - 20) * p + 12.5 * p * Snap.cos(60), + s + 90 * p * Snap.cos(30), + s + t - 12.5 * p * Snap.cos(60), + s + (100 - 12.5) * p * Snap.cos(30), ], - color: frappe.ui.color.get('green', 'dark') + color: frappe.ui.color.get("green", "dark"), }, silt: { points: [ - s + t - 12.5 * p * Snap.cos(60), s + (100 - 12.5) * p * Snap.cos(30), - s + (100 - 20) * p + 12.5 * p * Snap.cos(60), s + 90 * p * Snap.cos(30), - s + (100 - 20) * p, s + t * Snap.cos(30), - s + t, s + t * Snap.cos(30) + s + t - 12.5 * p * Snap.cos(60), + s + (100 - 12.5) * p * Snap.cos(30), + s + (100 - 20) * p + 12.5 * p * Snap.cos(60), + s + 90 * p * Snap.cos(30), + s + (100 - 20) * p, + s + t * Snap.cos(30), + s + t, + s + t * Snap.cos(30), ], - color: frappe.ui.color.get('green') + color: frappe.ui.color.get("green"), }, silty_clay_loam: { points: [ - s + t - 40 * p * Snap.cos(60), s + 60 * p * Snap.cos(30), - s + t - 40 * p * Snap.cos(60) - 20 * p, s + 60 * p * Snap.cos(30), - s + t - 27.5 * p * Snap.cos(60) - 20 * p, s + 72.5 * p * Snap.cos(30), - s + t - 27.5 * p * Snap.cos(60), s + 72.5 * p * Snap.cos(30) + s + t - 40 * p * Snap.cos(60), + s + 60 * p * Snap.cos(30), + s + t - 40 * p * Snap.cos(60) - 20 * p, + s + 60 * p * Snap.cos(30), + s + t - 27.5 * p * Snap.cos(60) - 20 * p, + s + 72.5 * p * Snap.cos(30), + s + t - 27.5 * p * Snap.cos(60), + s + 72.5 * p * Snap.cos(30), ], - color: frappe.ui.color.get('cyan', 'dark') + color: frappe.ui.color.get("cyan", "dark"), }, silty_clay: { points: [ - s + t - 60 * p * Snap.cos(60), s + 40 * p * Snap.cos(30), - s + t - 40 * p * Snap.cos(60) - 20 * p, s + 60 * p * Snap.cos(30), - s + t - 40 * p * Snap.cos(60), s + 60 * p * Snap.cos(30) + s + t - 60 * p * Snap.cos(60), + s + 40 * p * Snap.cos(30), + s + t - 40 * p * Snap.cos(60) - 20 * p, + s + 60 * p * Snap.cos(30), + s + t - 40 * p * Snap.cos(60), + s + 60 * p * Snap.cos(30), ], - color: frappe.ui.color.get('cyan') + color: frappe.ui.color.get("cyan"), }, clay_loam: { points: [ - s + t - 40 * p * Snap.cos(60) - 20 * p, s + 60 * p * Snap.cos(30), - s + t - 40 * p * Snap.cos(60) - 45 * p, s + 60 * p * Snap.cos(30), - s + t - 27.5 * p * Snap.cos(60) - 45 * p, s + 72.5 * p * Snap.cos(30), - s + t - 27.5 * p * Snap.cos(60) - 20 * p, s + 72.5 * p * Snap.cos(30) + s + t - 40 * p * Snap.cos(60) - 20 * p, + s + 60 * p * Snap.cos(30), + s + t - 40 * p * Snap.cos(60) - 45 * p, + s + 60 * p * Snap.cos(30), + s + t - 27.5 * p * Snap.cos(60) - 45 * p, + s + 72.5 * p * Snap.cos(30), + s + t - 27.5 * p * Snap.cos(60) - 20 * p, + s + 72.5 * p * Snap.cos(30), ], - color: frappe.ui.color.get('green', 'light') + color: frappe.ui.color.get("green", "light"), }, sandy_clay_loam: { points: [ - s + 35 * p * Snap.cos(60) + 20 * p, s + (100 - 35) * p * Snap.cos(30), - s + 35 * p * Snap.cos(60), s + (100 - 35) * p * Snap.cos(30), - s + 20 * p * Snap.cos(60), s + (100 - 20) * p * Snap.cos(30), - s + 20 * p * Snap.cos(60) + 27.5 * p, s + (100 - 20) * p * Snap.cos(30), - s + t - 27.5 * p * Snap.cos(60) - 45 * p, s + 72.5 * p * Snap.cos(30) + s + 35 * p * Snap.cos(60) + 20 * p, + s + (100 - 35) * p * Snap.cos(30), + s + 35 * p * Snap.cos(60), + s + (100 - 35) * p * Snap.cos(30), + s + 20 * p * Snap.cos(60), + s + (100 - 20) * p * Snap.cos(30), + s + 20 * p * Snap.cos(60) + 27.5 * p, + s + (100 - 20) * p * Snap.cos(30), + s + t - 27.5 * p * Snap.cos(60) - 45 * p, + s + 72.5 * p * Snap.cos(30), ], - color: frappe.ui.color.get('pink', 'dark') + color: frappe.ui.color.get("pink", "dark"), }, sandy_clay: { points: [ - s + 55 * p * Snap.cos(60), s + (100 - 55) * p * Snap.cos(30), - s + 35 * p * Snap.cos(60), s + (100 - 35) * p * Snap.cos(30), - s + 35 * p * Snap.cos(60) + 20 * p, s + (100 - 35) * p * Snap.cos(30) + s + 55 * p * Snap.cos(60), + s + (100 - 55) * p * Snap.cos(30), + s + 35 * p * Snap.cos(60), + s + (100 - 35) * p * Snap.cos(30), + s + 35 * p * Snap.cos(60) + 20 * p, + s + (100 - 35) * p * Snap.cos(30), ], - color: frappe.ui.color.get('red') + color: frappe.ui.color.get("red"), }, clay: { points: [ - s + t * Snap.cos(60), s, - s + 55 * p * Snap.cos(60), s + (100 - 55) * p * Snap.cos(30), - s + t - 40 * p * Snap.cos(60) - 45 * p, s + 60 * p * Snap.cos(30), - s + t - 40 * p * Snap.cos(60) - 20 * p, s + 60 * p * Snap.cos(30), - s + t - 60 * p * Snap.cos(60), s + 40 * p * Snap.cos(30) + s + t * Snap.cos(60), + s, + s + 55 * p * Snap.cos(60), + s + (100 - 55) * p * Snap.cos(30), + s + t - 40 * p * Snap.cos(60) - 45 * p, + s + 60 * p * Snap.cos(30), + s + t - 40 * p * Snap.cos(60) - 20 * p, + s + 60 * p * Snap.cos(30), + s + t - 60 * p * Snap.cos(60), + s + 40 * p * Snap.cos(30), ], - color: frappe.ui.color.get('yellow') + color: frappe.ui.color.get("yellow"), }, }; } @@ -164,7 +210,7 @@ agriculture.TernaryPlot = class TernaryPlot { this.paper.polygon(this.get_coords(soil_type)).attr({ fill: this.get_color(soil_type), stroke: this.config.stroke, - strokeWidth: this.config.strokeWidth + strokeWidth: this.config.strokeWidth, }); } } @@ -172,18 +218,18 @@ agriculture.TernaryPlot = class TernaryPlot { make_plot_marking() { let { triangle_side: t, spacing: s, scaling_factor: p } = this.config; - let clay = this.paper.text(t * Snap.cos(60) / 2, s + t * Snap.cos(30) / 2, __("Clay")).attr({ - fill: frappe.ui.color.get('black') + let clay = this.paper.text((t * Snap.cos(60)) / 2, s + (t * Snap.cos(30)) / 2, __("Clay")).attr({ + fill: frappe.ui.color.get("black"), }); clay.transform("r300"); - let silt = this.paper.text(t, s + t * Snap.cos(30) / 2, __("Silt")).attr({ - fill: frappe.ui.color.get('black') + let silt = this.paper.text(t, s + (t * Snap.cos(30)) / 2, __("Silt")).attr({ + fill: frappe.ui.color.get("black"), }); silt.transform("r60"); let sand = this.paper.text(35 + t * Snap.cos(60), 90 + t * Snap.cos(30), __("Sand")).attr({ - fill: frappe.ui.color.get('black') + fill: frappe.ui.color.get("black"), }); sand.transform("r0"); } @@ -194,25 +240,25 @@ agriculture.TernaryPlot = class TernaryPlot { let offset = 0; let exec_once = true; for (let soil_type in this.coords) { - if (index > 6 && exec_once){ + if (index > 6 && exec_once) { offset = 300; index = 1; exec_once = false; } - let rect = this.paper.rect(0+offset, 0+index*20, 100, 19, 5, 5).attr({ + let rect = this.paper.rect(0 + offset, 0 + index * 20, 100, 19, 5, 5).attr({ fill: this.get_color(soil_type), - stroke: frappe.ui.color.get('black') + stroke: frappe.ui.color.get("black"), }); - let text = this.paper.text(5+offset, 16+index*20, soil_type).attr({ - fill: frappe.ui.color.get('black'), - 'font-size': 12 + let text = this.paper.text(5 + offset, 16 + index * 20, soil_type).attr({ + fill: frappe.ui.color.get("black"), + "font-size": 12, }); index++; } } - mark_blip({clay, sand, silt} = this) { - if (clay + sand + silt != 0){ + mark_blip({ clay, sand, silt } = this) { + if (clay + sand + silt != 0) { let { triangle_side: t, spacing: s, scaling_factor: p } = this.config; let x_blip = s + clay * p * Snap.cos(60) + silt * p; @@ -220,13 +266,12 @@ agriculture.TernaryPlot = class TernaryPlot { this.blip = this.paper.circle(x_blip, y_blip, 4).attr({ fill: frappe.ui.color.get("orange"), stroke: frappe.ui.color.get("orange"), - strokeWidth: 2 + strokeWidth: 2, }); } } remove_blip() { - if (typeof this.blip !== 'undefined') - this.blip.remove(); + if (typeof this.blip !== "undefined") this.blip.remove(); } }; diff --git a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js index 0cda93880fa..a88d00c5d35 100644 --- a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js @@ -18,12 +18,11 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager { make_dt() { var me = this; frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions", args: { bank_account: this.bank_account, from_date: this.bank_statement_from_date, - to_date: this.bank_statement_to_date + to_date: this.bank_statement_to_date, }, callback: function (response) { me.format_data(response.message); @@ -62,18 +61,14 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager { editable: false, width: 100, format: (value) => - "" + - format_currency(value, this.currency) + - "", + "" + format_currency(value, this.currency) + "", }, { name: __("Withdrawal"), editable: false, width: 100, format: (value) => - "" + - format_currency(value, this.currency) + - "", + "" + format_currency(value, this.currency) + "", }, { name: __("Unallocated Amount"), @@ -139,14 +134,8 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager { checkboxColumn: false, inlineFilters: true, }; - this.datatable = new frappe.DataTable( - this.$reconciliation_tool_dt.get(0), - datatable_options - ); - $(`.${this.datatable.style.scopeClass} .dt-scrollable`).css( - "max-height", - "calc(100vh - 400px)" - ); + this.datatable = new frappe.DataTable(this.$reconciliation_tool_dt.get(0), datatable_options); + $(`.${this.datatable.style.scopeClass} .dt-scrollable`).css("max-height", "calc(100vh - 400px)"); if (this.transactions.length > 0) { this.$reconciliation_tool_dt.show(); @@ -159,27 +148,18 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager { set_listeners() { var me = this; - $(`.${this.datatable.style.scopeClass} .dt-scrollable`).on( - "click", - `.btn`, - function () { - me.dialog_manager.show_dialog( - $(this).attr("data-name"), - (bank_transaction) => me.update_dt_cards(bank_transaction) - ); - return true; - } - ); + $(`.${this.datatable.style.scopeClass} .dt-scrollable`).on("click", `.btn`, function () { + me.dialog_manager.show_dialog($(this).attr("data-name"), (bank_transaction) => + me.update_dt_cards(bank_transaction) + ); + return true; + }); } update_dt_cards(bank_transaction) { - const transaction_index = this.transaction_dt_map[ - bank_transaction.name - ]; + const transaction_index = this.transaction_dt_map[bank_transaction.name]; if (bank_transaction.unallocated_amount > 0) { - this.transactions[transaction_index] = this.format_row( - bank_transaction - ); + this.transactions[transaction_index] = this.format_row(bank_transaction); } else { this.transactions.splice(transaction_index, 1); for (const [k, v] of Object.entries(this.transaction_dt_map)) { @@ -195,14 +175,9 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager { // this.make_dt(); this.get_cleared_balance().then(() => { - this.cards_manager.$cards[1].set_value( - format_currency(this.cleared_balance), - this.currency - ); + this.cards_manager.$cards[1].set_value(format_currency(this.cleared_balance), this.currency); this.cards_manager.$cards[2].set_value( - format_currency( - this.bank_statement_closing_balance - this.cleared_balance - ), + format_currency(this.bank_statement_closing_balance - this.cleared_balance), this.currency ); this.cards_manager.$cards[2].set_value_color( @@ -216,14 +191,12 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager { get_cleared_balance() { if (this.bank_account && this.bank_statement_to_date) { return frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance", args: { bank_account: this.bank_account, till_date: this.bank_statement_to_date, }, - callback: (response) => - (this.cleared_balance = response.message), + callback: (response) => (this.cleared_balance = response.message), }); } } diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index ddc10530581..5cab0efa0e1 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -1,7 +1,15 @@ frappe.provide("erpnext.accounts.bank_reconciliation"); erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { - constructor(company, bank_account, bank_statement_from_date, bank_statement_to_date, filter_by_reference_date, from_reference_date, to_reference_date) { + constructor( + company, + bank_account, + bank_statement_from_date, + bank_statement_to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date + ) { this.bank_account = bank_account; this.company = company; this.make_dialog(); @@ -61,22 +69,20 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { get_linked_vouchers(document_types) { frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_linked_payments", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_linked_payments", args: { bank_transaction_name: this.bank_transaction_name, document_types: document_types, from_date: this.bank_statement_from_date, to_date: this.bank_statement_to_date, filter_by_reference_date: this.filter_by_reference_date, - from_reference_date:this.from_reference_date, - to_reference_date:this.to_reference_date + from_reference_date: this.from_reference_date, + to_reference_date: this.to_reference_date, }, callback: (result) => { const data = result.message; - if (data && data.length > 0) { const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper; proposals_wrapper.show(); @@ -99,7 +105,6 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper; proposals_wrapper.hide(); this.dialog.fields_dict.no_matching_vouchers.$wrapper.show(); - } this.dialog.show(); }, @@ -118,7 +123,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { editable: false, width: 1, format: (value, row) => { - return frappe.form.formatters.Link(value, {options: row[2].content}); + return frappe.form.formatters.Link(value, { options: row[2].content }); }, }, { @@ -153,10 +158,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { checkboxColumn: true, inlineFilters: true, }; - this.datatable = new frappe.DataTable( - proposals_wrapper.get(0), - datatable_options - ); + this.datatable = new frappe.DataTable(proposals_wrapper.get(0), datatable_options); } else { this.datatable.refresh(this.data, this.columns); this.datatable.rowmanager.checkMap = []; @@ -218,10 +220,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { title: __("Reconcile the Bank Transaction"), fields: fields, size: "large", - primary_action: (values) => - this.reconciliation_dialog_primary_action(values), + primary_action: (values) => this.reconciliation_dialog_primary_action(values), }); - } + }, }); } @@ -256,7 +257,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { { fieldtype: "HTML", fieldname: "no_matching_vouchers", - options: __("
    {0}
    ", [__("No Matching Vouchers Found")]) + options: __('
    {0}
    ', [ + __("No Matching Vouchers Found"), + ]), }, { fieldtype: "Section Break", @@ -300,8 +303,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { click: () => { this.edit_in_full_page(); }, - depends_on: - "eval:doc.action=='Create Voucher'", + depends_on: "eval:doc.action=='Create Voucher'", }, { fieldname: "column_break_7", @@ -314,8 +316,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { label: "Journal Entry Type", options: "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense", - depends_on: - "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'", + depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'", mandatory_depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'", }, @@ -324,8 +325,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldtype: "Link", label: "Account", options: "Account", - depends_on: - "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'", + depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'", mandatory_depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'", get_query: () => { @@ -343,14 +343,11 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { label: "Party Type", options: "DocType", mandatory_depends_on: - "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'", + "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'", get_query: function () { return { filters: { - name: [ - "in", - Object.keys(frappe.boot.party_account_types), - ], + name: ["in", Object.keys(frappe.boot.party_account_types)], }, }; }, @@ -368,16 +365,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldtype: "Link", label: "Project", options: "Project", - depends_on: - "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'", + depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'", }, { fieldname: "cost_center", fieldtype: "Link", label: "Cost Center", options: "Cost Center", - depends_on: - "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'", + depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'", }, { fieldtype: "Section Break", @@ -436,7 +431,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { options: "Currency", read_only: 1, hidden: 1, - } + }, ]; } @@ -458,18 +453,11 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { reconciliation_dialog_primary_action(values) { if (values.action == "Match Against Voucher") this.match(values); - if ( - values.action == "Create Voucher" && - values.document_type == "Payment Entry" - ) + if (values.action == "Create Voucher" && values.document_type == "Payment Entry") this.add_payment_entry(values); - if ( - values.action == "Create Voucher" && - values.document_type == "Journal Entry" - ) + if (values.action == "Create Voucher" && values.document_type == "Journal Entry") this.add_journal_entry(values); - else if (values.action == "Update Bank Transaction") - this.update_transaction(values); + else if (values.action == "Update Bank Transaction") this.update_transaction(values); } match() { @@ -487,8 +475,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { }); }); frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.reconcile_vouchers", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.reconcile_vouchers", args: { bank_transaction_name: this.bank_transaction.name, vouchers: vouchers, @@ -504,8 +491,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { add_payment_entry(values) { frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts", args: { bank_transaction_name: this.bank_transaction.name, reference_number: values.reference_number, @@ -518,7 +504,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { cost_center: values.cost_center, }, callback: (response) => { - const alert_string = __("Bank Transaction {0} added as Payment Entry", [this.bank_transaction.name]); + const alert_string = __("Bank Transaction {0} added as Payment Entry", [ + this.bank_transaction.name, + ]); frappe.show_alert(alert_string); this.update_dt_cards(response.message); this.dialog.hide(); @@ -528,8 +516,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { add_journal_entry(values) { frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts", args: { bank_transaction_name: this.bank_transaction.name, reference_number: values.reference_number, @@ -542,7 +529,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { second_account: values.second_account, }, callback: (response) => { - const alert_string = __("Bank Transaction {0} added as Journal Entry", [this.bank_transaction.name]); + const alert_string = __("Bank Transaction {0} added as Journal Entry", [ + this.bank_transaction.name, + ]); frappe.show_alert(alert_string); this.update_dt_cards(response.message); this.dialog.hide(); @@ -552,8 +541,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { update_transaction(values) { frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.update_bank_transaction", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.update_bank_transaction", args: { bank_transaction_name: this.bank_transaction.name, reference_number: values.reference_number, @@ -573,8 +561,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { const values = this.dialog.get_values(true); if (values.document_type == "Payment Entry") { frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts", args: { bank_transaction_name: this.bank_transaction.name, reference_number: values.reference_number, @@ -585,7 +572,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { mode_of_payment: values.mode_of_payment, project: values.project, cost_center: values.cost_center, - allow_edit: true + allow_edit: true, }, callback: (r) => { const doc = frappe.model.sync(r.message); @@ -594,8 +581,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { }); } else { frappe.call({ - method: - "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts", + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts", args: { bank_transaction_name: this.bank_transaction.name, reference_number: values.reference_number, @@ -606,7 +592,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { mode_of_payment: values.mode_of_payment, entry_type: values.journal_entry_type, second_account: values.second_account, - allow_edit: true + allow_edit: true, }, callback: (r) => { var doc = frappe.model.sync(r.message); @@ -615,5 +601,4 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { }); } } - }; diff --git a/erpnext/public/js/bank_reconciliation_tool/number_card.js b/erpnext/public/js/bank_reconciliation_tool/number_card.js index 7e1b2690a80..d39884c381e 100644 --- a/erpnext/public/js/bank_reconciliation_tool/number_card.js +++ b/erpnext/public/js/bank_reconciliation_tool/number_card.js @@ -26,8 +26,7 @@ erpnext.accounts.bank_reconciliation.NumberCardManager = class NumberCardManager currency: this.currency, }, { - value: - this.bank_statement_closing_balance - this.cleared_balance, + value: this.bank_statement_closing_balance - this.cleared_balance, label: __("Difference"), datatype: "Currency", currency: this.currency, @@ -41,11 +40,9 @@ erpnext.accounts.bank_reconciliation.NumberCardManager = class NumberCardManager number_card.$card.appendTo(this.$summary); }); this.$cards[2].set_value_color( - this.bank_statement_closing_balance - this.cleared_balance == 0 - ? "text-success" - : "text-danger" + this.bank_statement_closing_balance - this.cleared_balance == 0 ? "text-success" : "text-danger" ); - this.$summary.css({"border-bottom": "0px", "margin-left": "0px", "margin-right": "0px"}); + this.$summary.css({ "border-bottom": "0px", "margin-left": "0px", "margin-right": "0px" }); this.$summary.show(); } }; @@ -60,16 +57,10 @@ erpnext.accounts.NumberCard = class NumberCard { } set_value_color(color) { - this.$card - .find("div") - .removeClass("text-danger text-success") - .addClass(`${color}`); + this.$card.find("div").removeClass("text-danger text-success").addClass(`${color}`); } set_indicator(color) { - this.$card - .find("span") - .removeClass("indicator red green") - .addClass(`indicator ${color}`); + this.$card.find("span").removeClass("indicator red green").addClass(`indicator ${color}`); } }; diff --git a/erpnext/public/js/bulk_transaction_processing.js b/erpnext/public/js/bulk_transaction_processing.js index 101f50c64aa..f360bf5cd2c 100644 --- a/erpnext/public/js/bulk_transaction_processing.js +++ b/erpnext/public/js/bulk_transaction_processing.js @@ -1,30 +1,33 @@ frappe.provide("erpnext.bulk_transaction_processing"); $.extend(erpnext.bulk_transaction_processing, { - create: function(listview, from_doctype, to_doctype) { + create: function (listview, from_doctype, to_doctype) { let checked_items = listview.get_checked_items(); const doc_name = []; - checked_items.forEach((Item)=> { + checked_items.forEach((Item) => { if (Item.docstatus == 0) { doc_name.push(Item.name); } }); let count_of_rows = checked_items.length; - frappe.confirm(__("Create {0} {1} ?", [count_of_rows, to_doctype]), ()=>{ + frappe.confirm(__("Create {0} {1} ?", [count_of_rows, to_doctype]), () => { if (doc_name.length == 0) { - frappe.call({ - method: "erpnext.utilities.bulk_transaction.transaction_processing", - args: {data: checked_items, from_doctype: from_doctype, to_doctype: to_doctype} - }).then(()=> { - - }); + frappe + .call({ + method: "erpnext.utilities.bulk_transaction.transaction_processing", + args: { data: checked_items, from_doctype: from_doctype, to_doctype: to_doctype }, + }) + .then(() => {}); if (count_of_rows > 10) { - frappe.show_alert("Starting a background job to create {0} {1}", [count_of_rows, to_doctype]); + frappe.show_alert("Starting a background job to create {0} {1}", [ + count_of_rows, + to_doctype, + ]); } } else { frappe.msgprint(__("Selected document must be in submitted state")); } }); - } -}); \ No newline at end of file + }, +}); diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 2dbe999e05b..7155c799ddb 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -7,17 +7,17 @@ class CallPopup { } make() { - frappe.utils.play_sound('incoming-call'); + frappe.utils.play_sound("incoming-call"); this.dialog = new frappe.ui.Dialog({ - 'static': true, - 'minimizable': true + static: true, + minimizable: true, }); this.dialog.get_close_btn().show(); this.setup_dialog(); this.set_call_status(); frappe.utils.bind_actions_with_object(this.dialog.$body, this); - this.dialog.$wrapper.addClass('call-popup'); - this.dialog.get_close_btn().unbind('click').click(this.close_modal.bind(this)); + this.dialog.$wrapper.addClass("call-popup"); + this.dialog.get_close_btn().unbind("click").click(this.close_modal.bind(this)); this.dialog.show(); } @@ -26,28 +26,28 @@ class CallPopup { this.dialog.$body.empty().append(this.caller_info); } - set_indicator(color, blink=false) { - let classes = `indicator ${color} ${blink ? 'blink': ''}`; - this.dialog.header.find('.indicator').attr('class', classes); + set_indicator(color, blink = false) { + let classes = `indicator ${color} ${blink ? "blink" : ""}`; + this.dialog.header.find(".indicator").attr("class", classes); } set_call_status(call_status) { - let title = ''; + let title = ""; call_status = call_status || this.call_log.status; - if (['Ringing'].includes(call_status) || !call_status) { - title = __('Incoming call from {0}', [this.get_caller_name() || this.caller_number]); - this.set_indicator('blue', true); - } else if (call_status === 'In Progress') { - title = __('Call Connected'); - this.set_indicator('green'); - } else if (['No Answer', 'Missed'].includes(call_status)) { - this.set_indicator('yellow'); - title = __('Call Missed'); - } else if (['Completed', 'Busy', 'Failed'].includes(call_status)) { - this.set_indicator('red'); - title = __('Call Ended'); + if (["Ringing"].includes(call_status) || !call_status) { + title = __("Incoming call from {0}", [this.get_caller_name() || this.caller_number]); + this.set_indicator("blue", true); + } else if (call_status === "In Progress") { + title = __("Call Connected"); + this.set_indicator("green"); + } else if (["No Answer", "Missed"].includes(call_status)) { + this.set_indicator("yellow"); + title = __("Call Missed"); + } else if (["Completed", "Busy", "Failed"].includes(call_status)) { + this.set_indicator("red"); + title = __("Call Ended"); } else { - this.set_indicator('blue'); + this.set_indicator("blue"); title = call_status; } this.dialog.set_title(title); @@ -55,7 +55,7 @@ class CallPopup { update_call_log(call_log, missed) { this.call_log = call_log; - this.set_call_status(missed ? 'Missed': null); + this.set_call_status(missed ? "Missed" : null); } close_modal() { @@ -64,10 +64,10 @@ class CallPopup { } call_ended(call_log, missed) { - frappe.utils.play_sound('call-disconnect'); + frappe.utils.play_sound("call-disconnect"); this.update_call_log(call_log, missed); setTimeout(() => { - if (!this.dialog.get_value('call_summary')) { + if (!this.dialog.get_value("call_summary")) { this.close_modal(); } }, 60000); @@ -81,16 +81,16 @@ class CallPopup { get_contact_link() { let log = this.call_log; - let contact_link = log.links.find(d => d.link_doctype === 'Contact'); + let contact_link = log.links.find((d) => d.link_doctype === "Contact"); return contact_link || {}; } setup_listener() { - frappe.realtime.on(`call_${this.call_log.id}_ended`, call_log => { + frappe.realtime.on(`call_${this.call_log.id}_ended`, (call_log) => { this.call_ended(call_log); }); - frappe.realtime.on(`call_${this.call_log.id}_missed`, call_log => { + frappe.realtime.on(`call_${this.call_log.id}_missed`, (call_log) => { this.call_ended(call_log, true); }); } @@ -103,84 +103,102 @@ class CallPopup { setup_call_details() { this.caller_info = $(`
    `); this.call_details = new frappe.ui.FieldGroup({ - fields: [{ - 'fieldname': 'name', - 'label': 'Name', - 'default': this.get_caller_name() || __('Unknown Caller'), - 'fieldtype': 'Data', - 'read_only': 1 - }, { - 'fieldtype': 'Button', - 'label': __('Open Contact'), - 'click': () => frappe.set_route('Form', 'Contact', this.get_contact_link().link_name), - 'depends_on': () => this.get_caller_name() - }, { - 'fieldtype': 'Button', - 'label': __('Create New Contact'), - 'click': this.create_new_contact.bind(this), - 'depends_on': () => !this.get_caller_name() - }, { - 'fieldtype': 'Button', - 'label': __('Create New Customer'), - 'click': this.create_new_customer.bind(this), - 'depends_on': () => !this.get_caller_name() - }, { - 'fieldtype': 'Button', - 'label': __('Create New Lead'), - 'click': () => frappe.new_doc('Lead', { 'mobile_no': this.caller_number }), - 'depends_on': () => !this.get_caller_name() - }, { - 'fieldtype': 'Column Break', - }, { - 'fieldname': 'number', - 'label': 'Phone Number', - 'fieldtype': 'Data', - 'default': this.caller_number, - 'read_only': 1 - }, { - 'fieldtype': 'Section Break', - 'hide_border': 1, - }, { - 'fieldname': 'call_type', - 'label': 'Call Type', - 'fieldtype': 'Link', - 'options': 'Telephony Call Type', - }, { - 'fieldtype': 'Section Break', - 'hide_border': 1, - }, { - 'fieldtype': 'Small Text', - 'label': __('Call Summary'), - 'fieldname': 'call_summary', - }, { - 'fieldtype': 'Button', - 'label': __('Save'), - 'click': () => { - const call_summary = this.call_details.get_value('call_summary'); - const call_type = this.call_details.get_value('call_type'); - if (!call_summary) return; - frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary_and_call_type', { - 'call_log': this.call_log.name, - 'summary': call_summary, - 'call_type': call_type, - }).then(() => { - this.close_modal(); - frappe.show_alert({ - message: ` - ${__('Call Summary Saved')} + fields: [ + { + fieldname: "name", + label: "Name", + default: this.get_caller_name() || __("Unknown Caller"), + fieldtype: "Data", + read_only: 1, + }, + { + fieldtype: "Button", + label: __("Open Contact"), + click: () => frappe.set_route("Form", "Contact", this.get_contact_link().link_name), + depends_on: () => this.get_caller_name(), + }, + { + fieldtype: "Button", + label: __("Create New Contact"), + click: this.create_new_contact.bind(this), + depends_on: () => !this.get_caller_name(), + }, + { + fieldtype: "Button", + label: __("Create New Customer"), + click: this.create_new_customer.bind(this), + depends_on: () => !this.get_caller_name(), + }, + { + fieldtype: "Button", + label: __("Create New Lead"), + click: () => frappe.new_doc("Lead", { mobile_no: this.caller_number }), + depends_on: () => !this.get_caller_name(), + }, + { + fieldtype: "Column Break", + }, + { + fieldname: "number", + label: "Phone Number", + fieldtype: "Data", + default: this.caller_number, + read_only: 1, + }, + { + fieldtype: "Section Break", + hide_border: 1, + }, + { + fieldname: "call_type", + label: "Call Type", + fieldtype: "Link", + options: "Telephony Call Type", + }, + { + fieldtype: "Section Break", + hide_border: 1, + }, + { + fieldtype: "Small Text", + label: __("Call Summary"), + fieldname: "call_summary", + }, + { + fieldtype: "Button", + label: __("Save"), + click: () => { + const call_summary = this.call_details.get_value("call_summary"); + const call_type = this.call_details.get_value("call_type"); + if (!call_summary) return; + frappe + .xcall( + "erpnext.telephony.doctype.call_log.call_log.add_call_summary_and_call_type", + { + call_log: this.call_log.name, + summary: call_summary, + call_type: call_type, + } + ) + .then(() => { + this.close_modal(); + frappe.show_alert({ + message: ` + ${__("Call Summary Saved")}
    - ${__('View call log')} + ${__("View call log")} `, - indicator: 'green' - }); - }); - } - }], - body: this.caller_info + indicator: "green", + }); + }); + }, + }, + ], + body: this.caller_info, }); this.call_details.make(); } @@ -191,23 +209,23 @@ class CallPopup { create_new_customer() { // to avoid quick entry form - const new_customer = frappe.model.get_new_doc('Customer'); + const new_customer = frappe.model.get_new_doc("Customer"); new_customer.mobile_no = this.caller_number; - frappe.set_route('Form', new_customer.doctype, new_customer.name); + frappe.set_route("Form", new_customer.doctype, new_customer.name); } create_new_contact() { // TODO: fix new_doc, it should accept child table values - const new_contact = frappe.model.get_new_doc('Contact'); - const phone_no = frappe.model.add_child(new_contact, 'Contact Phone', 'phone_nos'); + const new_contact = frappe.model.get_new_doc("Contact"); + const phone_no = frappe.model.add_child(new_contact, "Contact Phone", "phone_nos"); phone_no.phone = this.caller_number; phone_no.is_primary_mobile_no = 1; - frappe.set_route('Form', new_contact.doctype, new_contact.name); + frappe.set_route("Form", new_contact.doctype, new_contact.name); } } -$(document).on('app_ready', function () { - frappe.realtime.on('show_call_popup', call_log => { +$(document).on("app_ready", function () { + frappe.realtime.on("show_call_popup", (call_log) => { let call_popup = erpnext.call_popup; if (call_popup && call_log.name === call_popup.call_log.name) { call_popup.update_call_log(call_log); diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js index f205d889658..d9187f8b678 100644 --- a/erpnext/public/js/communication.js +++ b/erpnext/public/js/communication.js @@ -1,33 +1,45 @@ frappe.ui.form.on("Communication", { refresh: (frm) => { // setup custom Make button only if Communication is Email - if(frm.doc.communication_medium == "Email" && frm.doc.sent_or_received == "Received") { + if (frm.doc.communication_medium == "Email" && frm.doc.sent_or_received == "Received") { frm.events.setup_custom_buttons(frm); } }, setup_custom_buttons: (frm) => { let confirm_msg = "Are you sure you want to create {0} from this email?"; - if(frm.doc.reference_doctype !== "Issue") { - frm.add_custom_button(__("Issue"), () => { - frappe.confirm(__(confirm_msg, [__("Issue")]), () => { - frm.trigger('make_issue_from_communication'); - }) - }, __("Create")); + if (frm.doc.reference_doctype !== "Issue") { + frm.add_custom_button( + __("Issue"), + () => { + frappe.confirm(__(confirm_msg, [__("Issue")]), () => { + frm.trigger("make_issue_from_communication"); + }); + }, + __("Create") + ); } - if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) { - frm.add_custom_button(__("Lead"), () => { - frappe.confirm(__(confirm_msg, [__("Lead")]), () => { - frm.trigger('make_lead_from_communication'); - }) - }, __('Create')); + if (!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) { + frm.add_custom_button( + __("Lead"), + () => { + frappe.confirm(__(confirm_msg, [__("Lead")]), () => { + frm.trigger("make_lead_from_communication"); + }); + }, + __("Create") + ); - frm.add_custom_button(__("Opportunity"), () => { - frappe.confirm(__(confirm_msg, [__("Opportunity")]), () => { - frm.trigger('make_opportunity_from_communication'); - }) - }, __('Create')); + frm.add_custom_button( + __("Opportunity"), + () => { + frappe.confirm(__(confirm_msg, [__("Opportunity")]), () => { + frm.trigger("make_opportunity_from_communication"); + }); + }, + __("Create") + ); } }, @@ -35,63 +47,69 @@ frappe.ui.form.on("Communication", { return frappe.call({ method: "erpnext.crm.doctype.lead.lead.make_lead_from_communication", args: { - communication: frm.doc.name + communication: frm.doc.name, }, freeze: true, callback: (r) => { - if(r.message) { - frm.reload_doc() + if (r.message) { + frm.reload_doc(); } - } - }) + }, + }); }, make_issue_from_communication: (frm) => { return frappe.call({ method: "erpnext.support.doctype.issue.issue.make_issue_from_communication", args: { - communication: frm.doc.name + communication: frm.doc.name, }, freeze: true, callback: (r) => { - if(r.message) { - frm.reload_doc() + if (r.message) { + frm.reload_doc(); } - } - }) + }, + }); }, make_opportunity_from_communication: (frm) => { - const fields = [{ - fieldtype: 'Link', - label: __('Select a Company'), - fieldname: 'company', - options: 'Company', - reqd: 1, - default: frappe.defaults.get_user_default("Company") - }]; + const fields = [ + { + fieldtype: "Link", + label: __("Select a Company"), + fieldname: "company", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), + }, + ]; - frappe.prompt(fields, data => { - frappe.call({ - method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication", - args: { - communication: frm.doc.name, - company: data.company - }, - freeze: true, - callback: (r) => { - if(r.message) { - frm.reload_doc(); - frappe.show_alert({ - message: __("Opportunity {0} created", - ['' + r.message + '']), - indicator: 'green' - }); - } - } - }); - }, - 'Create an Opportunity', - 'Create'); - } + frappe.prompt( + fields, + (data) => { + frappe.call({ + method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication", + args: { + communication: frm.doc.name, + company: data.company, + }, + freeze: true, + callback: (r) => { + if (r.message) { + frm.reload_doc(); + frappe.show_alert({ + message: __("Opportunity {0} created", [ + '' + r.message + "", + ]), + indicator: "green", + }); + } + }, + }); + }, + "Create an Opportunity", + "Create" + ); + }, }); diff --git a/erpnext/public/js/conf.js b/erpnext/public/js/conf.js index a0f56a2d07f..2e6e9ba1ad4 100644 --- a/erpnext/public/js/conf.js +++ b/erpnext/public/js/conf.js @@ -1,26 +1,26 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.provide('erpnext'); +frappe.provide("erpnext"); // preferred modules for breadcrumbs $.extend(frappe.breadcrumbs.preferred, { "Item Group": "Stock", "Customer Group": "Selling", "Supplier Group": "Buying", - "Territory": "Selling", + Territory: "Selling", "Sales Person": "Selling", "Sales Partner": "Selling", - "Brand": "Stock", + Brand: "Stock", "Maintenance Schedule": "Support", - "Maintenance Visit": "Support" + "Maintenance Visit": "Support", }); $.extend(frappe.breadcrumbs.module_map, { - 'ERPNext Integrations': 'Integrations', - 'Geo': 'Settings', - 'Portal': 'Website', - 'Utilities': 'Settings', - 'E-commerce': 'Website', - 'Contacts': 'CRM' + "ERPNext Integrations": "Integrations", + Geo: "Settings", + Portal: "Website", + Utilities: "Settings", + "E-commerce": "Website", + Contacts: "CRM", }); diff --git a/erpnext/public/js/contact.js b/erpnext/public/js/contact.js index 41a0e8a9f99..ecfc4eeef93 100644 --- a/erpnext/public/js/contact.js +++ b/erpnext/public/js/contact.js @@ -1,16 +1,14 @@ - - frappe.ui.form.on("Contact", { refresh(frm) { - frm.set_query('link_doctype', "links", function() { + frm.set_query("link_doctype", "links", function () { return { query: "frappe.contacts.address_and_contact.filter_dynamic_link_doctypes", filters: { fieldtype: ["in", ["HTML", "Text Editor"]], fieldname: ["in", ["contact_html", "company_description"]], - } + }, }; }); frm.refresh_field("links"); - } + }, }); diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js index 2674df9c4af..71de690b835 100644 --- a/erpnext/public/js/controllers/stock_controller.js +++ b/erpnext/public/js/controllers/stock_controller.js @@ -11,6 +11,18 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con } } + barcode(doc, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.barcode) { + erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => { + frappe.model.set_value(cdt, cdn, { + "item_code": r.message.item_code, + "qty": 1, + }); + }); + } + } + setup_warehouse_query() { var me = this; erpnext.queries.setup_queries(this.frm, "Warehouse", function() { diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index c179928b511..186c342a75e 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -22,7 +22,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { item_rate = flt(item.rate_with_margin , precision("rate", item)); - if (item.discount_percentage) { + if (item.discount_percentage && !item.discount_amount) { item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100; } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0f291e1eba7..66ac9582b2d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -140,7 +140,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } if(this.frm.fields_dict["items"]) { - this["items_remove"] = this.calculate_net_weight; + this["items_remove"] = this.process_item_removal; } if(this.frm.fields_dict["recurring_print_format"]) { @@ -354,6 +354,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe barcode_scanner.process_scan(); } + barcode(doc, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.barcode) { + erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => { + debugger + frappe.model.set_value(cdt, cdn, { + "item_code": r.message.item_code, + "qty": 1, + }); + }); + } + } + validate_has_items () { let table = this.frm.doc.items; this.frm.has_items = (table && table.length @@ -739,21 +752,21 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (me.frm.doc.price_list_currency == company_currency) { me.frm.set_value('plc_conversion_rate', 1.0); } - if (company_doc.default_letter_head) { + if (company_doc && company_doc.default_letter_head) { if(me.frm.fields_dict.letter_head) { me.frm.set_value("letter_head", company_doc.default_letter_head); } } let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"]; if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && - selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) { + selling_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) { me.frm.set_value("tc_name", company_doc.default_selling_terms); } let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order", "Material Request", "Purchase Receipt"]; // Purchase Invoice is excluded as per issue #3345 if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && - buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) { + buying_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) { me.frm.set_value("tc_name", company_doc.default_buying_terms); } @@ -1192,6 +1205,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + process_item_removal() { + this.frm.trigger("calculate_taxes_and_totals"); + this.frm.trigger("calculate_net_weight"); + } + calculate_net_weight(){ /* Calculate Total Net Weight then further applied shipping rule to calculate shipping charges.*/ var me = this; @@ -2071,7 +2089,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); this.frm.doc.items.forEach(item => { - if (!item.quality_inspection) { + if (this.has_inspection_required(item)) { let dialog_items = dialog.fields_dict.items; dialog_items.df.data.push({ "docname": item.name, @@ -2095,6 +2113,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + has_inspection_required(item) { + if (this.frm.doc.doctype === "Stock Entry" && this.frm.doc.purpose == "Manufacture" ) { + if (item.is_finished_item && !item.quality_inspection) { + return true; + } + } else if (!item.quality_inspection) { + return true; + } + } + get_method_for_payment() { var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){ diff --git a/erpnext/public/js/customer_reviews.js b/erpnext/public/js/customer_reviews.js index e13ded6b489..26deb9a6dd0 100644 --- a/erpnext/public/js/customer_reviews.js +++ b/erpnext/public/js/customer_reviews.js @@ -13,27 +13,27 @@ $(() => { write_review() { //TODO: make dialog popup on stray page - $('.page_content').on('click', '.btn-write-review', (e) => { + $(".page_content").on("click", ".btn-write-review", (e) => { // Bind action on write a review button const $btn = $(e.currentTarget); let d = new frappe.ui.Dialog({ title: __("Write a Review"), fields: [ - {fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1}, - {fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1}, - {fieldtype: "Section Break"}, - {fieldname: "comment", fieldtype: "Small Text", label: "Your Review"} + { fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1 }, + { fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1 }, + { fieldtype: "Section Break" }, + { fieldname: "comment", fieldtype: "Small Text", label: "Your Review" }, ], - primary_action: function() { + primary_action: function () { let data = d.get_values(); frappe.call({ method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review", args: { - web_item: $btn.attr('data-web-item'), + web_item: $btn.attr("data-web-item"), title: data.title, rating: data.rating, - comment: data.comment + comment: data.comment, }, freeze: true, freeze_message: __("Submitting Review ..."), @@ -42,25 +42,25 @@ $(() => { frappe.msgprint({ message: __("Thank you for submitting your review"), title: __("Review Submitted"), - indicator: "green" + indicator: "green", }); d.hide(); location.reload(); } - } + }, }); }, - primary_action_label: __('Submit') + primary_action_label: __("Submit"), }); d.show(); }); } view_more() { - $('.page_content').on('click', '.btn-view-more', (e) => { + $(".page_content").on("click", ".btn-view-more", (e) => { // Bind action on view more button const $btn = $(e.currentTarget); - $btn.prop('disabled', true); + $btn.prop("disabled", true); this.start += this.page_length; let me = this; @@ -68,30 +68,28 @@ $(() => { frappe.call({ method: "erpnext.e_commerce.doctype.item_review.item_review.get_item_reviews", args: { - web_item: $btn.attr('data-web-item'), + web_item: $btn.attr("data-web-item"), start: me.start, - end: me.page_length + end: me.page_length, }, callback: (result) => { if (result.message) { let res = result.message; me.get_user_review_html(res.reviews); - $btn.prop('disabled', false); - if (res.total_reviews <= (me.start + me.page_length)) { + $btn.prop("disabled", false); + if (res.total_reviews <= me.start + me.page_length) { $btn.hide(); } - } - } + }, }); }); - } get_user_review_html(reviews) { let me = this; - let $content = $('.user-reviews'); + let $content = $(".user-reviews"); reviews.forEach((review) => { $content.append(` @@ -123,7 +121,7 @@ $(() => { get_review_stars(rating) { let stars = ``; for (let i = 1; i < 6; i++) { - let fill_class = i <= rating ? 'star-click' : ''; + let fill_class = i <= rating ? "star-click" : ""; stars += ` @@ -135,4 +133,4 @@ $(() => { } new CustomerReviews(); -}); \ No newline at end of file +}); diff --git a/erpnext/public/js/erpnext-web.bundle.js b/erpnext/public/js/erpnext-web.bundle.js index cbe899dc060..7c20d6eb8d6 100644 --- a/erpnext/public/js/erpnext-web.bundle.js +++ b/erpnext/public/js/erpnext-web.bundle.js @@ -5,4 +5,4 @@ import "./customer_reviews"; import "../../e_commerce/product_ui/list"; import "../../e_commerce/product_ui/views"; import "../../e_commerce/product_ui/grid"; -import "../../e_commerce/product_ui/search"; \ No newline at end of file +import "../../e_commerce/product_ui/search"; diff --git a/erpnext/public/js/event.js b/erpnext/public/js/event.js index 665c8fbe1f4..a6733915a5c 100644 --- a/erpnext/public/js/event.js +++ b/erpnext/public/js/event.js @@ -3,33 +3,53 @@ frappe.provide("frappe.desk"); frappe.ui.form.on("Event", { - refresh: function(frm) { - frm.set_query('reference_doctype', "event_participants", function() { + refresh: function (frm) { + frm.set_query("reference_doctype", "event_participants", function () { return { - "filters": { - "name": ["in", ["Contact", "Lead", "Customer", "Supplier", "Employee", "Sales Partner"]] - } + filters: { + name: ["in", ["Contact", "Lead", "Customer", "Supplier", "Employee", "Sales Partner"]], + }, }; }); - frm.add_custom_button(__('Add Leads'), function() { - new frappe.desk.eventParticipants(frm, "Lead"); - }, __("Add Participants")); + frm.add_custom_button( + __("Add Leads"), + function () { + new frappe.desk.eventParticipants(frm, "Lead"); + }, + __("Add Participants") + ); - frm.add_custom_button(__('Add Customers'), function() { - new frappe.desk.eventParticipants(frm, "Customer"); - }, __("Add Participants")); + frm.add_custom_button( + __("Add Customers"), + function () { + new frappe.desk.eventParticipants(frm, "Customer"); + }, + __("Add Participants") + ); - frm.add_custom_button(__('Add Suppliers'), function() { - new frappe.desk.eventParticipants(frm, "Supplier"); - }, __("Add Participants")); + frm.add_custom_button( + __("Add Suppliers"), + function () { + new frappe.desk.eventParticipants(frm, "Supplier"); + }, + __("Add Participants") + ); - frm.add_custom_button(__('Add Employees'), function() { - new frappe.desk.eventParticipants(frm, "Employee"); - }, __("Add Participants")); + frm.add_custom_button( + __("Add Employees"), + function () { + new frappe.desk.eventParticipants(frm, "Employee"); + }, + __("Add Participants") + ); - frm.add_custom_button(__('Add Sales Partners'), function() { - new frappe.desk.eventParticipants(frm, "Sales Partners"); - }, __("Add Participants")); - } + frm.add_custom_button( + __("Add Sales Partners"), + function () { + new frappe.desk.eventParticipants(frm, "Sales Partners"); + }, + __("Add Participants") + ); + }, }); diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 5e1974299ee..d489c91c305 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -1,9 +1,57 @@ frappe.provide("erpnext.financial_statements"); erpnext.financial_statements = { - "filters": get_filters(), - "formatter": function(value, row, column, data, default_formatter, filter) { - if (data && column.fieldname=="account") { + filters: get_filters(), + baseData: null, + formatter: function (value, row, column, data, default_formatter, filter) { + if ( + frappe.query_report.get_filter_value("selected_view") == "Growth" && + data && + column.colIndex >= 3 + ) { + //Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns. + const lastAnnualValue = row[column.colIndex - 1].content; + const currentAnnualvalue = data[column.fieldname]; + if (currentAnnualvalue == undefined) return "NA"; //making this not applicable for undefined/null values + let annualGrowth = 0; + if (lastAnnualValue == 0 && currentAnnualvalue > 0) { + //If the previous year value is 0 and the current value is greater than 0 + annualGrowth = 1; + } else if (lastAnnualValue > 0) { + annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue; + } + + const growthPercent = Math.round(annualGrowth * 10000) / 100; //calculating the rounded off percentage + + value = $(`${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}`); + if (growthPercent < 0) { + value = $(value).addClass("text-danger"); + } else { + value = $(value).addClass("text-success"); + } + value = $(value).wrap("

    ").parent().html(); + + return value; + } else if (frappe.query_report.get_filter_value("selected_view") == "Margin" && data) { + if (column.fieldname == "account" && data.account_name == __("Income")) { + //Taking the total income from each column (for all the financial years) as the base (100%) + this.baseData = row; + } + if (column.colIndex >= 2) { + //Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns. + const currentAnnualvalue = data[column.fieldname]; + const baseValue = this.baseData[column.colIndex].content; + if (currentAnnualvalue == undefined || baseValue <= 0) return "NA"; + const marginPercent = Math.round((currentAnnualvalue / baseValue) * 10000) / 100; + + value = $(`${marginPercent + "%"}`); + if (marginPercent < 0) value = $(value).addClass("text-danger"); + else value = $(value).addClass("text-success"); + value = $(value).wrap("

    ").parent().html(); + return value; + } + } + if (data && column.fieldname == "account") { value = data.account_name || value; if (filter && filter?.text && filter?.type == "contains") { @@ -34,16 +82,18 @@ erpnext.financial_statements = { return value; }, - "open_general_ledger": function(data) { + open_general_ledger: function (data) { if (!data.account) return; - let project = $.grep(frappe.query_report.filters, function(e){ return e.df.fieldname == 'project'; }); + let project = $.grep(frappe.query_report.filters, function (e) { + return e.df.fieldname == "project"; + }); frappe.route_options = { - "account": data.account, - "company": frappe.query_report.get_filter_value('company'), - "from_date": data.from_date || data.year_start_date, - "to_date": data.to_date || data.year_end_date, - "project": (project && project.length > 0) ? project[0].$input.val() : "" + account: data.account, + company: frappe.query_report.get_filter_value("company"), + from_date: data.from_date || data.year_start_date, + to_date: data.to_date || data.year_end_date, + project: project && project.length > 0 ? project[0].$input.val() : "", }; let report = "General Ledger"; @@ -56,152 +106,161 @@ erpnext.financial_statements = { frappe.set_route("query-report", report); }, - "tree": true, - "name_field": "account", - "parent_field": "parent_account", - "initial_depth": 3, - onload: function(report) { + tree: true, + name_field: "account", + parent_field: "parent_account", + initial_depth: 3, + onload: function (report) { // dropdown for links to other financial statements - erpnext.financial_statements.filters = get_filters() + erpnext.financial_statements.filters = get_filters(); let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); - frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); frappe.query_report.set_filter_value({ period_start_date: fy.year_start_date, - period_end_date: fy.year_end_date + period_end_date: fy.year_end_date, }); }); - const views_menu = report.page.add_custom_button_group(__('Financial Statements')); + const views_menu = report.page.add_custom_button_group(__("Financial Statements")); - report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() { + report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function () { var filters = report.get_values(); - frappe.set_route('query-report', 'Balance Sheet', {company: filters.company}); + frappe.set_route("query-report", "Balance Sheet", { company: filters.company }); }); - report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() { + report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function () { var filters = report.get_values(); - frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company}); + frappe.set_route("query-report", "Profit and Loss Statement", { company: filters.company }); }); - report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() { + report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function () { var filters = report.get_values(); - frappe.set_route('query-report', 'Cash Flow', {company: filters.company}); + frappe.set_route("query-report", "Cash Flow", { company: filters.company }); }); - } + }, }; function get_filters() { let filters = [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"finance_book", - "label": __("Finance Book"), - "fieldtype": "Link", - "options": "Finance Book" + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", }, { - "fieldname":"filter_based_on", - "label": __("Filter Based On"), - "fieldtype": "Select", - "options": ["Fiscal Year", "Date Range"], - "default": ["Fiscal Year"], - "reqd": 1, - on_change: function() { - let filter_based_on = frappe.query_report.get_filter_value('filter_based_on'); - frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range'); - frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range'); - frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year'); - frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year'); + fieldname: "filter_based_on", + label: __("Filter Based On"), + fieldtype: "Select", + options: ["Fiscal Year", "Date Range"], + default: ["Fiscal Year"], + reqd: 1, + on_change: function () { + let filter_based_on = frappe.query_report.get_filter_value("filter_based_on"); + frappe.query_report.toggle_filter_display( + "from_fiscal_year", + filter_based_on === "Date Range" + ); + frappe.query_report.toggle_filter_display("to_fiscal_year", filter_based_on === "Date Range"); + frappe.query_report.toggle_filter_display( + "period_start_date", + filter_based_on === "Fiscal Year" + ); + frappe.query_report.toggle_filter_display( + "period_end_date", + filter_based_on === "Fiscal Year" + ); frappe.query_report.refresh(); - } + }, }, { - "fieldname":"period_start_date", - "label": __("Start Date"), - "fieldtype": "Date", - "reqd": 1, - "depends_on": "eval:doc.filter_based_on == 'Date Range'" + fieldname: "period_start_date", + label: __("Start Date"), + fieldtype: "Date", + reqd: 1, + depends_on: "eval:doc.filter_based_on == 'Date Range'", }, { - "fieldname":"period_end_date", - "label": __("End Date"), - "fieldtype": "Date", - "reqd": 1, - "depends_on": "eval:doc.filter_based_on == 'Date Range'" + fieldname: "period_end_date", + label: __("End Date"), + fieldtype: "Date", + reqd: 1, + depends_on: "eval:doc.filter_based_on == 'Date Range'", }, { - "fieldname":"from_fiscal_year", - "label": __("Start Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1, - "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'" + fieldname: "from_fiscal_year", + label: __("Start Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + depends_on: "eval:doc.filter_based_on == 'Fiscal Year'", }, { - "fieldname":"to_fiscal_year", - "label": __("End Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1, - "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'" + fieldname: "to_fiscal_year", + label: __("End Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + depends_on: "eval:doc.filter_based_on == 'Fiscal Year'", }, { - "fieldname": "periodicity", - "label": __("Periodicity"), - "fieldtype": "Select", - "options": [ - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Half-Yearly", "label": __("Half-Yearly") }, - { "value": "Yearly", "label": __("Yearly") } + fieldname: "periodicity", + label: __("Periodicity"), + fieldtype: "Select", + options: [ + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, ], - "default": "Yearly", - "reqd": 1 + default: "Yearly", + reqd: 1, }, // Note: // If you are modifying this array such that the presentation_currency object // is no longer the last object, please make adjustments in cash_flow.js // accordingly. { - "fieldname": "presentation_currency", - "label": __("Currency"), - "fieldtype": "Select", - "options": erpnext.get_presentation_currency_list() + fieldname: "presentation_currency", + label: __("Currency"), + fieldtype: "Select", + options: erpnext.get_presentation_currency_list(), }, { - "fieldname": "cost_center", - "label": __("Cost Center"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Cost Center', txt, { - 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") + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + 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"), + }); + }, + }, + ]; return filters; } diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js index b643ccae947..b37bd11ead0 100644 --- a/erpnext/public/js/help_links.js +++ b/erpnext/public/js/help_links.js @@ -14,9 +14,7 @@ frappe.help.help_links["Form/Rename Tool"] = [ frappe.help.help_links["List/User"] = [ { label: "New User", - url: - docsUrl + - "user/manual/en/setting-up/users-and-permissions/adding-users", + url: docsUrl + "user/manual/en/setting-up/users-and-permissions/adding-users", }, { label: "Rename User", @@ -27,9 +25,7 @@ frappe.help.help_links["List/User"] = [ frappe.help.help_links["permission-manager"] = [ { label: "Role Permissions Manager", - url: - docsUrl + - "user/manual/en/setting-up/users-and-permissions/role-based-permissions", + url: docsUrl + "user/manual/en/setting-up/users-and-permissions/role-based-permissions", }, { label: "Managing Perm Level in Permissions Manager", @@ -37,14 +33,11 @@ frappe.help.help_links["permission-manager"] = [ }, { label: "User Permissions", - url: - docsUrl + - "user/manual/en/setting-up/users-and-permissions/user-permissions", + url: docsUrl + "user/manual/en/setting-up/users-and-permissions/user-permissions", }, { label: "Sharing", - url: - docsUrl + "user/manual/en/setting-up/users-and-permissions/sharing", + url: docsUrl + "user/manual/en/setting-up/users-and-permissions/sharing", }, { label: "Password", @@ -66,9 +59,7 @@ frappe.help.help_links["Form/Data Import"] = [ }, { label: "Overwriting Data from Data Import Tool", - url: - docsUrl + - "user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool", + url: docsUrl + "user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool", }, ]; @@ -79,18 +70,14 @@ frappe.help.help_links["List/Data Import"] = [ }, { label: "Overwriting Data from Data Import Tool", - url: - docsUrl + - "user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool", + url: docsUrl + "user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool", }, ]; frappe.help.help_links["module_setup"] = [ { label: "Role Permissions Manager", - url: - docsUrl + - "user/manual/en/setting-up/users-and-permissions/role-based-permissions", + url: docsUrl + "user/manual/en/setting-up/users-and-permissions/role-based-permissions", }, ]; @@ -101,9 +88,7 @@ frappe.help.help_links["Form/Naming Series"] = [ }, { label: "Setting the Current Value for Naming Series", - url: - docsUrl + - "user/manual/en/setting-up/articles/naming-series-current-value", + url: docsUrl + "user/manual/en/setting-up/articles/naming-series-current-value", }, ]; @@ -217,18 +202,14 @@ frappe.help.help_links["print-format-builder"] = [ frappe.help.help_links["Form/PayPal Settings"] = [ { label: "PayPal Settings", - url: - docsUrl + - "user/manual/en/erpnext_integration/paypal-integration", + url: docsUrl + "user/manual/en/erpnext_integration/paypal-integration", }, ]; frappe.help.help_links["Form/Razorpay Settings"] = [ { label: "Razorpay Settings", - url: - docsUrl + - "user/manual/en/erpnext_integration/razorpay-integration", + url: docsUrl + "user/manual/en/erpnext_integration/razorpay-integration", }, ]; @@ -242,17 +223,14 @@ frappe.help.help_links["Form/Dropbox Settings"] = [ frappe.help.help_links["Form/LDAP Settings"] = [ { label: "LDAP Settings", - url: - docsUrl + "user/manual/en/erpnext_integration/ldap-integration", + url: docsUrl + "user/manual/en/erpnext_integration/ldap-integration", }, ]; frappe.help.help_links["Form/Stripe Settings"] = [ { label: "Stripe Settings", - url: - docsUrl + - "user/manual/en/erpnext_integration/stripe-integration", + url: docsUrl + "user/manual/en/erpnext_integration/stripe-integration", }, ]; @@ -266,9 +244,7 @@ frappe.help.help_links["Form/Quotation"] = [ }, { label: "Sales Person", - url: - docsUrl + - "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions", + url: docsUrl + "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions", }, { label: "Applying Margin", @@ -340,9 +316,7 @@ frappe.help.help_links["Form/Sales Order"] = [ }, { label: "Sales Person", - url: - docsUrl + - "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions", + url: docsUrl + "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions", }, { label: "Close Sales Order", @@ -421,15 +395,11 @@ frappe.help.help_links["Form/Purchase Order"] = [ }, { label: "Item UoM", - url: - docsUrl + - "user/manual/en/buying/articles/purchasing-in-different-unit", + url: docsUrl + "user/manual/en/buying/articles/purchasing-in-different-unit", }, { label: "Supplier Item Code", - url: - docsUrl + - "user/manual/en/buying/articles/maintaining-suppliers-part-no-in-item", + url: docsUrl + "user/manual/en/buying/articles/maintaining-suppliers-part-no-in-item", }, { label: "Recurring Purchase Order", @@ -472,9 +442,7 @@ frappe.help.help_links["Form/SMS Settings"] = [ frappe.help.help_links["List/Stock Reconciliation"] = [ { label: "Stock Reconciliation", - url: - docsUrl + - "user/manual/en/stock/stock-reconciliation", + url: docsUrl + "user/manual/en/stock/stock-reconciliation", }, ]; @@ -496,9 +464,7 @@ frappe.help.help_links["List/Company"] = [ }, { label: "Delete All Related Transactions for a Company", - url: - docsUrl + - "user/manual/en/setting-up/articles/delete-a-company-and-all-related-transactions", + url: docsUrl + "user/manual/en/setting-up/articles/delete-a-company-and-all-related-transactions", }, ]; @@ -511,9 +477,7 @@ frappe.help.help_links["Tree/Account"] = [ }, { label: "Managing Tree Mastes", - url: - docsUrl + - "user/manual/en/setting-up/articles/managing-tree-structure-masters", + url: docsUrl + "user/manual/en/setting-up/articles/managing-tree-structure-masters", }, ]; @@ -648,8 +612,7 @@ frappe.help.help_links["List/Item"] = [ }, { label: "Barcode", - url: - docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", + url: docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", }, { label: "Item Wise Taxation", @@ -669,9 +632,7 @@ frappe.help.help_links["List/Item"] = [ }, { label: "Item Valuation", - url: - docsUrl + - "user/manual/en/stock/articles/item-valuation-fifo-and-moving-average", + url: docsUrl + "user/manual/en/stock/articles/item-valuation-fifo-and-moving-average", }, ]; @@ -683,8 +644,7 @@ frappe.help.help_links["Form/Item"] = [ }, { label: "Barcode", - url: - docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", + url: docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", }, { label: "Item Wise Taxation", @@ -704,9 +664,7 @@ frappe.help.help_links["Form/Item"] = [ }, { label: "Item Valuation", - url: - docsUrl + - "user/manual/en/stock/item/item-valuation-fifo-and-moving-average", + url: docsUrl + "user/manual/en/stock/item/item-valuation-fifo-and-moving-average", }, ]; @@ -717,8 +675,7 @@ frappe.help.help_links["List/Purchase Receipt"] = [ }, { label: "Barcode", - url: - docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", + url: docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", }, ]; @@ -729,8 +686,7 @@ frappe.help.help_links["List/Delivery Note"] = [ }, { label: "Barcode", - url: - docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", + url: docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", }, { label: "Sales Return", @@ -749,8 +705,7 @@ frappe.help.help_links["Form/Delivery Note"] = [ }, { label: "Barcode", - url: - docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", + url: docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", }, ]; @@ -772,9 +727,7 @@ frappe.help.help_links["List/Material Request"] = [ }, { label: "Auto-creation of Material Request", - url: - docsUrl + - "user/manual/en/stock/articles/auto-creation-of-material-request", + url: docsUrl + "user/manual/en/stock/articles/auto-creation-of-material-request", }, ]; @@ -785,9 +738,7 @@ frappe.help.help_links["Form/Material Request"] = [ }, { label: "Auto-creation of Material Request", - url: - docsUrl + - "user/manual/en/stock/articles/auto-creation-of-material-request", + url: docsUrl + "user/manual/en/stock/articles/auto-creation-of-material-request", }, ]; @@ -827,13 +778,9 @@ frappe.help.help_links["Form/Serial No"] = [ { label: "Serial No", url: docsUrl + "user/manual/en/stock/serial-no" }, ]; -frappe.help.help_links["List/Batch"] = [ - { label: "Batch", url: docsUrl + "user/manual/en/stock/batch" }, -]; +frappe.help.help_links["List/Batch"] = [{ label: "Batch", url: docsUrl + "user/manual/en/stock/batch" }]; -frappe.help.help_links["Form/Batch"] = [ - { label: "Batch", url: docsUrl + "user/manual/en/stock/batch" }, -]; +frappe.help.help_links["Form/Batch"] = [{ label: "Batch", url: docsUrl + "user/manual/en/stock/batch" }]; frappe.help.help_links["Form/Packing Slip"] = [ { @@ -873,8 +820,7 @@ frappe.help.help_links["Form/Item Attribute"] = [ frappe.help.help_links["Form/UOM"] = [ { label: "Fractions in UOM", - url: - docsUrl + "user/manual/en/stock/articles/managing-fractions-in-uom", + url: docsUrl + "user/manual/en/stock/articles/managing-fractions-in-uom", }, ]; @@ -887,21 +833,15 @@ frappe.help.help_links["Form/Stock Reconciliation"] = [ //CRM -frappe.help.help_links["Form/Lead"] = [ - { label: "Lead", url: docsUrl + "user/manual/en/CRM/lead" }, -]; +frappe.help.help_links["Form/Lead"] = [{ label: "Lead", url: docsUrl + "user/manual/en/CRM/lead" }]; frappe.help.help_links["Form/Opportunity"] = [ { label: "Opportunity", url: docsUrl + "user/manual/en/CRM/opportunity" }, ]; -frappe.help.help_links["Form/Address"] = [ - { label: "Address", url: docsUrl + "user/manual/en/CRM/address" }, -]; +frappe.help.help_links["Form/Address"] = [{ label: "Address", url: docsUrl + "user/manual/en/CRM/address" }]; -frappe.help.help_links["Form/Contact"] = [ - { label: "Contact", url: docsUrl + "user/manual/en/CRM/contact" }, -]; +frappe.help.help_links["Form/Contact"] = [{ label: "Contact", url: docsUrl + "user/manual/en/CRM/contact" }]; frappe.help.help_links["Form/Newsletter"] = [ { label: "Newsletter", url: docsUrl + "user/manual/en/CRM/newsletter" }, @@ -921,15 +861,11 @@ frappe.help.help_links["Tree/Sales Person"] = [ frappe.help.help_links["Form/Sales Person"] = [ { label: "Sales Person Target", - url: - docsUrl + - "user/manual/en/selling/sales-person-target-allocation", + url: docsUrl + "user/manual/en/selling/sales-person-target-allocation", }, { label: "Sales Person in Transactions", - url: - docsUrl + - "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions", + url: docsUrl + "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions", }, ]; @@ -942,9 +878,7 @@ frappe.help.help_links["Form/BOM"] = [ }, { label: "Nested BOM Structure", - url: - docsUrl + - "user/manual/en/manufacturing/articles/managing-multi-level-bom", + url: docsUrl + "user/manual/en/manufacturing/articles/managing-multi-level-bom", }, ]; diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index a585aa614fb..013ace62d74 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -1,4 +1,4 @@ -import html2canvas from 'html2canvas'; +import html2canvas from "html2canvas"; erpnext.HierarchyChart = class { /* Options: - doctype @@ -13,7 +13,7 @@ erpnext.HierarchyChart = class { this.doctype = doctype; this.setup_page_style(); - this.page.main.addClass('frappe-card'); + this.page.main.addClass("frappe-card"); this.nodes = {}; this.setup_node_class(); @@ -21,10 +21,10 @@ erpnext.HierarchyChart = class { setup_page_style() { this.page.main.css({ - 'min-height': '300px', - 'max-height': '600px', - 'overflow': 'auto', - 'position': 'relative' + "min-height": "300px", + "max-height": "600px", + overflow: "auto", + position: "relative", }); } @@ -32,7 +32,15 @@ erpnext.HierarchyChart = class { let me = this; this.Node = class { constructor({ - id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line + id, + parent, + parent_id, + image, + name, + title, + expandable, + connections, + is_root, // eslint-disable-line }) { // to setup values passed via constructor $.extend(this, arguments[0]); @@ -52,14 +60,14 @@ erpnext.HierarchyChart = class { } make_node_element(node) { - let node_card = frappe.render_template('node_card', { + let node_card = frappe.render_template("node_card", { id: node.id, name: node.name, title: node.title, image: node.image, parent: node.parent_id, connections: node.connections, - is_mobile: false + is_mobile: false, }); node.parent.append(node_card); @@ -68,20 +76,20 @@ erpnext.HierarchyChart = class { show() { this.setup_actions(); - if ($(`[data-fieldname="company"]`).length) return; + if (this.page.main.find('[data-fieldname="company"]').length) return; let me = this; let company = this.page.add_field({ - fieldtype: 'Link', - options: 'Company', - fieldname: 'company', - placeholder: __('Select Company'), - default: frappe.defaults.get_default('company'), + fieldtype: "Link", + options: "Company", + fieldname: "company", + placeholder: __("Select Company"), + default: frappe.defaults.get_default("company"), only_select: true, reqd: 1, change: () => { - me.company = undefined; - $('#hierarchy-chart-wrapper').remove(); + me.company = ""; + $("#hierarchy-chart-wrapper").remove(); if (company.get_value()) { me.company = company.get_value(); @@ -92,75 +100,76 @@ erpnext.HierarchyChart = class { me.render_root_nodes(); me.all_nodes_expanded = false; } else { - frappe.throw(__('Please select a company first.')); + frappe.throw(__("Please select a company first.")); } - } + }, }); company.refresh(); - $(`[data-fieldname="company"]`).trigger('change'); - $(`[data-fieldname="company"] .link-field`).css('z-index', 2); + $(`[data-fieldname="company"]`).trigger("change"); + $(`[data-fieldname="company"] .link-field`).css("z-index", 2); } setup_actions() { let me = this; this.page.clear_inner_toolbar(); - this.page.add_inner_button(__('Export'), function() { + this.page.add_inner_button(__("Export"), function () { me.export_chart(); }); - this.page.add_inner_button(__('Expand All'), function() { + this.page.add_inner_button(__("Expand All"), function () { me.load_children(me.root_node, true); me.all_nodes_expanded = true; - me.page.remove_inner_button(__('Expand All')); - me.page.add_inner_button(__('Collapse All'), function() { + me.page.remove_inner_button(__("Expand All")); + me.page.add_inner_button(__("Collapse All"), function () { me.setup_hierarchy(); me.render_root_nodes(); me.all_nodes_expanded = false; - me.page.remove_inner_button(__('Collapse All')); + me.page.remove_inner_button(__("Collapse All")); me.setup_actions(); }); }); } export_chart() { - frappe.dom.freeze(__('Exporting...')); + frappe.dom.freeze(__("Exporting...")); this.page.main.css({ - 'min-height': '', - 'max-height': '', - 'overflow': 'visible', - 'position': 'fixed', - 'left': '0', - 'top': '0' + "min-height": "", + "max-height": "", + overflow: "visible", + position: "fixed", + left: "0", + top: "0", }); - $('.node-card').addClass('exported'); + $(".node-card").addClass("exported"); - html2canvas(document.querySelector('#hierarchy-chart-wrapper'), { + html2canvas(document.querySelector("#hierarchy-chart-wrapper"), { scrollY: -window.scrollY, - scrollX: 0 - }).then(function(canvas) { - // Export the canvas to its data URI representation - let dataURL = canvas.toDataURL('image/png'); + scrollX: 0, + }) + .then(function (canvas) { + // Export the canvas to its data URI representation + let dataURL = canvas.toDataURL("image/png"); - // download the image - let a = document.createElement('a'); - a.href = dataURL; - a.download = 'hierarchy_chart'; - a.click(); - }).finally(() => { - frappe.dom.unfreeze(); - }); + // download the image + let a = document.createElement("a"); + a.href = dataURL; + a.download = "hierarchy_chart"; + a.click(); + }) + .finally(() => { + frappe.dom.unfreeze(); + }); this.setup_page_style(); - $('.node-card').removeClass('exported'); + $(".node-card").removeClass("exported"); } setup_hierarchy() { - if (this.$hierarchy) - this.$hierarchy.remove(); + if (this.$hierarchy) this.$hierarchy.remove(); $(`#connectors`).empty(); @@ -170,18 +179,16 @@ erpnext.HierarchyChart = class {
    • - `); + ` + ); - this.page.main - .find('#hierarchy-chart') - .empty() - .append(this.$hierarchy); + this.page.main.find("#hierarchy-chart").empty().append(this.$hierarchy); this.nodes = {}; } make_svg_markers() { - $('#hierarchy-chart-wrapper').remove(); + $("#hierarchy-chart-wrapper").remove(); this.page.main.append(`
      @@ -209,45 +216,47 @@ erpnext.HierarchyChart = class {
      `); } - render_root_nodes(expanded_view=false) { + render_root_nodes(expanded_view = false) { let me = this; - return frappe.call({ - method: me.method, - args: { - company: me.company - } - }).then(r => { - if (r.message.length) { - let expand_node = undefined; - let node = undefined; + return frappe + .call({ + method: me.method, + args: { + company: me.company, + }, + }) + .then((r) => { + if (r.message.length) { + let expand_node; + let node; - $.each(r.message, (_i, data) => { - if ($(`[id="${data.id}"]`).length) - return; + $.each(r.message, (_i, data) => { + if ($(`[id="${data.id}"]`).length) return; - node = new me.Node({ - id: data.id, - parent: $('
    • ').appendTo(me.$hierarchy.find('.node-children')), - parent_id: undefined, - image: data.image, - name: data.name, - title: data.title, - expandable: true, - connections: data.connections, - is_root: true + node = new me.Node({ + id: data.id, + parent: $('
    • ').appendTo( + me.$hierarchy.find(".node-children") + ), + parent_id: "", + image: data.image, + name: data.name, + title: data.title, + expandable: true, + connections: data.connections, + is_root: true, + }); + + if (!expand_node && data.connections) expand_node = node; }); - if (!expand_node && data.connections) - expand_node = node; - }); - - me.root_node = expand_node; - if (!expanded_view) { - me.expand_node(expand_node); + me.root_node = expand_node; + if (!expanded_view) { + me.expand_node(expand_node); + } } - } - }); + }); } expand_node(node) { @@ -263,7 +272,7 @@ erpnext.HierarchyChart = class { this.refresh_connectors(node.parent_id); // rebuild incoming connections - let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent'); + let grandparent = $(`[id="${node.parent_id}"]`).attr("data-parent"); this.refresh_connectors(grandparent); } @@ -282,14 +291,18 @@ erpnext.HierarchyChart = class { show_active_path(node) { // mark node parent on active path - $(`[id="${node.parent_id}"]`).addClass('active-path'); + $(`[id="${node.parent_id}"]`).addClass("active-path"); } - load_children(node, deep=false) { + load_children(node, deep = false) { + if (!this.company) { + frappe.throw(__("Please select a company first.")); + } + if (!deep) { frappe.run_serially([ () => this.get_child_nodes(node.id), - (child_nodes) => this.render_child_nodes(node, child_nodes) + (child_nodes) => this.render_child_nodes(node, child_nodes), ]); } else { frappe.run_serially([ @@ -298,26 +311,28 @@ erpnext.HierarchyChart = class { () => this.render_root_nodes(true), () => this.get_all_nodes(), (data_list) => this.render_children_of_all_nodes(data_list), - () => frappe.dom.unfreeze() + () => frappe.dom.unfreeze(), ]); } } get_child_nodes(node_id) { let me = this; - return new Promise(resolve => { - frappe.call({ - method: me.method, - args: { - parent: node_id, - company: me.company - } - }).then(r => resolve(r.message)); + return new Promise((resolve) => { + frappe + .call({ + method: me.method, + args: { + parent: node_id, + company: me.company, + }, + }) + .then((r) => resolve(r.message)); }); } render_child_nodes(node, child_nodes) { - const last_level = this.$hierarchy.find('.level:last').index(); + const last_level = this.$hierarchy.find(".level:last").index(); const current_level = $(`[id="${node.id}"]`).parent().parent().parent().index(); if (last_level === current_level) { @@ -329,7 +344,7 @@ erpnext.HierarchyChart = class { if (!node.$children) { node.$children = $('
        ') .hide() - .appendTo(this.$hierarchy.find('.level:last')); + .appendTo(this.$hierarchy.find(".level:last")); node.$children.empty(); @@ -352,23 +367,23 @@ erpnext.HierarchyChart = class { get_all_nodes() { let me = this; - return new Promise(resolve => { + return new Promise((resolve) => { frappe.call({ - method: 'erpnext.utilities.hierarchy_chart.get_all_nodes', + method: "erpnext.utilities.hierarchy_chart.get_all_nodes", args: { method: me.method, - company: me.company + company: me.company, }, callback: (r) => { resolve(r.message); - } + }, }); }); } render_children_of_all_nodes(data_list) { - let entry = undefined; - let node = undefined; + let entry; + let node; while (data_list.length) { // to avoid overlapping connectors @@ -385,16 +400,16 @@ erpnext.HierarchyChart = class { render_child_nodes_for_expanded_view(node, child_nodes) { node.$children = $('
          '); - const last_level = this.$hierarchy.find('.level:last').index(); + const last_level = this.$hierarchy.find(".level:last").index(); const node_level = $(`[id="${node.id}"]`).parent().parent().parent().index(); if (last_level === node_level) { this.$hierarchy.append(`
        • `); - node.$children.appendTo(this.$hierarchy.find('.level:last')); + node.$children.appendTo(this.$hierarchy.find(".level:last")); } else { - node.$children.appendTo(this.$hierarchy.find('.level:eq(' + (node_level + 1) + ')')); + node.$children.appendTo(this.$hierarchy.find(".level:eq(" + (node_level + 1) + ")")); } node.$children.hide().empty(); @@ -423,7 +438,7 @@ erpnext.HierarchyChart = class { title: data.title, expandable: data.expandable, connections: data.connections, - children: undefined + children: null, }); } @@ -432,33 +447,40 @@ erpnext.HierarchyChart = class { const parent_node = document.getElementById(`${parent_id}`); const child_node = document.getElementById(`${child_id}`); - let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + let path = document.createElementNS("http://www.w3.org/2000/svg", "path"); // we need to connect right side of the parent to the left side of the child node const pos_parent_right = { x: parent_node.offsetLeft + parent_node.offsetWidth, - y: parent_node.offsetTop + parent_node.offsetHeight / 2 + y: parent_node.offsetTop + parent_node.offsetHeight / 2, }; const pos_child_left = { x: child_node.offsetLeft - 5, - y: child_node.offsetTop + child_node.offsetHeight / 2 + y: child_node.offsetTop + child_node.offsetHeight / 2, }; const connector = this.get_connector(pos_parent_right, pos_child_left); - path.setAttribute('d', connector); + path.setAttribute("d", connector); this.set_path_attributes(path, parent_id, child_id); - document.getElementById('connectors').appendChild(path); + document.getElementById("connectors").appendChild(path); } get_connector(pos_parent_right, pos_child_left) { if (pos_parent_right.y === pos_child_left.y) { // don't add arcs if it's a straight line - return "M" + - (pos_parent_right.x) + "," + (pos_parent_right.y) + " " + - "L"+ - (pos_child_left.x) + "," + (pos_child_left.y); + return ( + "M" + + pos_parent_right.x + + "," + + pos_parent_right.y + + " " + + "L" + + pos_child_left.x + + "," + + pos_child_left.y + ); } else { let arc_1 = ""; let arc_2 = ""; @@ -478,15 +500,29 @@ erpnext.HierarchyChart = class { offset = -10; } - return "M" + (pos_parent_right.x) + "," + (pos_parent_right.y) + " " + + return ( + "M" + + pos_parent_right.x + + "," + + pos_parent_right.y + + " " + "L" + - (pos_parent_right.x + 40) + "," + (pos_parent_right.y) + " " + + (pos_parent_right.x + 40) + + "," + + pos_parent_right.y + + " " + arc_1 + "L" + - (pos_parent_right.x + 50) + "," + (pos_child_left.y + offset) + " " + + (pos_parent_right.x + 50) + + "," + + (pos_child_left.y + offset) + + " " + arc_2 + - "L"+ - (pos_child_left.x) + "," + (pos_child_left.y); + "L" + + pos_child_left.x + + "," + + pos_child_left.y + ); } } @@ -495,7 +531,7 @@ erpnext.HierarchyChart = class { path.setAttribute("data-child", child_id); const parent = $(`[id="${parent_id}"]`); - if (parent.hasClass('active')) { + if (parent.hasClass("active")) { path.setAttribute("class", "active-connector"); path.setAttribute("marker-start", "url(#arrowstart-active)"); path.setAttribute("marker-end", "url(#arrowhead-active)"); @@ -508,24 +544,23 @@ erpnext.HierarchyChart = class { set_selected_node(node) { // remove active class from the current node - if (this.selected_node) - this.selected_node.$link.removeClass('active'); + if (this.selected_node) this.selected_node.$link.removeClass("active"); // add active class to the newly selected node this.selected_node = node; - node.$link.addClass('active'); + node.$link.addClass("active"); } collapse_previous_level_nodes(node) { let node_parent = $(`[id="${node.parent_id}"]`); - let previous_level_nodes = node_parent.parent().parent().children('li'); - let node_card = undefined; + let previous_level_nodes = node_parent.parent().parent().children("li"); + let node_card; - previous_level_nodes.each(function() { - node_card = $(this).find('.node-card'); + previous_level_nodes.each(function () { + node_card = $(this).find(".node-card"); - if (!node_card.hasClass('active-path')) { - node_card.addClass('collapsed'); + if (!node_card.hasClass("active-path")) { + node_card.addClass("collapsed"); } }); } @@ -543,7 +578,7 @@ erpnext.HierarchyChart = class { this.add_connector(node_parent, data.id); }); } - } + }, ]); } @@ -551,13 +586,15 @@ erpnext.HierarchyChart = class { let me = this; let node_element = $(`[id="${node.id}"]`); - node_element.click(function() { + node_element.click(function () { const is_sibling = me.selected_node.parent_id === node.parent_id; if (is_sibling) { me.collapse_node(); - } else if (node_element.is(':visible') - && (node_element.hasClass('collapsed') || node_element.hasClass('active-path'))) { + } else if ( + node_element.is(":visible") && + (node_element.hasClass("collapsed") || node_element.hasClass("active-path")) + ) { me.remove_levels_after_node(node); me.remove_orphaned_connectors(); } @@ -570,37 +607,36 @@ erpnext.HierarchyChart = class { let node_element = $(`[id="${node.id}"]`); let me = this; - node_element.find('.btn-edit-node').click(function() { - frappe.set_route('Form', me.doctype, node.id); + node_element.find(".btn-edit-node").click(function () { + frappe.set_route("Form", me.doctype, node.id); }); } remove_levels_after_node(node) { let level = $(`[id="${node.id}"]`).parent().parent().parent().index(); - level = $('.hierarchy > li:eq('+ level + ')'); - level.nextAll('li').remove(); + level = $(".hierarchy > li:eq(" + level + ")"); + level.nextAll("li").remove(); - let nodes = level.find('.node-card'); - let node_object = undefined; + let nodes = level.find(".node-card"); + let node_object; $.each(nodes, (_i, element) => { node_object = this.nodes[element.id]; node_object.expanded = 0; - node_object.$children = undefined; + node_object.$children = null; }); - nodes.removeClass('collapsed active-path'); + nodes.removeClass("collapsed active-path"); } remove_orphaned_connectors() { - let paths = $('#connectors > path'); + let paths = $("#connectors > path"); $.each(paths, (_i, path) => { - const parent = $(path).data('parent'); - const child = $(path).data('child'); + const parent = $(path).data("parent"); + const child = $(path).data("child"); - if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length) - return; + if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length) return; $(path).remove(); }); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 52236e7df96..e8fa078b6fe 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -12,12 +12,12 @@ erpnext.HierarchyChartMobile = class { this.doctype = doctype; this.page.main.css({ - 'min-height': '300px', - 'max-height': '600px', - 'overflow': 'auto', - 'position': 'relative' + "min-height": "300px", + "max-height": "600px", + overflow: "auto", + position: "relative", }); - this.page.main.addClass('frappe-card'); + this.page.main.addClass("frappe-card"); this.nodes = {}; this.setup_node_class(); @@ -27,7 +27,15 @@ erpnext.HierarchyChartMobile = class { let me = this; this.Node = class { constructor({ - id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line + id, + parent, + parent_id, + image, + name, + title, + expandable, + connections, + is_root, // eslint-disable-line }) { // to setup values passed via constructor $.extend(this, arguments[0]); @@ -43,35 +51,35 @@ erpnext.HierarchyChartMobile = class { } make_node_element(node) { - let node_card = frappe.render_template('node_card', { + let node_card = frappe.render_template("node_card", { id: node.id, name: node.name, title: node.title, image: node.image, parent: node.parent_id, connections: node.connections, - is_mobile: true + is_mobile: true, }); node.parent.append(node_card); node.$link = $(`[id="${node.id}"]`); - node.$link.addClass('mobile-node'); + node.$link.addClass("mobile-node"); } show() { + if (this.page.main.find('[data-fieldname="company"]').length) return; let me = this; - if ($(`[data-fieldname="company"]`).length) return; let company = this.page.add_field({ - fieldtype: 'Link', - options: 'Company', - fieldname: 'company', - placeholder: __('Select Company'), - default: frappe.defaults.get_default('company'), + fieldtype: "Link", + options: "Company", + fieldname: "company", + placeholder: __("Select Company"), + default: frappe.defaults.get_default("company"), only_select: true, reqd: 1, change: () => { - me.company = undefined; + me.company = ""; if (company.get_value() && me.company != company.get_value()) { me.company = company.get_value(); @@ -79,8 +87,7 @@ erpnext.HierarchyChartMobile = class { // svg for connectors me.make_svg_markers(); - if (me.$sibling_group) - me.$sibling_group.remove(); + if (me.$sibling_group) me.$sibling_group.remove(); // setup sibling group wrapper me.$sibling_group = $(`
          `); @@ -89,15 +96,15 @@ erpnext.HierarchyChartMobile = class { me.setup_hierarchy(); me.render_root_nodes(); } - } + }, }); company.refresh(); - $(`[data-fieldname="company"]`).trigger('change'); + $(`[data-fieldname="company"]`).trigger("change"); } make_svg_markers() { - $('#arrows').remove(); + $("#arrows").remove(); this.page.main.prepend(` @@ -123,16 +130,15 @@ erpnext.HierarchyChartMobile = class { setup_hierarchy() { $(`#connectors`).empty(); - if (this.$hierarchy) - this.$hierarchy.remove(); + if (this.$hierarchy) this.$hierarchy.remove(); - if (this.$sibling_group) - this.$sibling_group.empty(); + if (this.$sibling_group) this.$sibling_group.empty(); this.$hierarchy = $( `
          • -
          `); + ` + ); this.page.main.append(this.$hierarchy); } @@ -140,42 +146,43 @@ erpnext.HierarchyChartMobile = class { render_root_nodes() { let me = this; - frappe.call({ - method: me.method, - args: { - company: me.company - }, - }).then(r => { - if (r.message.length) { - let root_level = me.$hierarchy.find('.root-level'); - root_level.empty(); + frappe + .call({ + method: me.method, + args: { + company: me.company, + }, + }) + .then((r) => { + if (r.message.length) { + let root_level = me.$hierarchy.find(".root-level"); + root_level.empty(); - $.each(r.message, (_i, data) => { - return new me.Node({ - id: data.id, - parent: root_level, - parent_id: undefined, - image: data.image, - name: data.name, - title: data.title, - expandable: true, - connections: data.connections, - is_root: true + $.each(r.message, (_i, data) => { + return new me.Node({ + id: data.id, + parent: root_level, + parent_id: "", + image: data.image, + name: data.name, + title: data.title, + expandable: true, + connections: data.connections, + is_root: true, + }); }); - }); - } - }); + } + }); } expand_node(node) { - const is_same_node = (this.selected_node && this.selected_node.id === node.id); + const is_same_node = this.selected_node && this.selected_node.id === node.id; this.set_selected_node(node); this.show_active_path(node); if (this.$sibling_group) { - const sibling_parent = this.$sibling_group.find('.node-group').attr('data-parent'); - if (node.parent_id !== undefined && node.parent_id != sibling_parent) - this.$sibling_group.empty(); + const sibling_parent = this.$sibling_group.find(".node-group").attr("data-parent"); + if (node.parent_id != "" && node.parent_id != sibling_parent) this.$sibling_group.empty(); } if (!is_same_node) { @@ -184,7 +191,7 @@ erpnext.HierarchyChartMobile = class { this.refresh_connectors(node.parent_id, node.id); // rebuild incoming connections of parent - let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent'); + let grandparent = $(`[id="${node.parent_id}"]`).attr("data-parent"); this.refresh_connectors(grandparent, node.parent_id); } @@ -202,61 +209,61 @@ erpnext.HierarchyChartMobile = class { // add a collapsed level to show the collapsed parent // and a button beside it to move to that level let node_parent = node.$link.parent(); - node_parent.prepend( - `
          ` - ); + node_parent.prepend(`
          `); - node_parent - .find('.collapsed-level') - .append(node.$link); + node_parent.find(".collapsed-level").append(node.$link); frappe.run_serially([ () => this.get_child_nodes(node.parent_id, node.id), (child_nodes) => this.get_node_group(child_nodes, node.parent_id), - (node_group) => node_parent.find('.collapsed-level').append(node_group), - () => this.setup_node_group_action() + (node_group) => node_parent.find(".collapsed-level").append(node_group), + () => this.setup_node_group_action(), ]); } } show_active_path(node) { // mark node parent on active path - $(`[id="${node.parent_id}"]`).addClass('active-path'); + $(`[id="${node.parent_id}"]`).addClass("active-path"); } load_children(node) { + if (!this.company) { + frappe.throw(__("Please select a company first")); + } + frappe.run_serially([ () => this.get_child_nodes(node.id), - (child_nodes) => this.render_child_nodes(node, child_nodes) + (child_nodes) => this.render_child_nodes(node, child_nodes), ]); } - get_child_nodes(node_id, exclude_node=null) { + get_child_nodes(node_id, exclude_node = null) { let me = this; - return new Promise(resolve => { - frappe.call({ - method: me.method, - args: { - parent: node_id, - company: me.company, - exclude_node: exclude_node - } - }).then(r => resolve(r.message)); + return new Promise((resolve) => { + frappe + .call({ + method: me.method, + args: { + parent: node_id, + company: me.company, + exclude_node: exclude_node, + }, + }) + .then((r) => resolve(r.message)); }); } render_child_nodes(node, child_nodes) { if (!node.$children) { - node.$children = $('
            ') - .hide() - .appendTo(node.$link.parent()); + node.$children = $('
              ').hide().appendTo(node.$link.parent()); node.$children.empty(); if (child_nodes) { $.each(child_nodes, (_i, data) => { this.add_node(node, data); - $(`[id="${data.id}"]`).addClass('active-child'); + $(`[id="${data.id}"]`).addClass("active-child"); setTimeout(() => { this.add_connector(node.id, data.id); @@ -281,7 +288,7 @@ erpnext.HierarchyChartMobile = class { title: data.title, expandable: data.expandable, connections: data.connections, - children: undefined + children: null, }); } @@ -289,41 +296,49 @@ erpnext.HierarchyChartMobile = class { const parent_node = document.getElementById(`${parent_id}`); const child_node = document.getElementById(`${child_id}`); - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); - let connector = undefined; + let connector = null; - if ($(`[id="${parent_id}"]`).hasClass('active')) { + if ($(`[id="${parent_id}"]`).hasClass("active")) { connector = this.get_connector_for_active_node(parent_node, child_node); - } else if ($(`[id="${parent_id}"]`).hasClass('active-path')) { + } else if ($(`[id="${parent_id}"]`).hasClass("active-path")) { connector = this.get_connector_for_collapsed_node(parent_node, child_node); } - path.setAttribute('d', connector); + path.setAttribute("d", connector); this.set_path_attributes(path, parent_id, child_id); - document.getElementById('connectors').appendChild(path); + document.getElementById("connectors").appendChild(path); } get_connector_for_active_node(parent_node, child_node) { // we need to connect the bottom left of the parent to the left side of the child node let pos_parent_bottom = { x: parent_node.offsetLeft + 20, - y: parent_node.offsetTop + parent_node.offsetHeight + y: parent_node.offsetTop + parent_node.offsetHeight, }; let pos_child_left = { x: child_node.offsetLeft - 5, - y: child_node.offsetTop + child_node.offsetHeight / 2 + y: child_node.offsetTop + child_node.offsetHeight / 2, }; let connector = "M" + - (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + + pos_parent_bottom.x + + "," + + pos_parent_bottom.y + + " " + "L" + - (pos_parent_bottom.x) + "," + (pos_child_left.y - 10) + " " + + pos_parent_bottom.x + + "," + + (pos_child_left.y - 10) + + " " + "a10,10 1 0 0 10,10 " + "L" + - (pos_child_left.x) + "," + (pos_child_left.y); + pos_child_left.x + + "," + + pos_child_left.y; return connector; } @@ -332,18 +347,23 @@ erpnext.HierarchyChartMobile = class { // we need to connect the bottom left of the parent to the top left of the child node let pos_parent_bottom = { x: parent_node.offsetLeft + 20, - y: parent_node.offsetTop + parent_node.offsetHeight + y: parent_node.offsetTop + parent_node.offsetHeight, }; let pos_child_top = { x: child_node.offsetLeft + 20, - y: child_node.offsetTop + y: child_node.offsetTop, }; let connector = "M" + - (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + + pos_parent_bottom.x + + "," + + pos_parent_bottom.y + + " " + "L" + - (pos_child_top.x) + "," + (pos_child_top.y); + pos_child_top.x + + "," + + pos_child_top.y; return connector; } @@ -353,38 +373,37 @@ erpnext.HierarchyChartMobile = class { path.setAttribute("data-child", child_id); const parent = $(`[id="${parent_id}"]`); - if (parent.hasClass('active')) { + if (parent.hasClass("active")) { path.setAttribute("class", "active-connector"); path.setAttribute("marker-start", "url(#arrowstart-active)"); path.setAttribute("marker-end", "url(#arrowhead-active)"); - } else if (parent.hasClass('active-path')) { + } else if (parent.hasClass("active-path")) { path.setAttribute("class", "collapsed-connector"); } } set_selected_node(node) { // remove .active class from the current node - if (this.selected_node) - this.selected_node.$link.removeClass('active'); + if (this.selected_node) this.selected_node.$link.removeClass("active"); // add active class to the newly selected node this.selected_node = node; - node.$link.addClass('active'); + node.$link.addClass("active"); } setup_node_click_action(node) { let me = this; let node_element = $(`[id="${node.id}"]`); - node_element.click(function() { - let el = undefined; + node_element.click(function () { + let el = null; if (node.is_root) { el = $(this).detach(); me.$hierarchy.empty(); $(`#connectors`).empty(); me.add_node_to_hierarchy(el, node); - } else if (node_element.is(':visible') && node_element.hasClass('active-path')) { + } else if (node_element.is(":visible") && node_element.hasClass("active-path")) { me.remove_levels_after_node(node); me.remove_orphaned_connectors(); } else { @@ -401,17 +420,17 @@ erpnext.HierarchyChartMobile = class { let node_element = $(`[id="${node.id}"]`); let me = this; - node_element.find('.btn-edit-node').click(function() { - frappe.set_route('Form', me.doctype, node.id); + node_element.find(".btn-edit-node").click(function () { + frappe.set_route("Form", me.doctype, node.id); }); } setup_node_group_action() { let me = this; - $('.node-group').on('click', function() { - let parent = $(this).attr('data-parent'); - if (parent === 'undefined') { + $(".node-group").on("click", function () { + let parent = $(this).attr("data-parent"); + if (parent == "") { me.setup_hierarchy(); me.render_root_nodes(); } else { @@ -422,23 +441,21 @@ erpnext.HierarchyChartMobile = class { add_node_to_hierarchy(node_element, node) { this.$hierarchy.append(`
            • `); - node_element.removeClass('active-child active-path'); - this.$hierarchy.find('.level:last').append(node_element); + node_element.removeClass("active-child active-path"); + this.$hierarchy.find(".level:last").append(node_element); let node_object = this.nodes[node.id]; node_object.expanded = 0; - node_object.$children = undefined; + node_object.$children = null; this.nodes[node.id] = node_object; } - get_node_group(nodes, parent, collapsed=true) { + get_node_group(nodes, parent, collapsed = true) { let limit = 2; const display_nodes = nodes.slice(0, limit); const extra_nodes = nodes.slice(limit); - let html = display_nodes.map(node => - this.get_avatar(node) - ).join(''); + let html = display_nodes.map((node) => this.get_avatar(node)).join(""); if (extra_nodes.length === 1) { let node = extra_nodes[0]; @@ -448,7 +465,7 @@ erpnext.HierarchyChartMobile = class { ${html}
              + title="${extra_nodes.map((node) => node.name).join(", ")}"> +${extra_nodes.length}
              @@ -456,15 +473,13 @@ erpnext.HierarchyChartMobile = class { } if (html) { - const $node_group = - $(`
              + const $node_group = $(`
              ${html}
              `); - if (collapsed) - $node_group.addClass('collapsed'); + if (collapsed) $node_group.addClass("collapsed"); return $node_group; } @@ -482,9 +497,9 @@ erpnext.HierarchyChartMobile = class { let node_object = this.nodes[parent]; let node = node_object.$link; - node.removeClass('active-child active-path'); + node.removeClass("active-child active-path"); node_object.expanded = 0; - node_object.$children = undefined; + node_object.$children = null; this.nodes[node.id] = node_object; // show parent's siblings and expand parent node @@ -492,11 +507,10 @@ erpnext.HierarchyChartMobile = class { () => this.get_child_nodes(node_object.parent_id, node_object.id), (child_nodes) => this.get_node_group(child_nodes, node_object.parent_id, false), (node_group) => { - if (node_group) - this.$sibling_group.empty().append(node_group); + if (node_group) this.$sibling_group.empty().append(node_group); }, () => this.setup_node_group_action(), - () => this.reattach_and_expand_node(node, node_object) + () => this.reattach_and_expand_node(node, node_object), ]); } @@ -506,7 +520,7 @@ erpnext.HierarchyChartMobile = class { this.$hierarchy.empty().append(`
            • `); - this.$hierarchy.find('.level').append(el); + this.$hierarchy.find(".level").append(el); $(`#connectors`).empty(); this.expand_node(node_object); } @@ -514,28 +528,27 @@ erpnext.HierarchyChartMobile = class { remove_levels_after_node(node) { let level = $(`[id="${node.id}"]`).parent().parent().index(); - level = $('.hierarchy-mobile > li:eq('+ level + ')'); - level.nextAll('li').remove(); + level = $(".hierarchy-mobile > li:eq(" + level + ")"); + level.nextAll("li").remove(); let node_object = this.nodes[node.id]; let current_node = level.find(`[id="${node.id}"]`).detach(); - current_node.removeClass('active-child active-path'); + current_node.removeClass("active-child active-path"); node_object.expanded = 0; - node_object.$children = undefined; + node_object.$children = null; level.empty().append(current_node); } remove_orphaned_connectors() { - let paths = $('#connectors > path'); + let paths = $("#connectors > path"); $.each(paths, (_i, path) => { - const parent = $(path).data('parent'); - const child = $(path).data('child'); + const parent = $(path).data("parent"); + const child = $(path).data("child"); - if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length) - return; + if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length) return; $(path).remove(); }); diff --git a/erpnext/public/js/leaflet/leaflet.draw.js b/erpnext/public/js/leaflet/leaflet.draw.js index 26f1e19da5a..53450618780 100755 --- a/erpnext/public/js/leaflet/leaflet.draw.js +++ b/erpnext/public/js/leaflet/leaflet.draw.js @@ -6,138 +6,1731 @@ http://leafletjs.com https://github.com/jacobtoye */ -! function(t, e) { - L.drawVersion = "0.2.3", L.drawLocal = { draw: { toolbar: { actions: { title: "Cancel drawing", text: "Cancel" }, undo: { title: "Delete last point drawn", text: "Delete last point" }, buttons: { polyline: "Draw a polyline", polygon: "Draw a polygon", rectangle: "Draw a rectangle", circle: "Draw a circle", marker: "Draw a marker" } }, handlers: { circle: { tooltip: { start: "Click and drag to draw circle." } }, marker: { tooltip: { start: "Click map to place marker." } }, polygon: { tooltip: { start: "Click to start drawing shape.", cont: "Click to continue drawing shape.", end: "Click first point to close this shape." } }, polyline: { error: "Error: shape edges cannot cross!", tooltip: { start: "Click to start drawing line.", cont: "Click to continue drawing line.", end: "Click last point to finish line." } }, rectangle: { tooltip: { start: "Click and drag to draw rectangle." } }, simpleshape: { tooltip: { end: "Release mouse to finish drawing." } } } }, edit: { toolbar: { actions: { save: { title: "Save changes.", text: "Save" }, cancel: { title: "Cancel editing, discards all changes.", text: "Cancel" } }, buttons: { edit: "Edit layers.", editDisabled: "No layers to edit.", remove: "Delete layers.", removeDisabled: "No layers to delete." } }, handlers: { edit: { tooltip: { text: "Drag handles, or marker to edit feature.", subtext: "Click cancel to undo changes." } }, remove: { tooltip: { text: "Click on a feature to remove" } } } } }, L.Draw = {}, L.Draw.Feature = L.Handler.extend({ includes: L.Mixin.Events, initialize: function(t, e) { this._map = t, this._container = t._container, this._overlayPane = t._panes.overlayPane, this._popupPane = t._panes.popupPane, e && e.shapeOptions && (e.shapeOptions = L.Util.extend({}, this.options.shapeOptions, e.shapeOptions)), L.setOptions(this, e) }, enable: function() { this._enabled || (this.fire("enabled", { handler: this.type }), this._map.fire("draw:drawstart", { layerType: this.type }), L.Handler.prototype.enable.call(this)) }, disable: function() { this._enabled && (L.Handler.prototype.disable.call(this), this._map.fire("draw:drawstop", { layerType: this.type }), this.fire("disabled", { handler: this.type })) }, addHooks: function() { var t = this._map; - t && (L.DomUtil.disableTextSelection(), t.getContainer().focus(), this._tooltip = new L.Tooltip(this._map), L.DomEvent.on(this._container, "keyup", this._cancelDrawing, this)) }, removeHooks: function() { this._map && (L.DomUtil.enableTextSelection(), this._tooltip.dispose(), this._tooltip = null, L.DomEvent.off(this._container, "keyup", this._cancelDrawing, this)) }, setOptions: function(t) { L.setOptions(this, t) }, _fireCreatedEvent: function(t) { this._map.fire("draw:created", { layer: t, layerType: this.type }) }, _cancelDrawing: function(t) { 27 === t.keyCode && this.disable() } }), L.Draw.Polyline = L.Draw.Feature.extend({ statics: { TYPE: "polyline" }, Poly: L.Polyline, options: { allowIntersection: !0, repeatMode: !1, drawError: { color: "#b00b00", timeout: 2500 }, icon: new L.DivIcon({ iconSize: new L.Point(8, 8), className: "leaflet-div-icon leaflet-editing-icon" }), guidelineDistance: 20, maxGuideLineLength: 4e3, shapeOptions: { stroke: !0, color: "#f06eaa", weight: 4, opacity: .5, fill: !1, clickable: !0 }, metric: !0, showLength: !0, zIndexOffset: 2e3 }, initialize: function(t, e) { this.options.drawError.message = L.drawLocal.draw.handlers.polyline.error, e && e.drawError && (e.drawError = L.Util.extend({}, this.options.drawError, e.drawError)), this.type = L.Draw.Polyline.TYPE, L.Draw.Feature.prototype.initialize.call(this, t, e) }, addHooks: function() { L.Draw.Feature.prototype.addHooks.call(this), this._map && (this._markers = [], this._markerGroup = new L.LayerGroup, this._map.addLayer(this._markerGroup), this._poly = new L.Polyline([], this.options.shapeOptions), this._tooltip.updateContent(this._getTooltipText()), this._mouseMarker || (this._mouseMarker = L.marker(this._map.getCenter(), { icon: L.divIcon({ className: "leaflet-mouse-marker", iconAnchor: [20, 20], iconSize: [40, 40] }), opacity: 0, zIndexOffset: this.options.zIndexOffset })), this._mouseMarker.on("mousedown", this._onMouseDown, this).addTo(this._map), this._map.on("mousemove", this._onMouseMove, this).on("mouseup", this._onMouseUp, this).on("zoomend", this._onZoomEnd, this)) }, removeHooks: function() { L.Draw.Feature.prototype.removeHooks.call(this), this._clearHideErrorTimeout(), this._cleanUpShape(), this._map.removeLayer(this._markerGroup), delete this._markerGroup, delete this._markers, this._map.removeLayer(this._poly), delete this._poly, this._mouseMarker.off("mousedown", this._onMouseDown, this).off("mouseup", this._onMouseUp, this), this._map.removeLayer(this._mouseMarker), delete this._mouseMarker, this._clearGuides(), this._map.off("mousemove", this._onMouseMove, this).off("zoomend", this._onZoomEnd, this) }, deleteLastVertex: function() { if (!(this._markers.length <= 1)) { var t = this._markers.pop(), - e = this._poly, - i = this._poly.spliceLatLngs(e.getLatLngs().length - 1, 1)[0]; - this._markerGroup.removeLayer(t), e.getLatLngs().length < 2 && this._map.removeLayer(e), this._vertexChanged(i, !1) } }, addVertex: function(t) { var e = this._markers.length; return e > 0 && !this.options.allowIntersection && this._poly.newLatLngIntersects(t) ? void this._showErrorTooltip() : (this._errorShown && this._hideErrorTooltip(), this._markers.push(this._createMarker(t)), this._poly.addLatLng(t), 2 === this._poly.getLatLngs().length && this._map.addLayer(this._poly), void this._vertexChanged(t, !0)) }, _finishShape: function() { var t = this._poly.newLatLngIntersects(this._poly.getLatLngs()[0], !0); return !this.options.allowIntersection && t || !this._shapeIsValid() ? void this._showErrorTooltip() : (this._fireCreatedEvent(), this.disable(), void(this.options.repeatMode && this.enable())) }, _shapeIsValid: function() { return !0 }, _onZoomEnd: function() { this._updateGuide() }, _onMouseMove: function(t) { var e = t.layerPoint, - i = t.latlng; - this._currentLatLng = i, this._updateTooltip(i), this._updateGuide(e), this._mouseMarker.setLatLng(i), L.DomEvent.preventDefault(t.originalEvent) }, _vertexChanged: function(t, e) { this._updateFinishHandler(), this._updateRunningMeasure(t, e), this._clearGuides(), this._updateTooltip() }, _onMouseDown: function(t) { var e = t.originalEvent; - this._mouseDownOrigin = L.point(e.clientX, e.clientY) }, _onMouseUp: function(e) { if (this._mouseDownOrigin) { var i = L.point(e.originalEvent.clientX, e.originalEvent.clientY).distanceTo(this._mouseDownOrigin); - Math.abs(i) < 9 * (t.devicePixelRatio || 1) && this.addVertex(e.latlng) } - this._mouseDownOrigin = null }, _updateFinishHandler: function() { var t = this._markers.length; - t > 1 && this._markers[t - 1].on("click", this._finishShape, this), t > 2 && this._markers[t - 2].off("click", this._finishShape, this) }, _createMarker: function(t) { var e = new L.Marker(t, { icon: this.options.icon, zIndexOffset: 2 * this.options.zIndexOffset }); return this._markerGroup.addLayer(e), e }, _updateGuide: function(t) { var e = this._markers.length; - e > 0 && (t = t || this._map.latLngToLayerPoint(this._currentLatLng), this._clearGuides(), this._drawGuide(this._map.latLngToLayerPoint(this._markers[e - 1].getLatLng()), t)) }, _updateTooltip: function(t) { var e = this._getTooltipText(); - t && this._tooltip.updatePosition(t), this._errorShown || this._tooltip.updateContent(e) }, _drawGuide: function(t, e) { var i, o, a, s = Math.floor(Math.sqrt(Math.pow(e.x - t.x, 2) + Math.pow(e.y - t.y, 2))), - r = this.options.guidelineDistance, - n = this.options.maxGuideLineLength, - l = s > n ? s - n : r; for (this._guidesContainer || (this._guidesContainer = L.DomUtil.create("div", "leaflet-draw-guides", this._overlayPane)); s > l; l += this.options.guidelineDistance) i = l / s, o = { x: Math.floor(t.x * (1 - i) + i * e.x), y: Math.floor(t.y * (1 - i) + i * e.y) }, a = L.DomUtil.create("div", "leaflet-draw-guide-dash", this._guidesContainer), a.style.backgroundColor = this._errorShown ? this.options.drawError.color : this.options.shapeOptions.color, L.DomUtil.setPosition(a, o) }, _updateGuideColor: function(t) { if (this._guidesContainer) - for (var e = 0, i = this._guidesContainer.childNodes.length; i > e; e++) this._guidesContainer.childNodes[e].style.backgroundColor = t }, _clearGuides: function() { if (this._guidesContainer) - for (; this._guidesContainer.firstChild;) this._guidesContainer.removeChild(this._guidesContainer.firstChild) }, _getTooltipText: function() { var t, e, i = this.options.showLength; return 0 === this._markers.length ? t = { text: L.drawLocal.draw.handlers.polyline.tooltip.start } : (e = i ? this._getMeasurementString() : "", t = 1 === this._markers.length ? { text: L.drawLocal.draw.handlers.polyline.tooltip.cont, subtext: e } : { text: L.drawLocal.draw.handlers.polyline.tooltip.end, subtext: e }), t }, _updateRunningMeasure: function(t, e) { var i, o, a = this._markers.length; - 1 === this._markers.length ? this._measurementRunningTotal = 0 : (i = a - (e ? 2 : 1), o = t.distanceTo(this._markers[i].getLatLng()), this._measurementRunningTotal += o * (e ? 1 : -1)) }, _getMeasurementString: function() { var t, e = this._currentLatLng, - i = this._markers[this._markers.length - 1].getLatLng(); return t = this._measurementRunningTotal + e.distanceTo(i), L.GeometryUtil.readableDistance(t, this.options.metric) }, _showErrorTooltip: function() { this._errorShown = !0, this._tooltip.showAsError().updateContent({ text: this.options.drawError.message }), this._updateGuideColor(this.options.drawError.color), this._poly.setStyle({ color: this.options.drawError.color }), this._clearHideErrorTimeout(), this._hideErrorTimeout = setTimeout(L.Util.bind(this._hideErrorTooltip, this), this.options.drawError.timeout) }, _hideErrorTooltip: function() { this._errorShown = !1, this._clearHideErrorTimeout(), this._tooltip.removeError().updateContent(this._getTooltipText()), this._updateGuideColor(this.options.shapeOptions.color), this._poly.setStyle({ color: this.options.shapeOptions.color }) }, _clearHideErrorTimeout: function() { this._hideErrorTimeout && (clearTimeout(this._hideErrorTimeout), this._hideErrorTimeout = null) }, _cleanUpShape: function() { this._markers.length > 1 && this._markers[this._markers.length - 1].off("click", this._finishShape, this) }, _fireCreatedEvent: function() { var t = new this.Poly(this._poly.getLatLngs(), this.options.shapeOptions); - L.Draw.Feature.prototype._fireCreatedEvent.call(this, t) } }), L.Draw.Polygon = L.Draw.Polyline.extend({ statics: { TYPE: "polygon" }, Poly: L.Polygon, options: { showArea: !1, shapeOptions: { stroke: !0, color: "#f06eaa", weight: 4, opacity: .5, fill: !0, fillColor: null, fillOpacity: .2, clickable: !0 } }, initialize: function(t, e) { L.Draw.Polyline.prototype.initialize.call(this, t, e), this.type = L.Draw.Polygon.TYPE }, _updateFinishHandler: function() { var t = this._markers.length; - 1 === t && this._markers[0].on("click", this._finishShape, this), t > 2 && (this._markers[t - 1].on("dblclick", this._finishShape, this), t > 3 && this._markers[t - 2].off("dblclick", this._finishShape, this)) }, _getTooltipText: function() { var t, e; return 0 === this._markers.length ? t = L.drawLocal.draw.handlers.polygon.tooltip.start : this._markers.length < 3 ? t = L.drawLocal.draw.handlers.polygon.tooltip.cont : (t = L.drawLocal.draw.handlers.polygon.tooltip.end, e = this._getMeasurementString()), { text: t, subtext: e } }, _getMeasurementString: function() { var t = this._area; return t ? L.GeometryUtil.readableArea(t, this.options.metric) : null }, _shapeIsValid: function() { return this._markers.length >= 3 }, _vertexAdded: function() { if (!this.options.allowIntersection && this.options.showArea) { var t = this._poly.getLatLngs(); - this._area = L.GeometryUtil.geodesicArea(t) } }, _cleanUpShape: function() { var t = this._markers.length; - t > 0 && (this._markers[0].off("click", this._finishShape, this), t > 2 && this._markers[t - 1].off("dblclick", this._finishShape, this)) } }), L.SimpleShape = {}, L.Draw.SimpleShape = L.Draw.Feature.extend({ options: { repeatMode: !1 }, initialize: function(t, e) { this._endLabelText = L.drawLocal.draw.handlers.simpleshape.tooltip.end, L.Draw.Feature.prototype.initialize.call(this, t, e) }, addHooks: function() { L.Draw.Feature.prototype.addHooks.call(this), this._map && (this._mapDraggable = this._map.dragging.enabled(), this._mapDraggable && this._map.dragging.disable(), this._container.style.cursor = "crosshair", this._tooltip.updateContent({ text: this._initialLabelText }), this._map.on("mousedown", this._onMouseDown, this).on("mousemove", this._onMouseMove, this)) }, removeHooks: function() { L.Draw.Feature.prototype.removeHooks.call(this), this._map && (this._mapDraggable && this._map.dragging.enable(), this._container.style.cursor = "", this._map.off("mousedown", this._onMouseDown, this).off("mousemove", this._onMouseMove, this), L.DomEvent.off(e, "mouseup", this._onMouseUp, this), this._shape && (this._map.removeLayer(this._shape), delete this._shape)), this._isDrawing = !1 }, _onMouseDown: function(t) { this._isDrawing = !0, this._startLatLng = t.latlng, L.DomEvent.on(e, "mouseup", this._onMouseUp, this).preventDefault(t.originalEvent) }, _onMouseMove: function(t) { var e = t.latlng; - this._tooltip.updatePosition(e), this._isDrawing && (this._tooltip.updateContent({ text: this._endLabelText }), this._drawShape(e)) }, _onMouseUp: function() { this._shape && this._fireCreatedEvent(), this.disable(), this.options.repeatMode && this.enable() } }), L.Draw.Rectangle = L.Draw.SimpleShape.extend({ statics: { TYPE: "rectangle" }, options: { shapeOptions: { stroke: !0, color: "#f06eaa", weight: 4, opacity: .5, fill: !0, fillColor: null, fillOpacity: .2, clickable: !0 } }, initialize: function(t, e) { this.type = L.Draw.Rectangle.TYPE, this._initialLabelText = L.drawLocal.draw.handlers.rectangle.tooltip.start, L.Draw.SimpleShape.prototype.initialize.call(this, t, e) }, _drawShape: function(t) { this._shape ? this._shape.setBounds(new L.LatLngBounds(this._startLatLng, t)) : (this._shape = new L.Rectangle(new L.LatLngBounds(this._startLatLng, t), this.options.shapeOptions), this._map.addLayer(this._shape)) }, _fireCreatedEvent: function() { var t = new L.Rectangle(this._shape.getBounds(), this.options.shapeOptions); - L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, t) } }), L.Draw.Circle = L.Draw.SimpleShape.extend({ statics: { TYPE: "circle" }, options: { shapeOptions: { stroke: !0, color: "#f06eaa", weight: 4, opacity: .5, fill: !0, fillColor: null, fillOpacity: .2, clickable: !0 }, showRadius: !0, metric: !0 }, initialize: function(t, e) { this.type = L.Draw.Circle.TYPE, this._initialLabelText = L.drawLocal.draw.handlers.circle.tooltip.start, L.Draw.SimpleShape.prototype.initialize.call(this, t, e) }, _drawShape: function(t) { this._shape ? this._shape.setRadius(this._startLatLng.distanceTo(t)) : (this._shape = new L.Circle(this._startLatLng, this._startLatLng.distanceTo(t), this.options.shapeOptions), this._map.addLayer(this._shape)) }, _fireCreatedEvent: function() { var t = new L.Circle(this._startLatLng, this._shape.getRadius(), this.options.shapeOptions); - L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, t) }, _onMouseMove: function(t) { var e, i = t.latlng, - o = this.options.showRadius, - a = this.options.metric; - this._tooltip.updatePosition(i), this._isDrawing && (this._drawShape(i), e = this._shape.getRadius().toFixed(1), this._tooltip.updateContent({ text: this._endLabelText, subtext: o ? "Radius: " + L.GeometryUtil.readableDistance(e, a) : "" })) } }), L.Draw.Marker = L.Draw.Feature.extend({ statics: { TYPE: "marker" }, options: { icon: new L.Icon.Default, repeatMode: !1, zIndexOffset: 2e3 }, initialize: function(t, e) { this.type = L.Draw.Marker.TYPE, L.Draw.Feature.prototype.initialize.call(this, t, e) }, addHooks: function() { L.Draw.Feature.prototype.addHooks.call(this), this._map && (this._tooltip.updateContent({ text: L.drawLocal.draw.handlers.marker.tooltip.start }), this._mouseMarker || (this._mouseMarker = L.marker(this._map.getCenter(), { icon: L.divIcon({ className: "leaflet-mouse-marker", iconAnchor: [20, 20], iconSize: [40, 40] }), opacity: 0, zIndexOffset: this.options.zIndexOffset })), this._mouseMarker.on("click", this._onClick, this).addTo(this._map), this._map.on("mousemove", this._onMouseMove, this)) }, removeHooks: function() { L.Draw.Feature.prototype.removeHooks.call(this), this._map && (this._marker && (this._marker.off("click", this._onClick, this), this._map.off("click", this._onClick, this).removeLayer(this._marker), delete this._marker), this._mouseMarker.off("click", this._onClick, this), this._map.removeLayer(this._mouseMarker), delete this._mouseMarker, this._map.off("mousemove", this._onMouseMove, this)) }, _onMouseMove: function(t) { var e = t.latlng; - this._tooltip.updatePosition(e), this._mouseMarker.setLatLng(e), this._marker ? (e = this._mouseMarker.getLatLng(), this._marker.setLatLng(e)) : (this._marker = new L.Marker(e, { icon: this.options.icon, zIndexOffset: this.options.zIndexOffset }), this._marker.on("click", this._onClick, this), this._map.on("click", this._onClick, this).addLayer(this._marker)) }, _onClick: function() { this._fireCreatedEvent(), this.disable(), this.options.repeatMode && this.enable() }, _fireCreatedEvent: function() { var t = new L.Marker(this._marker.getLatLng(), { icon: this.options.icon }); - L.Draw.Feature.prototype._fireCreatedEvent.call(this, t) } }), L.Edit = L.Edit || {}, L.Edit.Poly = L.Handler.extend({ options: { icon: new L.DivIcon({ iconSize: new L.Point(8, 8), className: "leaflet-div-icon leaflet-editing-icon" }) }, initialize: function(t, e) { this._poly = t, L.setOptions(this, e) }, addHooks: function() { this._poly._map && (this._markerGroup || this._initMarkers(), this._poly._map.addLayer(this._markerGroup)) }, removeHooks: function() { this._poly._map && (this._poly._map.removeLayer(this._markerGroup), delete this._markerGroup, delete this._markers) }, updateMarkers: function() { this._markerGroup.clearLayers(), this._initMarkers() }, _initMarkers: function() { this._markerGroup || (this._markerGroup = new L.LayerGroup), this._markers = []; var t, e, i, o, a = this._poly._latlngs; for (t = 0, i = a.length; i > t; t++) o = this._createMarker(a[t], t), o.on("click", this._onMarkerClick, this), this._markers.push(o); var s, r; for (t = 0, e = i - 1; i > t; e = t++)(0 !== t || L.Polygon && this._poly instanceof L.Polygon) && (s = this._markers[e], r = this._markers[t], this._createMiddleMarker(s, r), this._updatePrevNext(s, r)) }, _createMarker: function(t, e) { var i = new L.Marker(t, { draggable: !0, icon: this.options.icon }); return i._origLatLng = t, i._index = e, i.on("drag", this._onMarkerDrag, this), i.on("dragend", this._fireEdit, this), this._markerGroup.addLayer(i), i }, _removeMarker: function(t) { var e = t._index; - this._markerGroup.removeLayer(t), this._markers.splice(e, 1), this._poly.spliceLatLngs(e, 1), this._updateIndexes(e, -1), t.off("drag", this._onMarkerDrag, this).off("dragend", this._fireEdit, this).off("click", this._onMarkerClick, this) }, _fireEdit: function() { this._poly.edited = !0, this._poly.fire("edit") }, _onMarkerDrag: function(t) { var e = t.target; - L.extend(e._origLatLng, e._latlng), e._middleLeft && e._middleLeft.setLatLng(this._getMiddleLatLng(e._prev, e)), e._middleRight && e._middleRight.setLatLng(this._getMiddleLatLng(e, e._next)), this._poly.redraw() }, _onMarkerClick: function(t) { var e = L.Polygon && this._poly instanceof L.Polygon ? 4 : 3, - i = t.target; - this._poly._latlngs.length < e || (this._removeMarker(i), this._updatePrevNext(i._prev, i._next), i._middleLeft && this._markerGroup.removeLayer(i._middleLeft), i._middleRight && this._markerGroup.removeLayer(i._middleRight), i._prev && i._next ? this._createMiddleMarker(i._prev, i._next) : i._prev ? i._next || (i._prev._middleRight = null) : i._next._middleLeft = null, this._fireEdit()) }, _updateIndexes: function(t, e) { this._markerGroup.eachLayer(function(i) { i._index > t && (i._index += e) }) }, _createMiddleMarker: function(t, e) { var i, o, a, s = this._getMiddleLatLng(t, e), - r = this._createMarker(s); - r.setOpacity(.6), t._middleRight = e._middleLeft = r, o = function() { var o = e._index; - r._index = o, r.off("click", i, this).on("click", this._onMarkerClick, this), s.lat = r.getLatLng().lat, s.lng = r.getLatLng().lng, this._poly.spliceLatLngs(o, 0, s), this._markers.splice(o, 0, r), r.setOpacity(1), this._updateIndexes(o, 1), e._index++, this._updatePrevNext(t, r), this._updatePrevNext(r, e), this._poly.fire("editstart") }, a = function() { r.off("dragstart", o, this), r.off("dragend", a, this), this._createMiddleMarker(t, r), this._createMiddleMarker(r, e) }, i = function() { o.call(this), a.call(this), this._fireEdit() }, r.on("click", i, this).on("dragstart", o, this).on("dragend", a, this), this._markerGroup.addLayer(r) }, _updatePrevNext: function(t, e) { t && (t._next = e), e && (e._prev = t) }, _getMiddleLatLng: function(t, e) { var i = this._poly._map, - o = i.project(t.getLatLng()), - a = i.project(e.getLatLng()); return i.unproject(o._add(a)._divideBy(2)) } }), L.Polyline.addInitHook(function() { this.editing || (L.Edit.Poly && (this.editing = new L.Edit.Poly(this), this.options.editable && this.editing.enable()), this.on("add", function() { this.editing && this.editing.enabled() && this.editing.addHooks() }), this.on("remove", function() { this.editing && this.editing.enabled() && this.editing.removeHooks() })) }), L.Edit = L.Edit || {}, L.Edit.SimpleShape = L.Handler.extend({ options: { moveIcon: new L.DivIcon({ iconSize: new L.Point(8, 8), className: "leaflet-div-icon leaflet-editing-icon leaflet-edit-move" }), resizeIcon: new L.DivIcon({ iconSize: new L.Point(8, 8), className: "leaflet-div-icon leaflet-editing-icon leaflet-edit-resize" }) }, initialize: function(t, e) { this._shape = t, L.Util.setOptions(this, e) }, addHooks: function() { this._shape._map && (this._map = this._shape._map, this._markerGroup || this._initMarkers(), this._map.addLayer(this._markerGroup)) }, removeHooks: function() { if (this._shape._map) { this._unbindMarker(this._moveMarker); for (var t = 0, e = this._resizeMarkers.length; e > t; t++) this._unbindMarker(this._resizeMarkers[t]); - this._resizeMarkers = null, this._map.removeLayer(this._markerGroup), delete this._markerGroup } - this._map = null }, updateMarkers: function() { this._markerGroup.clearLayers(), this._initMarkers() }, _initMarkers: function() { this._markerGroup || (this._markerGroup = new L.LayerGroup), this._createMoveMarker(), this._createResizeMarker() }, _createMoveMarker: function() {}, _createResizeMarker: function() {}, _createMarker: function(t, e) { var i = new L.Marker(t, { draggable: !0, icon: e, zIndexOffset: 10 }); return this._bindMarker(i), this._markerGroup.addLayer(i), i }, _bindMarker: function(t) { t.on("dragstart", this._onMarkerDragStart, this).on("drag", this._onMarkerDrag, this).on("dragend", this._onMarkerDragEnd, this) }, _unbindMarker: function(t) { t.off("dragstart", this._onMarkerDragStart, this).off("drag", this._onMarkerDrag, this).off("dragend", this._onMarkerDragEnd, this) }, _onMarkerDragStart: function(t) { var e = t.target; - e.setOpacity(0), this._shape.fire("editstart") }, _fireEdit: function() { this._shape.edited = !0, this._shape.fire("edit") }, _onMarkerDrag: function(t) { var e = t.target, - i = e.getLatLng(); - e === this._moveMarker ? this._move(i) : this._resize(i), this._shape.redraw() }, _onMarkerDragEnd: function(t) { var e = t.target; - e.setOpacity(1), this._fireEdit() }, _move: function() {}, _resize: function() {} }), L.Edit = L.Edit || {}, L.Edit.Rectangle = L.Edit.SimpleShape.extend({ _createMoveMarker: function() { var t = this._shape.getBounds(), - e = t.getCenter(); - this._moveMarker = this._createMarker(e, this.options.moveIcon) }, _createResizeMarker: function() { var t = this._getCorners(); - this._resizeMarkers = []; for (var e = 0, i = t.length; i > e; e++) this._resizeMarkers.push(this._createMarker(t[e], this.options.resizeIcon)), this._resizeMarkers[e]._cornerIndex = e }, _onMarkerDragStart: function(t) { L.Edit.SimpleShape.prototype._onMarkerDragStart.call(this, t); var e = this._getCorners(), - i = t.target, - o = i._cornerIndex; - this._oppositeCorner = e[(o + 2) % 4], this._toggleCornerMarkers(0, o) }, _onMarkerDragEnd: function(t) { var e, i, o = t.target; - o === this._moveMarker && (e = this._shape.getBounds(), i = e.getCenter(), o.setLatLng(i)), this._toggleCornerMarkers(1), this._repositionCornerMarkers(), L.Edit.SimpleShape.prototype._onMarkerDragEnd.call(this, t) }, _move: function(t) { for (var e, i = this._shape.getLatLngs(), o = this._shape.getBounds(), a = o.getCenter(), s = [], r = 0, n = i.length; n > r; r++) e = [i[r].lat - a.lat, i[r].lng - a.lng], s.push([t.lat + e[0], t.lng + e[1]]); - this._shape.setLatLngs(s), this._repositionCornerMarkers() }, _resize: function(t) { var e; - this._shape.setBounds(L.latLngBounds(t, this._oppositeCorner)), e = this._shape.getBounds(), this._moveMarker.setLatLng(e.getCenter()) }, _getCorners: function() { var t = this._shape.getBounds(), - e = t.getNorthWest(), - i = t.getNorthEast(), - o = t.getSouthEast(), - a = t.getSouthWest(); return [e, i, o, a] }, _toggleCornerMarkers: function(t) { for (var e = 0, i = this._resizeMarkers.length; i > e; e++) this._resizeMarkers[e].setOpacity(t) }, _repositionCornerMarkers: function() { for (var t = this._getCorners(), e = 0, i = this._resizeMarkers.length; i > e; e++) this._resizeMarkers[e].setLatLng(t[e]) } }), L.Rectangle.addInitHook(function() { L.Edit.Rectangle && (this.editing = new L.Edit.Rectangle(this), this.options.editable && this.editing.enable()) }), L.Edit = L.Edit || {}, L.Edit.Circle = L.Edit.SimpleShape.extend({ _createMoveMarker: function() { var t = this._shape.getLatLng(); - this._moveMarker = this._createMarker(t, this.options.moveIcon) }, _createResizeMarker: function() { var t = this._shape.getLatLng(), - e = this._getResizeMarkerPoint(t); - this._resizeMarkers = [], this._resizeMarkers.push(this._createMarker(e, this.options.resizeIcon)) }, _getResizeMarkerPoint: function(t) { var e = this._shape._radius * Math.cos(Math.PI / 4), - i = this._map.project(t); return this._map.unproject([i.x + e, i.y - e]) }, _move: function(t) { var e = this._getResizeMarkerPoint(t); - this._resizeMarkers[0].setLatLng(e), this._shape.setLatLng(t) }, _resize: function(t) { var e = this._moveMarker.getLatLng(), - i = e.distanceTo(t); - this._shape.setRadius(i) } }), L.Circle.addInitHook(function() { L.Edit.Circle && (this.editing = new L.Edit.Circle(this), this.options.editable && this.editing.enable()), this.on("add", function() { this.editing && this.editing.enabled() && this.editing.addHooks() }), this.on("remove", function() { this.editing && this.editing.enabled() && this.editing.removeHooks() }) }), L.LatLngUtil = { cloneLatLngs: function(t) { for (var e = [], i = 0, o = t.length; o > i; i++) e.push(this.cloneLatLng(t[i])); return e }, cloneLatLng: function(t) { return L.latLng(t.lat, t.lng) } }, L.GeometryUtil = L.extend(L.GeometryUtil || {}, { geodesicArea: function(t) { var e, i, o = t.length, - a = 0, - s = L.LatLng.DEG_TO_RAD; if (o > 2) { for (var r = 0; o > r; r++) e = t[r], i = t[(r + 1) % o], a += (i.lng - e.lng) * s * (2 + Math.sin(e.lat * s) + Math.sin(i.lat * s)); - a = 6378137 * a * 6378137 / 2 } return Math.abs(a) }, readableArea: function(t, e) { var i; return e ? i = t >= 1e4 ? (1e-4 * t).toFixed(2) + " ha" : t.toFixed(2) + " m²" : (t *= .836127, i = t >= 3097600 ? (t / 3097600).toFixed(2) + " mi²" : t >= 4840 ? (t / 4840).toFixed(2) + " acres" : Math.ceil(t) + " yd²"), i }, readableDistance: function(t, e) { var i; return e ? i = t > 1e3 ? (t / 1e3).toFixed(2) + " km" : Math.ceil(t) + " m" : (t *= 1.09361, i = t > 1760 ? (t / 1760).toFixed(2) + " miles" : Math.ceil(t) + " yd"), i } }), L.Util.extend(L.LineUtil, { segmentsIntersect: function(t, e, i, o) { return this._checkCounterclockwise(t, i, o) !== this._checkCounterclockwise(e, i, o) && this._checkCounterclockwise(t, e, i) !== this._checkCounterclockwise(t, e, o) }, _checkCounterclockwise: function(t, e, i) { return (i.y - t.y) * (e.x - t.x) > (e.y - t.y) * (i.x - t.x) } }), L.Polyline.include({ intersects: function() { var t, e, i, o = this._originalPoints, - a = o ? o.length : 0; if (this._tooFewPointsForIntersection()) return !1; for (t = a - 1; t >= 3; t--) - if (e = o[t - 1], i = o[t], this._lineSegmentsIntersectsRange(e, i, t - 2)) return !0; - return !1 }, newLatLngIntersects: function(t, e) { return this._map ? this.newPointIntersects(this._map.latLngToLayerPoint(t), e) : !1 }, newPointIntersects: function(t, e) { var i = this._originalPoints, - o = i ? i.length : 0, - a = i ? i[o - 1] : null, - s = o - 2; return this._tooFewPointsForIntersection(1) ? !1 : this._lineSegmentsIntersectsRange(a, t, s, e ? 1 : 0) }, _tooFewPointsForIntersection: function(t) { var e = this._originalPoints, - i = e ? e.length : 0; return i += t || 0, !this._originalPoints || 3 >= i }, _lineSegmentsIntersectsRange: function(t, e, i, o) { var a, s, r = this._originalPoints; - o = o || 0; for (var n = i; n > o; n--) - if (a = r[n - 1], s = r[n], L.LineUtil.segmentsIntersect(t, e, a, s)) return !0; - return !1 } }), L.Polygon.include({ intersects: function() { var t, e, i, o, a, s = this._originalPoints; return this._tooFewPointsForIntersection() ? !1 : (t = L.Polyline.prototype.intersects.call(this)) ? !0 : (e = s.length, i = s[0], o = s[e - 1], a = e - 2, this._lineSegmentsIntersectsRange(o, i, a, 1)) } }), L.Control.Draw = L.Control.extend({ options: { position: "topleft", draw: {}, edit: !1 }, initialize: function(t) { if (L.version < "0.7") throw new Error("Leaflet.draw 0.2.3+ requires Leaflet 0.7.0+. Download latest from https://github.com/Leaflet/Leaflet/"); - L.Control.prototype.initialize.call(this, t); var e, i; - this._toolbars = {}, L.DrawToolbar && this.options.draw && (i = new L.DrawToolbar(this.options.draw), e = L.stamp(i), this._toolbars[e] = i, this._toolbars[e].on("enable", this._toolbarEnabled, this)), L.EditToolbar && this.options.edit && (i = new L.EditToolbar(this.options.edit), e = L.stamp(i), this._toolbars[e] = i, this._toolbars[e].on("enable", this._toolbarEnabled, this)) }, onAdd: function(t) { var e, i = L.DomUtil.create("div", "leaflet-draw"), - o = !1, - a = "leaflet-draw-toolbar-top"; for (var s in this._toolbars) this._toolbars.hasOwnProperty(s) && (e = this._toolbars[s].addToolbar(t), e && (o || (L.DomUtil.hasClass(e, a) || L.DomUtil.addClass(e.childNodes[0], a), o = !0), i.appendChild(e))); return i }, onRemove: function() { for (var t in this._toolbars) this._toolbars.hasOwnProperty(t) && this._toolbars[t].removeToolbar() }, setDrawingOptions: function(t) { for (var e in this._toolbars) this._toolbars[e] instanceof L.DrawToolbar && this._toolbars[e].setOptions(t) }, _toolbarEnabled: function(t) { var e = "" + L.stamp(t.target); for (var i in this._toolbars) this._toolbars.hasOwnProperty(i) && i !== e && this._toolbars[i].disable() } }), L.Map.mergeOptions({ drawControlTooltips: !0, drawControl: !1 }), L.Map.addInitHook(function() { this.options.drawControl && (this.drawControl = new L.Control.Draw, this.addControl(this.drawControl)) }), L.Toolbar = L.Class.extend({ - includes: [L.Mixin.Events], - initialize: function(t) { L.setOptions(this, t), this._modes = {}, this._actionButtons = [], this._activeMode = null }, - enabled: function() { return null !== this._activeMode }, - disable: function() { this.enabled() && this._activeMode.handler.disable() }, - addToolbar: function(t) { var e, i = L.DomUtil.create("div", "leaflet-draw-section"), - o = 0, - a = this._toolbarClass || "", - s = this.getModeHandlers(t); for (this._toolbarContainer = L.DomUtil.create("div", "leaflet-draw-toolbar leaflet-bar"), this._map = t, e = 0; e < s.length; e++) s[e].enabled && this._initModeHandler(s[e].handler, this._toolbarContainer, o++, a, s[e].title); return o ? (this._lastButtonIndex = --o, this._actionsContainer = L.DomUtil.create("ul", "leaflet-draw-actions"), i.appendChild(this._toolbarContainer), i.appendChild(this._actionsContainer), i) : void 0 }, - removeToolbar: function() { for (var t in this._modes) this._modes.hasOwnProperty(t) && (this._disposeButton(this._modes[t].button, this._modes[t].handler.enable, this._modes[t].handler), this._modes[t].handler.disable(), this._modes[t].handler.off("enabled", this._handlerActivated, this).off("disabled", this._handlerDeactivated, this)); - this._modes = {}; for (var e = 0, i = this._actionButtons.length; i > e; e++) this._disposeButton(this._actionButtons[e].button, this._actionButtons[e].callback, this); - this._actionButtons = [], this._actionsContainer = null }, - _initModeHandler: function(t, e, i, o, a) { var s = t.type; - this._modes[s] = {}, this._modes[s].handler = t, this._modes[s].button = this._createButton({ title: a, className: o + "-" + s, container: e, callback: this._modes[s].handler.enable, context: this._modes[s].handler }), this._modes[s].buttonIndex = i, this._modes[s].handler.on("enabled", this._handlerActivated, this).on("disabled", this._handlerDeactivated, this) }, - _createButton: function(t) { var e = L.DomUtil.create("a", t.className || "", t.container); return e.href = "#", t.text && (e.innerHTML = t.text), t.title && (e.title = t.title), L.DomEvent.on(e, "click", L.DomEvent.stopPropagation).on(e, "mousedown", L.DomEvent.stopPropagation).on(e, "dblclick", L.DomEvent.stopPropagation).on(e, "click", L.DomEvent.preventDefault).on(e, "click", t.callback, t.context), e }, - _disposeButton: function(t, e) { L.DomEvent.off(t, "click", L.DomEvent.stopPropagation).off(t, "mousedown", L.DomEvent.stopPropagation).off(t, "dblclick", L.DomEvent.stopPropagation).off(t, "click", L.DomEvent.preventDefault).off(t, "click", e) }, - _handlerActivated: function(t) { this.disable(), this._activeMode = this._modes[t.handler], L.DomUtil.addClass(this._activeMode.button, "leaflet-draw-toolbar-button-enabled"), this._showActionsToolbar(), this.fire("enable") }, - _handlerDeactivated: function() { this._hideActionsToolbar(), L.DomUtil.removeClass(this._activeMode.button, "leaflet-draw-toolbar-button-enabled"), this._activeMode = null, this.fire("disable") }, - _createActions: function(t) { var e, i, o, a, s = this._actionsContainer, - r = this.getActions(t), - n = r.length; for (i = 0, o = this._actionButtons.length; o > i; i++) this._disposeButton(this._actionButtons[i].button, this._actionButtons[i].callback); for (this._actionButtons = []; s.firstChild;) s.removeChild(s.firstChild); for (var l = 0; n > l; l++) "enabled" in r[l] && !r[l].enabled || (e = L.DomUtil.create("li", "", s), a = this._createButton({ title: r[l].title, text: r[l].text, container: e, callback: r[l].callback, context: r[l].context }), this._actionButtons.push({ button: a, callback: r[l].callback })) }, - _showActionsToolbar: function() { - var t = this._activeMode.buttonIndex, - e = this._lastButtonIndex, - i = this._activeMode.button.offsetTop - 1; - this._createActions(this._activeMode.handler), this._actionsContainer.style.top = i + "px", 0 === t && (L.DomUtil.addClass(this._toolbarContainer, "leaflet-draw-toolbar-notop"), L.DomUtil.addClass(this._actionsContainer, "leaflet-draw-actions-top")), t === e && (L.DomUtil.addClass(this._toolbarContainer, "leaflet-draw-toolbar-nobottom"), L.DomUtil.addClass(this._actionsContainer, "leaflet-draw-actions-bottom")), this._actionsContainer.style.display = "block" - }, - _hideActionsToolbar: function() { this._actionsContainer.style.display = "none", L.DomUtil.removeClass(this._toolbarContainer, "leaflet-draw-toolbar-notop"), L.DomUtil.removeClass(this._toolbarContainer, "leaflet-draw-toolbar-nobottom"), L.DomUtil.removeClass(this._actionsContainer, "leaflet-draw-actions-top"), L.DomUtil.removeClass(this._actionsContainer, "leaflet-draw-actions-bottom") } - }), L.Tooltip = L.Class.extend({ initialize: function(t) { this._map = t, this._popupPane = t._panes.popupPane, this._container = t.options.drawControlTooltips ? L.DomUtil.create("div", "leaflet-draw-tooltip", this._popupPane) : null, this._singleLineLabel = !1 }, dispose: function() { this._container && (this._popupPane.removeChild(this._container), this._container = null) }, updateContent: function(t) { return this._container ? (t.subtext = t.subtext || "", 0 !== t.subtext.length || this._singleLineLabel ? t.subtext.length > 0 && this._singleLineLabel && (L.DomUtil.removeClass(this._container, "leaflet-draw-tooltip-single"), this._singleLineLabel = !1) : (L.DomUtil.addClass(this._container, "leaflet-draw-tooltip-single"), this._singleLineLabel = !0), this._container.innerHTML = (t.subtext.length > 0 ? '' + t.subtext + "
              " : "") + "" + t.text + "", this) : this }, updatePosition: function(t) { var e = this._map.latLngToLayerPoint(t), - i = this._container; return this._container && (i.style.visibility = "inherit", L.DomUtil.setPosition(i, e)), this }, showAsError: function() { return this._container && L.DomUtil.addClass(this._container, "leaflet-error-draw-tooltip"), this }, removeError: function() { return this._container && L.DomUtil.removeClass(this._container, "leaflet-error-draw-tooltip"), this } }), L.DrawToolbar = L.Toolbar.extend({ options: { polyline: {}, polygon: {}, rectangle: {}, circle: {}, marker: {} }, initialize: function(t) { for (var e in this.options) this.options.hasOwnProperty(e) && t[e] && (t[e] = L.extend({}, this.options[e], t[e])); - this._toolbarClass = "leaflet-draw-draw", L.Toolbar.prototype.initialize.call(this, t) }, getModeHandlers: function(t) { return [{ enabled: this.options.polyline, handler: new L.Draw.Polyline(t, this.options.polyline), title: L.drawLocal.draw.toolbar.buttons.polyline }, { enabled: this.options.polygon, handler: new L.Draw.Polygon(t, this.options.polygon), title: L.drawLocal.draw.toolbar.buttons.polygon }, { enabled: this.options.rectangle, handler: new L.Draw.Rectangle(t, this.options.rectangle), title: L.drawLocal.draw.toolbar.buttons.rectangle }, { enabled: this.options.circle, handler: new L.Draw.Circle(t, this.options.circle), title: L.drawLocal.draw.toolbar.buttons.circle }, { enabled: this.options.marker, handler: new L.Draw.Marker(t, this.options.marker), title: L.drawLocal.draw.toolbar.buttons.marker }] }, getActions: function(t) { return [{ enabled: t.deleteLastVertex, title: L.drawLocal.draw.toolbar.undo.title, text: L.drawLocal.draw.toolbar.undo.text, callback: t.deleteLastVertex, context: t }, { title: L.drawLocal.draw.toolbar.actions.title, text: L.drawLocal.draw.toolbar.actions.text, callback: this.disable, context: this }] }, setOptions: function(t) { L.setOptions(this, t); for (var e in this._modes) this._modes.hasOwnProperty(e) && t.hasOwnProperty(e) && this._modes[e].handler.setOptions(t[e]) } }), L.EditToolbar = L.Toolbar.extend({ options: { edit: { selectedPathOptions: { color: "#fe57a1", opacity: .6, dashArray: "10, 10", fill: !0, fillColor: "#fe57a1", fillOpacity: .1 } }, remove: {}, featureGroup: null }, initialize: function(t) { t.edit && ("undefined" == typeof t.edit.selectedPathOptions && (t.edit.selectedPathOptions = this.options.edit.selectedPathOptions), t.edit = L.extend({}, this.options.edit, t.edit)), t.remove && (t.remove = L.extend({}, this.options.remove, t.remove)), this._toolbarClass = "leaflet-draw-edit", L.Toolbar.prototype.initialize.call(this, t), this._selectedFeatureCount = 0 }, getModeHandlers: function(t) { var e = this.options.featureGroup; return [{ enabled: this.options.edit, handler: new L.EditToolbar.Edit(t, { featureGroup: e, selectedPathOptions: this.options.edit.selectedPathOptions }), title: L.drawLocal.edit.toolbar.buttons.edit }, { enabled: this.options.remove, handler: new L.EditToolbar.Delete(t, { featureGroup: e }), title: L.drawLocal.edit.toolbar.buttons.remove }] }, getActions: function() { return [{ title: L.drawLocal.edit.toolbar.actions.save.title, text: L.drawLocal.edit.toolbar.actions.save.text, callback: this._save, context: this }, { title: L.drawLocal.edit.toolbar.actions.cancel.title, text: L.drawLocal.edit.toolbar.actions.cancel.text, callback: this.disable, context: this }] }, addToolbar: function(t) { var e = L.Toolbar.prototype.addToolbar.call(this, t); return this._checkDisabled(), this.options.featureGroup.on("layeradd layerremove", this._checkDisabled, this), e }, removeToolbar: function() { this.options.featureGroup.off("layeradd layerremove", this._checkDisabled, this), L.Toolbar.prototype.removeToolbar.call(this) }, disable: function() { this.enabled() && (this._activeMode.handler.revertLayers(), L.Toolbar.prototype.disable.call(this)) }, _save: function() { this._activeMode.handler.save(), this._activeMode.handler.disable() }, _checkDisabled: function() { var t, e = this.options.featureGroup, - i = 0 !== e.getLayers().length; - this.options.edit && (t = this._modes[L.EditToolbar.Edit.TYPE].button, i ? L.DomUtil.removeClass(t, "leaflet-disabled") : L.DomUtil.addClass(t, "leaflet-disabled"), t.setAttribute("title", i ? L.drawLocal.edit.toolbar.buttons.edit : L.drawLocal.edit.toolbar.buttons.editDisabled)), this.options.remove && (t = this._modes[L.EditToolbar.Delete.TYPE].button, i ? L.DomUtil.removeClass(t, "leaflet-disabled") : L.DomUtil.addClass(t, "leaflet-disabled"), t.setAttribute("title", i ? L.drawLocal.edit.toolbar.buttons.remove : L.drawLocal.edit.toolbar.buttons.removeDisabled)) } }), L.EditToolbar.Edit = L.Handler.extend({ statics: { TYPE: "edit" }, includes: L.Mixin.Events, initialize: function(t, e) { if (L.Handler.prototype.initialize.call(this, t), this._selectedPathOptions = e.selectedPathOptions, this._featureGroup = e.featureGroup, !(this._featureGroup instanceof L.FeatureGroup)) throw new Error("options.featureGroup must be a L.FeatureGroup"); - this._uneditedLayerProps = {}, this.type = L.EditToolbar.Edit.TYPE }, enable: function() {!this._enabled && this._hasAvailableLayers() && (this.fire("enabled", { handler: this.type }), this._map.fire("draw:editstart", { handler: this.type }), L.Handler.prototype.enable.call(this), this._featureGroup.on("layeradd", this._enableLayerEdit, this).on("layerremove", this._disableLayerEdit, this)) }, disable: function() { this._enabled && (this._featureGroup.off("layeradd", this._enableLayerEdit, this).off("layerremove", this._disableLayerEdit, this), L.Handler.prototype.disable.call(this), this._map.fire("draw:editstop", { handler: this.type }), this.fire("disabled", { handler: this.type })) }, addHooks: function() { var t = this._map; - t && (t.getContainer().focus(), this._featureGroup.eachLayer(this._enableLayerEdit, this), this._tooltip = new L.Tooltip(this._map), this._tooltip.updateContent({ text: L.drawLocal.edit.handlers.edit.tooltip.text, subtext: L.drawLocal.edit.handlers.edit.tooltip.subtext }), this._map.on("mousemove", this._onMouseMove, this)) }, removeHooks: function() { this._map && (this._featureGroup.eachLayer(this._disableLayerEdit, this), this._uneditedLayerProps = {}, this._tooltip.dispose(), this._tooltip = null, this._map.off("mousemove", this._onMouseMove, this)) }, revertLayers: function() { this._featureGroup.eachLayer(function(t) { this._revertLayer(t) }, this) }, save: function() { var t = new L.LayerGroup; - this._featureGroup.eachLayer(function(e) { e.edited && (t.addLayer(e), e.edited = !1) }), this._map.fire("draw:edited", { layers: t }) }, _backupLayer: function(t) { var e = L.Util.stamp(t); - this._uneditedLayerProps[e] || (t instanceof L.Polyline || t instanceof L.Polygon || t instanceof L.Rectangle ? this._uneditedLayerProps[e] = { latlngs: L.LatLngUtil.cloneLatLngs(t.getLatLngs()) } : t instanceof L.Circle ? this._uneditedLayerProps[e] = { latlng: L.LatLngUtil.cloneLatLng(t.getLatLng()), radius: t.getRadius() } : t instanceof L.Marker && (this._uneditedLayerProps[e] = { latlng: L.LatLngUtil.cloneLatLng(t.getLatLng()) })) }, _revertLayer: function(t) { var e = L.Util.stamp(t); - t.edited = !1, this._uneditedLayerProps.hasOwnProperty(e) && (t instanceof L.Polyline || t instanceof L.Polygon || t instanceof L.Rectangle ? t.setLatLngs(this._uneditedLayerProps[e].latlngs) : t instanceof L.Circle ? (t.setLatLng(this._uneditedLayerProps[e].latlng), t.setRadius(this._uneditedLayerProps[e].radius)) : t instanceof L.Marker && t.setLatLng(this._uneditedLayerProps[e].latlng)) }, _toggleMarkerHighlight: function(t) { if (t._icon) { var e = t._icon; - e.style.display = "none", L.DomUtil.hasClass(e, "leaflet-edit-marker-selected") ? (L.DomUtil.removeClass(e, "leaflet-edit-marker-selected"), this._offsetMarker(e, -4)) : (L.DomUtil.addClass(e, "leaflet-edit-marker-selected"), this._offsetMarker(e, 4)), e.style.display = "" } }, _offsetMarker: function(t, e) { var i = parseInt(t.style.marginTop, 10) - e, - o = parseInt(t.style.marginLeft, 10) - e; - t.style.marginTop = i + "px", t.style.marginLeft = o + "px" }, _enableLayerEdit: function(t) { var e, i = t.layer || t.target || t, - o = i instanceof L.Marker; - (!o || i._icon) && (this._backupLayer(i), this._selectedPathOptions && (e = L.Util.extend({}, this._selectedPathOptions), o ? this._toggleMarkerHighlight(i) : (i.options.previousOptions = L.Util.extend({ dashArray: null }, i.options), i instanceof L.Circle || i instanceof L.Polygon || i instanceof L.Rectangle || (e.fill = !1), i.setStyle(e))), o ? (i.dragging.enable(), i.on("dragend", this._onMarkerDragEnd)) : i.editing.enable()) }, _disableLayerEdit: function(t) { var e = t.layer || t.target || t; - e.edited = !1, this._selectedPathOptions && (e instanceof L.Marker ? this._toggleMarkerHighlight(e) : (e.setStyle(e.options.previousOptions), delete e.options.previousOptions)), e instanceof L.Marker ? (e.dragging.disable(), e.off("dragend", this._onMarkerDragEnd, this)) : e.editing.disable() }, _onMarkerDragEnd: function(t) { var e = t.target; - e.edited = !0 }, _onMouseMove: function(t) { this._tooltip.updatePosition(t.latlng) }, _hasAvailableLayers: function() { return 0 !== this._featureGroup.getLayers().length } }), L.EditToolbar.Delete = L.Handler.extend({ statics: { TYPE: "remove" }, includes: L.Mixin.Events, initialize: function(t, e) { if (L.Handler.prototype.initialize.call(this, t), L.Util.setOptions(this, e), this._deletableLayers = this.options.featureGroup, !(this._deletableLayers instanceof L.FeatureGroup)) throw new Error("options.featureGroup must be a L.FeatureGroup"); - this.type = L.EditToolbar.Delete.TYPE }, enable: function() {!this._enabled && this._hasAvailableLayers() && (this.fire("enabled", { handler: this.type }), this._map.fire("draw:deletestart", { handler: this.type }), L.Handler.prototype.enable.call(this), this._deletableLayers.on("layeradd", this._enableLayerDelete, this).on("layerremove", this._disableLayerDelete, this)) }, disable: function() { this._enabled && (this._deletableLayers.off("layeradd", this._enableLayerDelete, this).off("layerremove", this._disableLayerDelete, this), L.Handler.prototype.disable.call(this), this._map.fire("draw:deletestop", { handler: this.type }), this.fire("disabled", { handler: this.type })) }, addHooks: function() { var t = this._map; - t && (t.getContainer().focus(), this._deletableLayers.eachLayer(this._enableLayerDelete, this), this._deletedLayers = new L.layerGroup, this._tooltip = new L.Tooltip(this._map), this._tooltip.updateContent({ text: L.drawLocal.edit.handlers.remove.tooltip.text }), this._map.on("mousemove", this._onMouseMove, this)) }, removeHooks: function() { this._map && (this._deletableLayers.eachLayer(this._disableLayerDelete, this), this._deletedLayers = null, this._tooltip.dispose(), this._tooltip = null, this._map.off("mousemove", this._onMouseMove, this)) }, revertLayers: function() { this._deletedLayers.eachLayer(function(t) { this._deletableLayers.addLayer(t) }, this) }, save: function() { this._map.fire("draw:deleted", { layers: this._deletedLayers }) }, _enableLayerDelete: function(t) { var e = t.layer || t.target || t; - e.on("click", this._removeLayer, this) }, _disableLayerDelete: function(t) { var e = t.layer || t.target || t; - e.off("click", this._removeLayer, this), this._deletedLayers.removeLayer(e) }, _removeLayer: function(t) { var e = t.layer || t.target || t; - this._deletableLayers.removeLayer(e), this._deletedLayers.addLayer(e) }, _onMouseMove: function(t) { this._tooltip.updatePosition(t.latlng) }, _hasAvailableLayers: function() { return 0 !== this._deletableLayers.getLayers().length } }) -}(window, document); +!(function (t, e) { + (L.drawVersion = "0.2.3"), + (L.drawLocal = { + draw: { + toolbar: { + actions: { title: "Cancel drawing", text: "Cancel" }, + undo: { title: "Delete last point drawn", text: "Delete last point" }, + buttons: { + polyline: "Draw a polyline", + polygon: "Draw a polygon", + rectangle: "Draw a rectangle", + circle: "Draw a circle", + marker: "Draw a marker", + }, + }, + handlers: { + circle: { tooltip: { start: "Click and drag to draw circle." } }, + marker: { tooltip: { start: "Click map to place marker." } }, + polygon: { + tooltip: { + start: "Click to start drawing shape.", + cont: "Click to continue drawing shape.", + end: "Click first point to close this shape.", + }, + }, + polyline: { + error: "Error: shape edges cannot cross!", + tooltip: { + start: "Click to start drawing line.", + cont: "Click to continue drawing line.", + end: "Click last point to finish line.", + }, + }, + rectangle: { tooltip: { start: "Click and drag to draw rectangle." } }, + simpleshape: { tooltip: { end: "Release mouse to finish drawing." } }, + }, + }, + edit: { + toolbar: { + actions: { + save: { title: "Save changes.", text: "Save" }, + cancel: { title: "Cancel editing, discards all changes.", text: "Cancel" }, + }, + buttons: { + edit: "Edit layers.", + editDisabled: "No layers to edit.", + remove: "Delete layers.", + removeDisabled: "No layers to delete.", + }, + }, + handlers: { + edit: { + tooltip: { + text: "Drag handles, or marker to edit feature.", + subtext: "Click cancel to undo changes.", + }, + }, + remove: { tooltip: { text: "Click on a feature to remove" } }, + }, + }, + }), + (L.Draw = {}), + (L.Draw.Feature = L.Handler.extend({ + includes: L.Mixin.Events, + initialize: function (t, e) { + (this._map = t), + (this._container = t._container), + (this._overlayPane = t._panes.overlayPane), + (this._popupPane = t._panes.popupPane), + e && + e.shapeOptions && + (e.shapeOptions = L.Util.extend({}, this.options.shapeOptions, e.shapeOptions)), + L.setOptions(this, e); + }, + enable: function () { + this._enabled || + (this.fire("enabled", { handler: this.type }), + this._map.fire("draw:drawstart", { layerType: this.type }), + L.Handler.prototype.enable.call(this)); + }, + disable: function () { + this._enabled && + (L.Handler.prototype.disable.call(this), + this._map.fire("draw:drawstop", { layerType: this.type }), + this.fire("disabled", { handler: this.type })); + }, + addHooks: function () { + var t = this._map; + t && + (L.DomUtil.disableTextSelection(), + t.getContainer().focus(), + (this._tooltip = new L.Tooltip(this._map)), + L.DomEvent.on(this._container, "keyup", this._cancelDrawing, this)); + }, + removeHooks: function () { + this._map && + (L.DomUtil.enableTextSelection(), + this._tooltip.dispose(), + (this._tooltip = null), + L.DomEvent.off(this._container, "keyup", this._cancelDrawing, this)); + }, + setOptions: function (t) { + L.setOptions(this, t); + }, + _fireCreatedEvent: function (t) { + this._map.fire("draw:created", { layer: t, layerType: this.type }); + }, + _cancelDrawing: function (t) { + 27 === t.keyCode && this.disable(); + }, + })), + (L.Draw.Polyline = L.Draw.Feature.extend({ + statics: { TYPE: "polyline" }, + Poly: L.Polyline, + options: { + allowIntersection: !0, + repeatMode: !1, + drawError: { color: "#b00b00", timeout: 2500 }, + icon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: "leaflet-div-icon leaflet-editing-icon", + }), + guidelineDistance: 20, + maxGuideLineLength: 4e3, + shapeOptions: { + stroke: !0, + color: "#f06eaa", + weight: 4, + opacity: 0.5, + fill: !1, + clickable: !0, + }, + metric: !0, + showLength: !0, + zIndexOffset: 2e3, + }, + initialize: function (t, e) { + (this.options.drawError.message = L.drawLocal.draw.handlers.polyline.error), + e && + e.drawError && + (e.drawError = L.Util.extend({}, this.options.drawError, e.drawError)), + (this.type = L.Draw.Polyline.TYPE), + L.Draw.Feature.prototype.initialize.call(this, t, e); + }, + addHooks: function () { + L.Draw.Feature.prototype.addHooks.call(this), + this._map && + ((this._markers = []), + (this._markerGroup = new L.LayerGroup()), + this._map.addLayer(this._markerGroup), + (this._poly = new L.Polyline([], this.options.shapeOptions)), + this._tooltip.updateContent(this._getTooltipText()), + this._mouseMarker || + (this._mouseMarker = L.marker(this._map.getCenter(), { + icon: L.divIcon({ + className: "leaflet-mouse-marker", + iconAnchor: [20, 20], + iconSize: [40, 40], + }), + opacity: 0, + zIndexOffset: this.options.zIndexOffset, + })), + this._mouseMarker.on("mousedown", this._onMouseDown, this).addTo(this._map), + this._map + .on("mousemove", this._onMouseMove, this) + .on("mouseup", this._onMouseUp, this) + .on("zoomend", this._onZoomEnd, this)); + }, + removeHooks: function () { + L.Draw.Feature.prototype.removeHooks.call(this), + this._clearHideErrorTimeout(), + this._cleanUpShape(), + this._map.removeLayer(this._markerGroup), + delete this._markerGroup, + delete this._markers, + this._map.removeLayer(this._poly), + delete this._poly, + this._mouseMarker + .off("mousedown", this._onMouseDown, this) + .off("mouseup", this._onMouseUp, this), + this._map.removeLayer(this._mouseMarker), + delete this._mouseMarker, + this._clearGuides(), + this._map.off("mousemove", this._onMouseMove, this).off("zoomend", this._onZoomEnd, this); + }, + deleteLastVertex: function () { + if (!(this._markers.length <= 1)) { + var t = this._markers.pop(), + e = this._poly, + i = this._poly.spliceLatLngs(e.getLatLngs().length - 1, 1)[0]; + this._markerGroup.removeLayer(t), + e.getLatLngs().length < 2 && this._map.removeLayer(e), + this._vertexChanged(i, !1); + } + }, + addVertex: function (t) { + var e = this._markers.length; + return e > 0 && !this.options.allowIntersection && this._poly.newLatLngIntersects(t) + ? void this._showErrorTooltip() + : (this._errorShown && this._hideErrorTooltip(), + this._markers.push(this._createMarker(t)), + this._poly.addLatLng(t), + 2 === this._poly.getLatLngs().length && this._map.addLayer(this._poly), + void this._vertexChanged(t, !0)); + }, + _finishShape: function () { + var t = this._poly.newLatLngIntersects(this._poly.getLatLngs()[0], !0); + return (!this.options.allowIntersection && t) || !this._shapeIsValid() + ? void this._showErrorTooltip() + : (this._fireCreatedEvent(), + this.disable(), + void (this.options.repeatMode && this.enable())); + }, + _shapeIsValid: function () { + return !0; + }, + _onZoomEnd: function () { + this._updateGuide(); + }, + _onMouseMove: function (t) { + var e = t.layerPoint, + i = t.latlng; + (this._currentLatLng = i), + this._updateTooltip(i), + this._updateGuide(e), + this._mouseMarker.setLatLng(i), + L.DomEvent.preventDefault(t.originalEvent); + }, + _vertexChanged: function (t, e) { + this._updateFinishHandler(), + this._updateRunningMeasure(t, e), + this._clearGuides(), + this._updateTooltip(); + }, + _onMouseDown: function (t) { + var e = t.originalEvent; + this._mouseDownOrigin = L.point(e.clientX, e.clientY); + }, + _onMouseUp: function (e) { + if (this._mouseDownOrigin) { + var i = L.point(e.originalEvent.clientX, e.originalEvent.clientY).distanceTo( + this._mouseDownOrigin + ); + Math.abs(i) < 9 * (t.devicePixelRatio || 1) && this.addVertex(e.latlng); + } + this._mouseDownOrigin = null; + }, + _updateFinishHandler: function () { + var t = this._markers.length; + t > 1 && this._markers[t - 1].on("click", this._finishShape, this), + t > 2 && this._markers[t - 2].off("click", this._finishShape, this); + }, + _createMarker: function (t) { + var e = new L.Marker(t, { + icon: this.options.icon, + zIndexOffset: 2 * this.options.zIndexOffset, + }); + return this._markerGroup.addLayer(e), e; + }, + _updateGuide: function (t) { + var e = this._markers.length; + e > 0 && + ((t = t || this._map.latLngToLayerPoint(this._currentLatLng)), + this._clearGuides(), + this._drawGuide(this._map.latLngToLayerPoint(this._markers[e - 1].getLatLng()), t)); + }, + _updateTooltip: function (t) { + var e = this._getTooltipText(); + t && this._tooltip.updatePosition(t), this._errorShown || this._tooltip.updateContent(e); + }, + _drawGuide: function (t, e) { + var i, + o, + a, + s = Math.floor(Math.sqrt(Math.pow(e.x - t.x, 2) + Math.pow(e.y - t.y, 2))), + r = this.options.guidelineDistance, + n = this.options.maxGuideLineLength, + l = s > n ? s - n : r; + for ( + this._guidesContainer || + (this._guidesContainer = L.DomUtil.create( + "div", + "leaflet-draw-guides", + this._overlayPane + )); + s > l; + l += this.options.guidelineDistance + ) + (i = l / s), + (o = { + x: Math.floor(t.x * (1 - i) + i * e.x), + y: Math.floor(t.y * (1 - i) + i * e.y), + }), + (a = L.DomUtil.create("div", "leaflet-draw-guide-dash", this._guidesContainer)), + (a.style.backgroundColor = this._errorShown + ? this.options.drawError.color + : this.options.shapeOptions.color), + L.DomUtil.setPosition(a, o); + }, + _updateGuideColor: function (t) { + if (this._guidesContainer) + for (var e = 0, i = this._guidesContainer.childNodes.length; i > e; e++) + this._guidesContainer.childNodes[e].style.backgroundColor = t; + }, + _clearGuides: function () { + if (this._guidesContainer) + for (; this._guidesContainer.firstChild; ) + this._guidesContainer.removeChild(this._guidesContainer.firstChild); + }, + _getTooltipText: function () { + var t, + e, + i = this.options.showLength; + return ( + 0 === this._markers.length + ? (t = { text: L.drawLocal.draw.handlers.polyline.tooltip.start }) + : ((e = i ? this._getMeasurementString() : ""), + (t = + 1 === this._markers.length + ? { text: L.drawLocal.draw.handlers.polyline.tooltip.cont, subtext: e } + : { text: L.drawLocal.draw.handlers.polyline.tooltip.end, subtext: e })), + t + ); + }, + _updateRunningMeasure: function (t, e) { + var i, + o, + a = this._markers.length; + 1 === this._markers.length + ? (this._measurementRunningTotal = 0) + : ((i = a - (e ? 2 : 1)), + (o = t.distanceTo(this._markers[i].getLatLng())), + (this._measurementRunningTotal += o * (e ? 1 : -1))); + }, + _getMeasurementString: function () { + var t, + e = this._currentLatLng, + i = this._markers[this._markers.length - 1].getLatLng(); + return ( + (t = this._measurementRunningTotal + e.distanceTo(i)), + L.GeometryUtil.readableDistance(t, this.options.metric) + ); + }, + _showErrorTooltip: function () { + (this._errorShown = !0), + this._tooltip.showAsError().updateContent({ text: this.options.drawError.message }), + this._updateGuideColor(this.options.drawError.color), + this._poly.setStyle({ color: this.options.drawError.color }), + this._clearHideErrorTimeout(), + (this._hideErrorTimeout = setTimeout( + L.Util.bind(this._hideErrorTooltip, this), + this.options.drawError.timeout + )); + }, + _hideErrorTooltip: function () { + (this._errorShown = !1), + this._clearHideErrorTimeout(), + this._tooltip.removeError().updateContent(this._getTooltipText()), + this._updateGuideColor(this.options.shapeOptions.color), + this._poly.setStyle({ color: this.options.shapeOptions.color }); + }, + _clearHideErrorTimeout: function () { + this._hideErrorTimeout && + (clearTimeout(this._hideErrorTimeout), (this._hideErrorTimeout = null)); + }, + _cleanUpShape: function () { + this._markers.length > 1 && + this._markers[this._markers.length - 1].off("click", this._finishShape, this); + }, + _fireCreatedEvent: function () { + var t = new this.Poly(this._poly.getLatLngs(), this.options.shapeOptions); + L.Draw.Feature.prototype._fireCreatedEvent.call(this, t); + }, + })), + (L.Draw.Polygon = L.Draw.Polyline.extend({ + statics: { TYPE: "polygon" }, + Poly: L.Polygon, + options: { + showArea: !1, + shapeOptions: { + stroke: !0, + color: "#f06eaa", + weight: 4, + opacity: 0.5, + fill: !0, + fillColor: null, + fillOpacity: 0.2, + clickable: !0, + }, + }, + initialize: function (t, e) { + L.Draw.Polyline.prototype.initialize.call(this, t, e), (this.type = L.Draw.Polygon.TYPE); + }, + _updateFinishHandler: function () { + var t = this._markers.length; + 1 === t && this._markers[0].on("click", this._finishShape, this), + t > 2 && + (this._markers[t - 1].on("dblclick", this._finishShape, this), + t > 3 && this._markers[t - 2].off("dblclick", this._finishShape, this)); + }, + _getTooltipText: function () { + var t, e; + return ( + 0 === this._markers.length + ? (t = L.drawLocal.draw.handlers.polygon.tooltip.start) + : this._markers.length < 3 + ? (t = L.drawLocal.draw.handlers.polygon.tooltip.cont) + : ((t = L.drawLocal.draw.handlers.polygon.tooltip.end), + (e = this._getMeasurementString())), + { text: t, subtext: e } + ); + }, + _getMeasurementString: function () { + var t = this._area; + return t ? L.GeometryUtil.readableArea(t, this.options.metric) : null; + }, + _shapeIsValid: function () { + return this._markers.length >= 3; + }, + _vertexAdded: function () { + if (!this.options.allowIntersection && this.options.showArea) { + var t = this._poly.getLatLngs(); + this._area = L.GeometryUtil.geodesicArea(t); + } + }, + _cleanUpShape: function () { + var t = this._markers.length; + t > 0 && + (this._markers[0].off("click", this._finishShape, this), + t > 2 && this._markers[t - 1].off("dblclick", this._finishShape, this)); + }, + })), + (L.SimpleShape = {}), + (L.Draw.SimpleShape = L.Draw.Feature.extend({ + options: { repeatMode: !1 }, + initialize: function (t, e) { + (this._endLabelText = L.drawLocal.draw.handlers.simpleshape.tooltip.end), + L.Draw.Feature.prototype.initialize.call(this, t, e); + }, + addHooks: function () { + L.Draw.Feature.prototype.addHooks.call(this), + this._map && + ((this._mapDraggable = this._map.dragging.enabled()), + this._mapDraggable && this._map.dragging.disable(), + (this._container.style.cursor = "crosshair"), + this._tooltip.updateContent({ text: this._initialLabelText }), + this._map + .on("mousedown", this._onMouseDown, this) + .on("mousemove", this._onMouseMove, this)); + }, + removeHooks: function () { + L.Draw.Feature.prototype.removeHooks.call(this), + this._map && + (this._mapDraggable && this._map.dragging.enable(), + (this._container.style.cursor = ""), + this._map + .off("mousedown", this._onMouseDown, this) + .off("mousemove", this._onMouseMove, this), + L.DomEvent.off(e, "mouseup", this._onMouseUp, this), + this._shape && (this._map.removeLayer(this._shape), delete this._shape)), + (this._isDrawing = !1); + }, + _onMouseDown: function (t) { + (this._isDrawing = !0), + (this._startLatLng = t.latlng), + L.DomEvent.on(e, "mouseup", this._onMouseUp, this).preventDefault(t.originalEvent); + }, + _onMouseMove: function (t) { + var e = t.latlng; + this._tooltip.updatePosition(e), + this._isDrawing && + (this._tooltip.updateContent({ text: this._endLabelText }), this._drawShape(e)); + }, + _onMouseUp: function () { + this._shape && this._fireCreatedEvent(), + this.disable(), + this.options.repeatMode && this.enable(); + }, + })), + (L.Draw.Rectangle = L.Draw.SimpleShape.extend({ + statics: { TYPE: "rectangle" }, + options: { + shapeOptions: { + stroke: !0, + color: "#f06eaa", + weight: 4, + opacity: 0.5, + fill: !0, + fillColor: null, + fillOpacity: 0.2, + clickable: !0, + }, + }, + initialize: function (t, e) { + (this.type = L.Draw.Rectangle.TYPE), + (this._initialLabelText = L.drawLocal.draw.handlers.rectangle.tooltip.start), + L.Draw.SimpleShape.prototype.initialize.call(this, t, e); + }, + _drawShape: function (t) { + this._shape + ? this._shape.setBounds(new L.LatLngBounds(this._startLatLng, t)) + : ((this._shape = new L.Rectangle( + new L.LatLngBounds(this._startLatLng, t), + this.options.shapeOptions + )), + this._map.addLayer(this._shape)); + }, + _fireCreatedEvent: function () { + var t = new L.Rectangle(this._shape.getBounds(), this.options.shapeOptions); + L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, t); + }, + })), + (L.Draw.Circle = L.Draw.SimpleShape.extend({ + statics: { TYPE: "circle" }, + options: { + shapeOptions: { + stroke: !0, + color: "#f06eaa", + weight: 4, + opacity: 0.5, + fill: !0, + fillColor: null, + fillOpacity: 0.2, + clickable: !0, + }, + showRadius: !0, + metric: !0, + }, + initialize: function (t, e) { + (this.type = L.Draw.Circle.TYPE), + (this._initialLabelText = L.drawLocal.draw.handlers.circle.tooltip.start), + L.Draw.SimpleShape.prototype.initialize.call(this, t, e); + }, + _drawShape: function (t) { + this._shape + ? this._shape.setRadius(this._startLatLng.distanceTo(t)) + : ((this._shape = new L.Circle( + this._startLatLng, + this._startLatLng.distanceTo(t), + this.options.shapeOptions + )), + this._map.addLayer(this._shape)); + }, + _fireCreatedEvent: function () { + var t = new L.Circle(this._startLatLng, this._shape.getRadius(), this.options.shapeOptions); + L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, t); + }, + _onMouseMove: function (t) { + var e, + i = t.latlng, + o = this.options.showRadius, + a = this.options.metric; + this._tooltip.updatePosition(i), + this._isDrawing && + (this._drawShape(i), + (e = this._shape.getRadius().toFixed(1)), + this._tooltip.updateContent({ + text: this._endLabelText, + subtext: o ? "Radius: " + L.GeometryUtil.readableDistance(e, a) : "", + })); + }, + })), + (L.Draw.Marker = L.Draw.Feature.extend({ + statics: { TYPE: "marker" }, + options: { icon: new L.Icon.Default(), repeatMode: !1, zIndexOffset: 2e3 }, + initialize: function (t, e) { + (this.type = L.Draw.Marker.TYPE), L.Draw.Feature.prototype.initialize.call(this, t, e); + }, + addHooks: function () { + L.Draw.Feature.prototype.addHooks.call(this), + this._map && + (this._tooltip.updateContent({ + text: L.drawLocal.draw.handlers.marker.tooltip.start, + }), + this._mouseMarker || + (this._mouseMarker = L.marker(this._map.getCenter(), { + icon: L.divIcon({ + className: "leaflet-mouse-marker", + iconAnchor: [20, 20], + iconSize: [40, 40], + }), + opacity: 0, + zIndexOffset: this.options.zIndexOffset, + })), + this._mouseMarker.on("click", this._onClick, this).addTo(this._map), + this._map.on("mousemove", this._onMouseMove, this)); + }, + removeHooks: function () { + L.Draw.Feature.prototype.removeHooks.call(this), + this._map && + (this._marker && + (this._marker.off("click", this._onClick, this), + this._map.off("click", this._onClick, this).removeLayer(this._marker), + delete this._marker), + this._mouseMarker.off("click", this._onClick, this), + this._map.removeLayer(this._mouseMarker), + delete this._mouseMarker, + this._map.off("mousemove", this._onMouseMove, this)); + }, + _onMouseMove: function (t) { + var e = t.latlng; + this._tooltip.updatePosition(e), + this._mouseMarker.setLatLng(e), + this._marker + ? ((e = this._mouseMarker.getLatLng()), this._marker.setLatLng(e)) + : ((this._marker = new L.Marker(e, { + icon: this.options.icon, + zIndexOffset: this.options.zIndexOffset, + })), + this._marker.on("click", this._onClick, this), + this._map.on("click", this._onClick, this).addLayer(this._marker)); + }, + _onClick: function () { + this._fireCreatedEvent(), this.disable(), this.options.repeatMode && this.enable(); + }, + _fireCreatedEvent: function () { + var t = new L.Marker(this._marker.getLatLng(), { icon: this.options.icon }); + L.Draw.Feature.prototype._fireCreatedEvent.call(this, t); + }, + })), + (L.Edit = L.Edit || {}), + (L.Edit.Poly = L.Handler.extend({ + options: { + icon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: "leaflet-div-icon leaflet-editing-icon", + }), + }, + initialize: function (t, e) { + (this._poly = t), L.setOptions(this, e); + }, + addHooks: function () { + this._poly._map && + (this._markerGroup || this._initMarkers(), this._poly._map.addLayer(this._markerGroup)); + }, + removeHooks: function () { + this._poly._map && + (this._poly._map.removeLayer(this._markerGroup), + delete this._markerGroup, + delete this._markers); + }, + updateMarkers: function () { + this._markerGroup.clearLayers(), this._initMarkers(); + }, + _initMarkers: function () { + this._markerGroup || (this._markerGroup = new L.LayerGroup()), (this._markers = []); + var t, + e, + i, + o, + a = this._poly._latlngs; + for (t = 0, i = a.length; i > t; t++) + (o = this._createMarker(a[t], t)), + o.on("click", this._onMarkerClick, this), + this._markers.push(o); + var s, r; + for (t = 0, e = i - 1; i > t; e = t++) + (0 !== t || (L.Polygon && this._poly instanceof L.Polygon)) && + ((s = this._markers[e]), + (r = this._markers[t]), + this._createMiddleMarker(s, r), + this._updatePrevNext(s, r)); + }, + _createMarker: function (t, e) { + var i = new L.Marker(t, { draggable: !0, icon: this.options.icon }); + return ( + (i._origLatLng = t), + (i._index = e), + i.on("drag", this._onMarkerDrag, this), + i.on("dragend", this._fireEdit, this), + this._markerGroup.addLayer(i), + i + ); + }, + _removeMarker: function (t) { + var e = t._index; + this._markerGroup.removeLayer(t), + this._markers.splice(e, 1), + this._poly.spliceLatLngs(e, 1), + this._updateIndexes(e, -1), + t + .off("drag", this._onMarkerDrag, this) + .off("dragend", this._fireEdit, this) + .off("click", this._onMarkerClick, this); + }, + _fireEdit: function () { + (this._poly.edited = !0), this._poly.fire("edit"); + }, + _onMarkerDrag: function (t) { + var e = t.target; + L.extend(e._origLatLng, e._latlng), + e._middleLeft && e._middleLeft.setLatLng(this._getMiddleLatLng(e._prev, e)), + e._middleRight && e._middleRight.setLatLng(this._getMiddleLatLng(e, e._next)), + this._poly.redraw(); + }, + _onMarkerClick: function (t) { + var e = L.Polygon && this._poly instanceof L.Polygon ? 4 : 3, + i = t.target; + this._poly._latlngs.length < e || + (this._removeMarker(i), + this._updatePrevNext(i._prev, i._next), + i._middleLeft && this._markerGroup.removeLayer(i._middleLeft), + i._middleRight && this._markerGroup.removeLayer(i._middleRight), + i._prev && i._next + ? this._createMiddleMarker(i._prev, i._next) + : i._prev + ? i._next || (i._prev._middleRight = null) + : (i._next._middleLeft = null), + this._fireEdit()); + }, + _updateIndexes: function (t, e) { + this._markerGroup.eachLayer(function (i) { + i._index > t && (i._index += e); + }); + }, + _createMiddleMarker: function (t, e) { + var i, + o, + a, + s = this._getMiddleLatLng(t, e), + r = this._createMarker(s); + r.setOpacity(0.6), + (t._middleRight = e._middleLeft = r), + (o = function () { + var o = e._index; + (r._index = o), + r.off("click", i, this).on("click", this._onMarkerClick, this), + (s.lat = r.getLatLng().lat), + (s.lng = r.getLatLng().lng), + this._poly.spliceLatLngs(o, 0, s), + this._markers.splice(o, 0, r), + r.setOpacity(1), + this._updateIndexes(o, 1), + e._index++, + this._updatePrevNext(t, r), + this._updatePrevNext(r, e), + this._poly.fire("editstart"); + }), + (a = function () { + r.off("dragstart", o, this), + r.off("dragend", a, this), + this._createMiddleMarker(t, r), + this._createMiddleMarker(r, e); + }), + (i = function () { + o.call(this), a.call(this), this._fireEdit(); + }), + r.on("click", i, this).on("dragstart", o, this).on("dragend", a, this), + this._markerGroup.addLayer(r); + }, + _updatePrevNext: function (t, e) { + t && (t._next = e), e && (e._prev = t); + }, + _getMiddleLatLng: function (t, e) { + var i = this._poly._map, + o = i.project(t.getLatLng()), + a = i.project(e.getLatLng()); + return i.unproject(o._add(a)._divideBy(2)); + }, + })), + L.Polyline.addInitHook(function () { + this.editing || + (L.Edit.Poly && + ((this.editing = new L.Edit.Poly(this)), this.options.editable && this.editing.enable()), + this.on("add", function () { + this.editing && this.editing.enabled() && this.editing.addHooks(); + }), + this.on("remove", function () { + this.editing && this.editing.enabled() && this.editing.removeHooks(); + })); + }), + (L.Edit = L.Edit || {}), + (L.Edit.SimpleShape = L.Handler.extend({ + options: { + moveIcon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: "leaflet-div-icon leaflet-editing-icon leaflet-edit-move", + }), + resizeIcon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: "leaflet-div-icon leaflet-editing-icon leaflet-edit-resize", + }), + }, + initialize: function (t, e) { + (this._shape = t), L.Util.setOptions(this, e); + }, + addHooks: function () { + this._shape._map && + ((this._map = this._shape._map), + this._markerGroup || this._initMarkers(), + this._map.addLayer(this._markerGroup)); + }, + removeHooks: function () { + if (this._shape._map) { + this._unbindMarker(this._moveMarker); + for (var t = 0, e = this._resizeMarkers.length; e > t; t++) + this._unbindMarker(this._resizeMarkers[t]); + (this._resizeMarkers = null), + this._map.removeLayer(this._markerGroup), + delete this._markerGroup; + } + this._map = null; + }, + updateMarkers: function () { + this._markerGroup.clearLayers(), this._initMarkers(); + }, + _initMarkers: function () { + this._markerGroup || (this._markerGroup = new L.LayerGroup()), + this._createMoveMarker(), + this._createResizeMarker(); + }, + _createMoveMarker: function () {}, + _createResizeMarker: function () {}, + _createMarker: function (t, e) { + var i = new L.Marker(t, { draggable: !0, icon: e, zIndexOffset: 10 }); + return this._bindMarker(i), this._markerGroup.addLayer(i), i; + }, + _bindMarker: function (t) { + t.on("dragstart", this._onMarkerDragStart, this) + .on("drag", this._onMarkerDrag, this) + .on("dragend", this._onMarkerDragEnd, this); + }, + _unbindMarker: function (t) { + t.off("dragstart", this._onMarkerDragStart, this) + .off("drag", this._onMarkerDrag, this) + .off("dragend", this._onMarkerDragEnd, this); + }, + _onMarkerDragStart: function (t) { + var e = t.target; + e.setOpacity(0), this._shape.fire("editstart"); + }, + _fireEdit: function () { + (this._shape.edited = !0), this._shape.fire("edit"); + }, + _onMarkerDrag: function (t) { + var e = t.target, + i = e.getLatLng(); + e === this._moveMarker ? this._move(i) : this._resize(i), this._shape.redraw(); + }, + _onMarkerDragEnd: function (t) { + var e = t.target; + e.setOpacity(1), this._fireEdit(); + }, + _move: function () {}, + _resize: function () {}, + })), + (L.Edit = L.Edit || {}), + (L.Edit.Rectangle = L.Edit.SimpleShape.extend({ + _createMoveMarker: function () { + var t = this._shape.getBounds(), + e = t.getCenter(); + this._moveMarker = this._createMarker(e, this.options.moveIcon); + }, + _createResizeMarker: function () { + var t = this._getCorners(); + this._resizeMarkers = []; + for (var e = 0, i = t.length; i > e; e++) + this._resizeMarkers.push(this._createMarker(t[e], this.options.resizeIcon)), + (this._resizeMarkers[e]._cornerIndex = e); + }, + _onMarkerDragStart: function (t) { + L.Edit.SimpleShape.prototype._onMarkerDragStart.call(this, t); + var e = this._getCorners(), + i = t.target, + o = i._cornerIndex; + (this._oppositeCorner = e[(o + 2) % 4]), this._toggleCornerMarkers(0, o); + }, + _onMarkerDragEnd: function (t) { + var e, + i, + o = t.target; + o === this._moveMarker && + ((e = this._shape.getBounds()), (i = e.getCenter()), o.setLatLng(i)), + this._toggleCornerMarkers(1), + this._repositionCornerMarkers(), + L.Edit.SimpleShape.prototype._onMarkerDragEnd.call(this, t); + }, + _move: function (t) { + for ( + var e, + i = this._shape.getLatLngs(), + o = this._shape.getBounds(), + a = o.getCenter(), + s = [], + r = 0, + n = i.length; + n > r; + r++ + ) + (e = [i[r].lat - a.lat, i[r].lng - a.lng]), s.push([t.lat + e[0], t.lng + e[1]]); + this._shape.setLatLngs(s), this._repositionCornerMarkers(); + }, + _resize: function (t) { + var e; + this._shape.setBounds(L.latLngBounds(t, this._oppositeCorner)), + (e = this._shape.getBounds()), + this._moveMarker.setLatLng(e.getCenter()); + }, + _getCorners: function () { + var t = this._shape.getBounds(), + e = t.getNorthWest(), + i = t.getNorthEast(), + o = t.getSouthEast(), + a = t.getSouthWest(); + return [e, i, o, a]; + }, + _toggleCornerMarkers: function (t) { + for (var e = 0, i = this._resizeMarkers.length; i > e; e++) + this._resizeMarkers[e].setOpacity(t); + }, + _repositionCornerMarkers: function () { + for (var t = this._getCorners(), e = 0, i = this._resizeMarkers.length; i > e; e++) + this._resizeMarkers[e].setLatLng(t[e]); + }, + })), + L.Rectangle.addInitHook(function () { + L.Edit.Rectangle && + ((this.editing = new L.Edit.Rectangle(this)), this.options.editable && this.editing.enable()); + }), + (L.Edit = L.Edit || {}), + (L.Edit.Circle = L.Edit.SimpleShape.extend({ + _createMoveMarker: function () { + var t = this._shape.getLatLng(); + this._moveMarker = this._createMarker(t, this.options.moveIcon); + }, + _createResizeMarker: function () { + var t = this._shape.getLatLng(), + e = this._getResizeMarkerPoint(t); + (this._resizeMarkers = []), + this._resizeMarkers.push(this._createMarker(e, this.options.resizeIcon)); + }, + _getResizeMarkerPoint: function (t) { + var e = this._shape._radius * Math.cos(Math.PI / 4), + i = this._map.project(t); + return this._map.unproject([i.x + e, i.y - e]); + }, + _move: function (t) { + var e = this._getResizeMarkerPoint(t); + this._resizeMarkers[0].setLatLng(e), this._shape.setLatLng(t); + }, + _resize: function (t) { + var e = this._moveMarker.getLatLng(), + i = e.distanceTo(t); + this._shape.setRadius(i); + }, + })), + L.Circle.addInitHook(function () { + L.Edit.Circle && + ((this.editing = new L.Edit.Circle(this)), this.options.editable && this.editing.enable()), + this.on("add", function () { + this.editing && this.editing.enabled() && this.editing.addHooks(); + }), + this.on("remove", function () { + this.editing && this.editing.enabled() && this.editing.removeHooks(); + }); + }), + (L.LatLngUtil = { + cloneLatLngs: function (t) { + for (var e = [], i = 0, o = t.length; o > i; i++) e.push(this.cloneLatLng(t[i])); + return e; + }, + cloneLatLng: function (t) { + return L.latLng(t.lat, t.lng); + }, + }), + (L.GeometryUtil = L.extend(L.GeometryUtil || {}, { + geodesicArea: function (t) { + var e, + i, + o = t.length, + a = 0, + s = L.LatLng.DEG_TO_RAD; + if (o > 2) { + for (var r = 0; o > r; r++) + (e = t[r]), + (i = t[(r + 1) % o]), + (a += (i.lng - e.lng) * s * (2 + Math.sin(e.lat * s) + Math.sin(i.lat * s))); + a = (6378137 * a * 6378137) / 2; + } + return Math.abs(a); + }, + readableArea: function (t, e) { + var i; + return ( + e + ? (i = t >= 1e4 ? (1e-4 * t).toFixed(2) + " ha" : t.toFixed(2) + " m²") + : ((t *= 0.836127), + (i = + t >= 3097600 + ? (t / 3097600).toFixed(2) + " mi²" + : t >= 4840 + ? (t / 4840).toFixed(2) + " acres" + : Math.ceil(t) + " yd²")), + i + ); + }, + readableDistance: function (t, e) { + var i; + return ( + e + ? (i = t > 1e3 ? (t / 1e3).toFixed(2) + " km" : Math.ceil(t) + " m") + : ((t *= 1.09361), + (i = t > 1760 ? (t / 1760).toFixed(2) + " miles" : Math.ceil(t) + " yd")), + i + ); + }, + })), + L.Util.extend(L.LineUtil, { + segmentsIntersect: function (t, e, i, o) { + return ( + this._checkCounterclockwise(t, i, o) !== this._checkCounterclockwise(e, i, o) && + this._checkCounterclockwise(t, e, i) !== this._checkCounterclockwise(t, e, o) + ); + }, + _checkCounterclockwise: function (t, e, i) { + return (i.y - t.y) * (e.x - t.x) > (e.y - t.y) * (i.x - t.x); + }, + }), + L.Polyline.include({ + intersects: function () { + var t, + e, + i, + o = this._originalPoints, + a = o ? o.length : 0; + if (this._tooFewPointsForIntersection()) return !1; + for (t = a - 1; t >= 3; t--) + if (((e = o[t - 1]), (i = o[t]), this._lineSegmentsIntersectsRange(e, i, t - 2))) + return !0; + return !1; + }, + newLatLngIntersects: function (t, e) { + return this._map ? this.newPointIntersects(this._map.latLngToLayerPoint(t), e) : !1; + }, + newPointIntersects: function (t, e) { + var i = this._originalPoints, + o = i ? i.length : 0, + a = i ? i[o - 1] : null, + s = o - 2; + return this._tooFewPointsForIntersection(1) + ? !1 + : this._lineSegmentsIntersectsRange(a, t, s, e ? 1 : 0); + }, + _tooFewPointsForIntersection: function (t) { + var e = this._originalPoints, + i = e ? e.length : 0; + return (i += t || 0), !this._originalPoints || 3 >= i; + }, + _lineSegmentsIntersectsRange: function (t, e, i, o) { + var a, + s, + r = this._originalPoints; + o = o || 0; + for (var n = i; n > o; n--) + if (((a = r[n - 1]), (s = r[n]), L.LineUtil.segmentsIntersect(t, e, a, s))) return !0; + return !1; + }, + }), + L.Polygon.include({ + intersects: function () { + var t, + e, + i, + o, + a, + s = this._originalPoints; + return this._tooFewPointsForIntersection() + ? !1 + : (t = L.Polyline.prototype.intersects.call(this)) + ? !0 + : ((e = s.length), + (i = s[0]), + (o = s[e - 1]), + (a = e - 2), + this._lineSegmentsIntersectsRange(o, i, a, 1)); + }, + }), + (L.Control.Draw = L.Control.extend({ + options: { position: "topleft", draw: {}, edit: !1 }, + initialize: function (t) { + if (L.version < "0.7") + throw new Error( + "Leaflet.draw 0.2.3+ requires Leaflet 0.7.0+. Download latest from https://github.com/Leaflet/Leaflet/" + ); + L.Control.prototype.initialize.call(this, t); + var e, i; + (this._toolbars = {}), + L.DrawToolbar && + this.options.draw && + ((i = new L.DrawToolbar(this.options.draw)), + (e = L.stamp(i)), + (this._toolbars[e] = i), + this._toolbars[e].on("enable", this._toolbarEnabled, this)), + L.EditToolbar && + this.options.edit && + ((i = new L.EditToolbar(this.options.edit)), + (e = L.stamp(i)), + (this._toolbars[e] = i), + this._toolbars[e].on("enable", this._toolbarEnabled, this)); + }, + onAdd: function (t) { + var e, + i = L.DomUtil.create("div", "leaflet-draw"), + o = !1, + a = "leaflet-draw-toolbar-top"; + for (var s in this._toolbars) + this._toolbars.hasOwnProperty(s) && + ((e = this._toolbars[s].addToolbar(t)), + e && + (o || + (L.DomUtil.hasClass(e, a) || L.DomUtil.addClass(e.childNodes[0], a), + (o = !0)), + i.appendChild(e))); + return i; + }, + onRemove: function () { + for (var t in this._toolbars) + this._toolbars.hasOwnProperty(t) && this._toolbars[t].removeToolbar(); + }, + setDrawingOptions: function (t) { + for (var e in this._toolbars) + this._toolbars[e] instanceof L.DrawToolbar && this._toolbars[e].setOptions(t); + }, + _toolbarEnabled: function (t) { + var e = "" + L.stamp(t.target); + for (var i in this._toolbars) + this._toolbars.hasOwnProperty(i) && i !== e && this._toolbars[i].disable(); + }, + })), + L.Map.mergeOptions({ drawControlTooltips: !0, drawControl: !1 }), + L.Map.addInitHook(function () { + this.options.drawControl && + ((this.drawControl = new L.Control.Draw()), this.addControl(this.drawControl)); + }), + (L.Toolbar = L.Class.extend({ + includes: [L.Mixin.Events], + initialize: function (t) { + L.setOptions(this, t), + (this._modes = {}), + (this._actionButtons = []), + (this._activeMode = null); + }, + enabled: function () { + return null !== this._activeMode; + }, + disable: function () { + this.enabled() && this._activeMode.handler.disable(); + }, + addToolbar: function (t) { + var e, + i = L.DomUtil.create("div", "leaflet-draw-section"), + o = 0, + a = this._toolbarClass || "", + s = this.getModeHandlers(t); + for ( + this._toolbarContainer = L.DomUtil.create("div", "leaflet-draw-toolbar leaflet-bar"), + this._map = t, + e = 0; + e < s.length; + e++ + ) + s[e].enabled && + this._initModeHandler(s[e].handler, this._toolbarContainer, o++, a, s[e].title); + return o + ? ((this._lastButtonIndex = --o), + (this._actionsContainer = L.DomUtil.create("ul", "leaflet-draw-actions")), + i.appendChild(this._toolbarContainer), + i.appendChild(this._actionsContainer), + i) + : void 0; + }, + removeToolbar: function () { + for (var t in this._modes) + this._modes.hasOwnProperty(t) && + (this._disposeButton( + this._modes[t].button, + this._modes[t].handler.enable, + this._modes[t].handler + ), + this._modes[t].handler.disable(), + this._modes[t].handler + .off("enabled", this._handlerActivated, this) + .off("disabled", this._handlerDeactivated, this)); + this._modes = {}; + for (var e = 0, i = this._actionButtons.length; i > e; e++) + this._disposeButton(this._actionButtons[e].button, this._actionButtons[e].callback, this); + (this._actionButtons = []), (this._actionsContainer = null); + }, + _initModeHandler: function (t, e, i, o, a) { + var s = t.type; + (this._modes[s] = {}), + (this._modes[s].handler = t), + (this._modes[s].button = this._createButton({ + title: a, + className: o + "-" + s, + container: e, + callback: this._modes[s].handler.enable, + context: this._modes[s].handler, + })), + (this._modes[s].buttonIndex = i), + this._modes[s].handler + .on("enabled", this._handlerActivated, this) + .on("disabled", this._handlerDeactivated, this); + }, + _createButton: function (t) { + var e = L.DomUtil.create("a", t.className || "", t.container); + return ( + (e.href = "#"), + t.text && (e.innerHTML = t.text), + t.title && (e.title = t.title), + L.DomEvent.on(e, "click", L.DomEvent.stopPropagation) + .on(e, "mousedown", L.DomEvent.stopPropagation) + .on(e, "dblclick", L.DomEvent.stopPropagation) + .on(e, "click", L.DomEvent.preventDefault) + .on(e, "click", t.callback, t.context), + e + ); + }, + _disposeButton: function (t, e) { + L.DomEvent.off(t, "click", L.DomEvent.stopPropagation) + .off(t, "mousedown", L.DomEvent.stopPropagation) + .off(t, "dblclick", L.DomEvent.stopPropagation) + .off(t, "click", L.DomEvent.preventDefault) + .off(t, "click", e); + }, + _handlerActivated: function (t) { + this.disable(), + (this._activeMode = this._modes[t.handler]), + L.DomUtil.addClass(this._activeMode.button, "leaflet-draw-toolbar-button-enabled"), + this._showActionsToolbar(), + this.fire("enable"); + }, + _handlerDeactivated: function () { + this._hideActionsToolbar(), + L.DomUtil.removeClass(this._activeMode.button, "leaflet-draw-toolbar-button-enabled"), + (this._activeMode = null), + this.fire("disable"); + }, + _createActions: function (t) { + var e, + i, + o, + a, + s = this._actionsContainer, + r = this.getActions(t), + n = r.length; + for (i = 0, o = this._actionButtons.length; o > i; i++) + this._disposeButton(this._actionButtons[i].button, this._actionButtons[i].callback); + for (this._actionButtons = []; s.firstChild; ) s.removeChild(s.firstChild); + for (var l = 0; n > l; l++) + ("enabled" in r[l] && !r[l].enabled) || + ((e = L.DomUtil.create("li", "", s)), + (a = this._createButton({ + title: r[l].title, + text: r[l].text, + container: e, + callback: r[l].callback, + context: r[l].context, + })), + this._actionButtons.push({ button: a, callback: r[l].callback })); + }, + _showActionsToolbar: function () { + var t = this._activeMode.buttonIndex, + e = this._lastButtonIndex, + i = this._activeMode.button.offsetTop - 1; + this._createActions(this._activeMode.handler), + (this._actionsContainer.style.top = i + "px"), + 0 === t && + (L.DomUtil.addClass(this._toolbarContainer, "leaflet-draw-toolbar-notop"), + L.DomUtil.addClass(this._actionsContainer, "leaflet-draw-actions-top")), + t === e && + (L.DomUtil.addClass(this._toolbarContainer, "leaflet-draw-toolbar-nobottom"), + L.DomUtil.addClass(this._actionsContainer, "leaflet-draw-actions-bottom")), + (this._actionsContainer.style.display = "block"); + }, + _hideActionsToolbar: function () { + (this._actionsContainer.style.display = "none"), + L.DomUtil.removeClass(this._toolbarContainer, "leaflet-draw-toolbar-notop"), + L.DomUtil.removeClass(this._toolbarContainer, "leaflet-draw-toolbar-nobottom"), + L.DomUtil.removeClass(this._actionsContainer, "leaflet-draw-actions-top"), + L.DomUtil.removeClass(this._actionsContainer, "leaflet-draw-actions-bottom"); + }, + })), + (L.Tooltip = L.Class.extend({ + initialize: function (t) { + (this._map = t), + (this._popupPane = t._panes.popupPane), + (this._container = t.options.drawControlTooltips + ? L.DomUtil.create("div", "leaflet-draw-tooltip", this._popupPane) + : null), + (this._singleLineLabel = !1); + }, + dispose: function () { + this._container && (this._popupPane.removeChild(this._container), (this._container = null)); + }, + updateContent: function (t) { + return this._container + ? ((t.subtext = t.subtext || ""), + 0 !== t.subtext.length || this._singleLineLabel + ? t.subtext.length > 0 && + this._singleLineLabel && + (L.DomUtil.removeClass(this._container, "leaflet-draw-tooltip-single"), + (this._singleLineLabel = !1)) + : (L.DomUtil.addClass(this._container, "leaflet-draw-tooltip-single"), + (this._singleLineLabel = !0)), + (this._container.innerHTML = + (t.subtext.length > 0 + ? '' + t.subtext + "
              " + : "") + + "" + + t.text + + ""), + this) + : this; + }, + updatePosition: function (t) { + var e = this._map.latLngToLayerPoint(t), + i = this._container; + return ( + this._container && ((i.style.visibility = "inherit"), L.DomUtil.setPosition(i, e)), this + ); + }, + showAsError: function () { + return ( + this._container && L.DomUtil.addClass(this._container, "leaflet-error-draw-tooltip"), this + ); + }, + removeError: function () { + return ( + this._container && L.DomUtil.removeClass(this._container, "leaflet-error-draw-tooltip"), + this + ); + }, + })), + (L.DrawToolbar = L.Toolbar.extend({ + options: { polyline: {}, polygon: {}, rectangle: {}, circle: {}, marker: {} }, + initialize: function (t) { + for (var e in this.options) + this.options.hasOwnProperty(e) && t[e] && (t[e] = L.extend({}, this.options[e], t[e])); + (this._toolbarClass = "leaflet-draw-draw"), L.Toolbar.prototype.initialize.call(this, t); + }, + getModeHandlers: function (t) { + return [ + { + enabled: this.options.polyline, + handler: new L.Draw.Polyline(t, this.options.polyline), + title: L.drawLocal.draw.toolbar.buttons.polyline, + }, + { + enabled: this.options.polygon, + handler: new L.Draw.Polygon(t, this.options.polygon), + title: L.drawLocal.draw.toolbar.buttons.polygon, + }, + { + enabled: this.options.rectangle, + handler: new L.Draw.Rectangle(t, this.options.rectangle), + title: L.drawLocal.draw.toolbar.buttons.rectangle, + }, + { + enabled: this.options.circle, + handler: new L.Draw.Circle(t, this.options.circle), + title: L.drawLocal.draw.toolbar.buttons.circle, + }, + { + enabled: this.options.marker, + handler: new L.Draw.Marker(t, this.options.marker), + title: L.drawLocal.draw.toolbar.buttons.marker, + }, + ]; + }, + getActions: function (t) { + return [ + { + enabled: t.deleteLastVertex, + title: L.drawLocal.draw.toolbar.undo.title, + text: L.drawLocal.draw.toolbar.undo.text, + callback: t.deleteLastVertex, + context: t, + }, + { + title: L.drawLocal.draw.toolbar.actions.title, + text: L.drawLocal.draw.toolbar.actions.text, + callback: this.disable, + context: this, + }, + ]; + }, + setOptions: function (t) { + L.setOptions(this, t); + for (var e in this._modes) + this._modes.hasOwnProperty(e) && + t.hasOwnProperty(e) && + this._modes[e].handler.setOptions(t[e]); + }, + })), + (L.EditToolbar = L.Toolbar.extend({ + options: { + edit: { + selectedPathOptions: { + color: "#fe57a1", + opacity: 0.6, + dashArray: "10, 10", + fill: !0, + fillColor: "#fe57a1", + fillOpacity: 0.1, + }, + }, + remove: {}, + featureGroup: null, + }, + initialize: function (t) { + t.edit && + ("undefined" == typeof t.edit.selectedPathOptions && + (t.edit.selectedPathOptions = this.options.edit.selectedPathOptions), + (t.edit = L.extend({}, this.options.edit, t.edit))), + t.remove && (t.remove = L.extend({}, this.options.remove, t.remove)), + (this._toolbarClass = "leaflet-draw-edit"), + L.Toolbar.prototype.initialize.call(this, t), + (this._selectedFeatureCount = 0); + }, + getModeHandlers: function (t) { + var e = this.options.featureGroup; + return [ + { + enabled: this.options.edit, + handler: new L.EditToolbar.Edit(t, { + featureGroup: e, + selectedPathOptions: this.options.edit.selectedPathOptions, + }), + title: L.drawLocal.edit.toolbar.buttons.edit, + }, + { + enabled: this.options.remove, + handler: new L.EditToolbar.Delete(t, { featureGroup: e }), + title: L.drawLocal.edit.toolbar.buttons.remove, + }, + ]; + }, + getActions: function () { + return [ + { + title: L.drawLocal.edit.toolbar.actions.save.title, + text: L.drawLocal.edit.toolbar.actions.save.text, + callback: this._save, + context: this, + }, + { + title: L.drawLocal.edit.toolbar.actions.cancel.title, + text: L.drawLocal.edit.toolbar.actions.cancel.text, + callback: this.disable, + context: this, + }, + ]; + }, + addToolbar: function (t) { + var e = L.Toolbar.prototype.addToolbar.call(this, t); + return ( + this._checkDisabled(), + this.options.featureGroup.on("layeradd layerremove", this._checkDisabled, this), + e + ); + }, + removeToolbar: function () { + this.options.featureGroup.off("layeradd layerremove", this._checkDisabled, this), + L.Toolbar.prototype.removeToolbar.call(this); + }, + disable: function () { + this.enabled() && + (this._activeMode.handler.revertLayers(), L.Toolbar.prototype.disable.call(this)); + }, + _save: function () { + this._activeMode.handler.save(), this._activeMode.handler.disable(); + }, + _checkDisabled: function () { + var t, + e = this.options.featureGroup, + i = 0 !== e.getLayers().length; + this.options.edit && + ((t = this._modes[L.EditToolbar.Edit.TYPE].button), + i + ? L.DomUtil.removeClass(t, "leaflet-disabled") + : L.DomUtil.addClass(t, "leaflet-disabled"), + t.setAttribute( + "title", + i + ? L.drawLocal.edit.toolbar.buttons.edit + : L.drawLocal.edit.toolbar.buttons.editDisabled + )), + this.options.remove && + ((t = this._modes[L.EditToolbar.Delete.TYPE].button), + i + ? L.DomUtil.removeClass(t, "leaflet-disabled") + : L.DomUtil.addClass(t, "leaflet-disabled"), + t.setAttribute( + "title", + i + ? L.drawLocal.edit.toolbar.buttons.remove + : L.drawLocal.edit.toolbar.buttons.removeDisabled + )); + }, + })), + (L.EditToolbar.Edit = L.Handler.extend({ + statics: { TYPE: "edit" }, + includes: L.Mixin.Events, + initialize: function (t, e) { + if ( + (L.Handler.prototype.initialize.call(this, t), + (this._selectedPathOptions = e.selectedPathOptions), + (this._featureGroup = e.featureGroup), + !(this._featureGroup instanceof L.FeatureGroup)) + ) + throw new Error("options.featureGroup must be a L.FeatureGroup"); + (this._uneditedLayerProps = {}), (this.type = L.EditToolbar.Edit.TYPE); + }, + enable: function () { + !this._enabled && + this._hasAvailableLayers() && + (this.fire("enabled", { handler: this.type }), + this._map.fire("draw:editstart", { handler: this.type }), + L.Handler.prototype.enable.call(this), + this._featureGroup + .on("layeradd", this._enableLayerEdit, this) + .on("layerremove", this._disableLayerEdit, this)); + }, + disable: function () { + this._enabled && + (this._featureGroup + .off("layeradd", this._enableLayerEdit, this) + .off("layerremove", this._disableLayerEdit, this), + L.Handler.prototype.disable.call(this), + this._map.fire("draw:editstop", { handler: this.type }), + this.fire("disabled", { handler: this.type })); + }, + addHooks: function () { + var t = this._map; + t && + (t.getContainer().focus(), + this._featureGroup.eachLayer(this._enableLayerEdit, this), + (this._tooltip = new L.Tooltip(this._map)), + this._tooltip.updateContent({ + text: L.drawLocal.edit.handlers.edit.tooltip.text, + subtext: L.drawLocal.edit.handlers.edit.tooltip.subtext, + }), + this._map.on("mousemove", this._onMouseMove, this)); + }, + removeHooks: function () { + this._map && + (this._featureGroup.eachLayer(this._disableLayerEdit, this), + (this._uneditedLayerProps = {}), + this._tooltip.dispose(), + (this._tooltip = null), + this._map.off("mousemove", this._onMouseMove, this)); + }, + revertLayers: function () { + this._featureGroup.eachLayer(function (t) { + this._revertLayer(t); + }, this); + }, + save: function () { + var t = new L.LayerGroup(); + this._featureGroup.eachLayer(function (e) { + e.edited && (t.addLayer(e), (e.edited = !1)); + }), + this._map.fire("draw:edited", { layers: t }); + }, + _backupLayer: function (t) { + var e = L.Util.stamp(t); + this._uneditedLayerProps[e] || + (t instanceof L.Polyline || t instanceof L.Polygon || t instanceof L.Rectangle + ? (this._uneditedLayerProps[e] = { + latlngs: L.LatLngUtil.cloneLatLngs(t.getLatLngs()), + }) + : t instanceof L.Circle + ? (this._uneditedLayerProps[e] = { + latlng: L.LatLngUtil.cloneLatLng(t.getLatLng()), + radius: t.getRadius(), + }) + : t instanceof L.Marker && + (this._uneditedLayerProps[e] = { + latlng: L.LatLngUtil.cloneLatLng(t.getLatLng()), + })); + }, + _revertLayer: function (t) { + var e = L.Util.stamp(t); + (t.edited = !1), + this._uneditedLayerProps.hasOwnProperty(e) && + (t instanceof L.Polyline || t instanceof L.Polygon || t instanceof L.Rectangle + ? t.setLatLngs(this._uneditedLayerProps[e].latlngs) + : t instanceof L.Circle + ? (t.setLatLng(this._uneditedLayerProps[e].latlng), + t.setRadius(this._uneditedLayerProps[e].radius)) + : t instanceof L.Marker && t.setLatLng(this._uneditedLayerProps[e].latlng)); + }, + _toggleMarkerHighlight: function (t) { + if (t._icon) { + var e = t._icon; + (e.style.display = "none"), + L.DomUtil.hasClass(e, "leaflet-edit-marker-selected") + ? (L.DomUtil.removeClass(e, "leaflet-edit-marker-selected"), + this._offsetMarker(e, -4)) + : (L.DomUtil.addClass(e, "leaflet-edit-marker-selected"), + this._offsetMarker(e, 4)), + (e.style.display = ""); + } + }, + _offsetMarker: function (t, e) { + var i = parseInt(t.style.marginTop, 10) - e, + o = parseInt(t.style.marginLeft, 10) - e; + (t.style.marginTop = i + "px"), (t.style.marginLeft = o + "px"); + }, + _enableLayerEdit: function (t) { + var e, + i = t.layer || t.target || t, + o = i instanceof L.Marker; + (!o || i._icon) && + (this._backupLayer(i), + this._selectedPathOptions && + ((e = L.Util.extend({}, this._selectedPathOptions)), + o + ? this._toggleMarkerHighlight(i) + : ((i.options.previousOptions = L.Util.extend({ dashArray: null }, i.options)), + i instanceof L.Circle || + i instanceof L.Polygon || + i instanceof L.Rectangle || + (e.fill = !1), + i.setStyle(e))), + o ? (i.dragging.enable(), i.on("dragend", this._onMarkerDragEnd)) : i.editing.enable()); + }, + _disableLayerEdit: function (t) { + var e = t.layer || t.target || t; + (e.edited = !1), + this._selectedPathOptions && + (e instanceof L.Marker + ? this._toggleMarkerHighlight(e) + : (e.setStyle(e.options.previousOptions), delete e.options.previousOptions)), + e instanceof L.Marker + ? (e.dragging.disable(), e.off("dragend", this._onMarkerDragEnd, this)) + : e.editing.disable(); + }, + _onMarkerDragEnd: function (t) { + var e = t.target; + e.edited = !0; + }, + _onMouseMove: function (t) { + this._tooltip.updatePosition(t.latlng); + }, + _hasAvailableLayers: function () { + return 0 !== this._featureGroup.getLayers().length; + }, + })), + (L.EditToolbar.Delete = L.Handler.extend({ + statics: { TYPE: "remove" }, + includes: L.Mixin.Events, + initialize: function (t, e) { + if ( + (L.Handler.prototype.initialize.call(this, t), + L.Util.setOptions(this, e), + (this._deletableLayers = this.options.featureGroup), + !(this._deletableLayers instanceof L.FeatureGroup)) + ) + throw new Error("options.featureGroup must be a L.FeatureGroup"); + this.type = L.EditToolbar.Delete.TYPE; + }, + enable: function () { + !this._enabled && + this._hasAvailableLayers() && + (this.fire("enabled", { handler: this.type }), + this._map.fire("draw:deletestart", { handler: this.type }), + L.Handler.prototype.enable.call(this), + this._deletableLayers + .on("layeradd", this._enableLayerDelete, this) + .on("layerremove", this._disableLayerDelete, this)); + }, + disable: function () { + this._enabled && + (this._deletableLayers + .off("layeradd", this._enableLayerDelete, this) + .off("layerremove", this._disableLayerDelete, this), + L.Handler.prototype.disable.call(this), + this._map.fire("draw:deletestop", { handler: this.type }), + this.fire("disabled", { handler: this.type })); + }, + addHooks: function () { + var t = this._map; + t && + (t.getContainer().focus(), + this._deletableLayers.eachLayer(this._enableLayerDelete, this), + (this._deletedLayers = new L.layerGroup()), + (this._tooltip = new L.Tooltip(this._map)), + this._tooltip.updateContent({ text: L.drawLocal.edit.handlers.remove.tooltip.text }), + this._map.on("mousemove", this._onMouseMove, this)); + }, + removeHooks: function () { + this._map && + (this._deletableLayers.eachLayer(this._disableLayerDelete, this), + (this._deletedLayers = null), + this._tooltip.dispose(), + (this._tooltip = null), + this._map.off("mousemove", this._onMouseMove, this)); + }, + revertLayers: function () { + this._deletedLayers.eachLayer(function (t) { + this._deletableLayers.addLayer(t); + }, this); + }, + save: function () { + this._map.fire("draw:deleted", { layers: this._deletedLayers }); + }, + _enableLayerDelete: function (t) { + var e = t.layer || t.target || t; + e.on("click", this._removeLayer, this); + }, + _disableLayerDelete: function (t) { + var e = t.layer || t.target || t; + e.off("click", this._removeLayer, this), this._deletedLayers.removeLayer(e); + }, + _removeLayer: function (t) { + var e = t.layer || t.target || t; + this._deletableLayers.removeLayer(e), this._deletedLayers.addLayer(e); + }, + _onMouseMove: function (t) { + this._tooltip.updatePosition(t.latlng); + }, + _hasAvailableLayers: function () { + return 0 !== this._deletableLayers.getLayers().length; + }, + })); +})(window, document); diff --git a/erpnext/public/js/leaflet/leaflet.js b/erpnext/public/js/leaflet/leaflet.js index 91dd3d434c7..cef823a2e1a 100755 --- a/erpnext/public/js/leaflet/leaflet.js +++ b/erpnext/public/js/leaflet/leaflet.js @@ -2,770 +2,5633 @@ Leaflet 1.0.0-beta.2 (dd0faa1), a JS library for interactive maps. http://leafletjs.com (c) 2010-2015 Vladimir Agafonkin, (c) 2010-2011 CloudMade */ -! function(t, e, i) { - function n() { var e = t.L; - o.noConflict = function() { return t.L = e, this }, t.L = o } - var o = { version: "1.0.0-beta.2" }; - "object" == typeof module && "object" == typeof module.exports ? module.exports = o : "function" == typeof define && define.amd && define(o), "undefined" != typeof t && n(), o.Util = { extend: function(t) { var e, i, n, o; for (i = 1, n = arguments.length; n > i; i++) { o = arguments[i]; for (e in o) t[e] = o[e] } return t }, create: Object.create || function() { - function t() {} return function(e) { return t.prototype = e, new t } }(), bind: function(t, e) { var i = Array.prototype.slice; if (t.bind) return t.bind.apply(t, i.call(arguments, 1)); var n = i.call(arguments, 2); return function() { return t.apply(e, n.length ? n.concat(i.call(arguments)) : arguments) } }, stamp: function(t) { return t._leaflet_id = t._leaflet_id || ++o.Util.lastId, t._leaflet_id }, lastId: 0, throttle: function(t, e, i) { var n, o, s, r; return r = function() { n = !1, o && (s.apply(i, o), o = !1) }, s = function() { n ? o = arguments : (t.apply(i, arguments), setTimeout(r, e), n = !0) } }, wrapNum: function(t, e, i) { var n = e[1], - o = e[0], - s = n - o; return t === n && i ? t : ((t - o) % s + s) % s + o }, falseFn: function() { return !1 }, formatNum: function(t, e) { var i = Math.pow(10, e || 5); return Math.round(t * i) / i }, trim: function(t) { return t.trim ? t.trim() : t.replace(/^\s+|\s+$/g, "") }, splitWords: function(t) { return o.Util.trim(t).split(/\s+/) }, setOptions: function(t, e) { t.hasOwnProperty("options") || (t.options = t.options ? o.Util.create(t.options) : {}); for (var i in e) t.options[i] = e[i]; return t.options }, getParamString: function(t, e, i) { var n = []; for (var o in t) n.push(encodeURIComponent(i ? o.toUpperCase() : o) + "=" + encodeURIComponent(t[o])); return (e && -1 !== e.indexOf("?") ? "&" : "?") + n.join("&") }, template: function(t, e) { return t.replace(o.Util.templateRe, function(t, n) { var o = e[n]; if (o === i) throw new Error("No value provided for variable " + t); return "function" == typeof o && (o = o(e)), o }) }, templateRe: /\{ *([\w_]+) *\}/g, isArray: Array.isArray || function(t) { return "[object Array]" === Object.prototype.toString.call(t) }, indexOf: function(t, e) { for (var i = 0; i < t.length; i++) - if (t[i] === e) return i; - return -1 }, emptyImageUrl: "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" }, - function() { - function e(e) { return t["webkit" + e] || t["moz" + e] || t["ms" + e] } +!(function (t, e, i) { + function n() { + var e = t.L; + (o.noConflict = function () { + return (t.L = e), this; + }), + (t.L = o); + } + var o = { version: "1.0.0-beta.2" }; + "object" == typeof module && "object" == typeof module.exports + ? (module.exports = o) + : "function" == typeof define && define.amd && define(o), + "undefined" != typeof t && n(), + (o.Util = { + extend: function (t) { + var e, i, n, o; + for (i = 1, n = arguments.length; n > i; i++) { + o = arguments[i]; + for (e in o) t[e] = o[e]; + } + return t; + }, + create: + Object.create || + (function () { + function t() {} + return function (e) { + return (t.prototype = e), new t(); + }; + })(), + bind: function (t, e) { + var i = Array.prototype.slice; + if (t.bind) return t.bind.apply(t, i.call(arguments, 1)); + var n = i.call(arguments, 2); + return function () { + return t.apply(e, n.length ? n.concat(i.call(arguments)) : arguments); + }; + }, + stamp: function (t) { + return (t._leaflet_id = t._leaflet_id || ++o.Util.lastId), t._leaflet_id; + }, + lastId: 0, + throttle: function (t, e, i) { + var n, o, s, r; + return ( + (r = function () { + (n = !1), o && (s.apply(i, o), (o = !1)); + }), + (s = function () { + n ? (o = arguments) : (t.apply(i, arguments), setTimeout(r, e), (n = !0)); + }) + ); + }, + wrapNum: function (t, e, i) { + var n = e[1], + o = e[0], + s = n - o; + return t === n && i ? t : ((((t - o) % s) + s) % s) + o; + }, + falseFn: function () { + return !1; + }, + formatNum: function (t, e) { + var i = Math.pow(10, e || 5); + return Math.round(t * i) / i; + }, + trim: function (t) { + return t.trim ? t.trim() : t.replace(/^\s+|\s+$/g, ""); + }, + splitWords: function (t) { + return o.Util.trim(t).split(/\s+/); + }, + setOptions: function (t, e) { + t.hasOwnProperty("options") || (t.options = t.options ? o.Util.create(t.options) : {}); + for (var i in e) t.options[i] = e[i]; + return t.options; + }, + getParamString: function (t, e, i) { + var n = []; + for (var o in t) + n.push(encodeURIComponent(i ? o.toUpperCase() : o) + "=" + encodeURIComponent(t[o])); + return (e && -1 !== e.indexOf("?") ? "&" : "?") + n.join("&"); + }, + template: function (t, e) { + return t.replace(o.Util.templateRe, function (t, n) { + var o = e[n]; + if (o === i) throw new Error("No value provided for variable " + t); + return "function" == typeof o && (o = o(e)), o; + }); + }, + templateRe: /\{ *([\w_]+) *\}/g, + isArray: + Array.isArray || + function (t) { + return "[object Array]" === Object.prototype.toString.call(t); + }, + indexOf: function (t, e) { + for (var i = 0; i < t.length; i++) if (t[i] === e) return i; + return -1; + }, + emptyImageUrl: "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=", + }), + (function () { + function e(e) { + return t["webkit" + e] || t["moz" + e] || t["ms" + e]; + } - function i(e) { var i = +new Date, - o = Math.max(0, 16 - (i - n)); return n = i + o, t.setTimeout(e, o) } var n = 0, - s = t.requestAnimationFrame || e("RequestAnimationFrame") || i, - r = t.cancelAnimationFrame || e("CancelAnimationFrame") || e("CancelRequestAnimationFrame") || function(e) { t.clearTimeout(e) }; - o.Util.requestAnimFrame = function(e, n, r) { return r && s === i ? void e.call(n) : s.call(t, o.bind(e, n)) }, o.Util.cancelAnimFrame = function(e) { e && r.call(t, e) } }(), o.extend = o.Util.extend, o.bind = o.Util.bind, o.stamp = o.Util.stamp, o.setOptions = o.Util.setOptions, o.Class = function() {}, o.Class.extend = function(t) { var e = function() { this.initialize && this.initialize.apply(this, arguments), this.callInitHooks() }, - i = e.__super__ = this.prototype, - n = o.Util.create(i); - n.constructor = e, e.prototype = n; for (var s in this) this.hasOwnProperty(s) && "prototype" !== s && (e[s] = this[s]); return t.statics && (o.extend(e, t.statics), delete t.statics), t.includes && (o.Util.extend.apply(null, [n].concat(t.includes)), delete t.includes), n.options && (t.options = o.Util.extend(o.Util.create(n.options), t.options)), o.extend(n, t), n._initHooks = [], n.callInitHooks = function() { if (!this._initHooksCalled) { i.callInitHooks && i.callInitHooks.call(this), this._initHooksCalled = !0; for (var t = 0, e = n._initHooks.length; e > t; t++) n._initHooks[t].call(this) } }, e }, o.Class.include = function(t) { o.extend(this.prototype, t) }, o.Class.mergeOptions = function(t) { o.extend(this.prototype.options, t) }, o.Class.addInitHook = function(t) { var e = Array.prototype.slice.call(arguments, 1), - i = "function" == typeof t ? t : function() { this[t].apply(this, e) }; - this.prototype._initHooks = this.prototype._initHooks || [], this.prototype._initHooks.push(i) }, o.Evented = o.Class.extend({ on: function(t, e, i) { if ("object" == typeof t) - for (var n in t) this._on(n, t[n], e); - else { t = o.Util.splitWords(t); for (var s = 0, r = t.length; r > s; s++) this._on(t[s], e, i) } return this }, off: function(t, e, i) { if (t) - if ("object" == typeof t) - for (var n in t) this._off(n, t[n], e); - else { t = o.Util.splitWords(t); for (var s = 0, r = t.length; r > s; s++) this._off(t[s], e, i) } - else delete this._events; return this }, _on: function(t, e, i) { var n = this._events = this._events || {}, - s = i && i !== this && o.stamp(i); if (s) { var r = t + "_idx", - a = t + "_len", - h = n[r] = n[r] || {}, - l = o.stamp(e) + "_" + s; - h[l] || (h[l] = { fn: e, ctx: i }, n[a] = (n[a] || 0) + 1) } else n[t] = n[t] || [], n[t].push({ fn: e }) }, _off: function(t, e, i) { var n = this._events, - s = t + "_idx", - r = t + "_len"; if (n) { if (!e) return delete n[t], delete n[s], void delete n[r]; var a, h, l, u, c, d = i && i !== this && o.stamp(i); if (d) c = o.stamp(e) + "_" + d, a = n[s], a && a[c] && (u = a[c], delete a[c], n[r]--); - else if (a = n[t]) - for (h = 0, l = a.length; l > h; h++) - if (a[h].fn === e) { u = a[h], a.splice(h, 1); break } - u && (u.fn = o.Util.falseFn) } }, fire: function(t, e, i) { if (!this.listens(t, i)) return this; var n = o.Util.extend({}, e, { type: t, target: this }), - s = this._events; if (s) { var r, a, h, l, u = s[t + "_idx"]; if (s[t]) - for (h = s[t].slice(), r = 0, a = h.length; a > r; r++) h[r].fn.call(this, n); for (l in u) u[l].fn.call(u[l].ctx, n) } return i && this._propagateEvent(n), this }, listens: function(t, e) { var i = this._events; if (i && (i[t] || i[t + "_len"])) return !0; if (e) - for (var n in this._eventParents) - if (this._eventParents[n].listens(t, e)) return !0; - return !1 }, once: function(t, e, i) { if ("object" == typeof t) { for (var n in t) this.once(n, t[n], e); return this } var s = o.bind(function() { this.off(t, e, i).off(t, s, i) }, this); return this.on(t, e, i).on(t, s, i) }, addEventParent: function(t) { return this._eventParents = this._eventParents || {}, this._eventParents[o.stamp(t)] = t, this }, removeEventParent: function(t) { return this._eventParents && delete this._eventParents[o.stamp(t)], this }, _propagateEvent: function(t) { for (var e in this._eventParents) this._eventParents[e].fire(t.type, o.extend({ layer: t.target }, t), !0) } }); - var s = o.Evented.prototype; - s.addEventListener = s.on, s.removeEventListener = s.clearAllEventListeners = s.off, s.addOneTimeEventListener = s.once, s.fireEvent = s.fire, s.hasEventListeners = s.listens, o.Mixin = { Events: s }, - function() { var i = navigator.userAgent.toLowerCase(), - n = e.documentElement, - s = "ActiveXObject" in t, - r = -1 !== i.indexOf("webkit"), - a = -1 !== i.indexOf("phantom"), - h = -1 !== i.search("android [23]"), - l = -1 !== i.indexOf("chrome"), - u = -1 !== i.indexOf("gecko") && !r && !t.opera && !s, - c = "undefined" != typeof orientation || -1 !== i.indexOf("mobile"), - d = !t.PointerEvent && t.MSPointerEvent, - _ = t.PointerEvent && navigator.pointerEnabled || d, - m = s && "transition" in n.style, - p = "WebKitCSSMatrix" in t && "m11" in new t.WebKitCSSMatrix && !h, - f = "MozPerspective" in n.style, - g = "OTransition" in n.style, - v = !t.L_NO_TOUCH && !a && (_ || "ontouchstart" in t || t.DocumentTouch && e instanceof t.DocumentTouch); - o.Browser = { ie: s, ielt9: s && !e.addEventListener, webkit: r, gecko: u, android: -1 !== i.indexOf("android"), android23: h, chrome: l, safari: !l && -1 !== i.indexOf("safari"), ie3d: m, webkit3d: p, gecko3d: f, opera12: g, any3d: !t.L_DISABLE_3D && (m || p || f) && !g && !a, mobile: c, mobileWebkit: c && r, mobileWebkit3d: c && p, mobileOpera: c && t.opera, mobileGecko: c && u, touch: !!v, msPointer: !!d, pointer: !!_, retina: (t.devicePixelRatio || t.screen.deviceXDPI / t.screen.logicalXDPI) > 1 } }(), o.Point = function(t, e, i) { this.x = i ? Math.round(t) : t, this.y = i ? Math.round(e) : e }, o.Point.prototype = { clone: function() { return new o.Point(this.x, this.y) }, add: function(t) { return this.clone()._add(o.point(t)) }, _add: function(t) { return this.x += t.x, this.y += t.y, this }, subtract: function(t) { return this.clone()._subtract(o.point(t)) }, _subtract: function(t) { return this.x -= t.x, this.y -= t.y, this }, divideBy: function(t) { return this.clone()._divideBy(t) }, _divideBy: function(t) { return this.x /= t, this.y /= t, this }, multiplyBy: function(t) { return this.clone()._multiplyBy(t) }, _multiplyBy: function(t) { return this.x *= t, this.y *= t, this }, scaleBy: function(t) { return new o.Point(this.x * t.x, this.y * t.y) }, unscaleBy: function(t) { return new o.Point(this.x / t.x, this.y / t.y) }, round: function() { return this.clone()._round() }, _round: function() { return this.x = Math.round(this.x), this.y = Math.round(this.y), this }, floor: function() { return this.clone()._floor() }, _floor: function() { return this.x = Math.floor(this.x), this.y = Math.floor(this.y), this }, ceil: function() { return this.clone()._ceil() }, _ceil: function() { return this.x = Math.ceil(this.x), this.y = Math.ceil(this.y), this }, distanceTo: function(t) { t = o.point(t); var e = t.x - this.x, - i = t.y - this.y; return Math.sqrt(e * e + i * i) }, equals: function(t) { return t = o.point(t), t.x === this.x && t.y === this.y }, contains: function(t) { return t = o.point(t), Math.abs(t.x) <= Math.abs(this.x) && Math.abs(t.y) <= Math.abs(this.y) }, toString: function() { return "Point(" + o.Util.formatNum(this.x) + ", " + o.Util.formatNum(this.y) + ")" } }, o.point = function(t, e, n) { return t instanceof o.Point ? t : o.Util.isArray(t) ? new o.Point(t[0], t[1]) : t === i || null === t ? t : new o.Point(t, e, n) }, o.Bounds = function(t, e) { if (t) - for (var i = e ? [t, e] : t, n = 0, o = i.length; o > n; n++) this.extend(i[n]) }, o.Bounds.prototype = { extend: function(t) { return t = o.point(t), this.min || this.max ? (this.min.x = Math.min(t.x, this.min.x), this.max.x = Math.max(t.x, this.max.x), this.min.y = Math.min(t.y, this.min.y), this.max.y = Math.max(t.y, this.max.y)) : (this.min = t.clone(), this.max = t.clone()), this }, getCenter: function(t) { return new o.Point((this.min.x + this.max.x) / 2, (this.min.y + this.max.y) / 2, t) }, getBottomLeft: function() { return new o.Point(this.min.x, this.max.y) }, getTopRight: function() { return new o.Point(this.max.x, this.min.y) }, getSize: function() { return this.max.subtract(this.min) }, contains: function(t) { var e, i; return t = "number" == typeof t[0] || t instanceof o.Point ? o.point(t) : o.bounds(t), t instanceof o.Bounds ? (e = t.min, i = t.max) : e = i = t, e.x >= this.min.x && i.x <= this.max.x && e.y >= this.min.y && i.y <= this.max.y }, intersects: function(t) { t = o.bounds(t); var e = this.min, - i = this.max, - n = t.min, - s = t.max, - r = s.x >= e.x && n.x <= i.x, - a = s.y >= e.y && n.y <= i.y; return r && a }, overlaps: function(t) { t = o.bounds(t); var e = this.min, - i = this.max, - n = t.min, - s = t.max, - r = s.x > e.x && n.x < i.x, - a = s.y > e.y && n.y < i.y; return r && a }, isValid: function() { return !(!this.min || !this.max) } }, o.bounds = function(t, e) { return !t || t instanceof o.Bounds ? t : new o.Bounds(t, e) }, o.Transformation = function(t, e, i, n) { this._a = t, this._b = e, this._c = i, this._d = n }, o.Transformation.prototype = { transform: function(t, e) { return this._transform(t.clone(), e) }, _transform: function(t, e) { return e = e || 1, t.x = e * (this._a * t.x + this._b), t.y = e * (this._c * t.y + this._d), t }, untransform: function(t, e) { return e = e || 1, new o.Point((t.x / e - this._b) / this._a, (t.y / e - this._d) / this._c) } }, o.DomUtil = { get: function(t) { return "string" == typeof t ? e.getElementById(t) : t }, getStyle: function(t, i) { var n = t.style[i] || t.currentStyle && t.currentStyle[i]; if ((!n || "auto" === n) && e.defaultView) { var o = e.defaultView.getComputedStyle(t, null); - n = o ? o[i] : null } return "auto" === n ? null : n }, create: function(t, i, n) { var o = e.createElement(t); return o.className = i, n && n.appendChild(o), o }, remove: function(t) { var e = t.parentNode; - e && e.removeChild(t) }, empty: function(t) { for (; t.firstChild;) t.removeChild(t.firstChild) }, toFront: function(t) { t.parentNode.appendChild(t) }, toBack: function(t) { var e = t.parentNode; - e.insertBefore(t, e.firstChild) }, hasClass: function(t, e) { if (t.classList !== i) return t.classList.contains(e); var n = o.DomUtil.getClass(t); return n.length > 0 && new RegExp("(^|\\s)" + e + "(\\s|$)").test(n) }, addClass: function(t, e) { if (t.classList !== i) - for (var n = o.Util.splitWords(e), s = 0, r = n.length; r > s; s++) t.classList.add(n[s]); - else if (!o.DomUtil.hasClass(t, e)) { var a = o.DomUtil.getClass(t); - o.DomUtil.setClass(t, (a ? a + " " : "") + e) } }, removeClass: function(t, e) { t.classList !== i ? t.classList.remove(e) : o.DomUtil.setClass(t, o.Util.trim((" " + o.DomUtil.getClass(t) + " ").replace(" " + e + " ", " "))) }, setClass: function(t, e) { t.className.baseVal === i ? t.className = e : t.className.baseVal = e }, getClass: function(t) { return t.className.baseVal === i ? t.className : t.className.baseVal }, setOpacity: function(t, e) { "opacity" in t.style ? t.style.opacity = e : "filter" in t.style && o.DomUtil._setOpacityIE(t, e) }, _setOpacityIE: function(t, e) { var i = !1, - n = "DXImageTransform.Microsoft.Alpha"; try { i = t.filters.item(n) } catch (o) { if (1 === e) return } - e = Math.round(100 * e), i ? (i.Enabled = 100 !== e, i.Opacity = e) : t.style.filter += " progid:" + n + "(opacity=" + e + ")" }, testProp: function(t) { for (var i = e.documentElement.style, n = 0; n < t.length; n++) - if (t[n] in i) return t[n]; - return !1 }, setTransform: function(t, e, i) { var n = e || new o.Point(0, 0); - t.style[o.DomUtil.TRANSFORM] = (o.Browser.ie3d ? "translate(" + n.x + "px," + n.y + "px)" : "translate3d(" + n.x + "px," + n.y + "px,0)") + (i ? " scale(" + i + ")" : "") }, setPosition: function(t, e) { t._leaflet_pos = e, o.Browser.any3d ? o.DomUtil.setTransform(t, e) : (t.style.left = e.x + "px", t.style.top = e.y + "px") }, getPosition: function(t) { return t._leaflet_pos } }, - function() { o.DomUtil.TRANSFORM = o.DomUtil.testProp(["transform", "WebkitTransform", "OTransform", "MozTransform", "msTransform"]); var i = o.DomUtil.TRANSITION = o.DomUtil.testProp(["webkitTransition", "transition", "OTransition", "MozTransition", "msTransition"]); if (o.DomUtil.TRANSITION_END = "webkitTransition" === i || "OTransition" === i ? i + "End" : "transitionend", "onselectstart" in e) o.DomUtil.disableTextSelection = function() { o.DomEvent.on(t, "selectstart", o.DomEvent.preventDefault) }, o.DomUtil.enableTextSelection = function() { o.DomEvent.off(t, "selectstart", o.DomEvent.preventDefault) }; - else { var n = o.DomUtil.testProp(["userSelect", "WebkitUserSelect", "OUserSelect", "MozUserSelect", "msUserSelect"]); - o.DomUtil.disableTextSelection = function() { if (n) { var t = e.documentElement.style; - this._userSelect = t[n], t[n] = "none" } }, o.DomUtil.enableTextSelection = function() { n && (e.documentElement.style[n] = this._userSelect, delete this._userSelect) } } - o.DomUtil.disableImageDrag = function() { o.DomEvent.on(t, "dragstart", o.DomEvent.preventDefault) }, o.DomUtil.enableImageDrag = function() { o.DomEvent.off(t, "dragstart", o.DomEvent.preventDefault) }, o.DomUtil.preventOutline = function(e) { for (; - 1 === e.tabIndex;) e = e.parentNode; - e && e.style && (o.DomUtil.restoreOutline(), this._outlineElement = e, this._outlineStyle = e.style.outline, e.style.outline = "none", o.DomEvent.on(t, "keydown", o.DomUtil.restoreOutline, this)) }, o.DomUtil.restoreOutline = function() { this._outlineElement && (this._outlineElement.style.outline = this._outlineStyle, delete this._outlineElement, delete this._outlineStyle, o.DomEvent.off(t, "keydown", o.DomUtil.restoreOutline, this)) } }(), o.LatLng = function(t, e, n) { if (isNaN(t) || isNaN(e)) throw new Error("Invalid LatLng object: (" + t + ", " + e + ")"); - this.lat = +t, this.lng = +e, n !== i && (this.alt = +n) }, o.LatLng.prototype = { equals: function(t, e) { if (!t) return !1; - t = o.latLng(t); var n = Math.max(Math.abs(this.lat - t.lat), Math.abs(this.lng - t.lng)); return (e === i ? 1e-9 : e) >= n }, toString: function(t) { return "LatLng(" + o.Util.formatNum(this.lat, t) + ", " + o.Util.formatNum(this.lng, t) + ")" }, distanceTo: function(t) { return o.CRS.Earth.distance(this, o.latLng(t)) }, wrap: function() { return o.CRS.Earth.wrapLatLng(this) }, toBounds: function(t) { var e = 180 * t / 40075017, - i = e / Math.cos(Math.PI / 180 * this.lat); return o.latLngBounds([this.lat - e, this.lng - i], [this.lat + e, this.lng + i]) }, clone: function() { return new o.LatLng(this.lat, this.lng, this.alt) } }, o.latLng = function(t, e, n) { return t instanceof o.LatLng ? t : o.Util.isArray(t) && "object" != typeof t[0] ? 3 === t.length ? new o.LatLng(t[0], t[1], t[2]) : 2 === t.length ? new o.LatLng(t[0], t[1]) : null : t === i || null === t ? t : "object" == typeof t && "lat" in t ? new o.LatLng(t.lat, "lng" in t ? t.lng : t.lon, t.alt) : e === i ? null : new o.LatLng(t, e, n) }, o.LatLngBounds = function(t, e) { if (t) - for (var i = e ? [t, e] : t, n = 0, o = i.length; o > n; n++) this.extend(i[n]) }, o.LatLngBounds.prototype = { extend: function(t) { var e, i, n = this._southWest, - s = this._northEast; if (t instanceof o.LatLng) e = t, i = t; - else { if (!(t instanceof o.LatLngBounds)) return t ? this.extend(o.latLng(t) || o.latLngBounds(t)) : this; if (e = t._southWest, i = t._northEast, !e || !i) return this } return n || s ? (n.lat = Math.min(e.lat, n.lat), n.lng = Math.min(e.lng, n.lng), s.lat = Math.max(i.lat, s.lat), s.lng = Math.max(i.lng, s.lng)) : (this._southWest = new o.LatLng(e.lat, e.lng), this._northEast = new o.LatLng(i.lat, i.lng)), this }, pad: function(t) { var e = this._southWest, - i = this._northEast, - n = Math.abs(e.lat - i.lat) * t, - s = Math.abs(e.lng - i.lng) * t; return new o.LatLngBounds(new o.LatLng(e.lat - n, e.lng - s), new o.LatLng(i.lat + n, i.lng + s)) }, getCenter: function() { return new o.LatLng((this._southWest.lat + this._northEast.lat) / 2, (this._southWest.lng + this._northEast.lng) / 2) }, getSouthWest: function() { return this._southWest }, getNorthEast: function() { return this._northEast }, getNorthWest: function() { return new o.LatLng(this.getNorth(), this.getWest()) }, getSouthEast: function() { return new o.LatLng(this.getSouth(), this.getEast()) }, getWest: function() { return this._southWest.lng }, getSouth: function() { return this._southWest.lat }, getEast: function() { return this._northEast.lng }, getNorth: function() { return this._northEast.lat }, contains: function(t) { t = "number" == typeof t[0] || t instanceof o.LatLng ? o.latLng(t) : o.latLngBounds(t); var e, i, n = this._southWest, - s = this._northEast; return t instanceof o.LatLngBounds ? (e = t.getSouthWest(), i = t.getNorthEast()) : e = i = t, e.lat >= n.lat && i.lat <= s.lat && e.lng >= n.lng && i.lng <= s.lng }, intersects: function(t) { t = o.latLngBounds(t); var e = this._southWest, - i = this._northEast, - n = t.getSouthWest(), - s = t.getNorthEast(), - r = s.lat >= e.lat && n.lat <= i.lat, - a = s.lng >= e.lng && n.lng <= i.lng; return r && a }, overlaps: function(t) { t = o.latLngBounds(t); var e = this._southWest, - i = this._northEast, - n = t.getSouthWest(), - s = t.getNorthEast(), - r = s.lat > e.lat && n.lat < i.lat, - a = s.lng > e.lng && n.lng < i.lng; return r && a }, toBBoxString: function() { return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(",") }, equals: function(t) { return t ? (t = o.latLngBounds(t), this._southWest.equals(t.getSouthWest()) && this._northEast.equals(t.getNorthEast())) : !1 }, isValid: function() { return !(!this._southWest || !this._northEast) } }, o.latLngBounds = function(t, e) { return !t || t instanceof o.LatLngBounds ? t : new o.LatLngBounds(t, e) }, o.Projection = {}, o.Projection.LonLat = { project: function(t) { return new o.Point(t.lng, t.lat) }, unproject: function(t) { return new o.LatLng(t.y, t.x) }, bounds: o.bounds([-180, -90], [180, 90]) }, o.Projection.SphericalMercator = { R: 6378137, MAX_LATITUDE: 85.0511287798, project: function(t) { var e = Math.PI / 180, - i = this.MAX_LATITUDE, - n = Math.max(Math.min(i, t.lat), -i), - s = Math.sin(n * e); return new o.Point(this.R * t.lng * e, this.R * Math.log((1 + s) / (1 - s)) / 2) }, unproject: function(t) { var e = 180 / Math.PI; return new o.LatLng((2 * Math.atan(Math.exp(t.y / this.R)) - Math.PI / 2) * e, t.x * e / this.R) }, bounds: function() { var t = 6378137 * Math.PI; return o.bounds([-t, -t], [t, t]) }() }, o.CRS = { latLngToPoint: function(t, e) { var i = this.projection.project(t), - n = this.scale(e); return this.transformation._transform(i, n) }, pointToLatLng: function(t, e) { var i = this.scale(e), - n = this.transformation.untransform(t, i); return this.projection.unproject(n) }, project: function(t) { return this.projection.project(t) }, unproject: function(t) { return this.projection.unproject(t) }, scale: function(t) { return 256 * Math.pow(2, t) }, zoom: function(t) { return Math.log(t / 256) / Math.LN2 }, getProjectedBounds: function(t) { if (this.infinite) return null; var e = this.projection.bounds, - i = this.scale(t), - n = this.transformation.transform(e.min, i), - s = this.transformation.transform(e.max, i); return o.bounds(n, s) }, wrapLatLng: function(t) { var e = this.wrapLng ? o.Util.wrapNum(t.lng, this.wrapLng, !0) : t.lng, - i = this.wrapLat ? o.Util.wrapNum(t.lat, this.wrapLat, !0) : t.lat, - n = t.alt; return o.latLng(i, e, n) } }, o.CRS.Simple = o.extend({}, o.CRS, { projection: o.Projection.LonLat, transformation: new o.Transformation(1, 0, -1, 0), scale: function(t) { return Math.pow(2, t) }, zoom: function(t) { return Math.log(t) / Math.LN2 }, distance: function(t, e) { var i = e.lng - t.lng, - n = e.lat - t.lat; return Math.sqrt(i * i + n * n) }, infinite: !0 }), o.CRS.Earth = o.extend({}, o.CRS, { wrapLng: [-180, 180], R: 6378137, distance: function(t, e) { var i = Math.PI / 180, - n = t.lat * i, - o = e.lat * i, - s = Math.sin(n) * Math.sin(o) + Math.cos(n) * Math.cos(o) * Math.cos((e.lng - t.lng) * i); return this.R * Math.acos(Math.min(s, 1)) } }), o.CRS.EPSG3857 = o.extend({}, o.CRS.Earth, { code: "EPSG:3857", projection: o.Projection.SphericalMercator, transformation: function() { var t = .5 / (Math.PI * o.Projection.SphericalMercator.R); return new o.Transformation(t, .5, -t, .5) }() }), o.CRS.EPSG900913 = o.extend({}, o.CRS.EPSG3857, { code: "EPSG:900913" }), o.CRS.EPSG4326 = o.extend({}, o.CRS.Earth, { code: "EPSG:4326", projection: o.Projection.LonLat, transformation: new o.Transformation(1 / 180, 1, -1 / 180, .5) }), o.Map = o.Evented.extend({ - options: { crs: o.CRS.EPSG3857, fadeAnimation: !0, trackResize: !0, markerZoomAnimation: !0, maxBoundsViscosity: 0, transform3DLimit: 8388608 }, - initialize: function(t, e) { e = o.setOptions(this, e), this._initContainer(t), this._initLayout(), this._onResize = o.bind(this._onResize, this), this._initEvents(), e.maxBounds && this.setMaxBounds(e.maxBounds), e.zoom !== i && (this._zoom = this._limitZoom(e.zoom)), e.center && e.zoom !== i && this.setView(o.latLng(e.center), e.zoom, { reset: !0 }), this._handlers = [], this._layers = {}, this._zoomBoundLayers = {}, this._sizeChanged = !0, this.callInitHooks(), this._addLayers(this.options.layers) }, - setView: function(t, e) { return e = e === i ? this.getZoom() : e, this._resetView(o.latLng(t), e), this }, - setZoom: function(t, e) { return this._loaded ? this.setView(this.getCenter(), t, { zoom: e }) : (this._zoom = t, this) }, - zoomIn: function(t, e) { return this.setZoom(this._zoom + (t || 1), e) }, - zoomOut: function(t, e) { return this.setZoom(this._zoom - (t || 1), e) }, - setZoomAround: function(t, e, i) { var n = this.getZoomScale(e), - s = this.getSize().divideBy(2), - r = t instanceof o.Point ? t : this.latLngToContainerPoint(t), - a = r.subtract(s).multiplyBy(1 - 1 / n), - h = this.containerPointToLatLng(s.add(a)); return this.setView(h, e, { zoom: i }) }, - _getBoundsCenterZoom: function(t, e) { e = e || {}, t = t.getBounds ? t.getBounds() : o.latLngBounds(t); var i = o.point(e.paddingTopLeft || e.padding || [0, 0]), - n = o.point(e.paddingBottomRight || e.padding || [0, 0]), - s = this.getBoundsZoom(t, !1, i.add(n)); - s = e.maxZoom ? Math.min(e.maxZoom, s) : s; var r = n.subtract(i).divideBy(2), - a = this.project(t.getSouthWest(), s), - h = this.project(t.getNorthEast(), s), - l = this.unproject(a.add(h).divideBy(2).add(r), s); return { center: l, zoom: s } }, - fitBounds: function(t, e) { var i = this._getBoundsCenterZoom(t, e); return this.setView(i.center, i.zoom, e) }, - fitWorld: function(t) { return this.fitBounds([ - [-90, -180], - [90, 180] - ], t) }, - panTo: function(t, e) { return this.setView(t, this._zoom, { pan: e }) }, - panBy: function(t) { return this.fire("movestart"), this._rawPanBy(o.point(t)), this.fire("move"), this.fire("moveend") }, - setMaxBounds: function(t) { return (t = o.latLngBounds(t)) ? (this.options.maxBounds && this.off("moveend", this._panInsideMaxBounds), this.options.maxBounds = t, this._loaded && this._panInsideMaxBounds(), this.on("moveend", this._panInsideMaxBounds)) : this.off("moveend", this._panInsideMaxBounds) }, - setMinZoom: function(t) { return this.options.minZoom = t, this._loaded && this.getZoom() < this.options.minZoom ? this.setZoom(t) : this }, - setMaxZoom: function(t) { return this.options.maxZoom = t, this._loaded && this.getZoom() > this.options.maxZoom ? this.setZoom(t) : this }, - panInsideBounds: function(t, e) { this._enforcingBounds = !0; var i = this.getCenter(), - n = this._limitCenter(i, this._zoom, o.latLngBounds(t)); return i.equals(n) ? this : (this.panTo(n, e), this._enforcingBounds = !1, this) }, - invalidateSize: function(t) { if (!this._loaded) return this; - t = o.extend({ animate: !1, pan: !0 }, t === !0 ? { animate: !0 } : t); var e = this.getSize(); - this._sizeChanged = !0, this._lastCenter = null; var i = this.getSize(), - n = e.divideBy(2).round(), - s = i.divideBy(2).round(), - r = n.subtract(s); return r.x || r.y ? (t.animate && t.pan ? this.panBy(r) : (t.pan && this._rawPanBy(r), this.fire("move"), t.debounceMoveend ? (clearTimeout(this._sizeTimer), this._sizeTimer = setTimeout(o.bind(this.fire, this, "moveend"), 200)) : this.fire("moveend")), this.fire("resize", { oldSize: e, newSize: i })) : this }, - stop: function() { return o.Util.cancelAnimFrame(this._flyToFrame), this._panAnim && this._panAnim.stop(), this }, - addHandler: function(t, e) { if (!e) return this; var i = this[t] = new e(this); return this._handlers.push(i), this.options[t] && i.enable(), this }, - remove: function() { this._initEvents(!0); try { delete this._container._leaflet } catch (t) { this._container._leaflet = i } - o.DomUtil.remove(this._mapPane), this._clearControlPos && this._clearControlPos(), this._clearHandlers(), this._loaded && this.fire("unload"); for (var e in this._layers) this._layers[e].remove(); return this }, - createPane: function(t, e) { var i = "leaflet-pane" + (t ? " leaflet-" + t.replace("Pane", "") + "-pane" : ""), - n = o.DomUtil.create("div", i, e || this._mapPane); return t && (this._panes[t] = n), n }, - getCenter: function() { return this._checkIfLoaded(), this._lastCenter && !this._moved() ? this._lastCenter : this.layerPointToLatLng(this._getCenterLayerPoint()) }, - getZoom: function() { return this._zoom }, - getBounds: function() { var t = this.getPixelBounds(), - e = this.unproject(t.getBottomLeft()), - i = this.unproject(t.getTopRight()); return new o.LatLngBounds(e, i) }, - getMinZoom: function() { return this.options.minZoom === i ? this._layersMinZoom || 0 : this.options.minZoom }, - getMaxZoom: function() { return this.options.maxZoom === i ? this._layersMaxZoom === i ? 1 / 0 : this._layersMaxZoom : this.options.maxZoom }, - getBoundsZoom: function(t, e, i) { t = o.latLngBounds(t); var n, s = this.getMinZoom() - (e ? 1 : 0), - r = this.getMaxZoom(), - a = this.getSize(), - h = t.getNorthWest(), - l = t.getSouthEast(), - u = !0; - i = o.point(i || [0, 0]); - do s++, n = this.project(l, s).subtract(this.project(h, s)).add(i).floor(), u = e ? n.x < a.x || n.y < a.y : a.contains(n); while (u && r >= s); return u && e ? null : e ? s : s - 1 }, - getSize: function() { return (!this._size || this._sizeChanged) && (this._size = new o.Point(this._container.clientWidth, this._container.clientHeight), this._sizeChanged = !1), this._size.clone() }, - getPixelBounds: function(t, e) { var i = this._getTopLeftPoint(t, e); return new o.Bounds(i, i.add(this.getSize())) }, - getPixelOrigin: function() { return this._checkIfLoaded(), this._pixelOrigin }, - getPixelWorldBounds: function(t) { return this.options.crs.getProjectedBounds(t === i ? this.getZoom() : t) }, - getPane: function(t) { return "string" == typeof t ? this._panes[t] : t }, - getPanes: function() { return this._panes }, - getContainer: function() { return this._container }, - getZoomScale: function(t, e) { var n = this.options.crs; return e = e === i ? this._zoom : e, n.scale(t) / n.scale(e) }, - getScaleZoom: function(t, e) { var n = this.options.crs; return e = e === i ? this._zoom : e, n.zoom(t * n.scale(e)) }, - project: function(t, e) { return e = e === i ? this._zoom : e, this.options.crs.latLngToPoint(o.latLng(t), e) }, - unproject: function(t, e) { return e = e === i ? this._zoom : e, this.options.crs.pointToLatLng(o.point(t), e) }, - layerPointToLatLng: function(t) { var e = o.point(t).add(this.getPixelOrigin()); return this.unproject(e) }, - latLngToLayerPoint: function(t) { var e = this.project(o.latLng(t))._round(); return e._subtract(this.getPixelOrigin()) }, - wrapLatLng: function(t) { return this.options.crs.wrapLatLng(o.latLng(t)) }, - distance: function(t, e) { return this.options.crs.distance(o.latLng(t), o.latLng(e)) }, - containerPointToLayerPoint: function(t) { return o.point(t).subtract(this._getMapPanePos()) }, - layerPointToContainerPoint: function(t) { return o.point(t).add(this._getMapPanePos()) }, - containerPointToLatLng: function(t) { var e = this.containerPointToLayerPoint(o.point(t)); return this.layerPointToLatLng(e) }, - latLngToContainerPoint: function(t) { return this.layerPointToContainerPoint(this.latLngToLayerPoint(o.latLng(t))) }, - mouseEventToContainerPoint: function(t) { return o.DomEvent.getMousePosition(t, this._container) }, - mouseEventToLayerPoint: function(t) { return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t)) }, - mouseEventToLatLng: function(t) { return this.layerPointToLatLng(this.mouseEventToLayerPoint(t)) }, - _initContainer: function(t) { var e = this._container = o.DomUtil.get(t); if (!e) throw new Error("Map container not found."); if (e._leaflet) throw new Error("Map container is already initialized."); - o.DomEvent.addListener(e, "scroll", this._onScroll, this), e._leaflet = !0 }, - _initLayout: function() { var t = this._container; - this._fadeAnimated = this.options.fadeAnimation && o.Browser.any3d, o.DomUtil.addClass(t, "leaflet-container" + (o.Browser.touch ? " leaflet-touch" : "") + (o.Browser.retina ? " leaflet-retina" : "") + (o.Browser.ielt9 ? " leaflet-oldie" : "") + (o.Browser.safari ? " leaflet-safari" : "") + (this._fadeAnimated ? " leaflet-fade-anim" : "")); var e = o.DomUtil.getStyle(t, "position"); "absolute" !== e && "relative" !== e && "fixed" !== e && (t.style.position = "relative"), this._initPanes(), this._initControlPos && this._initControlPos() }, - _initPanes: function() { var t = this._panes = {}; - this._paneRenderers = {}, this._mapPane = this.createPane("mapPane", this._container), o.DomUtil.setPosition(this._mapPane, new o.Point(0, 0)), this.createPane("tilePane"), this.createPane("shadowPane"), this.createPane("overlayPane"), this.createPane("markerPane"), this.createPane("popupPane"), this.options.markerZoomAnimation || (o.DomUtil.addClass(t.markerPane, "leaflet-zoom-hide"), o.DomUtil.addClass(t.shadowPane, "leaflet-zoom-hide")) }, - _resetView: function(t, e) { o.DomUtil.setPosition(this._mapPane, new o.Point(0, 0)); var i = !this._loaded; - this._loaded = !0, e = this._limitZoom(e); var n = this._zoom !== e; - this._moveStart(n)._move(t, e)._moveEnd(n), this.fire("viewreset"), i && this.fire("load") }, - _moveStart: function(t) { return t && this.fire("zoomstart"), this.fire("movestart") }, - _move: function(t, e, n) { e === i && (e = this._zoom); var o = this._zoom !== e; return this._zoom = e, this._lastCenter = t, this._pixelOrigin = this._getNewPixelOrigin(t), o && this.fire("zoom", n), this.fire("move", n) }, - _moveEnd: function(t) { return t && this.fire("zoomend"), this.fire("moveend") }, - _rawPanBy: function(t) { o.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(t)) }, - _getZoomSpan: function() { return this.getMaxZoom() - this.getMinZoom() }, - _panInsideMaxBounds: function() { this._enforcingBounds || this.panInsideBounds(this.options.maxBounds) }, - _checkIfLoaded: function() { if (!this._loaded) throw new Error("Set map center and zoom first.") }, - _initEvents: function(e) { if (o.DomEvent) { this._targets = {}, this._targets[o.stamp(this._container)] = this; var i = e ? "off" : "on"; - o.DomEvent[i](this._container, "click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress", this._handleDOMEvent, this), this.options.trackResize && o.DomEvent[i](t, "resize", this._onResize, this), o.Browser.any3d && this.options.transform3DLimit && this[i]("moveend", this._onMoveEnd) } }, - _onResize: function() { o.Util.cancelAnimFrame(this._resizeRequest), this._resizeRequest = o.Util.requestAnimFrame(function() { this.invalidateSize({ debounceMoveend: !0 }) }, this) }, - _onScroll: function() { this._container.scrollTop = 0, this._container.scrollLeft = 0 }, - _onMoveEnd: function() { var t = this._getMapPanePos(); - Math.max(Math.abs(t.x), Math.abs(t.y)) >= this.options.transform3DLimit && this._resetView(this.getCenter(), this.getZoom()) }, - _findEventTargets: function(t, e) { for (var i, n = [], s = "mouseout" === e || "mouseover" === e, r = t.target || t.srcElement; r;) { if (i = this._targets[o.stamp(r)], i && i.listens(e, !0)) { if (s && !o.DomEvent._isExternalTarget(r, t)) break; if (n.push(i), s) break } if (r === this._container) break; - r = r.parentNode } return n.length || s || !o.DomEvent._isExternalTarget(r, t) || (n = [this]), n }, - _handleDOMEvent: function(t) { if (this._loaded && !o.DomEvent._skipped(t)) { var e = "keypress" === t.type && 13 === t.keyCode ? "click" : t.type; if ("click" === t.type) { var i = o.Util.extend({}, t); - i.type = "preclick", this._handleDOMEvent(i) } "mousedown" === e && o.DomUtil.preventOutline(t.target || t.srcElement), this._fireDOMEvent(t, e) } }, - _fireDOMEvent: function(t, e, i) { if (!t._stopped && (i = (i || []).concat(this._findEventTargets(t, e)), i.length)) { var n = i[0]; if ("contextmenu" === e && n.listens(e, !0) && o.DomEvent.preventDefault(t), "click" !== t.type && "preclick" !== t.type || t._simulated || !this._draggableMoved(n)) { var s = { originalEvent: t }; if ("keypress" !== t.type) { var r = n instanceof o.Marker; - s.containerPoint = r ? this.latLngToContainerPoint(n.getLatLng()) : this.mouseEventToContainerPoint(t), s.layerPoint = this.containerPointToLayerPoint(s.containerPoint), s.latlng = r ? n.getLatLng() : this.layerPointToLatLng(s.layerPoint) } for (var a = 0; a < i.length; a++) - if (i[a].fire(e, s, !0), s.originalEvent._stopped || i[a].options.nonBubblingEvents && -1 !== o.Util.indexOf(i[a].options.nonBubblingEvents, e)) return } } }, - _draggableMoved: function(t) { return t = t.options.draggable ? t : this, t.dragging && t.dragging.moved() || this.boxZoom && this.boxZoom.moved() }, - _clearHandlers: function() { for (var t = 0, e = this._handlers.length; e > t; t++) this._handlers[t].disable() }, - whenReady: function(t, e) { return this._loaded ? t.call(e || this, { target: this }) : this.on("load", t, e), this }, - _getMapPanePos: function() { return o.DomUtil.getPosition(this._mapPane) || new o.Point(0, 0) }, - _moved: function() { var t = this._getMapPanePos(); return t && !t.equals([0, 0]) }, - _getTopLeftPoint: function(t, e) { var n = t && e !== i ? this._getNewPixelOrigin(t, e) : this.getPixelOrigin(); return n.subtract(this._getMapPanePos()) }, - _getNewPixelOrigin: function(t, e) { var i = this.getSize()._divideBy(2); return this.project(t, e)._subtract(i)._add(this._getMapPanePos())._round() }, - _latLngToNewLayerPoint: function(t, e, i) { - var n = this._getNewPixelOrigin(i, e); - return this.project(t, e)._subtract(n) - }, - _getCenterLayerPoint: function() { return this.containerPointToLayerPoint(this.getSize()._divideBy(2)) }, - _getCenterOffset: function(t) { return this.latLngToLayerPoint(t).subtract(this._getCenterLayerPoint()) }, - _limitCenter: function(t, e, i) { if (!i) return t; var n = this.project(t, e), - s = this.getSize().divideBy(2), - r = new o.Bounds(n.subtract(s), n.add(s)), - a = this._getBoundsOffset(r, i, e); return this.unproject(n.add(a), e) }, - _limitOffset: function(t, e) { if (!e) return t; var i = this.getPixelBounds(), - n = new o.Bounds(i.min.add(t), i.max.add(t)); return t.add(this._getBoundsOffset(n, e)) }, - _getBoundsOffset: function(t, e, i) { var n = this.project(e.getNorthWest(), i).subtract(t.min), - s = this.project(e.getSouthEast(), i).subtract(t.max), - r = this._rebound(n.x, -s.x), - a = this._rebound(n.y, -s.y); return new o.Point(r, a) }, - _rebound: function(t, e) { return t + e > 0 ? Math.round(t - e) / 2 : Math.max(0, Math.ceil(t)) - Math.max(0, Math.floor(e)) }, - _limitZoom: function(t) { var e = this.getMinZoom(), - i = this.getMaxZoom(); return o.Browser.any3d || (t = Math.round(t)), Math.max(e, Math.min(i, t)) } - }), o.map = function(t, e) { return new o.Map(t, e) }, o.Layer = o.Evented.extend({ options: { pane: "overlayPane", nonBubblingEvents: [] }, addTo: function(t) { return t.addLayer(this), this }, remove: function() { return this.removeFrom(this._map || this._mapToAdd) }, removeFrom: function(t) { return t && t.removeLayer(this), this }, getPane: function(t) { return this._map.getPane(t ? this.options[t] || t : this.options.pane) }, addInteractiveTarget: function(t) { return this._map._targets[o.stamp(t)] = this, this }, removeInteractiveTarget: function(t) { return delete this._map._targets[o.stamp(t)], this }, _layerAdd: function(t) { var e = t.target; - e.hasLayer(this) && (this._map = e, this._zoomAnimated = e._zoomAnimated, this.getEvents && e.on(this.getEvents(), this), this.onAdd(e), this.getAttribution && this._map.attributionControl && this._map.attributionControl.addAttribution(this.getAttribution()), this.fire("add"), e.fire("layeradd", { layer: this })) } }), o.Map.include({ addLayer: function(t) { var e = o.stamp(t); return this._layers[e] ? t : (this._layers[e] = t, t._mapToAdd = this, t.beforeAdd && t.beforeAdd(this), this.whenReady(t._layerAdd, t), this) }, removeLayer: function(t) { var e = o.stamp(t); return this._layers[e] ? (this._loaded && t.onRemove(this), t.getAttribution && this.attributionControl && this.attributionControl.removeAttribution(t.getAttribution()), t.getEvents && this.off(t.getEvents(), t), delete this._layers[e], this._loaded && (this.fire("layerremove", { layer: t }), t.fire("remove")), t._map = t._mapToAdd = null, this) : this }, hasLayer: function(t) { return !!t && o.stamp(t) in this._layers }, eachLayer: function(t, e) { for (var i in this._layers) t.call(e, this._layers[i]); return this }, _addLayers: function(t) { t = t ? o.Util.isArray(t) ? t : [t] : []; for (var e = 0, i = t.length; i > e; e++) this.addLayer(t[e]) }, _addZoomLimit: function(t) { - (isNaN(t.options.maxZoom) || !isNaN(t.options.minZoom)) && (this._zoomBoundLayers[o.stamp(t)] = t, this._updateZoomLevels()) }, _removeZoomLimit: function(t) { var e = o.stamp(t); - this._zoomBoundLayers[e] && (delete this._zoomBoundLayers[e], this._updateZoomLevels()) }, _updateZoomLevels: function() { var t = 1 / 0, - e = -(1 / 0), - n = this._getZoomSpan(); for (var o in this._zoomBoundLayers) { var s = this._zoomBoundLayers[o].options; - t = s.minZoom === i ? t : Math.min(t, s.minZoom), e = s.maxZoom === i ? e : Math.max(e, s.maxZoom) } - this._layersMaxZoom = e === -(1 / 0) ? i : e, this._layersMinZoom = t === 1 / 0 ? i : t, n !== this._getZoomSpan() && this.fire("zoomlevelschange") } }), o.Projection.Mercator = { R: 6378137, R_MINOR: 6356752.314245179, bounds: o.bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]), project: function(t) { var e = Math.PI / 180, - i = this.R, - n = t.lat * e, - s = this.R_MINOR / i, - r = Math.sqrt(1 - s * s), - a = r * Math.sin(n), - h = Math.tan(Math.PI / 4 - n / 2) / Math.pow((1 - a) / (1 + a), r / 2); return n = -i * Math.log(Math.max(h, 1e-10)), new o.Point(t.lng * e * i, n) }, unproject: function(t) { for (var e, i = 180 / Math.PI, n = this.R, s = this.R_MINOR / n, r = Math.sqrt(1 - s * s), a = Math.exp(-t.y / n), h = Math.PI / 2 - 2 * Math.atan(a), l = 0, u = .1; 15 > l && Math.abs(u) > 1e-7; l++) e = r * Math.sin(h), e = Math.pow((1 - e) / (1 + e), r / 2), u = Math.PI / 2 - 2 * Math.atan(a * e) - h, h += u; return new o.LatLng(h * i, t.x * i / n) } }, o.CRS.EPSG3395 = o.extend({}, o.CRS.Earth, { code: "EPSG:3395", projection: o.Projection.Mercator, transformation: function() { var t = .5 / (Math.PI * o.Projection.Mercator.R); return new o.Transformation(t, .5, -t, .5) }() }), o.GridLayer = o.Layer.extend({ options: { pane: "tilePane", tileSize: 256, opacity: 1, zIndex: 1, updateWhenIdle: o.Browser.mobile, updateInterval: 200, attribution: null, bounds: null, minZoom: 0 }, initialize: function(t) { t = o.setOptions(this, t) }, onAdd: function() { this._initContainer(), this._levels = {}, this._tiles = {}, this._resetView(), this._update() }, beforeAdd: function(t) { t._addZoomLimit(this) }, onRemove: function(t) { o.DomUtil.remove(this._container), t._removeZoomLimit(this), this._container = null, this._tileZoom = null }, bringToFront: function() { return this._map && (o.DomUtil.toFront(this._container), this._setAutoZIndex(Math.max)), this }, bringToBack: function() { return this._map && (o.DomUtil.toBack(this._container), this._setAutoZIndex(Math.min)), this }, getAttribution: function() { return this.options.attribution }, getContainer: function() { return this._container }, setOpacity: function(t) { return this.options.opacity = t, this._updateOpacity(), this }, setZIndex: function(t) { return this.options.zIndex = t, this._updateZIndex(), this }, isLoading: function() { return this._loading }, redraw: function() { return this._map && (this._removeAllTiles(), this._update()), this }, getEvents: function() { var t = { viewreset: this._resetAll, zoom: this._resetView, moveend: this._onMoveEnd }; return this.options.updateWhenIdle || (this._onMove || (this._onMove = o.Util.throttle(this._onMoveEnd, this.options.updateInterval, this)), t.move = this._onMove), this._zoomAnimated && (t.zoomanim = this._animateZoom), t }, createTile: function() { return e.createElement("div") }, getTileSize: function() { var t = this.options.tileSize; return t instanceof o.Point ? t : new o.Point(t, t) }, _updateZIndex: function() { this._container && this.options.zIndex !== i && null !== this.options.zIndex && (this._container.style.zIndex = this.options.zIndex) }, _setAutoZIndex: function(t) { for (var e, i = this.getPane().children, n = -t(-(1 / 0), 1 / 0), o = 0, s = i.length; s > o; o++) e = i[o].style.zIndex, i[o] !== this._container && e && (n = t(n, +e)); - isFinite(n) && (this.options.zIndex = n + t(-1, 1), this._updateZIndex()) }, _updateOpacity: function() { if (this._map && !o.Browser.ielt9 && this._map._fadeAnimated) { o.DomUtil.setOpacity(this._container, this.options.opacity); var t = +new Date, - e = !1, - i = !1; for (var n in this._tiles) { var s = this._tiles[n]; if (s.current && s.loaded) { var r = Math.min(1, (t - s.loaded) / 200); - o.DomUtil.setOpacity(s.el, r), 1 > r ? e = !0 : (s.active && (i = !0), s.active = !0) } } - i && !this._noPrune && this._pruneTiles(), e && (o.Util.cancelAnimFrame(this._fadeFrame), this._fadeFrame = o.Util.requestAnimFrame(this._updateOpacity, this)) } }, _initContainer: function() { this._container || (this._container = o.DomUtil.create("div", "leaflet-layer"), this._updateZIndex(), this.options.opacity < 1 && this._updateOpacity(), this.getPane().appendChild(this._container)) }, _updateLevels: function() { var t = this._tileZoom, - e = this.options.maxZoom; for (var i in this._levels) this._levels[i].el.children.length || i === t ? this._levels[i].el.style.zIndex = e - Math.abs(t - i) : (o.DomUtil.remove(this._levels[i].el), delete this._levels[i]); var n = this._levels[t], - s = this._map; return n || (n = this._levels[t] = {}, n.el = o.DomUtil.create("div", "leaflet-tile-container leaflet-zoom-animated", this._container), n.el.style.zIndex = e, n.origin = s.project(s.unproject(s.getPixelOrigin()), t).round(), n.zoom = t, this._setZoomTransform(n, s.getCenter(), s.getZoom()), o.Util.falseFn(n.el.offsetWidth)), this._level = n, n }, _pruneTiles: function() { var t, e, i = this._map.getZoom(); if (i > this.options.maxZoom || i < this.options.minZoom) return this._removeAllTiles(); for (t in this._tiles) e = this._tiles[t], e.retain = e.current; for (t in this._tiles) - if (e = this._tiles[t], e.current && !e.active) { var n = e.coords; - this._retainParent(n.x, n.y, n.z, n.z - 5) || this._retainChildren(n.x, n.y, n.z, n.z + 2) } - for (t in this._tiles) this._tiles[t].retain || this._removeTile(t) }, _removeAllTiles: function() { for (var t in this._tiles) this._removeTile(t) }, _resetAll: function() { for (var t in this._levels) o.DomUtil.remove(this._levels[t].el), delete this._levels[t]; - this._removeAllTiles(), this._tileZoom = null, this._resetView() }, _retainParent: function(t, e, i, n) { var o = Math.floor(t / 2), - s = Math.floor(e / 2), - r = i - 1, - a = o + ":" + s + ":" + r, - h = this._tiles[a]; return h && h.active ? (h.retain = !0, !0) : (h && h.loaded && (h.retain = !0), r > n ? this._retainParent(o, s, r, n) : !1) }, _retainChildren: function(t, e, i, n) { for (var o = 2 * t; 2 * t + 2 > o; o++) - for (var s = 2 * e; 2 * e + 2 > s; s++) { var r = o + ":" + s + ":" + (i + 1), - a = this._tiles[r]; - a && a.active ? a.retain = !0 : (a && a.loaded && (a.retain = !0), n > i + 1 && this._retainChildren(o, s, i + 1, n)) } }, _resetView: function(t) { var e = t && (t.pinch || t.flyTo); - this._setView(this._map.getCenter(), this._map.getZoom(), e, e) }, _animateZoom: function(t) { this._setView(t.center, t.zoom, !0, t.noUpdate) }, _setView: function(t, e, n, o) { var s = Math.round(e); - (this.options.maxZoom !== i && s > this.options.maxZoom || this.options.minZoom !== i && s < this.options.minZoom) && (s = i); var r = s !== this._tileZoom; - (!o || r) && (this._tileZoom = s, this._abortLoading && this._abortLoading(), this._updateLevels(), this._resetGrid(), s !== i && this._update(t), n || this._pruneTiles(), this._noPrune = !!n), this._setZoomTransforms(t, e) }, _setZoomTransforms: function(t, e) { for (var i in this._levels) this._setZoomTransform(this._levels[i], t, e) }, _setZoomTransform: function(t, e, i) { var n = this._map.getZoomScale(i, t.zoom), - s = t.origin.multiplyBy(n).subtract(this._map._getNewPixelOrigin(e, i)).round(); - o.Browser.any3d ? o.DomUtil.setTransform(t.el, s, n) : o.DomUtil.setPosition(t.el, s) }, _resetGrid: function() { var t = this._map, - e = t.options.crs, - i = this._tileSize = this.getTileSize(), - n = this._tileZoom, - o = this._map.getPixelWorldBounds(this._tileZoom); - o && (this._globalTileRange = this._pxBoundsToTileRange(o)), this._wrapX = e.wrapLng && !this.options.noWrap && [Math.floor(t.project([0, e.wrapLng[0]], n).x / i.x), Math.ceil(t.project([0, e.wrapLng[1]], n).x / i.y)], this._wrapY = e.wrapLat && !this.options.noWrap && [Math.floor(t.project([e.wrapLat[0], 0], n).y / i.x), Math.ceil(t.project([e.wrapLat[1], 0], n).y / i.y)] }, _onMoveEnd: function() { this._map && !this._map._animatingZoom && this._resetView() }, _getTiledPixelBounds: function(t, e, i) { var n = this._map, - s = n.getZoomScale(e, i), - r = n.project(t, i).floor(), - a = n.getSize().divideBy(2 * s); return new o.Bounds(r.subtract(a), r.add(a)) }, _update: function(t) { var n = this._map; if (n) { var s = n.getZoom(); if (t === i && (t = n.getCenter()), this._tileZoom !== i) { var r = this._getTiledPixelBounds(t, s, this._tileZoom), - a = this._pxBoundsToTileRange(r), - h = a.getCenter(), - l = []; for (var u in this._tiles) this._tiles[u].current = !1; if (Math.abs(s - this._tileZoom) > 1) return void this._setView(t, s); for (var c = a.min.y; c <= a.max.y; c++) - for (var d = a.min.x; d <= a.max.x; d++) { var _ = new o.Point(d, c); if (_.z = this._tileZoom, this._isValidTile(_)) { var m = this._tiles[this._tileCoordsToKey(_)]; - m ? m.current = !0 : l.push(_) } } - if (l.sort(function(t, e) { return t.distanceTo(h) - e.distanceTo(h) }), 0 !== l.length) { this._loading || (this._loading = !0, this.fire("loading")); var p = e.createDocumentFragment(); for (d = 0; d < l.length; d++) this._addTile(l[d], p); - this._level.el.appendChild(p) } } } }, _isValidTile: function(t) { var e = this._map.options.crs; if (!e.infinite) { var i = this._globalTileRange; if (!e.wrapLng && (t.x < i.min.x || t.x > i.max.x) || !e.wrapLat && (t.y < i.min.y || t.y > i.max.y)) return !1 } if (!this.options.bounds) return !0; var n = this._tileCoordsToBounds(t); return o.latLngBounds(this.options.bounds).overlaps(n) }, _keyToBounds: function(t) { return this._tileCoordsToBounds(this._keyToTileCoords(t)) }, _tileCoordsToBounds: function(t) { var e = this._map, - i = this.getTileSize(), - n = t.scaleBy(i), - s = n.add(i), - r = e.wrapLatLng(e.unproject(n, t.z)), - a = e.wrapLatLng(e.unproject(s, t.z)); return new o.LatLngBounds(r, a) }, _tileCoordsToKey: function(t) { return t.x + ":" + t.y + ":" + t.z }, _keyToTileCoords: function(t) { var e = t.split(":"), - i = new o.Point(+e[0], +e[1]); return i.z = +e[2], i }, _removeTile: function(t) { var e = this._tiles[t]; - e && (o.DomUtil.remove(e.el), delete this._tiles[t], this.fire("tileunload", { tile: e.el, coords: this._keyToTileCoords(t) })) }, _initTile: function(t) { o.DomUtil.addClass(t, "leaflet-tile"); var e = this.getTileSize(); - t.style.width = e.x + "px", t.style.height = e.y + "px", t.onselectstart = o.Util.falseFn, t.onmousemove = o.Util.falseFn, o.Browser.ielt9 && this.options.opacity < 1 && o.DomUtil.setOpacity(t, this.options.opacity), o.Browser.android && !o.Browser.android23 && (t.style.WebkitBackfaceVisibility = "hidden") }, _addTile: function(t, e) { var i = this._getTilePos(t), - n = this._tileCoordsToKey(t), - s = this.createTile(this._wrapCoords(t), o.bind(this._tileReady, this, t)); - this._initTile(s), this.createTile.length < 2 && o.Util.requestAnimFrame(o.bind(this._tileReady, this, t, null, s)), o.DomUtil.setPosition(s, i), this._tiles[n] = { el: s, coords: t, current: !0 }, e.appendChild(s), this.fire("tileloadstart", { tile: s, coords: t }) }, _tileReady: function(t, e, i) { if (this._map) { e && this.fire("tileerror", { error: e, tile: i, coords: t }); var n = this._tileCoordsToKey(t); - i = this._tiles[n], i && (i.loaded = +new Date, this._map._fadeAnimated ? (o.DomUtil.setOpacity(i.el, 0), o.Util.cancelAnimFrame(this._fadeFrame), this._fadeFrame = o.Util.requestAnimFrame(this._updateOpacity, this)) : (i.active = !0, this._pruneTiles()), o.DomUtil.addClass(i.el, "leaflet-tile-loaded"), this.fire("tileload", { tile: i.el, coords: t }), this._noTilesToLoad() && (this._loading = !1, this.fire("load"))) } }, _getTilePos: function(t) { return t.scaleBy(this.getTileSize()).subtract(this._level.origin) }, _wrapCoords: function(t) { var e = new o.Point(this._wrapX ? o.Util.wrapNum(t.x, this._wrapX) : t.x, this._wrapY ? o.Util.wrapNum(t.y, this._wrapY) : t.y); return e.z = t.z, e }, _pxBoundsToTileRange: function(t) { var e = this.getTileSize(); return new o.Bounds(t.min.unscaleBy(e).floor(), t.max.unscaleBy(e).ceil().subtract([1, 1])) }, _noTilesToLoad: function() { for (var t in this._tiles) - if (!this._tiles[t].loaded) return !1; - return !0 } }), o.gridLayer = function(t) { return new o.GridLayer(t) }, o.TileLayer = o.GridLayer.extend({ options: { maxZoom: 18, subdomains: "abc", errorTileUrl: "", zoomOffset: 0, maxNativeZoom: null, tms: !1, zoomReverse: !1, detectRetina: !1, crossOrigin: !1 }, initialize: function(t, e) { this._url = t, e = o.setOptions(this, e), e.detectRetina && o.Browser.retina && e.maxZoom > 0 && (e.tileSize = Math.floor(e.tileSize / 2), e.zoomOffset++, e.minZoom = Math.max(0, e.minZoom), e.maxZoom--), "string" == typeof e.subdomains && (e.subdomains = e.subdomains.split("")), o.Browser.android || this.on("tileunload", this._onTileRemove) }, setUrl: function(t, e) { return this._url = t, e || this.redraw(), this }, createTile: function(t, i) { var n = e.createElement("img"); return o.DomEvent.on(n, "load", o.bind(this._tileOnLoad, this, i, n)), o.DomEvent.on(n, "error", o.bind(this._tileOnError, this, i, n)), this.options.crossOrigin && (n.crossOrigin = ""), n.alt = "", n.src = this.getTileUrl(t), n }, getTileUrl: function(t) { return o.Util.template(this._url, o.extend({ r: this.options.detectRetina && o.Browser.retina && this.options.maxZoom > 0 ? "@2x" : "", s: this._getSubdomain(t), x: t.x, y: this.options.tms ? this._globalTileRange.max.y - t.y : t.y, z: this._getZoomForUrl() }, this.options)) }, _tileOnLoad: function(t, e) { o.Browser.ielt9 ? setTimeout(o.bind(t, this, null, e), 0) : t(null, e) }, _tileOnError: function(t, e, i) { var n = this.options.errorTileUrl; - n && (e.src = n), t(i, e) }, getTileSize: function() { var t = this._map, - e = o.GridLayer.prototype.getTileSize.call(this), - i = this._tileZoom + this.options.zoomOffset, - n = this.options.maxNativeZoom; return null !== n && i > n ? e.divideBy(t.getZoomScale(n, i)).round() : e }, _onTileRemove: function(t) { t.tile.onload = null }, _getZoomForUrl: function() { var t = this.options, - e = this._tileZoom; return t.zoomReverse && (e = t.maxZoom - e), e += t.zoomOffset, null !== t.maxNativeZoom ? Math.min(e, t.maxNativeZoom) : e }, _getSubdomain: function(t) { var e = Math.abs(t.x + t.y) % this.options.subdomains.length; return this.options.subdomains[e] }, _abortLoading: function() { var t, e; for (t in this._tiles) this._tiles[t].coords.z !== this._tileZoom && (e = this._tiles[t].el, e.onload = o.Util.falseFn, e.onerror = o.Util.falseFn, e.complete || (e.src = o.Util.emptyImageUrl, o.DomUtil.remove(e))) } }), o.tileLayer = function(t, e) { return new o.TileLayer(t, e) }, o.TileLayer.WMS = o.TileLayer.extend({ defaultWmsParams: { service: "WMS", request: "GetMap", version: "1.1.1", layers: "", styles: "", format: "image/jpeg", transparent: !1 }, options: { crs: null, uppercase: !1 }, initialize: function(t, e) { this._url = t; var i = o.extend({}, this.defaultWmsParams); for (var n in e) n in this.options || (i[n] = e[n]); - e = o.setOptions(this, e), i.width = i.height = e.tileSize * (e.detectRetina && o.Browser.retina ? 2 : 1), this.wmsParams = i }, onAdd: function(t) { this._crs = this.options.crs || t.options.crs, this._wmsVersion = parseFloat(this.wmsParams.version); var e = this._wmsVersion >= 1.3 ? "crs" : "srs"; - this.wmsParams[e] = this._crs.code, o.TileLayer.prototype.onAdd.call(this, t) }, getTileUrl: function(t) { var e = this._tileCoordsToBounds(t), - i = this._crs.project(e.getNorthWest()), - n = this._crs.project(e.getSouthEast()), - s = (this._wmsVersion >= 1.3 && this._crs === o.CRS.EPSG4326 ? [n.y, i.x, i.y, n.x] : [i.x, n.y, n.x, i.y]).join(","), - r = o.TileLayer.prototype.getTileUrl.call(this, t); return r + o.Util.getParamString(this.wmsParams, r, this.options.uppercase) + (this.options.uppercase ? "&BBOX=" : "&bbox=") + s }, setParams: function(t, e) { return o.extend(this.wmsParams, t), e || this.redraw(), this } }), o.tileLayer.wms = function(t, e) { return new o.TileLayer.WMS(t, e) }, o.ImageOverlay = o.Layer.extend({ options: { opacity: 1, alt: "", interactive: !1 }, initialize: function(t, e, i) { this._url = t, this._bounds = o.latLngBounds(e), o.setOptions(this, i) }, onAdd: function() { this._image || (this._initImage(), this.options.opacity < 1 && this._updateOpacity()), this.options.interactive && (o.DomUtil.addClass(this._image, "leaflet-interactive"), this.addInteractiveTarget(this._image)), this.getPane().appendChild(this._image), this._reset() }, onRemove: function() { o.DomUtil.remove(this._image), this.options.interactive && this.removeInteractiveTarget(this._image) }, setOpacity: function(t) { return this.options.opacity = t, this._image && this._updateOpacity(), this }, setStyle: function(t) { return t.opacity && this.setOpacity(t.opacity), this }, bringToFront: function() { return this._map && o.DomUtil.toFront(this._image), this }, bringToBack: function() { return this._map && o.DomUtil.toBack(this._image), this }, setUrl: function(t) { return this._url = t, this._image && (this._image.src = t), this }, setBounds: function(t) { return this._bounds = t, this._map && this._reset(), this }, getAttribution: function() { return this.options.attribution }, getEvents: function() { var t = { zoom: this._reset, viewreset: this._reset }; return this._zoomAnimated && (t.zoomanim = this._animateZoom), t }, getBounds: function() { return this._bounds }, getElement: function() { return this._image }, _initImage: function() { var t = this._image = o.DomUtil.create("img", "leaflet-image-layer " + (this._zoomAnimated ? "leaflet-zoom-animated" : "")); - t.onselectstart = o.Util.falseFn, t.onmousemove = o.Util.falseFn, t.onload = o.bind(this.fire, this, "load"), this.options.crossOrigin && (t.crossOrigin = ""), t.src = this._url, t.alt = this.options.alt }, _animateZoom: function(t) { var e = this._map.getZoomScale(t.zoom), - i = this._map._latLngToNewLayerPoint(this._bounds.getNorthWest(), t.zoom, t.center); - o.DomUtil.setTransform(this._image, i, e) }, _reset: function() { var t = this._image, - e = new o.Bounds(this._map.latLngToLayerPoint(this._bounds.getNorthWest()), this._map.latLngToLayerPoint(this._bounds.getSouthEast())), - i = e.getSize(); - o.DomUtil.setPosition(t, e.min), t.style.width = i.x + "px", t.style.height = i.y + "px" }, _updateOpacity: function() { o.DomUtil.setOpacity(this._image, this.options.opacity) } }), o.imageOverlay = function(t, e, i) { return new o.ImageOverlay(t, e, i) }, o.Icon = o.Class.extend({ initialize: function(t) { o.setOptions(this, t) }, createIcon: function(t) { return this._createIcon("icon", t) }, createShadow: function(t) { return this._createIcon("shadow", t) }, _createIcon: function(t, e) { var i = this._getIconUrl(t); if (!i) { if ("icon" === t) throw new Error("iconUrl not set in Icon options (see the docs)."); return null } var n = this._createImg(i, e && "IMG" === e.tagName ? e : null); return this._setIconStyles(n, t), n }, _setIconStyles: function(t, e) { var i = this.options, - n = o.point(i[e + "Size"]), - s = o.point("shadow" === e && i.shadowAnchor || i.iconAnchor || n && n.divideBy(2, !0)); - t.className = "leaflet-marker-" + e + " " + (i.className || ""), s && (t.style.marginLeft = -s.x + "px", t.style.marginTop = -s.y + "px"), n && (t.style.width = n.x + "px", t.style.height = n.y + "px") }, _createImg: function(t, i) { return i = i || e.createElement("img"), i.src = t, i }, _getIconUrl: function(t) { return o.Browser.retina && this.options[t + "RetinaUrl"] || this.options[t + "Url"] } }), o.icon = function(t) { return new o.Icon(t) }, o.Icon.Default = o.Icon.extend({ options: { iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] }, _getIconUrl: function(t) { var e = t + "Url"; if (this.options[e]) return this.options[e]; var i = o.Icon.Default.imagePath; if (!i) throw new Error("Couldn't autodetect L.Icon.Default.imagePath, set it manually."); return i + "/marker-" + t + (o.Browser.retina && "icon" === t ? "-2x" : "") + ".png" } }), o.Icon.Default.imagePath = function() { var t, i, n, o, s = e.getElementsByTagName("script"), - r = /[\/^]leaflet[\-\._]?([\w\-\._]*)\.js\??/; for (t = 0, i = s.length; i > t; t++) - if (n = s[t].src || "", n.match(r)) return o = n.split(r)[0], (o ? o + "/" : "") + "images" }(), o.Marker = o.Layer.extend({ options: { pane: "markerPane", nonBubblingEvents: ["click", "dblclick", "mouseover", "mouseout", "contextmenu"], icon: new o.Icon.Default, interactive: !0, keyboard: !0, zIndexOffset: 0, opacity: 1, riseOffset: 250 }, initialize: function(t, e) { o.setOptions(this, e), this._latlng = o.latLng(t) }, onAdd: function(t) { this._zoomAnimated = this._zoomAnimated && t.options.markerZoomAnimation, this._initIcon(), this.update() }, onRemove: function() { this.dragging && this.dragging.enabled() && (this.options.draggable = !0, this.dragging.removeHooks()), this._removeIcon(), this._removeShadow() }, getEvents: function() { var t = { zoom: this.update, viewreset: this.update }; return this._zoomAnimated && (t.zoomanim = this._animateZoom), t }, getLatLng: function() { return this._latlng }, setLatLng: function(t) { var e = this._latlng; return this._latlng = o.latLng(t), this.update(), this.fire("move", { oldLatLng: e, latlng: this._latlng }) }, setZIndexOffset: function(t) { return this.options.zIndexOffset = t, this.update() }, setIcon: function(t) { return this.options.icon = t, this._map && (this._initIcon(), this.update()), this._popup && this.bindPopup(this._popup, this._popup.options), this }, getElement: function() { return this._icon }, update: function() { if (this._icon) { var t = this._map.latLngToLayerPoint(this._latlng).round(); - this._setPos(t) } return this }, _initIcon: function() { var t = this.options, - e = "leaflet-zoom-" + (this._zoomAnimated ? "animated" : "hide"), - i = t.icon.createIcon(this._icon), - n = !1; - i !== this._icon && (this._icon && this._removeIcon(), n = !0, t.title && (i.title = t.title), t.alt && (i.alt = t.alt)), o.DomUtil.addClass(i, e), t.keyboard && (i.tabIndex = "0"), this._icon = i, t.riseOnHover && this.on({ mouseover: this._bringToFront, mouseout: this._resetZIndex }); var s = t.icon.createShadow(this._shadow), - r = !1; - s !== this._shadow && (this._removeShadow(), r = !0), s && o.DomUtil.addClass(s, e), this._shadow = s, t.opacity < 1 && this._updateOpacity(), n && (this.getPane().appendChild(this._icon), this._initInteraction()), s && r && this.getPane("shadowPane").appendChild(this._shadow) }, _removeIcon: function() { this.options.riseOnHover && this.off({ mouseover: this._bringToFront, mouseout: this._resetZIndex }), o.DomUtil.remove(this._icon), this.removeInteractiveTarget(this._icon), this._icon = null }, _removeShadow: function() { this._shadow && o.DomUtil.remove(this._shadow), this._shadow = null }, _setPos: function(t) { o.DomUtil.setPosition(this._icon, t), this._shadow && o.DomUtil.setPosition(this._shadow, t), this._zIndex = t.y + this.options.zIndexOffset, this._resetZIndex() }, _updateZIndex: function(t) { this._icon.style.zIndex = this._zIndex + t }, _animateZoom: function(t) { var e = this._map._latLngToNewLayerPoint(this._latlng, t.zoom, t.center).round(); - this._setPos(e) }, _initInteraction: function() { if (this.options.interactive && (o.DomUtil.addClass(this._icon, "leaflet-interactive"), this.addInteractiveTarget(this._icon), o.Handler.MarkerDrag)) { var t = this.options.draggable; - this.dragging && (t = this.dragging.enabled(), this.dragging.disable()), this.dragging = new o.Handler.MarkerDrag(this), t && this.dragging.enable() } }, setOpacity: function(t) { return this.options.opacity = t, this._map && this._updateOpacity(), this }, _updateOpacity: function() { var t = this.options.opacity; - o.DomUtil.setOpacity(this._icon, t), this._shadow && o.DomUtil.setOpacity(this._shadow, t) }, _bringToFront: function() { this._updateZIndex(this.options.riseOffset) }, _resetZIndex: function() { this._updateZIndex(0) } }), o.marker = function(t, e) { return new o.Marker(t, e) }, o.DivIcon = o.Icon.extend({ options: { iconSize: [12, 12], className: "leaflet-div-icon", html: !1 }, createIcon: function(t) { var i = t && "DIV" === t.tagName ? t : e.createElement("div"), - n = this.options; return i.innerHTML = n.html !== !1 ? n.html : "", n.bgPos && (i.style.backgroundPosition = -n.bgPos.x + "px " + -n.bgPos.y + "px"), this._setIconStyles(i, "icon"), i }, createShadow: function() { return null } }), o.divIcon = function(t) { return new o.DivIcon(t) }, o.Map.mergeOptions({ closePopupOnClick: !0 }), o.Popup = o.Layer.extend({ options: { pane: "popupPane", minWidth: 50, maxWidth: 300, offset: [0, 7], autoPan: !0, autoPanPadding: [5, 5], closeButton: !0, autoClose: !0, zoomAnimation: !0 }, initialize: function(t, e) { o.setOptions(this, t), this._source = e }, onAdd: function(t) { this._zoomAnimated = this._zoomAnimated && this.options.zoomAnimation, this._container || this._initLayout(), t._fadeAnimated && o.DomUtil.setOpacity(this._container, 0), clearTimeout(this._removeTimeout), this.getPane().appendChild(this._container), this.update(), t._fadeAnimated && o.DomUtil.setOpacity(this._container, 1), t.fire("popupopen", { popup: this }), this._source && this._source.fire("popupopen", { popup: this }, !0) }, openOn: function(t) { return t.openPopup(this), this }, onRemove: function(t) { t._fadeAnimated ? (o.DomUtil.setOpacity(this._container, 0), this._removeTimeout = setTimeout(o.bind(o.DomUtil.remove, o.DomUtil, this._container), 200)) : o.DomUtil.remove(this._container), t.fire("popupclose", { popup: this }), this._source && this._source.fire("popupclose", { popup: this }, !0) }, getLatLng: function() { return this._latlng }, setLatLng: function(t) { return this._latlng = o.latLng(t), this._map && (this._updatePosition(), this._adjustPan()), this }, getContent: function() { return this._content }, setContent: function(t) { return this._content = t, this.update(), this }, getElement: function() { return this._container }, update: function() { this._map && (this._container.style.visibility = "hidden", this._updateContent(), this._updateLayout(), this._updatePosition(), this._container.style.visibility = "", this._adjustPan()) }, getEvents: function() { var t = { zoom: this._updatePosition, viewreset: this._updatePosition }; return this._zoomAnimated && (t.zoomanim = this._animateZoom), ("closeOnClick" in this.options ? this.options.closeOnClick : this._map.options.closePopupOnClick) && (t.preclick = this._close), this.options.keepInView && (t.moveend = this._adjustPan), t }, isOpen: function() { return !!this._map && this._map.hasLayer(this) }, bringToFront: function() { return this._map && o.DomUtil.toFront(this._container), this }, bringToBack: function() { return this._map && o.DomUtil.toBack(this._container), this }, _close: function() { this._map && this._map.closePopup(this) }, _initLayout: function() { var t = "leaflet-popup", - e = this._container = o.DomUtil.create("div", t + " " + (this.options.className || "") + " leaflet-zoom-" + (this._zoomAnimated ? "animated" : "hide")); if (this.options.closeButton) { var i = this._closeButton = o.DomUtil.create("a", t + "-close-button", e); - i.href = "#close", i.innerHTML = "×", o.DomEvent.on(i, "click", this._onCloseButtonClick, this) } var n = this._wrapper = o.DomUtil.create("div", t + "-content-wrapper", e); - this._contentNode = o.DomUtil.create("div", t + "-content", n), o.DomEvent.disableClickPropagation(n).disableScrollPropagation(this._contentNode).on(n, "contextmenu", o.DomEvent.stopPropagation), this._tipContainer = o.DomUtil.create("div", t + "-tip-container", e), this._tip = o.DomUtil.create("div", t + "-tip", this._tipContainer) }, _updateContent: function() { if (this._content) { var t = this._contentNode, - e = "function" == typeof this._content ? this._content(this._source || this) : this._content; if ("string" == typeof e) t.innerHTML = e; - else { for (; t.hasChildNodes();) t.removeChild(t.firstChild); - t.appendChild(e) } - this.fire("contentupdate") } }, _updateLayout: function() { var t = this._contentNode, - e = t.style; - e.width = "", e.whiteSpace = "nowrap"; var i = t.offsetWidth; - i = Math.min(i, this.options.maxWidth), i = Math.max(i, this.options.minWidth), e.width = i + 1 + "px", e.whiteSpace = "", e.height = ""; var n = t.offsetHeight, - s = this.options.maxHeight, - r = "leaflet-popup-scrolled"; - s && n > s ? (e.height = s + "px", o.DomUtil.addClass(t, r)) : o.DomUtil.removeClass(t, r), this._containerWidth = this._container.offsetWidth }, _updatePosition: function() { if (this._map) { var t = this._map.latLngToLayerPoint(this._latlng), - e = o.point(this.options.offset); - this._zoomAnimated ? o.DomUtil.setPosition(this._container, t) : e = e.add(t); var i = this._containerBottom = -e.y, - n = this._containerLeft = -Math.round(this._containerWidth / 2) + e.x; - this._container.style.bottom = i + "px", this._container.style.left = n + "px" } }, _animateZoom: function(t) { var e = this._map._latLngToNewLayerPoint(this._latlng, t.zoom, t.center); - o.DomUtil.setPosition(this._container, e) }, _adjustPan: function() { if (!(!this.options.autoPan || this._map._panAnim && this._map._panAnim._inProgress)) { var t = this._map, - e = this._container.offsetHeight, - i = this._containerWidth, - n = new o.Point(this._containerLeft, -e - this._containerBottom); - this._zoomAnimated && n._add(o.DomUtil.getPosition(this._container)); var s = t.layerPointToContainerPoint(n), - r = o.point(this.options.autoPanPadding), - a = o.point(this.options.autoPanPaddingTopLeft || r), - h = o.point(this.options.autoPanPaddingBottomRight || r), - l = t.getSize(), - u = 0, - c = 0; - s.x + i + h.x > l.x && (u = s.x + i - l.x + h.x), s.x - u - a.x < 0 && (u = s.x - a.x), s.y + e + h.y > l.y && (c = s.y + e - l.y + h.y), s.y - c - a.y < 0 && (c = s.y - a.y), (u || c) && t.fire("autopanstart").panBy([u, c]) } }, _onCloseButtonClick: function(t) { this._close(), o.DomEvent.stop(t) } }), o.popup = function(t, e) { return new o.Popup(t, e) }, o.Map.include({ openPopup: function(t, e, i) { return t instanceof o.Popup || (t = new o.Popup(i).setContent(t)), e && t.setLatLng(e), this.hasLayer(t) ? this : (this._popup && this._popup.options.autoClose && this.closePopup(), this._popup = t, this.addLayer(t)) }, closePopup: function(t) { return t && t !== this._popup || (t = this._popup, this._popup = null), t && this.removeLayer(t), this } }), o.Layer.include({ bindPopup: function(t, e) { return t instanceof o.Popup ? (o.setOptions(t, e), this._popup = t, t._source = this) : ((!this._popup || e) && (this._popup = new o.Popup(e, this)), this._popup.setContent(t)), this._popupHandlersAdded || (this.on({ click: this._openPopup, remove: this.closePopup, move: this._movePopup }), this._popupHandlersAdded = !0), this._originalPopupOffset = this._popup.options.offset, this }, unbindPopup: function() { return this._popup && (this.off({ click: this._openPopup, remove: this.closePopup, move: this._movePopup }), this._popupHandlersAdded = !1, this._popup = null), this }, openPopup: function(t, e) { if (t instanceof o.Layer || (e = t, t = this), t instanceof o.FeatureGroup) - for (var i in this._layers) { t = this._layers[i]; break } - return e || (e = t.getCenter ? t.getCenter() : t.getLatLng()), this._popup && this._map && (this._popup.options.offset = this._popupAnchor(t), this._popup._source = t, this._popup.update(), this._map.openPopup(this._popup, e)), this }, closePopup: function() { return this._popup && this._popup._close(), this }, togglePopup: function(t) { return this._popup && (this._popup._map ? this.closePopup() : this.openPopup(t)), this }, isPopupOpen: function() { return this._popup.isOpen() }, setPopupContent: function(t) { return this._popup && this._popup.setContent(t), this }, getPopup: function() { return this._popup }, _openPopup: function(t) { var e = t.layer || t.target; if (this._popup && this._map) return e instanceof o.Path ? void this.openPopup(t.layer || t.target, t.latlng) : void(this._map.hasLayer(this._popup) && this._popup._source === e ? this.closePopup() : this.openPopup(e, t.latlng)) }, _popupAnchor: function(t) { var e = t._getPopupAnchor ? t._getPopupAnchor() : [0, 0], - i = this._originalPopupOffset || o.Popup.prototype.options.offset; return o.point(e).add(i) }, _movePopup: function(t) { this._popup.setLatLng(t.latlng) } }), o.Marker.include({ _getPopupAnchor: function() { return this.options.icon.options.popupAnchor || [0, 0] } }), o.LayerGroup = o.Layer.extend({ - initialize: function(t) { this._layers = {}; var e, i; if (t) - for (e = 0, i = t.length; i > e; e++) this.addLayer(t[e]) }, - addLayer: function(t) { var e = this.getLayerId(t); return this._layers[e] = t, this._map && this._map.addLayer(t), this }, - removeLayer: function(t) { - var e = t in this._layers ? t : this.getLayerId(t); - return this._map && this._layers[e] && this._map.removeLayer(this._layers[e]), - delete this._layers[e], this - }, - hasLayer: function(t) { return !!t && (t in this._layers || this.getLayerId(t) in this._layers) }, - clearLayers: function() { for (var t in this._layers) this.removeLayer(this._layers[t]); return this }, - invoke: function(t) { var e, i, n = Array.prototype.slice.call(arguments, 1); for (e in this._layers) i = this._layers[e], i[t] && i[t].apply(i, n); return this }, - onAdd: function(t) { for (var e in this._layers) t.addLayer(this._layers[e]) }, - onRemove: function(t) { for (var e in this._layers) t.removeLayer(this._layers[e]) }, - eachLayer: function(t, e) { for (var i in this._layers) t.call(e, this._layers[i]); return this }, - getLayer: function(t) { return this._layers[t] }, - getLayers: function() { var t = []; for (var e in this._layers) t.push(this._layers[e]); return t }, - setZIndex: function(t) { return this.invoke("setZIndex", t) }, - getLayerId: function(t) { return o.stamp(t) } - }), o.layerGroup = function(t) { return new o.LayerGroup(t) }, o.FeatureGroup = o.LayerGroup.extend({ addLayer: function(t) { return this.hasLayer(t) ? this : (t.addEventParent(this), o.LayerGroup.prototype.addLayer.call(this, t), this.fire("layeradd", { layer: t })) }, removeLayer: function(t) { return this.hasLayer(t) ? (t in this._layers && (t = this._layers[t]), t.removeEventParent(this), o.LayerGroup.prototype.removeLayer.call(this, t), this.fire("layerremove", { layer: t })) : this }, setStyle: function(t) { return this.invoke("setStyle", t) }, bringToFront: function() { return this.invoke("bringToFront") }, bringToBack: function() { return this.invoke("bringToBack") }, getBounds: function() { var t = new o.LatLngBounds; for (var e in this._layers) { var i = this._layers[e]; - t.extend(i.getBounds ? i.getBounds() : i.getLatLng()) } return t } }), o.featureGroup = function(t) { return new o.FeatureGroup(t) }, o.Renderer = o.Layer.extend({ options: { padding: .1 }, initialize: function(t) { o.setOptions(this, t), o.stamp(this) }, onAdd: function() { this._container || (this._initContainer(), this._zoomAnimated && o.DomUtil.addClass(this._container, "leaflet-zoom-animated")), this.getPane().appendChild(this._container), this._update() }, onRemove: function() { o.DomUtil.remove(this._container) }, getEvents: function() { var t = { viewreset: this._reset, zoomstart: this._onZoomStart, zoom: this._onZoom, moveend: this._update }; return this._zoomAnimated && (t.zoomanim = this._onAnimZoom), t }, _onAnimZoom: function(t) { this._updateTransform(t.center, t.zoom) }, _onZoom: function() { this._updateTransform(this._map.getCenter(), this._map.getZoom()) }, _onZoomStart: function() { this._update() }, _updateTransform: function(t, e) { var i = this._map.getZoomScale(e, this._zoom), - n = o.DomUtil.getPosition(this._container), - s = this._map.getSize().multiplyBy(.5 + this.options.padding), - r = this._map.project(this._center, e), - a = this._map.project(t, e), - h = a.subtract(r), - l = s.multiplyBy(-i).add(n).add(s).subtract(h); - o.DomUtil.setTransform(this._container, l, i) }, _reset: function() { this._update(), this._updateTransform(this._center, this._zoom) }, _update: function() { var t = this.options.padding, - e = this._map.getSize(), - i = this._map.containerPointToLayerPoint(e.multiplyBy(-t)).round(); - this._bounds = new o.Bounds(i, i.add(e.multiplyBy(1 + 2 * t)).round()), this._center = this._map.getCenter(), this._zoom = this._map.getZoom() } }), o.Map.include({ getRenderer: function(t) { var e = t.options.renderer || this._getPaneRenderer(t.options.pane) || this.options.renderer || this._renderer; return e || (e = this._renderer = this.options.preferCanvas && o.canvas() || o.svg()), this.hasLayer(e) || this.addLayer(e), e }, _getPaneRenderer: function(t) { if ("overlayPane" === t || t === i) return !1; var e = this._paneRenderers[t]; return e === i && (e = o.SVG && o.svg({ pane: t }) || o.Canvas && o.canvas({ pane: t }), this._paneRenderers[t] = e), e } }), o.Path = o.Layer.extend({ options: { stroke: !0, color: "#3388ff", weight: 3, opacity: 1, lineCap: "round", lineJoin: "round", fillOpacity: .2, fillRule: "evenodd", interactive: !0 }, beforeAdd: function(t) { this._renderer = t.getRenderer(this) }, onAdd: function() { this._renderer._initPath(this), this._reset(), this._renderer._addPath(this) }, onRemove: function() { this._renderer._removePath(this) }, getEvents: function() { return { zoomend: this._project, moveend: this._update, viewreset: this._reset } }, redraw: function() { return this._map && this._renderer._updatePath(this), this }, setStyle: function(t) { return o.setOptions(this, t), this._renderer && this._renderer._updateStyle(this), this }, bringToFront: function() { return this._renderer && this._renderer._bringToFront(this), this }, bringToBack: function() { return this._renderer && this._renderer._bringToBack(this), this }, getElement: function() { return this._path }, _reset: function() { this._project(), this._update() }, _clickTolerance: function() { return (this.options.stroke ? this.options.weight / 2 : 0) + (o.Browser.touch ? 10 : 0) } }), o.LineUtil = { simplify: function(t, e) { if (!e || !t.length) return t.slice(); var i = e * e; return t = this._reducePoints(t, i), t = this._simplifyDP(t, i) }, pointToSegmentDistance: function(t, e, i) { return Math.sqrt(this._sqClosestPointOnSegment(t, e, i, !0)) }, closestPointOnSegment: function(t, e, i) { return this._sqClosestPointOnSegment(t, e, i) }, _simplifyDP: function(t, e) { var n = t.length, - o = typeof Uint8Array != i + "" ? Uint8Array : Array, - s = new o(n); - s[0] = s[n - 1] = 1, this._simplifyDPStep(t, s, e, 0, n - 1); var r, a = []; for (r = 0; n > r; r++) s[r] && a.push(t[r]); return a }, _simplifyDPStep: function(t, e, i, n, o) { var s, r, a, h = 0; for (r = n + 1; o - 1 >= r; r++) a = this._sqClosestPointOnSegment(t[r], t[n], t[o], !0), a > h && (s = r, h = a); - h > i && (e[s] = 1, this._simplifyDPStep(t, e, i, n, s), this._simplifyDPStep(t, e, i, s, o)) }, _reducePoints: function(t, e) { for (var i = [t[0]], n = 1, o = 0, s = t.length; s > n; n++) this._sqDist(t[n], t[o]) > e && (i.push(t[n]), o = n); return s - 1 > o && i.push(t[s - 1]), i }, clipSegment: function(t, e, i, n, o) { var s, r, a, h = n ? this._lastCode : this._getBitCode(t, i), - l = this._getBitCode(e, i); for (this._lastCode = l;;) { if (!(h | l)) return [t, e]; if (h & l) return !1; - s = h || l, r = this._getEdgeIntersection(t, e, s, i, o), a = this._getBitCode(r, i), s === h ? (t = r, h = a) : (e = r, l = a) } }, _getEdgeIntersection: function(t, e, i, n, s) { var r, a, h = e.x - t.x, - l = e.y - t.y, - u = n.min, - c = n.max; return 8 & i ? (r = t.x + h * (c.y - t.y) / l, a = c.y) : 4 & i ? (r = t.x + h * (u.y - t.y) / l, a = u.y) : 2 & i ? (r = c.x, a = t.y + l * (c.x - t.x) / h) : 1 & i && (r = u.x, a = t.y + l * (u.x - t.x) / h), new o.Point(r, a, s) }, _getBitCode: function(t, e) { var i = 0; return t.x < e.min.x ? i |= 1 : t.x > e.max.x && (i |= 2), t.y < e.min.y ? i |= 4 : t.y > e.max.y && (i |= 8), i }, _sqDist: function(t, e) { var i = e.x - t.x, - n = e.y - t.y; return i * i + n * n }, _sqClosestPointOnSegment: function(t, e, i, n) { var s, r = e.x, - a = e.y, - h = i.x - r, - l = i.y - a, - u = h * h + l * l; return u > 0 && (s = ((t.x - r) * h + (t.y - a) * l) / u, s > 1 ? (r = i.x, a = i.y) : s > 0 && (r += h * s, a += l * s)), h = t.x - r, l = t.y - a, n ? h * h + l * l : new o.Point(r, a) } }, o.Polyline = o.Path.extend({ options: { smoothFactor: 1 }, initialize: function(t, e) { o.setOptions(this, e), this._setLatLngs(t) }, getLatLngs: function() { return this._latlngs }, setLatLngs: function(t) { return this._setLatLngs(t), this.redraw() }, isEmpty: function() { return !this._latlngs.length }, closestLayerPoint: function(t) { for (var e, i, n = 1 / 0, s = null, r = o.LineUtil._sqClosestPointOnSegment, a = 0, h = this._parts.length; h > a; a++) - for (var l = this._parts[a], u = 1, c = l.length; c > u; u++) { e = l[u - 1], i = l[u]; var d = r(t, e, i, !0); - n > d && (n = d, s = r(t, e, i)) } - return s && (s.distance = Math.sqrt(n)), s }, getCenter: function() { var t, e, i, n, o, s, r, a = this._rings[0], - h = a.length; if (!h) return null; for (t = 0, e = 0; h - 1 > t; t++) e += a[t].distanceTo(a[t + 1]) / 2; if (0 === e) return this._map.layerPointToLatLng(a[0]); for (t = 0, n = 0; h - 1 > t; t++) - if (o = a[t], s = a[t + 1], i = o.distanceTo(s), n += i, n > e) return r = (n - e) / i, this._map.layerPointToLatLng([s.x - r * (s.x - o.x), s.y - r * (s.y - o.y)]) }, getBounds: function() { return this._bounds }, addLatLng: function(t, e) { return e = e || this._defaultShape(), t = o.latLng(t), e.push(t), this._bounds.extend(t), this.redraw() }, _setLatLngs: function(t) { this._bounds = new o.LatLngBounds, this._latlngs = this._convertLatLngs(t) }, _defaultShape: function() { return o.Polyline._flat(this._latlngs) ? this._latlngs : this._latlngs[0] }, _convertLatLngs: function(t) { for (var e = [], i = o.Polyline._flat(t), n = 0, s = t.length; s > n; n++) i ? (e[n] = o.latLng(t[n]), this._bounds.extend(e[n])) : e[n] = this._convertLatLngs(t[n]); return e }, _project: function() { this._rings = [], this._projectLatlngs(this._latlngs, this._rings); var t = this._clickTolerance(), - e = new o.Point(t, -t); - this._bounds.isValid() && (this._pxBounds = new o.Bounds(this._map.latLngToLayerPoint(this._bounds.getSouthWest())._subtract(e), this._map.latLngToLayerPoint(this._bounds.getNorthEast())._add(e))) }, _projectLatlngs: function(t, e) { var i, n, s = t[0] instanceof o.LatLng, - r = t.length; if (s) { for (n = [], i = 0; r > i; i++) n[i] = this._map.latLngToLayerPoint(t[i]); - e.push(n) } else - for (i = 0; r > i; i++) this._projectLatlngs(t[i], e) }, _clipPoints: function() { var t = this._renderer._bounds; if (this._parts = [], this._pxBounds && this._pxBounds.intersects(t)) { if (this.options.noClip) return void(this._parts = this._rings); var e, i, n, s, r, a, h, l = this._parts; for (e = 0, n = 0, s = this._rings.length; s > e; e++) - for (h = this._rings[e], i = 0, r = h.length; r - 1 > i; i++) a = o.LineUtil.clipSegment(h[i], h[i + 1], t, i, !0), a && (l[n] = l[n] || [], l[n].push(a[0]), (a[1] !== h[i + 1] || i === r - 2) && (l[n].push(a[1]), n++)) } }, _simplifyPoints: function() { for (var t = this._parts, e = this.options.smoothFactor, i = 0, n = t.length; n > i; i++) t[i] = o.LineUtil.simplify(t[i], e) }, _update: function() { this._map && (this._clipPoints(), this._simplifyPoints(), this._updatePath()) }, _updatePath: function() { this._renderer._updatePoly(this) } }), o.polyline = function(t, e) { return new o.Polyline(t, e) }, o.Polyline._flat = function(t) { return !o.Util.isArray(t[0]) || "object" != typeof t[0][0] && "undefined" != typeof t[0][0] }, o.PolyUtil = {}, o.PolyUtil.clipPolygon = function(t, e, i) { var n, s, r, a, h, l, u, c, d, _ = [1, 4, 2, 8], - m = o.LineUtil; for (s = 0, u = t.length; u > s; s++) t[s]._code = m._getBitCode(t[s], e); for (a = 0; 4 > a; a++) { for (c = _[a], n = [], s = 0, u = t.length, r = u - 1; u > s; r = s++) h = t[s], l = t[r], h._code & c ? l._code & c || (d = m._getEdgeIntersection(l, h, c, e, i), d._code = m._getBitCode(d, e), n.push(d)) : (l._code & c && (d = m._getEdgeIntersection(l, h, c, e, i), d._code = m._getBitCode(d, e), n.push(d)), n.push(h)); - t = n } return t }, o.Polygon = o.Polyline.extend({ options: { fill: !0 }, isEmpty: function() { return !this._latlngs.length || !this._latlngs[0].length }, getCenter: function() { var t, e, i, n, o, s, r, a, h, l = this._rings[0], - u = l.length; if (!u) return null; for (s = r = a = 0, t = 0, e = u - 1; u > t; e = t++) i = l[t], n = l[e], o = i.y * n.x - n.y * i.x, r += (i.x + n.x) * o, a += (i.y + n.y) * o, s += 3 * o; return h = 0 === s ? l[0] : [r / s, a / s], this._map.layerPointToLatLng(h) }, _convertLatLngs: function(t) { var e = o.Polyline.prototype._convertLatLngs.call(this, t), - i = e.length; return i >= 2 && e[0] instanceof o.LatLng && e[0].equals(e[i - 1]) && e.pop(), e }, _setLatLngs: function(t) { o.Polyline.prototype._setLatLngs.call(this, t), o.Polyline._flat(this._latlngs) && (this._latlngs = [this._latlngs]) }, _defaultShape: function() { return o.Polyline._flat(this._latlngs[0]) ? this._latlngs[0] : this._latlngs[0][0] }, _clipPoints: function() { var t = this._renderer._bounds, - e = this.options.weight, - i = new o.Point(e, e); if (t = new o.Bounds(t.min.subtract(i), t.max.add(i)), this._parts = [], this._pxBounds && this._pxBounds.intersects(t)) { if (this.options.noClip) return void(this._parts = this._rings); for (var n, s = 0, r = this._rings.length; r > s; s++) n = o.PolyUtil.clipPolygon(this._rings[s], t, !0), n.length && this._parts.push(n) } }, _updatePath: function() { this._renderer._updatePoly(this, !0) } }), o.polygon = function(t, e) { return new o.Polygon(t, e) }, o.Rectangle = o.Polygon.extend({ initialize: function(t, e) { o.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(t), e) }, setBounds: function(t) { return this.setLatLngs(this._boundsToLatLngs(t)) }, _boundsToLatLngs: function(t) { return t = o.latLngBounds(t), [t.getSouthWest(), t.getNorthWest(), t.getNorthEast(), t.getSouthEast()] } }), o.rectangle = function(t, e) { return new o.Rectangle(t, e) }, o.CircleMarker = o.Path.extend({ options: { fill: !0, radius: 10 }, initialize: function(t, e) { o.setOptions(this, e), this._latlng = o.latLng(t), this._radius = this.options.radius }, setLatLng: function(t) { return this._latlng = o.latLng(t), this.redraw(), this.fire("move", { latlng: this._latlng }) }, getLatLng: function() { return this._latlng }, setRadius: function(t) { return this.options.radius = this._radius = t, this.redraw() }, getRadius: function() { return this._radius }, setStyle: function(t) { var e = t && t.radius || this._radius; return o.Path.prototype.setStyle.call(this, t), this.setRadius(e), this }, _project: function() { this._point = this._map.latLngToLayerPoint(this._latlng), this._updateBounds() }, _updateBounds: function() { var t = this._radius, - e = this._radiusY || t, - i = this._clickTolerance(), - n = [t + i, e + i]; - this._pxBounds = new o.Bounds(this._point.subtract(n), this._point.add(n)) }, _update: function() { this._map && this._updatePath() }, _updatePath: function() { this._renderer._updateCircle(this) }, _empty: function() { return this._radius && !this._renderer._bounds.intersects(this._pxBounds) } }), o.circleMarker = function(t, e) { return new o.CircleMarker(t, e) }, o.Circle = o.CircleMarker.extend({ initialize: function(t, e) { o.setOptions(this, e), this._latlng = o.latLng(t), this._mRadius = this.options.radius }, setRadius: function(t) { return this._mRadius = t, this.redraw() }, getRadius: function() { return this._mRadius }, getBounds: function() { var t = [this._radius, this._radiusY || this._radius]; return new o.LatLngBounds(this._map.layerPointToLatLng(this._point.subtract(t)), this._map.layerPointToLatLng(this._point.add(t))) }, setStyle: o.Path.prototype.setStyle, _project: function() { var t = this._latlng.lng, - e = this._latlng.lat, - i = this._map, - n = i.options.crs; if (n.distance === o.CRS.Earth.distance) { var s = Math.PI / 180, - r = this._mRadius / o.CRS.Earth.R / s, - a = i.project([e + r, t]), - h = i.project([e - r, t]), - l = a.add(h).divideBy(2), - u = i.unproject(l).lat, - c = Math.acos((Math.cos(r * s) - Math.sin(e * s) * Math.sin(u * s)) / (Math.cos(e * s) * Math.cos(u * s))) / s; - this._point = l.subtract(i.getPixelOrigin()), this._radius = isNaN(c) ? 0 : Math.max(Math.round(l.x - i.project([u, t - c]).x), 1), this._radiusY = Math.max(Math.round(l.y - a.y), 1) } else { var d = n.unproject(n.project(this._latlng).subtract([this._mRadius, 0])); - this._point = i.latLngToLayerPoint(this._latlng), this._radius = this._point.x - i.latLngToLayerPoint(d).x } - this._updateBounds() } }), o.circle = function(t, e, i) { return "number" == typeof e && (e = o.extend({}, i, { radius: e })), new o.Circle(t, e) }, o.SVG = o.Renderer.extend({ _initContainer: function() { this._container = o.SVG.create("svg"), this._container.setAttribute("pointer-events", "none"), this._rootGroup = o.SVG.create("g"), this._container.appendChild(this._rootGroup) }, _update: function() { if (!this._map._animatingZoom || !this._bounds) { o.Renderer.prototype._update.call(this); var t = this._bounds, - e = t.getSize(), - i = this._container; - this._svgSize && this._svgSize.equals(e) || (this._svgSize = e, i.setAttribute("width", e.x), i.setAttribute("height", e.y)), o.DomUtil.setPosition(i, t.min), i.setAttribute("viewBox", [t.min.x, t.min.y, e.x, e.y].join(" ")) } }, _initPath: function(t) { var e = t._path = o.SVG.create("path"); - t.options.className && o.DomUtil.addClass(e, t.options.className), t.options.interactive && o.DomUtil.addClass(e, "leaflet-interactive"), this._updateStyle(t) }, _addPath: function(t) { this._rootGroup.appendChild(t._path), t.addInteractiveTarget(t._path) }, _removePath: function(t) { o.DomUtil.remove(t._path), t.removeInteractiveTarget(t._path) }, _updatePath: function(t) { t._project(), t._update() }, _updateStyle: function(t) { var e = t._path, - i = t.options; - e && (i.stroke ? (e.setAttribute("stroke", i.color), e.setAttribute("stroke-opacity", i.opacity), e.setAttribute("stroke-width", i.weight), e.setAttribute("stroke-linecap", i.lineCap), e.setAttribute("stroke-linejoin", i.lineJoin), i.dashArray ? e.setAttribute("stroke-dasharray", i.dashArray) : e.removeAttribute("stroke-dasharray"), i.dashOffset ? e.setAttribute("stroke-dashoffset", i.dashOffset) : e.removeAttribute("stroke-dashoffset")) : e.setAttribute("stroke", "none"), i.fill ? (e.setAttribute("fill", i.fillColor || i.color), e.setAttribute("fill-opacity", i.fillOpacity), e.setAttribute("fill-rule", i.fillRule || "evenodd")) : e.setAttribute("fill", "none"), e.setAttribute("pointer-events", i.pointerEvents || (i.interactive ? "visiblePainted" : "none"))) }, _updatePoly: function(t, e) { this._setPath(t, o.SVG.pointsToPath(t._parts, e)) }, _updateCircle: function(t) { var e = t._point, - i = t._radius, - n = t._radiusY || i, - o = "a" + i + "," + n + " 0 1,0 ", - s = t._empty() ? "M0 0" : "M" + (e.x - i) + "," + e.y + o + 2 * i + ",0 " + o + 2 * -i + ",0 "; - this._setPath(t, s) }, _setPath: function(t, e) { t._path.setAttribute("d", e) }, _bringToFront: function(t) { o.DomUtil.toFront(t._path) }, _bringToBack: function(t) { o.DomUtil.toBack(t._path) } }), o.extend(o.SVG, { create: function(t) { return e.createElementNS("http://www.w3.org/2000/svg", t) }, pointsToPath: function(t, e) { var i, n, s, r, a, h, l = ""; for (i = 0, s = t.length; s > i; i++) { for (a = t[i], n = 0, r = a.length; r > n; n++) h = a[n], l += (n ? "L" : "M") + h.x + " " + h.y; - l += e ? o.Browser.svg ? "z" : "x" : "" } return l || "M0 0" } }), o.Browser.svg = !(!e.createElementNS || !o.SVG.create("svg").createSVGRect), o.svg = function(t) { return o.Browser.svg || o.Browser.vml ? new o.SVG(t) : null }, o.Browser.vml = !o.Browser.svg && function() { try { var t = e.createElement("div"); - t.innerHTML = ''; var i = t.firstChild; return i.style.behavior = "url(#default#VML)", i && "object" == typeof i.adj } catch (n) { return !1 } }(), o.SVG.include(o.Browser.vml ? { _initContainer: function() { this._container = o.DomUtil.create("div", "leaflet-vml-container") }, _update: function() { this._map._animatingZoom || o.Renderer.prototype._update.call(this) }, _initPath: function(t) { var e = t._container = o.SVG.create("shape"); - o.DomUtil.addClass(e, "leaflet-vml-shape " + (this.options.className || "")), e.coordsize = "1 1", t._path = o.SVG.create("path"), e.appendChild(t._path), this._updateStyle(t) }, _addPath: function(t) { var e = t._container; - this._container.appendChild(e), t.options.interactive && t.addInteractiveTarget(e) }, _removePath: function(t) { var e = t._container; - o.DomUtil.remove(e), t.removeInteractiveTarget(e) }, _updateStyle: function(t) { var e = t._stroke, - i = t._fill, - n = t.options, - s = t._container; - s.stroked = !!n.stroke, s.filled = !!n.fill, n.stroke ? (e || (e = t._stroke = o.SVG.create("stroke")), s.appendChild(e), e.weight = n.weight + "px", e.color = n.color, e.opacity = n.opacity, n.dashArray ? e.dashStyle = o.Util.isArray(n.dashArray) ? n.dashArray.join(" ") : n.dashArray.replace(/( *, *)/g, " ") : e.dashStyle = "", e.endcap = n.lineCap.replace("butt", "flat"), e.joinstyle = n.lineJoin) : e && (s.removeChild(e), t._stroke = null), n.fill ? (i || (i = t._fill = o.SVG.create("fill")), s.appendChild(i), i.color = n.fillColor || n.color, i.opacity = n.fillOpacity) : i && (s.removeChild(i), t._fill = null) }, _updateCircle: function(t) { var e = t._point.round(), - i = Math.round(t._radius), - n = Math.round(t._radiusY || i); - this._setPath(t, t._empty() ? "M0 0" : "AL " + e.x + "," + e.y + " " + i + "," + n + " 0,23592600") }, _setPath: function(t, e) { t._path.v = e }, _bringToFront: function(t) { o.DomUtil.toFront(t._container) }, _bringToBack: function(t) { o.DomUtil.toBack(t._container) } } : {}), o.Browser.vml && (o.SVG.create = function() { try { return e.namespaces.add("lvml", "urn:schemas-microsoft-com:vml"), - function(t) { return e.createElement("') } } catch (t) { return function(t) { return e.createElement("<" + t + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">') } } }()), o.Canvas = o.Renderer.extend({ onAdd: function() { o.Renderer.prototype.onAdd.call(this), this._layers = this._layers || {}, this._draw() }, _initContainer: function() { var t = this._container = e.createElement("canvas"); - o.DomEvent.on(t, "mousemove", o.Util.throttle(this._onMouseMove, 32, this), this).on(t, "click dblclick mousedown mouseup contextmenu", this._onClick, this).on(t, "mouseout", this._handleMouseOut, this), this._ctx = t.getContext("2d") }, _update: function() { if (!this._map._animatingZoom || !this._bounds) { this._drawnLayers = {}, o.Renderer.prototype._update.call(this); var t = this._bounds, - e = this._container, - i = t.getSize(), - n = o.Browser.retina ? 2 : 1; - o.DomUtil.setPosition(e, t.min), e.width = n * i.x, e.height = n * i.y, e.style.width = i.x + "px", e.style.height = i.y + "px", o.Browser.retina && this._ctx.scale(2, 2), this._ctx.translate(-t.min.x, -t.min.y) } }, _initPath: function(t) { this._layers[o.stamp(t)] = t }, _addPath: o.Util.falseFn, _removePath: function(t) { t._removed = !0, this._requestRedraw(t) }, _updatePath: function(t) { this._redrawBounds = t._pxBounds, this._draw(!0), t._project(), t._update(), this._draw(), this._redrawBounds = null }, _updateStyle: function(t) { this._requestRedraw(t) }, _requestRedraw: function(t) { if (this._map) { var e = (t.options.weight || 0) + 1; - this._redrawBounds = this._redrawBounds || new o.Bounds, this._redrawBounds.extend(t._pxBounds.min.subtract([e, e])), this._redrawBounds.extend(t._pxBounds.max.add([e, e])), this._redrawRequest = this._redrawRequest || o.Util.requestAnimFrame(this._redraw, this) } }, _redraw: function() { this._redrawRequest = null, this._draw(!0), this._draw(), this._redrawBounds = null }, _draw: function(t) { this._clear = t; var e, i = this._redrawBounds; - this._ctx.save(), i && (this._ctx.beginPath(), this._ctx.rect(i.min.x, i.min.y, i.max.x - i.min.x, i.max.y - i.min.y), this._ctx.clip()); for (var n in this._layers) e = this._layers[n], (!i || e._pxBounds.intersects(i)) && e._updatePath(), t && e._removed && (delete e._removed, delete this._layers[n]); - this._ctx.restore() }, _updatePoly: function(t, e) { var i, n, o, s, r = t._parts, - a = r.length, - h = this._ctx; if (a) { for (this._drawnLayers[t._leaflet_id] = t, h.beginPath(), i = 0; a > i; i++) { for (n = 0, o = r[i].length; o > n; n++) s = r[i][n], h[n ? "lineTo" : "moveTo"](s.x, s.y); - e && h.closePath() } - this._fillStroke(h, t) } }, _updateCircle: function(t) { if (!t._empty()) { var e = t._point, - i = this._ctx, - n = t._radius, - o = (t._radiusY || n) / n; - 1 !== o && (i.save(), i.scale(1, o)), i.beginPath(), i.arc(e.x, e.y / o, n, 0, 2 * Math.PI, !1), 1 !== o && i.restore(), this._fillStroke(i, t) } }, _fillStroke: function(t, e) { var i = this._clear, - n = e.options; - t.globalCompositeOperation = i ? "destination-out" : "source-over", n.fill && (t.globalAlpha = i ? 1 : n.fillOpacity, t.fillStyle = n.fillColor || n.color, t.fill(n.fillRule || "evenodd")), n.stroke && 0 !== n.weight && (t.globalAlpha = i ? 1 : n.opacity, e._prevWeight = t.lineWidth = i ? e._prevWeight + 1 : n.weight, t.strokeStyle = n.color, t.lineCap = n.lineCap, t.lineJoin = n.lineJoin, t.stroke()) }, _onClick: function(t) { var e = this._map.mouseEventToLayerPoint(t), - i = []; for (var n in this._layers) this._layers[n]._containsPoint(e) && (o.DomEvent._fakeStop(t), i.push(this._layers[n])); - i.length && this._fireEvent(i, t) }, _onMouseMove: function(t) { if (this._map && !this._map.dragging._draggable._moving && !this._map._animatingZoom) { var e = this._map.mouseEventToLayerPoint(t); - this._handleMouseOut(t, e), this._handleMouseHover(t, e) } }, _handleMouseOut: function(t, e) { var i = this._hoveredLayer;!i || "mouseout" !== t.type && i._containsPoint(e) || (o.DomUtil.removeClass(this._container, "leaflet-interactive"), this._fireEvent([i], t, "mouseout"), this._hoveredLayer = null) }, _handleMouseHover: function(t, e) { var i, n; if (!this._hoveredLayer) - for (i in this._drawnLayers) - if (n = this._drawnLayers[i], n.options.interactive && n._containsPoint(e)) { o.DomUtil.addClass(this._container, "leaflet-interactive"), this._fireEvent([n], t, "mouseover"), this._hoveredLayer = n; break } - this._hoveredLayer && this._fireEvent([this._hoveredLayer], t) }, _fireEvent: function(t, e, i) { this._map._fireDOMEvent(e, i || e.type, t) }, _bringToFront: o.Util.falseFn, _bringToBack: o.Util.falseFn }), o.Browser.canvas = function() { return !!e.createElement("canvas").getContext }(), o.canvas = function(t) { return o.Browser.canvas ? new o.Canvas(t) : null }, o.Polyline.prototype._containsPoint = function(t, e) { var i, n, s, r, a, h, l = this._clickTolerance(); if (!this._pxBounds.contains(t)) return !1; for (i = 0, r = this._parts.length; r > i; i++) - for (h = this._parts[i], n = 0, a = h.length, s = a - 1; a > n; s = n++) - if ((e || 0 !== n) && o.LineUtil.pointToSegmentDistance(t, h[s], h[n]) <= l) return !0; - return !1 }, o.Polygon.prototype._containsPoint = function(t) { var e, i, n, s, r, a, h, l, u = !1; if (!this._pxBounds.contains(t)) return !1; for (s = 0, h = this._parts.length; h > s; s++) - for (e = this._parts[s], r = 0, l = e.length, a = l - 1; l > r; a = r++) i = e[r], n = e[a], i.y > t.y != n.y > t.y && t.x < (n.x - i.x) * (t.y - i.y) / (n.y - i.y) + i.x && (u = !u); return u || o.Polyline.prototype._containsPoint.call(this, t, !0) }, o.CircleMarker.prototype._containsPoint = function(t) { return t.distanceTo(this._point) <= this._radius + this._clickTolerance() }, o.GeoJSON = o.FeatureGroup.extend({ initialize: function(t, e) { o.setOptions(this, e), this._layers = {}, t && this.addData(t) }, addData: function(t) { var e, i, n, s = o.Util.isArray(t) ? t : t.features; if (s) { for (e = 0, i = s.length; i > e; e++) n = s[e], (n.geometries || n.geometry || n.features || n.coordinates) && this.addData(n); return this } var r = this.options; if (r.filter && !r.filter(t)) return this; var a = o.GeoJSON.geometryToLayer(t, r); return a ? (a.feature = o.GeoJSON.asFeature(t), a.defaultOptions = a.options, this.resetStyle(a), r.onEachFeature && r.onEachFeature(t, a), this.addLayer(a)) : this }, resetStyle: function(t) { return t.options = t.defaultOptions, this._setLayerStyle(t, this.options.style), this }, setStyle: function(t) { return this.eachLayer(function(e) { this._setLayerStyle(e, t) }, this) }, _setLayerStyle: function(t, e) { "function" == typeof e && (e = e(t.feature)), t.setStyle && t.setStyle(e) } }), o.extend(o.GeoJSON, { geometryToLayer: function(t, e) { var i, n, s, r, a = "Feature" === t.type ? t.geometry : t, - h = a ? a.coordinates : null, - l = [], - u = e && e.pointToLayer, - c = e && e.coordsToLatLng || this.coordsToLatLng; if (!h && !a) return null; switch (a.type) { - case "Point": - return i = c(h), u ? u(t, i) : new o.Marker(i); - case "MultiPoint": - for (s = 0, r = h.length; r > s; s++) i = c(h[s]), l.push(u ? u(t, i) : new o.Marker(i)); return new o.FeatureGroup(l); - case "LineString": - case "MultiLineString": - return n = this.coordsToLatLngs(h, "LineString" === a.type ? 0 : 1, c), new o.Polyline(n, e); - case "Polygon": - case "MultiPolygon": - return n = this.coordsToLatLngs(h, "Polygon" === a.type ? 1 : 2, c), new o.Polygon(n, e); - case "GeometryCollection": - for (s = 0, r = a.geometries.length; r > s; s++) { var d = this.geometryToLayer({ geometry: a.geometries[s], type: "Feature", properties: t.properties }, e); - d && l.push(d) } return new o.FeatureGroup(l); - default: - throw new Error("Invalid GeoJSON object.") } }, coordsToLatLng: function(t) { return new o.LatLng(t[1], t[0], t[2]) }, coordsToLatLngs: function(t, e, i) { for (var n, o = [], s = 0, r = t.length; r > s; s++) n = e ? this.coordsToLatLngs(t[s], e - 1, i) : (i || this.coordsToLatLng)(t[s]), o.push(n); return o }, latLngToCoords: function(t) { return t.alt !== i ? [t.lng, t.lat, t.alt] : [t.lng, t.lat] }, latLngsToCoords: function(t, e, i) { for (var n = [], s = 0, r = t.length; r > s; s++) n.push(e ? o.GeoJSON.latLngsToCoords(t[s], e - 1, i) : o.GeoJSON.latLngToCoords(t[s])); return !e && i && n.push(n[0]), n }, getFeature: function(t, e) { return t.feature ? o.extend({}, t.feature, { geometry: e }) : o.GeoJSON.asFeature(e) }, asFeature: function(t) { return "Feature" === t.type ? t : { type: "Feature", properties: {}, geometry: t } } }); - var r = { toGeoJSON: function() { return o.GeoJSON.getFeature(this, { type: "Point", coordinates: o.GeoJSON.latLngToCoords(this.getLatLng()) }) } }; - o.Marker.include(r), o.Circle.include(r), o.CircleMarker.include(r), o.Polyline.prototype.toGeoJSON = function() { var t = !o.Polyline._flat(this._latlngs), - e = o.GeoJSON.latLngsToCoords(this._latlngs, t ? 1 : 0); return o.GeoJSON.getFeature(this, { type: (t ? "Multi" : "") + "LineString", coordinates: e }) }, o.Polygon.prototype.toGeoJSON = function() { var t = !o.Polyline._flat(this._latlngs), - e = t && !o.Polyline._flat(this._latlngs[0]), - i = o.GeoJSON.latLngsToCoords(this._latlngs, e ? 2 : t ? 1 : 0, !0); return t || (i = [i]), o.GeoJSON.getFeature(this, { type: (e ? "Multi" : "") + "Polygon", coordinates: i }) }, o.LayerGroup.include({ toMultiPoint: function() { var t = []; return this.eachLayer(function(e) { t.push(e.toGeoJSON().geometry.coordinates) }), o.GeoJSON.getFeature(this, { type: "MultiPoint", coordinates: t }) }, toGeoJSON: function() { var t = this.feature && this.feature.geometry && this.feature.geometry.type; if ("MultiPoint" === t) return this.toMultiPoint(); var e = "GeometryCollection" === t, - i = []; return this.eachLayer(function(t) { if (t.toGeoJSON) { var n = t.toGeoJSON(); - i.push(e ? n.geometry : o.GeoJSON.asFeature(n)) } }), e ? o.GeoJSON.getFeature(this, { geometries: i, type: "GeometryCollection" }) : { type: "FeatureCollection", features: i } } }), o.geoJson = function(t, e) { return new o.GeoJSON(t, e) }; - var a = "_leaflet_events"; - o.DomEvent = { on: function(t, e, i, n) { if ("object" == typeof e) - for (var s in e) this._on(t, s, e[s], i); - else { e = o.Util.splitWords(e); for (var r = 0, a = e.length; a > r; r++) this._on(t, e[r], i, n) } return this }, off: function(t, e, i, n) { if ("object" == typeof e) - for (var s in e) this._off(t, s, e[s], i); - else { e = o.Util.splitWords(e); for (var r = 0, a = e.length; a > r; r++) this._off(t, e[r], i, n) } return this }, _on: function(e, i, n, s) { var r = i + o.stamp(n) + (s ? "_" + o.stamp(s) : ""); if (e[a] && e[a][r]) return this; var h = function(i) { return n.call(s || e, i || t.event) }, - l = h; return o.Browser.pointer && 0 === i.indexOf("touch") ? this.addPointerListener(e, i, h, r) : o.Browser.touch && "dblclick" === i && this.addDoubleTapListener ? this.addDoubleTapListener(e, h, r) : "addEventListener" in e ? "mousewheel" === i ? (e.addEventListener("DOMMouseScroll", h, !1), e.addEventListener(i, h, !1)) : "mouseenter" === i || "mouseleave" === i ? (h = function(i) { i = i || t.event, o.DomEvent._isExternalTarget(e, i) && l(i) }, e.addEventListener("mouseenter" === i ? "mouseover" : "mouseout", h, !1)) : ("click" === i && o.Browser.android && (h = function(t) { return o.DomEvent._filterClick(t, l) }), e.addEventListener(i, h, !1)) : "attachEvent" in e && e.attachEvent("on" + i, h), e[a] = e[a] || {}, e[a][r] = h, this }, _off: function(t, e, i, n) { var s = e + o.stamp(i) + (n ? "_" + o.stamp(n) : ""), - r = t[a] && t[a][s]; return r ? (o.Browser.pointer && 0 === e.indexOf("touch") ? this.removePointerListener(t, e, s) : o.Browser.touch && "dblclick" === e && this.removeDoubleTapListener ? this.removeDoubleTapListener(t, s) : "removeEventListener" in t ? "mousewheel" === e ? (t.removeEventListener("DOMMouseScroll", r, !1), t.removeEventListener(e, r, !1)) : t.removeEventListener("mouseenter" === e ? "mouseover" : "mouseleave" === e ? "mouseout" : e, r, !1) : "detachEvent" in t && t.detachEvent("on" + e, r), t[a][s] = null, this) : this }, stopPropagation: function(t) { return t.stopPropagation ? t.stopPropagation() : t.originalEvent ? t.originalEvent._stopped = !0 : t.cancelBubble = !0, o.DomEvent._skipped(t), this }, disableScrollPropagation: function(t) { return o.DomEvent.on(t, "mousewheel MozMousePixelScroll", o.DomEvent.stopPropagation) }, disableClickPropagation: function(t) { var e = o.DomEvent.stopPropagation; return o.DomEvent.on(t, o.Draggable.START.join(" "), e), o.DomEvent.on(t, { click: o.DomEvent._fakeStop, dblclick: e }) }, preventDefault: function(t) { return t.preventDefault ? t.preventDefault() : t.returnValue = !1, this }, stop: function(t) { return o.DomEvent.preventDefault(t).stopPropagation(t) }, getMousePosition: function(t, e) { if (!e) return new o.Point(t.clientX, t.clientY); var i = e.getBoundingClientRect(); return new o.Point(t.clientX - i.left - e.clientLeft, t.clientY - i.top - e.clientTop) }, getWheelDelta: function(t) { var e = 0; return t.wheelDelta && (e = t.wheelDelta / 120), t.detail && (e = -t.detail / 3), e }, _skipEvents: {}, _fakeStop: function(t) { o.DomEvent._skipEvents[t.type] = !0 }, _skipped: function(t) { var e = this._skipEvents[t.type]; return this._skipEvents[t.type] = !1, e }, _isExternalTarget: function(t, e) { var i = e.relatedTarget; if (!i) return !0; try { for (; i && i !== t;) i = i.parentNode } catch (n) { return !1 } return i !== t }, _filterClick: function(t, e) { var i = t.timeStamp || t.originalEvent.timeStamp, - n = o.DomEvent._lastClick && i - o.DomEvent._lastClick; return n && n > 100 && 500 > n || t.target._simulatedClick && !t._simulated ? void o.DomEvent.stop(t) : (o.DomEvent._lastClick = i, void e(t)) } }, o.DomEvent.addListener = o.DomEvent.on, o.DomEvent.removeListener = o.DomEvent.off, o.Draggable = o.Evented.extend({ - statics: { START: o.Browser.touch ? ["touchstart", "mousedown"] : ["mousedown"], END: { mousedown: "mouseup", touchstart: "touchend", pointerdown: "touchend", MSPointerDown: "touchend" }, MOVE: { mousedown: "mousemove", touchstart: "touchmove", pointerdown: "touchmove", MSPointerDown: "touchmove" } }, - initialize: function(t, e, i) { this._element = t, this._dragStartTarget = e || t, this._preventOutline = i }, - enable: function() { this._enabled || (o.DomEvent.on(this._dragStartTarget, o.Draggable.START.join(" "), this._onDown, this), this._enabled = !0) }, - disable: function() { this._enabled && (o.DomEvent.off(this._dragStartTarget, o.Draggable.START.join(" "), this._onDown, this), this._enabled = !1, this._moved = !1) }, - _onDown: function(t) { if (this._moved = !1, !o.DomUtil.hasClass(this._element, "leaflet-zoom-anim") && !(o.Draggable._dragging || t.shiftKey || 1 !== t.which && 1 !== t.button && !t.touches) && this._enabled && (o.Draggable._dragging = !0, this._preventOutline && o.DomUtil.preventOutline(this._element), o.DomUtil.disableImageDrag(), o.DomUtil.disableTextSelection(), !this._moving)) { this.fire("down"); var i = t.touches ? t.touches[0] : t; - this._startPoint = new o.Point(i.clientX, i.clientY), this._startPos = this._newPos = o.DomUtil.getPosition(this._element), o.DomEvent.on(e, o.Draggable.MOVE[t.type], this._onMove, this).on(e, o.Draggable.END[t.type], this._onUp, this) } }, - _onMove: function(t) { - if (t.touches && t.touches.length > 1) return void(this._moved = !0); - var i = t.touches && 1 === t.touches.length ? t.touches[0] : t, - n = new o.Point(i.clientX, i.clientY), - s = n.subtract(this._startPoint); - (s.x || s.y) && (o.Browser.touch && Math.abs(s.x) + Math.abs(s.y) < 3 || (o.DomEvent.preventDefault(t), this._moved || (this.fire("dragstart"), this._moved = !0, this._startPos = o.DomUtil.getPosition(this._element).subtract(s), o.DomUtil.addClass(e.body, "leaflet-dragging"), this._lastTarget = t.target || t.srcElement, o.DomUtil.addClass(this._lastTarget, "leaflet-drag-target")), - this._newPos = this._startPos.add(s), this._moving = !0, o.Util.cancelAnimFrame(this._animRequest), this._lastEvent = t, this._animRequest = o.Util.requestAnimFrame(this._updatePosition, this, !0))) - }, - _updatePosition: function() { var t = { originalEvent: this._lastEvent }; - this.fire("predrag", t), o.DomUtil.setPosition(this._element, this._newPos), this.fire("drag", t) }, - _onUp: function() { o.DomUtil.removeClass(e.body, "leaflet-dragging"), this._lastTarget && (o.DomUtil.removeClass(this._lastTarget, "leaflet-drag-target"), this._lastTarget = null); for (var t in o.Draggable.MOVE) o.DomEvent.off(e, o.Draggable.MOVE[t], this._onMove, this).off(e, o.Draggable.END[t], this._onUp, this); - o.DomUtil.enableImageDrag(), o.DomUtil.enableTextSelection(), this._moved && this._moving && (o.Util.cancelAnimFrame(this._animRequest), this.fire("dragend", { distance: this._newPos.distanceTo(this._startPos) })), this._moving = !1, o.Draggable._dragging = !1 } - }), o.Handler = o.Class.extend({ initialize: function(t) { this._map = t }, enable: function() { this._enabled || (this._enabled = !0, this.addHooks()) }, disable: function() { this._enabled && (this._enabled = !1, this.removeHooks()) }, enabled: function() { return !!this._enabled } }), o.Map.mergeOptions({ dragging: !0, inertia: !o.Browser.android23, inertiaDeceleration: 3400, inertiaMaxSpeed: 1 / 0, easeLinearity: .2, worldCopyJump: !1 }), o.Map.Drag = o.Handler.extend({ addHooks: function() { if (!this._draggable) { var t = this._map; - this._draggable = new o.Draggable(t._mapPane, t._container), this._draggable.on({ down: this._onDown, dragstart: this._onDragStart, drag: this._onDrag, dragend: this._onDragEnd }, this), this._draggable.on("predrag", this._onPreDragLimit, this), t.options.worldCopyJump && (this._draggable.on("predrag", this._onPreDragWrap, this), t.on("zoomend", this._onZoomEnd, this), t.whenReady(this._onZoomEnd, this)) } - o.DomUtil.addClass(this._map._container, "leaflet-grab"), this._draggable.enable() }, removeHooks: function() { o.DomUtil.removeClass(this._map._container, "leaflet-grab"), this._draggable.disable() }, moved: function() { return this._draggable && this._draggable._moved }, _onDown: function() { this._map.stop() }, _onDragStart: function() { var t = this._map; if (this._map.options.maxBounds && this._map.options.maxBoundsViscosity) { var e = o.latLngBounds(this._map.options.maxBounds); - this._offsetLimit = o.bounds(this._map.latLngToContainerPoint(e.getNorthWest()).multiplyBy(-1), this._map.latLngToContainerPoint(e.getSouthEast()).multiplyBy(-1).add(this._map.getSize())), this._viscosity = Math.min(1, Math.max(0, this._map.options.maxBoundsViscosity)) } else this._offsetLimit = null; - t.fire("movestart").fire("dragstart"), t.options.inertia && (this._positions = [], this._times = []) }, _onDrag: function(t) { if (this._map.options.inertia) { var e = this._lastTime = +new Date, - i = this._lastPos = this._draggable._absPos || this._draggable._newPos; - this._positions.push(i), this._times.push(e), e - this._times[0] > 50 && (this._positions.shift(), this._times.shift()) } - this._map.fire("move", t).fire("drag", t) }, _onZoomEnd: function() { var t = this._map.getSize().divideBy(2), - e = this._map.latLngToLayerPoint([0, 0]); - this._initialWorldOffset = e.subtract(t).x, this._worldWidth = this._map.getPixelWorldBounds().getSize().x }, _viscousLimit: function(t, e) { return t - (t - e) * this._viscosity }, _onPreDragLimit: function() { if (this._viscosity && this._offsetLimit) { var t = this._draggable._newPos.subtract(this._draggable._startPos), - e = this._offsetLimit; - t.x < e.min.x && (t.x = this._viscousLimit(t.x, e.min.x)), t.y < e.min.y && (t.y = this._viscousLimit(t.y, e.min.y)), t.x > e.max.x && (t.x = this._viscousLimit(t.x, e.max.x)), t.y > e.max.y && (t.y = this._viscousLimit(t.y, e.max.y)), this._draggable._newPos = this._draggable._startPos.add(t) } }, _onPreDragWrap: function() { var t = this._worldWidth, - e = Math.round(t / 2), - i = this._initialWorldOffset, - n = this._draggable._newPos.x, - o = (n - e + i) % t + e - i, - s = (n + e + i) % t - e - i, - r = Math.abs(o + i) < Math.abs(s + i) ? o : s; - this._draggable._absPos = this._draggable._newPos.clone(), this._draggable._newPos.x = r }, _onDragEnd: function(t) { var e = this._map, - i = e.options, - n = !i.inertia || this._times.length < 2; if (e.fire("dragend", t), n) e.fire("moveend"); - else { var s = this._lastPos.subtract(this._positions[0]), - r = (this._lastTime - this._times[0]) / 1e3, - a = i.easeLinearity, - h = s.multiplyBy(a / r), - l = h.distanceTo([0, 0]), - u = Math.min(i.inertiaMaxSpeed, l), - c = h.multiplyBy(u / l), - d = u / (i.inertiaDeceleration * a), - _ = c.multiplyBy(-d / 2).round(); - _.x || _.y ? (_ = e._limitOffset(_, e.options.maxBounds), o.Util.requestAnimFrame(function() { e.panBy(_, { duration: d, easeLinearity: a, noMoveStart: !0, animate: !0 }) })) : e.fire("moveend") } } }), o.Map.addInitHook("addHandler", "dragging", o.Map.Drag), o.Map.mergeOptions({ doubleClickZoom: !0 }), o.Map.DoubleClickZoom = o.Handler.extend({ addHooks: function() { this._map.on("dblclick", this._onDoubleClick, this) }, removeHooks: function() { this._map.off("dblclick", this._onDoubleClick, this) }, _onDoubleClick: function(t) { var e = this._map, - i = e.getZoom(), - n = t.originalEvent.shiftKey ? Math.ceil(i) - 1 : Math.floor(i) + 1; "center" === e.options.doubleClickZoom ? e.setZoom(n) : e.setZoomAround(t.containerPoint, n) } }), o.Map.addInitHook("addHandler", "doubleClickZoom", o.Map.DoubleClickZoom), o.Map.mergeOptions({ scrollWheelZoom: !0, wheelDebounceTime: 40 }), o.Map.ScrollWheelZoom = o.Handler.extend({ addHooks: function() { o.DomEvent.on(this._map._container, { mousewheel: this._onWheelScroll, MozMousePixelScroll: o.DomEvent.preventDefault }, this), this._delta = 0 }, removeHooks: function() { o.DomEvent.off(this._map._container, { mousewheel: this._onWheelScroll, MozMousePixelScroll: o.DomEvent.preventDefault }, this) }, _onWheelScroll: function(t) { var e = o.DomEvent.getWheelDelta(t), - i = this._map.options.wheelDebounceTime; - this._delta += e, this._lastMousePos = this._map.mouseEventToContainerPoint(t), this._startTime || (this._startTime = +new Date); var n = Math.max(i - (+new Date - this._startTime), 0); - clearTimeout(this._timer), this._timer = setTimeout(o.bind(this._performZoom, this), n), o.DomEvent.stop(t) }, _performZoom: function() { var t = this._map, - e = this._delta, - i = t.getZoom(); - t.stop(), e = e > 0 ? Math.ceil(e) : Math.floor(e), e = Math.max(Math.min(e, 4), -4), e = t._limitZoom(i + e) - i, this._delta = 0, this._startTime = null, e && ("center" === t.options.scrollWheelZoom ? t.setZoom(i + e) : t.setZoomAround(this._lastMousePos, i + e)) } }), o.Map.addInitHook("addHandler", "scrollWheelZoom", o.Map.ScrollWheelZoom), o.extend(o.DomEvent, { _touchstart: o.Browser.msPointer ? "MSPointerDown" : o.Browser.pointer ? "pointerdown" : "touchstart", _touchend: o.Browser.msPointer ? "MSPointerUp" : o.Browser.pointer ? "pointerup" : "touchend", addDoubleTapListener: function(t, e, i) { - function n(t) { var e; if (e = o.Browser.pointer ? o.DomEvent._pointersCount : t.touches.length, !(e > 1)) { var i = Date.now(), - n = i - (r || i); - a = t.touches ? t.touches[0] : t, h = n > 0 && l >= n, r = i } } + function i(e) { + var i = +new Date(), + o = Math.max(0, 16 - (i - n)); + return (n = i + o), t.setTimeout(e, o); + } + var n = 0, + s = t.requestAnimationFrame || e("RequestAnimationFrame") || i, + r = + t.cancelAnimationFrame || + e("CancelAnimationFrame") || + e("CancelRequestAnimationFrame") || + function (e) { + t.clearTimeout(e); + }; + (o.Util.requestAnimFrame = function (e, n, r) { + return r && s === i ? void e.call(n) : s.call(t, o.bind(e, n)); + }), + (o.Util.cancelAnimFrame = function (e) { + e && r.call(t, e); + }); + })(), + (o.extend = o.Util.extend), + (o.bind = o.Util.bind), + (o.stamp = o.Util.stamp), + (o.setOptions = o.Util.setOptions), + (o.Class = function () {}), + (o.Class.extend = function (t) { + var e = function () { + this.initialize && this.initialize.apply(this, arguments), this.callInitHooks(); + }, + i = (e.__super__ = this.prototype), + n = o.Util.create(i); + (n.constructor = e), (e.prototype = n); + for (var s in this) this.hasOwnProperty(s) && "prototype" !== s && (e[s] = this[s]); + return ( + t.statics && (o.extend(e, t.statics), delete t.statics), + t.includes && (o.Util.extend.apply(null, [n].concat(t.includes)), delete t.includes), + n.options && (t.options = o.Util.extend(o.Util.create(n.options), t.options)), + o.extend(n, t), + (n._initHooks = []), + (n.callInitHooks = function () { + if (!this._initHooksCalled) { + i.callInitHooks && i.callInitHooks.call(this), (this._initHooksCalled = !0); + for (var t = 0, e = n._initHooks.length; e > t; t++) n._initHooks[t].call(this); + } + }), + e + ); + }), + (o.Class.include = function (t) { + o.extend(this.prototype, t); + }), + (o.Class.mergeOptions = function (t) { + o.extend(this.prototype.options, t); + }), + (o.Class.addInitHook = function (t) { + var e = Array.prototype.slice.call(arguments, 1), + i = + "function" == typeof t + ? t + : function () { + this[t].apply(this, e); + }; + (this.prototype._initHooks = this.prototype._initHooks || []), this.prototype._initHooks.push(i); + }), + (o.Evented = o.Class.extend({ + on: function (t, e, i) { + if ("object" == typeof t) for (var n in t) this._on(n, t[n], e); + else { + t = o.Util.splitWords(t); + for (var s = 0, r = t.length; r > s; s++) this._on(t[s], e, i); + } + return this; + }, + off: function (t, e, i) { + if (t) + if ("object" == typeof t) for (var n in t) this._off(n, t[n], e); + else { + t = o.Util.splitWords(t); + for (var s = 0, r = t.length; r > s; s++) this._off(t[s], e, i); + } + else delete this._events; + return this; + }, + _on: function (t, e, i) { + var n = (this._events = this._events || {}), + s = i && i !== this && o.stamp(i); + if (s) { + var r = t + "_idx", + a = t + "_len", + h = (n[r] = n[r] || {}), + l = o.stamp(e) + "_" + s; + h[l] || ((h[l] = { fn: e, ctx: i }), (n[a] = (n[a] || 0) + 1)); + } else (n[t] = n[t] || []), n[t].push({ fn: e }); + }, + _off: function (t, e, i) { + var n = this._events, + s = t + "_idx", + r = t + "_len"; + if (n) { + if (!e) return delete n[t], delete n[s], void delete n[r]; + var a, + h, + l, + u, + c, + d = i && i !== this && o.stamp(i); + if (d) + (c = o.stamp(e) + "_" + d), + (a = n[s]), + a && a[c] && ((u = a[c]), delete a[c], n[r]--); + else if ((a = n[t])) + for (h = 0, l = a.length; l > h; h++) + if (a[h].fn === e) { + (u = a[h]), a.splice(h, 1); + break; + } + u && (u.fn = o.Util.falseFn); + } + }, + fire: function (t, e, i) { + if (!this.listens(t, i)) return this; + var n = o.Util.extend({}, e, { type: t, target: this }), + s = this._events; + if (s) { + var r, + a, + h, + l, + u = s[t + "_idx"]; + if (s[t]) for (h = s[t].slice(), r = 0, a = h.length; a > r; r++) h[r].fn.call(this, n); + for (l in u) u[l].fn.call(u[l].ctx, n); + } + return i && this._propagateEvent(n), this; + }, + listens: function (t, e) { + var i = this._events; + if (i && (i[t] || i[t + "_len"])) return !0; + if (e) for (var n in this._eventParents) if (this._eventParents[n].listens(t, e)) return !0; + return !1; + }, + once: function (t, e, i) { + if ("object" == typeof t) { + for (var n in t) this.once(n, t[n], e); + return this; + } + var s = o.bind(function () { + this.off(t, e, i).off(t, s, i); + }, this); + return this.on(t, e, i).on(t, s, i); + }, + addEventParent: function (t) { + return ( + (this._eventParents = this._eventParents || {}), + (this._eventParents[o.stamp(t)] = t), + this + ); + }, + removeEventParent: function (t) { + return this._eventParents && delete this._eventParents[o.stamp(t)], this; + }, + _propagateEvent: function (t) { + for (var e in this._eventParents) + this._eventParents[e].fire(t.type, o.extend({ layer: t.target }, t), !0); + }, + })); + var s = o.Evented.prototype; + (s.addEventListener = s.on), + (s.removeEventListener = s.clearAllEventListeners = s.off), + (s.addOneTimeEventListener = s.once), + (s.fireEvent = s.fire), + (s.hasEventListeners = s.listens), + (o.Mixin = { Events: s }), + (function () { + var i = navigator.userAgent.toLowerCase(), + n = e.documentElement, + s = "ActiveXObject" in t, + r = -1 !== i.indexOf("webkit"), + a = -1 !== i.indexOf("phantom"), + h = -1 !== i.search("android [23]"), + l = -1 !== i.indexOf("chrome"), + u = -1 !== i.indexOf("gecko") && !r && !t.opera && !s, + c = "undefined" != typeof orientation || -1 !== i.indexOf("mobile"), + d = !t.PointerEvent && t.MSPointerEvent, + _ = (t.PointerEvent && navigator.pointerEnabled) || d, + m = s && "transition" in n.style, + p = "WebKitCSSMatrix" in t && "m11" in new t.WebKitCSSMatrix() && !h, + f = "MozPerspective" in n.style, + g = "OTransition" in n.style, + v = + !t.L_NO_TOUCH && + !a && + (_ || "ontouchstart" in t || (t.DocumentTouch && e instanceof t.DocumentTouch)); + o.Browser = { + ie: s, + ielt9: s && !e.addEventListener, + webkit: r, + gecko: u, + android: -1 !== i.indexOf("android"), + android23: h, + chrome: l, + safari: !l && -1 !== i.indexOf("safari"), + ie3d: m, + webkit3d: p, + gecko3d: f, + opera12: g, + any3d: !t.L_DISABLE_3D && (m || p || f) && !g && !a, + mobile: c, + mobileWebkit: c && r, + mobileWebkit3d: c && p, + mobileOpera: c && t.opera, + mobileGecko: c && u, + touch: !!v, + msPointer: !!d, + pointer: !!_, + retina: (t.devicePixelRatio || t.screen.deviceXDPI / t.screen.logicalXDPI) > 1, + }; + })(), + (o.Point = function (t, e, i) { + (this.x = i ? Math.round(t) : t), (this.y = i ? Math.round(e) : e); + }), + (o.Point.prototype = { + clone: function () { + return new o.Point(this.x, this.y); + }, + add: function (t) { + return this.clone()._add(o.point(t)); + }, + _add: function (t) { + return (this.x += t.x), (this.y += t.y), this; + }, + subtract: function (t) { + return this.clone()._subtract(o.point(t)); + }, + _subtract: function (t) { + return (this.x -= t.x), (this.y -= t.y), this; + }, + divideBy: function (t) { + return this.clone()._divideBy(t); + }, + _divideBy: function (t) { + return (this.x /= t), (this.y /= t), this; + }, + multiplyBy: function (t) { + return this.clone()._multiplyBy(t); + }, + _multiplyBy: function (t) { + return (this.x *= t), (this.y *= t), this; + }, + scaleBy: function (t) { + return new o.Point(this.x * t.x, this.y * t.y); + }, + unscaleBy: function (t) { + return new o.Point(this.x / t.x, this.y / t.y); + }, + round: function () { + return this.clone()._round(); + }, + _round: function () { + return (this.x = Math.round(this.x)), (this.y = Math.round(this.y)), this; + }, + floor: function () { + return this.clone()._floor(); + }, + _floor: function () { + return (this.x = Math.floor(this.x)), (this.y = Math.floor(this.y)), this; + }, + ceil: function () { + return this.clone()._ceil(); + }, + _ceil: function () { + return (this.x = Math.ceil(this.x)), (this.y = Math.ceil(this.y)), this; + }, + distanceTo: function (t) { + t = o.point(t); + var e = t.x - this.x, + i = t.y - this.y; + return Math.sqrt(e * e + i * i); + }, + equals: function (t) { + return (t = o.point(t)), t.x === this.x && t.y === this.y; + }, + contains: function (t) { + return ( + (t = o.point(t)), Math.abs(t.x) <= Math.abs(this.x) && Math.abs(t.y) <= Math.abs(this.y) + ); + }, + toString: function () { + return "Point(" + o.Util.formatNum(this.x) + ", " + o.Util.formatNum(this.y) + ")"; + }, + }), + (o.point = function (t, e, n) { + return t instanceof o.Point + ? t + : o.Util.isArray(t) + ? new o.Point(t[0], t[1]) + : t === i || null === t + ? t + : new o.Point(t, e, n); + }), + (o.Bounds = function (t, e) { + if (t) for (var i = e ? [t, e] : t, n = 0, o = i.length; o > n; n++) this.extend(i[n]); + }), + (o.Bounds.prototype = { + extend: function (t) { + return ( + (t = o.point(t)), + this.min || this.max + ? ((this.min.x = Math.min(t.x, this.min.x)), + (this.max.x = Math.max(t.x, this.max.x)), + (this.min.y = Math.min(t.y, this.min.y)), + (this.max.y = Math.max(t.y, this.max.y))) + : ((this.min = t.clone()), (this.max = t.clone())), + this + ); + }, + getCenter: function (t) { + return new o.Point((this.min.x + this.max.x) / 2, (this.min.y + this.max.y) / 2, t); + }, + getBottomLeft: function () { + return new o.Point(this.min.x, this.max.y); + }, + getTopRight: function () { + return new o.Point(this.max.x, this.min.y); + }, + getSize: function () { + return this.max.subtract(this.min); + }, + contains: function (t) { + var e, i; + return ( + (t = "number" == typeof t[0] || t instanceof o.Point ? o.point(t) : o.bounds(t)), + t instanceof o.Bounds ? ((e = t.min), (i = t.max)) : (e = i = t), + e.x >= this.min.x && i.x <= this.max.x && e.y >= this.min.y && i.y <= this.max.y + ); + }, + intersects: function (t) { + t = o.bounds(t); + var e = this.min, + i = this.max, + n = t.min, + s = t.max, + r = s.x >= e.x && n.x <= i.x, + a = s.y >= e.y && n.y <= i.y; + return r && a; + }, + overlaps: function (t) { + t = o.bounds(t); + var e = this.min, + i = this.max, + n = t.min, + s = t.max, + r = s.x > e.x && n.x < i.x, + a = s.y > e.y && n.y < i.y; + return r && a; + }, + isValid: function () { + return !(!this.min || !this.max); + }, + }), + (o.bounds = function (t, e) { + return !t || t instanceof o.Bounds ? t : new o.Bounds(t, e); + }), + (o.Transformation = function (t, e, i, n) { + (this._a = t), (this._b = e), (this._c = i), (this._d = n); + }), + (o.Transformation.prototype = { + transform: function (t, e) { + return this._transform(t.clone(), e); + }, + _transform: function (t, e) { + return ( + (e = e || 1), + (t.x = e * (this._a * t.x + this._b)), + (t.y = e * (this._c * t.y + this._d)), + t + ); + }, + untransform: function (t, e) { + return ( + (e = e || 1), new o.Point((t.x / e - this._b) / this._a, (t.y / e - this._d) / this._c) + ); + }, + }), + (o.DomUtil = { + get: function (t) { + return "string" == typeof t ? e.getElementById(t) : t; + }, + getStyle: function (t, i) { + var n = t.style[i] || (t.currentStyle && t.currentStyle[i]); + if ((!n || "auto" === n) && e.defaultView) { + var o = e.defaultView.getComputedStyle(t, null); + n = o ? o[i] : null; + } + return "auto" === n ? null : n; + }, + create: function (t, i, n) { + var o = e.createElement(t); + return (o.className = i), n && n.appendChild(o), o; + }, + remove: function (t) { + var e = t.parentNode; + e && e.removeChild(t); + }, + empty: function (t) { + for (; t.firstChild; ) t.removeChild(t.firstChild); + }, + toFront: function (t) { + t.parentNode.appendChild(t); + }, + toBack: function (t) { + var e = t.parentNode; + e.insertBefore(t, e.firstChild); + }, + hasClass: function (t, e) { + if (t.classList !== i) return t.classList.contains(e); + var n = o.DomUtil.getClass(t); + return n.length > 0 && new RegExp("(^|\\s)" + e + "(\\s|$)").test(n); + }, + addClass: function (t, e) { + if (t.classList !== i) + for (var n = o.Util.splitWords(e), s = 0, r = n.length; r > s; s++) t.classList.add(n[s]); + else if (!o.DomUtil.hasClass(t, e)) { + var a = o.DomUtil.getClass(t); + o.DomUtil.setClass(t, (a ? a + " " : "") + e); + } + }, + removeClass: function (t, e) { + t.classList !== i + ? t.classList.remove(e) + : o.DomUtil.setClass( + t, + o.Util.trim((" " + o.DomUtil.getClass(t) + " ").replace(" " + e + " ", " ")) + ); + }, + setClass: function (t, e) { + t.className.baseVal === i ? (t.className = e) : (t.className.baseVal = e); + }, + getClass: function (t) { + return t.className.baseVal === i ? t.className : t.className.baseVal; + }, + setOpacity: function (t, e) { + "opacity" in t.style + ? (t.style.opacity = e) + : "filter" in t.style && o.DomUtil._setOpacityIE(t, e); + }, + _setOpacityIE: function (t, e) { + var i = !1, + n = "DXImageTransform.Microsoft.Alpha"; + try { + i = t.filters.item(n); + } catch (o) { + if (1 === e) return; + } + (e = Math.round(100 * e)), + i + ? ((i.Enabled = 100 !== e), (i.Opacity = e)) + : (t.style.filter += " progid:" + n + "(opacity=" + e + ")"); + }, + testProp: function (t) { + for (var i = e.documentElement.style, n = 0; n < t.length; n++) if (t[n] in i) return t[n]; + return !1; + }, + setTransform: function (t, e, i) { + var n = e || new o.Point(0, 0); + t.style[o.DomUtil.TRANSFORM] = + (o.Browser.ie3d + ? "translate(" + n.x + "px," + n.y + "px)" + : "translate3d(" + n.x + "px," + n.y + "px,0)") + (i ? " scale(" + i + ")" : ""); + }, + setPosition: function (t, e) { + (t._leaflet_pos = e), + o.Browser.any3d + ? o.DomUtil.setTransform(t, e) + : ((t.style.left = e.x + "px"), (t.style.top = e.y + "px")); + }, + getPosition: function (t) { + return t._leaflet_pos; + }, + }), + (function () { + o.DomUtil.TRANSFORM = o.DomUtil.testProp([ + "transform", + "WebkitTransform", + "OTransform", + "MozTransform", + "msTransform", + ]); + var i = (o.DomUtil.TRANSITION = o.DomUtil.testProp([ + "webkitTransition", + "transition", + "OTransition", + "MozTransition", + "msTransition", + ])); + if ( + ((o.DomUtil.TRANSITION_END = + "webkitTransition" === i || "OTransition" === i ? i + "End" : "transitionend"), + "onselectstart" in e) + ) + (o.DomUtil.disableTextSelection = function () { + o.DomEvent.on(t, "selectstart", o.DomEvent.preventDefault); + }), + (o.DomUtil.enableTextSelection = function () { + o.DomEvent.off(t, "selectstart", o.DomEvent.preventDefault); + }); + else { + var n = o.DomUtil.testProp([ + "userSelect", + "WebkitUserSelect", + "OUserSelect", + "MozUserSelect", + "msUserSelect", + ]); + (o.DomUtil.disableTextSelection = function () { + if (n) { + var t = e.documentElement.style; + (this._userSelect = t[n]), (t[n] = "none"); + } + }), + (o.DomUtil.enableTextSelection = function () { + n && ((e.documentElement.style[n] = this._userSelect), delete this._userSelect); + }); + } + (o.DomUtil.disableImageDrag = function () { + o.DomEvent.on(t, "dragstart", o.DomEvent.preventDefault); + }), + (o.DomUtil.enableImageDrag = function () { + o.DomEvent.off(t, "dragstart", o.DomEvent.preventDefault); + }), + (o.DomUtil.preventOutline = function (e) { + for (; -1 === e.tabIndex; ) e = e.parentNode; + e && + e.style && + (o.DomUtil.restoreOutline(), + (this._outlineElement = e), + (this._outlineStyle = e.style.outline), + (e.style.outline = "none"), + o.DomEvent.on(t, "keydown", o.DomUtil.restoreOutline, this)); + }), + (o.DomUtil.restoreOutline = function () { + this._outlineElement && + ((this._outlineElement.style.outline = this._outlineStyle), + delete this._outlineElement, + delete this._outlineStyle, + o.DomEvent.off(t, "keydown", o.DomUtil.restoreOutline, this)); + }); + })(), + (o.LatLng = function (t, e, n) { + if (isNaN(t) || isNaN(e)) throw new Error("Invalid LatLng object: (" + t + ", " + e + ")"); + (this.lat = +t), (this.lng = +e), n !== i && (this.alt = +n); + }), + (o.LatLng.prototype = { + equals: function (t, e) { + if (!t) return !1; + t = o.latLng(t); + var n = Math.max(Math.abs(this.lat - t.lat), Math.abs(this.lng - t.lng)); + return (e === i ? 1e-9 : e) >= n; + }, + toString: function (t) { + return "LatLng(" + o.Util.formatNum(this.lat, t) + ", " + o.Util.formatNum(this.lng, t) + ")"; + }, + distanceTo: function (t) { + return o.CRS.Earth.distance(this, o.latLng(t)); + }, + wrap: function () { + return o.CRS.Earth.wrapLatLng(this); + }, + toBounds: function (t) { + var e = (180 * t) / 40075017, + i = e / Math.cos((Math.PI / 180) * this.lat); + return o.latLngBounds([this.lat - e, this.lng - i], [this.lat + e, this.lng + i]); + }, + clone: function () { + return new o.LatLng(this.lat, this.lng, this.alt); + }, + }), + (o.latLng = function (t, e, n) { + return t instanceof o.LatLng + ? t + : o.Util.isArray(t) && "object" != typeof t[0] + ? 3 === t.length + ? new o.LatLng(t[0], t[1], t[2]) + : 2 === t.length + ? new o.LatLng(t[0], t[1]) + : null + : t === i || null === t + ? t + : "object" == typeof t && "lat" in t + ? new o.LatLng(t.lat, "lng" in t ? t.lng : t.lon, t.alt) + : e === i + ? null + : new o.LatLng(t, e, n); + }), + (o.LatLngBounds = function (t, e) { + if (t) for (var i = e ? [t, e] : t, n = 0, o = i.length; o > n; n++) this.extend(i[n]); + }), + (o.LatLngBounds.prototype = { + extend: function (t) { + var e, + i, + n = this._southWest, + s = this._northEast; + if (t instanceof o.LatLng) (e = t), (i = t); + else { + if (!(t instanceof o.LatLngBounds)) + return t ? this.extend(o.latLng(t) || o.latLngBounds(t)) : this; + if (((e = t._southWest), (i = t._northEast), !e || !i)) return this; + } + return ( + n || s + ? ((n.lat = Math.min(e.lat, n.lat)), + (n.lng = Math.min(e.lng, n.lng)), + (s.lat = Math.max(i.lat, s.lat)), + (s.lng = Math.max(i.lng, s.lng))) + : ((this._southWest = new o.LatLng(e.lat, e.lng)), + (this._northEast = new o.LatLng(i.lat, i.lng))), + this + ); + }, + pad: function (t) { + var e = this._southWest, + i = this._northEast, + n = Math.abs(e.lat - i.lat) * t, + s = Math.abs(e.lng - i.lng) * t; + return new o.LatLngBounds( + new o.LatLng(e.lat - n, e.lng - s), + new o.LatLng(i.lat + n, i.lng + s) + ); + }, + getCenter: function () { + return new o.LatLng( + (this._southWest.lat + this._northEast.lat) / 2, + (this._southWest.lng + this._northEast.lng) / 2 + ); + }, + getSouthWest: function () { + return this._southWest; + }, + getNorthEast: function () { + return this._northEast; + }, + getNorthWest: function () { + return new o.LatLng(this.getNorth(), this.getWest()); + }, + getSouthEast: function () { + return new o.LatLng(this.getSouth(), this.getEast()); + }, + getWest: function () { + return this._southWest.lng; + }, + getSouth: function () { + return this._southWest.lat; + }, + getEast: function () { + return this._northEast.lng; + }, + getNorth: function () { + return this._northEast.lat; + }, + contains: function (t) { + t = "number" == typeof t[0] || t instanceof o.LatLng ? o.latLng(t) : o.latLngBounds(t); + var e, + i, + n = this._southWest, + s = this._northEast; + return ( + t instanceof o.LatLngBounds + ? ((e = t.getSouthWest()), (i = t.getNorthEast())) + : (e = i = t), + e.lat >= n.lat && i.lat <= s.lat && e.lng >= n.lng && i.lng <= s.lng + ); + }, + intersects: function (t) { + t = o.latLngBounds(t); + var e = this._southWest, + i = this._northEast, + n = t.getSouthWest(), + s = t.getNorthEast(), + r = s.lat >= e.lat && n.lat <= i.lat, + a = s.lng >= e.lng && n.lng <= i.lng; + return r && a; + }, + overlaps: function (t) { + t = o.latLngBounds(t); + var e = this._southWest, + i = this._northEast, + n = t.getSouthWest(), + s = t.getNorthEast(), + r = s.lat > e.lat && n.lat < i.lat, + a = s.lng > e.lng && n.lng < i.lng; + return r && a; + }, + toBBoxString: function () { + return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(","); + }, + equals: function (t) { + return t + ? ((t = o.latLngBounds(t)), + this._southWest.equals(t.getSouthWest()) && this._northEast.equals(t.getNorthEast())) + : !1; + }, + isValid: function () { + return !(!this._southWest || !this._northEast); + }, + }), + (o.latLngBounds = function (t, e) { + return !t || t instanceof o.LatLngBounds ? t : new o.LatLngBounds(t, e); + }), + (o.Projection = {}), + (o.Projection.LonLat = { + project: function (t) { + return new o.Point(t.lng, t.lat); + }, + unproject: function (t) { + return new o.LatLng(t.y, t.x); + }, + bounds: o.bounds([-180, -90], [180, 90]), + }), + (o.Projection.SphericalMercator = { + R: 6378137, + MAX_LATITUDE: 85.0511287798, + project: function (t) { + var e = Math.PI / 180, + i = this.MAX_LATITUDE, + n = Math.max(Math.min(i, t.lat), -i), + s = Math.sin(n * e); + return new o.Point(this.R * t.lng * e, (this.R * Math.log((1 + s) / (1 - s))) / 2); + }, + unproject: function (t) { + var e = 180 / Math.PI; + return new o.LatLng( + (2 * Math.atan(Math.exp(t.y / this.R)) - Math.PI / 2) * e, + (t.x * e) / this.R + ); + }, + bounds: (function () { + var t = 6378137 * Math.PI; + return o.bounds([-t, -t], [t, t]); + })(), + }), + (o.CRS = { + latLngToPoint: function (t, e) { + var i = this.projection.project(t), + n = this.scale(e); + return this.transformation._transform(i, n); + }, + pointToLatLng: function (t, e) { + var i = this.scale(e), + n = this.transformation.untransform(t, i); + return this.projection.unproject(n); + }, + project: function (t) { + return this.projection.project(t); + }, + unproject: function (t) { + return this.projection.unproject(t); + }, + scale: function (t) { + return 256 * Math.pow(2, t); + }, + zoom: function (t) { + return Math.log(t / 256) / Math.LN2; + }, + getProjectedBounds: function (t) { + if (this.infinite) return null; + var e = this.projection.bounds, + i = this.scale(t), + n = this.transformation.transform(e.min, i), + s = this.transformation.transform(e.max, i); + return o.bounds(n, s); + }, + wrapLatLng: function (t) { + var e = this.wrapLng ? o.Util.wrapNum(t.lng, this.wrapLng, !0) : t.lng, + i = this.wrapLat ? o.Util.wrapNum(t.lat, this.wrapLat, !0) : t.lat, + n = t.alt; + return o.latLng(i, e, n); + }, + }), + (o.CRS.Simple = o.extend({}, o.CRS, { + projection: o.Projection.LonLat, + transformation: new o.Transformation(1, 0, -1, 0), + scale: function (t) { + return Math.pow(2, t); + }, + zoom: function (t) { + return Math.log(t) / Math.LN2; + }, + distance: function (t, e) { + var i = e.lng - t.lng, + n = e.lat - t.lat; + return Math.sqrt(i * i + n * n); + }, + infinite: !0, + })), + (o.CRS.Earth = o.extend({}, o.CRS, { + wrapLng: [-180, 180], + R: 6378137, + distance: function (t, e) { + var i = Math.PI / 180, + n = t.lat * i, + o = e.lat * i, + s = Math.sin(n) * Math.sin(o) + Math.cos(n) * Math.cos(o) * Math.cos((e.lng - t.lng) * i); + return this.R * Math.acos(Math.min(s, 1)); + }, + })), + (o.CRS.EPSG3857 = o.extend({}, o.CRS.Earth, { + code: "EPSG:3857", + projection: o.Projection.SphericalMercator, + transformation: (function () { + var t = 0.5 / (Math.PI * o.Projection.SphericalMercator.R); + return new o.Transformation(t, 0.5, -t, 0.5); + })(), + })), + (o.CRS.EPSG900913 = o.extend({}, o.CRS.EPSG3857, { code: "EPSG:900913" })), + (o.CRS.EPSG4326 = o.extend({}, o.CRS.Earth, { + code: "EPSG:4326", + projection: o.Projection.LonLat, + transformation: new o.Transformation(1 / 180, 1, -1 / 180, 0.5), + })), + (o.Map = o.Evented.extend({ + options: { + crs: o.CRS.EPSG3857, + fadeAnimation: !0, + trackResize: !0, + markerZoomAnimation: !0, + maxBoundsViscosity: 0, + transform3DLimit: 8388608, + }, + initialize: function (t, e) { + (e = o.setOptions(this, e)), + this._initContainer(t), + this._initLayout(), + (this._onResize = o.bind(this._onResize, this)), + this._initEvents(), + e.maxBounds && this.setMaxBounds(e.maxBounds), + e.zoom !== i && (this._zoom = this._limitZoom(e.zoom)), + e.center && e.zoom !== i && this.setView(o.latLng(e.center), e.zoom, { reset: !0 }), + (this._handlers = []), + (this._layers = {}), + (this._zoomBoundLayers = {}), + (this._sizeChanged = !0), + this.callInitHooks(), + this._addLayers(this.options.layers); + }, + setView: function (t, e) { + return (e = e === i ? this.getZoom() : e), this._resetView(o.latLng(t), e), this; + }, + setZoom: function (t, e) { + return this._loaded + ? this.setView(this.getCenter(), t, { zoom: e }) + : ((this._zoom = t), this); + }, + zoomIn: function (t, e) { + return this.setZoom(this._zoom + (t || 1), e); + }, + zoomOut: function (t, e) { + return this.setZoom(this._zoom - (t || 1), e); + }, + setZoomAround: function (t, e, i) { + var n = this.getZoomScale(e), + s = this.getSize().divideBy(2), + r = t instanceof o.Point ? t : this.latLngToContainerPoint(t), + a = r.subtract(s).multiplyBy(1 - 1 / n), + h = this.containerPointToLatLng(s.add(a)); + return this.setView(h, e, { zoom: i }); + }, + _getBoundsCenterZoom: function (t, e) { + (e = e || {}), (t = t.getBounds ? t.getBounds() : o.latLngBounds(t)); + var i = o.point(e.paddingTopLeft || e.padding || [0, 0]), + n = o.point(e.paddingBottomRight || e.padding || [0, 0]), + s = this.getBoundsZoom(t, !1, i.add(n)); + s = e.maxZoom ? Math.min(e.maxZoom, s) : s; + var r = n.subtract(i).divideBy(2), + a = this.project(t.getSouthWest(), s), + h = this.project(t.getNorthEast(), s), + l = this.unproject(a.add(h).divideBy(2).add(r), s); + return { center: l, zoom: s }; + }, + fitBounds: function (t, e) { + var i = this._getBoundsCenterZoom(t, e); + return this.setView(i.center, i.zoom, e); + }, + fitWorld: function (t) { + return this.fitBounds( + [ + [-90, -180], + [90, 180], + ], + t + ); + }, + panTo: function (t, e) { + return this.setView(t, this._zoom, { pan: e }); + }, + panBy: function (t) { + return ( + this.fire("movestart"), + this._rawPanBy(o.point(t)), + this.fire("move"), + this.fire("moveend") + ); + }, + setMaxBounds: function (t) { + return (t = o.latLngBounds(t)) + ? (this.options.maxBounds && this.off("moveend", this._panInsideMaxBounds), + (this.options.maxBounds = t), + this._loaded && this._panInsideMaxBounds(), + this.on("moveend", this._panInsideMaxBounds)) + : this.off("moveend", this._panInsideMaxBounds); + }, + setMinZoom: function (t) { + return ( + (this.options.minZoom = t), + this._loaded && this.getZoom() < this.options.minZoom ? this.setZoom(t) : this + ); + }, + setMaxZoom: function (t) { + return ( + (this.options.maxZoom = t), + this._loaded && this.getZoom() > this.options.maxZoom ? this.setZoom(t) : this + ); + }, + panInsideBounds: function (t, e) { + this._enforcingBounds = !0; + var i = this.getCenter(), + n = this._limitCenter(i, this._zoom, o.latLngBounds(t)); + return i.equals(n) ? this : (this.panTo(n, e), (this._enforcingBounds = !1), this); + }, + invalidateSize: function (t) { + if (!this._loaded) return this; + t = o.extend({ animate: !1, pan: !0 }, t === !0 ? { animate: !0 } : t); + var e = this.getSize(); + (this._sizeChanged = !0), (this._lastCenter = null); + var i = this.getSize(), + n = e.divideBy(2).round(), + s = i.divideBy(2).round(), + r = n.subtract(s); + return r.x || r.y + ? (t.animate && t.pan + ? this.panBy(r) + : (t.pan && this._rawPanBy(r), + this.fire("move"), + t.debounceMoveend + ? (clearTimeout(this._sizeTimer), + (this._sizeTimer = setTimeout(o.bind(this.fire, this, "moveend"), 200))) + : this.fire("moveend")), + this.fire("resize", { oldSize: e, newSize: i })) + : this; + }, + stop: function () { + return o.Util.cancelAnimFrame(this._flyToFrame), this._panAnim && this._panAnim.stop(), this; + }, + addHandler: function (t, e) { + if (!e) return this; + var i = (this[t] = new e(this)); + return this._handlers.push(i), this.options[t] && i.enable(), this; + }, + remove: function () { + this._initEvents(!0); + try { + delete this._container._leaflet; + } catch (t) { + this._container._leaflet = i; + } + o.DomUtil.remove(this._mapPane), + this._clearControlPos && this._clearControlPos(), + this._clearHandlers(), + this._loaded && this.fire("unload"); + for (var e in this._layers) this._layers[e].remove(); + return this; + }, + createPane: function (t, e) { + var i = "leaflet-pane" + (t ? " leaflet-" + t.replace("Pane", "") + "-pane" : ""), + n = o.DomUtil.create("div", i, e || this._mapPane); + return t && (this._panes[t] = n), n; + }, + getCenter: function () { + return ( + this._checkIfLoaded(), + this._lastCenter && !this._moved() + ? this._lastCenter + : this.layerPointToLatLng(this._getCenterLayerPoint()) + ); + }, + getZoom: function () { + return this._zoom; + }, + getBounds: function () { + var t = this.getPixelBounds(), + e = this.unproject(t.getBottomLeft()), + i = this.unproject(t.getTopRight()); + return new o.LatLngBounds(e, i); + }, + getMinZoom: function () { + return this.options.minZoom === i ? this._layersMinZoom || 0 : this.options.minZoom; + }, + getMaxZoom: function () { + return this.options.maxZoom === i + ? this._layersMaxZoom === i + ? 1 / 0 + : this._layersMaxZoom + : this.options.maxZoom; + }, + getBoundsZoom: function (t, e, i) { + t = o.latLngBounds(t); + var n, + s = this.getMinZoom() - (e ? 1 : 0), + r = this.getMaxZoom(), + a = this.getSize(), + h = t.getNorthWest(), + l = t.getSouthEast(), + u = !0; + i = o.point(i || [0, 0]); + do + s++, + (n = this.project(l, s).subtract(this.project(h, s)).add(i).floor()), + (u = e ? n.x < a.x || n.y < a.y : a.contains(n)); + while (u && r >= s); + return u && e ? null : e ? s : s - 1; + }, + getSize: function () { + return ( + (!this._size || this._sizeChanged) && + ((this._size = new o.Point( + this._container.clientWidth, + this._container.clientHeight + )), + (this._sizeChanged = !1)), + this._size.clone() + ); + }, + getPixelBounds: function (t, e) { + var i = this._getTopLeftPoint(t, e); + return new o.Bounds(i, i.add(this.getSize())); + }, + getPixelOrigin: function () { + return this._checkIfLoaded(), this._pixelOrigin; + }, + getPixelWorldBounds: function (t) { + return this.options.crs.getProjectedBounds(t === i ? this.getZoom() : t); + }, + getPane: function (t) { + return "string" == typeof t ? this._panes[t] : t; + }, + getPanes: function () { + return this._panes; + }, + getContainer: function () { + return this._container; + }, + getZoomScale: function (t, e) { + var n = this.options.crs; + return (e = e === i ? this._zoom : e), n.scale(t) / n.scale(e); + }, + getScaleZoom: function (t, e) { + var n = this.options.crs; + return (e = e === i ? this._zoom : e), n.zoom(t * n.scale(e)); + }, + project: function (t, e) { + return (e = e === i ? this._zoom : e), this.options.crs.latLngToPoint(o.latLng(t), e); + }, + unproject: function (t, e) { + return (e = e === i ? this._zoom : e), this.options.crs.pointToLatLng(o.point(t), e); + }, + layerPointToLatLng: function (t) { + var e = o.point(t).add(this.getPixelOrigin()); + return this.unproject(e); + }, + latLngToLayerPoint: function (t) { + var e = this.project(o.latLng(t))._round(); + return e._subtract(this.getPixelOrigin()); + }, + wrapLatLng: function (t) { + return this.options.crs.wrapLatLng(o.latLng(t)); + }, + distance: function (t, e) { + return this.options.crs.distance(o.latLng(t), o.latLng(e)); + }, + containerPointToLayerPoint: function (t) { + return o.point(t).subtract(this._getMapPanePos()); + }, + layerPointToContainerPoint: function (t) { + return o.point(t).add(this._getMapPanePos()); + }, + containerPointToLatLng: function (t) { + var e = this.containerPointToLayerPoint(o.point(t)); + return this.layerPointToLatLng(e); + }, + latLngToContainerPoint: function (t) { + return this.layerPointToContainerPoint(this.latLngToLayerPoint(o.latLng(t))); + }, + mouseEventToContainerPoint: function (t) { + return o.DomEvent.getMousePosition(t, this._container); + }, + mouseEventToLayerPoint: function (t) { + return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t)); + }, + mouseEventToLatLng: function (t) { + return this.layerPointToLatLng(this.mouseEventToLayerPoint(t)); + }, + _initContainer: function (t) { + var e = (this._container = o.DomUtil.get(t)); + if (!e) throw new Error("Map container not found."); + if (e._leaflet) throw new Error("Map container is already initialized."); + o.DomEvent.addListener(e, "scroll", this._onScroll, this), (e._leaflet = !0); + }, + _initLayout: function () { + var t = this._container; + (this._fadeAnimated = this.options.fadeAnimation && o.Browser.any3d), + o.DomUtil.addClass( + t, + "leaflet-container" + + (o.Browser.touch ? " leaflet-touch" : "") + + (o.Browser.retina ? " leaflet-retina" : "") + + (o.Browser.ielt9 ? " leaflet-oldie" : "") + + (o.Browser.safari ? " leaflet-safari" : "") + + (this._fadeAnimated ? " leaflet-fade-anim" : "") + ); + var e = o.DomUtil.getStyle(t, "position"); + "absolute" !== e && "relative" !== e && "fixed" !== e && (t.style.position = "relative"), + this._initPanes(), + this._initControlPos && this._initControlPos(); + }, + _initPanes: function () { + var t = (this._panes = {}); + (this._paneRenderers = {}), + (this._mapPane = this.createPane("mapPane", this._container)), + o.DomUtil.setPosition(this._mapPane, new o.Point(0, 0)), + this.createPane("tilePane"), + this.createPane("shadowPane"), + this.createPane("overlayPane"), + this.createPane("markerPane"), + this.createPane("popupPane"), + this.options.markerZoomAnimation || + (o.DomUtil.addClass(t.markerPane, "leaflet-zoom-hide"), + o.DomUtil.addClass(t.shadowPane, "leaflet-zoom-hide")); + }, + _resetView: function (t, e) { + o.DomUtil.setPosition(this._mapPane, new o.Point(0, 0)); + var i = !this._loaded; + (this._loaded = !0), (e = this._limitZoom(e)); + var n = this._zoom !== e; + this._moveStart(n)._move(t, e)._moveEnd(n), this.fire("viewreset"), i && this.fire("load"); + }, + _moveStart: function (t) { + return t && this.fire("zoomstart"), this.fire("movestart"); + }, + _move: function (t, e, n) { + e === i && (e = this._zoom); + var o = this._zoom !== e; + return ( + (this._zoom = e), + (this._lastCenter = t), + (this._pixelOrigin = this._getNewPixelOrigin(t)), + o && this.fire("zoom", n), + this.fire("move", n) + ); + }, + _moveEnd: function (t) { + return t && this.fire("zoomend"), this.fire("moveend"); + }, + _rawPanBy: function (t) { + o.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(t)); + }, + _getZoomSpan: function () { + return this.getMaxZoom() - this.getMinZoom(); + }, + _panInsideMaxBounds: function () { + this._enforcingBounds || this.panInsideBounds(this.options.maxBounds); + }, + _checkIfLoaded: function () { + if (!this._loaded) throw new Error("Set map center and zoom first."); + }, + _initEvents: function (e) { + if (o.DomEvent) { + (this._targets = {}), (this._targets[o.stamp(this._container)] = this); + var i = e ? "off" : "on"; + o.DomEvent[i]( + this._container, + "click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress", + this._handleDOMEvent, + this + ), + this.options.trackResize && o.DomEvent[i](t, "resize", this._onResize, this), + o.Browser.any3d && + this.options.transform3DLimit && + this[i]("moveend", this._onMoveEnd); + } + }, + _onResize: function () { + o.Util.cancelAnimFrame(this._resizeRequest), + (this._resizeRequest = o.Util.requestAnimFrame(function () { + this.invalidateSize({ debounceMoveend: !0 }); + }, this)); + }, + _onScroll: function () { + (this._container.scrollTop = 0), (this._container.scrollLeft = 0); + }, + _onMoveEnd: function () { + var t = this._getMapPanePos(); + Math.max(Math.abs(t.x), Math.abs(t.y)) >= this.options.transform3DLimit && + this._resetView(this.getCenter(), this.getZoom()); + }, + _findEventTargets: function (t, e) { + for ( + var i, n = [], s = "mouseout" === e || "mouseover" === e, r = t.target || t.srcElement; + r; - function s() { if (h && !a.cancelBubble) { if (o.Browser.pointer) { var t, i, n = {}; for (i in a) t = a[i], n[i] = t && t.bind ? t.bind(a) : t; - a = n } - a.type = "dblclick", e(a), r = null } } var r, a, h = !1, - l = 250, - u = "_leaflet_", - c = this._touchstart, - d = this._touchend; return t[u + c + i] = n, t[u + d + i] = s, t.addEventListener(c, n, !1), t.addEventListener(d, s, !1), this }, removeDoubleTapListener: function(t, e) { var i = "_leaflet_", - n = t[i + this._touchend + e]; return t.removeEventListener(this._touchstart, t[i + this._touchstart + e], !1), t.removeEventListener(this._touchend, n, !1), this } }), o.extend(o.DomEvent, { POINTER_DOWN: o.Browser.msPointer ? "MSPointerDown" : "pointerdown", POINTER_MOVE: o.Browser.msPointer ? "MSPointerMove" : "pointermove", POINTER_UP: o.Browser.msPointer ? "MSPointerUp" : "pointerup", POINTER_CANCEL: o.Browser.msPointer ? "MSPointerCancel" : "pointercancel", _pointers: {}, _pointersCount: 0, addPointerListener: function(t, e, i, n) { return "touchstart" === e ? this._addPointerStart(t, i, n) : "touchmove" === e ? this._addPointerMove(t, i, n) : "touchend" === e && this._addPointerEnd(t, i, n), this }, removePointerListener: function(t, e, i) { var n = t["_leaflet_" + e + i]; return "touchstart" === e ? t.removeEventListener(this.POINTER_DOWN, n, !1) : "touchmove" === e ? t.removeEventListener(this.POINTER_MOVE, n, !1) : "touchend" === e && (t.removeEventListener(this.POINTER_UP, n, !1), t.removeEventListener(this.POINTER_CANCEL, n, !1)), this }, _addPointerStart: function(t, i, n) { var s = o.bind(function(t) { "mouse" !== t.pointerType && t.pointerType !== t.MSPOINTER_TYPE_MOUSE && o.DomEvent.preventDefault(t), this._handlePointer(t, i) }, this); if (t["_leaflet_touchstart" + n] = s, t.addEventListener(this.POINTER_DOWN, s, !1), !this._pointerDocListener) { var r = o.bind(this._globalPointerUp, this); - e.documentElement.addEventListener(this.POINTER_DOWN, o.bind(this._globalPointerDown, this), !0), e.documentElement.addEventListener(this.POINTER_MOVE, o.bind(this._globalPointerMove, this), !0), e.documentElement.addEventListener(this.POINTER_UP, r, !0), e.documentElement.addEventListener(this.POINTER_CANCEL, r, !0), this._pointerDocListener = !0 } }, _globalPointerDown: function(t) { this._pointers[t.pointerId] = t, this._pointersCount++ }, _globalPointerMove: function(t) { this._pointers[t.pointerId] && (this._pointers[t.pointerId] = t) }, _globalPointerUp: function(t) { delete this._pointers[t.pointerId], this._pointersCount-- }, _handlePointer: function(t, e) { t.touches = []; for (var i in this._pointers) t.touches.push(this._pointers[i]); - t.changedTouches = [t], e(t) }, _addPointerMove: function(t, e, i) { var n = o.bind(function(t) { - (t.pointerType !== t.MSPOINTER_TYPE_MOUSE && "mouse" !== t.pointerType || 0 !== t.buttons) && this._handlePointer(t, e) }, this); - t["_leaflet_touchmove" + i] = n, t.addEventListener(this.POINTER_MOVE, n, !1) }, _addPointerEnd: function(t, e, i) { var n = o.bind(function(t) { this._handlePointer(t, e) }, this); - t["_leaflet_touchend" + i] = n, t.addEventListener(this.POINTER_UP, n, !1), t.addEventListener(this.POINTER_CANCEL, n, !1) } }), o.Map.mergeOptions({ touchZoom: o.Browser.touch && !o.Browser.android23, bounceAtZoomLimits: !0 }), o.Map.TouchZoom = o.Handler.extend({ addHooks: function() { o.DomEvent.on(this._map._container, "touchstart", this._onTouchStart, this) }, removeHooks: function() { o.DomEvent.off(this._map._container, "touchstart", this._onTouchStart, this) }, _onTouchStart: function(t) { var i = this._map; if (t.touches && 2 === t.touches.length && !i._animatingZoom && !this._zooming) { var n = i.mouseEventToContainerPoint(t.touches[0]), - s = i.mouseEventToContainerPoint(t.touches[1]); - this._centerPoint = i.getSize()._divideBy(2), this._startLatLng = i.containerPointToLatLng(this._centerPoint), "center" !== i.options.touchZoom && (this._pinchStartLatLng = i.containerPointToLatLng(n.add(s)._divideBy(2))), this._startDist = n.distanceTo(s), this._startZoom = i.getZoom(), this._moved = !1, this._zooming = !0, i.stop(), o.DomEvent.on(e, "touchmove", this._onTouchMove, this).on(e, "touchend", this._onTouchEnd, this), o.DomEvent.preventDefault(t) } }, _onTouchMove: function(t) { if (t.touches && 2 === t.touches.length && this._zooming) { var e = this._map, - i = e.mouseEventToContainerPoint(t.touches[0]), - n = e.mouseEventToContainerPoint(t.touches[1]), - s = i.distanceTo(n) / this._startDist; if (this._zoom = e.getScaleZoom(s, this._startZoom), "center" === e.options.touchZoom) { if (this._center = this._startLatLng, 1 === s) return } else { var r = i._add(n)._divideBy(2)._subtract(this._centerPoint); if (1 === s && 0 === r.x && 0 === r.y) return; - this._center = e.unproject(e.project(this._pinchStartLatLng).subtract(r)) } if (e.options.bounceAtZoomLimits || !(this._zoom <= e.getMinZoom() && 1 > s || this._zoom >= e.getMaxZoom() && s > 1)) { this._moved || (e._moveStart(!0), this._moved = !0), o.Util.cancelAnimFrame(this._animRequest); var a = o.bind(e._move, e, this._center, this._zoom, { pinch: !0, round: !1 }); - this._animRequest = o.Util.requestAnimFrame(a, this, !0), o.DomEvent.preventDefault(t) } } }, _onTouchEnd: function() { if (!this._moved || !this._zooming) return void(this._zooming = !1); - this._zooming = !1, o.Util.cancelAnimFrame(this._animRequest), o.DomEvent.off(e, "touchmove", this._onTouchMove).off(e, "touchend", this._onTouchEnd); var t = this._zoom; - t = this._map._limitZoom(t - this._startZoom > 0 ? Math.ceil(t) : Math.floor(t)), this._map._animateZoom(this._center, t, !0, !0) } }), o.Map.addInitHook("addHandler", "touchZoom", o.Map.TouchZoom), o.Map.mergeOptions({ tap: !0, tapTolerance: 15 }), o.Map.Tap = o.Handler.extend({ addHooks: function() { o.DomEvent.on(this._map._container, "touchstart", this._onDown, this) }, removeHooks: function() { o.DomEvent.off(this._map._container, "touchstart", this._onDown, this) }, _onDown: function(t) { if (t.touches) { if (o.DomEvent.preventDefault(t), this._fireClick = !0, t.touches.length > 1) return this._fireClick = !1, void clearTimeout(this._holdTimeout); var i = t.touches[0], - n = i.target; - this._startPos = this._newPos = new o.Point(i.clientX, i.clientY), n.tagName && "a" === n.tagName.toLowerCase() && o.DomUtil.addClass(n, "leaflet-active"), this._holdTimeout = setTimeout(o.bind(function() { this._isTapValid() && (this._fireClick = !1, this._onUp(), this._simulateEvent("contextmenu", i)) }, this), 1e3), this._simulateEvent("mousedown", i), o.DomEvent.on(e, { touchmove: this._onMove, touchend: this._onUp }, this) } }, _onUp: function(t) { if (clearTimeout(this._holdTimeout), o.DomEvent.off(e, { touchmove: this._onMove, touchend: this._onUp }, this), this._fireClick && t && t.changedTouches) { var i = t.changedTouches[0], - n = i.target; - n && n.tagName && "a" === n.tagName.toLowerCase() && o.DomUtil.removeClass(n, "leaflet-active"), this._simulateEvent("mouseup", i), this._isTapValid() && this._simulateEvent("click", i) } }, _isTapValid: function() { return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance }, _onMove: function(t) { var e = t.touches[0]; - this._newPos = new o.Point(e.clientX, e.clientY), this._simulateEvent("mousemove", e) }, _simulateEvent: function(i, n) { var o = e.createEvent("MouseEvents"); - o._simulated = !0, n.target._simulatedClick = !0, o.initMouseEvent(i, !0, !0, t, 1, n.screenX, n.screenY, n.clientX, n.clientY, !1, !1, !1, !1, 0, null), n.target.dispatchEvent(o) } }), o.Browser.touch && !o.Browser.pointer && o.Map.addInitHook("addHandler", "tap", o.Map.Tap), o.Map.mergeOptions({ boxZoom: !0 }), o.Map.BoxZoom = o.Handler.extend({ initialize: function(t) { this._map = t, this._container = t._container, this._pane = t._panes.overlayPane }, addHooks: function() { o.DomEvent.on(this._container, "mousedown", this._onMouseDown, this) }, removeHooks: function() { o.DomEvent.off(this._container, "mousedown", this._onMouseDown, this) }, moved: function() { return this._moved }, _resetState: function() { this._moved = !1 }, _onMouseDown: function(t) { return !t.shiftKey || 1 !== t.which && 1 !== t.button ? !1 : (this._resetState(), o.DomUtil.disableTextSelection(), o.DomUtil.disableImageDrag(), this._startPoint = this._map.mouseEventToContainerPoint(t), void o.DomEvent.on(e, { contextmenu: o.DomEvent.stop, mousemove: this._onMouseMove, mouseup: this._onMouseUp, keydown: this._onKeyDown }, this)) }, _onMouseMove: function(t) { this._moved || (this._moved = !0, this._box = o.DomUtil.create("div", "leaflet-zoom-box", this._container), o.DomUtil.addClass(this._container, "leaflet-crosshair"), this._map.fire("boxzoomstart")), this._point = this._map.mouseEventToContainerPoint(t); var e = new o.Bounds(this._point, this._startPoint), - i = e.getSize(); - o.DomUtil.setPosition(this._box, e.min), this._box.style.width = i.x + "px", this._box.style.height = i.y + "px" }, _finish: function() { this._moved && (o.DomUtil.remove(this._box), o.DomUtil.removeClass(this._container, "leaflet-crosshair")), o.DomUtil.enableTextSelection(), o.DomUtil.enableImageDrag(), o.DomEvent.off(e, { contextmenu: o.DomEvent.stop, mousemove: this._onMouseMove, mouseup: this._onMouseUp, keydown: this._onKeyDown }, this) }, _onMouseUp: function(t) { if ((1 === t.which || 1 === t.button) && (this._finish(), this._moved)) { setTimeout(o.bind(this._resetState, this), 0); var e = new o.LatLngBounds(this._map.containerPointToLatLng(this._startPoint), this._map.containerPointToLatLng(this._point)); - this._map.fitBounds(e).fire("boxzoomend", { boxZoomBounds: e }) } }, _onKeyDown: function(t) { 27 === t.keyCode && this._finish() } }), o.Map.addInitHook("addHandler", "boxZoom", o.Map.BoxZoom), o.Map.mergeOptions({ keyboard: !0, keyboardPanOffset: 80, keyboardZoomOffset: 1 }), o.Map.Keyboard = o.Handler.extend({ keyCodes: { left: [37], right: [39], down: [40], up: [38], zoomIn: [187, 107, 61, 171], zoomOut: [189, 109, 54, 173] }, initialize: function(t) { this._map = t, this._setPanOffset(t.options.keyboardPanOffset), this._setZoomOffset(t.options.keyboardZoomOffset) }, addHooks: function() { var t = this._map._container; - t.tabIndex <= 0 && (t.tabIndex = "0"), o.DomEvent.on(t, { focus: this._onFocus, blur: this._onBlur, mousedown: this._onMouseDown }, this), this._map.on({ focus: this._addHooks, blur: this._removeHooks }, this) }, removeHooks: function() { this._removeHooks(), o.DomEvent.off(this._map._container, { focus: this._onFocus, blur: this._onBlur, mousedown: this._onMouseDown }, this), this._map.off({ focus: this._addHooks, blur: this._removeHooks }, this) }, _onMouseDown: function() { if (!this._focused) { var i = e.body, - n = e.documentElement, - o = i.scrollTop || n.scrollTop, - s = i.scrollLeft || n.scrollLeft; - this._map._container.focus(), t.scrollTo(s, o) } }, _onFocus: function() { this._focused = !0, this._map.fire("focus") }, _onBlur: function() { this._focused = !1, this._map.fire("blur") }, _setPanOffset: function(t) { var e, i, n = this._panKeys = {}, - o = this.keyCodes; for (e = 0, i = o.left.length; i > e; e++) n[o.left[e]] = [-1 * t, 0]; for (e = 0, i = o.right.length; i > e; e++) n[o.right[e]] = [t, 0]; for (e = 0, i = o.down.length; i > e; e++) n[o.down[e]] = [0, t]; for (e = 0, i = o.up.length; i > e; e++) n[o.up[e]] = [0, -1 * t] }, _setZoomOffset: function(t) { var e, i, n = this._zoomKeys = {}, - o = this.keyCodes; for (e = 0, i = o.zoomIn.length; i > e; e++) n[o.zoomIn[e]] = t; for (e = 0, i = o.zoomOut.length; i > e; e++) n[o.zoomOut[e]] = -t }, _addHooks: function() { o.DomEvent.on(e, "keydown", this._onKeyDown, this) }, _removeHooks: function() { o.DomEvent.off(e, "keydown", this._onKeyDown, this) }, _onKeyDown: function(t) { if (!(t.altKey || t.ctrlKey || t.metaKey)) { var e, i = t.keyCode, - n = this._map; if (i in this._panKeys) { if (n._panAnim && n._panAnim._inProgress) return; - e = this._panKeys[i], t.shiftKey && (e = o.point(e).multiplyBy(3)), n.panBy(e), n.options.maxBounds && n.panInsideBounds(n.options.maxBounds) } else if (i in this._zoomKeys) n.setZoom(n.getZoom() + (t.shiftKey ? 3 : 1) * this._zoomKeys[i]); - else { if (27 !== i) return; - n.closePopup() } - o.DomEvent.stop(t) } } }), o.Map.addInitHook("addHandler", "keyboard", o.Map.Keyboard), o.Handler.MarkerDrag = o.Handler.extend({ initialize: function(t) { this._marker = t }, addHooks: function() { var t = this._marker._icon; - this._draggable || (this._draggable = new o.Draggable(t, t, !0)), this._draggable.on({ dragstart: this._onDragStart, drag: this._onDrag, dragend: this._onDragEnd }, this).enable(), o.DomUtil.addClass(t, "leaflet-marker-draggable") }, removeHooks: function() { this._draggable.off({ dragstart: this._onDragStart, drag: this._onDrag, dragend: this._onDragEnd }, this).disable(), this._marker._icon && o.DomUtil.removeClass(this._marker._icon, "leaflet-marker-draggable") }, moved: function() { return this._draggable && this._draggable._moved }, _onDragStart: function() { this._marker.closePopup().fire("movestart").fire("dragstart") }, _onDrag: function(t) { var e = this._marker, - i = e._shadow, - n = o.DomUtil.getPosition(e._icon), - s = e._map.layerPointToLatLng(n); - i && o.DomUtil.setPosition(i, n), e._latlng = s, t.latlng = s, e.fire("move", t).fire("drag", t) }, _onDragEnd: function(t) { this._marker.fire("moveend").fire("dragend", t) } }), o.Control = o.Class.extend({ options: { position: "topright" }, initialize: function(t) { o.setOptions(this, t) }, getPosition: function() { return this.options.position }, setPosition: function(t) { var e = this._map; return e && e.removeControl(this), this.options.position = t, e && e.addControl(this), this }, getContainer: function() { return this._container }, addTo: function(t) { this.remove(), this._map = t; var e = this._container = this.onAdd(t), - i = this.getPosition(), - n = t._controlCorners[i]; return o.DomUtil.addClass(e, "leaflet-control"), -1 !== i.indexOf("bottom") ? n.insertBefore(e, n.firstChild) : n.appendChild(e), this }, remove: function() { return this._map ? (o.DomUtil.remove(this._container), this.onRemove && this.onRemove(this._map), this._map = null, this) : this }, _refocusOnMap: function(t) { this._map && t && t.screenX > 0 && t.screenY > 0 && this._map.getContainer().focus() } }), o.control = function(t) { return new o.Control(t) }, o.Map.include({ addControl: function(t) { return t.addTo(this), this }, removeControl: function(t) { return t.remove(), this }, _initControlPos: function() { - function t(t, s) { var r = i + t + " " + i + s; - e[t + s] = o.DomUtil.create("div", r, n) } var e = this._controlCorners = {}, - i = "leaflet-", - n = this._controlContainer = o.DomUtil.create("div", i + "control-container", this._container); - t("top", "left"), t("top", "right"), t("bottom", "left"), t("bottom", "right") }, _clearControlPos: function() { o.DomUtil.remove(this._controlContainer) } }), o.Control.Zoom = o.Control.extend({ options: { position: "topleft", zoomInText: "+", zoomInTitle: "Zoom in", zoomOutText: "-", zoomOutTitle: "Zoom out" }, onAdd: function(t) { var e = "leaflet-control-zoom", - i = o.DomUtil.create("div", e + " leaflet-bar"), - n = this.options; return this._zoomInButton = this._createButton(n.zoomInText, n.zoomInTitle, e + "-in", i, this._zoomIn), this._zoomOutButton = this._createButton(n.zoomOutText, n.zoomOutTitle, e + "-out", i, this._zoomOut), this._updateDisabled(), t.on("zoomend zoomlevelschange", this._updateDisabled, this), i }, onRemove: function(t) { t.off("zoomend zoomlevelschange", this._updateDisabled, this) }, disable: function() { return this._disabled = !0, this._updateDisabled(), this }, enable: function() { return this._disabled = !1, this._updateDisabled(), this }, _zoomIn: function(t) { this._disabled || this._map.zoomIn(t.shiftKey ? 3 : 1) }, _zoomOut: function(t) { this._disabled || this._map.zoomOut(t.shiftKey ? 3 : 1) }, _createButton: function(t, e, i, n, s) { var r = o.DomUtil.create("a", i, n); return r.innerHTML = t, r.href = "#", r.title = e, o.DomEvent.on(r, "mousedown dblclick", o.DomEvent.stopPropagation).on(r, "click", o.DomEvent.stop).on(r, "click", s, this).on(r, "click", this._refocusOnMap, this), r }, _updateDisabled: function() { var t = this._map, - e = "leaflet-disabled"; - o.DomUtil.removeClass(this._zoomInButton, e), o.DomUtil.removeClass(this._zoomOutButton, e), (this._disabled || t._zoom === t.getMinZoom()) && o.DomUtil.addClass(this._zoomOutButton, e), (this._disabled || t._zoom === t.getMaxZoom()) && o.DomUtil.addClass(this._zoomInButton, e) } }), o.Map.mergeOptions({ zoomControl: !0 }), o.Map.addInitHook(function() { this.options.zoomControl && (this.zoomControl = new o.Control.Zoom, this.addControl(this.zoomControl)) }), o.control.zoom = function(t) { return new o.Control.Zoom(t) }, o.Control.Attribution = o.Control.extend({ options: { position: "bottomright", prefix: 'Leaflet' }, initialize: function(t) { o.setOptions(this, t), this._attributions = {} }, onAdd: function(t) { this._container = o.DomUtil.create("div", "leaflet-control-attribution"), o.DomEvent && o.DomEvent.disableClickPropagation(this._container); for (var e in t._layers) t._layers[e].getAttribution && this.addAttribution(t._layers[e].getAttribution()); return this._update(), this._container }, setPrefix: function(t) { return this.options.prefix = t, this._update(), this }, addAttribution: function(t) { return t ? (this._attributions[t] || (this._attributions[t] = 0), this._attributions[t]++, this._update(), this) : this }, removeAttribution: function(t) { return t ? (this._attributions[t] && (this._attributions[t]--, this._update()), this) : this }, _update: function() { if (this._map) { var t = []; for (var e in this._attributions) this._attributions[e] && t.push(e); var i = []; - this.options.prefix && i.push(this.options.prefix), t.length && i.push(t.join(", ")), this._container.innerHTML = i.join(" | ") } } }), o.Map.mergeOptions({ attributionControl: !0 }), o.Map.addInitHook(function() { this.options.attributionControl && (this.attributionControl = (new o.Control.Attribution).addTo(this)) }), o.control.attribution = function(t) { return new o.Control.Attribution(t) }, o.Control.Scale = o.Control.extend({ options: { position: "bottomleft", maxWidth: 100, metric: !0, imperial: !0 }, onAdd: function(t) { var e = "leaflet-control-scale", - i = o.DomUtil.create("div", e), - n = this.options; return this._addScales(n, e + "-line", i), t.on(n.updateWhenIdle ? "moveend" : "move", this._update, this), t.whenReady(this._update, this), i }, onRemove: function(t) { t.off(this.options.updateWhenIdle ? "moveend" : "move", this._update, this) }, _addScales: function(t, e, i) { t.metric && (this._mScale = o.DomUtil.create("div", e, i)), t.imperial && (this._iScale = o.DomUtil.create("div", e, i)) }, _update: function() { var t = this._map, - e = t.getSize().y / 2, - i = t.distance(t.containerPointToLatLng([0, e]), t.containerPointToLatLng([this.options.maxWidth, e])); - this._updateScales(i) }, _updateScales: function(t) { this.options.metric && t && this._updateMetric(t), this.options.imperial && t && this._updateImperial(t) }, _updateMetric: function(t) { var e = this._getRoundNum(t), - i = 1e3 > e ? e + " m" : e / 1e3 + " km"; - this._updateScale(this._mScale, i, e / t) }, _updateImperial: function(t) { var e, i, n, o = 3.2808399 * t; - o > 5280 ? (e = o / 5280, i = this._getRoundNum(e), this._updateScale(this._iScale, i + " mi", i / e)) : (n = this._getRoundNum(o), this._updateScale(this._iScale, n + " ft", n / o)) }, _updateScale: function(t, e, i) { t.style.width = Math.round(this.options.maxWidth * i) + "px", t.innerHTML = e }, _getRoundNum: function(t) { var e = Math.pow(10, (Math.floor(t) + "").length - 1), - i = t / e; return i = i >= 10 ? 10 : i >= 5 ? 5 : i >= 3 ? 3 : i >= 2 ? 2 : 1, e * i } }), o.control.scale = function(t) { return new o.Control.Scale(t) }, o.Control.Layers = o.Control.extend({ options: { collapsed: !0, position: "topright", autoZIndex: !0, hideSingleBase: !1 }, initialize: function(t, e, i) { o.setOptions(this, i), this._layers = {}, this._lastZIndex = 0, this._handlingClick = !1; for (var n in t) this._addLayer(t[n], n); for (n in e) this._addLayer(e[n], n, !0) }, onAdd: function(t) { return this._initLayout(), this._update(), this._map = t, t.on("zoomend", this._checkDisabledLayers, this), this._container }, onRemove: function() { this._map.off("zoomend", this._checkDisabledLayers, this) }, addBaseLayer: function(t, e) { return this._addLayer(t, e), this._update() }, addOverlay: function(t, e) { return this._addLayer(t, e, !0), this._update() }, removeLayer: function(t) { return t.off("add remove", this._onLayerChange, this), delete this._layers[o.stamp(t)], this._update() }, _initLayout: function() { var t = "leaflet-control-layers", - e = this._container = o.DomUtil.create("div", t); - e.setAttribute("aria-haspopup", !0), o.DomEvent.disableClickPropagation(e), o.Browser.touch || o.DomEvent.disableScrollPropagation(e); var i = this._form = o.DomUtil.create("form", t + "-list"); if (this.options.collapsed) { o.Browser.android || o.DomEvent.on(e, { mouseenter: this._expand, mouseleave: this._collapse }, this); var n = this._layersLink = o.DomUtil.create("a", t + "-toggle", e); - n.href = "#", n.title = "Layers", o.Browser.touch ? o.DomEvent.on(n, "click", o.DomEvent.stop).on(n, "click", this._expand, this) : o.DomEvent.on(n, "focus", this._expand, this), o.DomEvent.on(i, "click", function() { setTimeout(o.bind(this._onInputClick, this), 0) }, this), this._map.on("click", this._collapse, this) } else this._expand(); - this._baseLayersList = o.DomUtil.create("div", t + "-base", i), this._separator = o.DomUtil.create("div", t + "-separator", i), this._overlaysList = o.DomUtil.create("div", t + "-overlays", i), e.appendChild(i) }, _addLayer: function(t, e, i) { t.on("add remove", this._onLayerChange, this); var n = o.stamp(t); - this._layers[n] = { layer: t, name: e, overlay: i }, this.options.autoZIndex && t.setZIndex && (this._lastZIndex++, t.setZIndex(this._lastZIndex)) }, _update: function() { if (!this._container) return this; - o.DomUtil.empty(this._baseLayersList), o.DomUtil.empty(this._overlaysList); var t, e, i, n, s = 0; for (i in this._layers) n = this._layers[i], this._addItem(n), e = e || n.overlay, t = t || !n.overlay, s += n.overlay ? 0 : 1; return this.options.hideSingleBase && (t = t && s > 1, this._baseLayersList.style.display = t ? "" : "none"), this._separator.style.display = e && t ? "" : "none", this }, _onLayerChange: function(t) { this._handlingClick || this._update(); var e = this._layers[o.stamp(t.target)], - i = e.overlay ? "add" === t.type ? "overlayadd" : "overlayremove" : "add" === t.type ? "baselayerchange" : null; - i && this._map.fire(i, e) }, _createRadioElement: function(t, i) { var n = '", - o = e.createElement("div"); return o.innerHTML = n, o.firstChild }, _addItem: function(t) { var i, n = e.createElement("label"), - s = this._map.hasLayer(t.layer); - t.overlay ? (i = e.createElement("input"), i.type = "checkbox", i.className = "leaflet-control-layers-selector", i.defaultChecked = s) : i = this._createRadioElement("leaflet-base-layers", s), i.layerId = o.stamp(t.layer), o.DomEvent.on(i, "click", this._onInputClick, this); var r = e.createElement("span"); - r.innerHTML = " " + t.name; var a = e.createElement("div"); - n.appendChild(a), a.appendChild(i), a.appendChild(r); var h = t.overlay ? this._overlaysList : this._baseLayersList; return h.appendChild(n), this._checkDisabledLayers(), n }, _onInputClick: function() { var t, e, i, n = this._form.getElementsByTagName("input"), - o = [], - s = []; - this._handlingClick = !0; for (var r = n.length - 1; r >= 0; r--) t = n[r], e = this._layers[t.layerId].layer, i = this._map.hasLayer(e), t.checked && !i ? o.push(e) : !t.checked && i && s.push(e); for (r = 0; r < s.length; r++) this._map.removeLayer(s[r]); for (r = 0; r < o.length; r++) this._map.addLayer(o[r]); - this._handlingClick = !1, this._refocusOnMap() }, _expand: function() { o.DomUtil.addClass(this._container, "leaflet-control-layers-expanded"), this._form.style.height = null; var t = this._map._size.y - (this._container.offsetTop + 50); - t < this._form.clientHeight ? (o.DomUtil.addClass(this._form, "leaflet-control-layers-scrollbar"), this._form.style.height = t + "px") : o.DomUtil.removeClass(this._form, "leaflet-control-layers-scrollbar"), this._checkDisabledLayers() }, _collapse: function() { o.DomUtil.removeClass(this._container, "leaflet-control-layers-expanded") }, _checkDisabledLayers: function() { for (var t, e, n = this._form.getElementsByTagName("input"), o = this._map.getZoom(), s = n.length - 1; s >= 0; s--) t = n[s], e = this._layers[t.layerId].layer, t.disabled = e.options.minZoom !== i && o < e.options.minZoom || e.options.maxZoom !== i && o > e.options.maxZoom } }), o.control.layers = function(t, e, i) { return new o.Control.Layers(t, e, i) }, o.PosAnimation = o.Evented.extend({ run: function(t, e, i, n) { this.stop(), this._el = t, this._inProgress = !0, this._duration = i || .25, this._easeOutPower = 1 / Math.max(n || .5, .2), this._startPos = o.DomUtil.getPosition(t), this._offset = e.subtract(this._startPos), this._startTime = +new Date, this.fire("start"), this._animate() }, stop: function() { this._inProgress && (this._step(!0), this._complete()) }, _animate: function() { this._animId = o.Util.requestAnimFrame(this._animate, this), this._step() }, _step: function(t) { var e = +new Date - this._startTime, - i = 1e3 * this._duration; - i > e ? this._runFrame(this._easeOut(e / i), t) : (this._runFrame(1), this._complete()) }, _runFrame: function(t, e) { var i = this._startPos.add(this._offset.multiplyBy(t)); - e && i._round(), o.DomUtil.setPosition(this._el, i), this.fire("step") }, _complete: function() { o.Util.cancelAnimFrame(this._animId), this._inProgress = !1, this.fire("end") }, _easeOut: function(t) { return 1 - Math.pow(1 - t, this._easeOutPower) } }), o.Map.include({ setView: function(t, e, n) { if (e = e === i ? this._zoom : this._limitZoom(e), t = this._limitCenter(o.latLng(t), e, this.options.maxBounds), n = n || {}, this.stop(), this._loaded && !n.reset && n !== !0) { n.animate !== i && (n.zoom = o.extend({ animate: n.animate }, n.zoom), n.pan = o.extend({ animate: n.animate, duration: n.duration }, n.pan)); var s = this._zoom !== e ? this._tryAnimatedZoom && this._tryAnimatedZoom(t, e, n.zoom) : this._tryAnimatedPan(t, n.pan); if (s) return clearTimeout(this._sizeTimer), this } return this._resetView(t, e), this }, panBy: function(t, e) { if (t = o.point(t).round(), e = e || {}, !t.x && !t.y) return this.fire("moveend"); if (e.animate !== !0 && !this.getSize().contains(t)) return this._resetView(this.unproject(this.project(this.getCenter()).add(t)), this.getZoom()), this; if (this._panAnim || (this._panAnim = new o.PosAnimation, this._panAnim.on({ step: this._onPanTransitionStep, end: this._onPanTransitionEnd }, this)), e.noMoveStart || this.fire("movestart"), e.animate !== !1) { o.DomUtil.addClass(this._mapPane, "leaflet-pan-anim"); var i = this._getMapPanePos().subtract(t); - this._panAnim.run(this._mapPane, i, e.duration || .25, e.easeLinearity) } else this._rawPanBy(t), this.fire("move").fire("moveend"); return this }, _onPanTransitionStep: function() { this.fire("move") }, _onPanTransitionEnd: function() { o.DomUtil.removeClass(this._mapPane, "leaflet-pan-anim"), this.fire("moveend") }, _tryAnimatedPan: function(t, e) { var i = this._getCenterOffset(t)._floor(); return (e && e.animate) === !0 || this.getSize().contains(i) ? (this.panBy(i, e), !0) : !1 } }), o.Map.mergeOptions({ zoomAnimation: !0, zoomAnimationThreshold: 4 }); - var h = o.DomUtil.TRANSITION && o.Browser.any3d && !o.Browser.mobileOpera; - h && o.Map.addInitHook(function() { this._zoomAnimated = this.options.zoomAnimation, this._zoomAnimated && (this._createAnimProxy(), o.DomEvent.on(this._proxy, o.DomUtil.TRANSITION_END, this._catchTransitionEnd, this)) }), o.Map.include(h ? { - _createAnimProxy: function() { - var t = this._proxy = o.DomUtil.create("div", "leaflet-proxy leaflet-zoom-animated"); - this._panes.mapPane.appendChild(t), this.on("zoomanim", function(e) { var i = o.DomUtil.TRANSFORM, - n = t.style[i]; - o.DomUtil.setTransform(t, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1)), n === t.style[i] && this._animatingZoom && this._onZoomTransitionEnd() }, this), this.on("load moveend", function() { - var e = this.getCenter(), - i = this.getZoom(); - o.DomUtil.setTransform(t, this.project(e, i), this.getZoomScale(i, 1)) - }, this) - }, - _catchTransitionEnd: function(t) { this._animatingZoom && t.propertyName.indexOf("transform") >= 0 && this._onZoomTransitionEnd() }, - _nothingToAnimate: function() { return !this._container.getElementsByClassName("leaflet-zoom-animated").length }, - _tryAnimatedZoom: function(t, e, i) { if (this._animatingZoom) return !0; if (i = i || {}, !this._zoomAnimated || i.animate === !1 || this._nothingToAnimate() || Math.abs(e - this._zoom) > this.options.zoomAnimationThreshold) return !1; var n = this.getZoomScale(e), - s = this._getCenterOffset(t)._divideBy(1 - 1 / n); return i.animate === !0 || this.getSize().contains(s) ? (o.Util.requestAnimFrame(function() { this._moveStart(!0)._animateZoom(t, e, !0) }, this), !0) : !1 }, - _animateZoom: function(t, e, i, n) { i && (this._animatingZoom = !0, this._animateToCenter = t, this._animateToZoom = e, o.DomUtil.addClass(this._mapPane, "leaflet-zoom-anim")), this.fire("zoomanim", { center: t, zoom: e, noUpdate: n }), setTimeout(o.bind(this._onZoomTransitionEnd, this), 250) }, - _onZoomTransitionEnd: function() { this._animatingZoom && (o.DomUtil.removeClass(this._mapPane, "leaflet-zoom-anim"), o.Util.requestAnimFrame(function() { this._animatingZoom = !1, this._move(this._animateToCenter, this._animateToZoom)._moveEnd(!0) }, this)) } - } : {}), o.Map.include({ flyTo: function(t, e, n) { - function s(t) { var e = (v * v - g * g + (t ? -1 : 1) * L * L * y * y) / (2 * (t ? v : g) * L * y); return Math.log(Math.sqrt(e * e + 1) - e) } + ) { + if (((i = this._targets[o.stamp(r)]), i && i.listens(e, !0))) { + if (s && !o.DomEvent._isExternalTarget(r, t)) break; + if ((n.push(i), s)) break; + } + if (r === this._container) break; + r = r.parentNode; + } + return n.length || s || !o.DomEvent._isExternalTarget(r, t) || (n = [this]), n; + }, + _handleDOMEvent: function (t) { + if (this._loaded && !o.DomEvent._skipped(t)) { + var e = "keypress" === t.type && 13 === t.keyCode ? "click" : t.type; + if ("click" === t.type) { + var i = o.Util.extend({}, t); + (i.type = "preclick"), this._handleDOMEvent(i); + } + "mousedown" === e && o.DomUtil.preventOutline(t.target || t.srcElement), + this._fireDOMEvent(t, e); + } + }, + _fireDOMEvent: function (t, e, i) { + if (!t._stopped && ((i = (i || []).concat(this._findEventTargets(t, e))), i.length)) { + var n = i[0]; + if ( + ("contextmenu" === e && n.listens(e, !0) && o.DomEvent.preventDefault(t), + ("click" !== t.type && "preclick" !== t.type) || + t._simulated || + !this._draggableMoved(n)) + ) { + var s = { originalEvent: t }; + if ("keypress" !== t.type) { + var r = n instanceof o.Marker; + (s.containerPoint = r + ? this.latLngToContainerPoint(n.getLatLng()) + : this.mouseEventToContainerPoint(t)), + (s.layerPoint = this.containerPointToLayerPoint(s.containerPoint)), + (s.latlng = r ? n.getLatLng() : this.layerPointToLatLng(s.layerPoint)); + } + for (var a = 0; a < i.length; a++) + if ( + (i[a].fire(e, s, !0), + s.originalEvent._stopped || + (i[a].options.nonBubblingEvents && + -1 !== o.Util.indexOf(i[a].options.nonBubblingEvents, e))) + ) + return; + } + } + }, + _draggableMoved: function (t) { + return ( + (t = t.options.draggable ? t : this), + (t.dragging && t.dragging.moved()) || (this.boxZoom && this.boxZoom.moved()) + ); + }, + _clearHandlers: function () { + for (var t = 0, e = this._handlers.length; e > t; t++) this._handlers[t].disable(); + }, + whenReady: function (t, e) { + return this._loaded ? t.call(e || this, { target: this }) : this.on("load", t, e), this; + }, + _getMapPanePos: function () { + return o.DomUtil.getPosition(this._mapPane) || new o.Point(0, 0); + }, + _moved: function () { + var t = this._getMapPanePos(); + return t && !t.equals([0, 0]); + }, + _getTopLeftPoint: function (t, e) { + var n = t && e !== i ? this._getNewPixelOrigin(t, e) : this.getPixelOrigin(); + return n.subtract(this._getMapPanePos()); + }, + _getNewPixelOrigin: function (t, e) { + var i = this.getSize()._divideBy(2); + return this.project(t, e)._subtract(i)._add(this._getMapPanePos())._round(); + }, + _latLngToNewLayerPoint: function (t, e, i) { + var n = this._getNewPixelOrigin(i, e); + return this.project(t, e)._subtract(n); + }, + _getCenterLayerPoint: function () { + return this.containerPointToLayerPoint(this.getSize()._divideBy(2)); + }, + _getCenterOffset: function (t) { + return this.latLngToLayerPoint(t).subtract(this._getCenterLayerPoint()); + }, + _limitCenter: function (t, e, i) { + if (!i) return t; + var n = this.project(t, e), + s = this.getSize().divideBy(2), + r = new o.Bounds(n.subtract(s), n.add(s)), + a = this._getBoundsOffset(r, i, e); + return this.unproject(n.add(a), e); + }, + _limitOffset: function (t, e) { + if (!e) return t; + var i = this.getPixelBounds(), + n = new o.Bounds(i.min.add(t), i.max.add(t)); + return t.add(this._getBoundsOffset(n, e)); + }, + _getBoundsOffset: function (t, e, i) { + var n = this.project(e.getNorthWest(), i).subtract(t.min), + s = this.project(e.getSouthEast(), i).subtract(t.max), + r = this._rebound(n.x, -s.x), + a = this._rebound(n.y, -s.y); + return new o.Point(r, a); + }, + _rebound: function (t, e) { + return t + e > 0 + ? Math.round(t - e) / 2 + : Math.max(0, Math.ceil(t)) - Math.max(0, Math.floor(e)); + }, + _limitZoom: function (t) { + var e = this.getMinZoom(), + i = this.getMaxZoom(); + return o.Browser.any3d || (t = Math.round(t)), Math.max(e, Math.min(i, t)); + }, + })), + (o.map = function (t, e) { + return new o.Map(t, e); + }), + (o.Layer = o.Evented.extend({ + options: { pane: "overlayPane", nonBubblingEvents: [] }, + addTo: function (t) { + return t.addLayer(this), this; + }, + remove: function () { + return this.removeFrom(this._map || this._mapToAdd); + }, + removeFrom: function (t) { + return t && t.removeLayer(this), this; + }, + getPane: function (t) { + return this._map.getPane(t ? this.options[t] || t : this.options.pane); + }, + addInteractiveTarget: function (t) { + return (this._map._targets[o.stamp(t)] = this), this; + }, + removeInteractiveTarget: function (t) { + return delete this._map._targets[o.stamp(t)], this; + }, + _layerAdd: function (t) { + var e = t.target; + e.hasLayer(this) && + ((this._map = e), + (this._zoomAnimated = e._zoomAnimated), + this.getEvents && e.on(this.getEvents(), this), + this.onAdd(e), + this.getAttribution && + this._map.attributionControl && + this._map.attributionControl.addAttribution(this.getAttribution()), + this.fire("add"), + e.fire("layeradd", { layer: this })); + }, + })), + o.Map.include({ + addLayer: function (t) { + var e = o.stamp(t); + return this._layers[e] + ? t + : ((this._layers[e] = t), + (t._mapToAdd = this), + t.beforeAdd && t.beforeAdd(this), + this.whenReady(t._layerAdd, t), + this); + }, + removeLayer: function (t) { + var e = o.stamp(t); + return this._layers[e] + ? (this._loaded && t.onRemove(this), + t.getAttribution && + this.attributionControl && + this.attributionControl.removeAttribution(t.getAttribution()), + t.getEvents && this.off(t.getEvents(), t), + delete this._layers[e], + this._loaded && (this.fire("layerremove", { layer: t }), t.fire("remove")), + (t._map = t._mapToAdd = null), + this) + : this; + }, + hasLayer: function (t) { + return !!t && o.stamp(t) in this._layers; + }, + eachLayer: function (t, e) { + for (var i in this._layers) t.call(e, this._layers[i]); + return this; + }, + _addLayers: function (t) { + t = t ? (o.Util.isArray(t) ? t : [t]) : []; + for (var e = 0, i = t.length; i > e; e++) this.addLayer(t[e]); + }, + _addZoomLimit: function (t) { + (isNaN(t.options.maxZoom) || !isNaN(t.options.minZoom)) && + ((this._zoomBoundLayers[o.stamp(t)] = t), this._updateZoomLevels()); + }, + _removeZoomLimit: function (t) { + var e = o.stamp(t); + this._zoomBoundLayers[e] && (delete this._zoomBoundLayers[e], this._updateZoomLevels()); + }, + _updateZoomLevels: function () { + var t = 1 / 0, + e = -(1 / 0), + n = this._getZoomSpan(); + for (var o in this._zoomBoundLayers) { + var s = this._zoomBoundLayers[o].options; + (t = s.minZoom === i ? t : Math.min(t, s.minZoom)), + (e = s.maxZoom === i ? e : Math.max(e, s.maxZoom)); + } + (this._layersMaxZoom = e === -(1 / 0) ? i : e), + (this._layersMinZoom = t === 1 / 0 ? i : t), + n !== this._getZoomSpan() && this.fire("zoomlevelschange"); + }, + }), + (o.Projection.Mercator = { + R: 6378137, + R_MINOR: 6356752.314245179, + bounds: o.bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]), + project: function (t) { + var e = Math.PI / 180, + i = this.R, + n = t.lat * e, + s = this.R_MINOR / i, + r = Math.sqrt(1 - s * s), + a = r * Math.sin(n), + h = Math.tan(Math.PI / 4 - n / 2) / Math.pow((1 - a) / (1 + a), r / 2); + return (n = -i * Math.log(Math.max(h, 1e-10))), new o.Point(t.lng * e * i, n); + }, + unproject: function (t) { + for ( + var e, + i = 180 / Math.PI, + n = this.R, + s = this.R_MINOR / n, + r = Math.sqrt(1 - s * s), + a = Math.exp(-t.y / n), + h = Math.PI / 2 - 2 * Math.atan(a), + l = 0, + u = 0.1; + 15 > l && Math.abs(u) > 1e-7; + l++ + ) + (e = r * Math.sin(h)), + (e = Math.pow((1 - e) / (1 + e), r / 2)), + (u = Math.PI / 2 - 2 * Math.atan(a * e) - h), + (h += u); + return new o.LatLng(h * i, (t.x * i) / n); + }, + }), + (o.CRS.EPSG3395 = o.extend({}, o.CRS.Earth, { + code: "EPSG:3395", + projection: o.Projection.Mercator, + transformation: (function () { + var t = 0.5 / (Math.PI * o.Projection.Mercator.R); + return new o.Transformation(t, 0.5, -t, 0.5); + })(), + })), + (o.GridLayer = o.Layer.extend({ + options: { + pane: "tilePane", + tileSize: 256, + opacity: 1, + zIndex: 1, + updateWhenIdle: o.Browser.mobile, + updateInterval: 200, + attribution: null, + bounds: null, + minZoom: 0, + }, + initialize: function (t) { + t = o.setOptions(this, t); + }, + onAdd: function () { + this._initContainer(), + (this._levels = {}), + (this._tiles = {}), + this._resetView(), + this._update(); + }, + beforeAdd: function (t) { + t._addZoomLimit(this); + }, + onRemove: function (t) { + o.DomUtil.remove(this._container), + t._removeZoomLimit(this), + (this._container = null), + (this._tileZoom = null); + }, + bringToFront: function () { + return this._map && (o.DomUtil.toFront(this._container), this._setAutoZIndex(Math.max)), this; + }, + bringToBack: function () { + return this._map && (o.DomUtil.toBack(this._container), this._setAutoZIndex(Math.min)), this; + }, + getAttribution: function () { + return this.options.attribution; + }, + getContainer: function () { + return this._container; + }, + setOpacity: function (t) { + return (this.options.opacity = t), this._updateOpacity(), this; + }, + setZIndex: function (t) { + return (this.options.zIndex = t), this._updateZIndex(), this; + }, + isLoading: function () { + return this._loading; + }, + redraw: function () { + return this._map && (this._removeAllTiles(), this._update()), this; + }, + getEvents: function () { + var t = { viewreset: this._resetAll, zoom: this._resetView, moveend: this._onMoveEnd }; + return ( + this.options.updateWhenIdle || + (this._onMove || + (this._onMove = o.Util.throttle( + this._onMoveEnd, + this.options.updateInterval, + this + )), + (t.move = this._onMove)), + this._zoomAnimated && (t.zoomanim = this._animateZoom), + t + ); + }, + createTile: function () { + return e.createElement("div"); + }, + getTileSize: function () { + var t = this.options.tileSize; + return t instanceof o.Point ? t : new o.Point(t, t); + }, + _updateZIndex: function () { + this._container && + this.options.zIndex !== i && + null !== this.options.zIndex && + (this._container.style.zIndex = this.options.zIndex); + }, + _setAutoZIndex: function (t) { + for ( + var e, i = this.getPane().children, n = -t(-(1 / 0), 1 / 0), o = 0, s = i.length; + s > o; + o++ + ) + (e = i[o].style.zIndex), i[o] !== this._container && e && (n = t(n, +e)); + isFinite(n) && ((this.options.zIndex = n + t(-1, 1)), this._updateZIndex()); + }, + _updateOpacity: function () { + if (this._map && !o.Browser.ielt9 && this._map._fadeAnimated) { + o.DomUtil.setOpacity(this._container, this.options.opacity); + var t = +new Date(), + e = !1, + i = !1; + for (var n in this._tiles) { + var s = this._tiles[n]; + if (s.current && s.loaded) { + var r = Math.min(1, (t - s.loaded) / 200); + o.DomUtil.setOpacity(s.el, r), + 1 > r ? (e = !0) : (s.active && (i = !0), (s.active = !0)); + } + } + i && !this._noPrune && this._pruneTiles(), + e && + (o.Util.cancelAnimFrame(this._fadeFrame), + (this._fadeFrame = o.Util.requestAnimFrame(this._updateOpacity, this))); + } + }, + _initContainer: function () { + this._container || + ((this._container = o.DomUtil.create("div", "leaflet-layer")), + this._updateZIndex(), + this.options.opacity < 1 && this._updateOpacity(), + this.getPane().appendChild(this._container)); + }, + _updateLevels: function () { + var t = this._tileZoom, + e = this.options.maxZoom; + for (var i in this._levels) + this._levels[i].el.children.length || i === t + ? (this._levels[i].el.style.zIndex = e - Math.abs(t - i)) + : (o.DomUtil.remove(this._levels[i].el), delete this._levels[i]); + var n = this._levels[t], + s = this._map; + return ( + n || + ((n = this._levels[t] = {}), + (n.el = o.DomUtil.create( + "div", + "leaflet-tile-container leaflet-zoom-animated", + this._container + )), + (n.el.style.zIndex = e), + (n.origin = s.project(s.unproject(s.getPixelOrigin()), t).round()), + (n.zoom = t), + this._setZoomTransform(n, s.getCenter(), s.getZoom()), + o.Util.falseFn(n.el.offsetWidth)), + (this._level = n), + n + ); + }, + _pruneTiles: function () { + var t, + e, + i = this._map.getZoom(); + if (i > this.options.maxZoom || i < this.options.minZoom) return this._removeAllTiles(); + for (t in this._tiles) (e = this._tiles[t]), (e.retain = e.current); + for (t in this._tiles) + if (((e = this._tiles[t]), e.current && !e.active)) { + var n = e.coords; + this._retainParent(n.x, n.y, n.z, n.z - 5) || + this._retainChildren(n.x, n.y, n.z, n.z + 2); + } + for (t in this._tiles) this._tiles[t].retain || this._removeTile(t); + }, + _removeAllTiles: function () { + for (var t in this._tiles) this._removeTile(t); + }, + _resetAll: function () { + for (var t in this._levels) o.DomUtil.remove(this._levels[t].el), delete this._levels[t]; + this._removeAllTiles(), (this._tileZoom = null), this._resetView(); + }, + _retainParent: function (t, e, i, n) { + var o = Math.floor(t / 2), + s = Math.floor(e / 2), + r = i - 1, + a = o + ":" + s + ":" + r, + h = this._tiles[a]; + return h && h.active + ? ((h.retain = !0), !0) + : (h && h.loaded && (h.retain = !0), r > n ? this._retainParent(o, s, r, n) : !1); + }, + _retainChildren: function (t, e, i, n) { + for (var o = 2 * t; 2 * t + 2 > o; o++) + for (var s = 2 * e; 2 * e + 2 > s; s++) { + var r = o + ":" + s + ":" + (i + 1), + a = this._tiles[r]; + a && a.active + ? (a.retain = !0) + : (a && a.loaded && (a.retain = !0), + n > i + 1 && this._retainChildren(o, s, i + 1, n)); + } + }, + _resetView: function (t) { + var e = t && (t.pinch || t.flyTo); + this._setView(this._map.getCenter(), this._map.getZoom(), e, e); + }, + _animateZoom: function (t) { + this._setView(t.center, t.zoom, !0, t.noUpdate); + }, + _setView: function (t, e, n, o) { + var s = Math.round(e); + ((this.options.maxZoom !== i && s > this.options.maxZoom) || + (this.options.minZoom !== i && s < this.options.minZoom)) && + (s = i); + var r = s !== this._tileZoom; + (!o || r) && + ((this._tileZoom = s), + this._abortLoading && this._abortLoading(), + this._updateLevels(), + this._resetGrid(), + s !== i && this._update(t), + n || this._pruneTiles(), + (this._noPrune = !!n)), + this._setZoomTransforms(t, e); + }, + _setZoomTransforms: function (t, e) { + for (var i in this._levels) this._setZoomTransform(this._levels[i], t, e); + }, + _setZoomTransform: function (t, e, i) { + var n = this._map.getZoomScale(i, t.zoom), + s = t.origin.multiplyBy(n).subtract(this._map._getNewPixelOrigin(e, i)).round(); + o.Browser.any3d ? o.DomUtil.setTransform(t.el, s, n) : o.DomUtil.setPosition(t.el, s); + }, + _resetGrid: function () { + var t = this._map, + e = t.options.crs, + i = (this._tileSize = this.getTileSize()), + n = this._tileZoom, + o = this._map.getPixelWorldBounds(this._tileZoom); + o && (this._globalTileRange = this._pxBoundsToTileRange(o)), + (this._wrapX = e.wrapLng && + !this.options.noWrap && [ + Math.floor(t.project([0, e.wrapLng[0]], n).x / i.x), + Math.ceil(t.project([0, e.wrapLng[1]], n).x / i.y), + ]), + (this._wrapY = e.wrapLat && + !this.options.noWrap && [ + Math.floor(t.project([e.wrapLat[0], 0], n).y / i.x), + Math.ceil(t.project([e.wrapLat[1], 0], n).y / i.y), + ]); + }, + _onMoveEnd: function () { + this._map && !this._map._animatingZoom && this._resetView(); + }, + _getTiledPixelBounds: function (t, e, i) { + var n = this._map, + s = n.getZoomScale(e, i), + r = n.project(t, i).floor(), + a = n.getSize().divideBy(2 * s); + return new o.Bounds(r.subtract(a), r.add(a)); + }, + _update: function (t) { + var n = this._map; + if (n) { + var s = n.getZoom(); + if ((t === i && (t = n.getCenter()), this._tileZoom !== i)) { + var r = this._getTiledPixelBounds(t, s, this._tileZoom), + a = this._pxBoundsToTileRange(r), + h = a.getCenter(), + l = []; + for (var u in this._tiles) this._tiles[u].current = !1; + if (Math.abs(s - this._tileZoom) > 1) return void this._setView(t, s); + for (var c = a.min.y; c <= a.max.y; c++) + for (var d = a.min.x; d <= a.max.x; d++) { + var _ = new o.Point(d, c); + if (((_.z = this._tileZoom), this._isValidTile(_))) { + var m = this._tiles[this._tileCoordsToKey(_)]; + m ? (m.current = !0) : l.push(_); + } + } + if ( + (l.sort(function (t, e) { + return t.distanceTo(h) - e.distanceTo(h); + }), + 0 !== l.length) + ) { + this._loading || ((this._loading = !0), this.fire("loading")); + var p = e.createDocumentFragment(); + for (d = 0; d < l.length; d++) this._addTile(l[d], p); + this._level.el.appendChild(p); + } + } + } + }, + _isValidTile: function (t) { + var e = this._map.options.crs; + if (!e.infinite) { + var i = this._globalTileRange; + if ( + (!e.wrapLng && (t.x < i.min.x || t.x > i.max.x)) || + (!e.wrapLat && (t.y < i.min.y || t.y > i.max.y)) + ) + return !1; + } + if (!this.options.bounds) return !0; + var n = this._tileCoordsToBounds(t); + return o.latLngBounds(this.options.bounds).overlaps(n); + }, + _keyToBounds: function (t) { + return this._tileCoordsToBounds(this._keyToTileCoords(t)); + }, + _tileCoordsToBounds: function (t) { + var e = this._map, + i = this.getTileSize(), + n = t.scaleBy(i), + s = n.add(i), + r = e.wrapLatLng(e.unproject(n, t.z)), + a = e.wrapLatLng(e.unproject(s, t.z)); + return new o.LatLngBounds(r, a); + }, + _tileCoordsToKey: function (t) { + return t.x + ":" + t.y + ":" + t.z; + }, + _keyToTileCoords: function (t) { + var e = t.split(":"), + i = new o.Point(+e[0], +e[1]); + return (i.z = +e[2]), i; + }, + _removeTile: function (t) { + var e = this._tiles[t]; + e && + (o.DomUtil.remove(e.el), + delete this._tiles[t], + this.fire("tileunload", { tile: e.el, coords: this._keyToTileCoords(t) })); + }, + _initTile: function (t) { + o.DomUtil.addClass(t, "leaflet-tile"); + var e = this.getTileSize(); + (t.style.width = e.x + "px"), + (t.style.height = e.y + "px"), + (t.onselectstart = o.Util.falseFn), + (t.onmousemove = o.Util.falseFn), + o.Browser.ielt9 && + this.options.opacity < 1 && + o.DomUtil.setOpacity(t, this.options.opacity), + o.Browser.android && + !o.Browser.android23 && + (t.style.WebkitBackfaceVisibility = "hidden"); + }, + _addTile: function (t, e) { + var i = this._getTilePos(t), + n = this._tileCoordsToKey(t), + s = this.createTile(this._wrapCoords(t), o.bind(this._tileReady, this, t)); + this._initTile(s), + this.createTile.length < 2 && + o.Util.requestAnimFrame(o.bind(this._tileReady, this, t, null, s)), + o.DomUtil.setPosition(s, i), + (this._tiles[n] = { el: s, coords: t, current: !0 }), + e.appendChild(s), + this.fire("tileloadstart", { tile: s, coords: t }); + }, + _tileReady: function (t, e, i) { + if (this._map) { + e && this.fire("tileerror", { error: e, tile: i, coords: t }); + var n = this._tileCoordsToKey(t); + (i = this._tiles[n]), + i && + ((i.loaded = +new Date()), + this._map._fadeAnimated + ? (o.DomUtil.setOpacity(i.el, 0), + o.Util.cancelAnimFrame(this._fadeFrame), + (this._fadeFrame = o.Util.requestAnimFrame(this._updateOpacity, this))) + : ((i.active = !0), this._pruneTiles()), + o.DomUtil.addClass(i.el, "leaflet-tile-loaded"), + this.fire("tileload", { tile: i.el, coords: t }), + this._noTilesToLoad() && ((this._loading = !1), this.fire("load"))); + } + }, + _getTilePos: function (t) { + return t.scaleBy(this.getTileSize()).subtract(this._level.origin); + }, + _wrapCoords: function (t) { + var e = new o.Point( + this._wrapX ? o.Util.wrapNum(t.x, this._wrapX) : t.x, + this._wrapY ? o.Util.wrapNum(t.y, this._wrapY) : t.y + ); + return (e.z = t.z), e; + }, + _pxBoundsToTileRange: function (t) { + var e = this.getTileSize(); + return new o.Bounds(t.min.unscaleBy(e).floor(), t.max.unscaleBy(e).ceil().subtract([1, 1])); + }, + _noTilesToLoad: function () { + for (var t in this._tiles) if (!this._tiles[t].loaded) return !1; + return !0; + }, + })), + (o.gridLayer = function (t) { + return new o.GridLayer(t); + }), + (o.TileLayer = o.GridLayer.extend({ + options: { + maxZoom: 18, + subdomains: "abc", + errorTileUrl: "", + zoomOffset: 0, + maxNativeZoom: null, + tms: !1, + zoomReverse: !1, + detectRetina: !1, + crossOrigin: !1, + }, + initialize: function (t, e) { + (this._url = t), + (e = o.setOptions(this, e)), + e.detectRetina && + o.Browser.retina && + e.maxZoom > 0 && + ((e.tileSize = Math.floor(e.tileSize / 2)), + e.zoomOffset++, + (e.minZoom = Math.max(0, e.minZoom)), + e.maxZoom--), + "string" == typeof e.subdomains && (e.subdomains = e.subdomains.split("")), + o.Browser.android || this.on("tileunload", this._onTileRemove); + }, + setUrl: function (t, e) { + return (this._url = t), e || this.redraw(), this; + }, + createTile: function (t, i) { + var n = e.createElement("img"); + return ( + o.DomEvent.on(n, "load", o.bind(this._tileOnLoad, this, i, n)), + o.DomEvent.on(n, "error", o.bind(this._tileOnError, this, i, n)), + this.options.crossOrigin && (n.crossOrigin = ""), + (n.alt = ""), + (n.src = this.getTileUrl(t)), + n + ); + }, + getTileUrl: function (t) { + return o.Util.template( + this._url, + o.extend( + { + r: + this.options.detectRetina && o.Browser.retina && this.options.maxZoom > 0 + ? "@2x" + : "", + s: this._getSubdomain(t), + x: t.x, + y: this.options.tms ? this._globalTileRange.max.y - t.y : t.y, + z: this._getZoomForUrl(), + }, + this.options + ) + ); + }, + _tileOnLoad: function (t, e) { + o.Browser.ielt9 ? setTimeout(o.bind(t, this, null, e), 0) : t(null, e); + }, + _tileOnError: function (t, e, i) { + var n = this.options.errorTileUrl; + n && (e.src = n), t(i, e); + }, + getTileSize: function () { + var t = this._map, + e = o.GridLayer.prototype.getTileSize.call(this), + i = this._tileZoom + this.options.zoomOffset, + n = this.options.maxNativeZoom; + return null !== n && i > n ? e.divideBy(t.getZoomScale(n, i)).round() : e; + }, + _onTileRemove: function (t) { + t.tile.onload = null; + }, + _getZoomForUrl: function () { + var t = this.options, + e = this._tileZoom; + return ( + t.zoomReverse && (e = t.maxZoom - e), + (e += t.zoomOffset), + null !== t.maxNativeZoom ? Math.min(e, t.maxNativeZoom) : e + ); + }, + _getSubdomain: function (t) { + var e = Math.abs(t.x + t.y) % this.options.subdomains.length; + return this.options.subdomains[e]; + }, + _abortLoading: function () { + var t, e; + for (t in this._tiles) + this._tiles[t].coords.z !== this._tileZoom && + ((e = this._tiles[t].el), + (e.onload = o.Util.falseFn), + (e.onerror = o.Util.falseFn), + e.complete || ((e.src = o.Util.emptyImageUrl), o.DomUtil.remove(e))); + }, + })), + (o.tileLayer = function (t, e) { + return new o.TileLayer(t, e); + }), + (o.TileLayer.WMS = o.TileLayer.extend({ + defaultWmsParams: { + service: "WMS", + request: "GetMap", + version: "1.1.1", + layers: "", + styles: "", + format: "image/jpeg", + transparent: !1, + }, + options: { crs: null, uppercase: !1 }, + initialize: function (t, e) { + this._url = t; + var i = o.extend({}, this.defaultWmsParams); + for (var n in e) n in this.options || (i[n] = e[n]); + (e = o.setOptions(this, e)), + (i.width = i.height = e.tileSize * (e.detectRetina && o.Browser.retina ? 2 : 1)), + (this.wmsParams = i); + }, + onAdd: function (t) { + (this._crs = this.options.crs || t.options.crs), + (this._wmsVersion = parseFloat(this.wmsParams.version)); + var e = this._wmsVersion >= 1.3 ? "crs" : "srs"; + (this.wmsParams[e] = this._crs.code), o.TileLayer.prototype.onAdd.call(this, t); + }, + getTileUrl: function (t) { + var e = this._tileCoordsToBounds(t), + i = this._crs.project(e.getNorthWest()), + n = this._crs.project(e.getSouthEast()), + s = ( + this._wmsVersion >= 1.3 && this._crs === o.CRS.EPSG4326 + ? [n.y, i.x, i.y, n.x] + : [i.x, n.y, n.x, i.y] + ).join(","), + r = o.TileLayer.prototype.getTileUrl.call(this, t); + return ( + r + + o.Util.getParamString(this.wmsParams, r, this.options.uppercase) + + (this.options.uppercase ? "&BBOX=" : "&bbox=") + + s + ); + }, + setParams: function (t, e) { + return o.extend(this.wmsParams, t), e || this.redraw(), this; + }, + })), + (o.tileLayer.wms = function (t, e) { + return new o.TileLayer.WMS(t, e); + }), + (o.ImageOverlay = o.Layer.extend({ + options: { opacity: 1, alt: "", interactive: !1 }, + initialize: function (t, e, i) { + (this._url = t), (this._bounds = o.latLngBounds(e)), o.setOptions(this, i); + }, + onAdd: function () { + this._image || (this._initImage(), this.options.opacity < 1 && this._updateOpacity()), + this.options.interactive && + (o.DomUtil.addClass(this._image, "leaflet-interactive"), + this.addInteractiveTarget(this._image)), + this.getPane().appendChild(this._image), + this._reset(); + }, + onRemove: function () { + o.DomUtil.remove(this._image), + this.options.interactive && this.removeInteractiveTarget(this._image); + }, + setOpacity: function (t) { + return (this.options.opacity = t), this._image && this._updateOpacity(), this; + }, + setStyle: function (t) { + return t.opacity && this.setOpacity(t.opacity), this; + }, + bringToFront: function () { + return this._map && o.DomUtil.toFront(this._image), this; + }, + bringToBack: function () { + return this._map && o.DomUtil.toBack(this._image), this; + }, + setUrl: function (t) { + return (this._url = t), this._image && (this._image.src = t), this; + }, + setBounds: function (t) { + return (this._bounds = t), this._map && this._reset(), this; + }, + getAttribution: function () { + return this.options.attribution; + }, + getEvents: function () { + var t = { zoom: this._reset, viewreset: this._reset }; + return this._zoomAnimated && (t.zoomanim = this._animateZoom), t; + }, + getBounds: function () { + return this._bounds; + }, + getElement: function () { + return this._image; + }, + _initImage: function () { + var t = (this._image = o.DomUtil.create( + "img", + "leaflet-image-layer " + (this._zoomAnimated ? "leaflet-zoom-animated" : "") + )); + (t.onselectstart = o.Util.falseFn), + (t.onmousemove = o.Util.falseFn), + (t.onload = o.bind(this.fire, this, "load")), + this.options.crossOrigin && (t.crossOrigin = ""), + (t.src = this._url), + (t.alt = this.options.alt); + }, + _animateZoom: function (t) { + var e = this._map.getZoomScale(t.zoom), + i = this._map._latLngToNewLayerPoint(this._bounds.getNorthWest(), t.zoom, t.center); + o.DomUtil.setTransform(this._image, i, e); + }, + _reset: function () { + var t = this._image, + e = new o.Bounds( + this._map.latLngToLayerPoint(this._bounds.getNorthWest()), + this._map.latLngToLayerPoint(this._bounds.getSouthEast()) + ), + i = e.getSize(); + o.DomUtil.setPosition(t, e.min), (t.style.width = i.x + "px"), (t.style.height = i.y + "px"); + }, + _updateOpacity: function () { + o.DomUtil.setOpacity(this._image, this.options.opacity); + }, + })), + (o.imageOverlay = function (t, e, i) { + return new o.ImageOverlay(t, e, i); + }), + (o.Icon = o.Class.extend({ + initialize: function (t) { + o.setOptions(this, t); + }, + createIcon: function (t) { + return this._createIcon("icon", t); + }, + createShadow: function (t) { + return this._createIcon("shadow", t); + }, + _createIcon: function (t, e) { + var i = this._getIconUrl(t); + if (!i) { + if ("icon" === t) throw new Error("iconUrl not set in Icon options (see the docs)."); + return null; + } + var n = this._createImg(i, e && "IMG" === e.tagName ? e : null); + return this._setIconStyles(n, t), n; + }, + _setIconStyles: function (t, e) { + var i = this.options, + n = o.point(i[e + "Size"]), + s = o.point( + ("shadow" === e && i.shadowAnchor) || i.iconAnchor || (n && n.divideBy(2, !0)) + ); + (t.className = "leaflet-marker-" + e + " " + (i.className || "")), + s && ((t.style.marginLeft = -s.x + "px"), (t.style.marginTop = -s.y + "px")), + n && ((t.style.width = n.x + "px"), (t.style.height = n.y + "px")); + }, + _createImg: function (t, i) { + return (i = i || e.createElement("img")), (i.src = t), i; + }, + _getIconUrl: function (t) { + return (o.Browser.retina && this.options[t + "RetinaUrl"]) || this.options[t + "Url"]; + }, + })), + (o.icon = function (t) { + return new o.Icon(t); + }), + (o.Icon.Default = o.Icon.extend({ + options: { + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowSize: [41, 41], + }, + _getIconUrl: function (t) { + var e = t + "Url"; + if (this.options[e]) return this.options[e]; + var i = o.Icon.Default.imagePath; + if (!i) throw new Error("Couldn't autodetect L.Icon.Default.imagePath, set it manually."); + return i + "/marker-" + t + (o.Browser.retina && "icon" === t ? "-2x" : "") + ".png"; + }, + })), + (o.Icon.Default.imagePath = (function () { + var t, + i, + n, + o, + s = e.getElementsByTagName("script"), + r = /[\/^]leaflet[\-\._]?([\w\-\._]*)\.js\??/; + for (t = 0, i = s.length; i > t; t++) + if (((n = s[t].src || ""), n.match(r))) + return (o = n.split(r)[0]), (o ? o + "/" : "") + "images"; + })()), + (o.Marker = o.Layer.extend({ + options: { + pane: "markerPane", + nonBubblingEvents: ["click", "dblclick", "mouseover", "mouseout", "contextmenu"], + icon: new o.Icon.Default(), + interactive: !0, + keyboard: !0, + zIndexOffset: 0, + opacity: 1, + riseOffset: 250, + }, + initialize: function (t, e) { + o.setOptions(this, e), (this._latlng = o.latLng(t)); + }, + onAdd: function (t) { + (this._zoomAnimated = this._zoomAnimated && t.options.markerZoomAnimation), + this._initIcon(), + this.update(); + }, + onRemove: function () { + this.dragging && + this.dragging.enabled() && + ((this.options.draggable = !0), this.dragging.removeHooks()), + this._removeIcon(), + this._removeShadow(); + }, + getEvents: function () { + var t = { zoom: this.update, viewreset: this.update }; + return this._zoomAnimated && (t.zoomanim = this._animateZoom), t; + }, + getLatLng: function () { + return this._latlng; + }, + setLatLng: function (t) { + var e = this._latlng; + return ( + (this._latlng = o.latLng(t)), + this.update(), + this.fire("move", { oldLatLng: e, latlng: this._latlng }) + ); + }, + setZIndexOffset: function (t) { + return (this.options.zIndexOffset = t), this.update(); + }, + setIcon: function (t) { + return ( + (this.options.icon = t), + this._map && (this._initIcon(), this.update()), + this._popup && this.bindPopup(this._popup, this._popup.options), + this + ); + }, + getElement: function () { + return this._icon; + }, + update: function () { + if (this._icon) { + var t = this._map.latLngToLayerPoint(this._latlng).round(); + this._setPos(t); + } + return this; + }, + _initIcon: function () { + var t = this.options, + e = "leaflet-zoom-" + (this._zoomAnimated ? "animated" : "hide"), + i = t.icon.createIcon(this._icon), + n = !1; + i !== this._icon && + (this._icon && this._removeIcon(), + (n = !0), + t.title && (i.title = t.title), + t.alt && (i.alt = t.alt)), + o.DomUtil.addClass(i, e), + t.keyboard && (i.tabIndex = "0"), + (this._icon = i), + t.riseOnHover && this.on({ mouseover: this._bringToFront, mouseout: this._resetZIndex }); + var s = t.icon.createShadow(this._shadow), + r = !1; + s !== this._shadow && (this._removeShadow(), (r = !0)), + s && o.DomUtil.addClass(s, e), + (this._shadow = s), + t.opacity < 1 && this._updateOpacity(), + n && (this.getPane().appendChild(this._icon), this._initInteraction()), + s && r && this.getPane("shadowPane").appendChild(this._shadow); + }, + _removeIcon: function () { + this.options.riseOnHover && + this.off({ mouseover: this._bringToFront, mouseout: this._resetZIndex }), + o.DomUtil.remove(this._icon), + this.removeInteractiveTarget(this._icon), + (this._icon = null); + }, + _removeShadow: function () { + this._shadow && o.DomUtil.remove(this._shadow), (this._shadow = null); + }, + _setPos: function (t) { + o.DomUtil.setPosition(this._icon, t), + this._shadow && o.DomUtil.setPosition(this._shadow, t), + (this._zIndex = t.y + this.options.zIndexOffset), + this._resetZIndex(); + }, + _updateZIndex: function (t) { + this._icon.style.zIndex = this._zIndex + t; + }, + _animateZoom: function (t) { + var e = this._map._latLngToNewLayerPoint(this._latlng, t.zoom, t.center).round(); + this._setPos(e); + }, + _initInteraction: function () { + if ( + this.options.interactive && + (o.DomUtil.addClass(this._icon, "leaflet-interactive"), + this.addInteractiveTarget(this._icon), + o.Handler.MarkerDrag) + ) { + var t = this.options.draggable; + this.dragging && ((t = this.dragging.enabled()), this.dragging.disable()), + (this.dragging = new o.Handler.MarkerDrag(this)), + t && this.dragging.enable(); + } + }, + setOpacity: function (t) { + return (this.options.opacity = t), this._map && this._updateOpacity(), this; + }, + _updateOpacity: function () { + var t = this.options.opacity; + o.DomUtil.setOpacity(this._icon, t), this._shadow && o.DomUtil.setOpacity(this._shadow, t); + }, + _bringToFront: function () { + this._updateZIndex(this.options.riseOffset); + }, + _resetZIndex: function () { + this._updateZIndex(0); + }, + })), + (o.marker = function (t, e) { + return new o.Marker(t, e); + }), + (o.DivIcon = o.Icon.extend({ + options: { iconSize: [12, 12], className: "leaflet-div-icon", html: !1 }, + createIcon: function (t) { + var i = t && "DIV" === t.tagName ? t : e.createElement("div"), + n = this.options; + return ( + (i.innerHTML = n.html !== !1 ? n.html : ""), + n.bgPos && (i.style.backgroundPosition = -n.bgPos.x + "px " + -n.bgPos.y + "px"), + this._setIconStyles(i, "icon"), + i + ); + }, + createShadow: function () { + return null; + }, + })), + (o.divIcon = function (t) { + return new o.DivIcon(t); + }), + o.Map.mergeOptions({ closePopupOnClick: !0 }), + (o.Popup = o.Layer.extend({ + options: { + pane: "popupPane", + minWidth: 50, + maxWidth: 300, + offset: [0, 7], + autoPan: !0, + autoPanPadding: [5, 5], + closeButton: !0, + autoClose: !0, + zoomAnimation: !0, + }, + initialize: function (t, e) { + o.setOptions(this, t), (this._source = e); + }, + onAdd: function (t) { + (this._zoomAnimated = this._zoomAnimated && this.options.zoomAnimation), + this._container || this._initLayout(), + t._fadeAnimated && o.DomUtil.setOpacity(this._container, 0), + clearTimeout(this._removeTimeout), + this.getPane().appendChild(this._container), + this.update(), + t._fadeAnimated && o.DomUtil.setOpacity(this._container, 1), + t.fire("popupopen", { popup: this }), + this._source && this._source.fire("popupopen", { popup: this }, !0); + }, + openOn: function (t) { + return t.openPopup(this), this; + }, + onRemove: function (t) { + t._fadeAnimated + ? (o.DomUtil.setOpacity(this._container, 0), + (this._removeTimeout = setTimeout( + o.bind(o.DomUtil.remove, o.DomUtil, this._container), + 200 + ))) + : o.DomUtil.remove(this._container), + t.fire("popupclose", { popup: this }), + this._source && this._source.fire("popupclose", { popup: this }, !0); + }, + getLatLng: function () { + return this._latlng; + }, + setLatLng: function (t) { + return ( + (this._latlng = o.latLng(t)), + this._map && (this._updatePosition(), this._adjustPan()), + this + ); + }, + getContent: function () { + return this._content; + }, + setContent: function (t) { + return (this._content = t), this.update(), this; + }, + getElement: function () { + return this._container; + }, + update: function () { + this._map && + ((this._container.style.visibility = "hidden"), + this._updateContent(), + this._updateLayout(), + this._updatePosition(), + (this._container.style.visibility = ""), + this._adjustPan()); + }, + getEvents: function () { + var t = { zoom: this._updatePosition, viewreset: this._updatePosition }; + return ( + this._zoomAnimated && (t.zoomanim = this._animateZoom), + ("closeOnClick" in this.options + ? this.options.closeOnClick + : this._map.options.closePopupOnClick) && (t.preclick = this._close), + this.options.keepInView && (t.moveend = this._adjustPan), + t + ); + }, + isOpen: function () { + return !!this._map && this._map.hasLayer(this); + }, + bringToFront: function () { + return this._map && o.DomUtil.toFront(this._container), this; + }, + bringToBack: function () { + return this._map && o.DomUtil.toBack(this._container), this; + }, + _close: function () { + this._map && this._map.closePopup(this); + }, + _initLayout: function () { + var t = "leaflet-popup", + e = (this._container = o.DomUtil.create( + "div", + t + + " " + + (this.options.className || "") + + " leaflet-zoom-" + + (this._zoomAnimated ? "animated" : "hide") + )); + if (this.options.closeButton) { + var i = (this._closeButton = o.DomUtil.create("a", t + "-close-button", e)); + (i.href = "#close"), + (i.innerHTML = "×"), + o.DomEvent.on(i, "click", this._onCloseButtonClick, this); + } + var n = (this._wrapper = o.DomUtil.create("div", t + "-content-wrapper", e)); + (this._contentNode = o.DomUtil.create("div", t + "-content", n)), + o.DomEvent.disableClickPropagation(n) + .disableScrollPropagation(this._contentNode) + .on(n, "contextmenu", o.DomEvent.stopPropagation), + (this._tipContainer = o.DomUtil.create("div", t + "-tip-container", e)), + (this._tip = o.DomUtil.create("div", t + "-tip", this._tipContainer)); + }, + _updateContent: function () { + if (this._content) { + var t = this._contentNode, + e = + "function" == typeof this._content + ? this._content(this._source || this) + : this._content; + if ("string" == typeof e) t.innerHTML = e; + else { + for (; t.hasChildNodes(); ) t.removeChild(t.firstChild); + t.appendChild(e); + } + this.fire("contentupdate"); + } + }, + _updateLayout: function () { + var t = this._contentNode, + e = t.style; + (e.width = ""), (e.whiteSpace = "nowrap"); + var i = t.offsetWidth; + (i = Math.min(i, this.options.maxWidth)), + (i = Math.max(i, this.options.minWidth)), + (e.width = i + 1 + "px"), + (e.whiteSpace = ""), + (e.height = ""); + var n = t.offsetHeight, + s = this.options.maxHeight, + r = "leaflet-popup-scrolled"; + s && n > s ? ((e.height = s + "px"), o.DomUtil.addClass(t, r)) : o.DomUtil.removeClass(t, r), + (this._containerWidth = this._container.offsetWidth); + }, + _updatePosition: function () { + if (this._map) { + var t = this._map.latLngToLayerPoint(this._latlng), + e = o.point(this.options.offset); + this._zoomAnimated ? o.DomUtil.setPosition(this._container, t) : (e = e.add(t)); + var i = (this._containerBottom = -e.y), + n = (this._containerLeft = -Math.round(this._containerWidth / 2) + e.x); + (this._container.style.bottom = i + "px"), (this._container.style.left = n + "px"); + } + }, + _animateZoom: function (t) { + var e = this._map._latLngToNewLayerPoint(this._latlng, t.zoom, t.center); + o.DomUtil.setPosition(this._container, e); + }, + _adjustPan: function () { + if (!(!this.options.autoPan || (this._map._panAnim && this._map._panAnim._inProgress))) { + var t = this._map, + e = this._container.offsetHeight, + i = this._containerWidth, + n = new o.Point(this._containerLeft, -e - this._containerBottom); + this._zoomAnimated && n._add(o.DomUtil.getPosition(this._container)); + var s = t.layerPointToContainerPoint(n), + r = o.point(this.options.autoPanPadding), + a = o.point(this.options.autoPanPaddingTopLeft || r), + h = o.point(this.options.autoPanPaddingBottomRight || r), + l = t.getSize(), + u = 0, + c = 0; + s.x + i + h.x > l.x && (u = s.x + i - l.x + h.x), + s.x - u - a.x < 0 && (u = s.x - a.x), + s.y + e + h.y > l.y && (c = s.y + e - l.y + h.y), + s.y - c - a.y < 0 && (c = s.y - a.y), + (u || c) && t.fire("autopanstart").panBy([u, c]); + } + }, + _onCloseButtonClick: function (t) { + this._close(), o.DomEvent.stop(t); + }, + })), + (o.popup = function (t, e) { + return new o.Popup(t, e); + }), + o.Map.include({ + openPopup: function (t, e, i) { + return ( + t instanceof o.Popup || (t = new o.Popup(i).setContent(t)), + e && t.setLatLng(e), + this.hasLayer(t) + ? this + : (this._popup && this._popup.options.autoClose && this.closePopup(), + (this._popup = t), + this.addLayer(t)) + ); + }, + closePopup: function (t) { + return ( + (t && t !== this._popup) || ((t = this._popup), (this._popup = null)), + t && this.removeLayer(t), + this + ); + }, + }), + o.Layer.include({ + bindPopup: function (t, e) { + return ( + t instanceof o.Popup + ? (o.setOptions(t, e), (this._popup = t), (t._source = this)) + : ((!this._popup || e) && (this._popup = new o.Popup(e, this)), + this._popup.setContent(t)), + this._popupHandlersAdded || + (this.on({ click: this._openPopup, remove: this.closePopup, move: this._movePopup }), + (this._popupHandlersAdded = !0)), + (this._originalPopupOffset = this._popup.options.offset), + this + ); + }, + unbindPopup: function () { + return ( + this._popup && + (this.off({ click: this._openPopup, remove: this.closePopup, move: this._movePopup }), + (this._popupHandlersAdded = !1), + (this._popup = null)), + this + ); + }, + openPopup: function (t, e) { + if ((t instanceof o.Layer || ((e = t), (t = this)), t instanceof o.FeatureGroup)) + for (var i in this._layers) { + t = this._layers[i]; + break; + } + return ( + e || (e = t.getCenter ? t.getCenter() : t.getLatLng()), + this._popup && + this._map && + ((this._popup.options.offset = this._popupAnchor(t)), + (this._popup._source = t), + this._popup.update(), + this._map.openPopup(this._popup, e)), + this + ); + }, + closePopup: function () { + return this._popup && this._popup._close(), this; + }, + togglePopup: function (t) { + return this._popup && (this._popup._map ? this.closePopup() : this.openPopup(t)), this; + }, + isPopupOpen: function () { + return this._popup.isOpen(); + }, + setPopupContent: function (t) { + return this._popup && this._popup.setContent(t), this; + }, + getPopup: function () { + return this._popup; + }, + _openPopup: function (t) { + var e = t.layer || t.target; + if (this._popup && this._map) + return e instanceof o.Path + ? void this.openPopup(t.layer || t.target, t.latlng) + : void (this._map.hasLayer(this._popup) && this._popup._source === e + ? this.closePopup() + : this.openPopup(e, t.latlng)); + }, + _popupAnchor: function (t) { + var e = t._getPopupAnchor ? t._getPopupAnchor() : [0, 0], + i = this._originalPopupOffset || o.Popup.prototype.options.offset; + return o.point(e).add(i); + }, + _movePopup: function (t) { + this._popup.setLatLng(t.latlng); + }, + }), + o.Marker.include({ + _getPopupAnchor: function () { + return this.options.icon.options.popupAnchor || [0, 0]; + }, + }), + (o.LayerGroup = o.Layer.extend({ + initialize: function (t) { + this._layers = {}; + var e, i; + if (t) for (e = 0, i = t.length; i > e; e++) this.addLayer(t[e]); + }, + addLayer: function (t) { + var e = this.getLayerId(t); + return (this._layers[e] = t), this._map && this._map.addLayer(t), this; + }, + removeLayer: function (t) { + var e = t in this._layers ? t : this.getLayerId(t); + return ( + this._map && this._layers[e] && this._map.removeLayer(this._layers[e]), + delete this._layers[e], + this + ); + }, + hasLayer: function (t) { + return !!t && (t in this._layers || this.getLayerId(t) in this._layers); + }, + clearLayers: function () { + for (var t in this._layers) this.removeLayer(this._layers[t]); + return this; + }, + invoke: function (t) { + var e, + i, + n = Array.prototype.slice.call(arguments, 1); + for (e in this._layers) (i = this._layers[e]), i[t] && i[t].apply(i, n); + return this; + }, + onAdd: function (t) { + for (var e in this._layers) t.addLayer(this._layers[e]); + }, + onRemove: function (t) { + for (var e in this._layers) t.removeLayer(this._layers[e]); + }, + eachLayer: function (t, e) { + for (var i in this._layers) t.call(e, this._layers[i]); + return this; + }, + getLayer: function (t) { + return this._layers[t]; + }, + getLayers: function () { + var t = []; + for (var e in this._layers) t.push(this._layers[e]); + return t; + }, + setZIndex: function (t) { + return this.invoke("setZIndex", t); + }, + getLayerId: function (t) { + return o.stamp(t); + }, + })), + (o.layerGroup = function (t) { + return new o.LayerGroup(t); + }), + (o.FeatureGroup = o.LayerGroup.extend({ + addLayer: function (t) { + return this.hasLayer(t) + ? this + : (t.addEventParent(this), + o.LayerGroup.prototype.addLayer.call(this, t), + this.fire("layeradd", { layer: t })); + }, + removeLayer: function (t) { + return this.hasLayer(t) + ? (t in this._layers && (t = this._layers[t]), + t.removeEventParent(this), + o.LayerGroup.prototype.removeLayer.call(this, t), + this.fire("layerremove", { layer: t })) + : this; + }, + setStyle: function (t) { + return this.invoke("setStyle", t); + }, + bringToFront: function () { + return this.invoke("bringToFront"); + }, + bringToBack: function () { + return this.invoke("bringToBack"); + }, + getBounds: function () { + var t = new o.LatLngBounds(); + for (var e in this._layers) { + var i = this._layers[e]; + t.extend(i.getBounds ? i.getBounds() : i.getLatLng()); + } + return t; + }, + })), + (o.featureGroup = function (t) { + return new o.FeatureGroup(t); + }), + (o.Renderer = o.Layer.extend({ + options: { padding: 0.1 }, + initialize: function (t) { + o.setOptions(this, t), o.stamp(this); + }, + onAdd: function () { + this._container || + (this._initContainer(), + this._zoomAnimated && o.DomUtil.addClass(this._container, "leaflet-zoom-animated")), + this.getPane().appendChild(this._container), + this._update(); + }, + onRemove: function () { + o.DomUtil.remove(this._container); + }, + getEvents: function () { + var t = { + viewreset: this._reset, + zoomstart: this._onZoomStart, + zoom: this._onZoom, + moveend: this._update, + }; + return this._zoomAnimated && (t.zoomanim = this._onAnimZoom), t; + }, + _onAnimZoom: function (t) { + this._updateTransform(t.center, t.zoom); + }, + _onZoom: function () { + this._updateTransform(this._map.getCenter(), this._map.getZoom()); + }, + _onZoomStart: function () { + this._update(); + }, + _updateTransform: function (t, e) { + var i = this._map.getZoomScale(e, this._zoom), + n = o.DomUtil.getPosition(this._container), + s = this._map.getSize().multiplyBy(0.5 + this.options.padding), + r = this._map.project(this._center, e), + a = this._map.project(t, e), + h = a.subtract(r), + l = s.multiplyBy(-i).add(n).add(s).subtract(h); + o.DomUtil.setTransform(this._container, l, i); + }, + _reset: function () { + this._update(), this._updateTransform(this._center, this._zoom); + }, + _update: function () { + var t = this.options.padding, + e = this._map.getSize(), + i = this._map.containerPointToLayerPoint(e.multiplyBy(-t)).round(); + (this._bounds = new o.Bounds(i, i.add(e.multiplyBy(1 + 2 * t)).round())), + (this._center = this._map.getCenter()), + (this._zoom = this._map.getZoom()); + }, + })), + o.Map.include({ + getRenderer: function (t) { + var e = + t.options.renderer || + this._getPaneRenderer(t.options.pane) || + this.options.renderer || + this._renderer; + return ( + e || (e = this._renderer = (this.options.preferCanvas && o.canvas()) || o.svg()), + this.hasLayer(e) || this.addLayer(e), + e + ); + }, + _getPaneRenderer: function (t) { + if ("overlayPane" === t || t === i) return !1; + var e = this._paneRenderers[t]; + return ( + e === i && + ((e = (o.SVG && o.svg({ pane: t })) || (o.Canvas && o.canvas({ pane: t }))), + (this._paneRenderers[t] = e)), + e + ); + }, + }), + (o.Path = o.Layer.extend({ + options: { + stroke: !0, + color: "#3388ff", + weight: 3, + opacity: 1, + lineCap: "round", + lineJoin: "round", + fillOpacity: 0.2, + fillRule: "evenodd", + interactive: !0, + }, + beforeAdd: function (t) { + this._renderer = t.getRenderer(this); + }, + onAdd: function () { + this._renderer._initPath(this), this._reset(), this._renderer._addPath(this); + }, + onRemove: function () { + this._renderer._removePath(this); + }, + getEvents: function () { + return { zoomend: this._project, moveend: this._update, viewreset: this._reset }; + }, + redraw: function () { + return this._map && this._renderer._updatePath(this), this; + }, + setStyle: function (t) { + return o.setOptions(this, t), this._renderer && this._renderer._updateStyle(this), this; + }, + bringToFront: function () { + return this._renderer && this._renderer._bringToFront(this), this; + }, + bringToBack: function () { + return this._renderer && this._renderer._bringToBack(this), this; + }, + getElement: function () { + return this._path; + }, + _reset: function () { + this._project(), this._update(); + }, + _clickTolerance: function () { + return (this.options.stroke ? this.options.weight / 2 : 0) + (o.Browser.touch ? 10 : 0); + }, + })), + (o.LineUtil = { + simplify: function (t, e) { + if (!e || !t.length) return t.slice(); + var i = e * e; + return (t = this._reducePoints(t, i)), (t = this._simplifyDP(t, i)); + }, + pointToSegmentDistance: function (t, e, i) { + return Math.sqrt(this._sqClosestPointOnSegment(t, e, i, !0)); + }, + closestPointOnSegment: function (t, e, i) { + return this._sqClosestPointOnSegment(t, e, i); + }, + _simplifyDP: function (t, e) { + var n = t.length, + o = typeof Uint8Array != i + "" ? Uint8Array : Array, + s = new o(n); + (s[0] = s[n - 1] = 1), this._simplifyDPStep(t, s, e, 0, n - 1); + var r, + a = []; + for (r = 0; n > r; r++) s[r] && a.push(t[r]); + return a; + }, + _simplifyDPStep: function (t, e, i, n, o) { + var s, + r, + a, + h = 0; + for (r = n + 1; o - 1 >= r; r++) + (a = this._sqClosestPointOnSegment(t[r], t[n], t[o], !0)), a > h && ((s = r), (h = a)); + h > i && + ((e[s] = 1), this._simplifyDPStep(t, e, i, n, s), this._simplifyDPStep(t, e, i, s, o)); + }, + _reducePoints: function (t, e) { + for (var i = [t[0]], n = 1, o = 0, s = t.length; s > n; n++) + this._sqDist(t[n], t[o]) > e && (i.push(t[n]), (o = n)); + return s - 1 > o && i.push(t[s - 1]), i; + }, + clipSegment: function (t, e, i, n, o) { + var s, + r, + a, + h = n ? this._lastCode : this._getBitCode(t, i), + l = this._getBitCode(e, i); + for (this._lastCode = l; ; ) { + if (!(h | l)) return [t, e]; + if (h & l) return !1; + (s = h || l), + (r = this._getEdgeIntersection(t, e, s, i, o)), + (a = this._getBitCode(r, i)), + s === h ? ((t = r), (h = a)) : ((e = r), (l = a)); + } + }, + _getEdgeIntersection: function (t, e, i, n, s) { + var r, + a, + h = e.x - t.x, + l = e.y - t.y, + u = n.min, + c = n.max; + return ( + 8 & i + ? ((r = t.x + (h * (c.y - t.y)) / l), (a = c.y)) + : 4 & i + ? ((r = t.x + (h * (u.y - t.y)) / l), (a = u.y)) + : 2 & i + ? ((r = c.x), (a = t.y + (l * (c.x - t.x)) / h)) + : 1 & i && ((r = u.x), (a = t.y + (l * (u.x - t.x)) / h)), + new o.Point(r, a, s) + ); + }, + _getBitCode: function (t, e) { + var i = 0; + return ( + t.x < e.min.x ? (i |= 1) : t.x > e.max.x && (i |= 2), + t.y < e.min.y ? (i |= 4) : t.y > e.max.y && (i |= 8), + i + ); + }, + _sqDist: function (t, e) { + var i = e.x - t.x, + n = e.y - t.y; + return i * i + n * n; + }, + _sqClosestPointOnSegment: function (t, e, i, n) { + var s, + r = e.x, + a = e.y, + h = i.x - r, + l = i.y - a, + u = h * h + l * l; + return ( + u > 0 && + ((s = ((t.x - r) * h + (t.y - a) * l) / u), + s > 1 ? ((r = i.x), (a = i.y)) : s > 0 && ((r += h * s), (a += l * s))), + (h = t.x - r), + (l = t.y - a), + n ? h * h + l * l : new o.Point(r, a) + ); + }, + }), + (o.Polyline = o.Path.extend({ + options: { smoothFactor: 1 }, + initialize: function (t, e) { + o.setOptions(this, e), this._setLatLngs(t); + }, + getLatLngs: function () { + return this._latlngs; + }, + setLatLngs: function (t) { + return this._setLatLngs(t), this.redraw(); + }, + isEmpty: function () { + return !this._latlngs.length; + }, + closestLayerPoint: function (t) { + for ( + var e, + i, + n = 1 / 0, + s = null, + r = o.LineUtil._sqClosestPointOnSegment, + a = 0, + h = this._parts.length; + h > a; + a++ + ) + for (var l = this._parts[a], u = 1, c = l.length; c > u; u++) { + (e = l[u - 1]), (i = l[u]); + var d = r(t, e, i, !0); + n > d && ((n = d), (s = r(t, e, i))); + } + return s && (s.distance = Math.sqrt(n)), s; + }, + getCenter: function () { + var t, + e, + i, + n, + o, + s, + r, + a = this._rings[0], + h = a.length; + if (!h) return null; + for (t = 0, e = 0; h - 1 > t; t++) e += a[t].distanceTo(a[t + 1]) / 2; + if (0 === e) return this._map.layerPointToLatLng(a[0]); + for (t = 0, n = 0; h - 1 > t; t++) + if (((o = a[t]), (s = a[t + 1]), (i = o.distanceTo(s)), (n += i), n > e)) + return ( + (r = (n - e) / i), + this._map.layerPointToLatLng([s.x - r * (s.x - o.x), s.y - r * (s.y - o.y)]) + ); + }, + getBounds: function () { + return this._bounds; + }, + addLatLng: function (t, e) { + return ( + (e = e || this._defaultShape()), + (t = o.latLng(t)), + e.push(t), + this._bounds.extend(t), + this.redraw() + ); + }, + _setLatLngs: function (t) { + (this._bounds = new o.LatLngBounds()), (this._latlngs = this._convertLatLngs(t)); + }, + _defaultShape: function () { + return o.Polyline._flat(this._latlngs) ? this._latlngs : this._latlngs[0]; + }, + _convertLatLngs: function (t) { + for (var e = [], i = o.Polyline._flat(t), n = 0, s = t.length; s > n; n++) + i + ? ((e[n] = o.latLng(t[n])), this._bounds.extend(e[n])) + : (e[n] = this._convertLatLngs(t[n])); + return e; + }, + _project: function () { + (this._rings = []), this._projectLatlngs(this._latlngs, this._rings); + var t = this._clickTolerance(), + e = new o.Point(t, -t); + this._bounds.isValid() && + (this._pxBounds = new o.Bounds( + this._map.latLngToLayerPoint(this._bounds.getSouthWest())._subtract(e), + this._map.latLngToLayerPoint(this._bounds.getNorthEast())._add(e) + )); + }, + _projectLatlngs: function (t, e) { + var i, + n, + s = t[0] instanceof o.LatLng, + r = t.length; + if (s) { + for (n = [], i = 0; r > i; i++) n[i] = this._map.latLngToLayerPoint(t[i]); + e.push(n); + } else for (i = 0; r > i; i++) this._projectLatlngs(t[i], e); + }, + _clipPoints: function () { + var t = this._renderer._bounds; + if (((this._parts = []), this._pxBounds && this._pxBounds.intersects(t))) { + if (this.options.noClip) return void (this._parts = this._rings); + var e, + i, + n, + s, + r, + a, + h, + l = this._parts; + for (e = 0, n = 0, s = this._rings.length; s > e; e++) + for (h = this._rings[e], i = 0, r = h.length; r - 1 > i; i++) + (a = o.LineUtil.clipSegment(h[i], h[i + 1], t, i, !0)), + a && + ((l[n] = l[n] || []), + l[n].push(a[0]), + (a[1] !== h[i + 1] || i === r - 2) && (l[n].push(a[1]), n++)); + } + }, + _simplifyPoints: function () { + for (var t = this._parts, e = this.options.smoothFactor, i = 0, n = t.length; n > i; i++) + t[i] = o.LineUtil.simplify(t[i], e); + }, + _update: function () { + this._map && (this._clipPoints(), this._simplifyPoints(), this._updatePath()); + }, + _updatePath: function () { + this._renderer._updatePoly(this); + }, + })), + (o.polyline = function (t, e) { + return new o.Polyline(t, e); + }), + (o.Polyline._flat = function (t) { + return !o.Util.isArray(t[0]) || ("object" != typeof t[0][0] && "undefined" != typeof t[0][0]); + }), + (o.PolyUtil = {}), + (o.PolyUtil.clipPolygon = function (t, e, i) { + var n, + s, + r, + a, + h, + l, + u, + c, + d, + _ = [1, 4, 2, 8], + m = o.LineUtil; + for (s = 0, u = t.length; u > s; s++) t[s]._code = m._getBitCode(t[s], e); + for (a = 0; 4 > a; a++) { + for (c = _[a], n = [], s = 0, u = t.length, r = u - 1; u > s; r = s++) + (h = t[s]), + (l = t[r]), + h._code & c + ? l._code & c || + ((d = m._getEdgeIntersection(l, h, c, e, i)), + (d._code = m._getBitCode(d, e)), + n.push(d)) + : (l._code & c && + ((d = m._getEdgeIntersection(l, h, c, e, i)), + (d._code = m._getBitCode(d, e)), + n.push(d)), + n.push(h)); + t = n; + } + return t; + }), + (o.Polygon = o.Polyline.extend({ + options: { fill: !0 }, + isEmpty: function () { + return !this._latlngs.length || !this._latlngs[0].length; + }, + getCenter: function () { + var t, + e, + i, + n, + o, + s, + r, + a, + h, + l = this._rings[0], + u = l.length; + if (!u) return null; + for (s = r = a = 0, t = 0, e = u - 1; u > t; e = t++) + (i = l[t]), + (n = l[e]), + (o = i.y * n.x - n.y * i.x), + (r += (i.x + n.x) * o), + (a += (i.y + n.y) * o), + (s += 3 * o); + return (h = 0 === s ? l[0] : [r / s, a / s]), this._map.layerPointToLatLng(h); + }, + _convertLatLngs: function (t) { + var e = o.Polyline.prototype._convertLatLngs.call(this, t), + i = e.length; + return i >= 2 && e[0] instanceof o.LatLng && e[0].equals(e[i - 1]) && e.pop(), e; + }, + _setLatLngs: function (t) { + o.Polyline.prototype._setLatLngs.call(this, t), + o.Polyline._flat(this._latlngs) && (this._latlngs = [this._latlngs]); + }, + _defaultShape: function () { + return o.Polyline._flat(this._latlngs[0]) ? this._latlngs[0] : this._latlngs[0][0]; + }, + _clipPoints: function () { + var t = this._renderer._bounds, + e = this.options.weight, + i = new o.Point(e, e); + if ( + ((t = new o.Bounds(t.min.subtract(i), t.max.add(i))), + (this._parts = []), + this._pxBounds && this._pxBounds.intersects(t)) + ) { + if (this.options.noClip) return void (this._parts = this._rings); + for (var n, s = 0, r = this._rings.length; r > s; s++) + (n = o.PolyUtil.clipPolygon(this._rings[s], t, !0)), n.length && this._parts.push(n); + } + }, + _updatePath: function () { + this._renderer._updatePoly(this, !0); + }, + })), + (o.polygon = function (t, e) { + return new o.Polygon(t, e); + }), + (o.Rectangle = o.Polygon.extend({ + initialize: function (t, e) { + o.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(t), e); + }, + setBounds: function (t) { + return this.setLatLngs(this._boundsToLatLngs(t)); + }, + _boundsToLatLngs: function (t) { + return ( + (t = o.latLngBounds(t)), + [t.getSouthWest(), t.getNorthWest(), t.getNorthEast(), t.getSouthEast()] + ); + }, + })), + (o.rectangle = function (t, e) { + return new o.Rectangle(t, e); + }), + (o.CircleMarker = o.Path.extend({ + options: { fill: !0, radius: 10 }, + initialize: function (t, e) { + o.setOptions(this, e), (this._latlng = o.latLng(t)), (this._radius = this.options.radius); + }, + setLatLng: function (t) { + return ( + (this._latlng = o.latLng(t)), this.redraw(), this.fire("move", { latlng: this._latlng }) + ); + }, + getLatLng: function () { + return this._latlng; + }, + setRadius: function (t) { + return (this.options.radius = this._radius = t), this.redraw(); + }, + getRadius: function () { + return this._radius; + }, + setStyle: function (t) { + var e = (t && t.radius) || this._radius; + return o.Path.prototype.setStyle.call(this, t), this.setRadius(e), this; + }, + _project: function () { + (this._point = this._map.latLngToLayerPoint(this._latlng)), this._updateBounds(); + }, + _updateBounds: function () { + var t = this._radius, + e = this._radiusY || t, + i = this._clickTolerance(), + n = [t + i, e + i]; + this._pxBounds = new o.Bounds(this._point.subtract(n), this._point.add(n)); + }, + _update: function () { + this._map && this._updatePath(); + }, + _updatePath: function () { + this._renderer._updateCircle(this); + }, + _empty: function () { + return this._radius && !this._renderer._bounds.intersects(this._pxBounds); + }, + })), + (o.circleMarker = function (t, e) { + return new o.CircleMarker(t, e); + }), + (o.Circle = o.CircleMarker.extend({ + initialize: function (t, e) { + o.setOptions(this, e), (this._latlng = o.latLng(t)), (this._mRadius = this.options.radius); + }, + setRadius: function (t) { + return (this._mRadius = t), this.redraw(); + }, + getRadius: function () { + return this._mRadius; + }, + getBounds: function () { + var t = [this._radius, this._radiusY || this._radius]; + return new o.LatLngBounds( + this._map.layerPointToLatLng(this._point.subtract(t)), + this._map.layerPointToLatLng(this._point.add(t)) + ); + }, + setStyle: o.Path.prototype.setStyle, + _project: function () { + var t = this._latlng.lng, + e = this._latlng.lat, + i = this._map, + n = i.options.crs; + if (n.distance === o.CRS.Earth.distance) { + var s = Math.PI / 180, + r = this._mRadius / o.CRS.Earth.R / s, + a = i.project([e + r, t]), + h = i.project([e - r, t]), + l = a.add(h).divideBy(2), + u = i.unproject(l).lat, + c = + Math.acos( + (Math.cos(r * s) - Math.sin(e * s) * Math.sin(u * s)) / + (Math.cos(e * s) * Math.cos(u * s)) + ) / s; + (this._point = l.subtract(i.getPixelOrigin())), + (this._radius = isNaN(c) + ? 0 + : Math.max(Math.round(l.x - i.project([u, t - c]).x), 1)), + (this._radiusY = Math.max(Math.round(l.y - a.y), 1)); + } else { + var d = n.unproject(n.project(this._latlng).subtract([this._mRadius, 0])); + (this._point = i.latLngToLayerPoint(this._latlng)), + (this._radius = this._point.x - i.latLngToLayerPoint(d).x); + } + this._updateBounds(); + }, + })), + (o.circle = function (t, e, i) { + return "number" == typeof e && (e = o.extend({}, i, { radius: e })), new o.Circle(t, e); + }), + (o.SVG = o.Renderer.extend({ + _initContainer: function () { + (this._container = o.SVG.create("svg")), + this._container.setAttribute("pointer-events", "none"), + (this._rootGroup = o.SVG.create("g")), + this._container.appendChild(this._rootGroup); + }, + _update: function () { + if (!this._map._animatingZoom || !this._bounds) { + o.Renderer.prototype._update.call(this); + var t = this._bounds, + e = t.getSize(), + i = this._container; + (this._svgSize && this._svgSize.equals(e)) || + ((this._svgSize = e), i.setAttribute("width", e.x), i.setAttribute("height", e.y)), + o.DomUtil.setPosition(i, t.min), + i.setAttribute("viewBox", [t.min.x, t.min.y, e.x, e.y].join(" ")); + } + }, + _initPath: function (t) { + var e = (t._path = o.SVG.create("path")); + t.options.className && o.DomUtil.addClass(e, t.options.className), + t.options.interactive && o.DomUtil.addClass(e, "leaflet-interactive"), + this._updateStyle(t); + }, + _addPath: function (t) { + this._rootGroup.appendChild(t._path), t.addInteractiveTarget(t._path); + }, + _removePath: function (t) { + o.DomUtil.remove(t._path), t.removeInteractiveTarget(t._path); + }, + _updatePath: function (t) { + t._project(), t._update(); + }, + _updateStyle: function (t) { + var e = t._path, + i = t.options; + e && + (i.stroke + ? (e.setAttribute("stroke", i.color), + e.setAttribute("stroke-opacity", i.opacity), + e.setAttribute("stroke-width", i.weight), + e.setAttribute("stroke-linecap", i.lineCap), + e.setAttribute("stroke-linejoin", i.lineJoin), + i.dashArray + ? e.setAttribute("stroke-dasharray", i.dashArray) + : e.removeAttribute("stroke-dasharray"), + i.dashOffset + ? e.setAttribute("stroke-dashoffset", i.dashOffset) + : e.removeAttribute("stroke-dashoffset")) + : e.setAttribute("stroke", "none"), + i.fill + ? (e.setAttribute("fill", i.fillColor || i.color), + e.setAttribute("fill-opacity", i.fillOpacity), + e.setAttribute("fill-rule", i.fillRule || "evenodd")) + : e.setAttribute("fill", "none"), + e.setAttribute( + "pointer-events", + i.pointerEvents || (i.interactive ? "visiblePainted" : "none") + )); + }, + _updatePoly: function (t, e) { + this._setPath(t, o.SVG.pointsToPath(t._parts, e)); + }, + _updateCircle: function (t) { + var e = t._point, + i = t._radius, + n = t._radiusY || i, + o = "a" + i + "," + n + " 0 1,0 ", + s = t._empty() + ? "M0 0" + : "M" + (e.x - i) + "," + e.y + o + 2 * i + ",0 " + o + 2 * -i + ",0 "; + this._setPath(t, s); + }, + _setPath: function (t, e) { + t._path.setAttribute("d", e); + }, + _bringToFront: function (t) { + o.DomUtil.toFront(t._path); + }, + _bringToBack: function (t) { + o.DomUtil.toBack(t._path); + }, + })), + o.extend(o.SVG, { + create: function (t) { + return e.createElementNS("http://www.w3.org/2000/svg", t); + }, + pointsToPath: function (t, e) { + var i, + n, + s, + r, + a, + h, + l = ""; + for (i = 0, s = t.length; s > i; i++) { + for (a = t[i], n = 0, r = a.length; r > n; n++) + (h = a[n]), (l += (n ? "L" : "M") + h.x + " " + h.y); + l += e ? (o.Browser.svg ? "z" : "x") : ""; + } + return l || "M0 0"; + }, + }), + (o.Browser.svg = !(!e.createElementNS || !o.SVG.create("svg").createSVGRect)), + (o.svg = function (t) { + return o.Browser.svg || o.Browser.vml ? new o.SVG(t) : null; + }), + (o.Browser.vml = + !o.Browser.svg && + (function () { + try { + var t = e.createElement("div"); + t.innerHTML = ''; + var i = t.firstChild; + return (i.style.behavior = "url(#default#VML)"), i && "object" == typeof i.adj; + } catch (n) { + return !1; + } + })()), + o.SVG.include( + o.Browser.vml + ? { + _initContainer: function () { + this._container = o.DomUtil.create("div", "leaflet-vml-container"); + }, + _update: function () { + this._map._animatingZoom || o.Renderer.prototype._update.call(this); + }, + _initPath: function (t) { + var e = (t._container = o.SVG.create("shape")); + o.DomUtil.addClass(e, "leaflet-vml-shape " + (this.options.className || "")), + (e.coordsize = "1 1"), + (t._path = o.SVG.create("path")), + e.appendChild(t._path), + this._updateStyle(t); + }, + _addPath: function (t) { + var e = t._container; + this._container.appendChild(e), + t.options.interactive && t.addInteractiveTarget(e); + }, + _removePath: function (t) { + var e = t._container; + o.DomUtil.remove(e), t.removeInteractiveTarget(e); + }, + _updateStyle: function (t) { + var e = t._stroke, + i = t._fill, + n = t.options, + s = t._container; + (s.stroked = !!n.stroke), + (s.filled = !!n.fill), + n.stroke + ? (e || (e = t._stroke = o.SVG.create("stroke")), + s.appendChild(e), + (e.weight = n.weight + "px"), + (e.color = n.color), + (e.opacity = n.opacity), + n.dashArray + ? (e.dashStyle = o.Util.isArray(n.dashArray) + ? n.dashArray.join(" ") + : n.dashArray.replace(/( *, *)/g, " ")) + : (e.dashStyle = ""), + (e.endcap = n.lineCap.replace("butt", "flat")), + (e.joinstyle = n.lineJoin)) + : e && (s.removeChild(e), (t._stroke = null)), + n.fill + ? (i || (i = t._fill = o.SVG.create("fill")), + s.appendChild(i), + (i.color = n.fillColor || n.color), + (i.opacity = n.fillOpacity)) + : i && (s.removeChild(i), (t._fill = null)); + }, + _updateCircle: function (t) { + var e = t._point.round(), + i = Math.round(t._radius), + n = Math.round(t._radiusY || i); + this._setPath( + t, + t._empty() + ? "M0 0" + : "AL " + e.x + "," + e.y + " " + i + "," + n + " 0,23592600" + ); + }, + _setPath: function (t, e) { + t._path.v = e; + }, + _bringToFront: function (t) { + o.DomUtil.toFront(t._container); + }, + _bringToBack: function (t) { + o.DomUtil.toBack(t._container); + }, + } + : {} + ), + o.Browser.vml && + (o.SVG.create = (function () { + try { + return ( + e.namespaces.add("lvml", "urn:schemas-microsoft-com:vml"), + function (t) { + return e.createElement("'); + } + ); + } catch (t) { + return function (t) { + return e.createElement( + "<" + t + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">' + ); + }; + } + })()), + (o.Canvas = o.Renderer.extend({ + onAdd: function () { + o.Renderer.prototype.onAdd.call(this), (this._layers = this._layers || {}), this._draw(); + }, + _initContainer: function () { + var t = (this._container = e.createElement("canvas")); + o.DomEvent.on(t, "mousemove", o.Util.throttle(this._onMouseMove, 32, this), this) + .on(t, "click dblclick mousedown mouseup contextmenu", this._onClick, this) + .on(t, "mouseout", this._handleMouseOut, this), + (this._ctx = t.getContext("2d")); + }, + _update: function () { + if (!this._map._animatingZoom || !this._bounds) { + (this._drawnLayers = {}), o.Renderer.prototype._update.call(this); + var t = this._bounds, + e = this._container, + i = t.getSize(), + n = o.Browser.retina ? 2 : 1; + o.DomUtil.setPosition(e, t.min), + (e.width = n * i.x), + (e.height = n * i.y), + (e.style.width = i.x + "px"), + (e.style.height = i.y + "px"), + o.Browser.retina && this._ctx.scale(2, 2), + this._ctx.translate(-t.min.x, -t.min.y); + } + }, + _initPath: function (t) { + this._layers[o.stamp(t)] = t; + }, + _addPath: o.Util.falseFn, + _removePath: function (t) { + (t._removed = !0), this._requestRedraw(t); + }, + _updatePath: function (t) { + (this._redrawBounds = t._pxBounds), + this._draw(!0), + t._project(), + t._update(), + this._draw(), + (this._redrawBounds = null); + }, + _updateStyle: function (t) { + this._requestRedraw(t); + }, + _requestRedraw: function (t) { + if (this._map) { + var e = (t.options.weight || 0) + 1; + (this._redrawBounds = this._redrawBounds || new o.Bounds()), + this._redrawBounds.extend(t._pxBounds.min.subtract([e, e])), + this._redrawBounds.extend(t._pxBounds.max.add([e, e])), + (this._redrawRequest = + this._redrawRequest || o.Util.requestAnimFrame(this._redraw, this)); + } + }, + _redraw: function () { + (this._redrawRequest = null), this._draw(!0), this._draw(), (this._redrawBounds = null); + }, + _draw: function (t) { + this._clear = t; + var e, + i = this._redrawBounds; + this._ctx.save(), + i && + (this._ctx.beginPath(), + this._ctx.rect(i.min.x, i.min.y, i.max.x - i.min.x, i.max.y - i.min.y), + this._ctx.clip()); + for (var n in this._layers) + (e = this._layers[n]), + (!i || e._pxBounds.intersects(i)) && e._updatePath(), + t && e._removed && (delete e._removed, delete this._layers[n]); + this._ctx.restore(); + }, + _updatePoly: function (t, e) { + var i, + n, + o, + s, + r = t._parts, + a = r.length, + h = this._ctx; + if (a) { + for (this._drawnLayers[t._leaflet_id] = t, h.beginPath(), i = 0; a > i; i++) { + for (n = 0, o = r[i].length; o > n; n++) + (s = r[i][n]), h[n ? "lineTo" : "moveTo"](s.x, s.y); + e && h.closePath(); + } + this._fillStroke(h, t); + } + }, + _updateCircle: function (t) { + if (!t._empty()) { + var e = t._point, + i = this._ctx, + n = t._radius, + o = (t._radiusY || n) / n; + 1 !== o && (i.save(), i.scale(1, o)), + i.beginPath(), + i.arc(e.x, e.y / o, n, 0, 2 * Math.PI, !1), + 1 !== o && i.restore(), + this._fillStroke(i, t); + } + }, + _fillStroke: function (t, e) { + var i = this._clear, + n = e.options; + (t.globalCompositeOperation = i ? "destination-out" : "source-over"), + n.fill && + ((t.globalAlpha = i ? 1 : n.fillOpacity), + (t.fillStyle = n.fillColor || n.color), + t.fill(n.fillRule || "evenodd")), + n.stroke && + 0 !== n.weight && + ((t.globalAlpha = i ? 1 : n.opacity), + (e._prevWeight = t.lineWidth = i ? e._prevWeight + 1 : n.weight), + (t.strokeStyle = n.color), + (t.lineCap = n.lineCap), + (t.lineJoin = n.lineJoin), + t.stroke()); + }, + _onClick: function (t) { + var e = this._map.mouseEventToLayerPoint(t), + i = []; + for (var n in this._layers) + this._layers[n]._containsPoint(e) && (o.DomEvent._fakeStop(t), i.push(this._layers[n])); + i.length && this._fireEvent(i, t); + }, + _onMouseMove: function (t) { + if (this._map && !this._map.dragging._draggable._moving && !this._map._animatingZoom) { + var e = this._map.mouseEventToLayerPoint(t); + this._handleMouseOut(t, e), this._handleMouseHover(t, e); + } + }, + _handleMouseOut: function (t, e) { + var i = this._hoveredLayer; + !i || + ("mouseout" !== t.type && i._containsPoint(e)) || + (o.DomUtil.removeClass(this._container, "leaflet-interactive"), + this._fireEvent([i], t, "mouseout"), + (this._hoveredLayer = null)); + }, + _handleMouseHover: function (t, e) { + var i, n; + if (!this._hoveredLayer) + for (i in this._drawnLayers) + if (((n = this._drawnLayers[i]), n.options.interactive && n._containsPoint(e))) { + o.DomUtil.addClass(this._container, "leaflet-interactive"), + this._fireEvent([n], t, "mouseover"), + (this._hoveredLayer = n); + break; + } + this._hoveredLayer && this._fireEvent([this._hoveredLayer], t); + }, + _fireEvent: function (t, e, i) { + this._map._fireDOMEvent(e, i || e.type, t); + }, + _bringToFront: o.Util.falseFn, + _bringToBack: o.Util.falseFn, + })), + (o.Browser.canvas = (function () { + return !!e.createElement("canvas").getContext; + })()), + (o.canvas = function (t) { + return o.Browser.canvas ? new o.Canvas(t) : null; + }), + (o.Polyline.prototype._containsPoint = function (t, e) { + var i, + n, + s, + r, + a, + h, + l = this._clickTolerance(); + if (!this._pxBounds.contains(t)) return !1; + for (i = 0, r = this._parts.length; r > i; i++) + for (h = this._parts[i], n = 0, a = h.length, s = a - 1; a > n; s = n++) + if ((e || 0 !== n) && o.LineUtil.pointToSegmentDistance(t, h[s], h[n]) <= l) return !0; + return !1; + }), + (o.Polygon.prototype._containsPoint = function (t) { + var e, + i, + n, + s, + r, + a, + h, + l, + u = !1; + if (!this._pxBounds.contains(t)) return !1; + for (s = 0, h = this._parts.length; h > s; s++) + for (e = this._parts[s], r = 0, l = e.length, a = l - 1; l > r; a = r++) + (i = e[r]), + (n = e[a]), + i.y > t.y != n.y > t.y && + t.x < ((n.x - i.x) * (t.y - i.y)) / (n.y - i.y) + i.x && + (u = !u); + return u || o.Polyline.prototype._containsPoint.call(this, t, !0); + }), + (o.CircleMarker.prototype._containsPoint = function (t) { + return t.distanceTo(this._point) <= this._radius + this._clickTolerance(); + }), + (o.GeoJSON = o.FeatureGroup.extend({ + initialize: function (t, e) { + o.setOptions(this, e), (this._layers = {}), t && this.addData(t); + }, + addData: function (t) { + var e, + i, + n, + s = o.Util.isArray(t) ? t : t.features; + if (s) { + for (e = 0, i = s.length; i > e; e++) + (n = s[e]), + (n.geometries || n.geometry || n.features || n.coordinates) && this.addData(n); + return this; + } + var r = this.options; + if (r.filter && !r.filter(t)) return this; + var a = o.GeoJSON.geometryToLayer(t, r); + return a + ? ((a.feature = o.GeoJSON.asFeature(t)), + (a.defaultOptions = a.options), + this.resetStyle(a), + r.onEachFeature && r.onEachFeature(t, a), + this.addLayer(a)) + : this; + }, + resetStyle: function (t) { + return (t.options = t.defaultOptions), this._setLayerStyle(t, this.options.style), this; + }, + setStyle: function (t) { + return this.eachLayer(function (e) { + this._setLayerStyle(e, t); + }, this); + }, + _setLayerStyle: function (t, e) { + "function" == typeof e && (e = e(t.feature)), t.setStyle && t.setStyle(e); + }, + })), + o.extend(o.GeoJSON, { + geometryToLayer: function (t, e) { + var i, + n, + s, + r, + a = "Feature" === t.type ? t.geometry : t, + h = a ? a.coordinates : null, + l = [], + u = e && e.pointToLayer, + c = (e && e.coordsToLatLng) || this.coordsToLatLng; + if (!h && !a) return null; + switch (a.type) { + case "Point": + return (i = c(h)), u ? u(t, i) : new o.Marker(i); + case "MultiPoint": + for (s = 0, r = h.length; r > s; s++) + (i = c(h[s])), l.push(u ? u(t, i) : new o.Marker(i)); + return new o.FeatureGroup(l); + case "LineString": + case "MultiLineString": + return ( + (n = this.coordsToLatLngs(h, "LineString" === a.type ? 0 : 1, c)), + new o.Polyline(n, e) + ); + case "Polygon": + case "MultiPolygon": + return ( + (n = this.coordsToLatLngs(h, "Polygon" === a.type ? 1 : 2, c)), + new o.Polygon(n, e) + ); + case "GeometryCollection": + for (s = 0, r = a.geometries.length; r > s; s++) { + var d = this.geometryToLayer( + { geometry: a.geometries[s], type: "Feature", properties: t.properties }, + e + ); + d && l.push(d); + } + return new o.FeatureGroup(l); + default: + throw new Error("Invalid GeoJSON object."); + } + }, + coordsToLatLng: function (t) { + return new o.LatLng(t[1], t[0], t[2]); + }, + coordsToLatLngs: function (t, e, i) { + for (var n, o = [], s = 0, r = t.length; r > s; s++) + (n = e ? this.coordsToLatLngs(t[s], e - 1, i) : (i || this.coordsToLatLng)(t[s])), + o.push(n); + return o; + }, + latLngToCoords: function (t) { + return t.alt !== i ? [t.lng, t.lat, t.alt] : [t.lng, t.lat]; + }, + latLngsToCoords: function (t, e, i) { + for (var n = [], s = 0, r = t.length; r > s; s++) + n.push(e ? o.GeoJSON.latLngsToCoords(t[s], e - 1, i) : o.GeoJSON.latLngToCoords(t[s])); + return !e && i && n.push(n[0]), n; + }, + getFeature: function (t, e) { + return t.feature ? o.extend({}, t.feature, { geometry: e }) : o.GeoJSON.asFeature(e); + }, + asFeature: function (t) { + return "Feature" === t.type ? t : { type: "Feature", properties: {}, geometry: t }; + }, + }); + var r = { + toGeoJSON: function () { + return o.GeoJSON.getFeature(this, { + type: "Point", + coordinates: o.GeoJSON.latLngToCoords(this.getLatLng()), + }); + }, + }; + o.Marker.include(r), + o.Circle.include(r), + o.CircleMarker.include(r), + (o.Polyline.prototype.toGeoJSON = function () { + var t = !o.Polyline._flat(this._latlngs), + e = o.GeoJSON.latLngsToCoords(this._latlngs, t ? 1 : 0); + return o.GeoJSON.getFeature(this, { type: (t ? "Multi" : "") + "LineString", coordinates: e }); + }), + (o.Polygon.prototype.toGeoJSON = function () { + var t = !o.Polyline._flat(this._latlngs), + e = t && !o.Polyline._flat(this._latlngs[0]), + i = o.GeoJSON.latLngsToCoords(this._latlngs, e ? 2 : t ? 1 : 0, !0); + return ( + t || (i = [i]), + o.GeoJSON.getFeature(this, { type: (e ? "Multi" : "") + "Polygon", coordinates: i }) + ); + }), + o.LayerGroup.include({ + toMultiPoint: function () { + var t = []; + return ( + this.eachLayer(function (e) { + t.push(e.toGeoJSON().geometry.coordinates); + }), + o.GeoJSON.getFeature(this, { type: "MultiPoint", coordinates: t }) + ); + }, + toGeoJSON: function () { + var t = this.feature && this.feature.geometry && this.feature.geometry.type; + if ("MultiPoint" === t) return this.toMultiPoint(); + var e = "GeometryCollection" === t, + i = []; + return ( + this.eachLayer(function (t) { + if (t.toGeoJSON) { + var n = t.toGeoJSON(); + i.push(e ? n.geometry : o.GeoJSON.asFeature(n)); + } + }), + e + ? o.GeoJSON.getFeature(this, { geometries: i, type: "GeometryCollection" }) + : { type: "FeatureCollection", features: i } + ); + }, + }), + (o.geoJson = function (t, e) { + return new o.GeoJSON(t, e); + }); + var a = "_leaflet_events"; + (o.DomEvent = { + on: function (t, e, i, n) { + if ("object" == typeof e) for (var s in e) this._on(t, s, e[s], i); + else { + e = o.Util.splitWords(e); + for (var r = 0, a = e.length; a > r; r++) this._on(t, e[r], i, n); + } + return this; + }, + off: function (t, e, i, n) { + if ("object" == typeof e) for (var s in e) this._off(t, s, e[s], i); + else { + e = o.Util.splitWords(e); + for (var r = 0, a = e.length; a > r; r++) this._off(t, e[r], i, n); + } + return this; + }, + _on: function (e, i, n, s) { + var r = i + o.stamp(n) + (s ? "_" + o.stamp(s) : ""); + if (e[a] && e[a][r]) return this; + var h = function (i) { + return n.call(s || e, i || t.event); + }, + l = h; + return ( + o.Browser.pointer && 0 === i.indexOf("touch") + ? this.addPointerListener(e, i, h, r) + : o.Browser.touch && "dblclick" === i && this.addDoubleTapListener + ? this.addDoubleTapListener(e, h, r) + : "addEventListener" in e + ? "mousewheel" === i + ? (e.addEventListener("DOMMouseScroll", h, !1), e.addEventListener(i, h, !1)) + : "mouseenter" === i || "mouseleave" === i + ? ((h = function (i) { + (i = i || t.event), o.DomEvent._isExternalTarget(e, i) && l(i); + }), + e.addEventListener("mouseenter" === i ? "mouseover" : "mouseout", h, !1)) + : ("click" === i && + o.Browser.android && + (h = function (t) { + return o.DomEvent._filterClick(t, l); + }), + e.addEventListener(i, h, !1)) + : "attachEvent" in e && e.attachEvent("on" + i, h), + (e[a] = e[a] || {}), + (e[a][r] = h), + this + ); + }, + _off: function (t, e, i, n) { + var s = e + o.stamp(i) + (n ? "_" + o.stamp(n) : ""), + r = t[a] && t[a][s]; + return r + ? (o.Browser.pointer && 0 === e.indexOf("touch") + ? this.removePointerListener(t, e, s) + : o.Browser.touch && "dblclick" === e && this.removeDoubleTapListener + ? this.removeDoubleTapListener(t, s) + : "removeEventListener" in t + ? "mousewheel" === e + ? (t.removeEventListener("DOMMouseScroll", r, !1), + t.removeEventListener(e, r, !1)) + : t.removeEventListener( + "mouseenter" === e ? "mouseover" : "mouseleave" === e ? "mouseout" : e, + r, + !1 + ) + : "detachEvent" in t && t.detachEvent("on" + e, r), + (t[a][s] = null), + this) + : this; + }, + stopPropagation: function (t) { + return ( + t.stopPropagation + ? t.stopPropagation() + : t.originalEvent + ? (t.originalEvent._stopped = !0) + : (t.cancelBubble = !0), + o.DomEvent._skipped(t), + this + ); + }, + disableScrollPropagation: function (t) { + return o.DomEvent.on(t, "mousewheel MozMousePixelScroll", o.DomEvent.stopPropagation); + }, + disableClickPropagation: function (t) { + var e = o.DomEvent.stopPropagation; + return ( + o.DomEvent.on(t, o.Draggable.START.join(" "), e), + o.DomEvent.on(t, { click: o.DomEvent._fakeStop, dblclick: e }) + ); + }, + preventDefault: function (t) { + return t.preventDefault ? t.preventDefault() : (t.returnValue = !1), this; + }, + stop: function (t) { + return o.DomEvent.preventDefault(t).stopPropagation(t); + }, + getMousePosition: function (t, e) { + if (!e) return new o.Point(t.clientX, t.clientY); + var i = e.getBoundingClientRect(); + return new o.Point(t.clientX - i.left - e.clientLeft, t.clientY - i.top - e.clientTop); + }, + getWheelDelta: function (t) { + var e = 0; + return t.wheelDelta && (e = t.wheelDelta / 120), t.detail && (e = -t.detail / 3), e; + }, + _skipEvents: {}, + _fakeStop: function (t) { + o.DomEvent._skipEvents[t.type] = !0; + }, + _skipped: function (t) { + var e = this._skipEvents[t.type]; + return (this._skipEvents[t.type] = !1), e; + }, + _isExternalTarget: function (t, e) { + var i = e.relatedTarget; + if (!i) return !0; + try { + for (; i && i !== t; ) i = i.parentNode; + } catch (n) { + return !1; + } + return i !== t; + }, + _filterClick: function (t, e) { + var i = t.timeStamp || t.originalEvent.timeStamp, + n = o.DomEvent._lastClick && i - o.DomEvent._lastClick; + return (n && n > 100 && 500 > n) || (t.target._simulatedClick && !t._simulated) + ? void o.DomEvent.stop(t) + : ((o.DomEvent._lastClick = i), void e(t)); + }, + }), + (o.DomEvent.addListener = o.DomEvent.on), + (o.DomEvent.removeListener = o.DomEvent.off), + (o.Draggable = o.Evented.extend({ + statics: { + START: o.Browser.touch ? ["touchstart", "mousedown"] : ["mousedown"], + END: { + mousedown: "mouseup", + touchstart: "touchend", + pointerdown: "touchend", + MSPointerDown: "touchend", + }, + MOVE: { + mousedown: "mousemove", + touchstart: "touchmove", + pointerdown: "touchmove", + MSPointerDown: "touchmove", + }, + }, + initialize: function (t, e, i) { + (this._element = t), (this._dragStartTarget = e || t), (this._preventOutline = i); + }, + enable: function () { + this._enabled || + (o.DomEvent.on(this._dragStartTarget, o.Draggable.START.join(" "), this._onDown, this), + (this._enabled = !0)); + }, + disable: function () { + this._enabled && + (o.DomEvent.off(this._dragStartTarget, o.Draggable.START.join(" "), this._onDown, this), + (this._enabled = !1), + (this._moved = !1)); + }, + _onDown: function (t) { + if ( + ((this._moved = !1), + !o.DomUtil.hasClass(this._element, "leaflet-zoom-anim") && + !( + o.Draggable._dragging || + t.shiftKey || + (1 !== t.which && 1 !== t.button && !t.touches) + ) && + this._enabled && + ((o.Draggable._dragging = !0), + this._preventOutline && o.DomUtil.preventOutline(this._element), + o.DomUtil.disableImageDrag(), + o.DomUtil.disableTextSelection(), + !this._moving)) + ) { + this.fire("down"); + var i = t.touches ? t.touches[0] : t; + (this._startPoint = new o.Point(i.clientX, i.clientY)), + (this._startPos = this._newPos = o.DomUtil.getPosition(this._element)), + o.DomEvent.on(e, o.Draggable.MOVE[t.type], this._onMove, this).on( + e, + o.Draggable.END[t.type], + this._onUp, + this + ); + } + }, + _onMove: function (t) { + if (t.touches && t.touches.length > 1) return void (this._moved = !0); + var i = t.touches && 1 === t.touches.length ? t.touches[0] : t, + n = new o.Point(i.clientX, i.clientY), + s = n.subtract(this._startPoint); + (s.x || s.y) && + ((o.Browser.touch && Math.abs(s.x) + Math.abs(s.y) < 3) || + (o.DomEvent.preventDefault(t), + this._moved || + (this.fire("dragstart"), + (this._moved = !0), + (this._startPos = o.DomUtil.getPosition(this._element).subtract(s)), + o.DomUtil.addClass(e.body, "leaflet-dragging"), + (this._lastTarget = t.target || t.srcElement), + o.DomUtil.addClass(this._lastTarget, "leaflet-drag-target")), + (this._newPos = this._startPos.add(s)), + (this._moving = !0), + o.Util.cancelAnimFrame(this._animRequest), + (this._lastEvent = t), + (this._animRequest = o.Util.requestAnimFrame(this._updatePosition, this, !0)))); + }, + _updatePosition: function () { + var t = { originalEvent: this._lastEvent }; + this.fire("predrag", t), + o.DomUtil.setPosition(this._element, this._newPos), + this.fire("drag", t); + }, + _onUp: function () { + o.DomUtil.removeClass(e.body, "leaflet-dragging"), + this._lastTarget && + (o.DomUtil.removeClass(this._lastTarget, "leaflet-drag-target"), + (this._lastTarget = null)); + for (var t in o.Draggable.MOVE) + o.DomEvent.off(e, o.Draggable.MOVE[t], this._onMove, this).off( + e, + o.Draggable.END[t], + this._onUp, + this + ); + o.DomUtil.enableImageDrag(), + o.DomUtil.enableTextSelection(), + this._moved && + this._moving && + (o.Util.cancelAnimFrame(this._animRequest), + this.fire("dragend", { distance: this._newPos.distanceTo(this._startPos) })), + (this._moving = !1), + (o.Draggable._dragging = !1); + }, + })), + (o.Handler = o.Class.extend({ + initialize: function (t) { + this._map = t; + }, + enable: function () { + this._enabled || ((this._enabled = !0), this.addHooks()); + }, + disable: function () { + this._enabled && ((this._enabled = !1), this.removeHooks()); + }, + enabled: function () { + return !!this._enabled; + }, + })), + o.Map.mergeOptions({ + dragging: !0, + inertia: !o.Browser.android23, + inertiaDeceleration: 3400, + inertiaMaxSpeed: 1 / 0, + easeLinearity: 0.2, + worldCopyJump: !1, + }), + (o.Map.Drag = o.Handler.extend({ + addHooks: function () { + if (!this._draggable) { + var t = this._map; + (this._draggable = new o.Draggable(t._mapPane, t._container)), + this._draggable.on( + { + down: this._onDown, + dragstart: this._onDragStart, + drag: this._onDrag, + dragend: this._onDragEnd, + }, + this + ), + this._draggable.on("predrag", this._onPreDragLimit, this), + t.options.worldCopyJump && + (this._draggable.on("predrag", this._onPreDragWrap, this), + t.on("zoomend", this._onZoomEnd, this), + t.whenReady(this._onZoomEnd, this)); + } + o.DomUtil.addClass(this._map._container, "leaflet-grab"), this._draggable.enable(); + }, + removeHooks: function () { + o.DomUtil.removeClass(this._map._container, "leaflet-grab"), this._draggable.disable(); + }, + moved: function () { + return this._draggable && this._draggable._moved; + }, + _onDown: function () { + this._map.stop(); + }, + _onDragStart: function () { + var t = this._map; + if (this._map.options.maxBounds && this._map.options.maxBoundsViscosity) { + var e = o.latLngBounds(this._map.options.maxBounds); + (this._offsetLimit = o.bounds( + this._map.latLngToContainerPoint(e.getNorthWest()).multiplyBy(-1), + this._map + .latLngToContainerPoint(e.getSouthEast()) + .multiplyBy(-1) + .add(this._map.getSize()) + )), + (this._viscosity = Math.min(1, Math.max(0, this._map.options.maxBoundsViscosity))); + } else this._offsetLimit = null; + t.fire("movestart").fire("dragstart"), + t.options.inertia && ((this._positions = []), (this._times = [])); + }, + _onDrag: function (t) { + if (this._map.options.inertia) { + var e = (this._lastTime = +new Date()), + i = (this._lastPos = this._draggable._absPos || this._draggable._newPos); + this._positions.push(i), + this._times.push(e), + e - this._times[0] > 50 && (this._positions.shift(), this._times.shift()); + } + this._map.fire("move", t).fire("drag", t); + }, + _onZoomEnd: function () { + var t = this._map.getSize().divideBy(2), + e = this._map.latLngToLayerPoint([0, 0]); + (this._initialWorldOffset = e.subtract(t).x), + (this._worldWidth = this._map.getPixelWorldBounds().getSize().x); + }, + _viscousLimit: function (t, e) { + return t - (t - e) * this._viscosity; + }, + _onPreDragLimit: function () { + if (this._viscosity && this._offsetLimit) { + var t = this._draggable._newPos.subtract(this._draggable._startPos), + e = this._offsetLimit; + t.x < e.min.x && (t.x = this._viscousLimit(t.x, e.min.x)), + t.y < e.min.y && (t.y = this._viscousLimit(t.y, e.min.y)), + t.x > e.max.x && (t.x = this._viscousLimit(t.x, e.max.x)), + t.y > e.max.y && (t.y = this._viscousLimit(t.y, e.max.y)), + (this._draggable._newPos = this._draggable._startPos.add(t)); + } + }, + _onPreDragWrap: function () { + var t = this._worldWidth, + e = Math.round(t / 2), + i = this._initialWorldOffset, + n = this._draggable._newPos.x, + o = ((n - e + i) % t) + e - i, + s = ((n + e + i) % t) - e - i, + r = Math.abs(o + i) < Math.abs(s + i) ? o : s; + (this._draggable._absPos = this._draggable._newPos.clone()), (this._draggable._newPos.x = r); + }, + _onDragEnd: function (t) { + var e = this._map, + i = e.options, + n = !i.inertia || this._times.length < 2; + if ((e.fire("dragend", t), n)) e.fire("moveend"); + else { + var s = this._lastPos.subtract(this._positions[0]), + r = (this._lastTime - this._times[0]) / 1e3, + a = i.easeLinearity, + h = s.multiplyBy(a / r), + l = h.distanceTo([0, 0]), + u = Math.min(i.inertiaMaxSpeed, l), + c = h.multiplyBy(u / l), + d = u / (i.inertiaDeceleration * a), + _ = c.multiplyBy(-d / 2).round(); + _.x || _.y + ? ((_ = e._limitOffset(_, e.options.maxBounds)), + o.Util.requestAnimFrame(function () { + e.panBy(_, { duration: d, easeLinearity: a, noMoveStart: !0, animate: !0 }); + })) + : e.fire("moveend"); + } + }, + })), + o.Map.addInitHook("addHandler", "dragging", o.Map.Drag), + o.Map.mergeOptions({ doubleClickZoom: !0 }), + (o.Map.DoubleClickZoom = o.Handler.extend({ + addHooks: function () { + this._map.on("dblclick", this._onDoubleClick, this); + }, + removeHooks: function () { + this._map.off("dblclick", this._onDoubleClick, this); + }, + _onDoubleClick: function (t) { + var e = this._map, + i = e.getZoom(), + n = t.originalEvent.shiftKey ? Math.ceil(i) - 1 : Math.floor(i) + 1; + "center" === e.options.doubleClickZoom ? e.setZoom(n) : e.setZoomAround(t.containerPoint, n); + }, + })), + o.Map.addInitHook("addHandler", "doubleClickZoom", o.Map.DoubleClickZoom), + o.Map.mergeOptions({ scrollWheelZoom: !0, wheelDebounceTime: 40 }), + (o.Map.ScrollWheelZoom = o.Handler.extend({ + addHooks: function () { + o.DomEvent.on( + this._map._container, + { mousewheel: this._onWheelScroll, MozMousePixelScroll: o.DomEvent.preventDefault }, + this + ), + (this._delta = 0); + }, + removeHooks: function () { + o.DomEvent.off( + this._map._container, + { mousewheel: this._onWheelScroll, MozMousePixelScroll: o.DomEvent.preventDefault }, + this + ); + }, + _onWheelScroll: function (t) { + var e = o.DomEvent.getWheelDelta(t), + i = this._map.options.wheelDebounceTime; + (this._delta += e), + (this._lastMousePos = this._map.mouseEventToContainerPoint(t)), + this._startTime || (this._startTime = +new Date()); + var n = Math.max(i - (+new Date() - this._startTime), 0); + clearTimeout(this._timer), + (this._timer = setTimeout(o.bind(this._performZoom, this), n)), + o.DomEvent.stop(t); + }, + _performZoom: function () { + var t = this._map, + e = this._delta, + i = t.getZoom(); + t.stop(), + (e = e > 0 ? Math.ceil(e) : Math.floor(e)), + (e = Math.max(Math.min(e, 4), -4)), + (e = t._limitZoom(i + e) - i), + (this._delta = 0), + (this._startTime = null), + e && + ("center" === t.options.scrollWheelZoom + ? t.setZoom(i + e) + : t.setZoomAround(this._lastMousePos, i + e)); + }, + })), + o.Map.addInitHook("addHandler", "scrollWheelZoom", o.Map.ScrollWheelZoom), + o.extend(o.DomEvent, { + _touchstart: o.Browser.msPointer + ? "MSPointerDown" + : o.Browser.pointer + ? "pointerdown" + : "touchstart", + _touchend: o.Browser.msPointer ? "MSPointerUp" : o.Browser.pointer ? "pointerup" : "touchend", + addDoubleTapListener: function (t, e, i) { + function n(t) { + var e; + if (((e = o.Browser.pointer ? o.DomEvent._pointersCount : t.touches.length), !(e > 1))) { + var i = Date.now(), + n = i - (r || i); + (a = t.touches ? t.touches[0] : t), (h = n > 0 && l >= n), (r = i); + } + } - function r(t) { return (Math.exp(t) - Math.exp(-t)) / 2 } + function s() { + if (h && !a.cancelBubble) { + if (o.Browser.pointer) { + var t, + i, + n = {}; + for (i in a) (t = a[i]), (n[i] = t && t.bind ? t.bind(a) : t); + a = n; + } + (a.type = "dblclick"), e(a), (r = null); + } + } + var r, + a, + h = !1, + l = 250, + u = "_leaflet_", + c = this._touchstart, + d = this._touchend; + return ( + (t[u + c + i] = n), + (t[u + d + i] = s), + t.addEventListener(c, n, !1), + t.addEventListener(d, s, !1), + this + ); + }, + removeDoubleTapListener: function (t, e) { + var i = "_leaflet_", + n = t[i + this._touchend + e]; + return ( + t.removeEventListener(this._touchstart, t[i + this._touchstart + e], !1), + t.removeEventListener(this._touchend, n, !1), + this + ); + }, + }), + o.extend(o.DomEvent, { + POINTER_DOWN: o.Browser.msPointer ? "MSPointerDown" : "pointerdown", + POINTER_MOVE: o.Browser.msPointer ? "MSPointerMove" : "pointermove", + POINTER_UP: o.Browser.msPointer ? "MSPointerUp" : "pointerup", + POINTER_CANCEL: o.Browser.msPointer ? "MSPointerCancel" : "pointercancel", + _pointers: {}, + _pointersCount: 0, + addPointerListener: function (t, e, i, n) { + return ( + "touchstart" === e + ? this._addPointerStart(t, i, n) + : "touchmove" === e + ? this._addPointerMove(t, i, n) + : "touchend" === e && this._addPointerEnd(t, i, n), + this + ); + }, + removePointerListener: function (t, e, i) { + var n = t["_leaflet_" + e + i]; + return ( + "touchstart" === e + ? t.removeEventListener(this.POINTER_DOWN, n, !1) + : "touchmove" === e + ? t.removeEventListener(this.POINTER_MOVE, n, !1) + : "touchend" === e && + (t.removeEventListener(this.POINTER_UP, n, !1), + t.removeEventListener(this.POINTER_CANCEL, n, !1)), + this + ); + }, + _addPointerStart: function (t, i, n) { + var s = o.bind(function (t) { + "mouse" !== t.pointerType && + t.pointerType !== t.MSPOINTER_TYPE_MOUSE && + o.DomEvent.preventDefault(t), + this._handlePointer(t, i); + }, this); + if ( + ((t["_leaflet_touchstart" + n] = s), + t.addEventListener(this.POINTER_DOWN, s, !1), + !this._pointerDocListener) + ) { + var r = o.bind(this._globalPointerUp, this); + e.documentElement.addEventListener( + this.POINTER_DOWN, + o.bind(this._globalPointerDown, this), + !0 + ), + e.documentElement.addEventListener( + this.POINTER_MOVE, + o.bind(this._globalPointerMove, this), + !0 + ), + e.documentElement.addEventListener(this.POINTER_UP, r, !0), + e.documentElement.addEventListener(this.POINTER_CANCEL, r, !0), + (this._pointerDocListener = !0); + } + }, + _globalPointerDown: function (t) { + (this._pointers[t.pointerId] = t), this._pointersCount++; + }, + _globalPointerMove: function (t) { + this._pointers[t.pointerId] && (this._pointers[t.pointerId] = t); + }, + _globalPointerUp: function (t) { + delete this._pointers[t.pointerId], this._pointersCount--; + }, + _handlePointer: function (t, e) { + t.touches = []; + for (var i in this._pointers) t.touches.push(this._pointers[i]); + (t.changedTouches = [t]), e(t); + }, + _addPointerMove: function (t, e, i) { + var n = o.bind(function (t) { + ((t.pointerType !== t.MSPOINTER_TYPE_MOUSE && "mouse" !== t.pointerType) || + 0 !== t.buttons) && + this._handlePointer(t, e); + }, this); + (t["_leaflet_touchmove" + i] = n), t.addEventListener(this.POINTER_MOVE, n, !1); + }, + _addPointerEnd: function (t, e, i) { + var n = o.bind(function (t) { + this._handlePointer(t, e); + }, this); + (t["_leaflet_touchend" + i] = n), + t.addEventListener(this.POINTER_UP, n, !1), + t.addEventListener(this.POINTER_CANCEL, n, !1); + }, + }), + o.Map.mergeOptions({ touchZoom: o.Browser.touch && !o.Browser.android23, bounceAtZoomLimits: !0 }), + (o.Map.TouchZoom = o.Handler.extend({ + addHooks: function () { + o.DomEvent.on(this._map._container, "touchstart", this._onTouchStart, this); + }, + removeHooks: function () { + o.DomEvent.off(this._map._container, "touchstart", this._onTouchStart, this); + }, + _onTouchStart: function (t) { + var i = this._map; + if (t.touches && 2 === t.touches.length && !i._animatingZoom && !this._zooming) { + var n = i.mouseEventToContainerPoint(t.touches[0]), + s = i.mouseEventToContainerPoint(t.touches[1]); + (this._centerPoint = i.getSize()._divideBy(2)), + (this._startLatLng = i.containerPointToLatLng(this._centerPoint)), + "center" !== i.options.touchZoom && + (this._pinchStartLatLng = i.containerPointToLatLng(n.add(s)._divideBy(2))), + (this._startDist = n.distanceTo(s)), + (this._startZoom = i.getZoom()), + (this._moved = !1), + (this._zooming = !0), + i.stop(), + o.DomEvent.on(e, "touchmove", this._onTouchMove, this).on( + e, + "touchend", + this._onTouchEnd, + this + ), + o.DomEvent.preventDefault(t); + } + }, + _onTouchMove: function (t) { + if (t.touches && 2 === t.touches.length && this._zooming) { + var e = this._map, + i = e.mouseEventToContainerPoint(t.touches[0]), + n = e.mouseEventToContainerPoint(t.touches[1]), + s = i.distanceTo(n) / this._startDist; + if ( + ((this._zoom = e.getScaleZoom(s, this._startZoom)), "center" === e.options.touchZoom) + ) { + if (((this._center = this._startLatLng), 1 === s)) return; + } else { + var r = i._add(n)._divideBy(2)._subtract(this._centerPoint); + if (1 === s && 0 === r.x && 0 === r.y) return; + this._center = e.unproject(e.project(this._pinchStartLatLng).subtract(r)); + } + if ( + e.options.bounceAtZoomLimits || + !((this._zoom <= e.getMinZoom() && 1 > s) || (this._zoom >= e.getMaxZoom() && s > 1)) + ) { + this._moved || (e._moveStart(!0), (this._moved = !0)), + o.Util.cancelAnimFrame(this._animRequest); + var a = o.bind(e._move, e, this._center, this._zoom, { pinch: !0, round: !1 }); + (this._animRequest = o.Util.requestAnimFrame(a, this, !0)), + o.DomEvent.preventDefault(t); + } + } + }, + _onTouchEnd: function () { + if (!this._moved || !this._zooming) return void (this._zooming = !1); + (this._zooming = !1), + o.Util.cancelAnimFrame(this._animRequest), + o.DomEvent.off(e, "touchmove", this._onTouchMove).off(e, "touchend", this._onTouchEnd); + var t = this._zoom; + (t = this._map._limitZoom(t - this._startZoom > 0 ? Math.ceil(t) : Math.floor(t))), + this._map._animateZoom(this._center, t, !0, !0); + }, + })), + o.Map.addInitHook("addHandler", "touchZoom", o.Map.TouchZoom), + o.Map.mergeOptions({ tap: !0, tapTolerance: 15 }), + (o.Map.Tap = o.Handler.extend({ + addHooks: function () { + o.DomEvent.on(this._map._container, "touchstart", this._onDown, this); + }, + removeHooks: function () { + o.DomEvent.off(this._map._container, "touchstart", this._onDown, this); + }, + _onDown: function (t) { + if (t.touches) { + if ((o.DomEvent.preventDefault(t), (this._fireClick = !0), t.touches.length > 1)) + return (this._fireClick = !1), void clearTimeout(this._holdTimeout); + var i = t.touches[0], + n = i.target; + (this._startPos = this._newPos = new o.Point(i.clientX, i.clientY)), + n.tagName && + "a" === n.tagName.toLowerCase() && + o.DomUtil.addClass(n, "leaflet-active"), + (this._holdTimeout = setTimeout( + o.bind(function () { + this._isTapValid() && + ((this._fireClick = !1), + this._onUp(), + this._simulateEvent("contextmenu", i)); + }, this), + 1e3 + )), + this._simulateEvent("mousedown", i), + o.DomEvent.on(e, { touchmove: this._onMove, touchend: this._onUp }, this); + } + }, + _onUp: function (t) { + if ( + (clearTimeout(this._holdTimeout), + o.DomEvent.off(e, { touchmove: this._onMove, touchend: this._onUp }, this), + this._fireClick && t && t.changedTouches) + ) { + var i = t.changedTouches[0], + n = i.target; + n && + n.tagName && + "a" === n.tagName.toLowerCase() && + o.DomUtil.removeClass(n, "leaflet-active"), + this._simulateEvent("mouseup", i), + this._isTapValid() && this._simulateEvent("click", i); + } + }, + _isTapValid: function () { + return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance; + }, + _onMove: function (t) { + var e = t.touches[0]; + (this._newPos = new o.Point(e.clientX, e.clientY)), this._simulateEvent("mousemove", e); + }, + _simulateEvent: function (i, n) { + var o = e.createEvent("MouseEvents"); + (o._simulated = !0), + (n.target._simulatedClick = !0), + o.initMouseEvent( + i, + !0, + !0, + t, + 1, + n.screenX, + n.screenY, + n.clientX, + n.clientY, + !1, + !1, + !1, + !1, + 0, + null + ), + n.target.dispatchEvent(o); + }, + })), + o.Browser.touch && !o.Browser.pointer && o.Map.addInitHook("addHandler", "tap", o.Map.Tap), + o.Map.mergeOptions({ boxZoom: !0 }), + (o.Map.BoxZoom = o.Handler.extend({ + initialize: function (t) { + (this._map = t), (this._container = t._container), (this._pane = t._panes.overlayPane); + }, + addHooks: function () { + o.DomEvent.on(this._container, "mousedown", this._onMouseDown, this); + }, + removeHooks: function () { + o.DomEvent.off(this._container, "mousedown", this._onMouseDown, this); + }, + moved: function () { + return this._moved; + }, + _resetState: function () { + this._moved = !1; + }, + _onMouseDown: function (t) { + return !t.shiftKey || (1 !== t.which && 1 !== t.button) + ? !1 + : (this._resetState(), + o.DomUtil.disableTextSelection(), + o.DomUtil.disableImageDrag(), + (this._startPoint = this._map.mouseEventToContainerPoint(t)), + void o.DomEvent.on( + e, + { + contextmenu: o.DomEvent.stop, + mousemove: this._onMouseMove, + mouseup: this._onMouseUp, + keydown: this._onKeyDown, + }, + this + )); + }, + _onMouseMove: function (t) { + this._moved || + ((this._moved = !0), + (this._box = o.DomUtil.create("div", "leaflet-zoom-box", this._container)), + o.DomUtil.addClass(this._container, "leaflet-crosshair"), + this._map.fire("boxzoomstart")), + (this._point = this._map.mouseEventToContainerPoint(t)); + var e = new o.Bounds(this._point, this._startPoint), + i = e.getSize(); + o.DomUtil.setPosition(this._box, e.min), + (this._box.style.width = i.x + "px"), + (this._box.style.height = i.y + "px"); + }, + _finish: function () { + this._moved && + (o.DomUtil.remove(this._box), + o.DomUtil.removeClass(this._container, "leaflet-crosshair")), + o.DomUtil.enableTextSelection(), + o.DomUtil.enableImageDrag(), + o.DomEvent.off( + e, + { + contextmenu: o.DomEvent.stop, + mousemove: this._onMouseMove, + mouseup: this._onMouseUp, + keydown: this._onKeyDown, + }, + this + ); + }, + _onMouseUp: function (t) { + if ((1 === t.which || 1 === t.button) && (this._finish(), this._moved)) { + setTimeout(o.bind(this._resetState, this), 0); + var e = new o.LatLngBounds( + this._map.containerPointToLatLng(this._startPoint), + this._map.containerPointToLatLng(this._point) + ); + this._map.fitBounds(e).fire("boxzoomend", { boxZoomBounds: e }); + } + }, + _onKeyDown: function (t) { + 27 === t.keyCode && this._finish(); + }, + })), + o.Map.addInitHook("addHandler", "boxZoom", o.Map.BoxZoom), + o.Map.mergeOptions({ keyboard: !0, keyboardPanOffset: 80, keyboardZoomOffset: 1 }), + (o.Map.Keyboard = o.Handler.extend({ + keyCodes: { + left: [37], + right: [39], + down: [40], + up: [38], + zoomIn: [187, 107, 61, 171], + zoomOut: [189, 109, 54, 173], + }, + initialize: function (t) { + (this._map = t), + this._setPanOffset(t.options.keyboardPanOffset), + this._setZoomOffset(t.options.keyboardZoomOffset); + }, + addHooks: function () { + var t = this._map._container; + t.tabIndex <= 0 && (t.tabIndex = "0"), + o.DomEvent.on( + t, + { focus: this._onFocus, blur: this._onBlur, mousedown: this._onMouseDown }, + this + ), + this._map.on({ focus: this._addHooks, blur: this._removeHooks }, this); + }, + removeHooks: function () { + this._removeHooks(), + o.DomEvent.off( + this._map._container, + { focus: this._onFocus, blur: this._onBlur, mousedown: this._onMouseDown }, + this + ), + this._map.off({ focus: this._addHooks, blur: this._removeHooks }, this); + }, + _onMouseDown: function () { + if (!this._focused) { + var i = e.body, + n = e.documentElement, + o = i.scrollTop || n.scrollTop, + s = i.scrollLeft || n.scrollLeft; + this._map._container.focus(), t.scrollTo(s, o); + } + }, + _onFocus: function () { + (this._focused = !0), this._map.fire("focus"); + }, + _onBlur: function () { + (this._focused = !1), this._map.fire("blur"); + }, + _setPanOffset: function (t) { + var e, + i, + n = (this._panKeys = {}), + o = this.keyCodes; + for (e = 0, i = o.left.length; i > e; e++) n[o.left[e]] = [-1 * t, 0]; + for (e = 0, i = o.right.length; i > e; e++) n[o.right[e]] = [t, 0]; + for (e = 0, i = o.down.length; i > e; e++) n[o.down[e]] = [0, t]; + for (e = 0, i = o.up.length; i > e; e++) n[o.up[e]] = [0, -1 * t]; + }, + _setZoomOffset: function (t) { + var e, + i, + n = (this._zoomKeys = {}), + o = this.keyCodes; + for (e = 0, i = o.zoomIn.length; i > e; e++) n[o.zoomIn[e]] = t; + for (e = 0, i = o.zoomOut.length; i > e; e++) n[o.zoomOut[e]] = -t; + }, + _addHooks: function () { + o.DomEvent.on(e, "keydown", this._onKeyDown, this); + }, + _removeHooks: function () { + o.DomEvent.off(e, "keydown", this._onKeyDown, this); + }, + _onKeyDown: function (t) { + if (!(t.altKey || t.ctrlKey || t.metaKey)) { + var e, + i = t.keyCode, + n = this._map; + if (i in this._panKeys) { + if (n._panAnim && n._panAnim._inProgress) return; + (e = this._panKeys[i]), + t.shiftKey && (e = o.point(e).multiplyBy(3)), + n.panBy(e), + n.options.maxBounds && n.panInsideBounds(n.options.maxBounds); + } else if (i in this._zoomKeys) + n.setZoom(n.getZoom() + (t.shiftKey ? 3 : 1) * this._zoomKeys[i]); + else { + if (27 !== i) return; + n.closePopup(); + } + o.DomEvent.stop(t); + } + }, + })), + o.Map.addInitHook("addHandler", "keyboard", o.Map.Keyboard), + (o.Handler.MarkerDrag = o.Handler.extend({ + initialize: function (t) { + this._marker = t; + }, + addHooks: function () { + var t = this._marker._icon; + this._draggable || (this._draggable = new o.Draggable(t, t, !0)), + this._draggable + .on( + { dragstart: this._onDragStart, drag: this._onDrag, dragend: this._onDragEnd }, + this + ) + .enable(), + o.DomUtil.addClass(t, "leaflet-marker-draggable"); + }, + removeHooks: function () { + this._draggable + .off({ dragstart: this._onDragStart, drag: this._onDrag, dragend: this._onDragEnd }, this) + .disable(), + this._marker._icon && + o.DomUtil.removeClass(this._marker._icon, "leaflet-marker-draggable"); + }, + moved: function () { + return this._draggable && this._draggable._moved; + }, + _onDragStart: function () { + this._marker.closePopup().fire("movestart").fire("dragstart"); + }, + _onDrag: function (t) { + var e = this._marker, + i = e._shadow, + n = o.DomUtil.getPosition(e._icon), + s = e._map.layerPointToLatLng(n); + i && o.DomUtil.setPosition(i, n), + (e._latlng = s), + (t.latlng = s), + e.fire("move", t).fire("drag", t); + }, + _onDragEnd: function (t) { + this._marker.fire("moveend").fire("dragend", t); + }, + })), + (o.Control = o.Class.extend({ + options: { position: "topright" }, + initialize: function (t) { + o.setOptions(this, t); + }, + getPosition: function () { + return this.options.position; + }, + setPosition: function (t) { + var e = this._map; + return e && e.removeControl(this), (this.options.position = t), e && e.addControl(this), this; + }, + getContainer: function () { + return this._container; + }, + addTo: function (t) { + this.remove(), (this._map = t); + var e = (this._container = this.onAdd(t)), + i = this.getPosition(), + n = t._controlCorners[i]; + return ( + o.DomUtil.addClass(e, "leaflet-control"), + -1 !== i.indexOf("bottom") ? n.insertBefore(e, n.firstChild) : n.appendChild(e), + this + ); + }, + remove: function () { + return this._map + ? (o.DomUtil.remove(this._container), + this.onRemove && this.onRemove(this._map), + (this._map = null), + this) + : this; + }, + _refocusOnMap: function (t) { + this._map && t && t.screenX > 0 && t.screenY > 0 && this._map.getContainer().focus(); + }, + })), + (o.control = function (t) { + return new o.Control(t); + }), + o.Map.include({ + addControl: function (t) { + return t.addTo(this), this; + }, + removeControl: function (t) { + return t.remove(), this; + }, + _initControlPos: function () { + function t(t, s) { + var r = i + t + " " + i + s; + e[t + s] = o.DomUtil.create("div", r, n); + } + var e = (this._controlCorners = {}), + i = "leaflet-", + n = (this._controlContainer = o.DomUtil.create( + "div", + i + "control-container", + this._container + )); + t("top", "left"), t("top", "right"), t("bottom", "left"), t("bottom", "right"); + }, + _clearControlPos: function () { + o.DomUtil.remove(this._controlContainer); + }, + }), + (o.Control.Zoom = o.Control.extend({ + options: { + position: "topleft", + zoomInText: "+", + zoomInTitle: "Zoom in", + zoomOutText: "-", + zoomOutTitle: "Zoom out", + }, + onAdd: function (t) { + var e = "leaflet-control-zoom", + i = o.DomUtil.create("div", e + " leaflet-bar"), + n = this.options; + return ( + (this._zoomInButton = this._createButton( + n.zoomInText, + n.zoomInTitle, + e + "-in", + i, + this._zoomIn + )), + (this._zoomOutButton = this._createButton( + n.zoomOutText, + n.zoomOutTitle, + e + "-out", + i, + this._zoomOut + )), + this._updateDisabled(), + t.on("zoomend zoomlevelschange", this._updateDisabled, this), + i + ); + }, + onRemove: function (t) { + t.off("zoomend zoomlevelschange", this._updateDisabled, this); + }, + disable: function () { + return (this._disabled = !0), this._updateDisabled(), this; + }, + enable: function () { + return (this._disabled = !1), this._updateDisabled(), this; + }, + _zoomIn: function (t) { + this._disabled || this._map.zoomIn(t.shiftKey ? 3 : 1); + }, + _zoomOut: function (t) { + this._disabled || this._map.zoomOut(t.shiftKey ? 3 : 1); + }, + _createButton: function (t, e, i, n, s) { + var r = o.DomUtil.create("a", i, n); + return ( + (r.innerHTML = t), + (r.href = "#"), + (r.title = e), + o.DomEvent.on(r, "mousedown dblclick", o.DomEvent.stopPropagation) + .on(r, "click", o.DomEvent.stop) + .on(r, "click", s, this) + .on(r, "click", this._refocusOnMap, this), + r + ); + }, + _updateDisabled: function () { + var t = this._map, + e = "leaflet-disabled"; + o.DomUtil.removeClass(this._zoomInButton, e), + o.DomUtil.removeClass(this._zoomOutButton, e), + (this._disabled || t._zoom === t.getMinZoom()) && + o.DomUtil.addClass(this._zoomOutButton, e), + (this._disabled || t._zoom === t.getMaxZoom()) && + o.DomUtil.addClass(this._zoomInButton, e); + }, + })), + o.Map.mergeOptions({ zoomControl: !0 }), + o.Map.addInitHook(function () { + this.options.zoomControl && + ((this.zoomControl = new o.Control.Zoom()), this.addControl(this.zoomControl)); + }), + (o.control.zoom = function (t) { + return new o.Control.Zoom(t); + }), + (o.Control.Attribution = o.Control.extend({ + options: { + position: "bottomright", + prefix: 'Leaflet', + }, + initialize: function (t) { + o.setOptions(this, t), (this._attributions = {}); + }, + onAdd: function (t) { + (this._container = o.DomUtil.create("div", "leaflet-control-attribution")), + o.DomEvent && o.DomEvent.disableClickPropagation(this._container); + for (var e in t._layers) + t._layers[e].getAttribution && this.addAttribution(t._layers[e].getAttribution()); + return this._update(), this._container; + }, + setPrefix: function (t) { + return (this.options.prefix = t), this._update(), this; + }, + addAttribution: function (t) { + return t + ? (this._attributions[t] || (this._attributions[t] = 0), + this._attributions[t]++, + this._update(), + this) + : this; + }, + removeAttribution: function (t) { + return t ? (this._attributions[t] && (this._attributions[t]--, this._update()), this) : this; + }, + _update: function () { + if (this._map) { + var t = []; + for (var e in this._attributions) this._attributions[e] && t.push(e); + var i = []; + this.options.prefix && i.push(this.options.prefix), + t.length && i.push(t.join(", ")), + (this._container.innerHTML = i.join(" | ")); + } + }, + })), + o.Map.mergeOptions({ attributionControl: !0 }), + o.Map.addInitHook(function () { + this.options.attributionControl && + (this.attributionControl = new o.Control.Attribution().addTo(this)); + }), + (o.control.attribution = function (t) { + return new o.Control.Attribution(t); + }), + (o.Control.Scale = o.Control.extend({ + options: { position: "bottomleft", maxWidth: 100, metric: !0, imperial: !0 }, + onAdd: function (t) { + var e = "leaflet-control-scale", + i = o.DomUtil.create("div", e), + n = this.options; + return ( + this._addScales(n, e + "-line", i), + t.on(n.updateWhenIdle ? "moveend" : "move", this._update, this), + t.whenReady(this._update, this), + i + ); + }, + onRemove: function (t) { + t.off(this.options.updateWhenIdle ? "moveend" : "move", this._update, this); + }, + _addScales: function (t, e, i) { + t.metric && (this._mScale = o.DomUtil.create("div", e, i)), + t.imperial && (this._iScale = o.DomUtil.create("div", e, i)); + }, + _update: function () { + var t = this._map, + e = t.getSize().y / 2, + i = t.distance( + t.containerPointToLatLng([0, e]), + t.containerPointToLatLng([this.options.maxWidth, e]) + ); + this._updateScales(i); + }, + _updateScales: function (t) { + this.options.metric && t && this._updateMetric(t), + this.options.imperial && t && this._updateImperial(t); + }, + _updateMetric: function (t) { + var e = this._getRoundNum(t), + i = 1e3 > e ? e + " m" : e / 1e3 + " km"; + this._updateScale(this._mScale, i, e / t); + }, + _updateImperial: function (t) { + var e, + i, + n, + o = 3.2808399 * t; + o > 5280 + ? ((e = o / 5280), + (i = this._getRoundNum(e)), + this._updateScale(this._iScale, i + " mi", i / e)) + : ((n = this._getRoundNum(o)), this._updateScale(this._iScale, n + " ft", n / o)); + }, + _updateScale: function (t, e, i) { + (t.style.width = Math.round(this.options.maxWidth * i) + "px"), (t.innerHTML = e); + }, + _getRoundNum: function (t) { + var e = Math.pow(10, (Math.floor(t) + "").length - 1), + i = t / e; + return (i = i >= 10 ? 10 : i >= 5 ? 5 : i >= 3 ? 3 : i >= 2 ? 2 : 1), e * i; + }, + })), + (o.control.scale = function (t) { + return new o.Control.Scale(t); + }), + (o.Control.Layers = o.Control.extend({ + options: { collapsed: !0, position: "topright", autoZIndex: !0, hideSingleBase: !1 }, + initialize: function (t, e, i) { + o.setOptions(this, i), + (this._layers = {}), + (this._lastZIndex = 0), + (this._handlingClick = !1); + for (var n in t) this._addLayer(t[n], n); + for (n in e) this._addLayer(e[n], n, !0); + }, + onAdd: function (t) { + return ( + this._initLayout(), + this._update(), + (this._map = t), + t.on("zoomend", this._checkDisabledLayers, this), + this._container + ); + }, + onRemove: function () { + this._map.off("zoomend", this._checkDisabledLayers, this); + }, + addBaseLayer: function (t, e) { + return this._addLayer(t, e), this._update(); + }, + addOverlay: function (t, e) { + return this._addLayer(t, e, !0), this._update(); + }, + removeLayer: function (t) { + return ( + t.off("add remove", this._onLayerChange, this), + delete this._layers[o.stamp(t)], + this._update() + ); + }, + _initLayout: function () { + var t = "leaflet-control-layers", + e = (this._container = o.DomUtil.create("div", t)); + e.setAttribute("aria-haspopup", !0), + o.DomEvent.disableClickPropagation(e), + o.Browser.touch || o.DomEvent.disableScrollPropagation(e); + var i = (this._form = o.DomUtil.create("form", t + "-list")); + if (this.options.collapsed) { + o.Browser.android || + o.DomEvent.on(e, { mouseenter: this._expand, mouseleave: this._collapse }, this); + var n = (this._layersLink = o.DomUtil.create("a", t + "-toggle", e)); + (n.href = "#"), + (n.title = "Layers"), + o.Browser.touch + ? o.DomEvent.on(n, "click", o.DomEvent.stop).on(n, "click", this._expand, this) + : o.DomEvent.on(n, "focus", this._expand, this), + o.DomEvent.on( + i, + "click", + function () { + setTimeout(o.bind(this._onInputClick, this), 0); + }, + this + ), + this._map.on("click", this._collapse, this); + } else this._expand(); + (this._baseLayersList = o.DomUtil.create("div", t + "-base", i)), + (this._separator = o.DomUtil.create("div", t + "-separator", i)), + (this._overlaysList = o.DomUtil.create("div", t + "-overlays", i)), + e.appendChild(i); + }, + _addLayer: function (t, e, i) { + t.on("add remove", this._onLayerChange, this); + var n = o.stamp(t); + (this._layers[n] = { layer: t, name: e, overlay: i }), + this.options.autoZIndex && + t.setZIndex && + (this._lastZIndex++, t.setZIndex(this._lastZIndex)); + }, + _update: function () { + if (!this._container) return this; + o.DomUtil.empty(this._baseLayersList), o.DomUtil.empty(this._overlaysList); + var t, + e, + i, + n, + s = 0; + for (i in this._layers) + (n = this._layers[i]), + this._addItem(n), + (e = e || n.overlay), + (t = t || !n.overlay), + (s += n.overlay ? 0 : 1); + return ( + this.options.hideSingleBase && + ((t = t && s > 1), (this._baseLayersList.style.display = t ? "" : "none")), + (this._separator.style.display = e && t ? "" : "none"), + this + ); + }, + _onLayerChange: function (t) { + this._handlingClick || this._update(); + var e = this._layers[o.stamp(t.target)], + i = e.overlay + ? "add" === t.type + ? "overlayadd" + : "overlayremove" + : "add" === t.type + ? "baselayerchange" + : null; + i && this._map.fire(i, e); + }, + _createRadioElement: function (t, i) { + var n = + '", + o = e.createElement("div"); + return (o.innerHTML = n), o.firstChild; + }, + _addItem: function (t) { + var i, + n = e.createElement("label"), + s = this._map.hasLayer(t.layer); + t.overlay + ? ((i = e.createElement("input")), + (i.type = "checkbox"), + (i.className = "leaflet-control-layers-selector"), + (i.defaultChecked = s)) + : (i = this._createRadioElement("leaflet-base-layers", s)), + (i.layerId = o.stamp(t.layer)), + o.DomEvent.on(i, "click", this._onInputClick, this); + var r = e.createElement("span"); + r.innerHTML = " " + t.name; + var a = e.createElement("div"); + n.appendChild(a), a.appendChild(i), a.appendChild(r); + var h = t.overlay ? this._overlaysList : this._baseLayersList; + return h.appendChild(n), this._checkDisabledLayers(), n; + }, + _onInputClick: function () { + var t, + e, + i, + n = this._form.getElementsByTagName("input"), + o = [], + s = []; + this._handlingClick = !0; + for (var r = n.length - 1; r >= 0; r--) + (t = n[r]), + (e = this._layers[t.layerId].layer), + (i = this._map.hasLayer(e)), + t.checked && !i ? o.push(e) : !t.checked && i && s.push(e); + for (r = 0; r < s.length; r++) this._map.removeLayer(s[r]); + for (r = 0; r < o.length; r++) this._map.addLayer(o[r]); + (this._handlingClick = !1), this._refocusOnMap(); + }, + _expand: function () { + o.DomUtil.addClass(this._container, "leaflet-control-layers-expanded"), + (this._form.style.height = null); + var t = this._map._size.y - (this._container.offsetTop + 50); + t < this._form.clientHeight + ? (o.DomUtil.addClass(this._form, "leaflet-control-layers-scrollbar"), + (this._form.style.height = t + "px")) + : o.DomUtil.removeClass(this._form, "leaflet-control-layers-scrollbar"), + this._checkDisabledLayers(); + }, + _collapse: function () { + o.DomUtil.removeClass(this._container, "leaflet-control-layers-expanded"); + }, + _checkDisabledLayers: function () { + for ( + var t, + e, + n = this._form.getElementsByTagName("input"), + o = this._map.getZoom(), + s = n.length - 1; + s >= 0; + s-- + ) + (t = n[s]), + (e = this._layers[t.layerId].layer), + (t.disabled = + (e.options.minZoom !== i && o < e.options.minZoom) || + (e.options.maxZoom !== i && o > e.options.maxZoom)); + }, + })), + (o.control.layers = function (t, e, i) { + return new o.Control.Layers(t, e, i); + }), + (o.PosAnimation = o.Evented.extend({ + run: function (t, e, i, n) { + this.stop(), + (this._el = t), + (this._inProgress = !0), + (this._duration = i || 0.25), + (this._easeOutPower = 1 / Math.max(n || 0.5, 0.2)), + (this._startPos = o.DomUtil.getPosition(t)), + (this._offset = e.subtract(this._startPos)), + (this._startTime = +new Date()), + this.fire("start"), + this._animate(); + }, + stop: function () { + this._inProgress && (this._step(!0), this._complete()); + }, + _animate: function () { + (this._animId = o.Util.requestAnimFrame(this._animate, this)), this._step(); + }, + _step: function (t) { + var e = +new Date() - this._startTime, + i = 1e3 * this._duration; + i > e ? this._runFrame(this._easeOut(e / i), t) : (this._runFrame(1), this._complete()); + }, + _runFrame: function (t, e) { + var i = this._startPos.add(this._offset.multiplyBy(t)); + e && i._round(), o.DomUtil.setPosition(this._el, i), this.fire("step"); + }, + _complete: function () { + o.Util.cancelAnimFrame(this._animId), (this._inProgress = !1), this.fire("end"); + }, + _easeOut: function (t) { + return 1 - Math.pow(1 - t, this._easeOutPower); + }, + })), + o.Map.include({ + setView: function (t, e, n) { + if ( + ((e = e === i ? this._zoom : this._limitZoom(e)), + (t = this._limitCenter(o.latLng(t), e, this.options.maxBounds)), + (n = n || {}), + this.stop(), + this._loaded && !n.reset && n !== !0) + ) { + n.animate !== i && + ((n.zoom = o.extend({ animate: n.animate }, n.zoom)), + (n.pan = o.extend({ animate: n.animate, duration: n.duration }, n.pan))); + var s = + this._zoom !== e + ? this._tryAnimatedZoom && this._tryAnimatedZoom(t, e, n.zoom) + : this._tryAnimatedPan(t, n.pan); + if (s) return clearTimeout(this._sizeTimer), this; + } + return this._resetView(t, e), this; + }, + panBy: function (t, e) { + if (((t = o.point(t).round()), (e = e || {}), !t.x && !t.y)) return this.fire("moveend"); + if (e.animate !== !0 && !this.getSize().contains(t)) + return ( + this._resetView( + this.unproject(this.project(this.getCenter()).add(t)), + this.getZoom() + ), + this + ); + if ( + (this._panAnim || + ((this._panAnim = new o.PosAnimation()), + this._panAnim.on( + { step: this._onPanTransitionStep, end: this._onPanTransitionEnd }, + this + )), + e.noMoveStart || this.fire("movestart"), + e.animate !== !1) + ) { + o.DomUtil.addClass(this._mapPane, "leaflet-pan-anim"); + var i = this._getMapPanePos().subtract(t); + this._panAnim.run(this._mapPane, i, e.duration || 0.25, e.easeLinearity); + } else this._rawPanBy(t), this.fire("move").fire("moveend"); + return this; + }, + _onPanTransitionStep: function () { + this.fire("move"); + }, + _onPanTransitionEnd: function () { + o.DomUtil.removeClass(this._mapPane, "leaflet-pan-anim"), this.fire("moveend"); + }, + _tryAnimatedPan: function (t, e) { + var i = this._getCenterOffset(t)._floor(); + return (e && e.animate) === !0 || this.getSize().contains(i) ? (this.panBy(i, e), !0) : !1; + }, + }), + o.Map.mergeOptions({ zoomAnimation: !0, zoomAnimationThreshold: 4 }); + var h = o.DomUtil.TRANSITION && o.Browser.any3d && !o.Browser.mobileOpera; + h && + o.Map.addInitHook(function () { + (this._zoomAnimated = this.options.zoomAnimation), + this._zoomAnimated && + (this._createAnimProxy(), + o.DomEvent.on(this._proxy, o.DomUtil.TRANSITION_END, this._catchTransitionEnd, this)); + }), + o.Map.include( + h + ? { + _createAnimProxy: function () { + var t = (this._proxy = o.DomUtil.create( + "div", + "leaflet-proxy leaflet-zoom-animated" + )); + this._panes.mapPane.appendChild(t), + this.on( + "zoomanim", + function (e) { + var i = o.DomUtil.TRANSFORM, + n = t.style[i]; + o.DomUtil.setTransform( + t, + this.project(e.center, e.zoom), + this.getZoomScale(e.zoom, 1) + ), + n === t.style[i] && + this._animatingZoom && + this._onZoomTransitionEnd(); + }, + this + ), + this.on( + "load moveend", + function () { + var e = this.getCenter(), + i = this.getZoom(); + o.DomUtil.setTransform( + t, + this.project(e, i), + this.getZoomScale(i, 1) + ); + }, + this + ); + }, + _catchTransitionEnd: function (t) { + this._animatingZoom && + t.propertyName.indexOf("transform") >= 0 && + this._onZoomTransitionEnd(); + }, + _nothingToAnimate: function () { + return !this._container.getElementsByClassName("leaflet-zoom-animated").length; + }, + _tryAnimatedZoom: function (t, e, i) { + if (this._animatingZoom) return !0; + if ( + ((i = i || {}), + !this._zoomAnimated || + i.animate === !1 || + this._nothingToAnimate() || + Math.abs(e - this._zoom) > this.options.zoomAnimationThreshold) + ) + return !1; + var n = this.getZoomScale(e), + s = this._getCenterOffset(t)._divideBy(1 - 1 / n); + return i.animate === !0 || this.getSize().contains(s) + ? (o.Util.requestAnimFrame(function () { + this._moveStart(!0)._animateZoom(t, e, !0); + }, this), + !0) + : !1; + }, + _animateZoom: function (t, e, i, n) { + i && + ((this._animatingZoom = !0), + (this._animateToCenter = t), + (this._animateToZoom = e), + o.DomUtil.addClass(this._mapPane, "leaflet-zoom-anim")), + this.fire("zoomanim", { center: t, zoom: e, noUpdate: n }), + setTimeout(o.bind(this._onZoomTransitionEnd, this), 250); + }, + _onZoomTransitionEnd: function () { + this._animatingZoom && + (o.DomUtil.removeClass(this._mapPane, "leaflet-zoom-anim"), + o.Util.requestAnimFrame(function () { + (this._animatingZoom = !1), + this._move(this._animateToCenter, this._animateToZoom)._moveEnd(!0); + }, this)); + }, + } + : {} + ), + o.Map.include({ + flyTo: function (t, e, n) { + function s(t) { + var e = (v * v - g * g + (t ? -1 : 1) * L * L * y * y) / (2 * (t ? v : g) * L * y); + return Math.log(Math.sqrt(e * e + 1) - e); + } - function a(t) { return (Math.exp(t) + Math.exp(-t)) / 2 } + function r(t) { + return (Math.exp(t) - Math.exp(-t)) / 2; + } - function h(t) { return r(t) / a(t) } + function a(t) { + return (Math.exp(t) + Math.exp(-t)) / 2; + } - function l(t) { return g * (a(x) / a(x + P * t)) } + function h(t) { + return r(t) / a(t); + } - function u(t) { return g * (a(x) * h(x + P * t) - r(x)) / L } + function l(t) { + return g * (a(x) / a(x + P * t)); + } - function c(t) { return 1 - Math.pow(1 - t, 1.5) } + function u(t) { + return (g * (a(x) * h(x + P * t) - r(x))) / L; + } - function d() { var i = (Date.now() - b) / D, - n = c(i) * w; - 1 >= i ? (this._flyToFrame = o.Util.requestAnimFrame(d, this), this._move(this.unproject(_.add(m.subtract(_).multiplyBy(u(n) / y)), f), this.getScaleZoom(g / l(n), f), { flyTo: !0 })) : this._move(t, e)._moveEnd(!0) } if (n = n || {}, n.animate === !1 || !o.Browser.any3d) return this.setView(t, e, n); - this.stop(); var _ = this.project(this.getCenter()), - m = this.project(t), - p = this.getSize(), - f = this._zoom; - t = o.latLng(t), e = e === i ? f : e; var g = Math.max(p.x, p.y), - v = g * this.getZoomScale(f, e), - y = m.distanceTo(_) || 1, - P = 1.42, - L = P * P, - x = s(0), - b = Date.now(), - w = (s(1) - x) / P, - D = n.duration ? 1e3 * n.duration : 1e3 * w * .8; return this._moveStart(!0), d.call(this), this }, flyToBounds: function(t, e) { var i = this._getBoundsCenterZoom(t, e); return this.flyTo(i.center, i.zoom, e) } }), o.Map.include({ _defaultLocateOptions: { timeout: 1e4, watch: !1 }, locate: function(t) { if (t = this._locateOptions = o.extend({}, this._defaultLocateOptions, t), !("geolocation" in navigator)) return this._handleGeolocationError({ code: 0, message: "Geolocation not supported." }), this; var e = o.bind(this._handleGeolocationResponse, this), - i = o.bind(this._handleGeolocationError, this); return t.watch ? this._locationWatchId = navigator.geolocation.watchPosition(e, i, t) : navigator.geolocation.getCurrentPosition(e, i, t), this }, stopLocate: function() { return navigator.geolocation && navigator.geolocation.clearWatch && navigator.geolocation.clearWatch(this._locationWatchId), this._locateOptions && (this._locateOptions.setView = !1), this }, _handleGeolocationError: function(t) { var e = t.code, - i = t.message || (1 === e ? "permission denied" : 2 === e ? "position unavailable" : "timeout"); - this._locateOptions.setView && !this._loaded && this.fitWorld(), this.fire("locationerror", { code: e, message: "Geolocation error: " + i + "." }) }, _handleGeolocationResponse: function(t) { var e = t.coords.latitude, - i = t.coords.longitude, - n = new o.LatLng(e, i), - s = n.toBounds(t.coords.accuracy), - r = this._locateOptions; if (r.setView) { var a = this.getBoundsZoom(s); - this.setView(n, r.maxZoom ? Math.min(a, r.maxZoom) : a) } var h = { latlng: n, bounds: s, timestamp: t.timestamp }; for (var l in t.coords) "number" == typeof t.coords[l] && (h[l] = t.coords[l]); - this.fire("locationfound", h) } }) -}(window, document); + function c(t) { + return 1 - Math.pow(1 - t, 1.5); + } + + function d() { + var i = (Date.now() - b) / D, + n = c(i) * w; + 1 >= i + ? ((this._flyToFrame = o.Util.requestAnimFrame(d, this)), + this._move( + this.unproject(_.add(m.subtract(_).multiplyBy(u(n) / y)), f), + this.getScaleZoom(g / l(n), f), + { flyTo: !0 } + )) + : this._move(t, e)._moveEnd(!0); + } + if (((n = n || {}), n.animate === !1 || !o.Browser.any3d)) return this.setView(t, e, n); + this.stop(); + var _ = this.project(this.getCenter()), + m = this.project(t), + p = this.getSize(), + f = this._zoom; + (t = o.latLng(t)), (e = e === i ? f : e); + var g = Math.max(p.x, p.y), + v = g * this.getZoomScale(f, e), + y = m.distanceTo(_) || 1, + P = 1.42, + L = P * P, + x = s(0), + b = Date.now(), + w = (s(1) - x) / P, + D = n.duration ? 1e3 * n.duration : 1e3 * w * 0.8; + return this._moveStart(!0), d.call(this), this; + }, + flyToBounds: function (t, e) { + var i = this._getBoundsCenterZoom(t, e); + return this.flyTo(i.center, i.zoom, e); + }, + }), + o.Map.include({ + _defaultLocateOptions: { timeout: 1e4, watch: !1 }, + locate: function (t) { + if ( + ((t = this._locateOptions = o.extend({}, this._defaultLocateOptions, t)), + !("geolocation" in navigator)) + ) + return ( + this._handleGeolocationError({ code: 0, message: "Geolocation not supported." }), this + ); + var e = o.bind(this._handleGeolocationResponse, this), + i = o.bind(this._handleGeolocationError, this); + return ( + t.watch + ? (this._locationWatchId = navigator.geolocation.watchPosition(e, i, t)) + : navigator.geolocation.getCurrentPosition(e, i, t), + this + ); + }, + stopLocate: function () { + return ( + navigator.geolocation && + navigator.geolocation.clearWatch && + navigator.geolocation.clearWatch(this._locationWatchId), + this._locateOptions && (this._locateOptions.setView = !1), + this + ); + }, + _handleGeolocationError: function (t) { + var e = t.code, + i = + t.message || + (1 === e ? "permission denied" : 2 === e ? "position unavailable" : "timeout"); + this._locateOptions.setView && !this._loaded && this.fitWorld(), + this.fire("locationerror", { code: e, message: "Geolocation error: " + i + "." }); + }, + _handleGeolocationResponse: function (t) { + var e = t.coords.latitude, + i = t.coords.longitude, + n = new o.LatLng(e, i), + s = n.toBounds(t.coords.accuracy), + r = this._locateOptions; + if (r.setView) { + var a = this.getBoundsZoom(s); + this.setView(n, r.maxZoom ? Math.min(a, r.maxZoom) : a); + } + var h = { latlng: n, bounds: s, timestamp: t.timestamp }; + for (var l in t.coords) "number" == typeof t.coords[l] && (h[l] = t.coords[l]); + this.fire("locationfound", h); + }, + }); +})(window, document); diff --git a/erpnext/public/js/newsletter.js b/erpnext/public/js/newsletter.js index 3a4dbf89330..96c65de8d6f 100644 --- a/erpnext/public/js/newsletter.js +++ b/erpnext/public/js/newsletter.js @@ -1,8 +1,8 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Newsletter', { +frappe.ui.form.on("Newsletter", { refresh() { erpnext.toggle_naming_series(); - } + }, }); diff --git a/erpnext/public/js/payment/payments.js b/erpnext/public/js/payment/payments.js index 4c23669dbbb..0e584205396 100644 --- a/erpnext/public/js/payment/payments.js +++ b/erpnext/public/js/payment/payments.js @@ -6,7 +6,7 @@ erpnext.payments = class payments extends erpnext.stock.StockController { var me = this; this.dialog = new frappe.ui.Dialog({ - title: 'Payment' + title: "Payment", }); this.dialog.show(); @@ -17,15 +17,17 @@ erpnext.payments = class payments extends erpnext.stock.StockController { } select_text() { - $(this.$body).find('.form-control').click(function() { - $(this).select(); - }); + $(this.$body) + .find(".form-control") + .click(function () { + $(this).select(); + }); } set_payment_primary_action() { var me = this; - this.dialog.set_primary_action(__("Submit"), function() { + this.dialog.set_primary_action(__("Submit"), function () { // Allow no ZERO payment $.each(me.frm.doc.payments, function (index, data) { if (data.amount != 0) { @@ -34,131 +36,147 @@ erpnext.payments = class payments extends erpnext.stock.StockController { return; } }); - }) + }); } - make_keyboard(){ + make_keyboard() { var me = this; $(this.$body).empty(); - $(this.$body).html(frappe.render_template('pos_payment', this.frm.doc)) + $(this.$body).html(frappe.render_template("pos_payment", this.frm.doc)); this.show_payment_details(); - this.bind_keyboard_event() - this.clear_amount() + this.bind_keyboard_event(); + this.clear_amount(); } - make_multimode_payment(){ + make_multimode_payment() { var me = this; if (this.frm.doc.change_amount > 0) { me.payment_val = me.doc.outstanding_amount; } - this.payments = frappe.model.add_child(this.frm.doc, 'Multi Mode Payment', "payments"); + this.payments = frappe.model.add_child(this.frm.doc, "Multi Mode Payment", "payments"); this.payments.mode_of_payment = this.dialog.fields_dict.mode_of_payment.get_value(); this.payments.amount = flt(this.payment_val); } - show_payment_details(){ + show_payment_details() { var me = this; - var multimode_payments = $(this.$body).find('.multimode-payments').empty(); + var multimode_payments = $(this.$body).find(".multimode-payments").empty(); if (this.frm.doc.payments.length) { - $.each(this.frm.doc.payments, function(index, data) { - $(frappe.render_template('payment_details', { - mode_of_payment: data.mode_of_payment, - amount: data.amount, - idx: data.idx, - currency: me.frm.doc.currency, - type: data.type - })).appendTo(multimode_payments) + $.each(this.frm.doc.payments, function (index, data) { + $( + frappe.render_template("payment_details", { + mode_of_payment: data.mode_of_payment, + amount: data.amount, + idx: data.idx, + currency: me.frm.doc.currency, + type: data.type, + }) + ).appendTo(multimode_payments); - if (data.type == 'Cash' && data.amount == me.frm.doc.paid_amount) { + if (data.type == "Cash" && data.amount == me.frm.doc.paid_amount) { me.idx = data.idx; - me.selected_mode = $(me.$body).find(repl("input[idx='%(idx)s']",{'idx': me.idx})); + me.selected_mode = $(me.$body).find(repl("input[idx='%(idx)s']", { idx: me.idx })); me.highlight_selected_row(); me.bind_amount_change_event(); } - }) - }else{ - $("

              No payment mode selected in pos profile

              ").appendTo(multimode_payments) + }); + } else { + $("

              No payment mode selected in pos profile

              ").appendTo(multimode_payments); } } - set_outstanding_amount(){ - this.selected_mode = $(this.$body).find(repl("input[idx='%(idx)s']",{'idx': this.idx})); + set_outstanding_amount() { + this.selected_mode = $(this.$body).find(repl("input[idx='%(idx)s']", { idx: this.idx })); this.highlight_selected_row(); this.payment_val = 0.0; if (this.frm.doc.outstanding_amount > 0 && flt(this.selected_mode.val()) == 0.0) { //When user first time click on row - this.payment_val = flt(this.frm.doc.outstanding_amount / this.frm.doc.conversion_rate, precision("outstanding_amount")) + this.payment_val = flt( + this.frm.doc.outstanding_amount / this.frm.doc.conversion_rate, + precision("outstanding_amount") + ); this.selected_mode.val(format_currency(this.payment_val, this.frm.doc.currency)); this.update_payment_amount(); } else if (flt(this.selected_mode.val()) > 0) { //If user click on existing row which has value this.payment_val = flt(this.selected_mode.val()); } - this.selected_mode.select() + this.selected_mode.select(); this.bind_amount_change_event(); } - bind_keyboard_event(){ + bind_keyboard_event() { var me = this; - this.payment_val = ''; + this.payment_val = ""; this.bind_form_control_event(); this.bind_numeric_keys_event(); } bind_form_control_event() { var me = this; - $(this.$body).find('.pos-payment-row').click(function() { - me.idx = $(this).attr("idx"); - me.set_outstanding_amount(); - }); + $(this.$body) + .find(".pos-payment-row") + .click(function () { + me.idx = $(this).attr("idx"); + me.set_outstanding_amount(); + }); - $(this.$body).find('.form-control').click(function() { - me.idx = $(this).attr("idx"); - me.set_outstanding_amount(); - me.update_paid_amount(true); - }); + $(this.$body) + .find(".form-control") + .click(function () { + me.idx = $(this).attr("idx"); + me.set_outstanding_amount(); + me.update_paid_amount(true); + }); - $(this.$body).find('.write_off_amount').change(function() { - me.write_off_amount(flt($(this).val()), precision("write_off_amount")); - }); + $(this.$body) + .find(".write_off_amount") + .change(function () { + me.write_off_amount(flt($(this).val()), precision("write_off_amount")); + }); - $(this.$body).find('.change_amount').change(function() { - me.change_amount(flt($(this).val()), precision("change_amount")); - }); + $(this.$body) + .find(".change_amount") + .change(function () { + me.change_amount(flt($(this).val()), precision("change_amount")); + }); } highlight_selected_row() { - var selected_row = $(this.$body).find(repl(".pos-payment-row[idx='%(idx)s']", {'idx': this.idx})); - $(this.$body).find('.pos-payment-row').removeClass('selected-payment-mode'); - selected_row.addClass('selected-payment-mode'); - $(this.$body).find('.amount').attr('disabled', true); - this.selected_mode.attr('disabled', false); + var selected_row = $(this.$body).find(repl(".pos-payment-row[idx='%(idx)s']", { idx: this.idx })); + $(this.$body).find(".pos-payment-row").removeClass("selected-payment-mode"); + selected_row.addClass("selected-payment-mode"); + $(this.$body).find(".amount").attr("disabled", true); + this.selected_mode.attr("disabled", false); } bind_numeric_keys_event() { var me = this; - $(this.$body).find('.pos-keyboard-key').click(function(){ - me.payment_val += $(this).text(); - me.selected_mode.val(format_currency(me.payment_val, me.frm.doc.currency)); - me.idx = me.selected_mode.attr("idx"); - me.update_paid_amount(); - }); - - $(this.$body).find('.delete-btn').click(function() { - me.payment_val = cstr(flt(me.selected_mode.val())).slice(0, -1); - me.selected_mode.val(format_currency(me.payment_val, me.frm.doc.currency)); - me.idx = me.selected_mode.attr("idx"); - me.update_paid_amount(); - }) + $(this.$body) + .find(".pos-keyboard-key") + .click(function () { + me.payment_val += $(this).text(); + me.selected_mode.val(format_currency(me.payment_val, me.frm.doc.currency)); + me.idx = me.selected_mode.attr("idx"); + me.update_paid_amount(); + }); + $(this.$body) + .find(".delete-btn") + .click(function () { + me.payment_val = cstr(flt(me.selected_mode.val())).slice(0, -1); + me.selected_mode.val(format_currency(me.payment_val, me.frm.doc.currency)); + me.idx = me.selected_mode.attr("idx"); + me.update_paid_amount(); + }); } bind_amount_change_event() { var me = this; - this.selected_mode.change(function() { - me.payment_val = flt($(this).val()) || 0.0; + this.selected_mode.change(function () { + me.payment_val = flt($(this).val()) || 0.0; me.selected_mode.val(format_currency(me.payment_val, me.frm.doc.currency)); me.idx = me.selected_mode.attr("idx"); me.update_payment_amount(); @@ -167,21 +185,25 @@ erpnext.payments = class payments extends erpnext.stock.StockController { clear_amount() { var me = this; - $(this.$body).find('.clr').click(function(e) { - e.stopPropagation(); - me.idx = $(this).attr("idx"); - me.selected_mode = $(me.$body).find(repl("input[idx='%(idx)s']",{'idx': me.idx})); - me.payment_val = 0.0; - me.selected_mode.val(0.0); - me.highlight_selected_row(); - me.update_payment_amount(); - }); + $(this.$body) + .find(".clr") + .click(function (e) { + e.stopPropagation(); + me.idx = $(this).attr("idx"); + me.selected_mode = $(me.$body).find(repl("input[idx='%(idx)s']", { idx: me.idx })); + me.payment_val = 0.0; + me.selected_mode.val(0.0); + me.highlight_selected_row(); + me.update_payment_amount(); + }); } write_off_amount(write_off_amount) { this.frm.doc.write_off_amount = flt(write_off_amount, precision("write_off_amount")); - this.frm.doc.base_write_off_amount = flt(this.frm.doc.write_off_amount * this.frm.doc.conversion_rate, - precision("base_write_off_amount")); + this.frm.doc.base_write_off_amount = flt( + this.frm.doc.write_off_amount * this.frm.doc.conversion_rate, + precision("base_write_off_amount") + ); this.calculate_outstanding_amount(false); this.show_amounts(); } @@ -196,13 +218,16 @@ erpnext.payments = class payments extends erpnext.stock.StockController { update_paid_amount(update_write_off) { var me = this; - if (in_list(['change_amount', 'write_off_amount'], this.idx)) { + if (in_list(["change_amount", "write_off_amount"], this.idx)) { var value = me.selected_mode.val(); - if (me.idx == 'change_amount') { + if (me.idx == "change_amount") { me.change_amount(value); } else { - if(flt(value) == 0 && update_write_off && me.frm.doc.outstanding_amount > 0) { - value = flt(me.frm.doc.outstanding_amount / me.frm.doc.conversion_rate, precision(me.idx)); + if (flt(value) == 0 && update_write_off && me.frm.doc.outstanding_amount > 0) { + value = flt( + me.frm.doc.outstanding_amount / me.frm.doc.conversion_rate, + precision(me.idx) + ); } me.write_off_amount(value); } @@ -211,25 +236,38 @@ erpnext.payments = class payments extends erpnext.stock.StockController { } } - update_payment_amount(){ + update_payment_amount() { var me = this; - $.each(this.frm.doc.payments, function(index, data) { + $.each(this.frm.doc.payments, function (index, data) { if (cint(me.idx) == cint(data.idx)) { data.amount = flt(me.selected_mode.val(), 2); } - }) + }); this.calculate_outstanding_amount(false); this.show_amounts(); } - show_amounts(){ + show_amounts() { var me = this; - $(this.$body).find(".write_off_amount").val(format_currency(this.frm.doc.write_off_amount, this.frm.doc.currency)); - $(this.$body).find('.paid_amount').text(format_currency(this.frm.doc.paid_amount, this.frm.doc.currency)); - $(this.$body).find('.change_amount').val(format_currency(this.frm.doc.change_amount, this.frm.doc.currency)); - $(this.$body).find('.outstanding_amount').text(format_currency(this.frm.doc.outstanding_amount, frappe.get_doc(":Company", this.frm.doc.company).default_currency)); + $(this.$body) + .find(".write_off_amount") + .val(format_currency(this.frm.doc.write_off_amount, this.frm.doc.currency)); + $(this.$body) + .find(".paid_amount") + .text(format_currency(this.frm.doc.paid_amount, this.frm.doc.currency)); + $(this.$body) + .find(".change_amount") + .val(format_currency(this.frm.doc.change_amount, this.frm.doc.currency)); + $(this.$body) + .find(".outstanding_amount") + .text( + format_currency( + this.frm.doc.outstanding_amount, + frappe.get_doc(":Company", this.frm.doc.company).default_currency + ) + ); this.update_invoice(); } -} +}; diff --git a/erpnext/public/js/projects/timer.js b/erpnext/public/js/projects/timer.js index 0209f4c2322..8370cc6ffff 100644 --- a/erpnext/public/js/projects/timer.js +++ b/erpnext/public/js/projects/timer.js @@ -1,26 +1,30 @@ frappe.provide("erpnext.timesheet"); -erpnext.timesheet.timer = function(frm, row, timestamp=0) { +erpnext.timesheet.timer = function (frm, row, timestamp = 0) { let dialog = new frappe.ui.Dialog({ title: __("Timer"), - fields: - [ - {"fieldtype": "Link", "label": __("Activity Type"), "fieldname": "activity_type", - "reqd": 1, "options": "Activity Type"}, - {"fieldtype": "Link", "label": __("Project"), "fieldname": "project", "options": "Project"}, - {"fieldtype": "Link", "label": __("Task"), "fieldname": "task", "options": "Task"}, - {"fieldtype": "Float", "label": __("Expected Hrs"), "fieldname": "expected_hours"}, - {"fieldtype": "Section Break"}, - {"fieldtype": "HTML", "fieldname": "timer_html"} - ] + fields: [ + { + fieldtype: "Link", + label: __("Activity Type"), + fieldname: "activity_type", + reqd: 1, + options: "Activity Type", + }, + { fieldtype: "Link", label: __("Project"), fieldname: "project", options: "Project" }, + { fieldtype: "Link", label: __("Task"), fieldname: "task", options: "Task" }, + { fieldtype: "Float", label: __("Expected Hrs"), fieldname: "expected_hours" }, + { fieldtype: "Section Break" }, + { fieldtype: "HTML", fieldname: "timer_html" }, + ], }); if (row) { dialog.set_values({ - 'activity_type': row.activity_type, - 'project': row.project, - 'task': row.task, - 'expected_hours': row.expected_hours + activity_type: row.activity_type, + project: row.project, + task: row.task, + expected_hours: row.expected_hours, }); } dialog.get_field("timer_html").$wrapper.append(get_timer_html()); @@ -34,8 +38,8 @@ erpnext.timesheet.timer = function(frm, row, timestamp=0) { 00
              - - + +
              `; } @@ -43,7 +47,7 @@ erpnext.timesheet.timer = function(frm, row, timestamp=0) { dialog.show(); }; -erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) { +erpnext.timesheet.control_timer = function (frm, dialog, row, timestamp = 0) { var $btn_start = dialog.$wrapper.find(".playpause .btn-start"); var $btn_complete = dialog.$wrapper.find(".playpause .btn-complete"); var interval = null; @@ -63,12 +67,16 @@ erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) { $btn_complete.hide(); } - $btn_start.click(function(e) { + $btn_start.click(function (e) { if (!initialized) { // New activity if no activities found var args = dialog.get_values(); - if(!args) return; - if (frm.doc.time_logs.length == 1 && !frm.doc.time_logs[0].activity_type && !frm.doc.time_logs[0].from_time) { + if (!args) return; + if ( + frm.doc.time_logs.length == 1 && + !frm.doc.time_logs[0].activity_type && + !frm.doc.time_logs[0].from_time + ) { frm.doc.time_logs = []; } row = frappe.model.add_child(frm.doc, "Timesheet Detail", "time_logs"); @@ -79,7 +87,7 @@ erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) { row.expected_hours = args.expected_hours; row.completed = 0; let d = moment(row.from_time); - if(row.expected_hours) { + if (row.expected_hours) { d.add(row.expected_hours, "hours"); row.to_time = d.format(frappe.defaultDatetimeFormat); } @@ -101,8 +109,8 @@ erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) { }); // Stop the timer and update the time logged by the timer on click of 'Complete' button - $btn_complete.click(function() { - var grid_row = cur_frm.fields_dict['time_logs'].grid.get_row(row.idx - 1); + $btn_complete.click(function () { + var grid_row = cur_frm.fields_dict["time_logs"].grid.get_row(row.idx - 1); var args = dialog.get_values(); grid_row.doc.completed = 1; grid_row.doc.activity_type = args.activity_type; @@ -119,7 +127,7 @@ erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) { }); function initializeTimer() { - interval = setInterval(function() { + interval = setInterval(function () { var current = setCurrentIncrement(); updateStopwatch(current); }, 1000); @@ -127,25 +135,24 @@ erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) { function updateStopwatch(increment) { var hours = Math.floor(increment / 3600); - var minutes = Math.floor((increment - (hours * 3600)) / 60); - var seconds = increment - (hours * 3600) - (minutes * 60); + var minutes = Math.floor((increment - hours * 3600) / 60); + var seconds = increment - hours * 3600 - minutes * 60; // If modal is closed by clicking anywhere outside, reset the timer - if (!$('.modal-dialog').is(':visible')) { + if (!$(".modal-dialog").is(":visible")) { reset(); } - if(hours > 99999) - reset(); - if(cur_dialog && cur_dialog.get_value('expected_hours') > 0) { - if(flag && (currentIncrement >= (cur_dialog.get_value('expected_hours') * 3600))) { + if (hours > 99999) reset(); + if (cur_dialog && cur_dialog.get_value("expected_hours") > 0) { + if (flag && currentIncrement >= cur_dialog.get_value("expected_hours") * 3600) { frappe.utils.play_sound("alert"); frappe.msgprint(__("Timer exceeded the given hours.")); flag = false; } } - $(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString()); - $(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString()); - $(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString()); + $(".hours").text(hours < 10 ? "0" + hours.toString() : hours.toString()); + $(".minutes").text(minutes < 10 ? "0" + minutes.toString() : minutes.toString()); + $(".seconds").text(seconds < 10 ? "0" + seconds.toString() : seconds.toString()); } function setCurrentIncrement() { diff --git a/erpnext/public/js/purchase_trends_filters.js b/erpnext/public/js/purchase_trends_filters.js index 77f1d2b496a..14ffaf82162 100644 --- a/erpnext/public/js/purchase_trends_filters.js +++ b/erpnext/public/js/purchase_trends_filters.js @@ -1,71 +1,67 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -erpnext.get_purchase_trends_filters = function() { +erpnext.get_purchase_trends_filters = function () { return [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"period", - "label": __("Period"), - "fieldtype": "Select", - "options": [ - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Half-Yearly", "label": __("Half-Yearly") }, - { "value": "Yearly", "label": __("Yearly") } + fieldname: "period", + label: __("Period"), + fieldtype: "Select", + options: [ + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, ], - "default": "Monthly" + default: "Monthly", }, { - "fieldname":"fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options":'Fiscal Year', - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()) + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), }, { - "fieldname":"period_based_on", - "label": __("Period based On"), - "fieldtype": "Select", - "options": [ - { "value": "posting_date", "label": __("Posting Date") }, - { "value": "bill_date", "label": __("Billing Date") }, + fieldname: "period_based_on", + label: __("Period based On"), + fieldtype: "Select", + options: [ + { value: "posting_date", label: __("Posting Date") }, + { value: "bill_date", label: __("Billing Date") }, ], - "default": "posting_date" + default: "posting_date", }, { - "fieldname":"based_on", - "label": __("Based On"), - "fieldtype": "Select", - "options": [ - { "value": "Item", "label": __("Item") }, - { "value": "Item Group", "label": __("Item Group") }, - { "value": "Supplier", "label": __("Supplier") }, - { "value": "Supplier Group", "label": __("Supplier Group") }, - { "value": "Project", "label": __("Project") } + fieldname: "based_on", + label: __("Based On"), + fieldtype: "Select", + options: [ + { value: "Item", label: __("Item") }, + { value: "Item Group", label: __("Item Group") }, + { value: "Supplier", label: __("Supplier") }, + { value: "Supplier Group", label: __("Supplier Group") }, + { value: "Project", label: __("Project") }, ], - "default": "Item", - "dashboard_config": { - "read_only": 1 - } + default: "Item", + dashboard_config: { + read_only: 1, + }, }, { - "fieldname":"group_by", - "label": __("Group By"), - "fieldtype": "Select", - "options": [ - "", - { "value": "Item", "label": __("Item") }, - { "value": "Supplier", "label": __("Supplier") } - ], - "default": "" + fieldname: "group_by", + label: __("Group By"), + fieldtype: "Select", + options: ["", { value: "Item", label: __("Item") }, { value: "Supplier", label: __("Supplier") }], + default: "", }, ]; -} +}; diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index b7d880ae408..b7e685cd6fb 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -4,129 +4,139 @@ // searches for enabled users frappe.provide("erpnext.queries"); $.extend(erpnext.queries, { - user: function() { + user: function () { return { query: "frappe.core.doctype.user.user.user_query" }; }, - lead: function() { + lead: function () { return { query: "erpnext.controllers.queries.lead_query" }; }, - customer: function() { + customer: function () { return { query: "erpnext.controllers.queries.customer_query" }; }, - supplier: function() { + supplier: function () { return { query: "erpnext.controllers.queries.supplier_query" }; }, - item: function(filters) { + item: function (filters) { var args = { query: "erpnext.controllers.queries.item_query" }; - if(filters) args["filters"] = filters; + if (filters) args["filters"] = filters; return args; }, - bom: function() { + bom: function () { return { query: "erpnext.controllers.queries.bom" }; }, - task: function() { + task: function () { return { query: "erpnext.projects.utils.query_task" }; }, - customer_filter: function(doc) { - if(!doc.customer) { - frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "customer", doc.name))])); + customer_filter: function (doc) { + if (!doc.customer) { + frappe.throw( + __("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "customer", doc.name))]) + ); } return { filters: { customer: doc.customer } }; }, - contact_query: function(doc) { - if(frappe.dynamic_link) { - if(!doc[frappe.dynamic_link.fieldname]) { - frappe.throw(__("Please set {0}", - [__(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name))])); + contact_query: function (doc) { + if (frappe.dynamic_link) { + if (!doc[frappe.dynamic_link.fieldname]) { + frappe.throw( + __("Please set {0}", [ + __(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name)), + ]) + ); } return { - query: 'frappe.contacts.doctype.contact.contact.contact_query', + query: "frappe.contacts.doctype.contact.contact.contact_query", filters: { link_doctype: frappe.dynamic_link.doctype, - link_name: doc[frappe.dynamic_link.fieldname] - } + link_name: doc[frappe.dynamic_link.fieldname], + }, }; } }, - address_query: function(doc) { - if(frappe.dynamic_link) { - if(!doc[frappe.dynamic_link.fieldname]) { - frappe.throw(__("Please set {0}", - [__(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name))])); + address_query: function (doc) { + if (frappe.dynamic_link) { + if (!doc[frappe.dynamic_link.fieldname]) { + frappe.throw( + __("Please set {0}", [ + __(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name)), + ]) + ); } return { - query: 'frappe.contacts.doctype.address.address.address_query', + query: "frappe.contacts.doctype.address.address.address_query", filters: { link_doctype: frappe.dynamic_link.doctype, - link_name: doc[frappe.dynamic_link.fieldname] - } + link_name: doc[frappe.dynamic_link.fieldname], + }, }; } }, - company_address_query: function(doc) { + company_address_query: function (doc) { return { - query: 'frappe.contacts.doctype.address.address.address_query', - filters: { is_your_company_address: 1, link_doctype: 'Company', link_name: doc.company || '' } + query: "frappe.contacts.doctype.address.address.address_query", + filters: { is_your_company_address: 1, link_doctype: "Company", link_name: doc.company || "" }, }; }, - dispatch_address_query: function(doc) { + dispatch_address_query: function (doc) { return { - query: 'frappe.contacts.doctype.address.address.address_query', - filters: { link_doctype: 'Company', link_name: doc.company || '' } + query: "frappe.contacts.doctype.address.address.address_query", + filters: { link_doctype: "Company", link_name: doc.company || "" }, }; }, - supplier_filter: function(doc) { - if(!doc.supplier) { - frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "supplier", doc.name))])); + supplier_filter: function (doc) { + if (!doc.supplier) { + frappe.throw( + __("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "supplier", doc.name))]) + ); } return { filters: { supplier: doc.supplier } }; }, - lead_filter: function(doc) { - if(!doc.lead) { - frappe.throw(__("Please specify a {0}", - [__(frappe.meta.get_label(doc.doctype, "lead", doc.name))])); + lead_filter: function (doc) { + if (!doc.lead) { + frappe.throw( + __("Please specify a {0}", [__(frappe.meta.get_label(doc.doctype, "lead", doc.name))]) + ); } return { filters: { lead: doc.lead } }; }, - not_a_group_filter: function() { + not_a_group_filter: function () { return { filters: { is_group: 0 } }; }, - employee: function() { - return { query: "erpnext.controllers.queries.employee_query" } + employee: function () { + return { query: "erpnext.controllers.queries.employee_query" }; }, - warehouse: function(doc) { + warehouse: function (doc) { return { filters: [ ["Warehouse", "company", "in", ["", cstr(doc.company)]], - ["Warehouse", "is_group", "=",0] - - ] + ["Warehouse", "is_group", "=", 0], + ], }; }, - get_filtered_dimensions: function(doc, child_fields, dimension, company) { - let account = ''; + get_filtered_dimensions: function (doc, child_fields, dimension, company) { + let account = ""; child_fields.forEach((field) => { if (!account) { @@ -137,21 +147,23 @@ $.extend(erpnext.queries, { return { query: "erpnext.controllers.queries.get_filtered_dimensions", filters: { - 'dimension': dimension, - 'account': account, - 'company': company - } + dimension: dimension, + account: account, + company: company, + }, }; - } + }, }); -erpnext.queries.setup_queries = function(frm, options, query_fn) { +erpnext.queries.setup_queries = function (frm, options, query_fn) { var me = this; - var set_query = function(doctype, parentfield) { - var link_fields = frappe.meta.get_docfields(doctype, frm.doc.name, - {"fieldtype": "Link", "options": options}); - $.each(link_fields, function(i, df) { - if(parentfield) { + var set_query = function (doctype, parentfield) { + var link_fields = frappe.meta.get_docfields(doctype, frm.doc.name, { + fieldtype: "Link", + options: options, + }); + $.each(link_fields, function (i, df) { + if (parentfield) { frm.set_query(df.fieldname, parentfield, query_fn); } else { frm.set_query(df.fieldname, query_fn); @@ -162,24 +174,26 @@ erpnext.queries.setup_queries = function(frm, options, query_fn) { set_query(frm.doc.doctype); // warehouse field in tables - $.each(frappe.meta.get_docfields(frm.doc.doctype, frm.doc.name, {"fieldtype": "Table"}), - function(i, df) { + $.each( + frappe.meta.get_docfields(frm.doc.doctype, frm.doc.name, { fieldtype: "Table" }), + function (i, df) { set_query(df.options, df.fieldname); - }); -} + } + ); +}; /* if item code is selected in child table then list down warehouses with its quantity else apply default filters. */ -erpnext.queries.setup_warehouse_query = function(frm){ - frm.set_query('warehouse', 'items', function(doc, cdt, cdn) { - var row = locals[cdt][cdn]; +erpnext.queries.setup_warehouse_query = function (frm) { + frm.set_query("warehouse", "items", function (doc, cdt, cdn) { + var row = locals[cdt][cdn]; var filters = erpnext.queries.warehouse(frm.doc); - if(row.item_code){ - $.extend(filters, {"query":"erpnext.controllers.queries.warehouse_query"}); + if (row.item_code) { + $.extend(filters, { query: "erpnext.controllers.queries.warehouse_query" }); filters["filters"].push(["Bin", "item_code", "=", row.item_code]); } - return filters + return filters; }); -} +}; diff --git a/erpnext/public/js/sales_trends_filters.js b/erpnext/public/js/sales_trends_filters.js index 9a70a3da4c6..85daa01ff67 100644 --- a/erpnext/public/js/sales_trends_filters.js +++ b/erpnext/public/js/sales_trends_filters.js @@ -1,61 +1,57 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -erpnext.get_sales_trends_filters = function() { - return[ +erpnext.get_sales_trends_filters = function () { + return [ { - "fieldname":"period", - "label": __("Period"), - "fieldtype": "Select", - "options": [ - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Half-Yearly", "label": __("Half-Yearly") }, - { "value": "Yearly", "label": __("Yearly") } + fieldname: "period", + label: __("Period"), + fieldtype: "Select", + options: [ + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, ], - "default": "Monthly" + default: "Monthly", }, { - "fieldname":"based_on", - "label": __("Based On"), - "fieldtype": "Select", - "options": [ - { "value": "Item", "label": __("Item") }, - { "value": "Item Group", "label": __("Item Group") }, - { "value": "Customer", "label": __("Customer") }, - { "value": "Customer Group", "label": __("Customer Group") }, - { "value": "Territory", "label": __("Territory") }, - { "value": "Project", "label": __("Project") } + fieldname: "based_on", + label: __("Based On"), + fieldtype: "Select", + options: [ + { value: "Item", label: __("Item") }, + { value: "Item Group", label: __("Item Group") }, + { value: "Customer", label: __("Customer") }, + { value: "Customer Group", label: __("Customer Group") }, + { value: "Territory", label: __("Territory") }, + { value: "Project", label: __("Project") }, ], - "default": "Item", - "dashboard_config": { - "read_only": 1, - } + default: "Item", + dashboard_config: { + read_only: 1, + }, }, { - "fieldname":"group_by", - "label": __("Group By"), - "fieldtype": "Select", - "options": [ - "", - { "value": "Item", "label": __("Item") }, - { "value": "Customer", "label": __("Customer") } - ], - "default": "" + fieldname: "group_by", + label: __("Group By"), + fieldtype: "Select", + options: ["", { value: "Item", label: __("Item") }, { value: "Customer", label: __("Customer") }], + default: "", }, { - "fieldname":"fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options":'Fiscal Year', - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()) + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), }, { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), }, ]; -} +}; diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index 934fd1f88ae..b146cd19a36 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -1,7 +1,7 @@ frappe.provide("erpnext.setup"); -frappe.pages['setup-wizard'].on_page_load = function(wrapper) { - if(frappe.sys_defaults.company) { +frappe.pages["setup-wizard"].on_page_load = function (wrapper) { + if (frappe.sys_defaults.company) { frappe.set_route("desk"); return; } @@ -14,32 +14,34 @@ frappe.setup.on("before_load", function () { erpnext.setup.slides_settings = [ { // Organization - name: 'organization', + name: "organization", title: __("Setup your organization"), icon: "fa fa-building", fields: [ { - fieldname: 'company_name', - label: __('Company Name'), - fieldtype: 'Data', - reqd: 1 + fieldname: "company_name", + label: __("Company Name"), + fieldtype: "Data", + reqd: 1, }, { fieldtype: "Column Break" }, { - fieldname: 'company_abbr', - label: __('Company Abbreviation'), - fieldtype: 'Data', - reqd: 1 + fieldname: "company_abbr", + label: __("Company Abbreviation"), + fieldtype: "Data", + reqd: 1, }, { fieldtype: "Section Break" }, { - fieldname: 'chart_of_accounts', label: __('Chart of Accounts'), - options: "", fieldtype: 'Select' + fieldname: "chart_of_accounts", + label: __("Chart of Accounts"), + options: "", + fieldtype: "Select", }, - { fieldname: 'view_coa', label: __('View Chart of Accounts'), fieldtype: 'Button' }, - { fieldname: 'fy_start_date', label: __('Financial Year Begins On'), fieldtype: 'Date', reqd: 1 }, + { fieldname: "view_coa", label: __("View Chart of Accounts"), fieldtype: "Button" }, + { fieldname: "fy_start_date", label: __("Financial Year Begins On"), fieldtype: "Date", reqd: 1 }, // end date should be hidden (auto calculated) - { fieldname: 'fy_end_date', label: __('End Date'), fieldtype: 'Date', reqd: 1, hidden: 1 }, + { fieldname: "fy_end_date", label: __("End Date"), fieldtype: "Date", reqd: 1, hidden: 1 }, ], onload: function (slide) { @@ -66,10 +68,10 @@ erpnext.setup.slides_settings = [ return true; }, - validate_fy_dates: function() { + validate_fy_dates: function () { // validate fiscal year start and end dates - const invalid = this.values.fy_start_date == 'Invalid date' || - this.values.fy_end_date == 'Invalid date'; + const invalid = + this.values.fy_start_date == "Invalid date" || this.values.fy_end_date == "Invalid date"; const start_greater_than_end = this.values.fy_start_date > this.values.fy_end_date; if (invalid || start_greater_than_end) { @@ -97,26 +99,24 @@ erpnext.setup.slides_settings = [ next_year = current_year; current_year -= 1; } - slide.get_field("fy_start_date").set_value(current_year + '-' + fy[0]); - slide.get_field("fy_end_date").set_value(next_year + '-' + fy[1]); + slide.get_field("fy_start_date").set_value(current_year + "-" + fy[0]); + slide.get_field("fy_end_date").set_value(next_year + "-" + fy[1]); } }, - load_chart_of_accounts: function (slide) { let country = frappe.wizard.values.country; if (country) { frappe.call({ method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.get_charts_for_country", - args: { "country": country, with_standard: true }, + args: { country: country, with_standard: true }, callback: function (r) { if (r.message) { - slide.get_input("chart_of_accounts").empty() - .add_options(r.message); + slide.get_input("chart_of_accounts").empty().add_options(r.message); } - } - }) + }, + }); } }, @@ -124,56 +124,74 @@ erpnext.setup.slides_settings = [ let me = this; slide.get_input("fy_start_date").on("change", function () { var start_date = slide.form.fields_dict.fy_start_date.get_value(); - var year_end_date = - frappe.datetime.add_days(frappe.datetime.add_months(start_date, 12), -1); + var year_end_date = frappe.datetime.add_days(frappe.datetime.add_months(start_date, 12), -1); slide.form.fields_dict.fy_end_date.set_value(year_end_date); }); - slide.get_input("view_coa").on("click", function() { + slide.get_input("view_coa").on("click", function () { let chart_template = slide.form.fields_dict.chart_of_accounts.get_value(); - if(!chart_template) return; + if (!chart_template) return; me.charts_modal(slide, chart_template); }); - slide.get_input("company_name").on("input", function () { - let parts = slide.get_input("company_name").val().split(" "); - let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join(""); - slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase()); - }).val(frappe.boot.sysdefaults.company_name || "").trigger("change"); + slide + .get_input("company_name") + .on("input", function () { + let parts = slide.get_input("company_name").val().split(" "); + let abbr = $.map(parts, function (p) { + return p ? p.substr(0, 1) : null; + }).join(""); + slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase()); + }) + .val(frappe.boot.sysdefaults.company_name || "") + .trigger("change"); - slide.get_input("company_abbr").on("change", function () { - let abbr = slide.get_input("company_abbr").val(); - if (abbr.length > 10) { - frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters")); - abbr = abbr.slice(0, 10); - } - slide.get_field("company_abbr").set_value(abbr); - }).val(frappe.boot.sysdefaults.company_abbr || "").trigger("change"); + slide + .get_input("company_abbr") + .on("change", function () { + let abbr = slide.get_input("company_abbr").val(); + if (abbr.length > 10) { + frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters")); + abbr = abbr.slice(0, 10); + } + slide.get_field("company_abbr").set_value(abbr); + }) + .val(frappe.boot.sysdefaults.company_abbr || "") + .trigger("change"); }, - charts_modal: function(slide, chart_template) { - let parent = __('All Accounts'); + charts_modal: function (slide, chart_template) { + let parent = __("All Accounts"); let dialog = new frappe.ui.Dialog({ title: chart_template, fields: [ - {'fieldname': 'expand_all', 'label': __('Expand All'), 'fieldtype': 'Button', - click: function() { + { + fieldname: "expand_all", + label: __("Expand All"), + fieldtype: "Button", + click: function () { // expand all nodes on button click coa_tree.load_children(coa_tree.root_node, true); - } + }, }, - {'fieldname': 'collapse_all', 'label': __('Collapse All'), 'fieldtype': 'Button', - click: function() { + { + fieldname: "collapse_all", + label: __("Collapse All"), + fieldtype: "Button", + click: function () { // collapse all nodes - coa_tree.get_all_nodes(coa_tree.root_node.data.value, coa_tree.root_node.is_root) - .then(data_list => { - data_list.map(d => { coa_tree.toggle_node(coa_tree.nodes[d.parent]); }); + coa_tree + .get_all_nodes(coa_tree.root_node.data.value, coa_tree.root_node.is_root) + .then((data_list) => { + data_list.map((d) => { + coa_tree.toggle_node(coa_tree.nodes[d.parent]); + }); }); - } - } - ] + }, + }, + ], }); // render tree structure in the dialog modal @@ -181,49 +199,49 @@ erpnext.setup.slides_settings = [ parent: $(dialog.body), label: parent, expandable: true, - method: 'erpnext.accounts.utils.get_coa', + method: "erpnext.accounts.utils.get_coa", args: { chart: chart_template, parent: parent, - doctype: 'Account' + doctype: "Account", }, - onclick: function(node) { + onclick: function (node) { parent = node.value; - } + }, }); // add class to show buttons side by side - const form_container = $(dialog.body).find('form'); - const buttons = $(form_container).find('.frappe-control'); - form_container.addClass('flex'); + const form_container = $(dialog.body).find("form"); + const buttons = $(form_container).find(".frappe-control"); + form_container.addClass("flex"); buttons.map((index, button) => { - $(button).css({"margin-right": "1em"}); - }) + $(button).css({ "margin-right": "1em" }); + }); dialog.show(); coa_tree.load_children(coa_tree.root_node, true); // expand all node trigger - } - } + }, + }, ]; // Source: https://en.wikipedia.org/wiki/Fiscal_year // default 1st Jan - 31st Dec erpnext.setup.fiscal_years = { - "Afghanistan": ["12-21", "12-20"], - "Australia": ["07-01", "06-30"], - "Bangladesh": ["07-01", "06-30"], - "Canada": ["04-01", "03-31"], + Afghanistan: ["12-21", "12-20"], + Australia: ["07-01", "06-30"], + Bangladesh: ["07-01", "06-30"], + Canada: ["04-01", "03-31"], "Costa Rica": ["10-01", "09-30"], - "Egypt": ["07-01", "06-30"], + Egypt: ["07-01", "06-30"], "Hong Kong": ["04-01", "03-31"], - "India": ["04-01", "03-31"], - "Iran": ["06-23", "06-22"], - "Myanmar": ["04-01", "03-31"], + India: ["04-01", "03-31"], + Iran: ["06-23", "06-22"], + Myanmar: ["04-01", "03-31"], "New Zealand": ["04-01", "03-31"], - "Pakistan": ["07-01", "06-30"], - "Singapore": ["04-01", "03-31"], + Pakistan: ["07-01", "06-30"], + Singapore: ["04-01", "03-31"], "South Africa": ["03-01", "02-28"], - "Thailand": ["10-01", "09-30"], + Thailand: ["10-01", "09-30"], "United Kingdom": ["04-01", "03-31"], }; diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js index d14740c1060..45ac9ad7ac4 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -7,45 +7,46 @@ var shopping_cart = erpnext.e_commerce.shopping_cart; var getParams = function (url) { var params = []; - var parser = document.createElement('a'); + var parser = document.createElement("a"); parser.href = url; var query = parser.search.substring(1); - var vars = query.split('&'); + var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { - var pair = vars[i].split('='); + var pair = vars[i].split("="); params[pair[0]] = decodeURIComponent(pair[1]); } return params; }; -frappe.ready(function() { +frappe.ready(function () { var full_name = frappe.session && frappe.session.user_fullname; // update user - if(full_name) { - $('.navbar li[data-label="User"] a') - .html(' ' + full_name); + if (full_name) { + $('.navbar li[data-label="User"] a').html( + ' ' + full_name + ); } // set coupon code and sales partner code var url_args = getParams(window.location.href); - var referral_coupon_code = url_args['cc']; - var referral_sales_partner = url_args['sp']; + var referral_coupon_code = url_args["cc"]; + var referral_sales_partner = url_args["sp"]; var d = new Date(); // expires within 30 minutes - d.setTime(d.getTime() + (0.02 * 24 * 60 * 60 * 1000)); - var expires = "expires="+d.toUTCString(); + d.setTime(d.getTime() + 0.02 * 24 * 60 * 60 * 1000); + var expires = "expires=" + d.toUTCString(); if (referral_coupon_code) { document.cookie = "referral_coupon_code=" + referral_coupon_code + ";" + expires + ";path=/"; } if (referral_sales_partner) { document.cookie = "referral_sales_partner=" + referral_sales_partner + ";" + expires + ";path=/"; } - referral_coupon_code=frappe.get_cookie("referral_coupon_code"); - referral_sales_partner=frappe.get_cookie("referral_sales_partner"); + referral_coupon_code = frappe.get_cookie("referral_coupon_code"); + referral_sales_partner = frappe.get_cookie("referral_sales_partner"); - if (referral_coupon_code && $(".tot_quotation_discount").val()==undefined ) { + if (referral_coupon_code && $(".tot_quotation_discount").val() == undefined) { $(".txtcoupon").val(referral_coupon_code); } if (referral_sales_partner) { @@ -59,27 +60,27 @@ frappe.ready(function() { }); $.extend(shopping_cart, { - show_shoppingcart_dropdown: function() { - $(".shopping-cart").on('shown.bs.dropdown', function() { - if (!$('.shopping-cart-menu .cart-container').length) { + show_shoppingcart_dropdown: function () { + $(".shopping-cart").on("shown.bs.dropdown", function () { + if (!$(".shopping-cart-menu .cart-container").length) { return frappe.call({ - method: 'erpnext.e_commerce.shopping_cart.cart.get_shopping_cart_menu', - callback: function(r) { + method: "erpnext.e_commerce.shopping_cart.cart.get_shopping_cart_menu", + callback: function (r) { if (r.message) { - $('.shopping-cart-menu').html(r.message); + $(".shopping-cart-menu").html(r.message); } - } + }, }); } }); }, - update_cart: function(opts) { - if (frappe.session.user==="Guest") { + update_cart: function (opts) { + if (frappe.session.user === "Guest") { if (localStorage) { localStorage.setItem("last_visited", window.location.pathname); } - frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => { + frappe.call("erpnext.e_commerce.api.get_guest_redirect_on_action").then((res) => { window.location.href = res.message || "/login"; }); } else { @@ -91,35 +92,34 @@ $.extend(shopping_cart, { item_code: opts.item_code, qty: opts.qty, additional_notes: opts.additional_notes !== undefined ? opts.additional_notes : undefined, - with_items: opts.with_items || 0 + with_items: opts.with_items || 0, }, btn: opts.btn, - callback: function(r) { + callback: function (r) { shopping_cart.unfreeze(); shopping_cart.set_cart_count(true); - if(opts.callback) - opts.callback(r); - } + if (opts.callback) opts.callback(r); + }, }); } }, - set_cart_count: function(animate=false) { + set_cart_count: function (animate = false) { $(".intermediate-empty-cart").remove(); var cart_count = frappe.get_cookie("cart_count"); - if(frappe.session.user==="Guest") { + if (frappe.session.user === "Guest") { cart_count = 0; } - if(cart_count) { - $(".shopping-cart").toggleClass('hidden', false); + if (cart_count) { + $(".shopping-cart").toggleClass("hidden", false); } - var $cart = $('.cart-icon'); + var $cart = $(".cart-icon"); var $badge = $cart.find("#cart-count"); - if(parseInt(cart_count) === 0 || cart_count === undefined) { + if (parseInt(cart_count) === 0 || cart_count === undefined) { $cart.css("display", "none"); $(".cart-tax-items").hide(); $(".btn-place-order").hide(); @@ -127,17 +127,16 @@ $.extend(shopping_cart, { let intermediate_empty_cart_msg = `
              - ${ __("Cart is Empty") } + ${__("Cart is Empty")}
              `; $(".cart-table").after(intermediate_empty_cart_msg); - } - else { + } else { $cart.css("display", "inline"); $("#cart-count").text(cart_count); } - if(cart_count) { + if (cart_count) { $badge.html(cart_count); if (animate) { @@ -151,15 +150,15 @@ $.extend(shopping_cart, { } }, - shopping_cart_update: function({item_code, qty, cart_dropdown, additional_notes}) { + shopping_cart_update: function ({ item_code, qty, cart_dropdown, additional_notes }) { shopping_cart.update_cart({ item_code, qty, additional_notes, with_items: 1, btn: this, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { $(".cart-items").html(r.message.items); $(".cart-tax-items").html(r.message.total); $(".payment-summary").html(r.message.taxes_and_totals); @@ -176,9 +175,9 @@ $.extend(shopping_cart, { show_cart_navbar: function () { frappe.call({ method: "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.is_cart_enabled", - callback: function(r) { - $(".shopping-cart").toggleClass('hidden', r.message ? false : true); - } + callback: function (r) { + $(".shopping-cart").toggleClass("hidden", r.message ? false : true); + }, }); }, @@ -188,43 +187,41 @@ $.extend(shopping_cart, { }, bind_add_to_cart_action() { - $('.page_content').on('click', '.btn-add-to-cart-list', (e) => { + $(".page_content").on("click", ".btn-add-to-cart-list", (e) => { const $btn = $(e.currentTarget); - $btn.prop('disabled', true); + $btn.prop("disabled", true); - if (frappe.session.user==="Guest") { + if (frappe.session.user === "Guest") { if (localStorage) { localStorage.setItem("last_visited", window.location.pathname); } - frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => { + frappe.call("erpnext.e_commerce.api.get_guest_redirect_on_action").then((res) => { window.location.href = res.message || "/login"; }); return; } - $btn.addClass('hidden'); - $btn.closest('.cart-action-container').addClass('d-flex'); - $btn.parent().find('.go-to-cart').removeClass('hidden'); - $btn.parent().find('.go-to-cart-grid').removeClass('hidden'); - $btn.parent().find('.cart-indicator').removeClass('hidden'); + $btn.addClass("hidden"); + $btn.closest(".cart-action-container").addClass("d-flex"); + $btn.parent().find(".go-to-cart").removeClass("hidden"); + $btn.parent().find(".go-to-cart-grid").removeClass("hidden"); + $btn.parent().find(".cart-indicator").removeClass("hidden"); - const item_code = $btn.data('item-code'); + const item_code = $btn.data("item-code"); erpnext.e_commerce.shopping_cart.update_cart({ item_code, - qty: 1 + qty: 1, }); - }); }, freeze() { if (window.location.pathname !== "/cart") return; - if (!$('#freeze').length) { - let freeze = $('') - .appendTo("body"); + if (!$("#freeze").length) { + let freeze = $('').appendTo("body"); - setTimeout(function() { + setTimeout(function () { freeze.addClass("show"); }, 1); } else { @@ -233,11 +230,11 @@ $.extend(shopping_cart, { }, unfreeze() { - if ($('#freeze').length) { - let freeze = $('#freeze').removeClass("show"); - setTimeout(function() { + if ($("#freeze").length) { + let freeze = $("#freeze").removeClass("show"); + setTimeout(function () { freeze.remove(); }, 1); } - } + }, }); diff --git a/erpnext/public/js/sms_manager.js b/erpnext/public/js/sms_manager.js index a058da23ac4..d3147bb4600 100644 --- a/erpnext/public/js/sms_manager.js +++ b/erpnext/public/js/sms_manager.js @@ -3,104 +3,114 @@ erpnext.SMSManager = function SMSManager(doc) { var me = this; - this.setup = function() { + this.setup = function () { var default_msg = { - 'Lead' : '', - 'Opportunity' : 'Your enquiry has been logged into the system. Ref No: ' + doc.name, - 'Quotation' : 'Quotation ' + doc.name + ' has been sent via email. Thanks!', - 'Sales Order' : 'Sales Order ' + doc.name + ' has been created against ' - + (doc.quotation_no ? ('Quote No:' + doc.quotation_no) : '') - + (doc.po_no ? (' for your PO: ' + doc.po_no) : ''), - 'Delivery Note' : 'Items has been delivered against delivery note: ' + doc.name - + (doc.po_no ? (' for your PO: ' + doc.po_no) : ''), - 'Sales Invoice': 'Invoice ' + doc.name + ' has been sent via email ' - + (doc.po_no ? (' for your PO: ' + doc.po_no) : ''), - 'Material Request' : 'Material Request ' + doc.name + ' has been raised in the system', - 'Purchase Order' : 'Purchase Order ' + doc.name + ' has been sent via email', - 'Purchase Receipt' : 'Items has been received against purchase receipt: ' + doc.name - } - - if (in_list(['Sales Order', 'Delivery Note', 'Sales Invoice'], doc.doctype)) - this.show(doc.contact_person, 'Customer', doc.customer, '', default_msg[doc.doctype]); - else if (doc.doctype === 'Quotation') - this.show(doc.contact_person, 'Customer', doc.party_name, '', default_msg[doc.doctype]); - else if (in_list(['Purchase Order', 'Purchase Receipt'], doc.doctype)) - this.show(doc.contact_person, 'Supplier', doc.supplier, '', default_msg[doc.doctype]); - else if (doc.doctype == 'Lead') - this.show('', '', '', doc.mobile_no, default_msg[doc.doctype]); - else if (doc.doctype == 'Opportunity') - this.show('', '', '', doc.contact_no, default_msg[doc.doctype]); - else if (doc.doctype == 'Material Request') - this.show('', '', '', '', default_msg[doc.doctype]); + Lead: "", + Opportunity: "Your enquiry has been logged into the system. Ref No: " + doc.name, + Quotation: "Quotation " + doc.name + " has been sent via email. Thanks!", + "Sales Order": + "Sales Order " + + doc.name + + " has been created against " + + (doc.quotation_no ? "Quote No:" + doc.quotation_no : "") + + (doc.po_no ? " for your PO: " + doc.po_no : ""), + "Delivery Note": + "Items has been delivered against delivery note: " + + doc.name + + (doc.po_no ? " for your PO: " + doc.po_no : ""), + "Sales Invoice": + "Invoice " + + doc.name + + " has been sent via email " + + (doc.po_no ? " for your PO: " + doc.po_no : ""), + "Material Request": "Material Request " + doc.name + " has been raised in the system", + "Purchase Order": "Purchase Order " + doc.name + " has been sent via email", + "Purchase Receipt": "Items has been received against purchase receipt: " + doc.name, + }; + if (in_list(["Sales Order", "Delivery Note", "Sales Invoice"], doc.doctype)) + this.show(doc.contact_person, "Customer", doc.customer, "", default_msg[doc.doctype]); + else if (doc.doctype === "Quotation") + this.show(doc.contact_person, "Customer", doc.party_name, "", default_msg[doc.doctype]); + else if (in_list(["Purchase Order", "Purchase Receipt"], doc.doctype)) + this.show(doc.contact_person, "Supplier", doc.supplier, "", default_msg[doc.doctype]); + else if (doc.doctype == "Lead") this.show("", "", "", doc.mobile_no, default_msg[doc.doctype]); + else if (doc.doctype == "Opportunity") + this.show("", "", "", doc.contact_no, default_msg[doc.doctype]); + else if (doc.doctype == "Material Request") this.show("", "", "", "", default_msg[doc.doctype]); }; - this.get_contact_number = function(contact, ref_doctype, ref_name) { + this.get_contact_number = function (contact, ref_doctype, ref_name) { frappe.call({ method: "frappe.core.doctype.sms_settings.sms_settings.get_contact_number", args: { contact_name: contact, ref_doctype: ref_doctype, - ref_name: ref_name + ref_name: ref_name, }, - callback: function(r) { - if(r.exc) { frappe.msgprint(r.exc); return; } + callback: function (r) { + if (r.exc) { + frappe.msgprint(r.exc); + return; + } me.number = r.message; me.show_dialog(); - } + }, }); }; - this.show = function(contact, ref_doctype, ref_name, mobile_nos, message) { + this.show = function (contact, ref_doctype, ref_name, mobile_nos, message) { this.message = message; if (mobile_nos) { me.number = mobile_nos; me.show_dialog(); - } else if (contact){ - this.get_contact_number(contact, ref_doctype, ref_name) + } else if (contact) { + this.get_contact_number(contact, ref_doctype, ref_name); } else { me.show_dialog(); } - } - this.show_dialog = function() { - if(!me.dialog) - me.make_dialog(); + }; + this.show_dialog = function () { + if (!me.dialog) me.make_dialog(); me.dialog.set_values({ - 'message': me.message, - 'number': me.number - }) + message: me.message, + number: me.number, + }); me.dialog.show(); - } - this.make_dialog = function() { + }; + this.make_dialog = function () { var d = new frappe.ui.Dialog({ - title: 'Send SMS', + title: "Send SMS", width: 400, fields: [ - {fieldname:'number', fieldtype:'Data', label:'Mobile Number', reqd:1}, - {fieldname:'message', fieldtype:'Text', label:'Message', reqd:1}, - {fieldname:'send', fieldtype:'Button', label:'Send'} - ] - }) - d.fields_dict.send.input.onclick = function() { + { fieldname: "number", fieldtype: "Data", label: "Mobile Number", reqd: 1 }, + { fieldname: "message", fieldtype: "Text", label: "Message", reqd: 1 }, + { fieldname: "send", fieldtype: "Button", label: "Send" }, + ], + }); + d.fields_dict.send.input.onclick = function () { var btn = d.fields_dict.send.input; var v = me.dialog.get_values(); - if(v) { + if (v) { $(btn).set_working(); frappe.call({ method: "frappe.core.doctype.sms_settings.sms_settings.send_sms", args: { receiver_list: [v.number], - msg: v.message + msg: v.message, }, - callback: function(r) { + callback: function (r) { $(btn).done_working(); - if(r.exc) {frappe.msgprint(r.exc); return; } + if (r.exc) { + frappe.msgprint(r.exc); + return; + } me.dialog.hide(); - } + }, }); } - } + }; this.dialog = d; - } + }; this.setup(); -} +}; diff --git a/erpnext/public/js/stock_analytics.js b/erpnext/public/js/stock_analytics.js index a343c3402af..1c75d161e64 100644 --- a/erpnext/public/js/stock_analytics.js +++ b/erpnext/public/js/stock_analytics.js @@ -1,68 +1,104 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - erpnext.StockAnalytics = class StockAnalytics extends erpnext.StockGridReport { constructor(wrapper, opts) { var args = { title: __("Stock Analytics"), - parent: $(wrapper).find('.layout-main'), + parent: $(wrapper).find(".layout-main"), page: wrapper.page, - doctypes: ["Item", "Item Group", "Warehouse", "Stock Ledger Entry", "Brand", - "Fiscal Year", "Serial No"], + doctypes: [ + "Item", + "Item Group", + "Warehouse", + "Stock Ledger Entry", + "Brand", + "Fiscal Year", + "Serial No", + ], tree_grid: { show: true, parent_field: "parent_item_group", - formatter: function(item) { - if(!item.is_group) { - return repl("\ - %(value)s", { + %(value)s", + { value: item.name, - }); + } + ); } else { return item.name; } - - } + }, }, - } + }; - if(opts) $.extend(args, opts); + if (opts) $.extend(args, opts); super(args); this.filters = [ - {fieldtype:"Select", label: __("Value or Qty"), fieldname: "value_or_qty", - options:[{label:__("Value"), value:"Value"}, {label:__("Quantity"), value:"Quantity"}], - filter: function(val, item, opts, me) { + { + fieldtype: "Select", + label: __("Value or Qty"), + fieldname: "value_or_qty", + options: [ + { label: __("Value"), value: "Value" }, + { label: __("Quantity"), value: "Quantity" }, + ], + filter: function (val, item, opts, me) { return me.apply_zero_filter(val, item, opts, me); - }}, - {fieldtype:"Select", label: __("Brand"), link:"Brand", fieldname: "brand", - default_value: __("Select Brand..."), filter: function(val, item, opts) { + }, + }, + { + fieldtype: "Select", + label: __("Brand"), + link: "Brand", + fieldname: "brand", + default_value: __("Select Brand..."), + filter: function (val, item, opts) { return val == opts.default_value || item.brand == val || item._show; - }, link_formatter: {filter_input: "brand"}}, - {fieldtype:"Select", label: __("Warehouse"), link:"Warehouse", fieldname: "warehouse", - default_value: __("Select Warehouse...")}, - {fieldtype:"Date", label: __("From Date"), fieldname: "from_date"}, - {fieldtype:"Date", label: __("To Date"), fieldname: "to_date"}, - {fieldtype:"Select", label: __("Range"), fieldname: "range", - options:[ - {label:__("Daily"), value:"Daily"}, - {label:__("Weekly"), value:"Weekly"}, - {label:__("Monthly"), value:"Monthly"}, - {label:__("Quarterly"), value:"Quarterly"}, - {label:__("Yearly"), value:"Yearly"}, - ]} + }, + link_formatter: { filter_input: "brand" }, + }, + { + fieldtype: "Select", + label: __("Warehouse"), + link: "Warehouse", + fieldname: "warehouse", + default_value: __("Select Warehouse..."), + }, + { fieldtype: "Date", label: __("From Date"), fieldname: "from_date" }, + { fieldtype: "Date", label: __("To Date"), fieldname: "to_date" }, + { + fieldtype: "Select", + label: __("Range"), + fieldname: "range", + options: [ + { label: __("Daily"), value: "Daily" }, + { label: __("Weekly"), value: "Weekly" }, + { label: __("Monthly"), value: "Monthly" }, + { label: __("Quarterly"), value: "Quarterly" }, + { label: __("Yearly"), value: "Yearly" }, + ], + }, ]; } setup_columns() { var std_columns = [ - {id: "name", name: __("Item"), field: "name", width: 300}, - {id: "brand", name: __("Brand"), field: "brand", width: 100}, - {id: "stock_uom", name: __("UOM"), field: "stock_uom", width: 100}, - {id: "opening", name: __("Opening"), field: "opening", hidden: true, - formatter: this.currency_formatter} + { id: "name", name: __("Item"), field: "name", width: 300 }, + { id: "brand", name: __("Brand"), field: "brand", width: 100 }, + { id: "stock_uom", name: __("UOM"), field: "stock_uom", width: 100 }, + { + id: "opening", + name: __("Opening"), + field: "opening", + hidden: true, + formatter: this.currency_formatter, + }, ]; this.make_date_range_columns(); @@ -79,24 +115,24 @@ erpnext.StockAnalytics = class StockAnalytics extends erpnext.StockGridReport { } init_filter_values() { super.init_filter_values(); - this.filter_inputs.range && this.filter_inputs.range.val('Monthly'); + this.filter_inputs.range && this.filter_inputs.range.val("Monthly"); } prepare_data() { var me = this; - if(!this.data) { + if (!this.data) { var items = this.prepare_tree("Item", "Item Group"); me.parent_map = {}; me.item_by_name = {}; me.data = []; - $.each(items, function(i, v) { + $.each(items, function (i, v) { var d = copy_dict(v); me.data.push(d); me.item_by_name[d.name] = d; - if(d.parent_item_group) { + if (d.parent_item_group) { me.parent_map[d.name] = d.parent_item_group; } me.reset_item_values(d); @@ -105,7 +141,7 @@ erpnext.StockAnalytics = class StockAnalytics extends erpnext.StockGridReport { this.data[0].checked = true; } else { // otherwise, only reset values - $.each(this.data, function(i, d) { + $.each(this.data, function (i, d) { me.reset_item_values(d); d["closing_qty_value"] = 0; }); @@ -113,7 +149,6 @@ erpnext.StockAnalytics = class StockAnalytics extends erpnext.StockGridReport { this.prepare_balances(); this.update_groups(); - } prepare_balances() { var me = this; @@ -124,23 +159,24 @@ erpnext.StockAnalytics = class StockAnalytics extends erpnext.StockGridReport { this.item_warehouse = {}; this.serialized_buying_rates = this.get_serialized_buying_rates(); - for(var i=0, j=data.length; i 0) { + if (sl.qty > 0) { // incoming - rate is given var rate = sl.incoming_rate; var add_qty = sl.qty; - if(wh.balance_qty < 0) { + if (wh.balance_qty < 0) { // negative valuation // only add value of quantity if // the balance goes above 0 add_qty = wh.balance_qty + sl.qty; - if(add_qty < 0) { + if (add_qty < 0) { add_qty = 0; } } - if(sl.serial_no) { + if (sl.serial_no) { var value_diff = this.get_serialized_value_diff(sl); } else { - var value_diff = (rate * add_qty); + var value_diff = rate * add_qty; } - if(add_qty) - wh.fifo_stack.push([add_qty, sl.incoming_rate, sl.posting_date]); + if (add_qty) wh.fifo_stack.push([add_qty, sl.incoming_rate, sl.posting_date]); } else { // called everytime for maintaining fifo stack var fifo_value_diff = this.get_fifo_value_diff(wh, sl); // outgoing - if(sl.serial_no) { + if (sl.serial_no) { var value_diff = -1 * this.get_serialized_value_diff(sl); - } else if(is_fifo) { + } else if (is_fifo) { var value_diff = fifo_value_diff; } else { // average rate for weighted average - var rate = (wh.balance_qty.toFixed(2) == 0.00 ? 0 : - flt(wh.balance_value) / flt(wh.balance_qty)); + var rate = wh.balance_qty.toFixed(2) == 0.0 ? 0 : flt(wh.balance_value) / flt(wh.balance_qty); // no change in value if negative qty - if((wh.balance_qty + sl.qty).toFixed(2) >= 0.00) - var value_diff = (rate * sl.qty); - else - var value_diff = -wh.balance_value; + if ((wh.balance_qty + sl.qty).toFixed(2) >= 0.0) var value_diff = rate * sl.qty; + else var value_diff = -wh.balance_value; } } @@ -66,14 +65,14 @@ erpnext.StockGridReport = class StockGridReport extends frappe.views.TreeGridRep var fifo_value_diff = 0.0; var qty = -sl.qty; - for(var i=0, j=fifo_stack.length; i= qty) { + if (batch[0] >= qty) { batch[0] = batch[0] - qty; - fifo_value_diff += (qty * batch[1]); + fifo_value_diff += qty * batch[1]; qty = 0.0; - if(batch[0]) { + if (batch[0]) { // batch still has qty put it back fifo_stack.push(batch); } @@ -82,7 +81,7 @@ erpnext.StockGridReport = class StockGridReport extends frappe.views.TreeGridRep break; } else { // consume this batch fully - fifo_value_diff += (batch[0] * batch[1]); + fifo_value_diff += batch[0] * batch[1]; qty = qty - batch[0]; } } @@ -96,8 +95,8 @@ erpnext.StockGridReport = class StockGridReport extends frappe.views.TreeGridRep var value_diff = 0.0; - $.each(sl.serial_no.trim().split("\n"), function(i, sr) { - if(sr) { + $.each(sl.serial_no.trim().split("\n"), function (i, sr) { + if (sr) { value_diff += flt(me.serialized_buying_rates[sr.trim().toLowerCase()]); } }); @@ -109,7 +108,7 @@ erpnext.StockGridReport = class StockGridReport extends frappe.views.TreeGridRep var serialized_buying_rates = {}; if (frappe.report_dump.data["Serial No"]) { - $.each(frappe.report_dump.data["Serial No"], function(i, sn) { + $.each(frappe.report_dump.data["Serial No"], function (i, sn) { serialized_buying_rates[sn.name.toLowerCase()] = flt(sn.incoming_rate); }); } diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js index 1c3e3147976..7caf87d3f67 100644 --- a/erpnext/public/js/telephony.js +++ b/erpnext/public/js/telephony.js @@ -1,13 +1,17 @@ -frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlData { +frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlData { make_input() { super.make_input(); - if (this.df.options == 'Phone') { + if (this.df.options == "Phone") { this.setup_phone(); } if (this.frm && this.frm.fields_dict) { - Object.values(this.frm.fields_dict).forEach(function(field) { - if (field.df.read_only === 1 && field.df.options === 'Phone' - && field.disp_area.style[0] != 'display' && !field.has_icon) { + Object.values(this.frm.fields_dict).forEach(function (field) { + if ( + field.df.read_only === 1 && + field.df.options === "Phone" && + field.disp_area.style[0] != "display" && + !field.has_icon + ) { field.setup_phone(); field.has_icon = true; } @@ -16,15 +20,18 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlDat } setup_phone() { if (frappe.phone_call.handler) { - let control = this.df.read_only ? '.control-value' : '.control-input'; - this.$wrapper.find(control) - .append(` + let control = this.df.read_only ? ".control-value" : ".control-input"; + this.$wrapper + .find(control) + .append( + ` - - ${frappe.utils.icon('call')} + + ${frappe.utils.icon("call")} - `) - .find('.phone-btn') + ` + ) + .find(".phone-btn") .click(() => { frappe.phone_call.handler(this.get_value(), this.frm); }); diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index fef55942e46..4d886d47f4e 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -2,45 +2,47 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext"); frappe.provide("erpnext.utils"); +frappe.provide("erpnext.stock.utils"); $.extend(erpnext, { - get_currency: function(company) { - if(!company && cur_frm) - company = cur_frm.doc.company; - if(company) + get_currency: function (company) { + if (!company && cur_frm) company = cur_frm.doc.company; + if (company) return frappe.get_doc(":Company", company).default_currency || frappe.boot.sysdefaults.currency; - else - return frappe.boot.sysdefaults.currency; + else return frappe.boot.sysdefaults.currency; }, get_presentation_currency_list: () => { const docs = frappe.boot.docs; - let currency_list = docs.filter(d => d.doctype === ":Currency").map(d => d.name); + let currency_list = docs.filter((d) => d.doctype === ":Currency").map((d) => d.name); currency_list.unshift(""); return currency_list; }, - toggle_naming_series: function() { - if(cur_frm.fields_dict.naming_series) { - cur_frm.toggle_display("naming_series", cur_frm.doc.__islocal?true:false); + toggle_naming_series: function () { + if (cur_frm && cur_frm.fields_dict.naming_series) { + cur_frm.toggle_display("naming_series", cur_frm.doc.__islocal ? true : false); } }, - hide_company: function() { - if(cur_frm.fields_dict.company) { + hide_company: function () { + if (cur_frm.fields_dict.company) { var companies = Object.keys(locals[":Company"] || {}); - if(companies.length === 1) { - if(!cur_frm.doc.company) cur_frm.set_value("company", companies[0]); + if (companies.length === 1) { + if (!cur_frm.doc.company) cur_frm.set_value("company", companies[0]); cur_frm.toggle_display("company", false); - } else if(erpnext.last_selected_company) { - if(!cur_frm.doc.company) cur_frm.set_value("company", erpnext.last_selected_company); + } else if (erpnext.last_selected_company) { + if (!cur_frm.doc.company) cur_frm.set_value("company", erpnext.last_selected_company); } } }, - is_perpetual_inventory_enabled: function(company) { - if(company) { - return frappe.get_doc(":Company", company).enable_perpetual_inventory + is_perpetual_inventory_enabled: function (company) { + if (company) { + let company_local = locals[":Company"] && locals[":Company"][company]; + if (company_local) { + return cint(company_local.enable_perpetual_inventory); + } } }, @@ -48,280 +50,321 @@ $.extend(erpnext, { return cint(frappe.boot.sysdefaults.allow_stale); }, - setup_serial_or_batch_no: function() { + setup_serial_or_batch_no: function () { let grid_row = cur_frm.open_grid_row(); - if (!grid_row || !grid_row.grid_form.fields_dict.serial_no || - grid_row.grid_form.fields_dict.serial_no.get_status() !== "Write") return; + if ( + !grid_row || + !grid_row.grid_form.fields_dict.serial_no || + grid_row.grid_form.fields_dict.serial_no.get_status() !== "Write" + ) + return; - frappe.model.get_value('Item', {'name': grid_row.doc.item_code}, - ['has_serial_no', 'has_batch_no'], ({has_serial_no, has_batch_no}) => { - Object.assign(grid_row.doc, {has_serial_no, has_batch_no}); + frappe.model.get_value( + "Item", + { name: grid_row.doc.item_code }, + ["has_serial_no", "has_batch_no"], + ({ has_serial_no, has_batch_no }) => { + Object.assign(grid_row.doc, { has_serial_no, has_batch_no }); if (has_serial_no) { - attach_selector_button(__("Add Serial No"), - grid_row.grid_form.fields_dict.serial_no.$wrapper, this, grid_row); + attach_selector_button( + __("Add Serial No"), + grid_row.grid_form.fields_dict.serial_no.$wrapper, + this, + grid_row + ); } else if (has_batch_no) { - attach_selector_button(__("Pick Batch No"), - grid_row.grid_form.fields_dict.batch_no.$wrapper, this, grid_row); + attach_selector_button( + __("Pick Batch No"), + grid_row.grid_form.fields_dict.batch_no.$wrapper, + this, + grid_row + ); } } ); }, route_to_adjustment_jv: (args) => { - frappe.model.with_doctype('Journal Entry', () => { + frappe.model.with_doctype("Journal Entry", () => { // route to adjustment Journal Entry to handle Account Balance and Stock Value mismatch - let journal_entry = frappe.model.get_new_doc('Journal Entry'); + let journal_entry = frappe.model.get_new_doc("Journal Entry"); args.accounts.forEach((je_account) => { let child_row = frappe.model.add_child(journal_entry, "accounts"); child_row.account = je_account.account; child_row.debit_in_account_currency = je_account.debit_in_account_currency; child_row.credit_in_account_currency = je_account.credit_in_account_currency; - child_row.party_type = "" ; + child_row.party_type = ""; }); - frappe.set_route('Form','Journal Entry', journal_entry.name); + frappe.set_route("Form", "Journal Entry", journal_entry.name); }); }, route_to_pending_reposts: (args) => { - frappe.set_route('List', 'Repost Item Valuation', args); + frappe.set_route("List", "Repost Item Valuation", args); }, }); - $.extend(erpnext.utils, { - set_party_dashboard_indicators: function(frm) { - if(frm.doc.__onload && frm.doc.__onload.dashboard_info) { + set_party_dashboard_indicators: function (frm) { + if (frm.doc.__onload && frm.doc.__onload.dashboard_info) { var company_wise_info = frm.doc.__onload.dashboard_info; - if(company_wise_info.length > 1) { - company_wise_info.forEach(function(info) { + if (company_wise_info.length > 1) { + company_wise_info.forEach(function (info) { erpnext.utils.add_indicator_for_multicompany(frm, info); }); } else if (company_wise_info.length === 1) { - frm.dashboard.add_indicator(__('Annual Billing: {0}', - [format_currency(company_wise_info[0].billing_this_year, company_wise_info[0].currency)]), 'blue'); - frm.dashboard.add_indicator(__('Total Unpaid: {0}', - [format_currency(company_wise_info[0].total_unpaid, company_wise_info[0].currency)]), - company_wise_info[0].total_unpaid ? 'orange' : 'green'); + frm.dashboard.add_indicator( + __("Annual Billing: {0}", [ + format_currency( + company_wise_info[0].billing_this_year, + company_wise_info[0].currency + ), + ]), + "blue" + ); + frm.dashboard.add_indicator( + __("Total Unpaid: {0}", [ + format_currency(company_wise_info[0].total_unpaid, company_wise_info[0].currency), + ]), + company_wise_info[0].total_unpaid ? "orange" : "green" + ); - if(company_wise_info[0].loyalty_points) { - frm.dashboard.add_indicator(__('Loyalty Points: {0}', - [company_wise_info[0].loyalty_points]), 'blue'); + if (company_wise_info[0].loyalty_points) { + frm.dashboard.add_indicator( + __("Loyalty Points: {0}", [company_wise_info[0].loyalty_points]), + "blue" + ); } } } }, - add_indicator_for_multicompany: function(frm, info) { + add_indicator_for_multicompany: function (frm, info) { frm.dashboard.stats_area.show(); - frm.dashboard.stats_area_row.addClass('flex'); - frm.dashboard.stats_area_row.css('flex-wrap', 'wrap'); + frm.dashboard.stats_area_row.addClass("flex"); + frm.dashboard.stats_area_row.css("flex-wrap", "wrap"); - var color = info.total_unpaid ? 'orange' : 'green'; + var color = info.total_unpaid ? "orange" : "green"; - var indicator = $('
              '+ - '
              '+info.company+'
              '+ + var indicator = $( + '
              ' + + '
              ' + + info.company + + "
              " + + '" + + '" + + "
              " + ).appendTo(frm.dashboard.stats_area_row); - ''+ - - ''+ - - - '
              ').appendTo(frm.dashboard.stats_area_row); - - if(info.loyalty_points){ - $('').appendTo(indicator); + if (info.loyalty_points) { + $( + '" + ).appendTo(indicator); } return indicator; }, - get_party_name: function(party_type) { - var dict = {'Customer': 'customer_name', 'Supplier': 'supplier_name', 'Employee': 'employee_name', - 'Member': 'member_name'}; + get_party_name: function (party_type) { + var dict = { + Customer: "customer_name", + Supplier: "supplier_name", + Employee: "employee_name", + Member: "member_name", + }; return dict[party_type]; }, - copy_value_in_all_rows: function(doc, dt, dn, table_fieldname, fieldname) { + copy_value_in_all_rows: function (doc, dt, dn, table_fieldname, fieldname) { var d = locals[dt][dn]; - if(d[fieldname]){ + if (d[fieldname]) { var cl = doc[table_fieldname] || []; - for(var i = 0; i < cl.length; i++) { - if(!cl[i][fieldname]) cl[i][fieldname] = d[fieldname]; + for (var i = 0; i < cl.length; i++) { + if (!cl[i][fieldname]) cl[i][fieldname] = d[fieldname]; } } refresh_field(table_fieldname); }, - get_terms: function(tc_name, doc, callback) { - if(tc_name) { + get_terms: function (tc_name, doc, callback) { + if (tc_name) { return frappe.call({ - method: 'erpnext.setup.doctype.terms_and_conditions.terms_and_conditions.get_terms_and_conditions', + method: "erpnext.setup.doctype.terms_and_conditions.terms_and_conditions.get_terms_and_conditions", args: { template_name: tc_name, - doc: doc + doc: doc, + }, + callback: function (r) { + callback(r); }, - callback: function(r) { - callback(r) - } }); } }, - make_bank_account: function(doctype, docname) { + make_bank_account: function (doctype, docname) { frappe.call({ method: "erpnext.accounts.doctype.bank_account.bank_account.make_bank_account", args: { doctype: doctype, - docname: docname + docname: docname, }, freeze: true, - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } - }) + }, + }); }, - add_dimensions: function(report_name, index) { + add_dimensions: function (report_name, index) { let filters = frappe.query_reports[report_name].filters; frappe.call({ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", - callback: function(r) { + callback: function (r) { let accounting_dimensions = r.message[0]; accounting_dimensions.forEach((dimension) => { - let found = filters.some(el => el.fieldname === dimension['fieldname']); + let found = filters.some((el) => el.fieldname === dimension["fieldname"]); if (!found) { filters.splice(index, 0, { - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "MultiSelectList", - get_data: function(txt) { + fieldname: dimension["fieldname"], + label: __(dimension["label"]), + fieldtype: "MultiSelectList", + get_data: function (txt) { return frappe.db.get_link_options(dimension["document_type"], txt); }, }); } }); - } + }, }); }, - add_inventory_dimensions: function(report_name, index) { + add_inventory_dimensions: function (report_name, index) { let filters = frappe.query_reports[report_name].filters; frappe.call({ method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_inventory_dimensions", - callback: function(r) { + callback: function (r) { if (r.message && r.message.length) { r.message.forEach((dimension) => { - let existing_filter = filters.filter(el => el.fieldname === dimension['fieldname']); + let existing_filter = filters.filter((el) => el.fieldname === dimension["fieldname"]); if (!existing_filter.length) { filters.splice(index, 0, { - "fieldname": dimension["fieldname"], - "label": __(dimension["doctype"]), - "fieldtype": "MultiSelectList", - get_data: function(txt) { + fieldname: dimension["fieldname"], + label: __(dimension["doctype"]), + fieldtype: "MultiSelectList", + get_data: function (txt) { return frappe.db.get_link_options(dimension["doctype"], txt); }, }); } else { - existing_filter[0]['fieldtype'] = "MultiSelectList"; - existing_filter[0]['get_data'] = function(txt) { + existing_filter[0]["fieldtype"] = "MultiSelectList"; + existing_filter[0]["get_data"] = function (txt) { return frappe.db.get_link_options(dimension["doctype"], txt); - } + }; } }); } - } + }, }); }, - make_subscription: function(doctype, docname) { + make_subscription: function (doctype, docname) { frappe.call({ method: "frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat", args: { doctype: doctype, - docname: docname + docname: docname, }, - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } - }) + }, + }); }, - make_pricing_rule: function(doctype, docname) { + make_pricing_rule: function (doctype, docname) { frappe.call({ method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.make_pricing_rule", args: { doctype: doctype, - docname: docname + docname: docname, }, - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } - }) + }, + }); }, /** - * Checks if the first row of a given child table is empty - * @param child_table - Child table Doctype - * @return {Boolean} - **/ - first_row_is_empty: function(child_table){ - if($.isArray(child_table) && child_table.length > 0) { + * Checks if the first row of a given child table is empty + * @param child_table - Child table Doctype + * @return {Boolean} + **/ + first_row_is_empty: function (child_table) { + if ($.isArray(child_table) && child_table.length > 0) { return !child_table[0].item_code; } return false; }, /** - * Removes the first row of a child table if it is empty - * @param {_Frm} frm - The current form - * @param {String} child_table_name - The child table field name - * @return {Boolean} - **/ - remove_empty_first_row: function(frm, child_table_name){ - const rows = frm['doc'][child_table_name]; - if (this.first_row_is_empty(rows)){ - frm['doc'][child_table_name] = rows.splice(1); + * Removes the first row of a child table if it is empty + * @param {_Frm} frm - The current form + * @param {String} child_table_name - The child table field name + * @return {Boolean} + **/ + remove_empty_first_row: function (frm, child_table_name) { + const rows = frm["doc"][child_table_name]; + if (this.first_row_is_empty(rows)) { + frm["doc"][child_table_name] = rows.splice(1); } return rows; }, - get_tree_options: function(option) { + get_tree_options: function (option) { // get valid options for tree based on user permission & locals dict let unscrub_option = frappe.model.unscrub(option); let user_permission = frappe.defaults.get_user_permissions(); let options; - if(user_permission && user_permission[unscrub_option]) { - options = user_permission[unscrub_option].map(perm => perm.doc); + if (user_permission && user_permission[unscrub_option]) { + options = user_permission[unscrub_option].map((perm) => perm.doc); } else { - options = $.map(locals[`:${unscrub_option}`], function(c) { return c.name; }).sort(); + options = $.map(locals[`:${unscrub_option}`], function (c) { + return c.name; + }).sort(); } // filter unique values, as there may be multiple user permissions for any value return options.filter((value, index, self) => self.indexOf(value) === index); }, - get_tree_default: function(option) { + get_tree_default: function (option) { // set default for a field based on user permission let options = this.get_tree_options(option); - if(options.includes(frappe.defaults.get_default(option))) { + if (options.includes(frappe.defaults.get_default(option))) { return frappe.defaults.get_default(option); } else { return options[0]; } }, - overrides_parent_value_in_all_rows: function(doc, dt, dn, table_fieldname, fieldname, parent_fieldname) { + overrides_parent_value_in_all_rows: function (doc, dt, dn, table_fieldname, fieldname, parent_fieldname) { if (doc[parent_fieldname]) { let cl = doc[table_fieldname] || []; for (let i = 0; i < cl.length; i++) { @@ -331,7 +374,7 @@ $.extend(erpnext.utils, { } }, create_new_doc: function (doctype, update_fields) { - frappe.model.with_doctype(doctype, function() { + frappe.model.with_doctype(doctype, function () { var new_doc = frappe.model.get_new_doc(doctype); for (let [key, value] of Object.entries(update_fields)) { new_doc[key] = value; @@ -343,12 +386,15 @@ $.extend(erpnext.utils, { // check if payments app is installed on site, if not warn user. check_payments_app: () => { if (frappe.boot.versions && !frappe.boot.versions.payments) { - const marketplace_link = '
              Marketplace' - const github_link = 'GitHub' - const msg = __("payments app is not installed. Please install it from {0} or {1}", [marketplace_link, github_link]) + const marketplace_link = + 'Marketplace'; + const github_link = 'GitHub'; + const msg = __("payments app is not installed. Please install it from {0} or {1}", [ + marketplace_link, + github_link, + ]); frappe.msgprint(msg); } - }, get_fiscal_year: function (date, with_dates = false, boolean = false) { @@ -378,117 +424,128 @@ $.extend(erpnext.utils, { }, }); -erpnext.utils.select_alternate_items = function(opts) { +erpnext.utils.select_alternate_items = function (opts) { const frm = opts.frm; - const warehouse_field = opts.warehouse_field || 'warehouse'; - const item_field = opts.item_field || 'item_code'; + const warehouse_field = opts.warehouse_field || "warehouse"; + const item_field = opts.item_field || "item_code"; this.data = []; const dialog = new frappe.ui.Dialog({ title: __("Select Alternate Item"), fields: [ - {fieldtype:'Section Break', label: __('Items')}, + { fieldtype: "Section Break", label: __("Items") }, { - fieldname: "alternative_items", fieldtype: "Table", cannot_add_rows: true, - in_place_edit: true, data: this.data, + fieldname: "alternative_items", + fieldtype: "Table", + cannot_add_rows: true, + in_place_edit: true, + data: this.data, get_data: () => { return this.data; }, - fields: [{ - fieldtype:'Data', - fieldname:"docname", - hidden: 1 - }, { - fieldtype:'Link', - fieldname:"item_code", - options: 'Item', - in_list_view: 1, - read_only: 1, - label: __('Item Code') - }, { - fieldtype:'Link', - fieldname:"alternate_item", - options: 'Item', - default: "", - in_list_view: 1, - label: __('Alternate Item'), - onchange: function() { - const item_code = this.get_value(); - const warehouse = this.grid_row.on_grid_fields_dict.warehouse.get_value(); - if (item_code && warehouse) { - frappe.call({ - method: "erpnext.stock.utils.get_latest_stock_qty", - args: { - item_code: item_code, - warehouse: warehouse - }, - callback: (r) => { - this.grid_row.on_grid_fields_dict - .actual_qty.set_value(r.message || 0); - } - }) - } + fields: [ + { + fieldtype: "Data", + fieldname: "docname", + hidden: 1, }, - get_query: (e) => { - return { - query: "erpnext.stock.doctype.item_alternative.item_alternative.get_alternative_items", - filters: { - item_code: e.item_code + { + fieldtype: "Link", + fieldname: "item_code", + options: "Item", + in_list_view: 1, + read_only: 1, + label: __("Item Code"), + }, + { + fieldtype: "Link", + fieldname: "alternate_item", + options: "Item", + default: "", + in_list_view: 1, + label: __("Alternate Item"), + onchange: function () { + const item_code = this.get_value(); + const warehouse = this.grid_row.on_grid_fields_dict.warehouse.get_value(); + if (item_code && warehouse) { + frappe.call({ + method: "erpnext.stock.utils.get_latest_stock_qty", + args: { + item_code: item_code, + warehouse: warehouse, + }, + callback: (r) => { + this.grid_row.on_grid_fields_dict.actual_qty.set_value( + r.message || 0 + ); + }, + }); } - }; - } - }, { - fieldtype:'Link', - fieldname:"warehouse", - options: 'Warehouse', - default: "", - in_list_view: 1, - label: __('Warehouse'), - onchange: function() { - const warehouse = this.get_value(); - const item_code = this.grid_row.on_grid_fields_dict.item_code.get_value(); - if (item_code && warehouse) { - frappe.call({ - method: "erpnext.stock.utils.get_latest_stock_qty", - args: { - item_code: item_code, - warehouse: warehouse + }, + get_query: (e) => { + return { + query: "erpnext.stock.doctype.item_alternative.item_alternative.get_alternative_items", + filters: { + item_code: e.item_code, }, - callback: (r) => { - this.grid_row.on_grid_fields_dict - .actual_qty.set_value(r.message || 0); - } - }) - } + }; + }, }, - }, { - fieldtype:'Float', - fieldname:"actual_qty", - default: 0, - read_only: 1, - in_list_view: 1, - label: __('Available Qty') - }] + { + fieldtype: "Link", + fieldname: "warehouse", + options: "Warehouse", + default: "", + in_list_view: 1, + label: __("Warehouse"), + onchange: function () { + const warehouse = this.get_value(); + const item_code = this.grid_row.on_grid_fields_dict.item_code.get_value(); + if (item_code && warehouse) { + frappe.call({ + method: "erpnext.stock.utils.get_latest_stock_qty", + args: { + item_code: item_code, + warehouse: warehouse, + }, + callback: (r) => { + this.grid_row.on_grid_fields_dict.actual_qty.set_value( + r.message || 0 + ); + }, + }); + } + }, + }, + { + fieldtype: "Float", + fieldname: "actual_qty", + default: 0, + read_only: 1, + in_list_view: 1, + label: __("Available Qty"), + }, + ], }, ], - primary_action: function() { + primary_action: function () { const args = this.get_values()["alternative_items"]; - const alternative_items = args.filter(d => { + const alternative_items = args.filter((d) => { if (d.alternate_item && d.item_code != d.alternate_item) { return true; } }); - alternative_items.forEach(d => { + alternative_items.forEach((d) => { let row = frappe.get_doc(opts.child_doctype, d.docname); let qty = null; - if (row.doctype === 'Work Order Item') { + if (row.doctype === "Work Order Item") { qty = row.required_qty; } else { qty = row.qty; } row[item_field] = d.alternate_item; - frappe.model.set_value(row.doctype, row.name, 'qty', qty); + frappe.model.set_value(row.doctype, row.name, "qty", qty); frappe.model.set_value(row.doctype, row.name, opts.original_item_field, d.item_code); frm.trigger(item_field, row.doctype, row.name); }); @@ -496,139 +553,145 @@ erpnext.utils.select_alternate_items = function(opts) { refresh_field(opts.child_docname); this.hide(); }, - primary_action_label: __('Update') + primary_action_label: __("Update"), }); - frm.doc[opts.child_docname].forEach(d => { + frm.doc[opts.child_docname].forEach((d) => { if (!opts.condition || opts.condition(d)) { dialog.fields_dict.alternative_items.df.data.push({ - "docname": d.name, - "item_code": d[item_field], - "warehouse": d[warehouse_field], - "actual_qty": d.actual_qty + docname: d.name, + item_code: d[item_field], + warehouse: d[warehouse_field], + actual_qty: d.actual_qty, }); } - }) + }); this.data = dialog.fields_dict.alternative_items.df.data; dialog.fields_dict.alternative_items.grid.refresh(); dialog.show(); -} +}; -erpnext.utils.update_child_items = function(opts) { +erpnext.utils.update_child_items = function (opts) { const frm = opts.frm; - const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row; - const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname; + const cannot_add_row = typeof opts.cannot_add_row === "undefined" ? true : opts.cannot_add_row; + const child_docname = typeof opts.cannot_add_row === "undefined" ? "items" : opts.child_docname; const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`); - const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision; + const get_precision = (fieldname) => child_meta.fields.find((f) => f.fieldname == fieldname).precision; this.data = frm.doc[opts.child_docname].map((d) => { return { - "docname": d.name, - "name": d.name, - "item_code": d.item_code, - "delivery_date": d.delivery_date, - "schedule_date": d.schedule_date, - "conversion_factor": d.conversion_factor, - "qty": d.qty, - "rate": d.rate, - "uom": d.uom - } + docname: d.name, + name: d.name, + item_code: d.item_code, + delivery_date: d.delivery_date, + schedule_date: d.schedule_date, + conversion_factor: d.conversion_factor, + qty: d.qty, + rate: d.rate, + uom: d.uom, + }; }); - const fields = [{ - fieldtype:'Data', - fieldname:"docname", - read_only: 1, - hidden: 1, - }, { - fieldtype:'Link', - fieldname:"item_code", - options: 'Item', - in_list_view: 1, - read_only: 0, - disabled: 0, - label: __('Item Code'), - get_query: function() { - let filters; - if (frm.doc.doctype == 'Sales Order') { - filters = {"is_sales_item": 1}; - } else if (frm.doc.doctype == 'Purchase Order') { - if (frm.doc.is_subcontracted) { - if (frm.doc.is_old_subcontracting_flow) { - filters = {"is_sub_contracted_item": 1}; - } else { - filters = {"is_stock_item": 0}; - } - } else { - filters = {"is_purchase_item": 1}; - } - } - return { - query: "erpnext.controllers.queries.item_query", - filters: filters - }; - } - }, { - fieldtype:'Link', - fieldname:'uom', - options: 'UOM', - read_only: 0, - label: __('UOM'), - reqd: 1, - onchange: function () { - frappe.call({ - method: "erpnext.stock.get_item_details.get_conversion_factor", - args: { item_code: this.doc.item_code, uom: this.value }, - callback: r => { - if(!r.exc) { - if (this.doc.conversion_factor == r.message.conversion_factor) return; - - const docname = this.doc.docname; - dialog.fields_dict.trans_items.df.data.some(doc => { - if (doc.docname == docname) { - doc.conversion_factor = r.message.conversion_factor; - dialog.fields_dict.trans_items.grid.refresh(); - return true; - } - }) - } - } - }); - } - }, { - fieldtype:'Float', - fieldname:"qty", - default: 0, - read_only: 0, - in_list_view: 1, - label: __('Qty'), - precision: get_precision("qty") - }, { - fieldtype:'Currency', - fieldname:"rate", - options: "currency", - default: 0, - read_only: 0, - in_list_view: 1, - label: __('Rate'), - precision: get_precision("rate") - }]; - - if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) { - fields.splice(2, 0, { - fieldtype: 'Date', - fieldname: frm.doc.doctype == 'Sales Order' ? "delivery_date" : "schedule_date", + const fields = [ + { + fieldtype: "Data", + fieldname: "docname", + read_only: 1, + hidden: 1, + }, + { + fieldtype: "Link", + fieldname: "item_code", + options: "Item", in_list_view: 1, - label: frm.doc.doctype == 'Sales Order' ? __("Delivery Date") : __("Reqd by date"), - reqd: 1 - }) + read_only: 0, + disabled: 0, + label: __("Item Code"), + get_query: function () { + let filters; + if (frm.doc.doctype == "Sales Order") { + filters = { is_sales_item: 1 }; + } else if (frm.doc.doctype == "Purchase Order") { + if (frm.doc.is_subcontracted) { + if (frm.doc.is_old_subcontracting_flow) { + filters = { is_sub_contracted_item: 1 }; + } else { + filters = { is_stock_item: 0 }; + } + } else { + filters = { is_purchase_item: 1 }; + } + } + return { + query: "erpnext.controllers.queries.item_query", + filters: filters, + }; + }, + }, + { + fieldtype: "Link", + fieldname: "uom", + options: "UOM", + read_only: 0, + label: __("UOM"), + reqd: 1, + onchange: function () { + frappe.call({ + method: "erpnext.stock.get_item_details.get_conversion_factor", + args: { item_code: this.doc.item_code, uom: this.value }, + callback: (r) => { + if (!r.exc) { + if (this.doc.conversion_factor == r.message.conversion_factor) return; + + const docname = this.doc.docname; + dialog.fields_dict.trans_items.df.data.some((doc) => { + if (doc.docname == docname) { + doc.conversion_factor = r.message.conversion_factor; + dialog.fields_dict.trans_items.grid.refresh(); + return true; + } + }); + } + }, + }); + }, + }, + { + fieldtype: "Float", + fieldname: "qty", + default: 0, + read_only: 0, + in_list_view: 1, + label: __("Qty"), + precision: get_precision("qty"), + }, + { + fieldtype: "Currency", + fieldname: "rate", + options: "currency", + default: 0, + read_only: 0, + in_list_view: 1, + label: __("Rate"), + precision: get_precision("rate"), + }, + ]; + + if (frm.doc.doctype == "Sales Order" || frm.doc.doctype == "Purchase Order") { + fields.splice(2, 0, { + fieldtype: "Date", + fieldname: frm.doc.doctype == "Sales Order" ? "delivery_date" : "schedule_date", + in_list_view: 1, + label: frm.doc.doctype == "Sales Order" ? __("Delivery Date") : __("Reqd by date"), + reqd: 1, + }); fields.splice(3, 0, { - fieldtype: 'Float', + fieldtype: "Float", fieldname: "conversion_factor", label: __("Conversion Factor"), - precision: get_precision('conversion_factor') - }) + precision: get_precision("conversion_factor"), + }); } new frappe.ui.Dialog({ @@ -646,86 +709,82 @@ erpnext.utils.update_child_items = function(opts) { get_data: () => { return this.data; }, - fields: fields + fields: fields, }, ], - primary_action: function() { + primary_action: function () { const trans_items = this.get_values()["trans_items"].filter((item) => !!item.item_code); frappe.call({ - method: 'erpnext.controllers.accounts_controller.update_child_qty_rate', + method: "erpnext.controllers.accounts_controller.update_child_qty_rate", freeze: true, args: { - 'parent_doctype': frm.doc.doctype, - 'trans_items': trans_items, - 'parent_doctype_name': frm.doc.name, - 'child_docname': child_docname + parent_doctype: frm.doc.doctype, + trans_items: trans_items, + parent_doctype_name: frm.doc.name, + child_docname: child_docname, }, - callback: function() { + callback: function () { frm.reload_doc(); - } + }, }); this.hide(); refresh_field("items"); }, - primary_action_label: __('Update') + primary_action_label: __("Update"), }).show(); -} +}; - - - -erpnext.utils.map_current_doc = function(opts) { +erpnext.utils.map_current_doc = function (opts) { function _map() { - if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) { + if ($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) { // remove first item row if empty - if(!cur_frm.doc.items[0].item_code) { + if (!cur_frm.doc.items[0].item_code) { cur_frm.doc.items = cur_frm.doc.items.splice(1); } // find the doctype of the items table - var items_doctype = frappe.meta.get_docfield(cur_frm.doctype, 'items').options; + var items_doctype = frappe.meta.get_docfield(cur_frm.doctype, "items").options; // find the link fieldname from items table for the given // source_doctype var link_fieldname = null; - frappe.get_meta(items_doctype).fields.forEach(function(d) { - if(d.options===opts.source_doctype) link_fieldname = d.fieldname; }); + frappe.get_meta(items_doctype).fields.forEach(function (d) { + if (d.options === opts.source_doctype) link_fieldname = d.fieldname; + }); // search in existing items if the source_name is already set and full qty fetched var already_set = false; var item_qty_map = {}; - $.each(cur_frm.doc.items, function(i, d) { - opts.source_name.forEach(function(src) { - if(d[link_fieldname]==src) { + $.each(cur_frm.doc.items, function (i, d) { + opts.source_name.forEach(function (src) { + if (d[link_fieldname] == src) { already_set = true; - if (item_qty_map[d.item_code]) - item_qty_map[d.item_code] += flt(d.qty); - else - item_qty_map[d.item_code] = flt(d.qty); + if (item_qty_map[d.item_code]) item_qty_map[d.item_code] += flt(d.qty); + else item_qty_map[d.item_code] = flt(d.qty); } }); }); - if(already_set) { - opts.source_name.forEach(function(src) { - frappe.model.with_doc(opts.source_doctype, src, function(r) { + if (already_set) { + opts.source_name.forEach(function (src) { + frappe.model.with_doc(opts.source_doctype, src, function (r) { var source_doc = frappe.model.get_doc(opts.source_doctype, src); - $.each(source_doc.items || [], function(i, row) { - if(row.qty > flt(item_qty_map[row.item_code])) { + $.each(source_doc.items || [], function (i, row) { + if (row.qty > flt(item_qty_map[row.item_code])) { already_set = false; return false; } - }) - }) + }); + }); - if(already_set) { - frappe.msgprint(__("You have already selected items from {0} {1}", - [opts.source_doctype, src])); + if (already_set) { + frappe.msgprint( + __("You have already selected items from {0} {1}", [opts.source_doctype, src]) + ); return; } - - }) + }); } } @@ -733,20 +792,20 @@ erpnext.utils.map_current_doc = function(opts) { // Sometimes we hit the limit for URL length of a GET request // as we send the full target_doc. Hence this is a POST request. type: "POST", - method: 'frappe.model.mapper.map_docs', + method: "frappe.model.mapper.map_docs", args: { - "method": opts.method, - "source_names": opts.source_name, - "target_doc": cur_frm.doc, - "args": opts.args + method: opts.method, + source_names: opts.source_name, + target_doc: cur_frm.doc, + args: opts.args, }, - callback: function(r) { - if(!r.exc) { - var doc = frappe.model.sync(r.message); + callback: function (r) { + if (!r.exc) { + frappe.model.sync(r.message); cur_frm.dirty(); cur_frm.refresh(); } - } + }, }); } @@ -764,25 +823,40 @@ erpnext.utils.map_current_doc = function(opts) { } if (opts.source_doctype) { + let data_fields = []; + if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) { + let target_meta = frappe.get_meta(cur_frm.doc.doctype); + if (target_meta.fields.find((f) => f.fieldname === "taxes")) { + data_fields.push({ + fieldname: "merge_taxes", + fieldtype: "Check", + label: __("Merge taxes from multiple documents"), + }); + } + } const d = new frappe.ui.form.MultiSelectDialog({ doctype: opts.source_doctype, target: opts.target, date_field: opts.date_field || undefined, setters: opts.setters, + data_fields: data_fields, get_query: opts.get_query, add_filters_group: 1, allow_child_item_selection: opts.allow_child_item_selection, child_fieldname: opts.child_fieldname, child_columns: opts.child_columns, size: opts.size, - action: function(selections, args) { + action: function (selections, args) { let values = selections; if (values.length === 0) { - frappe.msgprint(__("Please select {0}", [opts.source_doctype])) + frappe.msgprint(__("Please select {0}", [opts.source_doctype])); return; } opts.source_name = values; - if (opts.allow_child_item_selection) { + if ( + opts.allow_child_item_selection || + ["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype) + ) { // args contains filtered child docnames opts.args = args; } @@ -798,11 +872,11 @@ erpnext.utils.map_current_doc = function(opts) { opts.source_name = [opts.source_name]; _map(); } -} +}; -frappe.form.link_formatters['Item'] = function(value, doc) { +frappe.form.link_formatters["Item"] = function (value, doc) { if (doc && value && doc.item_name && doc.item_name !== value && doc.item_code === value) { - return value + ': ' + doc.item_name; + return value + ": " + doc.item_name; } else if (!value && doc.doctype && doc.item_name) { // format blank value in child table return doc.item_name; @@ -810,11 +884,11 @@ frappe.form.link_formatters['Item'] = function(value, doc) { // if value is blank in report view or item code and name are the same, return as is return value; } -} +}; -frappe.form.link_formatters['Employee'] = function(value, doc) { +frappe.form.link_formatters["Employee"] = function (value, doc) { if (doc && value && doc.employee_name && doc.employee_name !== value && doc.employee === value) { - return value + ': ' + doc.employee_name; + return value + ": " + doc.employee_name; } else if (!value && doc.doctype && doc.employee_name) { // format blank value in child table return doc.employee; @@ -822,11 +896,11 @@ frappe.form.link_formatters['Employee'] = function(value, doc) { // if value is blank in report view or project name and name are the same, return as is return value; } -} +}; -frappe.form.link_formatters['Project'] = function(value, doc) { +frappe.form.link_formatters["Project"] = function (value, doc) { if (doc && value && doc.project_name && doc.project_name !== value && doc.project === value) { - return value + ': ' + doc.project_name; + return value + ": " + doc.project_name; } else if (!value && doc.doctype && doc.project_name) { // format blank value in child table return doc.project; @@ -837,64 +911,74 @@ frappe.form.link_formatters['Project'] = function(value, doc) { }; // add description on posting time -$(document).on('app_ready', function() { - if(!frappe.datetime.is_timezone_same()) { - $.each(["Stock Reconciliation", "Stock Entry", "Stock Ledger Entry", - "Delivery Note", "Purchase Receipt", "Sales Invoice"], function(i, d) { - frappe.ui.form.on(d, "onload", function(frm) { - cur_frm.set_df_property("posting_time", "description", - frappe.sys_defaults.time_zone); - }); - }); +$(document).on("app_ready", function () { + if (!frappe.datetime.is_timezone_same()) { + $.each( + [ + "Stock Reconciliation", + "Stock Entry", + "Stock Ledger Entry", + "Delivery Note", + "Purchase Receipt", + "Sales Invoice", + ], + function (i, d) { + frappe.ui.form.on(d, "onload", function (frm) { + cur_frm.set_df_property("posting_time", "description", frappe.sys_defaults.time_zone); + }); + } + ); } }); // Show SLA dashboard -$(document).on('app_ready', function() { - $.each(frappe.boot.service_level_agreement_doctypes, function(_i, d) { +$(document).on("app_ready", function () { + $.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; + 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', + 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 + customer: frm.doc.customer, }, callback: function (r) { if (r && r.message) { - frm.set_query('priority', function() { + frm.set_query("priority", function () { return { filters: { - 'name': ['in', r.message.priority], - } + name: ["in", r.message.priority], + }, }; }); - frm.set_query('service_level_agreement', function() { + frm.set_query("service_level_agreement", function () { return { filters: { - 'name': ['in', r.message.service_level_agreements], - } + name: ["in", r.message.service_level_agreements], + }, }; }); } - } + }, }); }, - refresh: function(frm) { - if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement - && ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) { + 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', + method: "frappe.client.get", args: { - doctype: 'Service Level Agreement', - name: frm.doc.service_level_agreement + doctype: "Service Level Agreement", + name: frm.doc.service_level_agreement, }, - callback: function(data) { + callback: function (data) { let statuses = data.message.pause_sla_on; const hold_statuses = []; $.each(statuses, (_i, entry) => { @@ -902,32 +986,46 @@ $(document).on('app_ready', function() { }); 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)])}; + 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 +' ' + - '
              ' + - '
              ' + '' + + 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'}; + 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( '
              ' + '
              ' + - ' ' + - '
              ' + - '
              ' + ' " + + "" + + "" ); } }, @@ -953,7 +1051,6 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
              `; - if (apply_sla_for_resolution) { let time_to_resolve; if (!frm.doc.resolution_date) { @@ -970,34 +1067,39 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { `; } - alert += ''; + alert += ""; frm.dashboard.set_headline_alert(alert); } function get_time_left(timestamp, agreement_status) { const diff = moment(timestamp).diff(frappe.datetime.system_datetime(true)); - const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed'; - let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green'; - return {'diff_display': diff_display, 'indicator': indicator}; + const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed"; + let indicator = diff_display == "Failed" && agreement_status != "Fulfilled" ? "red" : "green"; + return { diff_display: diff_display, indicator: indicator }; } function get_status(expected, actual) { const time_left = moment(expected).diff(moment(actual)); if (time_left >= 0) { - return {'diff_display': 'Fulfilled', 'indicator': 'green'}; + return { diff_display: "Fulfilled", indicator: "green" }; } else { - return {'diff_display': 'Failed', 'indicator': 'red'}; + return { diff_display: "Failed", indicator: "red" }; } } function attach_selector_button(inner_text, append_loction, context, grid_row) { - let $btn_div = $("
              ").css({"margin-bottom": "10px", "margin-top": "10px"}) - .appendTo(append_loction); - let $btn = $(``) - .appendTo($btn_div); + let $btn_div = $("
              ").css({ "margin-bottom": "10px", "margin-top": "10px" }).appendTo(append_loction); + let $btn = $(``).appendTo($btn_div); - $btn.on("click", function() { + $btn.on("click", function () { context.show_serial_batch_selector(grid_row.frm, grid_row.doc, "", "", true); }); } + +$.extend(erpnext.stock.utils, { + set_item_details_using_barcode(frm, child_row, callback) { + const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm }); + barcode_scanner.scan_api_call(child_row.barcode, callback); + }, +}); diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index f1b53cb0721..15d13f9ffb1 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -1,6 +1,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { constructor(opts) { this.frm = opts.frm; + // frappe.flags.trigger_from_barcode_scanner is used for custom scripts // field from which to capture input of scanned data this.scan_field_name = opts.scan_field_name || "scan_barcode"; @@ -57,13 +58,15 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return; } - me.update_table(data).then(row => { - this.play_success_sound(); - resolve(row); - }).catch(() => { - this.play_fail_sound(); - reject(); - }); + me.update_table(data) + .then((row) => { + this.play_success_sound(); + resolve(row); + }) + .catch(() => { + this.play_fail_sound(); + reject(); + }); }); }); } @@ -82,10 +85,11 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } update_table(data) { - return new Promise(resolve => { + return new Promise((resolve) => { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; + frappe.flags.trigger_from_barcode_scanner = true; - const {item_code, barcode, batch_no, serial_no, uom} = data; + const { item_code, barcode, batch_no, serial_no, uom } = data; let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode); @@ -114,16 +118,17 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { frappe.run_serially([ () => this.set_selector_trigger_flag(data), - () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => { - this.show_scan_message(row.idx, row.item_code, qty); - }), + () => + this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => { + this.show_scan_message(row.idx, row.item_code, qty); + }), () => this.set_barcode_uom(row, uom), () => this.set_serial_no(row, serial_no), () => this.set_batch_no(row, batch_no), () => this.set_barcode(row, barcode), () => this.clean_up(), () => this.revert_selector_flag(), - () => resolve(row) + () => resolve(row), ]); }); } @@ -131,7 +136,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { // batch and serial selector is reduandant when all info can be added by scan // this flag on item row is used by transaction.js to avoid triggering selector set_selector_trigger_flag(data) { - const {batch_no, serial_no, has_batch_no, has_serial_no} = data; + const { batch_no, serial_no, has_batch_no, has_serial_no } = data; const require_selecting_batch = has_batch_no && !batch_no; const require_selecting_serial = has_serial_no && !serial_no; @@ -143,19 +148,21 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { revert_selector_flag() { frappe.flags.hide_serial_batch_dialog = false; + frappe.flags.trigger_from_barcode_scanner = false; } set_item(row, item_code, barcode, batch_no, serial_no) { - return new Promise(resolve => { + return new Promise((resolve) => { const increment = async (value = 1) => { - const item_data = {item_code: item_code}; - item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value); + const item_data = { item_code: item_code }; + frappe.flags.trigger_from_barcode_scanner = true; + item_data[this.qty_field] = Number(row[this.qty_field] || 0) + Number(value); await frappe.model.set_value(row.doctype, row.name, item_data); return value; }; if (this.prompt_qty) { - frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => { + frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({ value }) => { increment(value).then((value) => resolve(value)); }); } else if (this.frm.has_items) { @@ -171,14 +178,15 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.dialog = new frappe.ui.Dialog({ title: __("Scan barcode for item {0}", [item_code]), fields: me.get_fields_for_dialog(row, item_code, barcode, batch_no, serial_no), - }) + }); this.dialog.set_primary_action(__("Update"), () => { - const item_data = {item_code: item_code}; + const item_data = { item_code: item_code }; item_data[this.qty_field] = this.dialog.get_value("scanned_qty"); item_data["has_item_scanned"] = 1; - this.remaining_qty = flt(this.dialog.get_value("qty")) - flt(this.dialog.get_value("scanned_qty")); + this.remaining_qty = + flt(this.dialog.get_value("qty")) - flt(this.dialog.get_value("scanned_qty")); frappe.model.set_value(row.doctype, row.name, item_data); frappe.run_serially([ @@ -186,7 +194,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { () => this.set_barcode(row, this.dialog.get_value("barcode")), () => this.set_serial_no(row, this.dialog.get_value("serial_no")), () => this.add_child_for_remaining_qty(row), - () => this.clean_up() + () => this.clean_up(), ]); this.dialog.hide(); @@ -215,9 +223,9 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { if (r.message) { this.update_dialog_values(item_code, r); } - }) + }); } - } + }, }, { fieldtype: "Section Break", @@ -241,8 +249,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { }, { fieldtype: "Section Break", - } - ] + }, + ]; if (batch_no) { fields.push({ @@ -252,7 +260,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { label: __("Batch No"), default: batch_no, read_only: 1, - hidden: 1 + hidden: 1, }); } @@ -274,7 +282,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { label: __("Barcode"), default: barcode, read_only: 1, - hidden: 1 + hidden: 1, }); } @@ -282,18 +290,18 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } update_dialog_values(scanned_item, r) { - const {item_code, barcode, batch_no, serial_no} = r.message; + const { item_code, barcode, batch_no, serial_no } = r.message; this.dialog.set_value("barcode_scanner", ""); - if (item_code === scanned_item && - (this.dialog.get_value("barcode") === barcode || batch_no || serial_no)) { - + if ( + item_code === scanned_item && + (this.dialog.get_value("barcode") === barcode || batch_no || serial_no) + ) { if (batch_no) { this.dialog.set_value("batch_no", batch_no); } if (serial_no) { - this.validate_duplicate_serial_no(serial_no); let serial_nos = this.dialog.get_value("serial_no") + "\n" + serial_no; this.dialog.set_value("serial_no", serial_nos); @@ -305,8 +313,9 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } validate_duplicate_serial_no(serial_no) { - let serial_nos = this.dialog.get_value("serial_no") ? - this.dialog.get_value("serial_no").split("\n") : []; + let serial_nos = this.dialog.get_value("serial_no") + ? this.dialog.get_value("serial_no").split("\n") + : []; if (in_list(serial_nos, serial_no)) { frappe.throw(__("Serial No {0} already scanned", [serial_no])); @@ -314,12 +323,19 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } add_child_for_remaining_qty(prev_row) { - if (this.remaining_qty && this.remaining_qty >0) { + if (this.remaining_qty && this.remaining_qty > 0) { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; let row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name); - let ignore_fields = ["name", "idx", "batch_no", "barcode", - "received_qty", "serial_no", "has_item_scanned"]; + let ignore_fields = [ + "name", + "idx", + "batch_no", + "barcode", + "received_qty", + "serial_no", + "has_item_scanned", + ]; for (let key in prev_row) { if (in_list(ignore_fields, key)) { @@ -375,7 +391,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { if (exist) { this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green"); } else { - this.show_alert(__("Row #{0}: Item added", [idx]), "green") + this.show_alert(__("Row #{0}: Item added", [idx]), "green"); } } @@ -397,17 +413,19 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { const matching_row = (row) => { const item_match = row.item_code == item_code; - const batch_match = (!row[this.batch_no_field] || row[this.batch_no_field] == batch_no); + const batch_match = !row[this.batch_no_field] || row[this.batch_no_field] == batch_no; const uom_match = !uom || row[this.uom_field] == uom; const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]); const item_scanned = row.has_item_scanned; - return item_match - && uom_match - && !item_scanned - && (!is_batch_no_scan || batch_match) - && (!check_max_qty || qty_in_limit) - } + return ( + item_match && + uom_match && + !item_scanned && + (!is_batch_no_scan || batch_match) && + (!check_max_qty || qty_in_limit) + ); + }; return this.items_table.find(matching_row) || this.get_existing_blank_row(); } @@ -428,7 +446,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.scan_barcode_field.set_value(""); refresh_field(this.items_table_name); } - show_alert(msg, indicator, duration=3) { - frappe.show_alert({message: msg, indicator: indicator}, duration); + show_alert(msg, indicator, duration = 3) { + frappe.show_alert({ message: msg, indicator: indicator }, duration); } }; diff --git a/erpnext/public/js/utils/contact_address_quick_entry.js b/erpnext/public/js/utils/contact_address_quick_entry.js index adabf08c203..2f61dee1994 100644 --- a/erpnext/public/js/utils/contact_address_quick_entry.js +++ b/erpnext/public/js/utils/contact_address_quick_entry.js @@ -1,6 +1,8 @@ -frappe.provide('frappe.ui.form'); +frappe.provide("frappe.ui.form"); -frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm extends frappe.ui.form.QuickEntryForm { +frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm extends ( + frappe.ui.form.QuickEntryForm +) { constructor(doctype, after_insert, init_callback, doc, force) { super(doctype, after_insert, init_callback, doc, force); this.skip_redirect_on_error = true; @@ -17,8 +19,8 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm * Therefor, resulting in the fields being "hidden". */ const map_field_names = { - "email_address": "email_id", - "mobile_number": "mobile_no", + email_address: "email_id", + mobile_number: "mobile_no", }; Object.entries(map_field_names).forEach(([fieldname, new_fieldname]) => { @@ -30,71 +32,73 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm } get_variant_fields() { - var variant_fields = [{ - fieldtype: "Section Break", - label: __("Primary Contact Details"), - collapsible: 1 - }, - { - label: __("Email Id"), - fieldname: "email_address", - fieldtype: "Data", - options: "Email", - }, - { - fieldtype: "Column Break" - }, - { - label: __("Mobile Number"), - fieldname: "mobile_number", - fieldtype: "Data" - }, - { - fieldtype: "Section Break", - label: __("Primary Address Details"), - collapsible: 1 - }, - { - label: __("Address Line 1"), - fieldname: "address_line1", - fieldtype: "Data" - }, - { - label: __("Address Line 2"), - fieldname: "address_line2", - fieldtype: "Data" - }, - { - label: __("ZIP Code"), - fieldname: "pincode", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("City"), - fieldname: "city", - fieldtype: "Data" - }, - { - label: __("State"), - fieldname: "state", - fieldtype: "Data" - }, - { - label: __("Country"), - fieldname: "country", - fieldtype: "Link", - options: "Country" - }, - { - label: __("Customer POS Id"), - fieldname: "customer_pos_id", - fieldtype: "Data", - hidden: 1 - }]; + var variant_fields = [ + { + fieldtype: "Section Break", + label: __("Primary Contact Details"), + collapsible: 1, + }, + { + label: __("Email Id"), + fieldname: "email_address", + fieldtype: "Data", + options: "Email", + }, + { + fieldtype: "Column Break", + }, + { + label: __("Mobile Number"), + fieldname: "mobile_number", + fieldtype: "Data", + }, + { + fieldtype: "Section Break", + label: __("Primary Address Details"), + collapsible: 1, + }, + { + label: __("Address Line 1"), + fieldname: "address_line1", + fieldtype: "Data", + }, + { + label: __("Address Line 2"), + fieldname: "address_line2", + fieldtype: "Data", + }, + { + label: __("ZIP Code"), + fieldname: "pincode", + fieldtype: "Data", + }, + { + fieldtype: "Column Break", + }, + { + label: __("City"), + fieldname: "city", + fieldtype: "Data", + }, + { + label: __("State"), + fieldname: "state", + fieldtype: "Data", + }, + { + label: __("Country"), + fieldname: "country", + fieldtype: "Link", + options: "Country", + }, + { + label: __("Customer POS Id"), + fieldname: "customer_pos_id", + fieldtype: "Data", + hidden: 1, + }, + ]; return variant_fields; } -} +}; diff --git a/erpnext/public/js/utils/crm_activities.js b/erpnext/public/js/utils/crm_activities.js index ec79a10dfac..a5a225458c0 100644 --- a/erpnext/public/js/utils/crm_activities.js +++ b/erpnext/public/js/utils/crm_activities.js @@ -6,21 +6,21 @@ erpnext.utils.CRMActivities = class CRMActivities { refresh() { var me = this; $(this.open_activities_wrapper).empty(); - let cur_form_footer = this.form_wrapper.find('.form-footer'); + let cur_form_footer = this.form_wrapper.find(".form-footer"); // all activities - if (!$(this.all_activities_wrapper).find('.form-footer').length) { + if (!$(this.all_activities_wrapper).find(".form-footer").length) { this.all_activities_wrapper.empty(); $(cur_form_footer).appendTo(this.all_activities_wrapper); // remove frappe-control class to avoid absolute position for action-btn - $(this.all_activities_wrapper).removeClass('frappe-control'); + $(this.all_activities_wrapper).removeClass("frappe-control"); // hide new event button - $('.timeline-actions').find('.btn-default').hide(); + $(".timeline-actions").find(".btn-default").hide(); // hide new comment box $(".comment-box").hide(); // show only communications by default - $($('.timeline-content').find('.nav-link')[0]).tab('show'); + $($(".timeline-content").find(".nav-link")[0]).tab("show"); } // open activities @@ -28,66 +28,70 @@ erpnext.utils.CRMActivities = class CRMActivities { method: "erpnext.crm.utils.get_open_activities", args: { ref_doctype: this.frm.doc.doctype, - ref_docname: this.frm.doc.name + ref_docname: this.frm.doc.name, }, callback: (r) => { if (!r.exc) { - var activities_html = frappe.render_template('crm_activities', { + var activities_html = frappe.render_template("crm_activities", { tasks: r.message.tasks, - events: r.message.events + events: r.message.events, }); $(activities_html).appendTo(me.open_activities_wrapper); - $(".open-tasks").find(".completion-checkbox").on("click", function() { - me.update_status(this, "ToDo"); - }); + $(".open-tasks") + .find(".completion-checkbox") + .on("click", function () { + me.update_status(this, "ToDo"); + }); - $(".open-events").find(".completion-checkbox").on("click", function() { - me.update_status(this, "Event"); - }); + $(".open-events") + .find(".completion-checkbox") + .on("click", function () { + me.update_status(this, "Event"); + }); me.create_task(); me.create_event(); } - } + }, }); } - create_task () { + create_task() { let me = this; let _create_task = () => { const args = { doc: me.frm.doc, frm: me.frm, - title: __("New Task") + title: __("New Task"), }; let composer = new frappe.views.InteractionComposer(args); - composer.dialog.get_field('interaction_type').set_value("ToDo"); + composer.dialog.get_field("interaction_type").set_value("ToDo"); // hide column having interaction type field - $(composer.dialog.get_field('interaction_type').wrapper).closest('.form-column').hide(); + $(composer.dialog.get_field("interaction_type").wrapper).closest(".form-column").hide(); // hide summary field - $(composer.dialog.get_field('summary').wrapper).closest('.form-section').hide(); + $(composer.dialog.get_field("summary").wrapper).closest(".form-section").hide(); }; $(".new-task-btn").click(_create_task); } - create_event () { + create_event() { let me = this; let _create_event = () => { const args = { doc: me.frm.doc, frm: me.frm, - title: __("New Event") + title: __("New Event"), }; let composer = new frappe.views.InteractionComposer(args); - composer.dialog.get_field('interaction_type').set_value("Event"); - $(composer.dialog.get_field('interaction_type').wrapper).hide(); + composer.dialog.get_field("interaction_type").set_value("Event"); + $(composer.dialog.get_field("interaction_type").wrapper).hide(); }; $(".new-event-btn").click(_create_event); } - async update_status (input_field, doctype) { + async update_status(input_field, doctype) { let completed = $(input_field).prop("checked") ? 1 : 0; let docname = $(input_field).attr("name"); if (completed) { @@ -104,132 +108,129 @@ erpnext.utils.CRMNotes = class CRMNotes { refresh() { var me = this; - this.notes_wrapper.find('.notes-section').remove(); + this.notes_wrapper.find(".notes-section").remove(); let notes = this.frm.doc.notes || []; - notes.sort( - function(a, b) { - return new Date(b.added_on) - new Date(a.added_on); - } - ); + notes.sort(function (a, b) { + return new Date(b.added_on) - new Date(a.added_on); + }); - let notes_html = frappe.render_template( - 'crm_notes', - { - notes: notes - } - ); + let notes_html = frappe.render_template("crm_notes", { + notes: notes, + }); $(notes_html).appendTo(this.notes_wrapper); this.add_note(); - $(".notes-section").find(".edit-note-btn").on("click", function() { - me.edit_note(this); - }); + $(".notes-section") + .find(".edit-note-btn") + .on("click", function () { + me.edit_note(this); + }); - $(".notes-section").find(".delete-note-btn").on("click", function() { - me.delete_note(this); - }); + $(".notes-section") + .find(".delete-note-btn") + .on("click", function () { + me.delete_note(this); + }); } - - add_note () { + add_note() { let me = this; let _add_note = () => { var d = new frappe.ui.Dialog({ - title: __('Add a Note'), + title: __("Add a Note"), fields: [ { - "label": "Note", - "fieldname": "note", - "fieldtype": "Text Editor", - "reqd": 1, - "enable_mentions": true, - } + label: "Note", + fieldname: "note", + fieldtype: "Text Editor", + reqd: 1, + enable_mentions: true, + }, ], - primary_action: function() { + primary_action: function () { var data = d.get_values(); frappe.call({ method: "add_note", doc: me.frm.doc, args: { - note: data.note + note: data.note, }, freeze: true, - callback: function(r) { + callback: function (r) { if (!r.exc) { me.frm.refresh_field("notes"); me.refresh(); } d.hide(); - } + }, }); }, - primary_action_label: __('Add') + primary_action_label: __("Add"), }); d.show(); }; $(".new-note-btn").click(_add_note); } - edit_note (edit_btn) { + edit_note(edit_btn) { var me = this; - let row = $(edit_btn).closest('.comment-content'); + let row = $(edit_btn).closest(".comment-content"); let row_id = row.attr("name"); let row_content = $(row).find(".content").html(); if (row_content) { var d = new frappe.ui.Dialog({ - title: __('Edit Note'), + title: __("Edit Note"), fields: [ { - "label": "Note", - "fieldname": "note", - "fieldtype": "Text Editor", - "default": row_content - } + label: "Note", + fieldname: "note", + fieldtype: "Text Editor", + default: row_content, + }, ], - primary_action: function() { + primary_action: function () { var data = d.get_values(); frappe.call({ method: "edit_note", doc: me.frm.doc, args: { note: data.note, - row_id: row_id + row_id: row_id, }, freeze: true, - callback: function(r) { + callback: function (r) { if (!r.exc) { me.frm.refresh_field("notes"); me.refresh(); d.hide(); } - - } + }, }); }, - primary_action_label: __('Done') + primary_action_label: __("Done"), }); d.show(); } } - delete_note (delete_btn) { + delete_note(delete_btn) { var me = this; - let row_id = $(delete_btn).closest('.comment-content').attr("name"); + let row_id = $(delete_btn).closest(".comment-content").attr("name"); frappe.call({ method: "delete_note", doc: me.frm.doc, args: { - row_id: row_id + row_id: row_id, }, freeze: true, - callback: function(r) { + callback: function (r) { if (!r.exc) { me.frm.refresh_field("notes"); me.refresh(); } - } + }, }); } }; diff --git a/erpnext/public/js/utils/customer_quick_entry.js b/erpnext/public/js/utils/customer_quick_entry.js index b2532085f65..e9b77a356b2 100644 --- a/erpnext/public/js/utils/customer_quick_entry.js +++ b/erpnext/public/js/utils/customer_quick_entry.js @@ -1,3 +1,3 @@ -frappe.provide('frappe.ui.form'); +frappe.provide("frappe.ui.form"); frappe.ui.form.CustomerQuickEntryForm = frappe.ui.form.ContactAddressQuickEntryForm; diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index bb23f1512b9..36c0f1b51ae 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -1,4 +1,4 @@ -frappe.provide('erpnext.accounts'); +frappe.provide("erpnext.accounts"); erpnext.accounts.dimensions = { setup_dimension_filters(frm, doctype) { @@ -12,30 +12,38 @@ erpnext.accounts.dimensions = { frappe.call({ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", args: { - 'with_cost_center_and_project': true + with_cost_center_and_project: true, }, - callback: function(r) { + callback: function (r) { me.accounting_dimensions = r.message[0]; + // Ignoring "Project" as it is already handled specifically in Sales Order and Delivery Note + me.accounting_dimensions = me.accounting_dimensions.filter((x) => { + return x.document_type != "Project"; + }); me.default_dimensions = r.message[1]; me.setup_filters(frm, doctype); - } + }, }); }, setup_filters(frm, doctype) { + if (doctype == "Payment Entry" && this.accounting_dimensions) { + frm.dimension_filters = this.accounting_dimensions; + } + if (this.accounting_dimensions) { this.accounting_dimensions.forEach((dimension) => { - frappe.model.with_doctype(dimension['document_type'], () => { + frappe.model.with_doctype(dimension["document_type"], () => { let parent_fields = []; frappe.meta.get_docfields(doctype).forEach((df) => { - if (df.fieldtype === 'Link' && df.options === 'Account') { + if (df.fieldtype === "Link" && df.options === "Account") { parent_fields.push(df.fieldname); - } else if (df.fieldtype === 'Table') { - this.setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); + } else if (df.fieldtype === "Table") { + this.setup_child_filters(frm, df.options, df.fieldname, dimension["fieldname"]); } - if (frappe.meta.has_field(doctype, dimension['fieldname'])) { - this.setup_account_filters(frm, dimension['fieldname'], parent_fields); + if (frappe.meta.has_field(doctype, dimension["fieldname"])) { + this.setup_account_filters(frm, dimension["fieldname"], parent_fields); } }); }); @@ -49,12 +57,12 @@ erpnext.accounts.dimensions = { if (frappe.meta.has_field(doctype, dimension)) { frappe.model.with_doctype(doctype, () => { frappe.meta.get_docfields(doctype).forEach((df) => { - if (df.fieldtype === 'Link' && df.options === 'Account') { + if (df.fieldtype === "Link" && df.options === "Account") { fields.push(df.fieldname); } }); - frm.set_query(dimension, parentfield, function(doc, cdt, cdn) { + frm.set_query(dimension, parentfield, function (doc, cdt, cdn) { let row = locals[cdt][cdn]; return erpnext.queries.get_filtered_dimensions(row, fields, dimension, doc.company); }); @@ -63,7 +71,7 @@ erpnext.accounts.dimensions = { }, setup_account_filters(frm, dimension, fields) { - frm.set_query(dimension, function(doc) { + frm.set_query(dimension, function (doc) { return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company); }); }, @@ -72,18 +80,26 @@ erpnext.accounts.dimensions = { if (this.accounting_dimensions) { this.accounting_dimensions.forEach((dimension) => { if (frm.is_new()) { - if (frm.doc.company && Object.keys(this.default_dimensions || {}).length > 0 - && this.default_dimensions[frm.doc.company]) { - - let default_dimension = this.default_dimensions[frm.doc.company][dimension['fieldname']]; + if ( + frm.doc.company && + Object.keys(this.default_dimensions || {}).length > 0 && + this.default_dimensions[frm.doc.company] + ) { + let default_dimension = + this.default_dimensions[frm.doc.company][dimension["fieldname"]]; if (default_dimension) { - if (frappe.meta.has_field(doctype, dimension['fieldname'])) { - frm.set_value(dimension['fieldname'], default_dimension); + if (frappe.meta.has_field(doctype, dimension["fieldname"])) { + frm.set_value(dimension["fieldname"], default_dimension); } - $.each(frm.doc.items || frm.doc.accounts || [], function(i, row) { - frappe.model.set_value(row.doctype, row.name, dimension['fieldname'], default_dimension); + $.each(frm.doc.items || frm.doc.accounts || [], function (i, row) { + frappe.model.set_value( + row.doctype, + row.name, + dimension["fieldname"], + default_dimension + ); }); } } @@ -96,8 +112,8 @@ erpnext.accounts.dimensions = { if (frappe.meta.has_field(frm.doctype, fieldname) && this.accounting_dimensions) { this.accounting_dimensions.forEach((dimension) => { let row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); + frm.script_manager.copy_from_first_row(fieldname, row, [dimension["fieldname"]]); }); } - } + }, }; diff --git a/erpnext/public/js/utils/item_quick_entry.js b/erpnext/public/js/utils/item_quick_entry.js index 7e0198d33b3..42c60870e0d 100644 --- a/erpnext/public/js/utils/item_quick_entry.js +++ b/erpnext/public/js/utils/item_quick_entry.js @@ -1,4 +1,4 @@ -frappe.provide('frappe.ui.form'); +frappe.provide("frappe.ui.form"); frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.form.QuickEntryForm { constructor(doctype, after_insert) { @@ -12,12 +12,14 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f super.render_dialog(); this.init_post_render_dialog_operations(); this.preset_fields_for_template(); - this.dialog.$wrapper.find('.edit-full').text(__('Edit in full page for more options like assets, serial nos, batches etc.')) + this.dialog.$wrapper + .find(".edit-full") + .text(__("Edit in full page for more options like assets, serial nos, batches etc.")); } check_naming_series_based_on() { if (frappe.defaults.get_default("item_naming_by") === "Naming Series") { - this.mandatory = this.mandatory.filter(d => d.fieldname !== "item_code"); + this.mandatory = this.mandatory.filter((d) => d.fieldname !== "item_code"); } } @@ -33,7 +35,7 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f register_primary_action() { var me = this; - this.dialog.set_primary_action(__('Save'), function() { + this.dialog.set_primary_action(__("Save"), function () { if (me.dialog.working) return; var data = me.dialog.get_values(); @@ -52,8 +54,8 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f me.dialog.working = true; var values = me.update_doc(); //patch for manufacturer type variants as extend is overwriting it. - if (variant_values['variant_based_on'] == "Manufacturer") { - values['variant_based_on'] = "Manufacturer"; + if (variant_values["variant_based_on"] == "Manufacturer") { + values["variant_based_on"] = "Manufacturer"; } $.extend(variant_values, values); me.insert(variant_values); @@ -63,13 +65,13 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f insert(variant_values) { let me = this; - return new Promise(resolve => { + return new Promise((resolve) => { frappe.call({ method: "frappe.client.insert", args: { - doc: variant_values + doc: variant_values, }, - callback: function(r) { + callback: function (r) { me.dialog.hide(); // delete the old doc frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name); @@ -84,14 +86,14 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f } } }, - error: function() { + error: function () { me.open_doc(); }, - always: function() { + always: function () { me.dialog.working = false; resolve(me.dialog.doc); }, - freeze: true + freeze: true, }); }); } @@ -101,60 +103,66 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f this.update_doc(); if (this.dialog.fields_dict.create_variant.$input.prop("checked")) { var template = this.dialog.fields_dict.item_template.input.value; - if (template) - frappe.set_route("Form", this.doctype, template); + if (template) frappe.set_route("Form", this.doctype, template); } else { - frappe.set_route('Form', this.doctype, this.doc.name); + frappe.set_route("Form", this.doctype, this.doc.name); } } get_variant_fields() { - var variant_fields = [{ - fieldname: "create_variant", - fieldtype: "Check", - label: __("Create Variant") - }, - { - fieldname: 'item_template', - label: __('Item Template'), - reqd: 0, - fieldtype: 'Link', - options: "Item", - get_query: function() { - return { - filters: { - "has_variants": 1 - } - }; - } - }]; + var variant_fields = [ + { + fieldname: "create_variant", + fieldtype: "Check", + label: __("Create Variant"), + }, + { + fieldname: "item_template", + label: __("Item Template"), + reqd: 0, + fieldtype: "Link", + options: "Item", + get_query: function () { + return { + filters: { + has_variants: 1, + }, + }; + }, + }, + ]; return variant_fields; } get_manufacturing_fields() { - this.manufacturer_fields = [{ - fieldtype: 'Link', - options: 'Manufacturer', - label: 'Manufacturer', - fieldname: "manufacturer", - hidden: 1, - reqd: 0 - }, { - fieldtype: 'Data', - label: 'Manufacturer Part Number', - fieldname: 'manufacturer_part_no', - hidden: 1, - reqd: 0 - }]; + this.manufacturer_fields = [ + { + fieldtype: "Link", + options: "Manufacturer", + label: "Manufacturer", + fieldname: "manufacturer", + hidden: 1, + reqd: 0, + }, + { + fieldtype: "Data", + label: "Manufacturer Part Number", + fieldname: "manufacturer_part_no", + hidden: 1, + reqd: 0, + }, + ]; return this.manufacturer_fields; } get_attributes_fields() { - var attribute_fields = [{ - fieldname: 'attribute_html', - fieldtype: 'HTML' - }] + var attribute_fields = [ + { + fieldname: "attribute_html", + fieldtype: "HTML", + }, + ]; attribute_fields = attribute_fields.concat(this.get_manufacturing_fields()); return attribute_fields; @@ -163,38 +171,37 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f init_for_create_variant_trigger() { var me = this; - this.dialog.fields_dict.create_variant.$input.on("click", function() { + this.dialog.fields_dict.create_variant.$input.on("click", function () { me.preset_fields_for_template(); me.init_post_template_trigger_operations(false, [], true); }); } preset_fields_for_template() { - var for_variant = this.dialog.get_value('create_variant'); + var for_variant = this.dialog.get_value("create_variant"); // setup template field, seen and mandatory if variant let template_field = this.dialog.get_field("item_template"); template_field.df.reqd = for_variant; - template_field.set_value(''); + template_field.set_value(""); template_field.df.hidden = !for_variant; template_field.refresh(); // hide properties for variant - ['item_code', 'item_name', 'item_group', 'stock_uom'].forEach((d) => { + ["item_code", "item_name", "item_group", "stock_uom"].forEach((d) => { let f = this.dialog.get_field(d); f.df.hidden = for_variant; f.refresh(); }); - this.dialog.get_field('attribute_html').toggle(false); + this.dialog.get_field("attribute_html").toggle(false); // non mandatory for variants - ['item_code', 'stock_uom', 'item_group'].forEach((d) => { + ["item_code", "stock_uom", "item_group"].forEach((d) => { let f = this.dialog.get_field(d); f.df.reqd = !for_variant; f.refresh(); }); - } init_for_item_template_trigger() { @@ -208,26 +215,29 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f method: "frappe.client.get", args: { doctype: "Item", - name: template + name: template, }, - callback: function(r) { + callback: function (r) { me.template_doc = r.message; me.is_manufacturer = false; if (me.template_doc.variant_based_on === "Manufacturer") { me.init_post_template_trigger_operations(true, [], true); } else { - - me.init_post_template_trigger_operations(false, me.template_doc.attributes, false); + me.init_post_template_trigger_operations( + false, + me.template_doc.attributes, + false + ); me.render_attributes(me.template_doc.attributes); } - } + }, }); } else { - me.dialog.get_field('attribute_html').toggle(false); + me.dialog.get_field("attribute_html").toggle(false); me.init_post_template_trigger_operations(false, [], true); } - } + }; } init_post_template_trigger_operations(is_manufacturer, attributes, attributes_flag) { @@ -238,15 +248,20 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes").empty(); this.is_manufacturer = is_manufacturer; this.toggle_manufacturer_fields(); - this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes").toggleClass("hide-control", attributes_flag); - this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes-header").toggleClass("hide-control", attributes_flag); + this.dialog.fields_dict.attribute_html.$wrapper + .find(".attributes") + .toggleClass("hide-control", attributes_flag); + this.dialog.fields_dict.attribute_html.$wrapper + .find(".attributes-header") + .toggleClass("hide-control", attributes_flag); } toggle_manufacturer_fields() { var me = this; - $.each(this.manufacturer_fields, function(i, dialog_field) { + $.each(this.manufacturer_fields, function (i, dialog_field) { me.dialog.get_field(dialog_field.fieldname).df.hidden = !me.is_manufacturer; - me.dialog.get_field(dialog_field.fieldname).df.reqd = dialog_field.fieldname == 'manufacturer' ? me.is_manufacturer : false; + me.dialog.get_field(dialog_field.fieldname).df.reqd = + dialog_field.fieldname == "manufacturer" ? me.is_manufacturer : false; me.dialog.get_field(dialog_field.fieldname).refresh(); }); } @@ -259,35 +274,48 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f render_attributes(attributes) { var me = this; - this.dialog.get_field('attribute_html').toggle(true); + this.dialog.get_field("attribute_html").toggle(true); - $.each(attributes, function(index, row) { + $.each(attributes, function (index, row) { var desc = ""; var fieldtype = "Data"; if (row.numeric_values) { fieldtype = "Float"; - desc = "Min Value: " + row.from_range + " , Max Value: " + row.to_range + ", in Increments of: " + row.increment; + desc = + "Min Value: " + + row.from_range + + " , Max Value: " + + row.to_range + + ", in Increments of: " + + row.increment; } me.init_make_control(fieldtype, row); me[row.attribute].set_value(me.attribute_values[row.attribute] || ""); - me[row.attribute].$wrapper.toggleClass("has-error", me.attribute_values[row.attribute] ? false : true); + me[row.attribute].$wrapper.toggleClass( + "has-error", + me.attribute_values[row.attribute] ? false : true + ); // Set Label explicitly as make_control is not displaying label $(me[row.attribute].label_area).text(__(row.attribute)); if (desc) { - $(repl(``, { - "desc": desc - })).insertAfter(me[row.attribute].input_area); + $( + repl(``, { + desc: desc, + }) + ).insertAfter(me[row.attribute].input_area); } if (!row.numeric_values) { me.init_awesomplete_for_attribute(row); } else { - me[row.attribute].$input.on("change", function() { + me[row.attribute].$input.on("change", function () { me.attribute_values[row.attribute] = $(this).val(); - $(this).closest(".frappe-control").toggleClass("has-error", $(this).val() ? false : true); + $(this) + .closest(".frappe-control") + .toggleClass("has-error", $(this).val() ? false : true); }); } }); @@ -296,13 +324,13 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f init_make_control(fieldtype, row) { this[row.attribute] = frappe.ui.form.make_control({ df: { - "fieldtype": fieldtype, - "label": row.attribute, - "fieldname": row.attribute, - "options": row.options || "" + fieldtype: fieldtype, + label: row.attribute, + fieldname: row.attribute, + options: row.options || "", }, parent: $(this.dialog.fields_dict.attribute_html.wrapper).find(".attributes"), - only_input: false + only_input: false, }); this[row.attribute].make_input(); } @@ -317,32 +345,37 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f list: [], }); - this[row.attribute].$input.on('input', function(e) { - frappe.call({ - method: "frappe.client.get_list", - args: { - doctype: "Item Attribute Value", - filters: [ - ["parent", "=", $(e.target).attr("data-fieldname")], - ["attribute_value", "like", e.target.value + "%"] - ], - fields: ["attribute_value"], - parent: "Item Attribute" - }, - callback: function(r) { - if (r.message) { - e.target.awesomplete.list = r.message.map(function(d) { - return d.attribute_value; - }); - } - } + this[row.attribute].$input + .on("input", function (e) { + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Item Attribute Value", + filters: [ + ["parent", "=", $(e.target).attr("data-fieldname")], + ["attribute_value", "like", e.target.value + "%"], + ], + fields: ["attribute_value"], + parent: "Item Attribute", + }, + callback: function (r) { + if (r.message) { + e.target.awesomplete.list = r.message.map(function (d) { + return d.attribute_value; + }); + } + }, + }); + }) + .on("focus", function (e) { + $(e.target).val("").trigger("input"); + }) + .on("awesomplete-close", function (e) { + me.attribute_values[$(e.target).attr("data-fieldname")] = e.target.value; + $(e.target) + .closest(".frappe-control") + .toggleClass("has-error", e.target.value ? false : true); }); - }).on('focus', function(e) { - $(e.target).val('').trigger('input'); - }).on("awesomplete-close", function (e) { - me.attribute_values[$(e.target).attr("data-fieldname")] = e.target.value; - $(e.target).closest(".frappe-control").toggleClass("has-error", e.target.value ? false : true); - }); } get_variant_doc() { @@ -354,31 +387,38 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f frappe.call({ method: "erpnext.controllers.item_variant.create_variant_doc_for_quick_entry", args: { - "template": me.dialog.fields_dict.item_template.$input.val(), - args: attribute + template: me.dialog.fields_dict.item_template.$input.val(), + args: attribute, }, async: false, - callback: function(r) { + callback: function (r) { if (Object.prototype.toString.call(r.message) == "[object Object]") { variant_doc = r.message; } else { - var msgprint_dialog = frappe.msgprint(__("Item Variant {0} already exists with same attributes", [repl('%(item)s', { - item: r.message - })])); + var msgprint_dialog = frappe.msgprint( + __("Item Variant {0} already exists with same attributes", [ + repl( + '%(item)s', + { + item: r.message, + } + ), + ]) + ); - msgprint_dialog.$wrapper.find(".variant-click").on("click", function() { + msgprint_dialog.$wrapper.find(".variant-click").on("click", function () { msgprint_dialog.hide(); me.dialog.hide(); if (frappe._from_link) { frappe._from_link.set_value($(this).attr("data-item-code")); } else { - frappe.set_route('Form', "Item", $(this).attr("data-item-code")); + frappe.set_route("Form", "Item", $(this).attr("data-item-code")); } }); } - } - }) + }, + }); } return variant_doc; } @@ -388,17 +428,17 @@ frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.f var attribute = {}; var mandatory = []; - $.each(this.attributes, function(index, attr) { + $.each(this.attributes, function (index, attr) { var value = me.attribute_values[attr.attribute] || ""; if (value) { attribute[attr.attribute] = attr.numeric_values ? flt(value) : value; } else { mandatory.push(attr.attribute); } - }) + }); if (this.is_manufacturer) { - $.each(this.manufacturer_fields, function(index, field) { + $.each(this.manufacturer_fields, function (index, field) { attribute[field.fieldname] = me.dialog.fields_dict[field.fieldname].input.value; }); } diff --git a/erpnext/public/js/utils/item_selector.js b/erpnext/public/js/utils/item_selector.js index 9fc264086a3..5d2e915022e 100644 --- a/erpnext/public/js/utils/item_selector.js +++ b/erpnext/public/js/utils/item_selector.js @@ -3,7 +3,7 @@ erpnext.ItemSelector = class ItemSelector { $.extend(this, opts); if (!this.item_field) { - this.item_field = 'item_code'; + this.item_field = "item_code"; } if (!this.item_query) { @@ -16,39 +16,43 @@ erpnext.ItemSelector = class ItemSelector { setup() { var me = this; - if(!this.grid.add_items_button) { - this.grid.add_items_button = this.grid.add_custom_button(__('Add Items'), function() { - if(!me.dialog) { + if (!this.grid.add_items_button) { + this.grid.add_items_button = this.grid.add_custom_button(__("Add Items"), function () { + if (!me.dialog) { me.make_dialog(); } me.dialog.show(); me.render_items(); - setTimeout(function() { me.dialog.input.focus(); }, 1000); + setTimeout(function () { + me.dialog.input.focus(); + }, 1000); }); } } make_dialog() { this.dialog = new frappe.ui.Dialog({ - title: __('Add Items') + title: __("Add Items"), }); var body = $(this.dialog.body); - body.html('

              \ -
              '); + body.html( + '

              \ +
              ' + ); - this.dialog.input = body.find('.form-control'); - this.dialog.results = body.find('.results'); + this.dialog.input = body.find(".form-control"); + this.dialog.results = body.find(".results"); var me = this; - this.dialog.results.on('click', '.image-view-item', function() { - me.add_item($(this).attr('data-name')); + this.dialog.results.on("click", ".image-view-item", function () { + me.add_item($(this).attr("data-name")); }); - this.dialog.input.on('keyup', function() { - if(me.timeout_id) { + this.dialog.input.on("keyup", function () { + if (me.timeout_id) { clearTimeout(me.timeout_id); } - me.timeout_id = setTimeout(function() { + me.timeout_id = setTimeout(function () { me.render_items(); me.timeout_id = undefined; }, 500); @@ -61,33 +65,34 @@ erpnext.ItemSelector = class ItemSelector { // find row with item if exists $.each(this.frm.doc.items || [], (i, d) => { - if(d[this.item_field]===item_code) { - frappe.model.set_value(d.doctype, d.name, 'qty', d.qty + 1); - frappe.show_alert({message: __("Added {0} ({1})", [item_code, d.qty]), indicator: 'green'}); + if (d[this.item_field] === item_code) { + frappe.model.set_value(d.doctype, d.name, "qty", d.qty + 1); + frappe.show_alert({ message: __("Added {0} ({1})", [item_code, d.qty]), indicator: "green" }); added = true; return false; } }); - if(!added) { + if (!added) { var d = null; frappe.run_serially([ - () => { d = this.grid.add_new_row(); }, + () => { + d = this.grid.add_new_row(); + }, () => frappe.model.set_value(d.doctype, d.name, this.item_field, item_code), () => frappe.timeout(0.1), () => { - frappe.model.set_value(d.doctype, d.name, 'qty', 1); - frappe.show_alert({message: __("Added {0} ({1})", [item_code, 1]), indicator: 'green'}); - } + frappe.model.set_value(d.doctype, d.name, "qty", 1); + frappe.show_alert({ message: __("Added {0} ({1})", [item_code, 1]), indicator: "green" }); + }, ]); } - } render_items() { let args = { query: this.item_query, - filters: {} + filters: {}, }; args.txt = this.dialog.input.val(); args.as_dict = 1; @@ -97,14 +102,14 @@ erpnext.ItemSelector = class ItemSelector { } var me = this; - frappe.link_search("Item", args, function(r) { - $.each(r.values, function(i, d) { - if(!d.image) { + frappe.link_search("Item", args, function (r) { + $.each(r.values, function (i, d) { + if (!d.image) { d.abbr = frappe.get_abbr(d.item_name); d.color = frappe.get_palette(d.item_name); } }); - me.dialog.results.html(frappe.render_template('item_selector', {'data':r.values})); + me.dialog.results.html(frappe.render_template("item_selector", { data: r.values })); }); } }; diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index cba615c0d22..801376b2ed7 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -3,18 +3,19 @@ frappe.provide("erpnext.utils"); -const SALES_DOCTYPES = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']; -const PURCHASE_DOCTYPES = ['Supplier Quotation','Purchase Order', 'Purchase Receipt', 'Purchase Invoice']; +const SALES_DOCTYPES = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]; +const PURCHASE_DOCTYPES = ["Supplier Quotation", "Purchase Order", "Purchase Receipt", "Purchase Invoice"]; -erpnext.utils.get_party_details = function(frm, method, args, callback) { +erpnext.utils.get_party_details = function (frm, method, args, callback) { if (!method) { method = "erpnext.accounts.party.get_party_details"; } if (!args) { - if ((frm.doctype != "Purchase Order" && frm.doc.customer) - || (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) { - + if ( + (frm.doctype != "Purchase Order" && frm.doc.customer) || + (frm.doc.party_name && in_list(["Quotation", "Opportunity"], frm.doc.doctype)) + ) { let party_type = "Customer"; if (frm.doc.quotation_to && in_list(["Lead", "Prospect"], frm.doc.quotation_to)) { party_type = frm.doc.quotation_to; @@ -23,14 +24,14 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { args = { party: frm.doc.customer || frm.doc.party_name, party_type: party_type, - price_list: frm.doc.selling_price_list + price_list: frm.doc.selling_price_list, }; } else if (frm.doc.supplier) { args = { party: frm.doc.supplier, party_type: "Supplier", bill_date: frm.doc.bill_date, - price_list: frm.doc.buying_price_list + price_list: frm.doc.buying_price_list, }; } @@ -38,14 +39,14 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if (in_list(SALES_DOCTYPES, frm.doc.doctype)) { args = { party: frm.doc.customer || frm.doc.party_name, - party_type: 'Customer' + party_type: "Customer", }; } if (in_list(PURCHASE_DOCTYPES, frm.doc.doctype)) { args = { party: frm.doc.supplier, - party_type: 'Supplier' + party_type: "Supplier", }; } } @@ -72,13 +73,26 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { } } - if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", - args.posting_date, args.party_type=="Customer" ? "customer": "supplier")) return; + if ( + !erpnext.utils.validate_mandatory( + frm, + "Posting / Transaction Date", + args.posting_date, + args.party_type == "Customer" ? "customer" : "supplier" + ) + ) + return; } - if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, args.party_type=="Customer" ? "customer": "supplier")) { + if ( + !erpnext.utils.validate_mandatory( + frm, + "Company", + frm.doc.company, + args.party_type == "Customer" ? "customer" : "supplier" + ) + ) { return; } @@ -88,7 +102,7 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { frappe.call({ method: method, args: args, - callback: function(r) { + callback: function (r) { if (r.message) { frm.supplier_tds = r.message.supplier_tds; frm.updating_party_details = true; @@ -99,28 +113,28 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if (callback) callback(); frm.refresh(); erpnext.utils.add_item(frm); - } + }, ]); } - } + }, }); -} +}; -erpnext.utils.add_item = function(frm) { +erpnext.utils.add_item = function (frm) { if (frm.is_new()) { var prev_route = frappe.get_prev_route(); - if (prev_route[1]==='Item' && !(frm.doc.items && frm.doc.items.length)) { + if (prev_route[1] === "Item" && !(frm.doc.items && frm.doc.items.length)) { // add row - var item = frm.add_child('items'); - frm.refresh_field('items'); + var item = frm.add_child("items"); + frm.refresh_field("items"); // set item - frappe.model.set_value(item.doctype, item.name, 'item_code', prev_route[2]); + frappe.model.set_value(item.doctype, item.name, "item_code", prev_route[2]); } } -} +}; -erpnext.utils.get_address_display = function(frm, address_field, display_field, is_your_company_address) { +erpnext.utils.get_address_display = function (frm, address_field, display_field, is_your_company_address) { if (frm.updating_party_details) return; if (!address_field) { @@ -135,29 +149,46 @@ erpnext.utils.get_address_display = function(frm, address_field, display_field, if (frm.doc[address_field]) { frappe.call({ method: "frappe.contacts.doctype.address.address.get_address_display", - args: {"address_dict": frm.doc[address_field] }, - callback: function(r) { + args: { address_dict: frm.doc[address_field] }, + callback: function (r) { if (r.message) { - frm.set_value(display_field, r.message) + frm.set_value(display_field, r.message); } - } - }) + }, + }); } else { - frm.set_value(display_field, ''); + frm.set_value(display_field, ""); } }; -erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billing_address_field, shipping_address_field) { +erpnext.utils.set_taxes_from_address = function ( + frm, + triggered_from_field, + billing_address_field, + shipping_address_field +) { if (frm.updating_party_details) return; if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier", - frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) { + if ( + !erpnext.utils.validate_mandatory( + frm, + "Lead / Customer / Supplier", + frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, + triggered_from_field + ) + ) { return; } - if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", - frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) { + if ( + !erpnext.utils.validate_mandatory( + frm, + "Posting / Transaction Date", + frm.doc.posting_date || frm.doc.transaction_date, + triggered_from_field + ) + ) { return; } } else { @@ -167,35 +198,47 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi frappe.call({ method: "erpnext.accounts.party.get_address_tax_category", args: { - "tax_category": frm.doc.tax_category, - "billing_address": frm.doc[billing_address_field], - "shipping_address": frm.doc[shipping_address_field] + tax_category: frm.doc.tax_category, + billing_address: frm.doc[billing_address_field], + shipping_address: frm.doc[shipping_address_field], }, - callback: function(r) { - if (!r.exc){ + callback: function (r) { + if (!r.exc) { if (frm.doc.tax_category != r.message) { frm.set_value("tax_category", r.message); } else { erpnext.utils.set_taxes(frm, triggered_from_field); } } - } + }, }); }; -erpnext.utils.set_taxes = function(frm, triggered_from_field) { +erpnext.utils.set_taxes = function (frm, triggered_from_field) { if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, triggered_from_field)) { return; } - if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier", - frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) { + if ( + !erpnext.utils.validate_mandatory( + frm, + "Lead / Customer / Supplier", + frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, + triggered_from_field + ) + ) { return; } - if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", - frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) { + if ( + !erpnext.utils.validate_mandatory( + frm, + "Posting / Transaction Date", + frm.doc.posting_date || frm.doc.transaction_date, + triggered_from_field + ) + ) { return; } } else { @@ -204,15 +247,15 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { var party_type, party; if (frm.doc.lead) { - party_type = 'Lead'; + party_type = "Lead"; party = frm.doc.lead; } else if (frm.doc.customer) { - party_type = 'Customer'; + party_type = "Customer"; party = frm.doc.customer; } else if (frm.doc.supplier) { - party_type = 'Supplier'; + party_type = "Supplier"; party = frm.doc.supplier; - } else if (frm.doc.quotation_to){ + } else if (frm.doc.quotation_to) { party_type = frm.doc.quotation_to; party = frm.doc.party_name; } @@ -224,21 +267,22 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { frappe.call({ method: "erpnext.accounts.party.set_taxes", args: { - "party": party, - "party_type": party_type, - "posting_date": frm.doc.posting_date || frm.doc.transaction_date, - "company": frm.doc.company, - "customer_group": frm.doc.customer_group, - "supplier_group": frm.doc.supplier_group, - "tax_category": frm.doc.tax_category, - "billing_address": ((frm.doc.customer || frm.doc.lead) ? (frm.doc.customer_address) : (frm.doc.supplier_address)), - "shipping_address": frm.doc.shipping_address_name + party: party, + party_type: party_type, + posting_date: frm.doc.posting_date || frm.doc.transaction_date, + company: frm.doc.company, + customer_group: frm.doc.customer_group, + supplier_group: frm.doc.supplier_group, + tax_category: frm.doc.tax_category, + billing_address: + frm.doc.customer || frm.doc.lead ? frm.doc.customer_address : frm.doc.supplier_address, + shipping_address: frm.doc.shipping_address_name, }, - callback: function(r) { - if (r.message){ - frm.set_value("taxes_and_charges", r.message) + callback: function (r) { + if (r.message) { + frm.set_value("taxes_and_charges", r.message); } - } + }, }); }; @@ -266,20 +310,23 @@ erpnext.utils.get_contact_details = function (frm) { } }; -erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { +erpnext.utils.validate_mandatory = function (frm, label, value, trigger_on) { if (!value) { frm.doc[trigger_on] = ""; refresh_field(trigger_on); - frappe.throw({message:__("Please enter {0} first", [label]), title:__("Mandatory")}); + frappe.throw({ message: __("Please enter {0} first", [label]), title: __("Mandatory") }); return false; } return true; -} +}; -erpnext.utils.get_shipping_address = function(frm, callback) { +erpnext.utils.get_shipping_address = function (frm, callback) { if (frm.doc.company) { - if ((frm.doc.inter_company_order_reference || frm.doc.internal_invoice_reference || - frm.doc.internal_order_reference)) { + if ( + frm.doc.inter_company_order_reference || + frm.doc.internal_invoice_reference || + frm.doc.internal_order_reference + ) { if (callback) { return callback(); } @@ -288,20 +335,20 @@ erpnext.utils.get_shipping_address = function(frm, callback) { method: "erpnext.accounts.custom.address.get_shipping_address", args: { company: frm.doc.company, - address: frm.doc.shipping_address + address: frm.doc.shipping_address, }, - callback: function(r) { + callback: function (r) { if (r.message) { - frm.set_value("shipping_address", r.message[0]) //Address title or name - frm.set_value("shipping_address_display", r.message[1]) //Address to be displayed on the page + frm.set_value("shipping_address", r.message[0]); //Address title or name + frm.set_value("shipping_address_display", r.message[1]); //Address to be displayed on the page } - if (callback){ + if (callback) { return callback(); } - } + }, }); } else { frappe.msgprint(__("Select company first")); } -} +}; diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js deleted file mode 100644 index 5514963c966..00000000000 --- a/erpnext/public/js/utils/sales_common.js +++ /dev/null @@ -1,431 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt - -frappe.provide("erpnext.selling"); - -erpnext.sales_common = { - setup_selling_controller:function() { - erpnext.selling.SellingController = class SellingController extends erpnext.TransactionController { - setup() { - super.setup(); - this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_sales"); - this.frm.email_field = "contact_email"; - } - - onload() { - super.onload(); - this.setup_queries(); - this.frm.set_query('shipping_rule', function() { - return { - filters: { - "shipping_rule_type": "Selling" - } - }; - }); - } - - setup_queries() { - var me = this; - - $.each([["customer", "customer"], - ["lead", "lead"]], - function(i, opts) { - if(me.frm.fields_dict[opts[0]]) - me.frm.set_query(opts[0], erpnext.queries[opts[1]]); - }); - - me.frm.set_query('contact_person', erpnext.queries.contact_query); - me.frm.set_query('customer_address', erpnext.queries.address_query); - me.frm.set_query('shipping_address_name', erpnext.queries.address_query); - me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query); - - erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); - - if(this.frm.fields_dict.selling_price_list) { - this.frm.set_query("selling_price_list", function() { - return { filters: { selling: 1 } }; - }); - } - - if(this.frm.fields_dict.tc_name) { - this.frm.set_query("tc_name", function() { - return { filters: { selling: 1 } }; - }); - } - - if(!this.frm.fields_dict["items"]) { - return; - } - - if(this.frm.fields_dict["items"].grid.get_field('item_code')) { - this.frm.set_query("item_code", "items", function() { - return { - query: "erpnext.controllers.queries.item_query", - filters: {'is_sales_item': 1, 'customer': me.frm.doc.customer, 'has_variants': 0} - } - }); - } - - if(this.frm.fields_dict["packed_items"] && - this.frm.fields_dict["packed_items"].grid.get_field('batch_no')) { - this.frm.set_query("batch_no", "packed_items", function(doc, cdt, cdn) { - return me.set_query_for_batch(doc, cdt, cdn) - }); - } - - if(this.frm.fields_dict["items"].grid.get_field('item_code')) { - this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) { - return me.set_query_for_item_tax_template(doc, cdt, cdn) - }); - } - - } - - refresh() { - super.refresh(); - - frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} - - this.frm.toggle_display("customer_name", - (this.frm.doc.customer_name && this.frm.doc.customer_name!==this.frm.doc.customer)); - - this.toggle_editable_price_list_rate(); - } - - customer() { - var me = this; - erpnext.utils.get_party_details(this.frm, null, null, function() { - me.apply_price_list(); - }); - } - - customer_address() { - erpnext.utils.get_address_display(this.frm, "customer_address"); - erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name"); - } - - shipping_address_name() { - erpnext.utils.get_address_display(this.frm, "shipping_address_name", "shipping_address"); - erpnext.utils.set_taxes_from_address(this.frm, "shipping_address_name", "customer_address", "shipping_address_name"); - } - - dispatch_address_name() { - erpnext.utils.get_address_display(this.frm, "dispatch_address_name", "dispatch_address"); - } - - sales_partner() { - this.apply_pricing_rule(); - } - - campaign() { - this.apply_pricing_rule(); - } - - selling_price_list() { - this.apply_price_list(); - this.set_dynamic_labels(); - } - - discount_percentage(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); - item.discount_amount = 0.0; - this.apply_discount_on_item(doc, cdt, cdn, 'discount_percentage'); - } - - discount_amount(doc, cdt, cdn) { - - if(doc.name === cdn) { - return; - } - - var item = frappe.get_doc(cdt, cdn); - item.discount_percentage = 0.0; - this.apply_discount_on_item(doc, cdt, cdn, 'discount_amount'); - } - - commission_rate() { - this.calculate_commission(); - } - - total_commission() { - frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]); - - const { amount_eligible_for_commission } = this.frm.doc; - if(!amount_eligible_for_commission) return; - - this.frm.set_value( - "commission_rate", flt( - this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission - ) - ); - } - - allocated_percentage(doc, cdt, cdn) { - var sales_person = frappe.get_doc(cdt, cdn); - if(sales_person.allocated_percentage) { - - sales_person.allocated_percentage = flt(sales_person.allocated_percentage, - precision("allocated_percentage", sales_person)); - - sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission * - sales_person.allocated_percentage / 100.0, - precision("allocated_amount", sales_person)); - refresh_field(["allocated_amount"], sales_person); - - this.calculate_incentive(sales_person); - refresh_field(["allocated_percentage", "allocated_amount", "commission_rate","incentives"], sales_person.name, - sales_person.parentfield); - } - } - - sales_person(doc, cdt, cdn) { - var row = frappe.get_doc(cdt, cdn); - this.calculate_incentive(row); - refresh_field("incentives",row.name,row.parentfield); - } - - toggle_editable_price_list_rate() { - var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name); - var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate")); - - if(df && editable_price_list_rate) { - const parent_field = frappe.meta.get_parentfield(this.frm.doc.doctype, this.frm.doc.doctype + " Item"); - if (!this.frm.fields_dict[parent_field]) return; - - this.frm.fields_dict[parent_field].grid.update_docfield_property( - 'price_list_rate', 'read_only', 0 - ); - } - } - - calculate_commission() { - if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return; - - if(this.frm.doc.commission_rate > 100) { - this.frm.set_value("commission_rate", 100); - frappe.throw(`${__(frappe.meta.get_label( - this.frm.doc.doctype, "commission_rate", this.frm.doc.name - ))} ${__("cannot be greater than 100")}`); - } - - this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce( - (sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0 - ) - - this.frm.doc.total_commission = flt( - this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0, - precision("total_commission") - ); - - refresh_field(["amount_eligible_for_commission", "total_commission"]); - } - - calculate_contribution() { - var me = this; - $.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) { - frappe.model.round_floats_in(sales_person); - if (!sales_person.allocated_percentage) return; - - sales_person.allocated_amount = flt( - me.frm.doc.amount_eligible_for_commission - * sales_person.allocated_percentage - / 100.0, - precision("allocated_amount", sales_person) - ); - }); - } - - calculate_incentive(row) { - if(row.allocated_amount) - { - row.incentives = flt( - row.allocated_amount * row.commission_rate / 100.0, - precision("incentives", row)); - } - } - - set_dynamic_labels() { - super.set_dynamic_labels(); - this.set_product_bundle_help(this.frm.doc); - } - - set_product_bundle_help(doc) { - if(!this.frm.fields_dict.packing_list) return; - if ((doc.packed_items || []).length) { - $(this.frm.fields_dict.packing_list.row.wrapper).toggle(true); - - if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) { - var help_msg = "
              " + - __("For 'Product Bundle' items, Warehouse, Serial No and Batch No will be considered from the 'Packing List' table. If Warehouse and Batch No are same for all packing items for any 'Product Bundle' item, those values can be entered in the main Item table, values will be copied to 'Packing List' table.")+ - "
              "; - frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = help_msg; - } - } else { - $(this.frm.fields_dict.packing_list.row.wrapper).toggle(false); - if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) { - frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = ''; - } - } - refresh_field('product_bundle_help'); - } - - company_address() { - var me = this; - if(this.frm.doc.company_address) { - frappe.call({ - method: "frappe.contacts.doctype.address.address.get_address_display", - args: {"address_dict": this.frm.doc.company_address }, - callback: function(r) { - if(r.message) { - me.frm.set_value("company_address_display", r.message) - } - } - }) - } else { - this.frm.set_value("company_address_display", ""); - } - } - - conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate) { - super.conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate); - } - - qty(doc, cdt, cdn) { - super.qty(doc, cdt, cdn); - } - - pick_serial_and_batch(doc, cdt, cdn) { - let item = locals[cdt][cdn]; - let me = this; - let path = "assets/erpnext/js/utils/serial_no_batch_selector.js"; - - frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) - .then((r) => { - if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { - item.has_serial_no = r.message.has_serial_no; - item.has_batch_no = r.message.has_batch_no; - item.type_of_transaction = item.qty > 0 ? "Outward":"Inward"; - - item.title = item.has_serial_no ? - __("Select Serial No") : __("Select Batch No"); - - if (item.has_serial_no && item.has_batch_no) { - item.title = __("Select Serial and Batch"); - } - - frappe.require(path, function() { - new erpnext.SerialBatchPackageSelector( - me.frm, item, (r) => { - if (r) { - frappe.model.set_value(item.doctype, item.name, { - "serial_and_batch_bundle": r.name, - "qty": Math.abs(r.total_qty) - }); - } - } - ); - }); - } - }); - } - - update_auto_repeat_reference(doc) { - if (doc.auto_repeat) { - frappe.call({ - method:"frappe.automation.doctype.auto_repeat.auto_repeat.update_reference", - args:{ - docname: doc.auto_repeat, - reference:doc.name - }, - callback: function(r){ - if (r.message=="success") { - frappe.show_alert({message:__("Auto repeat document updated"), indicator:'green'}); - } else { - frappe.show_alert({message:__("An error occurred during the update process"), indicator:'red'}); - } - } - }) - } - } - - project() { - let me = this; - if(in_list(["Delivery Note", "Sales Invoice", "Sales Order"], this.frm.doc.doctype)) { - if(this.frm.doc.project) { - frappe.call({ - method:'erpnext.projects.doctype.project.project.get_cost_center_name' , - args: {project: this.frm.doc.project}, - callback: function(r, rt) { - if(!r.exc) { - $.each(me.frm.doc["items"] || [], function(i, row) { - if(r.message) { - frappe.model.set_value(row.doctype, row.name, "cost_center", r.message); - frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message])); - } - }) - } - } - }) - } - } - } - - coupon_code() { - this.frm.set_value("discount_amount", 0); - this.frm.set_value("additional_discount_percentage", 0); - } - }; - } -} - -erpnext.pre_sales = { - set_as_lost: function(doctype) { - frappe.ui.form.on(doctype, { - set_as_lost_dialog: function(frm) { - var dialog = new frappe.ui.Dialog({ - title: __("Set as Lost"), - fields: [ - { - "fieldtype": "Table MultiSelect", - "label": __("Lost Reasons"), - "fieldname": "lost_reason", - "options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail', - "reqd": 1 - }, - { - "fieldtype": "Table MultiSelect", - "label": __("Competitors"), - "fieldname": "competitors", - "options": "Competitor Detail" - }, - { - "fieldtype": "Small Text", - "label": __("Detailed Reason"), - "fieldname": "detailed_reason" - }, - ], - primary_action: function() { - let values = dialog.get_values(); - - frm.call({ - doc: frm.doc, - method: 'declare_enquiry_lost', - args: { - 'lost_reasons_list': values.lost_reason, - 'competitors': values.competitors ? values.competitors : [], - 'detailed_reason': values.detailed_reason - }, - callback: function(r) { - dialog.hide(); - frm.reload_doc(); - }, - }); - }, - primary_action_label: __('Declare Lost') - }); - - dialog.show(); - } - }); - } -} \ No newline at end of file diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 22120988ad0..6fd7f7f4588 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -1,15 +1,15 @@ - erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { constructor(opts, show_dialog) { $.extend(this, opts); this.show_dialog = show_dialog; // frm, item, warehouse_details, has_batch, oldest let d = this.item; - this.has_batch = 0; this.has_serial_no = 0; + this.has_batch = 0; + this.has_serial_no = 0; if (d && d.has_batch_no && (!d.batch_no || this.show_dialog)) this.has_batch = 1; // !(this.show_dialog == false) ensures that show_dialog is implictly true, even when undefined - if(d && d.has_serial_no && !(this.show_dialog == false)) this.has_serial_no = 1; + if (d && d.has_serial_no && !(this.show_dialog == false)) this.has_serial_no = 1; this.setup(); } @@ -28,70 +28,70 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { let title = ""; let fields = [ { - fieldname: 'item_code', + fieldname: "item_code", read_only: 1, - fieldtype:'Link', - options: 'Item', - label: __('Item Code'), - default: me.item_code + fieldtype: "Link", + options: "Item", + label: __("Item Code"), + default: me.item_code, }, { - fieldname: 'warehouse', - fieldtype:'Link', - options: 'Warehouse', + fieldname: "warehouse", + fieldtype: "Link", + options: "Warehouse", reqd: me.has_batch && !me.has_serial_no ? 0 : 1, label: __(me.warehouse_details.type), - default: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '', - onchange: function(e) { + default: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : "", + onchange: function (e) { me.warehouse_details.name = this.get_value(); - if(me.has_batch && !me.has_serial_no) { + if (me.has_batch && !me.has_serial_no) { fields = fields.concat(me.get_batch_fields()); } else { fields = fields.concat(me.get_serial_no_fields()); } var batches = this.layout.fields_dict.batches; - if(batches) { + if (batches) { batches.grid.df.data = []; batches.grid.refresh(); batches.grid.add_new_row(null, null, null); } }, - get_query: function() { + get_query: function () { return { query: "erpnext.controllers.queries.warehouse_query", filters: [ ["Bin", "item_code", "=", me.item_code], ["Warehouse", "is_group", "=", 0], - ["Warehouse", "company", "=", me.frm.doc.company] - ] - } - } + ["Warehouse", "company", "=", me.frm.doc.company], + ], + }; + }, }, - {fieldtype:'Column Break'}, + { fieldtype: "Column Break" }, { - fieldname: 'qty', - fieldtype:'Float', + fieldname: "qty", + fieldtype: "Float", read_only: me.has_batch && !me.has_serial_no, - label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'), + label: __(me.has_batch && !me.has_serial_no ? "Selected Qty" : "Qty"), default: flt(me.item.stock_qty) || flt(me.item.transfer_qty), }, ...get_pending_qty_fields(me), { - fieldname: 'uom', + fieldname: "uom", read_only: 1, - fieldtype: 'Link', - options: 'UOM', - label: __('UOM'), - default: me.item.uom + fieldtype: "Link", + options: "UOM", + label: __("UOM"), + default: me.item.uom, }, { - fieldname: 'auto_fetch_button', - fieldtype:'Button', + fieldname: "auto_fetch_button", + fieldtype: "Button", hidden: me.has_batch && !me.has_serial_no, - label: __('Auto Fetch'), - description: __('Fetch Serial Numbers based on FIFO'), + label: __("Auto Fetch"), + description: __("Fetch Serial Numbers based on FIFO"), click: () => { let qty = this.dialog.fields_dict.qty.get_value(); let already_selected_serial_nos = get_selected_serial_nos(me); @@ -100,11 +100,12 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { args: { qty: qty, item_code: me.item_code, - warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '', + warehouse: + typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : "", batch_nos: me.item.batch_no || null, posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date, - exclude_sr_nos: already_selected_serial_nos - } + exclude_sr_nos: already_selected_serial_nos, + }, }); numbers.then((data) => { @@ -113,18 +114,23 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { if (!records_length) { const warehouse = me.dialog.fields_dict.warehouse.get_value().bold(); frappe.msgprint( - __('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [me.item.item_code.bold(), warehouse]) + __( + "Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.", + [me.item.item_code.bold(), warehouse] + ) ); } if (records_length < qty) { - frappe.msgprint(__('Fetched only {0} available serial numbers.', [records_length])); + frappe.msgprint( + __("Fetched only {0} available serial numbers.", [records_length]) + ); } let serial_no_list_field = this.dialog.fields_dict.serial_no; - numbers = auto_fetched_serial_numbers.join('\n'); + numbers = auto_fetched_serial_numbers.join("\n"); serial_no_list_field.set_value(numbers); }); - } - } + }, + }, ]; if (this.has_batch && !this.has_serial_no) { @@ -139,12 +145,12 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { this.dialog = new frappe.ui.Dialog({ title: title, - fields: fields + fields: fields, }); - this.dialog.set_primary_action(__('Insert'), function() { + this.dialog.set_primary_action(__("Insert"), function () { me.values = me.dialog.get_values(); - if(me.validate()) { + if (me.validate()) { frappe.run_serially([ () => me.update_batch_items(), () => me.update_serial_no_item(), @@ -156,25 +162,25 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { return me.callback(me.item); } }, - () => me.dialog.hide() - ]) + () => me.dialog.hide(), + ]); } }); - if(this.show_dialog) { + if (this.show_dialog) { let d = this.item; if (this.item.serial_no) { this.dialog.fields_dict.serial_no.set_value(this.item.serial_no); } if (this.has_batch && !this.has_serial_no && d.batch_no) { - this.frm.doc.items.forEach(data => { - if(data.item_code == d.item_code) { + this.frm.doc.items.forEach((data) => { + if (data.item_code == d.item_code) { this.dialog.fields_dict.batches.df.data.push({ - 'batch_no': data.batch_no, - 'actual_qty': data.actual_qty, - 'selected_qty': data.qty, - 'available_qty': data.actual_batch_qty + batch_no: data.batch_no, + actual_qty: data.actual_qty, + selected_qty: data.qty, + available_qty: data.actual_batch_qty, }); } }); @@ -191,33 +197,32 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { } on_close_dialog() { - this.dialog.get_close_btn().on('click', () => { + this.dialog.get_close_btn().on("click", () => { this.on_close && this.on_close(this.item); }); } validate() { let values = this.values; - if(!values.warehouse) { + if (!values.warehouse) { frappe.throw(__("Please select a warehouse")); return false; } - if(this.has_batch && !this.has_serial_no) { - if(values.batches.length === 0 || !values.batches) { + if (this.has_batch && !this.has_serial_no) { + if (values.batches.length === 0 || !values.batches) { frappe.throw(__("Please select batches for batched item {0}", [values.item_code])); } values.batches.map((batch, i) => { - if(!batch.selected_qty || batch.selected_qty === 0 ) { + if (!batch.selected_qty || batch.selected_qty === 0) { if (!this.show_dialog) { - frappe.throw(__("Please select quantity on row {0}", [i+1])); + frappe.throw(__("Please select quantity on row {0}", [i + 1])); } } }); return true; - } else { - let serial_nos = values.serial_no || ''; - if (!serial_nos || !serial_nos.replace(/\s/g, '').length) { + let serial_nos = values.serial_no || ""; + if (!serial_nos || !serial_nos.replace(/\s/g, "").length) { frappe.throw(__("Please enter serial numbers for serialized item {0}", [values.item_code])); } return true; @@ -226,87 +231,92 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { update_batch_items() { // clones an items if muliple batches are selected. - if(this.has_batch && !this.has_serial_no) { + if (this.has_batch && !this.has_serial_no) { this.values.batches.map((batch, i) => { let batch_no = batch.batch_no; - let row = ''; + let row = ""; if (i !== 0 && !this.batch_exists(batch_no)) { row = this.frm.add_child("items", { ...this.item }); } else { - row = this.frm.doc.items.find(i => i.batch_no === batch_no); + row = this.frm.doc.items.find((i) => i.batch_no === batch_no); } if (!row) { row = this.item; } // this ensures that qty & batch no is set - this.map_row_values(row, batch, 'batch_no', - 'selected_qty', this.values.warehouse); + this.map_row_values(row, batch, "batch_no", "selected_qty", this.values.warehouse); }); } } update_serial_no_item() { // just updates serial no for the item - if(this.has_serial_no && !this.has_batch) { - this.map_row_values(this.item, this.values, 'serial_no', 'qty'); + if (this.has_serial_no && !this.has_batch) { + this.map_row_values(this.item, this.values, "serial_no", "qty"); } } update_batch_serial_no_items() { // if serial no selected is from different batches, adds new rows for each batch. - if(this.has_batch && this.has_serial_no) { - const selected_serial_nos = this.values.serial_no.split(/\n/g).filter(s => s); + if (this.has_batch && this.has_serial_no) { + const selected_serial_nos = this.values.serial_no.split(/\n/g).filter((s) => s); - return frappe.db.get_list("Serial No", { - filters: { 'name': ["in", selected_serial_nos]}, - fields: ["batch_no", "name"] - }).then((data) => { - // data = [{batch_no: 'batch-1', name: "SR-001"}, - // {batch_no: 'batch-2', name: "SR-003"}, {batch_no: 'batch-2', name: "SR-004"}] - const batch_serial_map = data.reduce((acc, d) => { - if (!acc[d['batch_no']]) acc[d['batch_no']] = []; - acc[d['batch_no']].push(d['name']) - return acc - }, {}) - // batch_serial_map = { "batch-1": ['SR-001'], "batch-2": ["SR-003", "SR-004"]} - Object.keys(batch_serial_map).map((batch_no, i) => { - let row = ''; - const serial_no = batch_serial_map[batch_no]; - if (i == 0) { - row = this.item; - this.map_row_values(row, {qty: serial_no.length, batch_no: batch_no}, 'batch_no', - 'qty', this.values.warehouse); - } else if (!this.batch_exists(batch_no)) { - row = this.frm.add_child("items", { ...this.item }); - row.batch_no = batch_no; - } else { - row = this.frm.doc.items.find(i => i.batch_no === batch_no); - } - const values = { - 'qty': serial_no.length, - 'serial_no': serial_no.join('\n') - } - this.map_row_values(row, values, 'serial_no', - 'qty', this.values.warehouse); + return frappe.db + .get_list("Serial No", { + filters: { name: ["in", selected_serial_nos] }, + fields: ["batch_no", "name"], + }) + .then((data) => { + // data = [{batch_no: 'batch-1', name: "SR-001"}, + // {batch_no: 'batch-2', name: "SR-003"}, {batch_no: 'batch-2', name: "SR-004"}] + const batch_serial_map = data.reduce((acc, d) => { + if (!acc[d["batch_no"]]) acc[d["batch_no"]] = []; + acc[d["batch_no"]].push(d["name"]); + return acc; + }, {}); + // batch_serial_map = { "batch-1": ['SR-001'], "batch-2": ["SR-003", "SR-004"]} + Object.keys(batch_serial_map).map((batch_no, i) => { + let row = ""; + const serial_no = batch_serial_map[batch_no]; + if (i == 0) { + row = this.item; + this.map_row_values( + row, + { qty: serial_no.length, batch_no: batch_no }, + "batch_no", + "qty", + this.values.warehouse + ); + } else if (!this.batch_exists(batch_no)) { + row = this.frm.add_child("items", { ...this.item }); + row.batch_no = batch_no; + } else { + row = this.frm.doc.items.find((i) => i.batch_no === batch_no); + } + const values = { + qty: serial_no.length, + serial_no: serial_no.join("\n"), + }; + this.map_row_values(row, values, "serial_no", "qty", this.values.warehouse); + }); }); - }) } } batch_exists(batch) { - const batches = this.frm.doc.items.map(data => data.batch_no); - return (batches && in_list(batches, batch)) ? true : false; + const batches = this.frm.doc.items.map((data) => data.batch_no); + return batches && in_list(batches, batch) ? true : false; } map_row_values(row, values, number, qty_field, warehouse) { row.qty = values[qty_field]; row.transfer_qty = flt(values[qty_field]) * flt(row.conversion_factor); row[number] = values[number]; - if(this.warehouse_details.type === 'Source Warehouse') { + if (this.warehouse_details.type === "Source Warehouse") { row.s_warehouse = values.warehouse || warehouse; - } else if(this.warehouse_details.type === 'Target Warehouse') { + } else if (this.warehouse_details.type === "Target Warehouse") { row.t_warehouse = values.warehouse || warehouse; } else { row.warehouse = values.warehouse || warehouse; @@ -319,7 +329,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { let qty_field = this.dialog.fields_dict.qty; let total_qty = 0; - this.dialog.fields_dict.batches.df.data.forEach(data => { + this.dialog.fields_dict.batches.df.data.forEach((data) => { total_qty += flt(data.selected_qty); }); @@ -346,30 +356,35 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { var me = this; return [ - {fieldtype:'Section Break', label: __('Batches')}, - {fieldname: 'batches', fieldtype: 'Table', label: __('Batch Entries'), + { fieldtype: "Section Break", label: __("Batches") }, + { + fieldname: "batches", + fieldtype: "Table", + label: __("Batch Entries"), fields: [ { - 'fieldtype': 'Link', - 'read_only': 0, - 'fieldname': 'batch_no', - 'options': 'Batch', - 'label': __('Select Batch'), - 'in_list_view': 1, + fieldtype: "Link", + read_only: 0, + fieldname: "batch_no", + options: "Batch", + label: __("Select Batch"), + in_list_view: 1, get_query: function () { return { filters: { item_code: me.item_code, - warehouse: me.warehouse || typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '' + warehouse: + me.warehouse || typeof me.warehouse_details.name == "string" + ? me.warehouse_details.name + : "", }, - query: 'erpnext.controllers.queries.get_batch_no' + query: "erpnext.controllers.queries.get_batch_no", }; }, change: function () { const batch_no = this.get_value(); if (!batch_no) { - this.grid_row.on_grid_fields_dict - .available_qty.set_value(0); + this.grid_row.on_grid_fields_dict.available_qty.set_value(0); return; } let selected_batches = this.grid.grid_rows.map((row) => { @@ -383,48 +398,48 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { }); if (selected_batches.includes(batch_no)) { this.set_value(""); - frappe.throw(__('Batch {0} already selected.', [batch_no])); + frappe.throw(__("Batch {0} already selected.", [batch_no])); } if (me.warehouse_details.name) { frappe.call({ - method: 'erpnext.stock.doctype.batch.batch.get_batch_qty', + method: "erpnext.stock.doctype.batch.batch.get_batch_qty", args: { batch_no, warehouse: me.warehouse_details.name, - item_code: me.item_code + item_code: me.item_code, }, callback: (r) => { - this.grid_row.on_grid_fields_dict - .available_qty.set_value(r.message || 0); - } + this.grid_row.on_grid_fields_dict.available_qty.set_value( + r.message || 0 + ); + }, }); - } else { this.set_value(""); - frappe.throw(__('Please select a warehouse to get available quantities')); + frappe.throw(__("Please select a warehouse to get available quantities")); } // e.stopImmediatePropagation(); - } + }, }, { - 'fieldtype': 'Float', - 'read_only': 1, - 'fieldname': 'available_qty', - 'label': __('Available'), - 'in_list_view': 1, - 'default': 0, + fieldtype: "Float", + read_only: 1, + fieldname: "available_qty", + label: __("Available"), + in_list_view: 1, + default: 0, change: function () { - this.grid_row.on_grid_fields_dict.selected_qty.set_value('0'); - } + this.grid_row.on_grid_fields_dict.selected_qty.set_value("0"); + }, }, { - 'fieldtype': 'Float', - 'read_only': 0, - 'fieldname': 'selected_qty', - 'label': __('Qty'), - 'in_list_view': 1, - 'default': 0, + fieldtype: "Float", + read_only: 0, + fieldname: "selected_qty", + label: __("Qty"), + in_list_view: 1, + default: 0, change: function () { var batch_no = this.grid_row.on_grid_fields_dict.batch_no.get_value(); var available_qty = this.grid_row.on_grid_fields_dict.available_qty.get_value(); @@ -433,18 +448,23 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { if (batch_no.length === 0 && parseInt(selected_qty) !== 0) { frappe.throw(__("Please select a batch")); } - if (me.warehouse_details.type === 'Source Warehouse' && - parseFloat(available_qty) < parseFloat(selected_qty)) { - - this.set_value('0'); - frappe.throw(__('For transfer from source, selected quantity cannot be greater than available quantity')); + if ( + me.warehouse_details.type === "Source Warehouse" && + parseFloat(available_qty) < parseFloat(selected_qty) + ) { + this.set_value("0"); + frappe.throw( + __( + "For transfer from source, selected quantity cannot be greater than available quantity" + ) + ); } else { this.grid.refresh(); } me.update_total_qty(); me.update_pending_qtys(); - } + }, }, ], in_place_edit: true, @@ -452,7 +472,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { get_data: function () { return this.data; }, - } + }, ]; } @@ -462,43 +482,48 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { let serial_no_filters = { item_code: me.item_code, - delivery_document_no: "" - } + delivery_document_no: "", + }; if (this.item.batch_no) { serial_no_filters["batch_no"] = this.item.batch_no; } if (me.warehouse_details.name) { - serial_no_filters['warehouse'] = me.warehouse_details.name; + serial_no_filters["warehouse"] = me.warehouse_details.name; } - if (me.frm.doc.doctype === 'POS Invoice' && !this.showing_reserved_serial_nos_error) { - frappe.call({ - method: "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos", - args: { - filters: { - item_code: me.item_code, - warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '', - } - } - }).then((data) => { - serial_no_filters['name'] = ["not in", data.message[0]] - }) + if (me.frm.doc.doctype === "POS Invoice" && !this.showing_reserved_serial_nos_error) { + frappe + .call({ + method: "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos", + args: { + filters: { + item_code: me.item_code, + warehouse: + typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : "", + }, + }, + }) + .then((data) => { + serial_no_filters["name"] = ["not in", data.message[0]]; + }); } return [ - {fieldtype: 'Section Break', label: __('Serial Numbers')}, + { fieldtype: "Section Break", label: __("Serial Numbers") }, { - fieldtype: 'Link', fieldname: 'serial_no_select', options: 'Serial No', - label: __('Select to add Serial Number.'), - get_query: function() { + fieldtype: "Link", + fieldname: "serial_no_select", + options: "Serial No", + label: __("Select to add Serial Number."), + get_query: function () { return { - filters: serial_no_filters + filters: serial_no_filters, }; }, - onchange: function(e) { - if(this.in_local_change) return; + onchange: function (e) { + if (this.in_local_change) return; this.in_local_change = 1; let serial_no_list_field = this.layout.fields_dict.serial_no; @@ -506,22 +531,22 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { let new_number = this.get_value(); let list_value = serial_no_list_field.get_value(); - let new_line = '\n'; - if(!list_value) { - new_line = ''; + let new_line = "\n"; + if (!list_value) { + new_line = ""; } else { me.serial_list = list_value.split(/\n/g) || []; } - if(!me.serial_list.includes(new_number)) { - this.set_new_description(''); - serial_no_list_field.set_value(me.serial_list.join('\n') + new_line + new_number); + if (!me.serial_list.includes(new_number)) { + this.set_new_description(""); + serial_no_list_field.set_value(me.serial_list.join("\n") + new_line + new_number); me.serial_list = serial_no_list_field.get_value().split(/\n/g) || []; } else { - this.set_new_description(new_number + ' is already selected.'); + this.set_new_description(new_number + " is already selected."); } - me.serial_list = me.serial_list.filter(serial => { + me.serial_list = me.serial_list.filter((serial) => { if (serial) { return true; } @@ -530,61 +555,68 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { qty_field.set_input(me.serial_list.length); this.$input.val(""); this.in_local_change = 0; - } + }, }, - {fieldtype: 'Section Break'}, + { fieldtype: "Section Break" }, { - fieldname: 'serial_no', - fieldtype: 'Text', - label: __(me.has_batch && !me.has_serial_no ? 'Selected Batch Numbers' : 'Selected Serial Numbers'), - onchange: function() { + fieldname: "serial_no", + fieldtype: "Text", + label: __( + me.has_batch && !me.has_serial_no ? "Selected Batch Numbers" : "Selected Serial Numbers" + ), + onchange: function () { me.serial_list = this.get_value().split(/\n/g); - me.serial_list = me.serial_list.filter(serial => { + me.serial_list = me.serial_list.filter((serial) => { if (serial) { return true; } }); this.layout.fields_dict.qty.set_input(me.serial_list.length); - } - } + }, + }, ]; } }; function get_pending_qty_fields(me) { if (!check_can_calculate_pending_qty(me)) return []; - const { frm: { doc: { fg_completed_qty }}, item: { item_code, stock_qty }} = me; + const { + frm: { + doc: { fg_completed_qty }, + }, + item: { item_code, stock_qty }, + } = me; const { qty_consumed_per_unit } = erpnext.stock.bom.items[item_code]; const total_selected_qty = calc_total_selected_qty(me); const required_qty = flt(fg_completed_qty) * flt(qty_consumed_per_unit); const pending_qty = required_qty - (flt(stock_qty) + total_selected_qty); - const pending_qty_fields = [ - { fieldtype: 'Section Break', label: __('Pending Quantity') }, + const pending_qty_fields = [ + { fieldtype: "Section Break", label: __("Pending Quantity") }, { - fieldname: 'required_qty', + fieldname: "required_qty", read_only: 1, - fieldtype: 'Float', - label: __('Required Qty'), - default: required_qty + fieldtype: "Float", + label: __("Required Qty"), + default: required_qty, }, - { fieldtype: 'Column Break' }, + { fieldtype: "Column Break" }, { - fieldname: 'total_selected_qty', + fieldname: "total_selected_qty", read_only: 1, - fieldtype: 'Float', - label: __('Total Selected Qty'), - default: total_selected_qty + fieldtype: "Float", + label: __("Total Selected Qty"), + default: total_selected_qty, }, - { fieldtype: 'Column Break' }, + { fieldtype: "Column Break" }, { - fieldname: 'pending_qty', + fieldname: "pending_qty", read_only: 1, - fieldtype: 'Float', - label: __('Pending Qty'), - default: pending_qty + fieldtype: "Float", + label: __("Pending Qty"), + default: pending_qty, }, ]; return pending_qty_fields; @@ -592,37 +624,45 @@ function get_pending_qty_fields(me) { // get all items with same item code except row for which selector is open. function get_rows_with_same_item_code(me) { - const { frm: { doc: { items }}, item: { name, item_code }} = me; - return items.filter(item => (item.name !== name) && (item.item_code === item_code)) + const { + frm: { + doc: { items }, + }, + item: { name, item_code }, + } = me; + return items.filter((item) => item.name !== name && item.item_code === item_code); } function calc_total_selected_qty(me) { const totalSelectedQty = get_rows_with_same_item_code(me) - .map(item => flt(item.qty)) + .map((item) => flt(item.qty)) .reduce((i, j) => i + j, 0); return totalSelectedQty; } function get_selected_serial_nos(me) { const selected_serial_nos = get_rows_with_same_item_code(me) - .map(item => item.serial_no) - .filter(serial => serial) - .map(sr_no_string => sr_no_string.split('\n')) + .map((item) => item.serial_no) + .filter((serial) => serial) + .map((sr_no_string) => sr_no_string.split("\n")) .reduce((acc, arr) => acc.concat(arr), []) - .filter(serial => serial); + .filter((serial) => serial); return selected_serial_nos; -}; +} function check_can_calculate_pending_qty(me) { - const { frm: { doc }, item } = me; - const docChecks = doc.bom_no - && doc.fg_completed_qty - && erpnext.stock.bom - && erpnext.stock.bom.name === doc.bom_no; - const itemChecks = !!item - && !item.original_item - && erpnext.stock.bom && erpnext.stock.bom.items - && (item.item_code in erpnext.stock.bom.items); + const { + frm: { doc }, + item, + } = me; + const docChecks = + doc.bom_no && doc.fg_completed_qty && erpnext.stock.bom && erpnext.stock.bom.name === doc.bom_no; + const itemChecks = + !!item && + !item.original_item && + erpnext.stock.bom && + erpnext.stock.bom.items && + item.item_code in erpnext.stock.bom.items; return docChecks && itemChecks; } diff --git a/erpnext/public/js/utils/supplier_quick_entry.js b/erpnext/public/js/utils/supplier_quick_entry.js index 687b01454a2..968ef74c3a0 100644 --- a/erpnext/public/js/utils/supplier_quick_entry.js +++ b/erpnext/public/js/utils/supplier_quick_entry.js @@ -1,3 +1,3 @@ -frappe.provide('frappe.ui.form'); +frappe.provide("frappe.ui.form"); frappe.ui.form.SupplierQuickEntryForm = frappe.ui.form.ContactAddressQuickEntryForm; diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 79490a162d3..6864e2865d3 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -1,27 +1,34 @@ -frappe.provide('erpnext.accounts'); +frappe.provide("erpnext.accounts"); erpnext.accounts.unreconcile_payment = { add_unreconcile_btn(frm) { if (frm.doc.docstatus == 1) { - if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry")) - || !["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes(frm.doc.doctype) - ) { + if ( + (frm.doc.doctype == "Journal Entry" && frm.doc.voucher_type != "Journal Entry") || + !["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes( + frm.doc.doctype + ) + ) { return; } frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.doc_has_references", - "args": { - "doctype": frm.doc.doctype, - "docname": frm.doc.name + method: "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.doc_has_references", + args: { + doctype: frm.doc.doctype, + docname: frm.doc.name, }, - callback: function(r) { + callback: function (r) { if (r.message) { - frm.add_custom_button(__("UnReconcile"), function() { - erpnext.accounts.unreconcile_payment.build_unreconcile_dialog(frm); - }, __('Actions')); + frm.add_custom_button( + __("UnReconcile"), + function () { + erpnext.accounts.unreconcile_payment.build_unreconcile_dialog(frm); + }, + __("Actions") + ); } - } + }, }); } }, @@ -30,18 +37,18 @@ erpnext.accounts.unreconcile_payment = { // assuming each row is an individual voucher // pass this to server side method that creates unreconcile doc for each row let selection_map = []; - if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) { - selection_map = selections.map(function(elem) { + if (["Sales Invoice", "Purchase Invoice"].includes(frm.doc.doctype)) { + selection_map = selections.map(function (elem) { return { company: elem.company, voucher_type: elem.voucher_type, voucher_no: elem.voucher_no, against_voucher_type: frm.doc.doctype, - against_voucher_no: frm.doc.name + against_voucher_no: frm.doc.name, }; }); - } else if (['Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { - selection_map = selections.map(function(elem) { + } else if (["Payment Entry", "Journal Entry"].includes(frm.doc.doctype)) { + selection_map = selections.map(function (elem) { return { company: elem.company, voucher_type: frm.doc.doctype, @@ -55,18 +62,41 @@ erpnext.accounts.unreconcile_payment = { }, build_unreconcile_dialog(frm) { - if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { + if ( + ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"].includes(frm.doc.doctype) + ) { let child_table_fields = [ - { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, - { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, - { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Currency", in_list_view: 1, read_only: 1 , options: "account_currency"}, - { label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1}, - ] + { + label: __("Voucher Type"), + fieldname: "voucher_type", + fieldtype: "Dynamic Link", + options: "DocType", + in_list_view: 1, + read_only: 1, + }, + { + label: __("Voucher No"), + fieldname: "voucher_no", + fieldtype: "Link", + options: "voucher_type", + in_list_view: 1, + read_only: 1, + }, + { + label: __("Allocated Amount"), + fieldname: "allocated_amount", + fieldtype: "Currency", + in_list_view: 1, + read_only: 1, + options: "account_currency", + }, + { label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1 }, + ]; let unreconcile_dialog_fields = [ { - label: __('Allocations'), - fieldname: 'allocations', - fieldtype: 'Table', + label: __("Allocations"), + fieldname: "allocations", + fieldtype: "Table", read_only: 1, fields: child_table_fields, }, @@ -74,54 +104,57 @@ erpnext.accounts.unreconcile_payment = { // get linked payments frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.get_linked_payments_for_doc", - "args": { - "company": frm.doc.company, - "doctype": frm.doc.doctype, - "docname": frm.doc.name + method: "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.get_linked_payments_for_doc", + args: { + company: frm.doc.company, + doctype: frm.doc.doctype, + docname: frm.doc.name, }, - callback: function(r) { + callback: function (r) { if (r.message) { // populate child table with allocations unreconcile_dialog_fields[0].data = r.message; - unreconcile_dialog_fields[0].get_data = function(){ return r.message}; + unreconcile_dialog_fields[0].get_data = function () { + return r.message; + }; let d = new frappe.ui.Dialog({ - title: 'UnReconcile Allocations', + title: "UnReconcile Allocations", fields: unreconcile_dialog_fields, - size: 'large', + size: "large", cannot_add_rows: true, - primary_action_label: 'UnReconcile', + primary_action_label: "UnReconcile", primary_action(values) { - - let selected_allocations = values.allocations.filter(x=>x.__checked); + let selected_allocations = values.allocations.filter((x) => x.__checked); if (selected_allocations.length > 0) { - let selection_map = erpnext.accounts.unreconcile_payment.build_selection_map(frm, selected_allocations); - erpnext.accounts.unreconcile_payment.create_unreconcile_docs(selection_map); + let selection_map = + erpnext.accounts.unreconcile_payment.build_selection_map( + frm, + selected_allocations + ); + erpnext.accounts.unreconcile_payment.create_unreconcile_docs( + selection_map + ); d.hide(); - } else { frappe.msgprint("No Selection"); } - } + }, }); d.show(); } - } + }, }); } }, create_unreconcile_docs(selection_map) { frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.create_unreconcile_doc_for_selection", - "args": { - "selections": selection_map + method: "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.create_unreconcile_doc_for_selection", + args: { + selections: selection_map, }, }); - } - - - -} + }, +}; diff --git a/erpnext/public/js/website_theme.js b/erpnext/public/js/website_theme.js index 0009cacf61e..9c2b8cd11b9 100644 --- a/erpnext/public/js/website_theme.js +++ b/erpnext/public/js/website_theme.js @@ -1,14 +1,15 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors // MIT License. See license.txt -frappe.ui.form.on('Website Theme', { +frappe.ui.form.on("Website Theme", { validate(frm) { let theme_scss = frm.doc.theme_scss; - if (theme_scss && theme_scss.includes('frappe/public/scss/website') - && !theme_scss.includes('erpnext/public/scss/website') + if ( + theme_scss && + theme_scss.includes("frappe/public/scss/website") && + !theme_scss.includes("erpnext/public/scss/website") ) { - frm.set_value('theme_scss', - `${frm.doc.theme_scss}\n@import "erpnext/public/scss/website";`); + frm.set_value("theme_scss", `${frm.doc.theme_scss}\n@import "erpnext/public/scss/website";`); } - } + }, }); diff --git a/erpnext/public/js/website_utils.js b/erpnext/public/js/website_utils.js index b5416065d79..f196d7c7ade 100644 --- a/erpnext/public/js/website_utils.js +++ b/erpnext/public/js/website_utils.js @@ -1,29 +1,29 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -if(!window.erpnext) window.erpnext = {}; +if (!window.erpnext) window.erpnext = {}; // Add / update a new Lead / Communication // subject, sender, description -frappe.send_message = function(opts, btn) { +frappe.send_message = function (opts, btn) { return frappe.call({ type: "POST", method: "erpnext.templates.utils.send_message", btn: btn, args: opts, - callback: opts.callback + callback: opts.callback, }); }; -erpnext.subscribe_to_newsletter = function(opts, btn) { +erpnext.subscribe_to_newsletter = function (opts, btn) { return frappe.call({ type: "POST", method: "frappe.email.doctype.newsletter.newsletter.subscribe", btn: btn, - args: {"email": opts.email}, - callback: opts.callback + args: { email: opts.email }, + callback: opts.callback, }); -} +}; // for backward compatibility erpnext.send_message = frappe.send_message; diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js index f6599e9f6d1..04e28bf75fa 100644 --- a/erpnext/public/js/wishlist.js +++ b/erpnext/public/js/wishlist.js @@ -5,18 +5,18 @@ frappe.provide("erpnext.e_commerce.shopping_cart"); var shopping_cart = erpnext.e_commerce.shopping_cart; $.extend(wishlist, { - set_wishlist_count: function(animate=false) { + set_wishlist_count: function (animate = false) { // set badge count for wishlist icon var wish_count = frappe.get_cookie("wish_count"); - if (frappe.session.user==="Guest") { + if (frappe.session.user === "Guest") { wish_count = 0; } if (wish_count) { - $(".wishlist").toggleClass('hidden', false); + $(".wishlist").toggleClass("hidden", false); } - var $wishlist = $('.wishlist-icon'); + var $wishlist = $(".wishlist-icon"); var $badge = $wishlist.find("#wish-count"); if (parseInt(wish_count) === 0 || wish_count === undefined) { @@ -27,9 +27,9 @@ $.extend(wishlist, { if (wish_count) { $badge.html(wish_count); if (animate) { - $wishlist.addClass('cart-animate'); + $wishlist.addClass("cart-animate"); setTimeout(() => { - $wishlist.removeClass('cart-animate'); + $wishlist.removeClass("cart-animate"); }, 500); } } else { @@ -37,19 +37,19 @@ $.extend(wishlist, { } }, - bind_move_to_cart_action: function() { + bind_move_to_cart_action: function () { // move item to cart from wishlist - $('.page_content').on("click", ".btn-add-to-cart", (e) => { + $(".page_content").on("click", ".btn-add-to-cart", (e) => { const $move_to_cart_btn = $(e.currentTarget); let item_code = $move_to_cart_btn.data("item-code"); shopping_cart.shopping_cart_update({ item_code, qty: 1, - cart_dropdown: true + cart_dropdown: true, }); - let success_action = function() { + let success_action = function () { const $card_wrapper = $move_to_cart_btn.closest(".wishlist-card"); $card_wrapper.addClass("wish-removed"); }; @@ -58,15 +58,15 @@ $.extend(wishlist, { }); }, - bind_remove_action: function() { + bind_remove_action: function () { // remove item from wishlist let me = this; - $('.page_content').on("click", ".remove-wish", (e) => { + $(".page_content").on("click", ".remove-wish", (e) => { const $remove_wish_btn = $(e.currentTarget); let item_code = $remove_wish_btn.data("item-code"); - let success_action = function() { + let success_action = function () { const $card_wrapper = $remove_wish_btn.closest(".wishlist-card"); $card_wrapper.addClass("wish-removed"); if (frappe.get_cookie("wish_count") == 0) { @@ -81,17 +81,17 @@ $.extend(wishlist, { bind_wishlist_action() { // 'wish'('like') or 'unwish' item in product listing - $('.page_content').on('click', '.like-action, .like-action-list', (e) => { + $(".page_content").on("click", ".like-action, .like-action-list", (e) => { const $btn = $(e.currentTarget); this.wishlist_action($btn); }); }, wishlist_action(btn) { - const $wish_icon = btn.find('.wish-icon'); + const $wish_icon = btn.find(".wish-icon"); let me = this; - if (frappe.session.user==="Guest") { + if (frappe.session.user === "Guest") { if (localStorage) { localStorage.setItem("last_visited", window.location.pathname); } @@ -99,30 +99,30 @@ $.extend(wishlist, { return; } - let success_action = function() { + let success_action = function () { erpnext.e_commerce.wishlist.set_wishlist_count(true); }; - if ($wish_icon.hasClass('wished')) { + if ($wish_icon.hasClass("wished")) { // un-wish item btn.removeClass("like-animate"); btn.addClass("like-action-wished"); - this.toggle_button_class($wish_icon, 'wished', 'not-wished'); + this.toggle_button_class($wish_icon, "wished", "not-wished"); - let args = { item_code: btn.data('item-code') }; - let failure_action = function() { - me.toggle_button_class($wish_icon, 'not-wished', 'wished'); + let args = { item_code: btn.data("item-code") }; + let failure_action = function () { + me.toggle_button_class($wish_icon, "not-wished", "wished"); }; this.add_remove_from_wishlist("remove", args, success_action, failure_action); } else { // wish item btn.addClass("like-animate"); btn.addClass("like-action-wished"); - this.toggle_button_class($wish_icon, 'not-wished', 'wished'); + this.toggle_button_class($wish_icon, "not-wished", "wished"); - let args = {item_code: btn.data('item-code')}; - let failure_action = function() { - me.toggle_button_class($wish_icon, 'wished', 'not-wished'); + let args = { item_code: btn.data("item-code") }; + let failure_action = function () { + me.toggle_button_class($wish_icon, "wished", "not-wished"); }; this.add_remove_from_wishlist("add", args, success_action, failure_action); } @@ -133,14 +133,14 @@ $.extend(wishlist, { button.addClass(add); }, - add_remove_from_wishlist(action, args, success_action, failure_action, async=false) { + add_remove_from_wishlist(action, args, success_action, failure_action, async = false) { /* AJAX call to add or remove Item from Wishlist action: "add" or "remove" args: args for method (item_code, price, formatted_price), success_action: method to execute on successs, failure_action: method to execute on failure, async: make call asynchronously (true/false). */ - if (frappe.session.user==="Guest") { + if (frappe.session.user === "Guest") { if (localStorage) { localStorage.setItem("last_visited", window.location.pathname); } @@ -158,23 +158,24 @@ $.extend(wishlist, { args: args, callback: function (r) { if (r.exc) { - if (failure_action && (typeof failure_action === 'function')) { + if (failure_action && typeof failure_action === "function") { failure_action(); } frappe.msgprint({ message: __("Sorry, something went wrong. Please refresh."), - indicator: "red", title: __("Note") + indicator: "red", + title: __("Note"), }); - } else if (success_action && (typeof success_action === 'function')) { + } else if (success_action && typeof success_action === "function") { success_action(); } - } + }, }); } }, redirect_guest() { - frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => { + frappe.call("erpnext.e_commerce.api.get_guest_redirect_on_action").then((res) => { window.location.href = res.message || "/login"; }); }, @@ -185,20 +186,18 @@ $.extend(wishlist, {
              Empty Cart
              -
              ${ __('Wishlist is empty !') }

              +
              ${__("Wishlist is empty !")}

              `); - } - + }, }); -frappe.ready(function() { +frappe.ready(function () { if (window.location.pathname !== "/wishlist") { - $(".wishlist").toggleClass('hidden', true); + $(".wishlist").toggleClass("hidden", true); wishlist.set_wishlist_count(); } else { wishlist.bind_move_to_cart_action(); wishlist.bind_remove_action(); } - -}); \ No newline at end of file +}); diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index 8ab5973debd..be3aed1ed0b 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -51,7 +51,7 @@ } // assessment tool -.frappe-control[data-fieldname='result_html'] { +.frappe-control[data-fieldname="result_html"] { overflow: scroll; } .assessment-result-tool { @@ -70,7 +70,9 @@ text-overflow: ellipsis; } - .total-score, .grade, .score { + .total-score, + .grade, + .score { text-align: right; } } @@ -78,13 +80,13 @@ /* pos */ body[data-route="pos"] { - .pos-bill-toolbar { padding: 10px 0px; height: 51px; } - .pos-bill-item:hover, .list-customers-table > .pos-list-row:hover { + .pos-bill-item:hover, + .list-customers-table > .pos-list-row:hover { background-color: #f5f7fa; cursor: pointer; } @@ -135,50 +137,52 @@ body[data-route="pos"] { } .pos-payment-row .col-xs-6 { - padding :15px; + padding: 15px; } .pos-payment-row { - border-bottom:1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); margin: 2px 0px 5px 0px; height: 60px; margin-top: 0px; margin-bottom: 0px; } - .pos-payment-row:hover, .pos-keyboard-key:hover{ + .pos-payment-row:hover, + .pos-keyboard-key:hover { background-color: var(--bg-color); cursor: pointer; } - .pos-keyboard-key, .delete-btn { + .pos-keyboard-key, + .delete-btn { border: 1px solid var(--border-color); - height:85px; - width:85px; - margin:10px 10px; - font-size:24px; - font-weight:200; - background-color: #FDFDFD; + height: 85px; + width: 85px; + margin: 10px 10px; + font-size: 24px; + font-weight: 200; + background-color: #fdfdfd; border-color: #e8e8e8; } .numeric-keypad { border: 1px solid var(--border-color); - height:69px; - width:69px; - font-size:20px; - font-weight:200; - background-color: #FDFDFD; + height: 69px; + width: 69px; + font-size: 20px; + font-weight: 200; + background-color: #fdfdfd; border-color: #e8e8e8; - margin-left:-4px; + margin-left: -4px; } .pos-pay { - height:69px; - width:69px; - font-size:17px; - font-weight:200; - margin-left:-4px; + height: 69px; + width: 69px; + font-size: 17px; + font-weight: 200; + margin-left: -4px; } .numeric-keypad { @@ -188,7 +192,7 @@ body[data-route="pos"] { font-weight: 200; border-radius: 0; background-color: #fff; - margin-left:-4px; + margin-left: -4px; @media (max-width: var(--xl-width)) { height: 45px; @@ -253,7 +257,8 @@ body[data-route="pos"] { .amount-row h3 { font-size: 15px; } - .pos-keyboard-key, .delete-btn { + .pos-keyboard-key, + .delete-btn { height: 50px; } .multimode-payments { @@ -277,7 +282,8 @@ body[data-route="pos"] { padding: 15px 10px; } - .write_off_amount, .change_amount { + .write_off_amount, + .change_amount { margin: 15px; width: 130px; } @@ -301,10 +307,11 @@ body[data-route="pos"] { } .subject { - width: 40% + width: 40%; } - .list-row-checkbox, .list-select-all { + .list-row-checkbox, + .list-select-all { margin-right: 7px; } } @@ -397,7 +404,7 @@ body[data-route="pos"] { padding-top: 0; } - &> .pos-list-row { + & > .pos-list-row { border: none; @media (max-width: var(--xl-width)) { @@ -442,13 +449,12 @@ body[data-route="pos"] { padding: 5px 9px; border-radius: 3px; color: #fff; - } // Healthcare .exercise-card { - box-shadow: 0 1px 3px rgba(0,0,0,0.30); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); border-radius: 2px; padding: 6px 6px 6px 8px; margin-top: 10px; @@ -490,3 +496,7 @@ body[data-route="pos"] { .exercise-col { padding: 10px; } + +.frappe-control[data-fieldname="other_charges_calculation"] .ql-editor { + white-space: normal; +} diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index 57d5e8414ae..834d5ccb194 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -22,12 +22,12 @@ } .node-card.exported { - box-shadow: none + box-shadow: none; } .node-image { - width: 3.0rem; - height: 3.0rem; + width: 3rem; + height: 3rem; } .node-name { @@ -61,8 +61,8 @@ display: flex; background: var(--blue-100); color: var(--blue-500); - padding: .25rem .5rem; - font-size: .75rem; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; justify-content: center; box-shadow: var(--shadow-sm); margin-left: auto; @@ -77,7 +77,7 @@ display: block; } - .node-edit-icon > .icon{ + .node-edit-icon > .icon { stroke: var(--blue-500); } @@ -96,7 +96,7 @@ border-radius: 0.5rem; padding: 0.75rem; width: 15rem; - height: 3.0rem; + height: 3rem; .btn-edit-node { display: none !important; @@ -144,7 +144,7 @@ border-radius: 0.5rem; padding: 0.75rem; width: 15rem; - height: 3.0rem; + height: 3rem; .btn-edit-node { display: none !important; @@ -206,7 +206,8 @@ margin: 0px 0px 16px 0px; } -.hierarchy, .hierarchy-mobile { +.hierarchy, +.hierarchy-mobile { .level { margin-right: 8px; align-items: flex-start; diff --git a/erpnext/public/scss/order-page.scss b/erpnext/public/scss/order-page.scss index 6f5fe5d4d7a..2b1dae282a3 100644 --- a/erpnext/public/scss/order-page.scss +++ b/erpnext/public/scss/order-page.scss @@ -1,115 +1,113 @@ #page-order { - .main-column { - .page-content-wrapper { + .main-column { + .page-content-wrapper { + .breadcrumb-container { + @media screen and (min-width: 567px) { + padding-left: var(--padding-sm); + } + } - .breadcrumb-container { - @media screen and (min-width: 567px) { - padding-left: var(--padding-sm); - } - } + .container.my-4 { + background-color: var(--fg-color); - .container.my-4 { - background-color: var(--fg-color); - - @media screen and (min-width: 567px) { - padding: 1.25rem 1.5rem; - border-radius: var(--border-radius-md); - box-shadow: var(--card-shadow); - } - } - } - } + @media screen and (min-width: 567px) { + padding: 1.25rem 1.5rem; + border-radius: var(--border-radius-md); + box-shadow: var(--card-shadow); + } + } + } + } } .indicator-container { - @media screen and (max-width: 567px) { - padding-bottom: 0.8rem; - } + @media screen and (max-width: 567px) { + padding-bottom: 0.8rem; + } } .order-items { - padding: 1.5rem 0; - border-bottom: 1px solid var(--border-color); - color: var(--gray-700); + padding: 1.5rem 0; + border-bottom: 1px solid var(--border-color); + color: var(--gray-700); - @media screen and (max-width: 567px) { - align-items: flex-start !important; - } - .col-2 { - @media screen and (max-width: 567px) { - flex: auto; - max-width: 28%; - } - } + @media screen and (max-width: 567px) { + align-items: flex-start !important; + } + .col-2 { + @media screen and (max-width: 567px) { + flex: auto; + max-width: 28%; + } + } - .order-item-name { - font-size: var(--text-base); - font-weight: 500; - } + .order-item-name { + font-size: var(--text-base); + font-weight: 500; + } - .btn:focus, - .btn:hover { - background-color: var(--control-bg); - } + .btn:focus, + .btn:hover { + background-color: var(--control-bg); + } + .col-6 { + @media screen and (max-width: 567px) { + max-width: 100%; + } - .col-6 { - @media screen and (max-width: 567px) { - max-width: 100%; - } - - &.order-item-name { - font-size: var(--text-base); - } - } + &.order-item-name { + font-size: var(--text-base); + } + } } .item-grand-total { - font-size: var(--text-base); + font-size: var(--text-base); } .list-item-name, .item-total, .order-container, .order-qty { - font-size: var(--text-md); + font-size: var(--text-md); } .d-s-n { - @media screen and (max-width: 567px) { - display: none; - } + @media screen and (max-width: 567px) { + display: none; + } } .d-l-n { - @media screen and (min-width: 567px) { - display: none; - } + @media screen and (min-width: 567px) { + display: none; + } } .border-btm { - border-bottom: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); } .order-taxes { - display: flex; + display: flex; - @media screen and (min-width: 567px) { - justify-content: flex-end; - } + @media screen and (min-width: 567px) { + justify-content: flex-end; + } - .col-4 { - padding-right: 0; + .col-4 { + padding-right: 0; - .col-8 { - padding-left: 0; - padding-right: 0; - } + .col-8 { + padding-left: 0; + padding-right: 0; + } - @media screen and (max-width: 567px) { - padding-left: 0; - flex: auto; - max-width: 100%; - } - } -} \ No newline at end of file + @media screen and (max-width: 567px) { + padding-left: 0; + flex: auto; + max-width: 100%; + } + } +} diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index 7b7530b1501..f50e2a52117 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -185,7 +185,6 @@ font-weight: 700; } } - } } } @@ -262,12 +261,12 @@ flex-direction: column; margin-right: auto; - >.customer-name { + > .customer-name { font-weight: 700; font-size: var(--text-lg); } - >.customer-desc { + > .customer-desc { color: var(--gray-600); font-weight: 500; font-size: var(--text-sm); @@ -279,7 +278,6 @@ align-items: center; cursor: pointer; } - } > .customer-fields-container { @@ -408,7 +406,6 @@ font-size: var(--text-lg); } - > .item-name-desc { @extend .nowrap; display: flex; @@ -456,7 +453,6 @@ } } } - } } @@ -855,7 +851,7 @@ display: flex; flex: 1; height: 100%; - position: relative; + position: relative; justify-content: flex-end; > .fields-section { @@ -1136,7 +1132,6 @@ } } - > .summary-btns { display: flex; justify-content: space-between; @@ -1148,7 +1143,7 @@ > .new-btn { background-color: var(--blue-500); - color:white; + color: white; font-weight: 500; } } diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 6ae464d2c21..35c1fb64e36 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -1,9 +1,9 @@ @import "frappe/public/scss/common/mixins"; :root { - --green-info: #38A160; + --green-info: #38a160; --product-bg-color: white; - --body-bg-color: var(--gray-50); + --body-bg-color: var(--gray-50); } body.product-page { @@ -96,7 +96,8 @@ body.product-page { } } - .card:hover, .card:focus-within { + .card:hover, + .card:focus-within { .btn-add-to-cart-list { visibility: visible; } @@ -108,7 +109,6 @@ body.product-page { } } - .card-img-container { height: 210px; width: 100%; @@ -205,7 +205,8 @@ body.product-page { } } -#products-list-area, #products-grid-area { +#products-list-area, +#products-grid-area { padding: 0 5px; } @@ -216,7 +217,8 @@ body.product-page { border-radius: 8px; border-bottom: 1px solid var(--gray-50); - &:hover, &:focus-within { + &:hover, + &:focus-within { box-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04); transition: box-shadow 400ms; @@ -347,13 +349,13 @@ body.product-page { .btn-add-to-wishlist { svg use { - --icon-stroke: #F47A7A; + --icon-stroke: #f47a7a; } } .btn-view-in-wishlist { svg use { - fill: #F47A7A; + fill: #f47a7a; --icon-stroke: none; } } @@ -384,13 +386,12 @@ body.product-page { width: 350px; } - img { + img { object-fit: contain; } } .item-slideshow { - @media (max-width: var(--md-width)) { max-height: 320px; } @@ -411,7 +412,8 @@ body.product-page { border-radius: 4px; cursor: pointer; - &:hover, &.active { + &:hover, + &.active { border-color: var(--primary); } } @@ -460,11 +462,11 @@ body.product-page { .recommendation-header { font-size: 16px; - font-weight: 500 + font-weight: 500; } .recommendation-container { - padding: .5rem; + padding: 0.5rem; min-height: 0px; .r-item-image { @@ -476,11 +478,12 @@ body.product-page { } .no-image-r-item { - display: flex; justify-content: center; + display: flex; + justify-content: center; background-color: var(--gray-200); align-items: center; color: var(--gray-400); - margin-top: .15rem; + margin-top: 0.15rem; border-radius: 6px; height: 100%; font-size: 24px; @@ -513,16 +516,16 @@ body.product-page { } .product-code { - padding: .5rem 0; + padding: 0.5rem 0; color: var(--text-muted); font-size: 14px; .product-item-group { - padding-right: .25rem; + padding-right: 0.25rem; border-right: solid 1px var(--text-muted); } .product-item-code { - padding-left: .5rem; + padding-left: 0.5rem; } } @@ -561,14 +564,13 @@ body.product-page { } .item-group-slideshow { - .carousel-inner.rounded-carousel { border-radius: var(--card-border-radius); } } .sub-category-container { - padding-bottom: .5rem; + padding-bottom: 0.5rem; margin-bottom: 1.25rem; border-bottom: 1px solid var(--table-border-color); @@ -590,7 +592,6 @@ body.product-page { } } - .shopping-badge { position: relative; top: -10px; @@ -602,7 +603,6 @@ body.product-page { border-radius: 50%; } - .cart-animate { animation: wiggle 0.5s linear; } @@ -656,7 +656,9 @@ body.product-page { margin-bottom: 1rem; } - th, tr, td { + th, + tr, + td { border-color: var(--border-color); border-width: 1px; } @@ -725,7 +727,6 @@ body.product-page { height: 60px; font-size: 14px; } - } .cart-tax-items { @@ -759,7 +760,7 @@ body.product-page { background-color: var(--gray-100); float: right; cursor: pointer; - margin-top: .25rem; + margin-top: 0.25rem; justify-content: center; } @@ -859,11 +860,12 @@ body.product-page { .no-image-cart-item { max-height: 112px; - display: flex; justify-content: center; + display: flex; + justify-content: center; background-color: var(--gray-200); align-items: center; color: var(--gray-400); - margin-top: .15rem; + margin-top: 0.15rem; border-radius: 6px; height: 100%; font-size: 24px; @@ -911,7 +913,8 @@ body.product-page { } .address-header { - margin-top: .15rem;padding: 0; + margin-top: 0.15rem; + padding: 0; } .btn-new-address { @@ -920,7 +923,8 @@ body.product-page { color: var(--primary-color) !important; } -.btn-new-address:hover, .btn-change-address:hover { +.btn-new-address:hover, +.btn-change-address:hover { color: var(--primary-color) !important; } @@ -952,7 +956,6 @@ body.product-page { } } - .like-action { visibility: hidden; text-align: center; @@ -1013,31 +1016,31 @@ body.product-page { @keyframes expand { 30% { - transform: scale(1.3); + transform: scale(1.3); } 50% { - transform: scale(0.8); + transform: scale(0.8); } 70% { transform: scale(1.1); } 100% { - transform: scale(1); + transform: scale(1); } - } +} .not-wished { cursor: pointer; - --icon-stroke: #F47A7A !important; + --icon-stroke: #f47a7a !important; &:hover { - fill: #F47A7A; + fill: #f47a7a; } } .wished { --icon-stroke: none; - fill: #F47A7A !important; + fill: #f47a7a !important; } .list-row-checkbox { @@ -1052,7 +1055,7 @@ body.product-page { } #pay-for-order { - padding: .5rem 1rem; // Pay button in SO + padding: 0.5rem 1rem; // Pay button in SO } .btn-explore-variants { @@ -1074,7 +1077,7 @@ body.product-page { } } -.btn-add-to-cart-list{ +.btn-add-to-cart-list { visibility: hidden; box-shadow: none; margin: var(--margin-sm) 0; @@ -1108,7 +1111,7 @@ body.product-page { background-color: white; position: absolute; cursor: pointer; - top:10px; + top: 10px; right: 20px; width: 32px; height: 32px; @@ -1123,7 +1126,7 @@ body.product-page { } .item-website-specification { - font-size: .875rem; + font-size: 0.875rem; .product-title { font-size: 18px; } @@ -1150,8 +1153,8 @@ body.product-page { } .ratings-reviews-section { - border-top: 1px solid #E2E6E9; - padding: .5rem 1rem; + border-top: 1px solid #e2e6e9; + padding: 0.5rem 1rem; } .reviews-header { @@ -1165,7 +1168,7 @@ body.product-page { .btn-write-review { float: right; - padding: .5rem 1rem; + padding: 0.5rem 1rem; font-size: 14px; font-weight: 400; border: none !important; @@ -1218,7 +1221,7 @@ body.product-page { .ratings-pill { background-color: var(--gray-100); - padding: .5rem 1rem; + padding: 0.5rem 1rem; border-radius: 66px; } @@ -1226,7 +1229,7 @@ body.product-page { max-width: 80%; line-height: 1.6; padding-bottom: 0.5rem; - border-bottom: 1px solid #E2E6E9; + border-bottom: 1px solid #e2e6e9; } .review-signature { @@ -1266,7 +1269,7 @@ body.product-page { #search-results-container { border: 1px solid var(--gray-200); - padding: .25rem 1rem; + padding: 0.25rem 1rem; .category-chip { background-color: var(--gray-100); @@ -1275,7 +1278,7 @@ body.product-page { } .recent-search { - padding: .5rem .5rem; + padding: 0.5rem 0.5rem; border-radius: var(--border-radius); &:hover { @@ -1313,11 +1316,11 @@ body.product-page { } .placeholder-div { - height:80%; + height: 80%; width: -webkit-fill-available; padding: 50px; text-align: center; - background-color: #F9FAFA; + background-color: #f9fafa; border-top-left-radius: calc(0.75rem - 1px); border-top-right-radius: calc(0.75rem - 1px); } @@ -1342,7 +1345,8 @@ body.product-page { color: gray; } -.btn-next, .btn-prev { +.btn-next, +.btn-prev { font-size: 14px; } @@ -1369,7 +1373,7 @@ body.product-page { font-weight: 400; font-size: 14px; line-height: 20px; - color: #F47A7A; + color: #f47a7a; } .mt-minus-2 { @@ -1378,4 +1382,4 @@ body.product-page { .mt-minus-1 { margin-top: -1rem; -} \ No newline at end of file +} diff --git a/erpnext/public/scss/website.scss b/erpnext/public/scss/website.scss index b5e97f1c34b..dd2ae210c76 100644 --- a/erpnext/public/scss/website.scss +++ b/erpnext/public/scss/website.scss @@ -1,4 +1,4 @@ -@import './order-page'; +@import "./order-page"; .filter-options { max-height: 300px; @@ -52,7 +52,8 @@ border-bottom: 1px solid var(--border-color); position: relative; - &:only-child, &:last-child { + &:only-child, + &:last-child { border: 0; } @@ -80,7 +81,8 @@ } } -.list-item-name, .item-total { +.list-item-name, +.item-total { font-size: var(--font-size-sm); } @@ -88,4 +90,4 @@ @media screen and (max-width: 567px) { margin-top: 1rem; } -} \ No newline at end of file +} diff --git a/erpnext/quality_management/doctype/non_conformance/non_conformance.js b/erpnext/quality_management/doctype/non_conformance/non_conformance.js index e7f5eee623e..5524d7472b2 100644 --- a/erpnext/quality_management/doctype/non_conformance/non_conformance.js +++ b/erpnext/quality_management/doctype/non_conformance/non_conformance.js @@ -1,8 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Non Conformance', { +frappe.ui.form.on("Non Conformance", { // refresh: function(frm) { - // } }); diff --git a/erpnext/quality_management/doctype/quality_action/quality_action.js b/erpnext/quality_management/doctype/quality_action/quality_action.js index b44f2a20344..8261fab1ab5 100644 --- a/erpnext/quality_management/doctype/quality_action/quality_action.js +++ b/erpnext/quality_management/doctype/quality_action/quality_action.js @@ -1,6 +1,4 @@ // Copyright (c) 2018, Frappe and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Action', { - -}); +frappe.ui.form.on("Quality Action", {}); diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.js b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.js index 6fb326776ed..8166c257d46 100644 --- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.js +++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.js @@ -1,10 +1,10 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Feedback', { - template: function(frm) { +frappe.ui.form.on("Quality Feedback", { + template: function (frm) { if (frm.doc.template) { - frm.call('set_parameters'); + frm.call("set_parameters"); } - } + }, }); diff --git a/erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.js b/erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.js index 490eed97065..054572a80eb 100644 --- a/erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.js +++ b/erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Feedback Template', { +frappe.ui.form.on("Quality Feedback Template", { // refresh: function(frm) { - // } }); diff --git a/erpnext/quality_management/doctype/quality_goal/quality_goal.js b/erpnext/quality_management/doctype/quality_goal/quality_goal.js index 40cb4d92464..5b7cae5d22a 100644 --- a/erpnext/quality_management/doctype/quality_goal/quality_goal.js +++ b/erpnext/quality_management/doctype/quality_goal/quality_goal.js @@ -1,7 +1,7 @@ // Copyright (c) 2018, Frappe and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Goal', { +frappe.ui.form.on("Quality Goal", { // refresh: function(frm) { // } }); diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.js b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.js index eb7a8c32d73..00ab1e4c37a 100644 --- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.js +++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.js @@ -1,6 +1,4 @@ // Copyright (c) 2018, Frappe and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Meeting', { - -}); +frappe.ui.form.on("Quality Meeting", {}); diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting_list.js b/erpnext/quality_management/doctype/quality_meeting/quality_meeting_list.js index 5fd1b30eb45..ae3112f3d09 100644 --- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting_list.js +++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting_list.js @@ -1,11 +1,10 @@ -frappe.listview_settings['Quality Meeting'] = { +frappe.listview_settings["Quality Meeting"] = { add_fields: ["status"], - get_indicator: function(doc) { - if(doc.status == "Open") { + get_indicator: function (doc) { + if (doc.status == "Open") { return [__("Open"), "red", "status=,Open"]; - } - else if(doc.status == "Close") { + } else if (doc.status == "Close") { return [__("Close"), "green", ",status=,Close"]; } - } + }, }; diff --git a/erpnext/quality_management/doctype/quality_meeting_agenda/quality_meeting_agenda.js b/erpnext/quality_management/doctype/quality_meeting_agenda/quality_meeting_agenda.js index 09989dc643f..ad037c25651 100644 --- a/erpnext/quality_management/doctype/quality_meeting_agenda/quality_meeting_agenda.js +++ b/erpnext/quality_management/doctype/quality_meeting_agenda/quality_meeting_agenda.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Meeting Agenda', { +frappe.ui.form.on("Quality Meeting Agenda", { // refresh: function(frm) { - // } }); diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js index 79fd2ebdbe9..a2c16dddadb 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js @@ -1,23 +1,23 @@ // Copyright (c) 2018, Frappe and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Procedure', { - refresh: function(frm) { - frm.set_query('procedure', 'processes', (frm) =>{ +frappe.ui.form.on("Quality Procedure", { + refresh: function (frm) { + frm.set_query("procedure", "processes", (frm) => { return { filters: { - name: ['not in', [frm.parent_quality_procedure, frm.name]] - } + name: ["not in", [frm.parent_quality_procedure, frm.name]], + }, }; }); - frm.set_query('parent_quality_procedure', function(){ + frm.set_query("parent_quality_procedure", function () { return { filters: { is_group: 1, - name: ['!=', frm.doc.name] - } + name: ["!=", frm.doc.name], + }, }; }); - } + }, }); diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py index 6834abc9d41..5497e8db7ae 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py @@ -54,7 +54,9 @@ class QualityProcedure(NestedSet): for process in self.processes: if process.procedure: - if not frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure"): + if not frappe.db.get_value( + "Quality Procedure", process.procedure, "parent_quality_procedure" + ): frappe.db.set_value( "Quality Procedure", process.procedure, "parent_quality_procedure", self.name ) @@ -66,9 +68,13 @@ class QualityProcedure(NestedSet): if old_child_procedures := set([d.procedure for d in old_doc.processes if d.procedure]): current_child_procedures = set([d.procedure for d in self.processes if d.procedure]) - if removed_child_procedures := list(old_child_procedures.difference(current_child_procedures)): + if removed_child_procedures := list( + old_child_procedures.difference(current_child_procedures) + ): for child_procedure in removed_child_procedures: - frappe.db.set_value("Quality Procedure", child_procedure, "parent_quality_procedure", None) + frappe.db.set_value( + "Quality Procedure", child_procedure, "parent_quality_procedure", None + ) def add_child_to_parent(self): """Add `Child Procedure` to `Parent Procedure`""" @@ -102,7 +108,8 @@ def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=Fa # return the list in order return [ dict( - value=d.procedure, expandable=frappe.db.get_value("Quality Procedure", d.procedure, "is_group") + value=d.procedure, + expandable=frappe.db.get_value("Quality Procedure", d.procedure, "is_group"), ) for d in parent_procedure.processes if d.procedure diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js index 2851fcc5969..25f9a5906a1 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js @@ -1,18 +1,18 @@ frappe.treeview_settings["Quality Procedure"] = { - ignore_fields:["parent_quality_procedure"], - get_tree_nodes: 'erpnext.quality_management.doctype.quality_procedure.quality_procedure.get_children', - add_tree_node: 'erpnext.quality_management.doctype.quality_procedure.quality_procedure.add_node', + ignore_fields: ["parent_quality_procedure"], + get_tree_nodes: "erpnext.quality_management.doctype.quality_procedure.quality_procedure.get_children", + add_tree_node: "erpnext.quality_management.doctype.quality_procedure.quality_procedure.add_node", filters: [ { fieldname: "parent_quality_procedure", fieldtype: "Link", options: "Quality Procedure", label: __("Quality Procedure"), - get_query: function() { + get_query: function () { return { - filters: [["Quality Procedure", 'is_group', '=', 1]] + filters: [["Quality Procedure", "is_group", "=", 1]], }; - } + }, }, ], breadcrumb: "Quality Management", @@ -22,13 +22,13 @@ frappe.treeview_settings["Quality Procedure"] = { menu_items: [ { label: __("New Quality Procedure"), - action: function() { + action: function () { frappe.new_doc("Quality Procedure", true); }, - condition: 'frappe.boot.user.can_create.indexOf("Quality Procedure") !== -1' - } + condition: 'frappe.boot.user.can_create.indexOf("Quality Procedure") !== -1', + }, ], - onload: function(treeview) { + onload: function (treeview) { treeview.make_tree(); }, }; diff --git a/erpnext/quality_management/doctype/quality_review/quality_review.js b/erpnext/quality_management/doctype/quality_review/quality_review.js index 0e6b7034101..504f8fceb2e 100644 --- a/erpnext/quality_management/doctype/quality_review/quality_review.js +++ b/erpnext/quality_management/doctype/quality_review/quality_review.js @@ -1,15 +1,15 @@ // Copyright (c) 2018, Frappe and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Review', { - goal: function(frm) { +frappe.ui.form.on("Quality Review", { + goal: function (frm) { frappe.call({ - "method": "frappe.client.get", + method: "frappe.client.get", args: { doctype: "Quality Goal", - name: frm.doc.goal + name: frm.doc.goal, }, - callback: function(data){ + callback: function (data) { frm.fields_dict.reviews.grid.remove_all(); let objectives = data.message.objectives; for (var i in objectives) { @@ -19,7 +19,7 @@ frappe.ui.form.on('Quality Review', { frm.fields_dict.reviews.get_value()[i].uom = objectives[i].uom; } frm.refresh(); - } + }, }); }, }); diff --git a/erpnext/quality_management/doctype/quality_review/quality_review.py b/erpnext/quality_management/doctype/quality_review/quality_review.py index f691005566d..19aa6273500 100644 --- a/erpnext/quality_management/doctype/quality_review/quality_review.py +++ b/erpnext/quality_management/doctype/quality_review/quality_review.py @@ -47,9 +47,7 @@ def review(): def create_review(goal): goal = frappe.get_doc("Quality Goal", goal) - review = frappe.get_doc( - {"doctype": "Quality Review", "goal": goal.name, "date": frappe.utils.getdate()} - ) + review = frappe.get_doc({"doctype": "Quality Review", "goal": goal.name, "date": frappe.utils.getdate()}) review.insert(ignore_permissions=True) diff --git a/erpnext/quality_management/doctype/quality_review/quality_review_list.js b/erpnext/quality_management/doctype/quality_review/quality_review_list.js index b0be783de56..3be6c10a89d 100644 --- a/erpnext/quality_management/doctype/quality_review/quality_review_list.js +++ b/erpnext/quality_management/doctype/quality_review/quality_review_list.js @@ -1,12 +1,10 @@ -frappe.listview_settings['Quality Review'] = { +frappe.listview_settings["Quality Review"] = { add_fields: ["action"], - get_indicator: function(doc) - { - if(doc.action == "No Action") { + get_indicator: function (doc) { + if (doc.action == "No Action") { return [__("No Action"), "green", "action,=,No Action"]; - } - else if(doc.action == "Action Initialised") { + } else if (doc.action == "Action Initialised") { return [__("Action Initialised"), "red", "action,=,Action Initialised"]; } - } + }, }; diff --git a/erpnext/regional/address_template/setup.py b/erpnext/regional/address_template/setup.py index fd1dfa726b6..684c76620a4 100644 --- a/erpnext/regional/address_template/setup.py +++ b/erpnext/regional/address_template/setup.py @@ -1,5 +1,6 @@ """Import Address Templates from ./templates directory.""" import os + import frappe @@ -26,7 +27,7 @@ def get_address_templates(): def get_file_content(file_name): """Convert 'united_states.html' to '/path/to/united_states.html'.""" full_path = os.path.join(template_dir, file_name) - with open(full_path, "r") as f: + with open(full_path) as f: content = f.read() return content @@ -41,7 +42,7 @@ def get_address_templates(): def update_address_template(country, html, is_default=0): """Update existing Address Template or create a new one.""" if not frappe.db.exists("Country", country): - frappe.log_error("Country {} for regional Address Template does not exist.".format(country)) + frappe.log_error(f"Country {country} for regional Address Template does not exist.") return if frappe.db.exists("Address Template", country): diff --git a/erpnext/regional/address_template/test_regional_address_template.py b/erpnext/regional/address_template/test_regional_address_template.py index 523653b5846..952748b3338 100644 --- a/erpnext/regional/address_template/test_regional_address_template.py +++ b/erpnext/regional/address_template/test_regional_address_template.py @@ -32,7 +32,7 @@ class TestRegionalAddressTemplate(TestCase): """Update an existing Address Template.""" country = ensure_country("Germany") if not frappe.db.exists("Address Template", country.name): - template = frappe.get_doc( + frappe.get_doc( {"doctype": "Address Template", "country": country.name, "template": "EXISTING"} ).insert() diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js index 5918ec8b316..5fbb5cb7e01 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js @@ -1,39 +1,39 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Import Supplier Invoice', { - onload: function(frm) { +frappe.ui.form.on("Import Supplier Invoice", { + onload: function (frm) { frappe.realtime.on("import_invoice_update", function (data) { frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message); if (data.count == data.total) { - window.setTimeout(title => frm.dashboard.hide_progress(title), 1500, data.title); + window.setTimeout((title) => frm.dashboard.hide_progress(title), 1500, data.title); } }); }, - setup: function(frm) { - frm.set_query("tax_account", function(doc) { + setup: function (frm) { + frm.set_query("tax_account", function (doc) { return { filters: { - account_type: 'Tax', - company: doc.company - } + account_type: "Tax", + company: doc.company, + }, }; }); - frm.set_query("default_buying_price_list", function(doc) { + frm.set_query("default_buying_price_list", function (doc) { return { filters: { - currency: frappe.get_doc(":Company", doc.company).default_currency - } + currency: frappe.get_doc(":Company", doc.company).default_currency, + }, }; }); }, - refresh: function(frm) { + refresh: function (frm) { frm.trigger("toggle_read_only_fields"); }, - toggle_read_only_fields: function(frm) { + toggle_read_only_fields: function (frm) { if (in_list(["File Import Completed", "Processing File Data"], frm.doc.status)) { cur_frm.set_read_only(); cur_frm.refresh_fields(); @@ -41,6 +41,5 @@ frappe.ui.form.on('Import Supplier Invoice', { } else { frm.set_df_property("import_invoices", "hidden", 0); } - } - + }, }); diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py index c52685e330a..204e6f483da 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py @@ -77,13 +77,13 @@ class ImportSupplierInvoice(Document): invoices_args["terms"] = get_payment_terms_from_file(file_content) supplier_name = create_supplier(self.supplier_group, supp_dict) - address = create_address(supplier_name, supp_dict) + create_address(supplier_name, supp_dict) pi_name = create_purchase_invoice(supplier_name, file_name, invoices_args, self.name) self.file_count += 1 if pi_name: self.purchase_invoices_count += 1 - file_save = save_file( + save_file( file_name, encoded_content, "Purchase Invoice", @@ -161,7 +161,7 @@ def get_file_content(file_name, zip_file_object): except UnicodeDecodeError: try: content = encoded_content.decode("utf-16") - except UnicodeDecodeError as e: + except UnicodeDecodeError: frappe.log_error("UTF-16 encoding error for File Name: " + file_name) return content @@ -279,7 +279,6 @@ def create_supplier(supplier_group, args): return existing_supplier_name else: - new_supplier = frappe.new_doc("Supplier") new_supplier.supplier_name = re.sub("&", "&", args.supplier) new_supplier.supplier_group = supplier_group @@ -390,7 +389,7 @@ def create_purchase_invoice(supplier_name, file_name, args, name): pi.imported_grand_total = calc_total pi.save() return pi.name - except Exception as e: + except Exception: frappe.db.set_value("Import Supplier Invoice", name, "status", "Error") pi.log_error("Unable to create Puchase Invoice") return None diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js index 72613f4064f..c70973dcc35 100644 --- a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js +++ b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js @@ -1,8 +1,7 @@ // Copyright (c) 2021, Havenir Solutions and contributors // For license information, please see license.txt -frappe.ui.form.on('KSA VAT Sales Account', { +frappe.ui.form.on("KSA VAT Sales Account", { // refresh: function(frm) { - // } }); diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js index 00b62b9adfb..5db1641f3d7 100644 --- a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js +++ b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js @@ -1,8 +1,8 @@ // Copyright (c) 2021, Havenir Solutions and contributors // For license information, please see license.txt -frappe.ui.form.on('KSA VAT Setting', { +frappe.ui.form.on("KSA VAT Setting", { onload: function () { - frappe.breadcrumbs.add('Accounts', 'KSA VAT Setting'); - } + frappe.breadcrumbs.add("Accounts", "KSA VAT Setting"); + }, }); diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js index 269cbec5fb4..39f8584cb72 100644 --- a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js +++ b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js @@ -1,5 +1,5 @@ -frappe.listview_settings['KSA VAT Setting'] = { - onload () { - frappe.breadcrumbs.add('Accounts'); - } -} \ No newline at end of file +frappe.listview_settings["KSA VAT Setting"] = { + onload() { + frappe.breadcrumbs.add("Accounts"); + }, +}; diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js index 8257bf8a969..02eaff53bb9 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js @@ -1,8 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Lower Deduction Certificate', { +frappe.ui.form.on("Lower Deduction Certificate", { // refresh: function(frm) { - // } }); diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json index d332b4e76bd..b94cfe673b6 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json @@ -135,14 +135,51 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-04-18 08:25:35.302081", + "modified": "2024-04-18 15:25:25.808355", "modified_by": "Administrator", "module": "Regional", "name": "Lower Deduction Certificate", "naming_rule": "By fieldname", "owner": "Administrator", - "permissions": [], - "sort_field": "modified", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 diff --git a/erpnext/regional/doctype/product_tax_category/product_tax_category.js b/erpnext/regional/doctype/product_tax_category/product_tax_category.js index 9f8e7950156..ffbdc5d149c 100644 --- a/erpnext/regional/doctype/product_tax_category/product_tax_category.js +++ b/erpnext/regional/doctype/product_tax_category/product_tax_category.js @@ -1,8 +1,7 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Product Tax Category', { +frappe.ui.form.on("Product Tax Category", { // refresh: function(frm) { - // } }); diff --git a/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.js b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.js index e37a61ac853..86384e41b34 100644 --- a/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.js +++ b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.js @@ -1,23 +1,23 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('South Africa VAT Settings', { - refresh: function(frm) { - frm.set_query("company", function() { +frappe.ui.form.on("South Africa VAT Settings", { + refresh: function (frm) { + frm.set_query("company", function () { return { filters: { country: "South Africa", - } + }, }; }); - frm.set_query("account", "vat_accounts", function() { + frm.set_query("account", "vat_accounts", function () { return { filters: { company: frm.doc.company, account_type: "Tax", - is_group: 0 - } + is_group: 0, + }, }; }); - } + }, }); diff --git a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js index 66531412faf..c7d0f054b51 100644 --- a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js +++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js @@ -1,14 +1,14 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('UAE VAT Settings', { - onload: function(frm) { - frm.set_query('account', 'uae_vat_accounts', function() { +frappe.ui.form.on("UAE VAT Settings", { + onload: function (frm) { + frm.set_query("account", "uae_vat_accounts", function () { return { filters: { - 'company': frm.doc.company - } + company: frm.doc.company, + }, }; }); - } + }, }); diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 1f66b361221..23406ea85a6 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -7,10 +7,11 @@ import frappe from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.permissions import add_permission, update_permission_property + from erpnext.regional.italy import ( fiscal_regimes, - tax_exemption_reasons, mode_of_payment_codes, + tax_exemption_reasons, vat_collectability_options, ) diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py index f5b2e2d96b8..1e0a8805075 100644 --- a/erpnext/regional/italy/utils.py +++ b/erpnext/regional/italy/utils.py @@ -39,7 +39,7 @@ def export_invoices(filters=None): attachments = get_e_invoice_attachments(invoices) - zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) + zip_filename = "{}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) download_zip(attachments, zip_filename) @@ -307,7 +307,9 @@ def sales_invoice_validate(doc): for row in doc.taxes: if row.rate == 0 and row.tax_amount == 0 and not row.tax_exemption_reason: frappe.throw( - _("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges").format(row.idx), + _("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges").format( + row.idx + ), title=_("E-Invoicing Information Missing"), ) @@ -338,9 +340,7 @@ def sales_invoice_on_submit(doc, method): _("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx), title=_("E-Invoicing Information Missing"), ) - elif not frappe.db.get_value( - "Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code" - ): + elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"): frappe.throw( _("Row {0}: Please set the correct code on Mode of Payment {1}").format( schedule.idx, schedule.mode_of_payment @@ -473,9 +473,7 @@ def get_progressive_name_and_number(doc, replace=False): filename = attachment.file_name.split(".xml")[0] return filename, filename.split("_")[1] - company_tax_id = ( - doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id - ) + company_tax_id = doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id progressive_name = frappe.model.naming.make_autoname(company_tax_id + "_.#####") progressive_number = progressive_name.split("_")[1] diff --git a/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js b/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js index d7e3ac9a5d3..1512d95a7eb 100644 --- a/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js +++ b/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js @@ -3,51 +3,53 @@ /* eslint-disable */ frappe.query_reports["Electronic Invoice Register"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "width": "80" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + width: "80", }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, { - "fieldname":"customer", - "label": __("Customer"), - "fieldtype": "Link", - "options": "Customer" + fieldname: "customer", + label: __("Customer"), + fieldtype: "Link", + options: "Customer", }, { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), }, ], - "onload": function(reportview) { - reportview.page.add_inner_button(__("Export E-Invoices"), function() { + onload: function (reportview) { + reportview.page.add_inner_button(__("Export E-Invoices"), function () { //TODO: refactor condition to disallow export if report has no data. if (!reportview.data.length) { frappe.msgprint(__("No data to export")); - return + return; } var w = window.open( frappe.urllib.get_full_url( - "/api/method/erpnext.regional.italy.utils.export_invoices?" - + "filters=" + JSON.stringify(reportview.get_filter_values()) + "/api/method/erpnext.regional.italy.utils.export_invoices?" + + "filters=" + + JSON.stringify(reportview.get_filter_values()) ) ); if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; + frappe.msgprint(__("Please enable pop-ups")); + return; } - }) - } -} + }); + }, +}; 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 b85b58f636a..9a373affa29 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 @@ -2,40 +2,40 @@ // For license information, please see license.txt frappe.query_reports["Fichier des Ecritures Comptables [FEC]"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname": "fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1 - } + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + }, ], - onload: function(query_report) { - query_report.page.add_inner_button(__("Export"), function() { + onload: function (query_report) { + query_report.page.add_inner_button(__("Export"), function () { fec_export(query_report); }); - query_report.add_make_chart_button = function() { + query_report.add_make_chart_button = function () { // }; - query_report.export_report = function() { + query_report.export_report = function () { fec_export(query_report); }; - } + }, }; -let fec_export = function(query_report) { +let fec_export = function (query_report) { const fiscal_year = query_report.get_values().fiscal_year; const company = query_report.get_values().company; @@ -46,38 +46,38 @@ let fec_export = function(query_report) { } else { frappe.db.get_value("Fiscal Year", fiscal_year, "year_end_date", (r) => { const fy = r.year_end_date; - const title = company_data + "FEC" + moment(fy).format('YYYYMMDD'); - const column_row = query_report.columns.map(col => col.label); + const title = company_data + "FEC" + moment(fy).format("YYYYMMDD"); + const column_row = query_report.columns.map((col) => col.label); const column_data = query_report.get_data_for_csv(false); const result = [column_row].concat(column_data); downloadify(result, null, title); }); - } }); }; -let downloadify = function(data, roles, title) { +let downloadify = function (data, roles, title) { if (roles && roles.length && !has_common(roles, roles)) { - frappe.msgprint(__("Export not allowed. You need {0} role to export.", [frappe.utils.comma_or(roles)])); + frappe.msgprint( + __("Export not allowed. You need {0} role to export.", [frappe.utils.comma_or(roles)]) + ); return; } const filename = title + ".txt"; let csv_data = to_tab_csv(data); - const a = document.createElement('a'); + const a = document.createElement("a"); if ("download" in a) { // Used Blob object, because it can handle large files let blob_object = new Blob([csv_data], { - type: 'text/csv;charset=UTF-8' + type: "text/csv;charset=UTF-8", }); a.href = URL.createObjectURL(blob_object); a.download = filename; - } else { // use old method - a.href = 'data:attachment/csv,' + encodeURIComponent(csv_data); + a.href = "data:attachment/csv," + encodeURIComponent(csv_data); a.download = filename; a.target = "_blank"; } @@ -88,9 +88,9 @@ let downloadify = function(data, roles, title) { document.body.removeChild(a); }; -let to_tab_csv = function(data) { +let to_tab_csv = function (data) { let res = []; - $.each(data, function(i, row) { + $.each(data, function (i, row) { res.push(row.join("\t")); }); return res.join("\n"); diff --git a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py index 67179890088..1b2bc771f7b 100644 --- a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py +++ b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py @@ -149,12 +149,8 @@ def get_gl_entries(company, fiscal_year): debit = frappe.query_builder.functions.Sum(gle.debit).as_("debit") credit = frappe.query_builder.functions.Sum(gle.credit).as_("credit") - debit_currency = frappe.query_builder.functions.Sum(gle.debit_in_account_currency).as_( - "debitCurr" - ) - credit_currency = frappe.query_builder.functions.Sum(gle.credit_in_account_currency).as_( - "creditCurr" - ) + debit_currency = frappe.query_builder.functions.Sum(gle.debit_in_account_currency).as_("debitCurr") + credit_currency = frappe.query_builder.functions.Sum(gle.credit_in_account_currency).as_("creditCurr") query = ( frappe.qb.from_(gle) @@ -222,25 +218,21 @@ def get_result(company, fiscal_year): result = [] company_currency = frappe.get_cached_value("Company", company, "default_currency") - accounts = frappe.get_all( - "Account", filters={"Company": company}, fields=["name", "account_number"] - ) + accounts = frappe.get_all("Account", filters={"Company": company}, fields=["name", "account_number"]) for d in data: JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0] - if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith( - "{0}/".format(JournalCode) + if d.get("voucher_no").startswith(f"{JournalCode}-") or d.get("voucher_no").startswith( + f"{JournalCode}/" ): EcritureNum = re.split("-|/", d.get("voucher_no"))[1] else: - EcritureNum = re.search(r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE)[1] + EcritureNum = re.search(rf"{JournalCode}(\d+)", d.get("voucher_no"), re.IGNORECASE)[1] EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") - account_number = [ - account.account_number for account in accounts if account.name == d.get("account") - ] + account_number = [account.account_number for account in accounts if account.name == d.get("account")] if account_number[0] is not None: CompteNum = account_number[0] else: diff --git a/erpnext/regional/report/irs_1099/irs_1099.js b/erpnext/regional/report/irs_1099/irs_1099.js index b3508e40a9f..385468b58aa 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.js +++ b/erpnext/regional/report/irs_1099/irs_1099.js @@ -2,33 +2,33 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["IRS 1099"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1, - "width": 80, + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + width: 80, }, { - "fieldname": "fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - "reqd": 1, - "width": 80, + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + width: 80, }, { - "fieldname": "supplier_group", - "label": __("Supplier Group"), - "fieldtype": "Link", - "options": "Supplier Group", - "default": "", - "reqd": 0, - "width": 80 + fieldname: "supplier_group", + label: __("Supplier Group"), + fieldtype: "Link", + options: "Supplier Group", + default: "", + reqd: 0, + width: 80, }, ], @@ -36,12 +36,15 @@ frappe.query_reports["IRS 1099"] = { query_report.page.add_inner_button(__("Print IRS 1099 Forms"), () => { build_1099_print(query_report); }); - } + }, }; function build_1099_print(query_report) { let filters = JSON.stringify(query_report.get_values()); - let w = window.open('/api/method/erpnext.regional.report.irs_1099.irs_1099.irs_1099_print?' + - '&filters=' + encodeURIComponent(filters)); + let w = window.open( + "/api/method/erpnext.regional.report.irs_1099.irs_1099.irs_1099_print?" + + "&filters=" + + encodeURIComponent(filters) + ); // w.print(); } diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index 66ade1f89fb..304a07c7e78 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -34,7 +34,7 @@ def execute(filters=None): conditions += "AND s.supplier_group = %s" % frappe.db.escape(filters.get("supplier_group")) data = frappe.db.sql( - """ + f""" SELECT s.supplier_group as "supplier_group", gl.party AS "supplier", @@ -55,9 +55,7 @@ def execute(filters=None): gl.party ORDER BY - gl.party DESC""".format( - conditions=conditions - ), + gl.party DESC""", {"fiscal_year": filters.fiscal_year, "company": filters.company}, as_dict=True, ) @@ -117,7 +115,7 @@ def irs_1099_print(filters): "Supplier", row.supplier ) row["payments"] = fmt_money(row["payments"], precision=0, currency="USD") - pdf = get_pdf(render_template(template, row), output=output if output else None) + get_pdf(render_template(template, row), output=output if output else None) frappe.local.response.filename = ( f"{filters.fiscal_year} {filters.company} IRS 1099 Forms{IRS_1099_FORMS_FILE_EXTENSION}" diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.js b/erpnext/regional/report/ksa_vat/ksa_vat.js index 59e72c3e638..b21ce6b7f70 100644 --- a/erpnext/regional/report/ksa_vat/ksa_vat.js +++ b/erpnext/regional/report/ksa_vat/ksa_vat.js @@ -4,54 +4,56 @@ frappe.query_reports["KSA VAT"] = { onload() { - frappe.breadcrumbs.add('Accounts'); + frappe.breadcrumbs.add("Accounts"); }, - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.get_today() - } + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.get_today(), + }, ], - "formatter": function(value, row, column, data, default_formatter) { - if (data - && (data.title=='VAT on Sales' || data.title=='VAT on Purchases') - && data.title==value) { + formatter: function (value, row, column, data, default_formatter) { + if ( + data && + (data.title == "VAT on Sales" || data.title == "VAT on Purchases") && + data.title == value + ) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); value = $value.wrap("

              ").parent().html(); - return value - }else if (data.title=='Grand Total'){ - if (data.title==value) { + return value; + } else if (data.title == "Grand Total") { + if (data.title == value) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); value = $value.wrap("

              ").parent().html(); - return value - }else{ + return value; + } else { value = default_formatter(value, row, column, data); value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); value = $value.wrap("

              ").parent().html(); - return value + return value; } - }else{ + } else { value = default_formatter(value, row, column, data); return value; } diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.js b/erpnext/regional/report/uae_vat_201/uae_vat_201.js index 59574247701..ddfa5d42689 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.js +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.js @@ -3,34 +3,37 @@ /* eslint-disable */ frappe.query_reports["UAE VAT 201"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -3), + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -3), }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.get_today(), }, ], - "formatter": function(value, row, column, data, default_formatter) { - if (data - && (data.legend=='VAT on Sales and All Other Outputs' || data.legend=='VAT on Expenses and All Other Inputs') - && data.legend==value) { + formatter: function (value, row, column, data, default_formatter) { + if ( + data && + (data.legend == "VAT on Sales and All Other Outputs" || + data.legend == "VAT on Expenses and All Other Inputs") && + data.legend == value + ) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); value = $value.wrap("

              ").parent().html(); diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 6ef21e52ca1..1b77b3c0dd1 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -62,13 +62,9 @@ def append_vat_on_sales(data, filters): frappe.format(get_reverse_charge_tax(filters), "Currency"), ) - append_data( - data, "4", _("Zero Rated"), frappe.format(get_zero_rated_total(filters), "Currency"), "-" - ) + append_data(data, "4", _("Zero Rated"), frappe.format(get_zero_rated_total(filters), "Currency"), "-") - append_data( - data, "5", _("Exempt Supplies"), frappe.format(get_exempt_total(filters), "Currency"), "-" - ) + append_data(data, "5", _("Exempt Supplies"), frappe.format(get_exempt_total(filters), "Currency"), "-") append_data(data, "", "", "", "") @@ -139,7 +135,7 @@ def get_total_emiratewise(filters): conditions = get_conditions(filters) try: return frappe.db.sql( - """ + f""" select s.vat_emirate as emirate, sum(i.base_net_amount) as total, sum(i.tax_amount) from @@ -148,12 +144,10 @@ def get_total_emiratewise(filters): i.parent = s.name where s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1 - {where_conditions} + {conditions} group by s.vat_emirate; - """.format( - where_conditions=conditions - ), + """, filters, ) except (IndexError, TypeError): @@ -198,7 +192,7 @@ def get_reverse_charge_tax(filters): conditions = get_conditions_join(filters) return ( frappe.db.sql( - """ + f""" select sum(debit) from `tabPurchase Invoice` p inner join `tabGL Entry` gl on @@ -208,10 +202,8 @@ def get_reverse_charge_tax(filters): and p.docstatus = 1 and gl.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) - {where_conditions} ; - """.format( - where_conditions=conditions - ), + {conditions} ; + """, filters, )[0][0] or 0 @@ -240,7 +232,7 @@ def get_reverse_charge_recoverable_tax(filters): conditions = get_conditions_join(filters) return ( frappe.db.sql( - """ + f""" select sum(debit * p.recoverable_reverse_charge / 100) from @@ -253,10 +245,8 @@ def get_reverse_charge_recoverable_tax(filters): and p.recoverable_reverse_charge > 0 and gl.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) - {where_conditions} ; - """.format( - where_conditions=conditions - ), + {conditions} ; + """, filters, )[0][0] or 0 @@ -354,7 +344,7 @@ def get_zero_rated_total(filters): try: return ( frappe.db.sql( - """ + f""" select sum(i.base_net_amount) as total from @@ -363,10 +353,8 @@ def get_zero_rated_total(filters): i.parent = s.name where s.docstatus = 1 and i.is_zero_rated = 1 - {where_conditions} ; - """.format( - where_conditions=conditions - ), + {conditions} ; + """, filters, )[0][0] or 0 @@ -381,7 +369,7 @@ def get_exempt_total(filters): try: return ( frappe.db.sql( - """ + f""" select sum(i.base_net_amount) as total from @@ -390,10 +378,8 @@ def get_exempt_total(filters): i.parent = s.name where s.docstatus = 1 and i.is_exempt = 1 - {where_conditions} ; - """.format( - where_conditions=conditions - ), + {conditions} ; + """, filters, )[0][0] or 0 diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.js b/erpnext/regional/report/vat_audit_report/vat_audit_report.js index 39ef9b563ac..fa8c8a99e07 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.js +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.js @@ -3,29 +3,29 @@ /* eslint-disable */ frappe.query_reports["VAT Audit Report"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -2), - "width": "80" + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -2), + width: "80", }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.get_today() - } - ] + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.get_today(), + }, + ], }; diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 3d486ce6506..718b6c0df31 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -13,7 +13,7 @@ def execute(filters=None): return VATAuditReport(filters).run() -class VATAuditReport(object): +class VATAuditReport: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.columns = [] @@ -58,19 +58,17 @@ class VATAuditReport(object): self.invoices = frappe._dict() invoice_data = frappe.db.sql( - """ + f""" SELECT - {select_columns} + {self.select_columns} FROM `tab{doctype}` WHERE - docstatus = 1 {where_conditions} + docstatus = 1 {conditions} and is_opening = 'No' ORDER BY posting_date DESC - """.format( - select_columns=self.select_columns, doctype=doctype, where_conditions=conditions - ), + """, self.filters, as_dict=1, ) @@ -86,11 +84,10 @@ class VATAuditReport(object): SELECT item_code, parent, base_net_amount, is_zero_rated FROM - `tab%s Item` + `tab{} Item` WHERE - parent in (%s) - """ - % (doctype, ", ".join(["%s"] * len(self.invoices))), + parent in ({}) + """.format(doctype, ", ".join(["%s"] * len(self.invoices))), tuple(self.invoices), as_dict=1, ) @@ -111,15 +108,14 @@ class VATAuditReport(object): SELECT parent, account_head, item_wise_tax_detail FROM - `tab%s` + `tab{}` WHERE - parenttype = %s and docstatus = 1 - and parent in (%s) + parenttype = {} and docstatus = 1 + and parent in ({}) ORDER BY account_head - """ - % (self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))), - tuple([doctype] + list(self.invoices.keys())), + """.format(self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))), + tuple([doctype, *list(self.invoices.keys())]), ) for parent, account, item_wise_tax_detail in self.tax_details: diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 7f41c462cce..c6ba28c5993 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -2,11 +2,12 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.permissions import add_permission, update_permission_property + from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import ( create_ksa_vat_setting, ) -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def setup(company=None, patch=True): diff --git a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py index 66d9df224e7..cf4f6e81b7b 100644 --- a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py +++ b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py @@ -10,7 +10,7 @@ def create_ksa_vat_setting(company): company = frappe.get_doc("Company", company) file_path = os.path.join(os.path.dirname(__file__), "..", "data", "ksa_vat_settings.json") - with open(file_path, "r") as json_file: + with open(file_path) as json_file: account_data = json.load(json_file) # Creating KSA VAT Setting diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index efeaeed324c..d71b87bd903 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -25,7 +25,7 @@ def update_itemised_tax_data(doc): # dont even bother checking in item tax template as it contains both input and output accounts - double the tax rate item_code = row.item_code or row.item_name if itemised_tax.get(item_code): - for tax in itemised_tax.get(row.item_code).values(): + for tax in itemised_tax.get(item_code).values(): _tax_rate = flt(tax.get("tax_rate", 0), row.precision("tax_rate")) tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate @@ -55,14 +55,12 @@ def get_account_currency(account): def get_tax_accounts(company): """Get the list of tax accounts for a specific company.""" tax_accounts_dict = frappe._dict() - tax_accounts_list = frappe.get_all( - "UAE VAT Account", filters={"parent": company}, fields=["Account"] - ) + tax_accounts_list = frappe.get_all("UAE VAT Account", filters={"parent": company}, fields=["Account"]) if not tax_accounts_list and not frappe.flags.in_test: frappe.throw(_('Please set Vat Accounts for Company: "{0}" in UAE VAT Settings').format(company)) for tax_account in tax_accounts_list: - for account, name in tax_account.items(): + for _account, name in tax_account.items(): tax_accounts_dict[name] = name return tax_accounts_dict @@ -106,7 +104,6 @@ def update_totals(vat_tax, base_vat_tax, doc): doc.grand_total -= vat_tax if doc.meta.get_field("rounded_total"): - if doc.is_rounded_total_disabled(): doc.outstanding_amount = doc.grand_total @@ -120,9 +117,7 @@ def update_totals(vat_tax, base_vat_tax, doc): doc.outstanding_amount = doc.rounded_total or doc.grand_total doc.in_words = money_in_words(doc.grand_total, doc.currency) - doc.base_in_words = money_in_words( - doc.base_grand_total, erpnext.get_company_currency(doc.company) - ) + doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company)) doc.set_payment_schedule() diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index 47f24ef4e72..cb139e6ee86 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -1,10 +1,8 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt + import frappe -import os -import json -from frappe.permissions import add_permission, update_permission_property from frappe.custom.doctype.custom_field.custom_field import create_custom_fields diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index 83ba6ed3ad1..e54552b0332 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -10,7 +10,6 @@ from erpnext.regional.report.irs_1099.irs_1099 import execute as execute_1099_re class TestUnitedStates(unittest.TestCase): def test_irs_1099_custom_field(self): - if not frappe.db.exists("Supplier", "_US 1099 Test Supplier"): doc = frappe.new_doc("Supplier") doc.supplier_name = "_US 1099 Test Supplier" @@ -38,7 +37,6 @@ class TestUnitedStates(unittest.TestCase): def make_payment_entry_to_irs_1099_supplier(): - frappe.db.sql("delete from `tabGL Entry` where party='_US 1099 Test Supplier'") frappe.db.sql("delete from `tabGL Entry` where against='_US 1099 Test Supplier'") frappe.db.sql("delete from `tabPayment Entry` where party='_US 1099 Test Supplier'") diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 432abfce73b..1507d8e923f 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -2,147 +2,172 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Customer", { - setup: function(frm) { - + setup: function (frm) { frm.make_methods = { - 'Quotation': () => frappe.model.open_mapped_doc({ - method: "erpnext.selling.doctype.customer.customer.make_quotation", - frm: cur_frm - }), - 'Opportunity': () => frappe.model.open_mapped_doc({ - method: "erpnext.selling.doctype.customer.customer.make_opportunity", - frm: cur_frm - }) - } + Quotation: () => + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.customer.customer.make_quotation", + frm: cur_frm, + }), + Opportunity: () => + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.customer.customer.make_opportunity", + frm: cur_frm, + }), + }; - frm.add_fetch('lead_name', 'company_name', 'customer_name'); - frm.add_fetch('default_sales_partner','commission_rate','default_commission_rate'); - frm.set_query('customer_group', {'is_group': 0}); - frm.set_query('default_price_list', { 'selling': 1}); - frm.set_query('account', 'accounts', function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; + frm.add_fetch("lead_name", "company_name", "customer_name"); + frm.add_fetch("default_sales_partner", "commission_rate", "default_commission_rate"); + frm.set_query("customer_group", { is_group: 0 }); + frm.set_query("default_price_list", { selling: 1 }); + frm.set_query("account", "accounts", function (doc, cdt, cdn) { + var d = locals[cdt][cdn]; var filters = { - 'account_type': 'Receivable', - 'company': d.company, - "is_group": 0 + account_type: "Receivable", + company: d.company, + is_group: 0, }; - if(doc.party_account_currency) { - $.extend(filters, {"account_currency": doc.party_account_currency}); + if (doc.party_account_currency) { + $.extend(filters, { account_currency: doc.party_account_currency }); } return { - filters: filters - } + filters: filters, + }; }); if (frm.doc.__islocal == 1) { frm.set_value("represents_company", ""); } - frm.set_query('customer_primary_contact', function(doc) { + frm.set_query("customer_primary_contact", function (doc) { return { query: "erpnext.selling.doctype.customer.customer.get_customer_primary_contact", filters: { - 'customer': doc.name - } - } - }) - frm.set_query('customer_primary_address', function(doc) { + customer: doc.name, + }, + }; + }); + frm.set_query("customer_primary_address", function (doc) { return { filters: { - 'link_doctype': 'Customer', - 'link_name': doc.name - } - } - }) + link_doctype: "Customer", + link_name: doc.name, + }, + }; + }); - frm.set_query('default_bank_account', function() { + frm.set_query("default_bank_account", function () { return { filters: { - 'is_company_account': 1 - } - } + is_company_account: 1, + }, + }; }); }, - customer_primary_address: function(frm){ - if(frm.doc.customer_primary_address){ + customer_primary_address: function (frm) { + if (frm.doc.customer_primary_address) { frappe.call({ - method: 'frappe.contacts.doctype.address.address.get_address_display', + method: "frappe.contacts.doctype.address.address.get_address_display", args: { - "address_dict": frm.doc.customer_primary_address + address_dict: frm.doc.customer_primary_address, }, - callback: function(r) { + callback: function (r) { frm.set_value("primary_address", r.message); - } + }, }); } - if(!frm.doc.customer_primary_address){ + if (!frm.doc.customer_primary_address) { frm.set_value("primary_address", ""); } }, - is_internal_customer: function(frm) { + is_internal_customer: function (frm) { if (frm.doc.is_internal_customer == 1) { frm.toggle_reqd("represents_company", true); - } - else { + } else { frm.toggle_reqd("represents_company", false); } }, - customer_primary_contact: function(frm){ - if(!frm.doc.customer_primary_contact){ + customer_primary_contact: function (frm) { + if (!frm.doc.customer_primary_contact) { frm.set_value("mobile_no", ""); frm.set_value("email_id", ""); } }, - loyalty_program: function(frm) { - if(frm.doc.loyalty_program) { - frm.set_value('loyalty_program_tier', null); + loyalty_program: function (frm) { + if (frm.doc.loyalty_program) { + frm.set_value("loyalty_program_tier", null); } }, - refresh: function(frm) { - if(frappe.defaults.get_default("cust_master_name")!="Naming Series") { + refresh: function (frm) { + if (frappe.defaults.get_default("cust_master_name") != "Naming Series") { frm.toggle_display("naming_series", false); } else { erpnext.toggle_naming_series(); } - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Customer'} + frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: "Customer" }; - if(!frm.doc.__islocal) { + if (!frm.doc.__islocal) { frappe.contacts.render_address_and_contact(frm); // custom buttons - frm.add_custom_button(__('Accounts Receivable'), function () { - frappe.set_route('query-report', 'Accounts Receivable', { party_type: "Customer", party: frm.doc.name }); - }, __('View')); + frm.add_custom_button( + __("Accounts Receivable"), + function () { + frappe.set_route("query-report", "Accounts Receivable", { + party_type: "Customer", + party: frm.doc.name, + }); + }, + __("View") + ); - frm.add_custom_button(__('Accounting Ledger'), function () { - frappe.set_route('query-report', 'General Ledger', - {party_type: 'Customer', party: frm.doc.name, party_name: frm.doc.customer_name}); - }, __('View')); + frm.add_custom_button( + __("Accounting Ledger"), + function () { + frappe.set_route("query-report", "General Ledger", { + party_type: "Customer", + party: frm.doc.name, + party_name: frm.doc.customer_name, + }); + }, + __("View") + ); - frm.add_custom_button(__('Pricing Rule'), function () { - erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); - }, __('Create')); + frm.add_custom_button( + __("Pricing Rule"), + function () { + erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); + }, + __("Create") + ); - frm.add_custom_button(__('Get Customer Group Details'), function () { - frm.trigger("get_customer_group_details"); - }, __('Actions')); + frm.add_custom_button( + __("Get Customer Group Details"), + function () { + frm.trigger("get_customer_group_details"); + }, + __("Actions") + ); if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) { - frm.add_custom_button(__('Link with Supplier'), function () { - frm.trigger('show_party_link_dialog'); - }, __('Actions')); + frm.add_custom_button( + __("Link with Supplier"), + function () { + frm.trigger("show_party_link_dialog"); + }, + __("Actions") + ); } // indicator erpnext.utils.set_party_dashboard_indicators(frm); - } else { frappe.contacts.clear_address_and_contact(frm); } @@ -151,55 +176,58 @@ frappe.ui.form.on("Customer", { grid.set_column_disp("allocated_amount", false); grid.set_column_disp("incentives", false); }, - validate: function(frm) { - if(frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name); - + validate: function (frm) { + if (frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name); }, - get_customer_group_details: function(frm) { + get_customer_group_details: function (frm) { frappe.call({ method: "get_customer_group_details", doc: frm.doc, - callback: function() { + callback: function () { frm.refresh(); - } + }, }); - }, - show_party_link_dialog: function(frm) { + show_party_link_dialog: function (frm) { const dialog = new frappe.ui.Dialog({ - title: __('Select a Supplier'), - fields: [{ - fieldtype: 'Link', label: __('Supplier'), - options: 'Supplier', fieldname: 'supplier', reqd: 1 - }], - primary_action: function({ supplier }) { + title: __("Select a Supplier"), + fields: [ + { + fieldtype: "Link", + label: __("Supplier"), + options: "Supplier", + fieldname: "supplier", + reqd: 1, + }, + ], + primary_action: function ({ supplier }) { frappe.call({ - method: 'erpnext.accounts.doctype.party_link.party_link.create_party_link', + method: "erpnext.accounts.doctype.party_link.party_link.create_party_link", args: { - primary_role: 'Customer', + primary_role: "Customer", primary_party: frm.doc.name, - secondary_party: supplier + secondary_party: supplier, }, freeze: true, - callback: function() { + callback: function () { dialog.hide(); frappe.msgprint({ - message: __('Successfully linked to Supplier'), - alert: true + message: __("Successfully linked to Supplier"), + alert: true, }); }, - error: function() { + error: function () { dialog.hide(); frappe.msgprint({ - message: __('Linking to Supplier Failed. Please try again.'), - title: __('Linking Failed'), - indicator: 'red' + message: __("Linking to Supplier Failed. Please try again."), + title: __("Linking Failed"), + indicator: "red", }); - } + }, }); }, - primary_action_label: __('Create Link') + primary_action_label: __("Create Link"), }); dialog.show(); - } + }, }); diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index c3c7766b9f5..9844e0902c4 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -447,7 +447,6 @@ "report_hide": 1 }, { - "default": "0", "fieldname": "credit_limits", "fieldtype": "Table", "label": "Credit Limit", @@ -568,7 +567,7 @@ "link_fieldname": "party" } ], - "modified": "2023-10-19 16:56:27.327035", + "modified": "2023-12-28 13:15:36.298369", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 4bbce9b2f00..182e10a4765 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -18,9 +18,8 @@ from frappe.model.rename_doc import update_linked_doctypes from frappe.utils import cint, cstr, flt, get_formatted_email, today from frappe.utils.user import get_users_with_role -from erpnext.accounts.party import ( # noqa +from erpnext.accounts.party import ( get_dashboard_info, - get_timeline_data, validate_party_accounts, ) from erpnext.utilities.transaction_base import TransactionBase @@ -49,17 +48,16 @@ class Customer(TransactionBase): self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) def get_customer_name(self): - if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import: count = frappe.db.sql( """select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer where name like %s""", - "%{0} - %".format(self.customer_name), + f"%{self.customer_name} - %", as_list=1, )[0][0] count = cint(count) + 1 - new_customer_name = "{0} - {1}".format(self.customer_name, cstr(count)) + new_customer_name = f"{self.customer_name} - {cstr(count)}" msgprint( _("Changed customer name to '{}' as '{}' already exists.").format( @@ -171,6 +169,7 @@ class Customer(TransactionBase): if self.flags.is_new_doc: self.link_lead_address_and_contact() + self.copy_communication() self.update_customer_groups() @@ -224,6 +223,17 @@ class Customer(TransactionBase): linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name)) linked_doc.save(ignore_permissions=self.flags.ignore_permissions) + def copy_communication(self): + if not self.lead_name or not frappe.db.get_single_value( + "CRM Settings", "carry_forward_communication_and_comments" + ): + return + + from erpnext.crm.utils import copy_comments, link_communications + + copy_comments("Lead", self.lead_name, self) + link_communications("Lead", self.lead_name, self) + def validate_name_with_customer_group(self): if frappe.db.exists("Customer Group", self.name): frappe.throw( @@ -247,9 +257,7 @@ class Customer(TransactionBase): ) ] - current_credit_limits = [ - d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company) - ] + current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] if past_credit_limits == current_credit_limits: return @@ -430,9 +438,7 @@ def get_loyalty_programs(doc): ) and ( not loyalty_program.customer_territory or doc.territory - in get_nested_links( - "Territory", loyalty_program.customer_territory, doc.flags.ignore_permissions - ) + in get_nested_links("Territory", loyalty_program.customer_territory, doc.flags.ignore_permissions) ): lp_details.append(loyalty_program.name) @@ -468,14 +474,14 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): fields = get_fields("Customer", fields) match_conditions = build_match_conditions("Customer") - match_conditions = "and {}".format(match_conditions) if match_conditions else "" + match_conditions = f"and {match_conditions}" if match_conditions else "" if filters: filter_conditions = get_filters_cond(doctype, filters, []) - match_conditions += "{}".format(filter_conditions) + match_conditions += f"{filter_conditions}" return frappe.db.sql( - """ + f""" select %s from `tabCustomer` where docstatus < 2 @@ -485,9 +491,7 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): case when name like %s then 0 else 1 end, case when customer_name like %s then 0 else 1 end, name, customer_name limit %s, %s - """.format( - match_conditions=match_conditions - ) + """ % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len), ) @@ -522,12 +526,12 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, ] if not credit_controller_users_formatted: frappe.throw( - _("Please contact your administrator to extend the credit limits for {0}.").format(customer) + _("Please contact your administrator to extend the credit limits for {0}.").format( + customer + ) ) - user_list = "

              • {0}
              ".format( - "
            • ".join(credit_controller_users_formatted) - ) + user_list = "

              • {}
              ".format("
            • ".join(credit_controller_users_formatted)) message = _( "Please contact any of the following users to extend the credit limits for {0}: {1}" @@ -560,33 +564,25 @@ def send_emails(args): message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format( args.get("customer"), args.get("customer_outstanding"), args.get("credit_limit") ) - frappe.sendmail( - recipients=args.get("credit_controller_users_list"), subject=subject, message=message - ) + frappe.sendmail(recipients=args.get("credit_controller_users_list"), subject=subject, message=message) -def get_customer_outstanding( - customer, company, ignore_outstanding_sales_order=False, cost_center=None -): +def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): # Outstanding based on GL Entries cond = "" if cost_center: lft, rgt = frappe.get_cached_value("Cost Center", cost_center, ["lft", "rgt"]) - cond = """ and cost_center in (select name from `tabCost Center` where - lft >= {0} and rgt <= {1})""".format( - lft, rgt - ) + cond = f""" and cost_center in (select name from `tabCost Center` where + lft >= {lft} and rgt <= {rgt})""" outstanding_based_on_gle = frappe.db.sql( - """ + f""" select sum(debit) - sum(credit) from `tabGL Entry` where party_type = 'Customer' and is_cancelled = 0 and party = %s - and company=%s {0}""".format( - cond - ), + and company=%s {cond}""", (customer, company), ) @@ -690,8 +686,12 @@ def make_contact(args, is_primary_contact=1): "is_primary_contact": is_primary_contact, "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], } - if args.customer_type == "Individual": - first, middle, last = parse_full_name(args.get("customer_name")) + + party_type = args.customer_type if args.doctype == "Customer" else args.supplier_type + party_name_key = "customer_name" if args.doctype == "Customer" else "supplier_name" + + if party_type == "Individual": + first, middle, last = parse_full_name(args.get(party_name_key)) values.update( { "first_name": first, @@ -702,9 +702,13 @@ def make_contact(args, is_primary_contact=1): else: values.update( { - "company_name": args.get("customer_name"), + "first_name": args.get("customer_name") + if args.doctype == "Customer" + else args.get("supplier_name"), + "company_name": args.get(party_name_key), } ) + contact = frappe.get_doc(values) if args.get("email_id"): @@ -729,14 +733,16 @@ def make_address(args, is_primary_address=1, is_shipping_address=1): if reqd_fields: msg = _("Following fields are mandatory to create address:") frappe.throw( - "{0}

                {1}
              ".format(msg, "\n".join(reqd_fields)), + "{}

                {}
              ".format(msg, "\n".join(reqd_fields)), title=_("Missing Values Required"), ) + party_name_key = "customer_name" if args.doctype == "Customer" else "supplier_name" + address = frappe.get_doc( { "doctype": "Address", - "address_title": args.get("customer_name"), + "address_title": args.get(party_name_key), "address_line1": args.get("address_line1"), "address_line2": args.get("address_line2"), "city": args.get("city"), diff --git a/erpnext/selling/doctype/customer/customer_list.js b/erpnext/selling/doctype/customer/customer_list.js index 38fc9ad1f13..6149c6f873d 100644 --- a/erpnext/selling/doctype/customer/customer_list.js +++ b/erpnext/selling/doctype/customer/customer_list.js @@ -1,3 +1,3 @@ -frappe.listview_settings['Customer'] = { +frappe.listview_settings["Customer"] = { add_fields: ["customer_name", "territory", "customer_group", "customer_type", "image"], }; diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7a601a78876..61e5d5e457c 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -268,7 +268,6 @@ class TestCustomer(FrappeTestCase): def test_customer_credit_limit(self): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice - from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note @@ -297,11 +296,35 @@ class TestCustomer(FrappeTestCase): if credit_limit > outstanding_amt: set_credit_limit("_Test Customer", "_Test Company", credit_limit) - # Makes Sales invoice from Sales Order - so.save(ignore_permissions=True) - si = make_sales_invoice(so.name) - si.save(ignore_permissions=True) - self.assertRaises(frappe.ValidationError, make_sales_order) + def test_customer_credit_limit_after_submit(self): + from erpnext.controllers.accounts_controller import update_child_qty_rate + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + outstanding_amt = self.get_customer_outstanding_amount() + credit_limit = get_credit_limit("_Test Customer", "_Test Company") + + if outstanding_amt <= 0.0: + item_qty = int((abs(outstanding_amt) + 200) / 100) + make_sales_order(qty=item_qty) + + if credit_limit <= 0.0: + set_credit_limit("_Test Customer", "_Test Company", outstanding_amt + 100) + + so = make_sales_order(rate=100, qty=1) + # Update qty in submitted Sales Order to trigger Credit Limit validation + fields = ["name", "item_code", "delivery_date", "conversion_factor", "qty", "rate", "uom", "idx"] + modified_item = frappe._dict() + for x in fields: + modified_item[x] = so.items[0].get(x) + modified_item["docname"] = so.items[0].name + modified_item["qty"] = 2 + self.assertRaises( + frappe.ValidationError, + update_child_qty_rate, + so.doctype, + frappe.json.dumps([modified_item]), + so.name, + ) def test_customer_credit_limit_on_change(self): outstanding_amt = self.get_customer_outstanding_amount() @@ -419,17 +442,13 @@ def set_credit_limit(customer, company, credit_limit): customer.credit_limits[-1].db_insert() -def create_internal_customer( - customer_name=None, represents_company=None, allowed_to_interact_with=None -): +def create_internal_customer(customer_name=None, represents_company=None, allowed_to_interact_with=None): if not customer_name: customer_name = represents_company if not allowed_to_interact_with: allowed_to_interact_with = represents_company - exisiting_representative = frappe.db.get_value( - "Customer", {"represents_company": represents_company} - ) + exisiting_representative = frappe.db.get_value("Customer", {"represents_company": represents_company}) if exisiting_representative: return exisiting_representative diff --git a/erpnext/selling/doctype/industry_type/industry_type.js b/erpnext/selling/doctype/industry_type/industry_type.js index 3680906057f..273e30fd197 100644 --- a/erpnext/selling/doctype/industry_type/industry_type.js +++ b/erpnext/selling/doctype/industry_type/industry_type.js @@ -1,13 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - - //--------- ONLOAD ------------- -cur_frm.cscript.onload = function(doc, cdt, cdn) { +cur_frm.cscript.onload = function (doc, cdt, cdn) {}; -} - -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} +cur_frm.cscript.refresh = function (doc, cdt, cdn) {}; diff --git a/erpnext/selling/doctype/installation_note/installation_note.js b/erpnext/selling/doctype/installation_note/installation_note.js index 27a3b35ccfb..f26b40bdb04 100644 --- a/erpnext/selling/doctype/installation_note/installation_note.js +++ b/erpnext/selling/doctype/installation_note/installation_note.js @@ -1,30 +1,30 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Installation Note', { - setup: function(frm) { - frappe.dynamic_link = {doc: frm.doc, fieldname: 'customer', doctype: 'Customer'} - frm.set_query('customer_address', erpnext.queries.address_query); - frm.set_query('contact_person', erpnext.queries.contact_query); - frm.set_query('customer', erpnext.queries.customer); +frappe.ui.form.on("Installation Note", { + setup: function (frm) { + frappe.dynamic_link = { doc: frm.doc, fieldname: "customer", doctype: "Customer" }; + frm.set_query("customer_address", erpnext.queries.address_query); + frm.set_query("contact_person", erpnext.queries.contact_query); + frm.set_query("customer", erpnext.queries.customer); }, - onload: function(frm) { - if(!frm.doc.status) { - frm.set_value({ status:'Draft'}); + onload: function (frm) { + if (!frm.doc.status) { + frm.set_value({ status: "Draft" }); } - if(frm.doc.__islocal) { - frm.set_value({inst_date: frappe.datetime.get_today()}); + if (frm.doc.__islocal) { + frm.set_value({ inst_date: frappe.datetime.get_today() }); } }, - customer: function(frm) { + customer: function (frm) { erpnext.utils.get_party_details(frm); }, - customer_address: function(frm) { + customer_address: function (frm) { erpnext.utils.get_address_display(frm); }, - contact_person: function(frm) { + contact_person: function (frm) { erpnext.utils.get_contact_details(frm); - } + }, }); frappe.provide("erpnext.selling"); @@ -33,9 +33,10 @@ frappe.provide("erpnext.selling"); erpnext.selling.InstallationNote = class InstallationNote extends frappe.ui.form.Controller { refresh() { var me = this; - if (this.frm.doc.docstatus===0) { - this.frm.add_custom_button(__('From Delivery Note'), - function() { + if (this.frm.doc.docstatus === 0) { + this.frm.add_custom_button( + __("From Delivery Note"), + function () { erpnext.utils.map_current_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_installation_note", source_doctype: "Delivery Note", @@ -48,13 +49,15 @@ erpnext.selling.InstallationNote = class InstallationNote extends frappe.ui.form docstatus: 1, status: ["not in", ["Stopped", "Closed"]], per_installed: ["<", 99.99], - company: me.frm.doc.company - } - }) - }, "fa fa-download", "btn-default" + company: me.frm.doc.company, + }, + }); + }, + "fa fa-download", + "btn-default" ); } } }; -extend_cscript(cur_frm.cscript, new erpnext.selling.InstallationNote({frm: cur_frm})); +extend_cscript(cur_frm.cscript, new erpnext.selling.InstallationNote({ frm: cur_frm })); diff --git a/erpnext/selling/doctype/installation_note/installation_note.py b/erpnext/selling/doctype/installation_note/installation_note.py index 0ef4754bc68..c2464cbcb61 100644 --- a/erpnext/selling/doctype/installation_note/installation_note.py +++ b/erpnext/selling/doctype/installation_note/installation_note.py @@ -12,7 +12,7 @@ from erpnext.utilities.transaction_base import TransactionBase class InstallationNote(TransactionBase): def __init__(self, *args, **kwargs): - super(InstallationNote, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.status_updater = [ { "source_dt": "Installation Note Item", diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.js b/erpnext/selling/doctype/party_specific_item/party_specific_item.js index 077b93631ec..0decc70d7ac 100644 --- a/erpnext/selling/doctype/party_specific_item/party_specific_item.js +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.js @@ -1,8 +1,7 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Party Specific Item', { +frappe.ui.form.on("Party Specific Item", { // refresh: function(frm) { - // } }); diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 3d4ffebbfb4..fe4933ccdd9 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -49,7 +49,7 @@ class ProductBundle(Document): if len(invoice_links): frappe.throw( - "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle".format( + "This Product Bundle is linked with {}. You will have to cancel these documents in order to delete this Product Bundle".format( ", ".join(invoice_links) ), title=_("Not Allowed"), @@ -81,9 +81,7 @@ def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): query = ( frappe.qb.from_(item) .select(item.item_code, item.item_name) - .where( - (item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%")) - ) + .where((item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%"))) .limit(page_len) .offset(start) ) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 2ffa6a5c120..40fa986951e 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -556,7 +556,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1072,7 +1072,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "modified": "2023-04-14 16:50:44.550098", + "modified": "2024-03-20 16:04:21.567847", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 88e24ad542a..bd23440aaf0 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -22,9 +22,10 @@ class Quotation(SellingController): self.indicator_title = "Expired" def validate(self): - super(Quotation, self).validate() + super().validate() self.set_status() - self.validate_uom_is_integer("stock_uom", "qty") + self.validate_uom_is_integer("stock_uom", "stock_qty") + self.validate_uom_is_integer("uom", "qty") self.validate_valid_till() self.validate_shopping_cart_items() self.set_customer_name() @@ -105,7 +106,8 @@ class Quotation(SellingController): def is_in_sales_order(row): in_sales_order = bool( frappe.db.exists( - "Sales Order Item", {"quotation_item": row.name, "item_code": row.item_code, "docstatus": 1} + "Sales Order Item", + {"quotation_item": row.name, "item_code": row.item_code, "docstatus": 1}, ) ) return in_sales_order @@ -195,7 +197,7 @@ class Quotation(SellingController): def on_cancel(self): if self.lost_reasons: self.lost_reasons = [] - super(Quotation, self).on_cancel() + super().on_cancel() # update enquiry status self.set_status(update=True) @@ -288,18 +290,18 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ) # sales team - for d in customer.get("sales_team") or []: - target.append( - "sales_team", - { - "sales_person": d.sales_person, - "allocated_percentage": d.allocated_percentage or None, - "commission_rate": d.commission_rate, - }, - ) + if not target.get("sales_team"): + for d in customer.get("sales_team") or []: + target.append( + "sales_team", + { + "sales_person": d.sales_person, + "allocated_percentage": d.allocated_percentage or None, + "commission_rate": d.commission_rate, + }, + ) target.flags.ignore_permissions = ignore_permissions - target.delivery_date = nowdate() target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -307,7 +309,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0) target.qty = balance_qty if balance_qty > 0 else 0 target.stock_qty = flt(target.qty) * flt(obj.conversion_factor) - target.delivery_date = nowdate() if obj.against_blanket_order: target.against_blanket_order = obj.against_blanket_order @@ -370,12 +371,8 @@ def set_expired_status(): # if not exists any SO, set status as Expired frappe.db.multisql( { - "mariadb": """UPDATE `tabQuotation` SET `tabQuotation`.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""".format( - cond=cond, so_against_quo=so_against_quo - ), - "postgres": """UPDATE `tabQuotation` SET status = 'Expired' FROM `tabSales Order`, `tabSales Order Item` WHERE {cond} and not exists({so_against_quo})""".format( - cond=cond, so_against_quo=so_against_quo - ), + "mariadb": f"""UPDATE `tabQuotation` SET `tabQuotation`.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""", + "postgres": f"""UPDATE `tabQuotation` SET status = 'Expired' FROM `tabSales Order`, `tabSales Order Item` WHERE {cond} and not exists({so_against_quo})""", }, (nowdate()), ) @@ -463,7 +460,8 @@ def _make_customer(source_name, ignore_permissions=False): frappe.local.message_log = [] lead_link = frappe.utils.get_link_to_form("Lead", lead_name) message = ( - _("Could not auto create Customer due to the following missing mandatory field(s):") + "
              " + _("Could not auto create Customer due to the following missing mandatory field(s):") + + "
              " ) message += "
              • " + "
              • ".join(mandatory_fields) + "
              " message += _("Please create Customer from Lead {0}.").format(lead_link) diff --git a/erpnext/selling/doctype/quotation/quotation_list.js b/erpnext/selling/doctype/quotation/quotation_list.js index 32fce1f2ad8..ae744b9cba3 100644 --- a/erpnext/selling/doctype/quotation/quotation_list.js +++ b/erpnext/selling/doctype/quotation/quotation_list.js @@ -1,38 +1,37 @@ -frappe.listview_settings['Quotation'] = { - add_fields: ["customer_name", "base_grand_total", "status", - "company", "currency", 'valid_till'], +frappe.listview_settings["Quotation"] = { + add_fields: ["customer_name", "base_grand_total", "status", "company", "currency", "valid_till"], - onload: function(listview) { + onload: function (listview) { if (listview.page.fields_dict.quotation_to) { - listview.page.fields_dict.quotation_to.get_query = function() { + listview.page.fields_dict.quotation_to.get_query = function () { return { - "filters": { - "name": ["in", ["Customer", "Lead"]], - } + filters: { + name: ["in", ["Customer", "Lead"]], + }, }; }; } - listview.page.add_action_item(__("Sales Order"), ()=>{ + listview.page.add_action_item(__("Sales Order"), () => { erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order"); }); - listview.page.add_action_item(__("Sales Invoice"), ()=>{ + listview.page.add_action_item(__("Sales Invoice"), () => { erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice"); }); }, - get_indicator: function(doc) { - if(doc.status==="Open") { + get_indicator: function (doc) { + if (doc.status === "Open") { return [__("Open"), "orange", "status,=,Open"]; - } else if (doc.status==="Partially Ordered") { + } else if (doc.status === "Partially Ordered") { return [__("Partially Ordered"), "yellow", "status,=,Partially Ordered"]; - } else if(doc.status==="Ordered") { + } else if (doc.status === "Ordered") { return [__("Ordered"), "green", "status,=,Ordered"]; - } else if(doc.status==="Lost") { + } else if (doc.status === "Lost") { return [__("Lost"), "gray", "status,=,Lost"]; - } else if(doc.status==="Expired") { + } else if (doc.status === "Expired") { return [__("Expired"), "gray", "status,=,Expired"]; } - } + }, }; diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 5623a12cdda..4219d0698ca 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -30,6 +30,39 @@ class TestQuotation(FrappeTestCase): self.assertTrue(sales_order.get("payment_schedule")) + def test_gross_profit(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + from erpnext.stock.get_item_details import insert_item_price + + item_doc = make_item("_Test Item for Gross Profit", {"is_stock_item": 1}) + item_code = item_doc.name + make_stock_entry(item_code=item_code, qty=10, rate=100, target="_Test Warehouse - _TC") + + selling_price_list = frappe.get_all("Price List", filters={"selling": 1}, limit=1)[0].name + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1) + insert_item_price( + frappe._dict( + { + "item_code": item_code, + "price_list": selling_price_list, + "price_list_rate": 300, + "rate": 300, + "conversion_factor": 1, + "discount_amount": 0.0, + "currency": frappe.db.get_value("Price List", selling_price_list, "currency"), + "uom": item_doc.stock_uom, + } + ) + ) + + quotation = make_quotation( + item_code=item_code, qty=1, rate=300, selling_price_list=selling_price_list + ) + self.assertEqual(quotation.items[0].valuation_rate, 100) + self.assertEqual(quotation.items[0].gross_profit, 200) + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) + def test_maintain_rate_in_sales_cycle_is_enforced(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order @@ -87,9 +120,9 @@ class TestQuotation(FrappeTestCase): self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name) self.assertEqual(sales_order.customer, "_Test Customer") - sales_order.delivery_date = "2014-01-01" sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = nowdate() + sales_order.delivery_date = nowdate() sales_order.insert() def test_make_sales_order_with_terms(self): @@ -108,9 +141,7 @@ class TestQuotation(FrappeTestCase): self.assertEqual(quotation.payment_schedule[0].payment_amount, 8906.00) self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date) self.assertEqual(quotation.payment_schedule[1].payment_amount, 8906.00) - self.assertEqual( - quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30) - ) + self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30)) sales_order = make_sales_order(quotation.name) @@ -120,9 +151,9 @@ class TestQuotation(FrappeTestCase): self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name) self.assertEqual(sales_order.customer, "_Test Customer") - sales_order.delivery_date = "2014-01-01" sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = nowdate() + sales_order.delivery_date = nowdate() sales_order.insert() # Remove any unknown taxes if applied @@ -144,9 +175,7 @@ class TestQuotation(FrappeTestCase): def test_so_from_expired_quotation(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order - frappe.db.set_single_value( - "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0 - ) + frappe.db.set_single_value("Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0) quotation = frappe.copy_doc(test_records[0]) quotation.valid_till = add_days(nowdate(), -1) @@ -155,9 +184,7 @@ class TestQuotation(FrappeTestCase): self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) - frappe.db.set_single_value( - "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1 - ) + frappe.db.set_single_value("Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1) make_sales_order(quotation.name) @@ -590,6 +617,22 @@ class TestQuotation(FrappeTestCase): quotation.reload() self.assertEqual(quotation.status, "Ordered") + def test_uom_validation(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = "_Test Item FOR UOM Validation" + make_item(item, {"is_stock_item": 1}) + + if not frappe.db.exists("UOM", "lbs"): + frappe.get_doc({"doctype": "UOM", "uom_name": "lbs", "must_be_whole_number": 1}).insert() + else: + frappe.db.set_value("UOM", "lbs", "must_be_whole_number", 1) + + quotation = make_quotation(item_code=item, qty=1, rate=100, do_not_submit=1) + quotation.items[0].uom = "lbs" + quotation.items[0].conversion_factor = 2.23 + self.assertRaises(frappe.ValidationError, quotation.save) + test_records = frappe.get_test_records("Quotation") diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index e9a6cc385d3..2995a1e99bc 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -89,6 +89,27 @@ frappe.ui.form.on("Sales Order", { }, __('Get Items From')); }, + // 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) { + if (r.message) { + frm.set_value("company_address", r.message); + } else { + frm.set_value("company_address", ""); + } + }, + }); + } + }, + onload: function(frm) { if (!frm.doc.transaction_date){ frm.set_value('transaction_date', frappe.datetime.get_today()) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 490bd7a9830..09b73878aa2 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -774,7 +774,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "hide_days": 1, "hide_seconds": 1, "label": "Taxes and Charges Calculation", @@ -1631,7 +1631,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-10-18 12:41:54.813462", + "modified": "2024-03-20 16:04:43.627183", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index cbc11ed02af..68fa67e92f1 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -42,10 +42,10 @@ class WarehouseRequired(frappe.ValidationError): class SalesOrder(SellingController): def __init__(self, *args, **kwargs): - super(SalesOrder, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate(self): - super(SalesOrder, self).validate() + super().validate() self.validate_delivery_date() self.validate_proj_cust() self.validate_po() @@ -85,7 +85,9 @@ class SalesOrder(SellingController): for d in self.get("items"): if d.delivery_date and getdate(self.po_date) > getdate(d.delivery_date): frappe.throw( - _("Row #{0}: Expected Delivery Date cannot be before Purchase Order Date").format(d.idx) + _("Row #{0}: Expected Delivery Date cannot be before Purchase Order Date").format( + d.idx + ) ) if self.po_no and self.customer and not self.skip_delivery_note: @@ -100,9 +102,10 @@ class SalesOrder(SellingController): frappe.db.get_single_value("Selling Settings", "allow_against_multiple_purchase_orders") ): frappe.msgprint( - _("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format( - frappe.bold(so[0][0]), frappe.bold(self.po_no) - ) + _( + "Warning: Sales Order {0} already exists against Customer's Purchase Order {1}" + ).format(frappe.bold(so[0][0]), frappe.bold(self.po_no)), + alert=True, ) else: frappe.throw( @@ -111,14 +114,15 @@ class SalesOrder(SellingController): ).format( frappe.bold(so[0][0]), frappe.bold(self.po_no), - frappe.bold(_("'Allow Multiple Sales Orders Against a Customer's Purchase Order'")), + frappe.bold( + _("'Allow Multiple Sales Orders Against a Customer's Purchase Order'") + ), get_link_to_form("Selling Settings", "Selling Settings"), ) ) def validate_for_items(self): for d in self.get("items"): - # used for production plan d.transaction_date = self.transaction_date @@ -148,7 +152,9 @@ class SalesOrder(SellingController): (d.prevdoc_docname, self.order_type), ) if not res: - frappe.msgprint(_("Quotation {0} not of type {1}").format(d.prevdoc_docname, self.order_type)) + frappe.msgprint( + _("Quotation {0} not of type {1}").format(d.prevdoc_docname, self.order_type) + ) def validate_delivery_date(self): if self.order_type == "Sales" and not self.skip_delivery_note: @@ -187,13 +193,16 @@ class SalesOrder(SellingController): ) def validate_warehouse(self): - super(SalesOrder, self).validate_warehouse() + super().validate_warehouse() for d in self.get("items"): if ( ( frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 - or (self.has_product_bundle(d.item_code) and self.product_bundle_has_stock_item(d.item_code)) + or ( + self.has_product_bundle(d.item_code) + and self.product_bundle_has_stock_item(d.item_code) + ) ) and not d.warehouse and not cint(d.delivered_by_supplier) @@ -203,7 +212,7 @@ class SalesOrder(SellingController): ) def validate_with_previous_doc(self): - super(SalesOrder, self).validate_with_previous_doc( + super().validate_with_previous_doc( { "Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}, "Quotation Item": { @@ -261,7 +270,7 @@ class SalesOrder(SellingController): def on_cancel(self): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") - super(SalesOrder, self).on_cancel() + super().on_cancel() # Cannot cancel closed SO if self.status == "Closed": @@ -283,9 +292,7 @@ class SalesOrder(SellingController): update_coupon_code_count(self.coupon_code, "cancelled") def update_project(self): - if ( - frappe.db.get_single_value("Selling Settings", "sales_update_frequency") != "Each Transaction" - ): + if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") != "Each Transaction": return if self.project: @@ -323,7 +330,7 @@ class SalesOrder(SellingController): def check_modified_date(self): mod_db = frappe.db.get_value("Sales Order", self.name, "modified") - date_diff = frappe.db.sql("select TIMEDIFF('%s', '%s')" % (mod_db, cstr(self.modified))) + date_diff = frappe.db.sql(f"select TIMEDIFF('{mod_db}', '{cstr(self.modified)}')") if date_diff and date_diff[0][0]: frappe.throw(_("{0} {1} has been modified. Please refresh.").format(self.doctype, self.name)) @@ -362,6 +369,9 @@ class SalesOrder(SellingController): def on_update(self): pass + def on_update_after_submit(self): + self.check_credit_limit() + def before_update_after_submit(self): self.validate_po() self.validate_drop_ship() @@ -432,17 +442,17 @@ class SalesOrder(SellingController): def set_indicator(self): """Set indicator for portal""" - if self.per_billed < 100 and self.per_delivered < 100: - self.indicator_color = "orange" - self.indicator_title = _("Not Paid and Not Delivered") + self.indicator_color = { + "Draft": "red", + "On Hold": "orange", + "To Deliver and Bill": "orange", + "To Bill": "orange", + "To Deliver": "orange", + "Completed": "green", + "Cancelled": "red", + }.get(self.status, "blue") - elif self.per_billed == 100 and self.per_delivered < 100: - self.indicator_color = "orange" - self.indicator_title = _("Paid and Not Delivered") - - else: - self.indicator_color = "green" - self.indicator_title = _("Paid") + self.indicator_title = _(self.status) def on_recurring(self, reference_doc, auto_repeat_doc): def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): @@ -495,9 +505,9 @@ class SalesOrder(SellingController): ) if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}): frappe.throw( - _("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format( - item.item_code - ) + _( + "No active BOM found for item {0}. Delivery by Serial No cannot be ensured" + ).format(item.item_code) ) reserved_items.append(item.item_code) else: @@ -873,7 +883,7 @@ def get_events(start, end, filters=None): conditions = get_event_conditions("Sales Order", filters) data = frappe.db.sql( - """ + f""" select distinct `tabSales Order`.name, `tabSales Order`.customer_name, `tabSales Order`.status, `tabSales Order`.delivery_status, `tabSales Order`.billing_status, @@ -886,9 +896,7 @@ def get_events(start, end, filters=None): and (`tabSales Order Item`.delivery_date between %(start)s and %(end)s) and `tabSales Order`.docstatus < 2 {conditions} - """.format( - conditions=conditions - ), + """, {"start": start, "end": end}, as_dict=True, update={"allDay": 0}, @@ -962,9 +970,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t items_to_map = list(set(items_to_map)) if not suppliers: - frappe.throw( - _("Please set a Supplier against the Items to be considered in the Purchase Order.") - ) + frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) purchase_orders = [] for supplier in suppliers: @@ -1030,9 +1036,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): selected_items = json.loads(selected_items) items_to_map = [ - item.get("item_code") - for item in selected_items - if item.get("item_code") and item.get("item_code") + item.get("item_code") for item in selected_items if item.get("item_code") and item.get("item_code") ] items_to_map = list(set(items_to_map)) @@ -1215,17 +1219,13 @@ def make_raw_material_request(items, company, sales_order, project=None): for item in items.get("items"): item["include_exploded_items"] = items.get("include_exploded_items") item["ignore_existing_ordered_qty"] = items.get("ignore_existing_ordered_qty") - item["include_raw_materials_from_sales_order"] = items.get( - "include_raw_materials_from_sales_order" - ) + item["include_raw_materials_from_sales_order"] = items.get("include_raw_materials_from_sales_order") items.update({"company": company, "sales_order": sales_order}) raw_materials = get_items_for_material_requests(items) if not raw_materials: - frappe.msgprint( - _("Material Request not created, as quantity for Raw Materials already available.") - ) + frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available.")) return material_request = frappe.new_doc("Material Request") @@ -1301,7 +1301,11 @@ def create_pick_list(source_name, target_doc=None): "Sales Order", source_name, { - "Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}}, + "Sales Order": { + "doctype": "Pick List", + "field_map": {"set_warehouse": "parent_warehouse"}, + "validation": {"docstatus": ["=", 1]}, + }, "Sales Order Item": { "doctype": "Pick List Item", "field_map": {"parent": "sales_order", "name": "sales_order_item"}, diff --git a/erpnext/selling/doctype/sales_order/sales_order_calendar.js b/erpnext/selling/doctype/sales_order/sales_order_calendar.js index cb412cfb4a7..f4c0e2ba72a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_calendar.js +++ b/erpnext/selling/doctype/sales_order/sales_order_calendar.js @@ -3,43 +3,44 @@ frappe.views.calendar["Sales Order"] = { field_map: { - "start": "delivery_date", - "end": "delivery_date", - "id": "name", - "title": "customer_name", - "allDay": "allDay" + start: "delivery_date", + end: "delivery_date", + id: "name", + title: "customer_name", + allDay: "allDay", }, gantt: true, filters: [ { - "fieldtype": "Link", - "fieldname": "customer", - "options": "Customer", - "label": __("Customer") + fieldtype: "Link", + fieldname: "customer", + options: "Customer", + label: __("Customer"), }, { - "fieldtype": "Select", - "fieldname": "delivery_status", - "options": "Not Delivered\nFully Delivered\nPartly Delivered\nClosed\nNot Applicable", - "label": __("Delivery Status") + fieldtype: "Select", + fieldname: "delivery_status", + options: "Not Delivered\nFully Delivered\nPartly Delivered\nClosed\nNot Applicable", + label: __("Delivery Status"), }, { - "fieldtype": "Select", - "fieldname": "billing_status", - "options": "Not Billed\nFully Billed\nPartly Billed\nClosed", - "label": __("Billing Status") + fieldtype: "Select", + fieldname: "billing_status", + options: "Not Billed\nFully Billed\nPartly Billed\nClosed", + label: __("Billing Status"), }, ], get_events_method: "erpnext.selling.doctype.sales_order.sales_order.get_events", - get_css_class: function(data) { - if(data.status=="Closed") { - return "success"; - } if(data.delivery_status=="Not Delivered") { - return "danger"; - } else if(data.delivery_status=="Partly Delivered") { - return "warning"; - } else if(data.delivery_status=="Fully Delivered") { + get_css_class: function (data) { + if (data.status == "Closed") { return "success"; } - } -} + if (data.delivery_status == "Not Delivered") { + return "danger"; + } else if (data.delivery_status == "Partly Delivered") { + return "warning"; + } else if (data.delivery_status == "Fully Delivered") { + return "success"; + } + }, +}; diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 64c58ef5d7b..61a29c9bcd7 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -1,6 +1,16 @@ -frappe.listview_settings['Sales Order'] = { - add_fields: ["base_grand_total", "customer_name", "currency", "delivery_date", - "per_delivered", "per_billed", "status", "order_type", "name", "skip_delivery_note"], +frappe.listview_settings["Sales Order"] = { + add_fields: [ + "base_grand_total", + "customer_name", + "currency", + "delivery_date", + "per_delivered", + "per_billed", + "status", + "order_type", + "name", + "skip_delivery_note", + ], get_indicator: function (doc) { if (doc.status === "Closed") { // Closed @@ -12,53 +22,54 @@ frappe.listview_settings['Sales Order'] = { return [__("Completed"), "green", "status,=,Completed"]; } else if (!doc.skip_delivery_note && flt(doc.per_delivered, 6) < 100) { if (frappe.datetime.get_diff(doc.delivery_date) < 0) { - // not delivered & overdue - return [__("Overdue"), "red", - "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"]; + // not delivered & overdue + return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"]; } else if (flt(doc.grand_total) === 0) { // not delivered (zeroount order) - return [__("To Deliver"), "orange", - "per_delivered,<,100|grand_total,=,0|status,!=,Closed"]; + return [__("To Deliver"), "orange", "per_delivered,<,100|grand_total,=,0|status,!=,Closed"]; } else if (flt(doc.per_billed, 6) < 100) { // not delivered & not billed - return [__("To Deliver and Bill"), "orange", - "per_delivered,<,100|per_billed,<,100|status,!=,Closed"]; + return [ + __("To Deliver and Bill"), + "orange", + "per_delivered,<,100|per_billed,<,100|status,!=,Closed", + ]; } else { // not billed - return [__("To Deliver"), "orange", - "per_delivered,<,100|per_billed,=,100|status,!=,Closed"]; + return [__("To Deliver"), "orange", "per_delivered,<,100|per_billed,=,100|status,!=,Closed"]; } - } else if ((flt(doc.per_delivered, 6) === 100) && flt(doc.grand_total) !== 0 - && flt(doc.per_billed, 6) < 100) { + } else if ( + flt(doc.per_delivered, 6) === 100 && + flt(doc.grand_total) !== 0 && + flt(doc.per_billed, 6) < 100 + ) { // to bill - return [__("To Bill"), "orange", - "per_delivered,=,100|per_billed,<,100|status,!=,Closed"]; - } else if (doc.skip_delivery_note && flt(doc.per_billed, 6) < 100){ + return [__("To Bill"), "orange", "per_delivered,=,100|per_billed,<,100|status,!=,Closed"]; + } else if (doc.skip_delivery_note && flt(doc.per_billed, 6) < 100) { return [__("To Bill"), "orange", "per_billed,<,100|status,!=,Closed"]; } }, - onload: function(listview) { + onload: function (listview) { var method = "erpnext.selling.doctype.sales_order.sales_order.close_or_unclose_sales_orders"; - listview.page.add_menu_item(__("Close"), function() { - listview.call_for_selected_items(method, {"status": "Closed"}); + listview.page.add_menu_item(__("Close"), function () { + listview.call_for_selected_items(method, { status: "Closed" }); }); - listview.page.add_menu_item(__("Re-open"), function() { - listview.call_for_selected_items(method, {"status": "Submitted"}); + listview.page.add_menu_item(__("Re-open"), function () { + listview.call_for_selected_items(method, { status: "Submitted" }); }); - listview.page.add_action_item(__("Sales Invoice"), ()=>{ + listview.page.add_action_item(__("Sales Invoice"), () => { erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice"); }); - listview.page.add_action_item(__("Delivery Note"), ()=>{ + listview.page.add_action_item(__("Delivery Note"), () => { erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note"); }); - listview.page.add_action_item(__("Advance Payment"), ()=>{ + listview.page.add_action_item(__("Advance Payment"), () => { erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry"); }); - - } + }, }; diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 3d4c035fdca..4ebd2a0c2d0 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -20,6 +20,7 @@ from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import ( WarehouseRequired, + create_pick_list, make_delivery_note, make_material_request, make_raw_material_request, @@ -308,9 +309,7 @@ class TestSalesOrder(FrappeTestCase): def test_reserved_qty_for_partial_delivery_with_packing_list(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - make_stock_entry( - item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100 - ) + make_stock_entry(item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100) existing_reserved_qty_item1 = get_reserved_qty("_Test Item") existing_reserved_qty_item2 = get_reserved_qty("_Test Item Home Desktop 100") @@ -318,16 +317,12 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order(item_code="_Test Product Bundle Item") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual( - get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 - ) + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20) dn = create_dn_against_so(so.name) self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 25) - self.assertEqual( - get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10 - ) + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10) # close so so.load_from_db() @@ -341,15 +336,11 @@ class TestSalesOrder(FrappeTestCase): so.update_status("Draft") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 25) - self.assertEqual( - get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10 - ) + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10) dn.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual( - get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 - ) + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20) so.load_from_db() so.cancel() @@ -365,9 +356,7 @@ class TestSalesOrder(FrappeTestCase): def test_reserved_qty_for_over_delivery_with_packing_list(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - make_stock_entry( - item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100 - ) + make_stock_entry(item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100) # set over-delivery allowance frappe.db.set_value("Item", "_Test Product Bundle Item", "over_delivery_receipt_allowance", 50) @@ -378,9 +367,7 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order(item_code="_Test Product Bundle Item") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual( - get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 - ) + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20) dn = create_dn_against_so(so.name, 15) @@ -389,9 +376,7 @@ class TestSalesOrder(FrappeTestCase): dn.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual( - get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 - ) + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20) def test_update_child_adding_new_item(self): so = make_sales_order(item_code="_Test Item", qty=4) @@ -461,9 +446,7 @@ class TestSalesOrder(FrappeTestCase): trans_item = json.dumps( [{"item_code": "_Test Item 2", "qty": 2, "rate": 500, "docname": so.get("items")[1].name}] ) - self.assertRaises( - frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name - ) + self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name) # remove last added item trans_item = json.dumps( @@ -502,9 +485,7 @@ class TestSalesOrder(FrappeTestCase): trans_item = json.dumps( [{"item_code": "_Test Item", "rate": 200, "qty": 2, "docname": so.items[0].name}] ) - self.assertRaises( - frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name - ) + self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name) def test_update_child_with_precision(self): from frappe.custom.doctype.property_setter.property_setter import make_property_setter @@ -534,15 +515,11 @@ class TestSalesOrder(FrappeTestCase): trans_item = json.dumps( [{"item_code": "_Test Item", "rate": 200, "qty": 7, "docname": so.items[0].name}] ) - self.assertRaises( - frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name - ) + self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name) # add new item trans_item = json.dumps([{"item_code": "_Test Item", "rate": 100, "qty": 2}]) - self.assertRaises( - frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name - ) + self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name) def test_update_child_qty_rate_with_workflow(self): from frappe.model.workflow import apply_workflow @@ -560,9 +537,7 @@ class TestSalesOrder(FrappeTestCase): trans_item = json.dumps( [{"item_code": "_Test Item", "rate": 150, "qty": 2, "docname": so.items[0].name}] ) - self.assertRaises( - frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name - ) + self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name) frappe.set_user("Administrator") user2 = "test2@example.com" @@ -820,9 +795,7 @@ class TestSalesOrder(FrappeTestCase): frappe.set_user("Administrator") frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", test_user.name) - frappe.permissions.remove_user_permission( - "Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name - ) + frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name) frappe.permissions.remove_user_permission("Company", "_Test Company 1", test_user_2.name) def test_block_delivery_note_against_cancelled_sales_order(self): @@ -941,9 +914,7 @@ class TestSalesOrder(FrappeTestCase): from erpnext.selling.doctype.sales_order.sales_order import update_status as so_update_status # make items - po_item = make_item( - "_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1} - ) + po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1}) dn_item = make_item("_Test Regular Item", {"is_stock_item": 1}) so_items = [ @@ -1226,17 +1197,13 @@ class TestSalesOrder(FrappeTestCase): new_so = frappe.copy_doc(so) new_so.save(ignore_permissions=True) - self.assertEqual( - new_so.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate) - ) + self.assertEqual(new_so.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate)) new_so.items[0].margin_rate_or_amount = 25 new_so.payment_schedule = [] new_so.save() new_so.submit() - self.assertEqual( - new_so.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate) - ) + self.assertEqual(new_so.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate)) def test_terms_auto_added(self): so = make_sales_order(do_not_save=1) @@ -1600,7 +1567,7 @@ class TestSalesOrder(FrappeTestCase): Second Sales Order should not add on to Blanket Orders Ordered Quantity. """ - bo = make_blanket_order(blanket_order_type="Selling", quantity=10, rate=10) + make_blanket_order(blanket_order_type="Selling", quantity=10, rate=10) so = make_sales_order(item_code="_Test Item", qty=5, against_blanket_order=1) so_doc = frappe.get_doc("Sales Order", so.get("name")) @@ -1821,7 +1788,10 @@ class TestSalesOrder(FrappeTestCase): wo.submit() make_stock_entry( - item_code="_Test Item", target="Work In Progress - _TC", qty=4, basic_rate=100 # Stock RM + item_code="_Test Item", + target="Work In Progress - _TC", + qty=4, + basic_rate=100, # Stock RM ) make_stock_entry( item_code="_Test Item Home Desktop 100", # Stock RM @@ -1929,10 +1899,6 @@ class TestSalesOrder(FrappeTestCase): def test_delivered_item_material_request(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." - from erpnext.manufacturing.doctype.work_order.work_order import ( - make_stock_entry as make_se_from_wo, - ) - from erpnext.stock.doctype.material_request.material_request import raise_work_orders so = make_sales_order( item_list=[ @@ -1940,9 +1906,7 @@ class TestSalesOrder(FrappeTestCase): ] ) - make_stock_entry( - item_code="_Test FG Item", target="Work In Progress - _TC", qty=4, basic_rate=100 - ) + make_stock_entry(item_code="_Test FG Item", target="Work In Progress - _TC", qty=4, basic_rate=100) dn = make_delivery_note(so.name) dn.items[0].qty = 4 @@ -1967,7 +1931,8 @@ class TestSalesOrder(FrappeTestCase): if not frappe.db.exists("Item", product_bundle): bundle_item = make_item(product_bundle, {"is_stock_item": 0}) bundle_item.append( - "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + "item_defaults", + {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}, ) bundle_item.save(ignore_permissions=True) @@ -2037,7 +2002,8 @@ class TestSalesOrder(FrappeTestCase): if not frappe.db.exists("Item", product_bundle): bundle_item = make_item(product_bundle, {"is_stock_item": 0}) bundle_item.append( - "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + "item_defaults", + {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}, ) bundle_item.save(ignore_permissions=True) @@ -2082,6 +2048,117 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.items[0].rate, scenario.get("expected_rate")) self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate")) + def test_pick_list_without_rejected_materials(self): + serial_and_batch_item = make_item( + "_Test Serial and Batch Item for Rejected Materials", + properties={ + "has_serial_no": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT-TSBIFRM-.#####", + "serial_no_series": "SN-TSBIFRM-.#####", + }, + ).name + + serial_item = make_item( + "_Test Serial Item for Rejected Materials", + properties={ + "has_serial_no": 1, + "serial_no_series": "SN-TSIFRM-.#####", + }, + ).name + + batch_item = make_item( + "_Test Batch Item for Rejected Materials", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT-TBIFRM-.#####", + }, + ).name + + normal_item = make_item("_Test Normal Item for Rejected Materials").name + + warehouse = "_Test Warehouse - _TC" + rejected_warehouse = "_Test Dummy Rejected Warehouse - _TC" + + if not frappe.db.exists("Warehouse", rejected_warehouse): + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": rejected_warehouse, + "company": "_Test Company", + "warehouse_group": "_Test Warehouse Group", + "is_rejected_warehouse": 1, + } + ).insert() + + se = make_stock_entry(item_code=normal_item, qty=1, to_warehouse=warehouse, do_not_submit=True) + for item in [serial_and_batch_item, serial_item, batch_item]: + se.append("items", {"item_code": item, "qty": 1, "t_warehouse": warehouse}) + + se.save() + se.submit() + + se = make_stock_entry( + item_code=normal_item, qty=1, to_warehouse=rejected_warehouse, do_not_submit=True + ) + for item in [serial_and_batch_item, serial_item, batch_item]: + se.append("items", {"item_code": item, "qty": 1, "t_warehouse": rejected_warehouse}) + + se.save() + se.submit() + + so = make_sales_order(item_code=normal_item, qty=2, do_not_submit=True) + + for item in [serial_and_batch_item, serial_item, batch_item]: + so.append("items", {"item_code": item, "qty": 2, "warehouse": warehouse}) + + so.save() + so.submit() + + pick_list = create_pick_list(so.name) + + pick_list.save() + for row in pick_list.locations: + self.assertEqual(row.qty, 1.0) + self.assertFalse(row.warehouse == rejected_warehouse) + self.assertTrue(row.warehouse == warehouse) + + def test_auto_update_price_list(self): + item = make_item( + "_Test Auto Update Price List Item", + ) + + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1) + so = make_sales_order( + item_code=item.name, currency="USD", qty=1, rate=100, price_list_rate=100, do_not_submit=True + ) + so.save() + + item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate") + self.assertEqual(item_price, 100) + + so = make_sales_order( + item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=100, do_not_submit=True + ) + so.save() + + item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate") + self.assertEqual(item_price, 100) + + frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 1) + so = make_sales_order( + item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=200, do_not_submit=True + ) + so.save() + + item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate") + self.assertEqual(item_price, 200) + + frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0) + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") @@ -2147,20 +2224,19 @@ def make_sales_order(**args): return so -def create_dn_against_so(so, delivered_qty=0): - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) +def create_dn_against_so(so, delivered_qty=0, do_not_submit=False): + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) dn = make_delivery_note(so) dn.get("items")[0].qty = delivered_qty or 5 dn.insert() - dn.submit() + if not do_not_submit: + dn.submit() return dn def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): - return flt( - frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "reserved_qty") - ) + return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "reserved_qty")) test_dependencies = ["Currency Exchange"] @@ -2173,9 +2249,7 @@ def make_sales_order_workflow(): doc.save() return doc - frappe.get_doc(dict(doctype="Role", role_name="Test Junior Approver")).insert( - ignore_if_duplicate=True - ) + frappe.get_doc(dict(doctype="Role", role_name="Test Junior Approver")).insert(ignore_if_duplicate=True) frappe.get_doc(dict(doctype="Role", role_name="Test Approver")).insert(ignore_if_duplicate=True) frappe.cache().hdel("roles", frappe.session.user) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 9797b6ae11f..110d80253d3 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -832,7 +832,8 @@ "label": "Purchase Order", "options": "Purchase Order", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "column_break_89", @@ -875,7 +876,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-24 19:07:17.715231", + "modified": "2024-03-21 18:15:56.625005", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.js b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.js index 3e183f6e6fc..ac5a8134f3c 100644 --- a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.js +++ b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Sales Partner Type', { - refresh: function() { - - } +frappe.ui.form.on("Sales Partner Type", { + refresh: function () {}, }); diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.js b/erpnext/selling/doctype/selling_settings/selling_settings.js index cf6fb2806ee..4471458fb10 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.js +++ b/erpnext/selling/doctype/selling_settings/selling_settings.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Selling Settings', { - refresh: function(frm) { - - } +frappe.ui.form.on("Selling Settings", { + refresh: function (frm) {}, }); diff --git a/erpnext/selling/doctype/sms_center/sms_center.js b/erpnext/selling/doctype/sms_center/sms_center.js index 974cfc79181..f513213e71c 100644 --- a/erpnext/selling/doctype/sms_center/sms_center.js +++ b/erpnext/selling/doctype/sms_center/sms_center.js @@ -8,10 +8,10 @@ extend_cscript(cur_frm.cscript, { if (total_characters > 160) { total_msg = cint(total_characters / 160); - total_msg = (total_characters % 160 == 0 ? total_msg : total_msg + 1); + total_msg = total_characters % 160 == 0 ? total_msg : total_msg + 1; } this.frm.set_value("total_characters", total_characters); this.frm.set_value("total_messages", this.frm.doc.message ? total_msg : 0); - } + }, }); diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 6db4150be94..6e4d1d0a373 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -1,19 +1,19 @@ -frappe.provide('erpnext.PointOfSale'); +frappe.provide("erpnext.PointOfSale"); -frappe.pages['point-of-sale'].on_page_load = function(wrapper) { +frappe.pages["point-of-sale"].on_page_load = function (wrapper) { frappe.ui.make_app_page({ parent: wrapper, - title: __('Point of Sale'), - single_column: true + title: __("Point of Sale"), + single_column: true, }); - frappe.require('point-of-sale.bundle.js', function() { + frappe.require("point-of-sale.bundle.js", function () { wrapper.pos = new erpnext.PointOfSale.Controller(wrapper); window.cur_pos = wrapper.pos; }); }; -frappe.pages['point-of-sale'].refresh = function(wrapper) { +frappe.pages["point-of-sale"].refresh = function (wrapper) { if (document.scannerDetectionData) { onScan.detachFrom(document); wrapper.pos.wrapper.html(""); diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 6744a5d49e7..bc94d3b706e 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -3,7 +3,6 @@ import json -from typing import Dict, Optional import frappe from frappe.utils import cint @@ -178,16 +177,14 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te @frappe.whitelist() -def search_for_serial_or_batch_or_barcode_number(search_value: str) -> Dict[str, Optional[str]]: +def search_for_serial_or_batch_or_barcode_number(search_value: str) -> dict[str, str | None]: return scan_barcode(search_value) def get_conditions(search_term): condition = "(" condition += """item.name like {search_term} - or item.item_name like {search_term}""".format( - search_term=frappe.db.escape("%" + search_term + "%") - ) + or item.item_name like {search_term}""".format(search_term=frappe.db.escape("%" + search_term + "%")) condition += add_search_fields_condition(search_term) condition += ")" @@ -199,7 +196,7 @@ def add_search_fields_condition(search_term): search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"]) if search_fields: for field in search_fields: - condition += " or item.`{0}` like {1}".format( + condition += " or item.`{}` like {}".format( field["fieldname"], frappe.db.escape("%" + search_term + "%") ) return condition @@ -229,10 +226,8 @@ def item_group_query(doctype, txt, searchfield, start, page_len, filters): cond = cond % tuple(item_groups) return frappe.db.sql( - """ select distinct name from `tabItem Group` - where {condition} and (name like %(txt)s) limit {page_len} offset {start}""".format( - condition=cond, start=start, page_len=page_len - ), + f""" select distinct name from `tabItem Group` + where {cond} and (name like %(txt)s) limit {page_len} offset {start}""", {"txt": "%%%s%%" % txt}, ) @@ -277,12 +272,12 @@ def get_past_order_list(search_term, status, limit=20): if search_term and status: invoices_by_customer = frappe.db.get_all( "POS Invoice", - filters={"customer": ["like", "%{}%".format(search_term)], "status": status}, + filters={"customer": ["like", f"%{search_term}%"], "status": status}, fields=fields, ) invoices_by_name = frappe.db.get_all( "POS Invoice", - filters={"name": ["like", "%{}%".format(search_term)], "status": status}, + filters={"name": ["like", f"%{search_term}%"], "status": status}, fields=fields, ) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 07b1c2eb7ee..3d42f3ea23f 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -1,13 +1,15 @@ erpnext.PointOfSale.Controller = class { constructor(wrapper) { - this.wrapper = $(wrapper).find('.layout-main-section'); + this.wrapper = $(wrapper).find(".layout-main-section"); this.page = wrapper.page; this.check_opening_entry(); } fetch_opening_entry() { - return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.check_opening_entry", { "user": frappe.session.user }); + return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.check_opening_entry", { + user: frappe.session.user, + }); } check_opening_entry() { @@ -25,50 +27,62 @@ erpnext.PointOfSale.Controller = class { const me = this; const table_fields = [ { - fieldname: "mode_of_payment", fieldtype: "Link", - in_list_view: 1, label: "Mode of Payment", - options: "Mode of Payment", reqd: 1 + fieldname: "mode_of_payment", + fieldtype: "Link", + in_list_view: 1, + label: "Mode of Payment", + options: "Mode of Payment", + reqd: 1, }, { - fieldname: "opening_amount", fieldtype: "Currency", - in_list_view: 1, label: "Opening Amount", + fieldname: "opening_amount", + fieldtype: "Currency", + in_list_view: 1, + label: "Opening Amount", options: "company:company_currency", change: function () { - dialog.fields_dict.balance_details.df.data.some(d => { + dialog.fields_dict.balance_details.df.data.some((d) => { if (d.idx == this.doc.idx) { d.opening_amount = this.value; dialog.fields_dict.balance_details.grid.refresh(); return true; } }); - } - } + }, + }, ]; const fetch_pos_payment_methods = () => { const pos_profile = dialog.fields_dict.pos_profile.get_value(); if (!pos_profile) return; frappe.db.get_doc("POS Profile", pos_profile).then(({ payments }) => { dialog.fields_dict.balance_details.df.data = []; - payments.forEach(pay => { + payments.forEach((pay) => { const { mode_of_payment } = pay; - dialog.fields_dict.balance_details.df.data.push({ mode_of_payment, opening_amount: '0' }); + dialog.fields_dict.balance_details.df.data.push({ mode_of_payment, opening_amount: "0" }); }); dialog.fields_dict.balance_details.grid.refresh(); }); - } + }; const dialog = new frappe.ui.Dialog({ - title: __('Create POS Opening Entry'), + title: __("Create POS Opening Entry"), static: true, fields: [ { - fieldtype: 'Link', label: __('Company'), default: frappe.defaults.get_default('company'), - options: 'Company', fieldname: 'company', reqd: 1 + fieldtype: "Link", + label: __("Company"), + default: frappe.defaults.get_default("company"), + options: "Company", + fieldname: "company", + reqd: 1, }, { - fieldtype: 'Link', label: __('POS Profile'), - options: 'POS Profile', fieldname: 'pos_profile', reqd: 1, + fieldtype: "Link", + label: __("POS Profile"), + options: "POS Profile", + fieldname: "pos_profile", + reqd: 1, get_query: () => pos_profile_query(), - onchange: () => fetch_pos_payment_methods() + onchange: () => fetch_pos_payment_methods(), }, { fieldname: "balance_details", @@ -78,34 +92,38 @@ erpnext.PointOfSale.Controller = class { in_place_edit: true, reqd: 1, data: [], - fields: table_fields - } + fields: table_fields, + }, ], - primary_action: async function({ company, pos_profile, balance_details }) { + primary_action: async function ({ company, pos_profile, balance_details }) { if (!balance_details.length) { frappe.show_alert({ message: __("Please add Mode of payments and opening balance details."), - indicator: 'red' - }) + indicator: "red", + }); return frappe.utils.play_sound("error"); } // filter balance details for empty rows - balance_details = balance_details.filter(d => d.mode_of_payment); + balance_details = balance_details.filter((d) => d.mode_of_payment); const method = "erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher"; - const res = await frappe.call({ method, args: { pos_profile, company, balance_details }, freeze:true }); + const res = await frappe.call({ + method, + args: { pos_profile, company, balance_details }, + freeze: true, + }); !res.exc && me.prepare_app_defaults(res.message); dialog.hide(); }, - primary_action_label: __('Submit') + primary_action_label: __("Submit"), }); dialog.show(); const pos_profile_query = () => { return { - query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query', - filters: { company: dialog.fields_dict.company.get_value() } - } + query: "erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query", + filters: { company: dialog.fields_dict.company.get_value() }, + }; }; } @@ -117,19 +135,19 @@ erpnext.PointOfSale.Controller = class { this.item_stock_map = {}; this.settings = {}; - frappe.db.get_value('Stock Settings', undefined, 'allow_negative_stock').then(({ message }) => { + frappe.db.get_value("Stock Settings", undefined, "allow_negative_stock").then(({ message }) => { this.allow_negative_stock = flt(message.allow_negative_stock) || false; }); frappe.call({ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data", - args: { "pos_profile": this.pos_profile }, + args: { pos_profile: this.pos_profile }, callback: (res) => { const profile = res.message; Object.assign(this.settings, profile); - this.settings.customer_groups = profile.customer_groups.map(group => group.name); + this.settings.customer_groups = profile.customer_groups.map((group) => group.name); this.make_app(); - } + }, }); } @@ -139,7 +157,8 @@ erpnext.PointOfSale.Controller = class { Opened at ${moment(this.pos_opening_time).format("Do MMMM, h:mma")} - `); + ` + ); } make_app() { @@ -150,11 +169,9 @@ erpnext.PointOfSale.Controller = class { } prepare_dom() { - this.wrapper.append( - `
              ` - ); + this.wrapper.append(`
              `); - this.$components_wrapper = this.wrapper.find('.point-of-sale-app'); + this.$components_wrapper = this.wrapper.find(".point-of-sale-app"); } prepare_components() { @@ -169,13 +186,18 @@ erpnext.PointOfSale.Controller = class { prepare_menu() { this.page.clear_menu(); - this.page.add_menu_item(__("Open Form View"), this.open_form_view.bind(this), false, 'Ctrl+F'); + this.page.add_menu_item(__("Open Form View"), this.open_form_view.bind(this), false, "Ctrl+F"); - this.page.add_menu_item(__("Toggle Recent Orders"), this.toggle_recent_order.bind(this), false, 'Ctrl+O'); + this.page.add_menu_item( + __("Toggle Recent Orders"), + this.toggle_recent_order.bind(this), + false, + "Ctrl+O" + ); - this.page.add_menu_item(__("Save as Draft"), this.save_draft_invoice.bind(this), false, 'Ctrl+S'); + this.page.add_menu_item(__("Save as Draft"), this.save_draft_invoice.bind(this), false, "Ctrl+S"); - this.page.add_menu_item(__('Close the POS'), this.close_pos.bind(this), false, 'Shift+Ctrl+C'); + this.page.add_menu_item(__("Close the POS"), this.close_pos.bind(this), false, "Shift+Ctrl+C"); } open_form_view() { @@ -184,7 +206,7 @@ erpnext.PointOfSale.Controller = class { } toggle_recent_order() { - const show = this.recent_order_list.$component.is(':hidden'); + const show = this.recent_order_list.$component.is(":hidden"); this.toggle_recent_order_list(show); } @@ -194,38 +216,40 @@ erpnext.PointOfSale.Controller = class { if (this.frm.doc.items.length == 0) { frappe.show_alert({ message: __("You must add atleast one item to save it as draft."), - indicator:'red' + indicator: "red", }); frappe.utils.play_sound("error"); return; } - this.frm.save(undefined, undefined, undefined, () => { - frappe.show_alert({ - message: __("There was an error saving the document."), - indicator: 'red' + this.frm + .save(undefined, undefined, undefined, () => { + frappe.show_alert({ + message: __("There was an error saving the document."), + indicator: "red", + }); + frappe.utils.play_sound("error"); + }) + .then(() => { + frappe.run_serially([ + () => frappe.dom.freeze(), + () => this.make_new_invoice(), + () => frappe.dom.unfreeze(), + ]); }); - frappe.utils.play_sound("error"); - }).then(() => { - frappe.run_serially([ - () => frappe.dom.freeze(), - () => this.make_new_invoice(), - () => frappe.dom.unfreeze(), - ]); - }); } close_pos() { if (!this.$components_wrapper.is(":visible")) return; - let voucher = frappe.model.get_new_doc('POS Closing Entry'); + let voucher = frappe.model.get_new_doc("POS Closing Entry"); voucher.pos_profile = this.frm.doc.pos_profile; voucher.user = frappe.session.user; voucher.company = this.frm.doc.company; voucher.pos_opening_entry = this.pos_opening; voucher.period_end_date = frappe.datetime.now_datetime(); voucher.posting_date = frappe.datetime.now_date(); - frappe.set_route('Form', 'POS Closing Entry', voucher.name); + frappe.set_route("Form", "POS Closing Entry", voucher.name); } init_item_selector() { @@ -234,11 +258,11 @@ erpnext.PointOfSale.Controller = class { pos_profile: this.pos_profile, settings: this.settings, events: { - item_selected: args => this.on_cart_update(args), + item_selected: (args) => this.on_cart_update(args), - get_frm: () => this.frm || {} - } - }) + get_frm: () => this.frm || {}, + }, + }); } init_item_cart() { @@ -263,9 +287,9 @@ erpnext.PointOfSale.Controller = class { this.customer_details = details; // will add/remove LP payment method this.payment.render_loyalty_points_payment_mode(); - } - } - }) + }, + }, + }); } init_item_details() { @@ -286,7 +310,7 @@ erpnext.PointOfSale.Controller = class { const args = { field, value, - item: this.item_details.current_item + item: this.item_details.current_item, }; return this.on_cart_update(args); } @@ -303,24 +327,28 @@ erpnext.PointOfSale.Controller = class { this.cart.toggle_numpad_field_edit(fieldname); }, set_value_in_current_cart_item: (selector, value) => { - this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item); + this.cart.update_selector_value_in_cart_item( + selector, + value, + this.item_details.current_item + ); }, clone_new_batch_item_in_frm: (batch_serial_map, item) => { // called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches // for each unique batch new item row is added in the form & cart - Object.keys(batch_serial_map).forEach(batch => { - const item_to_clone = this.frm.doc.items.find(i => i.name == item.name); + Object.keys(batch_serial_map).forEach((batch) => { + const item_to_clone = this.frm.doc.items.find((i) => i.name == item.name); const new_row = this.frm.add_child("items", { ...item_to_clone }); // update new serialno and batch new_row.batch_no = batch; new_row.serial_no = batch_serial_map[batch].join(`\n`); new_row.qty = batch_serial_map[batch].length; - this.frm.doc.items.forEach(row => { + this.frm.doc.items.forEach((row) => { if (item.item_code === row.item_code) { this.update_cart_html(row); } }); - }) + }); }, remove_item_from_cart: () => this.remove_item_from_cart(), get_item_stock_map: () => this.item_stock_map, @@ -329,8 +357,8 @@ erpnext.PointOfSale.Controller = class { this.cart.prev_action = null; this.cart.toggle_item_highlight(); }, - get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse) - } + get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse), + }, }); } @@ -344,7 +372,9 @@ erpnext.PointOfSale.Controller = class { toggle_other_sections: (show) => { if (show) { - this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : ''; + this.item_details.$component.is(":visible") + ? this.item_details.$component.css("display", "none") + : ""; this.item_selector.toggle_component(false); } else { this.item_selector.toggle_component(true); @@ -352,18 +382,17 @@ erpnext.PointOfSale.Controller = class { }, submit_invoice: () => { - this.frm.savesubmit() - .then((r) => { - this.toggle_components(false); - this.order_summary.toggle_component(true); - this.order_summary.load_summary_of(this.frm.doc, true); - frappe.show_alert({ - indicator: 'green', - message: __('POS invoice {0} created succesfully', [r.doc.name]) - }); + this.frm.savesubmit().then((r) => { + this.toggle_components(false); + this.order_summary.toggle_component(true); + this.order_summary.load_summary_of(this.frm.doc, true); + frappe.show_alert({ + indicator: "green", + message: __("POS invoice {0} created succesfully", [r.doc.name]), }); - } - } + }); + }, + }, }); } @@ -372,13 +401,13 @@ erpnext.PointOfSale.Controller = class { wrapper: this.$components_wrapper, events: { open_invoice_data: (name) => { - frappe.db.get_doc('POS Invoice', name).then((doc) => { + frappe.db.get_doc("POS Invoice", name).then((doc) => { this.order_summary.load_summary_of(doc); }); }, - reset_summary: () => this.order_summary.toggle_summary_placeholder(true) - } - }) + reset_summary: () => this.order_summary.toggle_summary_placeholder(true), + }, + }); } init_order_summary() { @@ -389,11 +418,11 @@ erpnext.PointOfSale.Controller = class { process_return: (name) => { this.recent_order_list.toggle_component(false); - frappe.db.get_doc('POS Invoice', name).then((doc) => { + frappe.db.get_doc("POS Invoice", name).then((doc) => { frappe.run_serially([ () => this.make_return_invoice(doc), () => this.cart.load_invoice(), - () => this.item_selector.toggle_component(true) + () => this.item_selector.toggle_component(true), ]); }); }, @@ -401,9 +430,9 @@ erpnext.PointOfSale.Controller = class { this.recent_order_list.toggle_component(false); frappe.run_serially([ () => this.frm.refresh(name), - () => this.frm.call('reset_mode_of_payments'), + () => this.frm.call("reset_mode_of_payments"), () => this.cart.load_invoice(), - () => this.item_selector.toggle_component(true) + () => this.item_selector.toggle_component(true), ]); }, delete_order: (name) => { @@ -418,9 +447,9 @@ erpnext.PointOfSale.Controller = class { () => this.item_selector.toggle_component(true), () => frappe.dom.unfreeze(), ]); - } - } - }) + }, + }, + }); } toggle_recent_order_list(show) { @@ -434,7 +463,7 @@ erpnext.PointOfSale.Controller = class { this.item_selector.toggle_component(show); // do not show item details or payment if recent order is toggled off - !show ? (this.item_details.toggle_component(false) || this.payment.toggle_component(false)) : ''; + !show ? this.item_details.toggle_component(false) || this.payment.toggle_component(false) : ""; } make_new_invoice() { @@ -444,23 +473,23 @@ erpnext.PointOfSale.Controller = class { () => this.set_pos_profile_data(), () => this.set_pos_profile_status(), () => this.cart.load_invoice(), - () => frappe.dom.unfreeze() + () => frappe.dom.unfreeze(), ]); } make_sales_invoice_frm() { - const doctype = 'POS Invoice'; - return new Promise(resolve => { + const doctype = "POS Invoice"; + return new Promise((resolve) => { if (this.frm) { this.frm = this.get_new_frm(this.frm); this.frm.doc.items = []; - this.frm.doc.is_pos = 1 + this.frm.doc.is_pos = 1; resolve(); } else { frappe.model.with_doctype(doctype, () => { this.frm = this.get_new_frm(); this.frm.doc.items = []; - this.frm.doc.is_pos = 1 + this.frm.doc.is_pos = 1; resolve(); }); } @@ -468,8 +497,8 @@ erpnext.PointOfSale.Controller = class { } get_new_frm(_frm) { - const doctype = 'POS Invoice'; - const page = $('
              '); + const doctype = "POS Invoice"; + const page = $("
              "); const frm = _frm || new frappe.ui.form.Form(doctype, page, false); const name = frappe.model.make_new_doc_and_get_name(doctype, true); frm.refresh(name); @@ -484,8 +513,8 @@ erpnext.PointOfSale.Controller = class { return frappe.call({ method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return", args: { - 'source_name': doc.name, - 'target_doc': this.frm.doc + source_name: doc.name, + target_doc: this.frm.doc, }, callback: (r) => { frappe.model.sync(r.message); @@ -493,13 +522,16 @@ erpnext.PointOfSale.Controller = class { this.set_pos_profile_data().then(() => { frappe.dom.unfreeze(); }); - } + }, }); } set_pos_profile_data() { if (this.company && !this.frm.doc.company) this.frm.doc.company = this.company; - if ((this.pos_profile && !this.frm.doc.pos_profile) | (this.frm.doc.is_return && this.pos_profile != this.frm.doc.pos_profile)) { + if ( + (this.pos_profile && !this.frm.doc.pos_profile) | + (this.frm.doc.is_return && this.pos_profile != this.frm.doc.pos_profile) + ) { this.frm.doc.pos_profile = this.pos_profile; } @@ -520,16 +552,15 @@ erpnext.PointOfSale.Controller = class { item_row = this.get_item_from_frm(item); const item_row_exists = !$.isEmptyObject(item_row); - const from_selector = field === 'qty' && value === "+1"; - if (from_selector) - value = flt(item_row.stock_qty) + flt(value); + const from_selector = field === "qty" && value === "+1"; + if (from_selector) value = flt(item_row.stock_qty) + flt(value); if (item_row_exists) { - if (field === 'qty') - value = flt(value); + if (field === "qty") value = flt(value); - if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) { - const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value; + if (["qty", "conversion_factor"].includes(field) && value > 0 && !this.allow_negative_stock) { + const qty_needed = + field === "qty" ? value * item_row.conversion_factor : item_row.qty * value; await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); } @@ -537,29 +568,25 @@ erpnext.PointOfSale.Controller = class { await frappe.model.set_value(item_row.doctype, item_row.name, field, value); this.update_cart_html(item_row); } - } else { - if (!this.frm.doc.customer) - return this.raise_customer_selection_alert(); + if (!this.frm.doc.customer) return this.raise_customer_selection_alert(); const { item_code, batch_no, serial_no, rate } = item; - if (!item_code) - return; + if (!item_code) return; const new_item = { item_code, batch_no, rate, [field]: value }; if (serial_no) { await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); - new_item['serial_no'] = serial_no; + new_item["serial_no"] = serial_no; } - if (field === 'serial_no') - new_item['qty'] = value.split(`\n`).length || 0; + if (field === "serial_no") new_item["qty"] = value.split(`\n`).length || 0; - item_row = this.frm.add_child('items', new_item); + item_row = this.frm.add_child("items", new_item); - if (field === 'qty' && value !== 0 && !this.allow_negative_stock) { + if (field === "qty" && value !== 0 && !this.allow_negative_stock) { const qty_needed = value * item_row.conversion_factor; await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); } @@ -568,13 +595,14 @@ erpnext.PointOfSale.Controller = class { this.update_cart_html(item_row); - if (this.item_details.$component.is(':visible')) - this.edit_item_details_of(item_row); + if (this.item_details.$component.is(":visible")) this.edit_item_details_of(item_row); - if (this.check_serial_batch_selection_needed(item_row) && !this.item_details.$component.is(':visible')) + if ( + this.check_serial_batch_selection_needed(item_row) && + !this.item_details.$component.is(":visible") + ) this.edit_item_details_of(item_row); } - } catch (error) { console.log(error); } finally { @@ -586,8 +614,8 @@ erpnext.PointOfSale.Controller = class { raise_customer_selection_alert() { frappe.dom.unfreeze(); frappe.show_alert({ - message: __('You must select a customer before adding an item.'), - indicator: 'orange' + message: __("You must select a customer before adding an item."), + indicator: "orange", }); frappe.utils.play_sound("error"); } @@ -595,17 +623,18 @@ erpnext.PointOfSale.Controller = class { get_item_from_frm({ name, item_code, batch_no, uom, rate }) { let item_row = null; if (name) { - item_row = this.frm.doc.items.find(i => i.name == name); + item_row = this.frm.doc.items.find((i) => i.name == name); } else { // if item is clicked twice from item selector // then "item_code, batch_no, uom, rate" will help in getting the exact item // to increase the qty by one const has_batch_no = batch_no; item_row = this.frm.doc.items.find( - i => i.item_code === item_code - && (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) - && (i.uom === uom) - && (i.rate == rate) + (i) => + i.item_code === item_code && + (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) && + i.uom === uom && + i.rate == rate ); } @@ -633,16 +662,19 @@ erpnext.PointOfSale.Controller = class { const no_serial_selected = !item_row.serial_no; const no_batch_selected = !item_row.batch_no; - if ((serialized && no_serial_selected) || (batched && no_batch_selected) || - (serialized && batched && (no_batch_selected || no_serial_selected))) { + if ( + (serialized && no_serial_selected) || + (batched && no_batch_selected) || + (serialized && batched && (no_batch_selected || no_serial_selected)) + ) { return true; } return false; } async trigger_new_item_events(item_row) { - await this.frm.script_manager.trigger('item_code', item_row.doctype, item_row.name); - await this.frm.script_manager.trigger('qty', item_row.doctype, item_row.name); + await this.frm.script_manager.trigger("item_code", item_row.doctype, item_row.name); + await this.frm.script_manager.trigger("qty", item_row.doctype, item_row.name); } async check_stock_availability(item_row, qty_needed, warehouse) { @@ -653,21 +685,27 @@ erpnext.PointOfSale.Controller = class { frappe.dom.unfreeze(); const bold_item_code = item_row.item_code.bold(); const bold_warehouse = warehouse.bold(); - const bold_available_qty = available_qty.toString().bold() + const bold_available_qty = available_qty.toString().bold(); if (!(available_qty > 0)) { if (is_stock_item) { frappe.model.clear_doc(item_row.doctype, item_row.name); frappe.throw({ title: __("Not Available"), - message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) + message: __("Item Code: {0} is not available under warehouse {1}.", [ + bold_item_code, + bold_warehouse, + ]), }); } else { return; } } else if (is_stock_item && available_qty < qty_needed) { frappe.throw({ - message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), - indicator: 'orange' + message: __( + "Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.", + [bold_item_code, bold_warehouse, bold_available_qty] + ), + indicator: "orange", }); frappe.utils.play_sound("error"); } @@ -676,13 +714,15 @@ erpnext.PointOfSale.Controller = class { async check_serial_no_availablilty(item_code, warehouse, serial_no) { const method = "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos"; - const args = {filters: { item_code, warehouse }} + const args = { filters: { item_code, warehouse } }; const res = await frappe.call({ method, args }); if (res.message.includes(serial_no)) { frappe.throw({ title: __("Not Available"), - message: __('Serial No: {0} has already been transacted into another POS Invoice.', [serial_no.bold()]) + message: __("Serial No: {0} has already been transacted into another POS Invoice.", [ + serial_no.bold(), + ]), }); } } @@ -692,21 +732,20 @@ erpnext.PointOfSale.Controller = class { return frappe.call({ method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability", args: { - 'item_code': item_code, - 'warehouse': warehouse, + item_code: item_code, + warehouse: warehouse, }, callback(res) { - if (!me.item_stock_map[item_code]) - me.item_stock_map[item_code] = {}; + if (!me.item_stock_map[item_code]) me.item_stock_map[item_code] = {}; me.item_stock_map[item_code][warehouse] = res.message; - } + }, }); } update_item_field(value, field_or_action) { - if (field_or_action === 'checkout') { + if (field_or_action === "checkout") { this.item_details.toggle_item_details_section(null); - } else if (field_or_action === 'remove') { + } else if (field_or_action === "remove") { this.remove_item_from_cart(); } else { const field_control = this.item_details[`${field_or_action}_control`]; @@ -720,26 +759,28 @@ erpnext.PointOfSale.Controller = class { frappe.dom.freeze(); const { doctype, name, current_item } = this.item_details; - return frappe.model.set_value(doctype, name, 'qty', 0) + return frappe.model + .set_value(doctype, name, "qty", 0) .then(() => { frappe.model.clear_doc(doctype, name); this.update_cart_html(current_item, true); this.item_details.toggle_item_details_section(null); frappe.dom.unfreeze(); }) - .catch(e => console.log(e)); + .catch((e) => console.log(e)); } async save_and_checkout() { if (this.frm.is_dirty()) { let save_error = false; - await this.frm.save(null, null, null, () => save_error = true); + await this.frm.save(null, null, null, () => (save_error = true)); // only move to payment section if save is successful !save_error && this.payment.checkout(); // show checkout button on error - save_error && setTimeout(() => { - this.cart.toggle_checkout_btn(true); - }, 300); // wait for save to finish + save_error && + setTimeout(() => { + this.cart.toggle_checkout_btn(true); + }, 300); // wait for save to finish } else { this.payment.checkout(); } diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index eacf480ef8f..08bebd11252 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -18,11 +18,9 @@ erpnext.PointOfSale.ItemCart = class { } prepare_dom() { - this.wrapper.append( - `
              ` - ) + this.wrapper.append(`
              `); - this.$component = this.wrapper.find('.customer-cart-container'); + this.$component = this.wrapper.find(".customer-cart-container"); } init_child_components() { @@ -31,16 +29,14 @@ erpnext.PointOfSale.ItemCart = class { } init_customer_selector() { - this.$component.append( - `
              ` - ) - this.$customer_section = this.$component.find('.customer-section'); + this.$component.append(`
              `); + this.$customer_section = this.$component.find(".customer-section"); this.make_customer_selector(); } reset_customer_selector() { const frm = this.events.get_frm(); - frm.set_value('customer', ''); + frm.set_value("customer", ""); this.make_customer_selector(); this.customer_field.set_focus(); } @@ -49,11 +45,11 @@ erpnext.PointOfSale.ItemCart = class { this.$component.append( `
              -
              ${__('Item Cart')}
              +
              ${__("Item Cart")}
              -
              ${__('Item')}
              -
              ${__('Quantity')}
              -
              ${__('Amount')}
              +
              ${__("Item")}
              +
              ${__("Quantity")}
              +
              ${__("Amount")}
              @@ -61,7 +57,7 @@ erpnext.PointOfSale.ItemCart = class {
              ` ); - this.$cart_container = this.$component.find('.cart-container'); + this.$cart_container = this.$component.find(".cart-container"); this.make_cart_totals_section(); this.make_cart_items_section(); @@ -69,39 +65,35 @@ erpnext.PointOfSale.ItemCart = class { } make_cart_items_section() { - this.$cart_header = this.$component.find('.cart-header'); - this.$cart_items_wrapper = this.$component.find('.cart-items-section'); + this.$cart_header = this.$component.find(".cart-header"); + this.$cart_items_wrapper = this.$component.find(".cart-items-section"); this.make_no_items_placeholder(); } make_no_items_placeholder() { - this.$cart_header.css('display', 'none'); - this.$cart_items_wrapper.html( - `
              ${__('No items in cart')}
              ` - ); + this.$cart_header.css("display", "none"); + this.$cart_items_wrapper.html(`
              ${__("No items in cart")}
              `); } get_discount_icon() { - return ( - ` + return ` - ` - ); + `; } make_cart_totals_section() { - this.$totals_section = this.$component.find('.cart-totals-section'); + this.$totals_section = this.$component.find(".cart-totals-section"); this.$totals_section.append( `
              - ${this.get_discount_icon()} ${__('Add Discount')} + ${this.get_discount_icon()} ${__("Add Discount")}
              -
              ${__('Total Items')}
              +
              ${__("Total Items")}
              0.00
              @@ -110,39 +102,39 @@ erpnext.PointOfSale.ItemCart = class {
              -
              ${__('Grand Total')}
              +
              ${__("Grand Total")}
              0.00
              -
              ${__('Checkout')}
              -
              ${__('Edit Cart')}
              ` - ) +
              ${__("Checkout")}
              +
              ${__("Edit Cart")}
              ` + ); this.$add_discount_elem = this.$component.find(".add-discount-wrapper"); } make_cart_numpad() { - this.$numpad_section = this.$component.find('.numpad-section'); + this.$numpad_section = this.$component.find(".numpad-section"); this.number_pad = new erpnext.PointOfSale.NumberPad({ wrapper: this.$numpad_section, events: { - numpad_event: this.on_numpad_event.bind(this) + numpad_event: this.on_numpad_event.bind(this), }, cols: 5, keys: [ - [ 1, 2, 3, 'Quantity' ], - [ 4, 5, 6, 'Discount' ], - [ 7, 8, 9, 'Rate' ], - [ '.', 0, 'Delete', 'Remove' ] + [1, 2, 3, "Quantity"], + [4, 5, 6, "Discount"], + [7, 8, 9, "Rate"], + [".", 0, "Delete", "Remove"], ], css_classes: [ - [ '', '', '', 'col-span-2' ], - [ '', '', '', 'col-span-2' ], - [ '', '', '', 'col-span-2' ], - [ '', '', '', 'col-span-2 remove-btn' ] + ["", "", "", "col-span-2"], + ["", "", "", "col-span-2"], + ["", "", "", "col-span-2"], + ["", "", "", "col-span-2 remove-btn"], ], - fieldnames_map: { 'Quantity': 'qty', 'Discount': 'discount_percentage' } - }) + fieldnames_map: { Quantity: "qty", Discount: "discount_percentage" }, + }); this.$numpad_section.prepend( `
              @@ -150,49 +142,49 @@ erpnext.PointOfSale.ItemCart = class {
              ` - ) + ); this.$numpad_section.append( - `
              ${__('Checkout')}
              ` - ) + `
              ${__("Checkout")}
              ` + ); } bind_events() { const me = this; - this.$customer_section.on('click', '.reset-customer-btn', function () { + this.$customer_section.on("click", ".reset-customer-btn", function () { me.reset_customer_selector(); }); - this.$customer_section.on('click', '.close-details-btn', function () { + this.$customer_section.on("click", ".close-details-btn", function () { me.toggle_customer_info(false); }); - this.$customer_section.on('click', '.customer-display', function(e) { - if ($(e.target).closest('.reset-customer-btn').length) return; + this.$customer_section.on("click", ".customer-display", function (e) { + if ($(e.target).closest(".reset-customer-btn").length) return; - const show = me.$cart_container.is(':visible'); + const show = me.$cart_container.is(":visible"); me.toggle_customer_info(show); }); - this.$cart_items_wrapper.on('click', '.cart-item-wrapper', function() { + this.$cart_items_wrapper.on("click", ".cart-item-wrapper", function () { const $cart_item = $(this); me.toggle_item_highlight(this); - const payment_section_hidden = !me.$totals_section.find('.edit-cart-btn').is(':visible'); + const payment_section_hidden = !me.$totals_section.find(".edit-cart-btn").is(":visible"); if (!payment_section_hidden) { // payment section is visible // edit cart first and then open item details section me.$totals_section.find(".edit-cart-btn").click(); } - const item_row_name = unescape($cart_item.attr('data-row-name')); + const item_row_name = unescape($cart_item.attr("data-row-name")); me.events.cart_item_clicked({ name: item_row_name }); - this.numpad_value = ''; + this.numpad_value = ""; }); - this.$component.on('click', '.checkout-btn', async function() { - if ($(this).attr('style').indexOf('--blue-500') == -1) return; + this.$component.on("click", ".checkout-btn", async function () { + if ($(this).attr("style").indexOf("--blue-500") == -1) return; await me.events.checkout(); me.toggle_checkout_btn(false); @@ -200,18 +192,18 @@ erpnext.PointOfSale.ItemCart = class { me.allow_discount_change && me.$add_discount_elem.removeClass("d-none"); }); - this.$totals_section.on('click', '.edit-cart-btn', () => { + this.$totals_section.on("click", ".edit-cart-btn", () => { this.events.edit_cart(); this.toggle_checkout_btn(true); }); - this.$component.on('click', '.add-discount-wrapper', () => { - const can_edit_discount = this.$add_discount_elem.find('.edit-discount-btn').length; + this.$component.on("click", ".add-discount-wrapper", () => { + const can_edit_discount = this.$add_discount_elem.find(".edit-discount-btn").length; - if(!this.discount_field || can_edit_discount) this.show_discount_control(); + if (!this.discount_field || can_edit_discount) this.show_discount_control(); }); - frappe.ui.form.on("POS Invoice", "paid_amount", frm => { + frappe.ui.form.on("POS Invoice", "paid_amount", (frm) => { // called when discount is applied this.update_totals_section(frm); }); @@ -220,43 +212,49 @@ erpnext.PointOfSale.ItemCart = class { attach_shortcuts() { for (let row of this.number_pad.keys) { for (let btn of row) { - if (typeof btn !== 'string') continue; // do not make shortcuts for numbers + if (typeof btn !== "string") continue; // do not make shortcuts for numbers let shortcut_key = `ctrl+${frappe.scrub(String(btn))[0]}`; - if (btn === 'Delete') shortcut_key = 'ctrl+backspace'; - if (btn === 'Remove') shortcut_key = 'shift+ctrl+backspace' - if (btn === '.') shortcut_key = 'ctrl+>'; + if (btn === "Delete") shortcut_key = "ctrl+backspace"; + if (btn === "Remove") shortcut_key = "shift+ctrl+backspace"; + if (btn === ".") shortcut_key = "ctrl+>"; // to account for fieldname map - const fieldname = this.number_pad.fieldnames[btn] ? this.number_pad.fieldnames[btn] : - typeof btn === 'string' ? frappe.scrub(btn) : btn; + const fieldname = this.number_pad.fieldnames[btn] + ? this.number_pad.fieldnames[btn] + : typeof btn === "string" + ? frappe.scrub(btn) + : btn; - let shortcut_label = shortcut_key.split('+').map(frappe.utils.to_title_case).join('+'); - shortcut_label = frappe.utils.is_mac() ? shortcut_label.replace('Ctrl', '⌘') : shortcut_label; - this.$numpad_section.find(`.numpad-btn[data-button-value="${fieldname}"]`).attr("title", shortcut_label); + let shortcut_label = shortcut_key.split("+").map(frappe.utils.to_title_case).join("+"); + shortcut_label = frappe.utils.is_mac() ? shortcut_label.replace("Ctrl", "⌘") : shortcut_label; + this.$numpad_section + .find(`.numpad-btn[data-button-value="${fieldname}"]`) + .attr("title", shortcut_label); frappe.ui.keys.on(`${shortcut_key}`, () => { const cart_is_visible = this.$component.is(":visible"); if (cart_is_visible && this.item_is_selected && this.$numpad_section.is(":visible")) { this.$numpad_section.find(`.numpad-btn[data-button-value="${fieldname}"]`).click(); } - }) + }); } } - const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; + const ctrl_label = frappe.utils.is_mac() ? "⌘" : "Ctrl"; this.$component.find(".checkout-btn").attr("title", `${ctrl_label}+Enter`); frappe.ui.keys.add_shortcut({ shortcut: "ctrl+enter", action: () => this.$component.find(".checkout-btn").click(), - condition: () => this.$component.is(":visible") && !this.$totals_section.find('.edit-cart-btn').is(':visible'), + condition: () => + this.$component.is(":visible") && !this.$totals_section.find(".edit-cart-btn").is(":visible"), description: __("Checkout Order / Submit Order / New Order"), ignore_inputs: true, - page: cur_page.page.page + page: cur_page.page.page, }); this.$component.find(".edit-cart-btn").attr("title", `${ctrl_label}+E`); frappe.ui.keys.on("ctrl+e", () => { const item_cart_visible = this.$component.is(":visible"); - const checkout_btn_invisible = !this.$totals_section.find('.checkout-btn').is('visible'); + const checkout_btn_invisible = !this.$totals_section.find(".checkout-btn").is("visible"); if (item_cart_visible && checkout_btn_invisible) { this.$component.find(".edit-cart-btn").click(); } @@ -268,7 +266,7 @@ erpnext.PointOfSale.ItemCart = class { condition: () => this.$add_discount_elem.is(":visible"), description: __("Add Order Discount"), ignore_inputs: true, - page: cur_page.page.page + page: cur_page.page.page, }); frappe.ui.keys.on("escape", () => { const item_cart_visible = this.$component.is(":visible"); @@ -284,11 +282,11 @@ erpnext.PointOfSale.ItemCart = class { if (!item || item_is_highlighted) { this.item_is_selected = false; - this.$cart_container.find('.cart-item-wrapper').css("background-color", ""); + this.$cart_container.find(".cart-item-wrapper").css("background-color", ""); } else { $cart_item.css("background-color", "var(--gray-50)"); this.item_is_selected = true; - this.$cart_container.find('.cart-item-wrapper').not(item).css("background-color", ""); + this.$cart_container.find(".cart-item-wrapper").not(item).css("background-color", ""); } } @@ -297,38 +295,38 @@ erpnext.PointOfSale.ItemCart = class {
              `); const me = this; - const query = { query: 'erpnext.controllers.queries.customer_query' }; + const query = { query: "erpnext.controllers.queries.customer_query" }; const allowed_customer_group = this.allowed_customer_groups || []; if (allowed_customer_group.length) { query.filters = { - customer_group: ['in', allowed_customer_group] - } + customer_group: ["in", allowed_customer_group], + }; } this.customer_field = frappe.ui.form.make_control({ df: { - label: __('Customer'), - fieldtype: 'Link', - options: 'Customer', - placeholder: __('Search by customer name, phone, email.'), + label: __("Customer"), + fieldtype: "Link", + options: "Customer", + placeholder: __("Search by customer name, phone, email."), get_query: () => query, - onchange: function() { + onchange: function () { if (this.value) { const frm = me.events.get_frm(); frappe.dom.freeze(); - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'customer', this.value); - frm.script_manager.trigger('customer', frm.doc.doctype, frm.doc.name).then(() => { + frappe.model.set_value(frm.doc.doctype, frm.doc.name, "customer", this.value); + frm.script_manager.trigger("customer", frm.doc.doctype, frm.doc.name).then(() => { frappe.run_serially([ () => me.fetch_customer_details(this.value), () => me.events.customer_details_updated(me.customer_info), () => me.update_customer_section(), () => me.update_totals_section(), - () => frappe.dom.unfreeze() + () => frappe.dom.unfreeze(), ]); - }) + }); } }, }, - parent: this.$customer_section.find('.customer-field'), + parent: this.$customer_section.find(".customer-field"), render_input: true, }); this.customer_field.toggle_label(false); @@ -337,66 +335,81 @@ erpnext.PointOfSale.ItemCart = class { fetch_customer_details(customer) { if (customer) { return new Promise((resolve) => { - frappe.db.get_value('Customer', customer, ["email_id", "mobile_no", "image", "loyalty_program"]).then(({ message }) => { - const { loyalty_program } = message; - // if loyalty program then fetch loyalty points too - if (loyalty_program) { - frappe.call({ - method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details_with_points", - args: { customer, loyalty_program, "silent": true }, - callback: (r) => { - const { loyalty_points, conversion_factor } = r.message; - if (!r.exc) { - this.customer_info = { ...message, customer, loyalty_points, conversion_factor }; - resolve(); - } - } - }); - } else { - this.customer_info = { ...message, customer }; - resolve(); - } - }); + frappe.db + .get_value("Customer", customer, ["email_id", "mobile_no", "image", "loyalty_program"]) + .then(({ message }) => { + const { loyalty_program } = message; + // if loyalty program then fetch loyalty points too + if (loyalty_program) { + frappe.call({ + method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details_with_points", + args: { customer, loyalty_program, silent: true }, + callback: (r) => { + const { loyalty_points, conversion_factor } = r.message; + if (!r.exc) { + this.customer_info = { + ...message, + customer, + loyalty_points, + conversion_factor, + }; + resolve(); + } + }, + }); + } else { + this.customer_info = { ...message, customer }; + resolve(); + } + }); }); } else { return new Promise((resolve) => { - this.customer_info = {} + this.customer_info = {}; resolve(); }); } } show_discount_control() { - this.$add_discount_elem.css({ 'padding': '0px', 'border': 'none' }); - this.$add_discount_elem.html( - `
              ` - ); + this.$add_discount_elem.css({ padding: "0px", border: "none" }); + this.$add_discount_elem.html(`
              `); const me = this; const frm = me.events.get_frm(); let discount = frm.doc.additional_discount_percentage; this.discount_field = frappe.ui.form.make_control({ df: { - label: __('Discount'), - fieldtype: 'Data', - placeholder: ( discount ? discount + '%' : __('Enter discount percentage.') ), - input_class: 'input-xs', - onchange: function() { + label: __("Discount"), + fieldtype: "Data", + placeholder: discount ? discount + "%" : __("Enter discount percentage."), + input_class: "input-xs", + onchange: function () { if (flt(this.value) != 0) { - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value)); + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "additional_discount_percentage", + flt(this.value) + ); me.hide_discount_control(this.value); } else { - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', 0); + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "additional_discount_percentage", + 0 + ); me.$add_discount_elem.css({ - 'border': '1px dashed var(--gray-500)', - 'padding': 'var(--padding-sm) var(--padding-md)' + border: "1px dashed var(--gray-500)", + padding: "var(--padding-sm) var(--padding-md)", }); - me.$add_discount_elem.html(`${me.get_discount_icon()} ${__('Add Discount')}`); + me.$add_discount_elem.html(`${me.get_discount_icon()} ${__("Add Discount")}`); me.discount_field = undefined; } }, }, - parent: this.$add_discount_elem.find('.add-discount-field'), + parent: this.$add_discount_elem.find(".add-discount-field"), render_input: true, }); this.discount_field.toggle_label(false); @@ -405,14 +418,12 @@ erpnext.PointOfSale.ItemCart = class { hide_discount_control(discount) { if (!discount) { - this.$add_discount_elem.css({ 'padding': '0px', 'border': 'none' }); - this.$add_discount_elem.html( - `
              ` - ); + this.$add_discount_elem.css({ padding: "0px", border: "none" }); + this.$add_discount_elem.html(`
              `); } else { this.$add_discount_elem.css({ - 'border': '1px dashed var(--dark-green-500)', - 'padding': 'var(--padding-sm) var(--padding-md)' + border: "1px dashed var(--dark-green-500)", + padding: "var(--padding-sm) var(--padding-md)", }); this.$add_discount_elem.html( `
              @@ -424,7 +435,7 @@ erpnext.PointOfSale.ItemCart = class { update_customer_section() { const me = this; - const { customer, email_id='', mobile_no='', image } = this.customer_info || {}; + const { customer, email_id = "", mobile_no = "", image } = this.customer_info || {}; if (customer) { this.$customer_section.html( @@ -450,7 +461,7 @@ erpnext.PointOfSale.ItemCart = class { function get_customer_description() { if (!email_id && !mobile_no) { - return `
              ${__('Click to add email / phone')}
              `; + return `
              ${__("Click to add email / phone")}
              `; } else if (email_id && !mobile_no) { return `
              ${email_id}
              `; } else if (mobile_no && !email_id) { @@ -459,7 +470,6 @@ erpnext.PointOfSale.ItemCart = class { return `
              ${email_id} - ${mobile_no}
              `; } } - } get_customer_image() { @@ -476,7 +486,9 @@ erpnext.PointOfSale.ItemCart = class { this.render_net_total(frm.doc.net_total); this.render_total_item_qty(frm.doc.items); - const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) + ? frm.doc.grand_total + : frm.doc.rounded_total; this.render_grand_total(grand_total); this.render_taxes(frm.doc.taxes); @@ -484,13 +496,13 @@ erpnext.PointOfSale.ItemCart = class { render_net_total(value) { const currency = this.events.get_frm().doc.currency; - this.$totals_section.find('.net-total-container').html( - `
              ${__('Net Total')}
              ${format_currency(value, currency)}
              ` - ) + this.$totals_section + .find(".net-total-container") + .html(`
              ${__("Net Total")}
              ${format_currency(value, currency)}
              `); - this.$numpad_section.find('.numpad-net-total').html( - `
              ${__('Net Total')}: ${format_currency(value, currency)}
              ` - ); + this.$numpad_section + .find(".numpad-net-total") + .html(`
              ${__("Net Total")}: ${format_currency(value, currency)}
              `); } render_total_item_qty(items) { @@ -499,40 +511,44 @@ erpnext.PointOfSale.ItemCart = class { total_item_qty = total_item_qty + item.qty; }); - this.$totals_section.find('.item-qty-total-container').html( - `
              ${__('Total Quantity')}
              ${total_item_qty}
              ` - ); + this.$totals_section + .find(".item-qty-total-container") + .html(`
              ${__("Total Quantity")}
              ${total_item_qty}
              `); - this.$numpad_section.find('.numpad-item-qty-total').html( - `
              ${__('Total Quantity')}: ${total_item_qty}
              ` - ); + this.$numpad_section + .find(".numpad-item-qty-total") + .html(`
              ${__("Total Quantity")}: ${total_item_qty}
              `); } render_grand_total(value) { const currency = this.events.get_frm().doc.currency; - this.$totals_section.find('.grand-total-container').html( - `
              ${__('Grand Total')}
              ${format_currency(value, currency)}
              ` - ) + this.$totals_section + .find(".grand-total-container") + .html(`
              ${__("Grand Total")}
              ${format_currency(value, currency)}
              `); - this.$numpad_section.find('.numpad-grand-total').html( - `
              ${__('Grand Total')}: ${format_currency(value, currency)}
              ` - ); + this.$numpad_section + .find(".numpad-grand-total") + .html(`
              ${__("Grand Total")}: ${format_currency(value, currency)}
              `); } render_taxes(taxes) { if (taxes.length) { const currency = this.events.get_frm().doc.currency; - const taxes_html = taxes.map(t => { - if (t.tax_amount_after_discount_amount == 0.0) return; - const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; - return `
              + const taxes_html = taxes + .map((t) => { + if (t.tax_amount_after_discount_amount == 0.0) return; + const description = /[0-9]+/.test(t.description) + ? t.description + : `${t.description} @ ${t.rate}%`; + return `
              ${description}
              ${format_currency(t.tax_amount_after_discount_amount, currency)}
              `; - }).join(''); - this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html); + }) + .join(""); + this.$totals_section.find(".taxes-container").css("display", "flex").html(taxes_html); } else { - this.$totals_section.find('.taxes-container').css('display', 'none').html(''); + this.$totals_section.find(".taxes-container").css("display", "none").html(""); } } @@ -543,7 +559,7 @@ erpnext.PointOfSale.ItemCart = class { get_item_from_frm(item) { const doc = this.events.get_frm().doc; - return doc.items.find(i => i.name == item.name); + return doc.items.find((i) => i.name == item.name); } update_item_html(item, remove_item) { @@ -556,7 +572,7 @@ erpnext.PointOfSale.ItemCart = class { this.render_cart_item(item_row, $item); } - const no_of_cart_items = this.$cart_items_wrapper.find('.cart-item-wrapper').length; + const no_of_cart_items = this.$cart_items_wrapper.find(".cart-item-wrapper").length; this.highlight_checkout_btn(no_of_cart_items > 0); this.update_empty_cart_section(no_of_cart_items); @@ -570,7 +586,7 @@ erpnext.PointOfSale.ItemCart = class { this.$cart_items_wrapper.append( `
              ` - ) + ); $item_to_update = this.get_cart_item(item_data); } @@ -583,7 +599,7 @@ erpnext.PointOfSale.ItemCart = class { ${get_description_html()}
              ${get_rate_discount_html()}` - ) + ); set_dynamic_rate_header_width(); @@ -592,8 +608,7 @@ erpnext.PointOfSale.ItemCart = class { me.$cart_header.find(".rate-amount-header").css("width", ""); me.$cart_items_wrapper.find(".item-rate-amount").css("width", ""); let max_width = rate_cols.reduce((max_width, elm) => { - if ($(elm).width() > max_width) - max_width = $(elm).width(); + if ($(elm).width() > max_width) max_width = $(elm).width(); return max_width; }, 0); @@ -613,7 +628,7 @@ erpnext.PointOfSale.ItemCart = class {
              ${format_currency(item_data.amount, currency)}
              ${format_currency(item_data.rate, currency)}
              -
              ` +
              `; } else { return `
              @@ -621,17 +636,20 @@ erpnext.PointOfSale.ItemCart = class {
              ${format_currency(item_data.rate, currency)}
              -
              ` +
            • `; } } function get_description_html() { if (item_data.description) { - if (item_data.description.indexOf('
              ') != -1) { + if (item_data.description.indexOf("
              ") != -1) { try { item_data.description = $(item_data.description).text(); } catch (error) { - item_data.description = item_data.description.replace(/
              /g, ' ').replace(/<\/div>/g, ' ').replace(/ +/g, ' '); + item_data.description = item_data.description + .replace(/
              /g, " ") + .replace(/<\/div>/g, " ") + .replace(/ +/g, " "); } } item_data.description = frappe.ellipsis(item_data.description, 45); @@ -656,7 +674,7 @@ erpnext.PointOfSale.ItemCart = class { } handle_broken_image($img) { - const item_abbr = $($img).attr('alt'); + const item_abbr = $($img).attr("alt"); $($img).parent().replaceWith(`
              ${item_abbr}
              `); } @@ -667,44 +685,48 @@ erpnext.PointOfSale.ItemCart = class { toggle_checkout_btn(show_checkout) { if (show_checkout) { - this.$totals_section.find('.checkout-btn').css('display', 'flex'); - this.$totals_section.find('.edit-cart-btn').css('display', 'none'); + this.$totals_section.find(".checkout-btn").css("display", "flex"); + this.$totals_section.find(".edit-cart-btn").css("display", "none"); } else { - this.$totals_section.find('.checkout-btn').css('display', 'none'); - this.$totals_section.find('.edit-cart-btn').css('display', 'flex'); + this.$totals_section.find(".checkout-btn").css("display", "none"); + this.$totals_section.find(".edit-cart-btn").css("display", "flex"); } } highlight_checkout_btn(toggle) { if (toggle) { - this.$add_discount_elem.css('display', 'flex'); - this.$cart_container.find('.checkout-btn').css({ - 'background-color': 'var(--blue-500)' + this.$add_discount_elem.css("display", "flex"); + this.$cart_container.find(".checkout-btn").css({ + "background-color": "var(--blue-500)", }); } else { - this.$add_discount_elem.css('display', 'none'); - this.$cart_container.find('.checkout-btn').css({ - 'background-color': 'var(--blue-200)' + this.$add_discount_elem.css("display", "none"); + this.$cart_container.find(".checkout-btn").css({ + "background-color": "var(--blue-200)", }); } } update_empty_cart_section(no_of_cart_items) { - const $no_item_element = this.$cart_items_wrapper.find('.no-item-wrapper'); + const $no_item_element = this.$cart_items_wrapper.find(".no-item-wrapper"); // if cart has items and no item is present - no_of_cart_items > 0 && $no_item_element && $no_item_element.remove() && this.$cart_header.css('display', 'flex'); + no_of_cart_items > 0 && + $no_item_element && + $no_item_element.remove() && + this.$cart_header.css("display", "flex"); no_of_cart_items === 0 && !$no_item_element.length && this.make_no_items_placeholder(); } on_numpad_event($btn) { - const current_action = $btn.attr('data-button-value'); - const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action); - const action_is_allowed = action_is_field_edit ? ( - (current_action == 'rate' && this.allow_rate_change) || - (current_action == 'discount_percentage' && this.allow_discount_change) || - (current_action == 'qty')) : true; + const current_action = $btn.attr("data-button-value"); + const action_is_field_edit = ["qty", "discount_percentage", "rate"].includes(current_action); + const action_is_allowed = action_is_field_edit + ? (current_action == "rate" && this.allow_rate_change) || + (current_action == "discount_percentage" && this.allow_discount_change) || + current_action == "qty" + : true; const action_is_pressed_twice = this.prev_action === current_action; const first_click_event = !this.prev_action; @@ -712,11 +734,11 @@ erpnext.PointOfSale.ItemCart = class { if (action_is_field_edit) { if (!action_is_allowed) { - const label = current_action == 'rate' ? 'Rate'.bold() : 'Discount'.bold(); - const message = __('Editing {0} is not allowed as per POS Profile settings', [label]); + const label = current_action == "rate" ? "Rate".bold() : "Discount".bold(); + const message = __("Editing {0} is not allowed as per POS Profile settings", [label]); frappe.show_alert({ - indicator: 'red', - message: message + indicator: "red", + message: message, }); frappe.utils.play_sound("error"); return; @@ -727,20 +749,22 @@ erpnext.PointOfSale.ItemCart = class { } else if (action_is_pressed_twice) { this.prev_action = undefined; } - this.numpad_value = ''; - - } else if (current_action === 'checkout') { + this.numpad_value = ""; + } else if (current_action === "checkout") { this.prev_action = undefined; this.toggle_item_highlight(); this.events.numpad_event(undefined, current_action); return; - } else if (current_action === 'remove') { + } else if (current_action === "remove") { this.prev_action = undefined; this.toggle_item_highlight(); this.events.numpad_event(undefined, current_action); return; } else { - this.numpad_value = current_action === 'delete' ? this.numpad_value.slice(0, -1) : this.numpad_value + current_action; + this.numpad_value = + current_action === "delete" + ? this.numpad_value.slice(0, -1) + : this.numpad_value + current_action; this.numpad_value = this.numpad_value || 0; } @@ -748,17 +772,17 @@ erpnext.PointOfSale.ItemCart = class { if (first_click_event_is_not_field_edit) { frappe.show_alert({ - indicator: 'red', - message: __('Please select a field to edit from numpad') + indicator: "red", + message: __("Please select a field to edit from numpad"), }); frappe.utils.play_sound("error"); return; } - if (flt(this.numpad_value) > 100 && this.prev_action === 'discount_percentage') { + if (flt(this.numpad_value) > 100 && this.prev_action === "discount_percentage") { frappe.show_alert({ - message: __('Discount cannot be greater than 100%'), - indicator: 'orange' + message: __("Discount cannot be greater than 100%"), + indicator: "orange", }); frappe.utils.play_sound("error"); this.numpad_value = current_action; @@ -769,48 +793,48 @@ erpnext.PointOfSale.ItemCart = class { } highlight_numpad_btn($btn, curr_action) { - const curr_action_is_highlighted = $btn.hasClass('highlighted-numpad-btn'); - const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action); + const curr_action_is_highlighted = $btn.hasClass("highlighted-numpad-btn"); + const curr_action_is_action = ["qty", "discount_percentage", "rate", "done"].includes(curr_action); if (!curr_action_is_highlighted) { - $btn.addClass('highlighted-numpad-btn'); + $btn.addClass("highlighted-numpad-btn"); } if (this.prev_action === curr_action && curr_action_is_highlighted) { // if Qty is pressed twice - $btn.removeClass('highlighted-numpad-btn'); + $btn.removeClass("highlighted-numpad-btn"); } if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) { // Order: Qty -> Rate then remove Qty highlight const prev_btn = $(`[data-button-value='${this.prev_action}']`); - prev_btn.removeClass('highlighted-numpad-btn'); + prev_btn.removeClass("highlighted-numpad-btn"); } - if (!curr_action_is_action || curr_action === 'done') { + if (!curr_action_is_action || curr_action === "done") { // if numbers are clicked setTimeout(() => { - $btn.removeClass('highlighted-numpad-btn'); + $btn.removeClass("highlighted-numpad-btn"); }, 200); } } toggle_numpad(show) { if (show) { - this.$totals_section.css('display', 'none'); - this.$numpad_section.css('display', 'flex'); + this.$totals_section.css("display", "none"); + this.$numpad_section.css("display", "flex"); } else { - this.$totals_section.css('display', 'flex'); - this.$numpad_section.css('display', 'none'); + this.$totals_section.css("display", "flex"); + this.$numpad_section.css("display", "none"); } this.reset_numpad(); } reset_numpad() { - this.numpad_value = ''; + this.numpad_value = ""; this.prev_action = undefined; - this.$numpad_section.find('.highlighted-numpad-btn').removeClass('highlighted-numpad-btn'); + this.$numpad_section.find(".highlighted-numpad-btn").removeClass("highlighted-numpad-btn"); } toggle_numpad_field_edit(fieldname) { - if (['qty', 'discount_percentage', 'rate'].includes(fieldname)) { + if (["qty", "discount_percentage", "rate"].includes(fieldname)) { this.$numpad_section.find(`[data-button-value="${fieldname}"]`).click(); } } @@ -819,12 +843,12 @@ erpnext.PointOfSale.ItemCart = class { if (show) { const { customer } = this.customer_info || {}; - this.$cart_container.css('display', 'none'); + this.$cart_container.css("display", "none"); this.$customer_section.css({ - 'height': '100%', - 'padding-top': '0px' + height: "100%", + "padding-top": "0px", }); - this.$customer_section.find('.customer-details').html( + this.$customer_section.find(".customer-details").html( `
              Contact Details
              @@ -853,12 +877,11 @@ erpnext.PointOfSale.ItemCart = class { this.render_customer_fields(); this.fetch_customer_transactions(); - } else { - this.$cart_container.css('display', 'flex'); + this.$cart_container.css("display", "flex"); this.$customer_section.css({ - 'height': '', - 'padding-top': '' + height: "", + "padding-top": "", }); this.update_customer_section(); @@ -866,100 +889,107 @@ erpnext.PointOfSale.ItemCart = class { } render_customer_fields() { - const $customer_form = this.$customer_section.find('.customer-fields-container'); + const $customer_form = this.$customer_section.find(".customer-fields-container"); - const dfs = [{ - fieldname: 'email_id', - label: __('Email'), - fieldtype: 'Data', - options: 'email', - placeholder: __("Enter customer's email") - },{ - fieldname: 'mobile_no', - label: __('Phone Number'), - fieldtype: 'Data', - placeholder: __("Enter customer's phone number") - },{ - fieldname: 'loyalty_program', - label: __('Loyalty Program'), - fieldtype: 'Link', - options: 'Loyalty Program', - placeholder: __("Select Loyalty Program") - },{ - fieldname: 'loyalty_points', - label: __('Loyalty Points'), - fieldtype: 'Data', - read_only: 1 - }]; + const dfs = [ + { + fieldname: "email_id", + label: __("Email"), + fieldtype: "Data", + options: "email", + placeholder: __("Enter customer's email"), + }, + { + fieldname: "mobile_no", + label: __("Phone Number"), + fieldtype: "Data", + placeholder: __("Enter customer's phone number"), + }, + { + fieldname: "loyalty_program", + label: __("Loyalty Program"), + fieldtype: "Link", + options: "Loyalty Program", + placeholder: __("Select Loyalty Program"), + }, + { + fieldname: "loyalty_points", + label: __("Loyalty Points"), + fieldtype: "Data", + read_only: 1, + }, + ]; const me = this; - dfs.forEach(df => { + dfs.forEach((df) => { this[`customer_${df.fieldname}_field`] = frappe.ui.form.make_control({ - df: { ...df, - onchange: handle_customer_field_change, - }, + df: { ...df, onchange: handle_customer_field_change }, parent: $customer_form.find(`.${df.fieldname}-field`), render_input: true, }); this[`customer_${df.fieldname}_field`].set_value(this.customer_info[df.fieldname]); - }) + }); function handle_customer_field_change() { const current_value = me.customer_info[this.df.fieldname]; const current_customer = me.customer_info.customer; - if (this.value && current_value != this.value && this.df.fieldname != 'loyalty_points') { + if (this.value && current_value != this.value && this.df.fieldname != "loyalty_points") { frappe.call({ - method: 'erpnext.selling.page.point_of_sale.point_of_sale.set_customer_info', + method: "erpnext.selling.page.point_of_sale.point_of_sale.set_customer_info", args: { fieldname: this.df.fieldname, customer: current_customer, - value: this.value + value: this.value, }, callback: (r) => { - if(!r.exc) { + if (!r.exc) { me.customer_info[this.df.fieldname] = this.value; frappe.show_alert({ message: __("Customer contact updated successfully."), - indicator: 'green' + indicator: "green", }); frappe.utils.play_sound("submit"); } - } + }, }); } } } fetch_customer_transactions() { - frappe.db.get_list('POS Invoice', { - filters: { customer: this.customer_info.customer, docstatus: 1 }, - fields: ['name', 'grand_total', 'status', 'posting_date', 'posting_time', 'currency'], - limit: 20 - }).then((res) => { - const transaction_container = this.$customer_section.find('.customer-transactions'); + frappe.db + .get_list("POS Invoice", { + filters: { customer: this.customer_info.customer, docstatus: 1 }, + fields: ["name", "grand_total", "status", "posting_date", "posting_time", "currency"], + limit: 20, + }) + .then((res) => { + const transaction_container = this.$customer_section.find(".customer-transactions"); - if (!res.length) { - transaction_container.html( - `
              No recent transactions found
              ` - ) - return; - }; + if (!res.length) { + transaction_container.html( + `
              No recent transactions found
              ` + ); + return; + } - const elapsed_time = moment(res[0].posting_date+" "+res[0].posting_time).fromNow(); - this.$customer_section.find('.customer-desc').html(`Last transacted ${elapsed_time}`); + const elapsed_time = moment(res[0].posting_date + " " + res[0].posting_time).fromNow(); + this.$customer_section.find(".customer-desc").html(`Last transacted ${elapsed_time}`); - res.forEach(invoice => { - const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma"); - let indicator_color = { - 'Paid': 'green', - 'Draft': 'red', - 'Return': 'gray', - 'Consolidated': 'blue' - }; + res.forEach((invoice) => { + const posting_datetime = moment(invoice.posting_date + " " + invoice.posting_time).format( + "Do MMMM, h:mma" + ); + let indicator_color = { + Paid: "green", + Draft: "red", + Return: "gray", + Consolidated: "blue", + }; - transaction_container.append( - `
              + transaction_container.append( + `
              ${invoice.name}
              ${posting_datetime}
              @@ -976,17 +1006,17 @@ erpnext.PointOfSale.ItemCart = class {
              ` - ) + ); + }); }); - }); } attach_refresh_field_event(frm) { - $(frm.wrapper).off('refresh-fields'); - $(frm.wrapper).on('refresh-fields', () => { + $(frm.wrapper).off("refresh-fields"); + $(frm.wrapper).on("refresh-fields", () => { if (frm.doc.items.length) { - this.$cart_items_wrapper.html(''); - frm.doc.items.forEach(item => { + this.$cart_items_wrapper.html(""); + frm.doc.items.forEach((item) => { this.update_item_html(item); }); } @@ -1004,9 +1034,9 @@ erpnext.PointOfSale.ItemCart = class { this.update_customer_section(); }); - this.$cart_items_wrapper.html(''); + this.$cart_items_wrapper.html(""); if (frm.doc.items.length) { - frm.doc.items.forEach(item => { + frm.doc.items.forEach((item) => { this.update_item_html(item); }); } else { @@ -1016,19 +1046,18 @@ erpnext.PointOfSale.ItemCart = class { this.update_totals_section(frm); - if(frm.doc.docstatus === 1) { - this.$totals_section.find('.checkout-btn').css('display', 'none'); - this.$totals_section.find('.edit-cart-btn').css('display', 'none'); + if (frm.doc.docstatus === 1) { + this.$totals_section.find(".checkout-btn").css("display", "none"); + this.$totals_section.find(".edit-cart-btn").css("display", "none"); } else { - this.$totals_section.find('.checkout-btn').css('display', 'flex'); - this.$totals_section.find('.edit-cart-btn').css('display', 'none'); + this.$totals_section.find(".checkout-btn").css("display", "flex"); + this.$totals_section.find(".edit-cart-btn").css("display", "none"); } this.toggle_component(true); } toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); + show ? this.$component.css("display", "flex") : this.$component.css("display", "none"); } - -} +}; diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index f9b5bb2e452..9a6d2cc36ae 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -18,17 +18,15 @@ erpnext.PointOfSale.ItemDetails = class { } prepare_dom() { - this.wrapper.append( - `
              ` - ) + this.wrapper.append(`
              `); - this.$component = this.wrapper.find('.item-details-container'); + this.$component = this.wrapper.find(".item-details-container"); } init_child_components() { this.$component.html( `
              -
              ${__('Item Details')}
              +
              ${__("Item Details")}
              @@ -45,14 +43,14 @@ erpnext.PointOfSale.ItemDetails = class {
              ` - ) + ); - this.$item_name = this.$component.find('.item-name'); - this.$item_description = this.$component.find('.item-desc'); - this.$item_price = this.$component.find('.item-price'); - this.$item_image = this.$component.find('.item-image'); - this.$form_container = this.$component.find('.form-container'); - this.$dicount_section = this.$component.find('.discount-section'); + this.$item_name = this.$component.find(".item-name"); + this.$item_description = this.$component.find(".item-desc"); + this.$item_price = this.$component.find(".item-price"); + this.$item_image = this.$component.find(".item-image"); + this.$form_container = this.$component.find(".form-container"); + this.$dicount_section = this.$component.find(".discount-section"); } compare_with_current_item(item) { @@ -95,7 +93,7 @@ erpnext.PointOfSale.ItemDetails = class { validate_serial_batch_item() { const doc = this.events.get_frm().doc; - const item_row = doc.items.find(item => item.name === this.name); + const item_row = doc.items.find((item) => item.name === this.name); if (!item_row) return; @@ -104,12 +102,14 @@ erpnext.PointOfSale.ItemDetails = class { const no_serial_selected = !item_row.serial_no; const no_batch_selected = !item_row.batch_no; - if ((serialized && no_serial_selected) || (batched && no_batch_selected) || - (serialized && batched && (no_batch_selected || no_serial_selected))) { - + if ( + (serialized && no_serial_selected) || + (batched && no_batch_selected) || + (serialized && batched && (no_batch_selected || no_serial_selected)) + ) { frappe.show_alert({ message: __("Item is removed since no serial / batch no selected."), - indicator: 'orange' + indicator: "orange", }); frappe.utils.play_sound("cancel"); return this.events.remove_item_from_cart(); @@ -121,7 +121,10 @@ erpnext.PointOfSale.ItemDetails = class { function get_description_html() { if (description) { - description = description.indexOf('...') === -1 && description.length > 140 ? description.substr(0, 139) + '...' : description; + description = + description.indexOf("...") === -1 && description.length > 140 + ? description.substr(0, 139) + "..." + : description; return description; } return ``; @@ -141,11 +144,10 @@ erpnext.PointOfSale.ItemDetails = class { } else { this.$item_image.html(`
              ${frappe.get_abbr(item_name)}
              `); } - } handle_broken_image($img) { - const item_abbr = $($img).attr('alt'); + const item_abbr = $($img).attr("alt"); $($img).replaceWith(`
              ${item_abbr}
              `); } @@ -154,36 +156,36 @@ erpnext.PointOfSale.ItemDetails = class { this.$dicount_section.html( `
              ${format_currency(item.price_list_rate, this.currency)}
              ${item.discount_percentage}% off
              ` - ) + ); this.$item_price.html(format_currency(item.rate, this.currency)); } else { - this.$dicount_section.html(``) + this.$dicount_section.html(``); } } render_form(item) { const fields_to_display = this.get_form_fields(item); - this.$form_container.html(''); + this.$form_container.html(""); fields_to_display.forEach((fieldname, idx) => { this.$form_container.append( `
              ` - ) + ); - const field_meta = this.item_meta.fields.find(df => df.fieldname === fieldname); - fieldname === 'discount_percentage' ? (field_meta.label = __('Discount (%)')) : ''; + const field_meta = this.item_meta.fields.find((df) => df.fieldname === fieldname); + fieldname === "discount_percentage" ? (field_meta.label = __("Discount (%)")) : ""; const me = this; this[`${fieldname}_control`] = frappe.ui.form.make_control({ df: { ...field_meta, - onchange: function() { + onchange: function () { me.events.form_updated(me.current_item, fieldname, this.value); - } + }, }, parent: this.$form_container.find(`.${fieldname}-control`), render_input: true, - }) + }); this[`${fieldname}_control`].set_value(item[fieldname]); }); @@ -193,33 +195,40 @@ erpnext.PointOfSale.ItemDetails = class { } get_form_fields(item) { - const fields = ['qty', 'uom', 'rate', 'conversion_factor', 'discount_percentage', 'warehouse', 'actual_qty', 'price_list_rate']; - if (item.has_serial_no) fields.push('serial_no'); - if (item.has_batch_no) fields.push('batch_no'); + const fields = [ + "qty", + "uom", + "rate", + "conversion_factor", + "discount_percentage", + "warehouse", + "actual_qty", + "price_list_rate", + ]; + if (item.has_serial_no) fields.push("serial_no"); + if (item.has_batch_no) fields.push("batch_no"); return fields; } make_auto_serial_selection_btn(item) { if (item.has_serial_no) { if (!item.has_batch_no) { - this.$form_container.append( - `
              ` - ); + this.$form_container.append(`
              `); } - const label = __('Auto Fetch Serial Numbers'); + const label = __("Auto Fetch Serial Numbers"); this.$form_container.append( `
              ${label}
              ` ); - this.$form_container.find('.serial_no-control').find('textarea').css('height', '6rem'); + this.$form_container.find(".serial_no-control").find("textarea").css("height", "6rem"); } } bind_custom_control_change_event() { const me = this; if (this.rate_control) { - this.rate_control.df.onchange = function() { + this.rate_control.df.onchange = function () { if (this.value || flt(this.value) === 0) { - me.events.form_updated(me.current_item, 'rate', this.value).then(() => { + me.events.form_updated(me.current_item, "rate", this.value).then(() => { const item_row = frappe.get_doc(me.doctype, me.name); const doc = me.events.get_frm().doc; me.$item_price.html(format_currency(item_row.rate, doc.currency)); @@ -238,43 +247,48 @@ erpnext.PointOfSale.ItemDetails = class { if (this.warehouse_control) { this.warehouse_control.df.reqd = 1; - this.warehouse_control.df.onchange = function() { + this.warehouse_control.df.onchange = function () { if (this.value) { - me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => { + me.events.form_updated(me.current_item, "warehouse", this.value).then(() => { me.item_stock_map = me.events.get_item_stock_map(); const available_qty = me.item_stock_map[me.item_row.item_code][this.value][0]; - const is_stock_item = Boolean(me.item_stock_map[me.item_row.item_code][this.value][1]); + const is_stock_item = Boolean( + me.item_stock_map[me.item_row.item_code][this.value][1] + ); if (available_qty === undefined) { me.events.get_available_stock(me.item_row.item_code, this.value).then(() => { // item stock map is updated now reset warehouse me.warehouse_control.set_value(this.value); - }) + }); } else if (available_qty === 0 && is_stock_item) { - me.warehouse_control.set_value(''); + me.warehouse_control.set_value(""); const bold_item_code = me.item_row.item_code.bold(); const bold_warehouse = this.value.bold(); frappe.throw( - __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) + __("Item Code: {0} is not available under warehouse {1}.", [ + bold_item_code, + bold_warehouse, + ]) ); } me.actual_qty_control.set_value(available_qty); }); } - } + }; this.warehouse_control.df.get_query = () => { return { - filters: { company: this.events.get_frm().doc.company } - } + filters: { company: this.events.get_frm().doc.company }, + }; }; this.warehouse_control.refresh(); } if (this.serial_no_control) { this.serial_no_control.df.reqd = 1; - this.serial_no_control.df.onchange = async function() { - !me.current_item.batch_no && await me.auto_update_batch_no(); - me.events.form_updated(me.current_item, 'serial_no', this.value); - } + this.serial_no_control.df.onchange = async function () { + !me.current_item.batch_no && (await me.auto_update_batch_no()); + me.events.form_updated(me.current_item, "serial_no", this.value); + }; this.serial_no_control.refresh(); } @@ -282,25 +296,25 @@ erpnext.PointOfSale.ItemDetails = class { this.batch_no_control.df.reqd = 1; this.batch_no_control.df.get_query = () => { return { - query: 'erpnext.controllers.queries.get_batch_no', + query: "erpnext.controllers.queries.get_batch_no", filters: { item_code: me.item_row.item_code, warehouse: me.item_row.warehouse, - posting_date: me.events.get_frm().doc.posting_date - } - } + posting_date: me.events.get_frm().doc.posting_date, + }, + }; }; this.batch_no_control.refresh(); } if (this.uom_control) { - this.uom_control.df.onchange = function() { - me.events.form_updated(me.current_item, 'uom', this.value); + this.uom_control.df.onchange = function () { + me.events.form_updated(me.current_item, "uom", this.value); const item_row = frappe.get_doc(me.doctype, me.name); - me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value); + me.conversion_factor_control.df.read_only = item_row.stock_uom == this.value; me.conversion_factor_control.refresh(); - } + }; } frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { @@ -316,13 +330,16 @@ erpnext.PointOfSale.ItemDetails = class { async auto_update_batch_no() { if (this.serial_no_control && this.batch_no_control) { - const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s); + const selected_serial_nos = this.serial_no_control + .get_value() + .split(`\n`) + .filter((s) => s); if (!selected_serial_nos.length) return; // find batch nos of the selected serial no const serials_with_batch_no = await frappe.db.get_list("Serial No", { - filters: { 'name': ["in", selected_serial_nos]}, - fields: ["batch_no", "name"] + filters: { name: ["in", selected_serial_nos] }, + fields: ["batch_no", "name"], }); const batch_serial_map = serials_with_batch_no.reduce((acc, r) => { if (!acc[r.batch_no]) { @@ -335,10 +352,11 @@ erpnext.PointOfSale.ItemDetails = class { const batch_no = Object.keys(batch_serial_map)[0]; const batch_serial_nos = batch_serial_map[batch_no].join(`\n`); // eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch - const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length; + const serial_nos_belongs_to_other_batch = + selected_serial_nos.length !== batch_serial_map[batch_no].length; const current_batch_no = this.batch_no_control.get_value(); - current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no); + current_batch_no != batch_no && (await this.batch_no_control.set_value(batch_no)); if (serial_nos_belongs_to_other_batch) { this.serial_no_control.set_value(batch_serial_nos); @@ -354,13 +372,13 @@ erpnext.PointOfSale.ItemDetails = class { this.bind_auto_serial_fetch_event(); this.bind_fields_to_numpad_fields(); - this.$component.on('click', '.close-btn', () => { + this.$component.on("click", ".close-btn", () => { this.events.close_item_details(); }); } attach_shortcuts() { - this.wrapper.find('.close-btn').attr("title", "Esc"); + this.wrapper.find(".close-btn").attr("title", "Esc"); frappe.ui.keys.on("escape", () => { const item_details_visible = this.$component.is(":visible"); if (item_details_visible) { @@ -371,8 +389,8 @@ erpnext.PointOfSale.ItemDetails = class { bind_fields_to_numpad_fields() { const me = this; - this.$form_container.on('click', '.input-with-feedback', function() { - const fieldname = $(this).attr('data-fieldname'); + this.$form_container.on("click", ".input-with-feedback", function () { + const fieldname = $(this).attr("data-fieldname"); if (this.last_field_focused != fieldname) { me.events.item_field_focused(fieldname); this.last_field_focused = fieldname; @@ -381,8 +399,8 @@ erpnext.PointOfSale.ItemDetails = class { } bind_auto_serial_fetch_event() { - this.$form_container.on('click', '.auto-fetch-btn', () => { - this.batch_no_control && this.batch_no_control.set_value(''); + this.$form_container.on("click", ".auto-fetch-btn", () => { + this.batch_no_control && this.batch_no_control.set_value(""); let qty = this.qty_control.get_value(); let conversion_factor = this.conversion_factor_control.get_value(); let expiry_date = this.item_row.has_batch_no ? this.events.get_frm().doc.posting_date : ""; @@ -392,11 +410,11 @@ erpnext.PointOfSale.ItemDetails = class { args: { qty: qty * conversion_factor, item_code: this.current_item.item_code, - warehouse: this.warehouse_control.get_value() || '', - batch_nos: this.current_item.batch_no || '', + warehouse: this.warehouse_control.get_value() || "", + batch_nos: this.current_item.batch_no || "", posting_date: expiry_date, - for_doctype: 'POS Invoice' - } + for_doctype: "POS Invoice", + }, }); numbers.then((data) => { @@ -406,21 +424,22 @@ erpnext.PointOfSale.ItemDetails = class { const warehouse = this.warehouse_control.get_value().bold(); const item_code = this.current_item.item_code.bold(); frappe.msgprint( - __('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [item_code, warehouse]) + __( + "Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.", + [item_code, warehouse] + ) ); } else if (records_length < qty) { - frappe.msgprint( - __('Fetched only {0} available serial numbers.', [records_length]) - ); + frappe.msgprint(__("Fetched only {0} available serial numbers.", [records_length])); this.qty_control.set_value(records_length); } numbers = auto_fetched_serial_numbers.join(`\n`); this.serial_no_control.set_value(numbers); }); - }) + }); } toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); + show ? this.$component.css("display", "flex") : this.$component.css("display", "none"); } -} +}; diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index b5eb0489f9d..dba36bd2429 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -1,4 +1,4 @@ -import onScan from 'onscan.js'; +import onScan from "onscan.js"; erpnext.PointOfSale.ItemSelector = class { // eslint-disable-next-line no-unused-vars @@ -24,7 +24,7 @@ erpnext.PointOfSale.ItemSelector = class { this.wrapper.append( `
              -
              ${__('All Items')}
              +
              ${__("All Items")}
              @@ -32,13 +32,13 @@ erpnext.PointOfSale.ItemSelector = class {
              ` ); - this.$component = this.wrapper.find('.items-selector'); - this.$items_container = this.$component.find('.items-container'); + this.$component = this.wrapper.find(".items-selector"); + this.$items_container = this.$component.find(".items-container"); } async load_items_data() { if (!this.item_group) { - const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name"); + const res = await frappe.db.get_value("Item Group", { lft: 1, is_group: 1 }, "name"); this.parent_item_group = res.message.name; } if (!this.price_list) { @@ -46,12 +46,12 @@ erpnext.PointOfSale.ItemSelector = class { this.price_list = res.message.selling_price_list; } - this.get_items({}).then(({message}) => { + this.get_items({}).then(({ message }) => { this.render_item_list(message.items); }); } - get_items({start = 0, page_length = 40, search_term=''}) { + get_items({ start = 0, page_length = 40, search_term = "" }) { const doc = this.events.get_frm().doc; const price_list = (doc && doc.selling_price_list) || this.price_list; let { item_group, pos_profile } = this; @@ -65,11 +65,10 @@ erpnext.PointOfSale.ItemSelector = class { }); } - render_item_list(items) { - this.$items_container.html(''); + this.$items_container.html(""); - items.forEach(item => { + items.forEach((item) => { const item_html = this.get_item_html(item); this.$items_container.append(item_html); }); @@ -84,15 +83,15 @@ erpnext.PointOfSale.ItemSelector = class { let qty_to_display = actual_qty; if (item.is_stock_item) { - indicator_color = (actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"); + indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"; if (Math.round(qty_to_display) > 999) { - qty_to_display = Math.round(qty_to_display)/1000; - qty_to_display = qty_to_display.toFixed(1) + 'K'; + qty_to_display = Math.round(qty_to_display) / 1000; + qty_to_display = qty_to_display.toFixed(1) + "K"; } } else { - indicator_color = ''; - qty_to_display = ''; + indicator_color = ""; + qty_to_display = ""; } function get_item_image_html() { @@ -115,8 +114,7 @@ erpnext.PointOfSale.ItemSelector = class { } } - return ( - `
              ${format_currency(price_list_rate, item.currency, precision) || 0}
              -
              ` - ); +
              `; } handle_broken_image($img) { - const item_abbr = $($img).attr('alt'); + const item_abbr = $($img).attr("alt"); $($img).parent().replaceWith(`
              ${item_abbr}
              `); } make_search_bar() { const me = this; const doc = me.events.get_frm().doc; - this.$component.find('.search-field').html(''); - this.$component.find('.item-group-field').html(''); + this.$component.find(".search-field").html(""); + this.$component.find(".item-group-field").html(""); this.search_field = frappe.ui.form.make_control({ df: { - label: __('Search'), - fieldtype: 'Data', - placeholder: __('Search by item code, serial number or barcode') + label: __("Search"), + fieldtype: "Data", + placeholder: __("Search by item code, serial number or barcode"), }, - parent: this.$component.find('.search-field'), + parent: this.$component.find(".search-field"), render_input: true, }); this.item_group_field = frappe.ui.form.make_control({ df: { - label: __('Item Group'), - fieldtype: 'Link', - options: 'Item Group', - placeholder: __('Select item group'), - onchange: function() { + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", + placeholder: __("Select item group"), + onchange: function () { me.item_group = this.value; !me.item_group && (me.item_group = me.parent_item_group); me.filter_items(); }, get_query: function () { return { - query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query', + query: "erpnext.selling.page.point_of_sale.point_of_sale.item_group_query", filters: { - pos_profile: doc ? doc.pos_profile : '' - } + pos_profile: doc ? doc.pos_profile : "", + }, }; }, }, - parent: this.$component.find('.item-group-field'), + parent: this.$component.find(".item-group-field"), render_input: true, }); this.search_field.toggle_label(false); @@ -184,18 +181,18 @@ erpnext.PointOfSale.ItemSelector = class { } attach_clear_btn() { - this.search_field.$wrapper.find('.control-input').append( + this.search_field.$wrapper.find(".control-input").append( ` - ${frappe.utils.icon('close', 'sm')} + ${frappe.utils.icon("close", "sm")} ` ); - this.$clear_search_btn = this.search_field.$wrapper.find('.link-btn'); + this.$clear_search_btn = this.search_field.$wrapper.find(".link-btn"); - this.$clear_search_btn.on('click', 'a', () => { - this.set_search_value(''); + this.$clear_search_btn.on("click", "a", () => { + this.set_search_value(""); this.search_field.set_focus(); }); } @@ -217,39 +214,43 @@ erpnext.PointOfSale.ItemSelector = class { case iCode >= 186 && iCode <= 194: // (; = , - . / `) case iCode >= 219 && iCode <= 222: // ([ \ ] ') case iCode == 32: // spacebar - if (oEvent.key !== undefined && oEvent.key !== '') { + if (oEvent.key !== undefined && oEvent.key !== "") { return oEvent.key; } var sDecoded = String.fromCharCode(iCode); switch (oEvent.shiftKey) { - case false: sDecoded = sDecoded.toLowerCase(); break; - case true: sDecoded = sDecoded.toUpperCase(); break; + case false: + sDecoded = sDecoded.toLowerCase(); + break; + case true: + sDecoded = sDecoded.toUpperCase(); + break; } return sDecoded; case iCode >= 96 && iCode <= 105: // numbers on numeric keypad return 0 + (iCode - 96); } - return ''; + return ""; }; onScan.attachTo(document, { onScan: (sScancode) => { - if (this.search_field && this.$component.is(':visible')) { + if (this.search_field && this.$component.is(":visible")) { this.search_field.set_focus(); this.set_search_value(sScancode); this.barcode_scanned = true; } - } + }, }); - this.$component.on('click', '.item-wrapper', function() { + this.$component.on("click", ".item-wrapper", function () { const $item = $(this); - const item_code = unescape($item.attr('data-item-code')); - let batch_no = unescape($item.attr('data-batch-no')); - let serial_no = unescape($item.attr('data-serial-no')); - let uom = unescape($item.attr('data-uom')); - let rate = unescape($item.attr('data-rate')); + const item_code = unescape($item.attr("data-item-code")); + let batch_no = unescape($item.attr("data-batch-no")); + let serial_no = unescape($item.attr("data-serial-no")); + let uom = unescape($item.attr("data-uom")); + let rate = unescape($item.attr("data-rate")); // escape(undefined) returns "undefined" then unescape returns "undefined" batch_no = batch_no === "undefined" ? undefined : batch_no; @@ -258,76 +259,72 @@ erpnext.PointOfSale.ItemSelector = class { rate = rate === "undefined" ? undefined : rate; me.events.item_selected({ - field: 'qty', + field: "qty", value: "+1", - item: { item_code, batch_no, serial_no, uom, rate } + item: { item_code, batch_no, serial_no, uom, rate }, }); me.search_field.set_focus(); }); - this.search_field.$input.on('input', (e) => { + this.search_field.$input.on("input", (e) => { clearTimeout(this.last_search); this.last_search = setTimeout(() => { const search_term = e.target.value; this.filter_items({ search_term }); }, 300); - this.$clear_search_btn.toggle( - Boolean(this.search_field.$input.val()) - ); + this.$clear_search_btn.toggle(Boolean(this.search_field.$input.val())); }); - this.search_field.$input.on('focus', () => { - this.$clear_search_btn.toggle( - Boolean(this.search_field.$input.val()) - ); + this.search_field.$input.on("focus", () => { + this.$clear_search_btn.toggle(Boolean(this.search_field.$input.val())); }); } attach_shortcuts() { - const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; + const ctrl_label = frappe.utils.is_mac() ? "⌘" : "Ctrl"; this.search_field.parent.attr("title", `${ctrl_label}+I`); frappe.ui.keys.add_shortcut({ shortcut: "ctrl+i", action: () => this.search_field.set_focus(), - condition: () => this.$component.is(':visible'), + condition: () => this.$component.is(":visible"), description: __("Focus on search input"), ignore_inputs: true, - page: cur_page.page.page + page: cur_page.page.page, }); this.item_group_field.parent.attr("title", `${ctrl_label}+G`); frappe.ui.keys.add_shortcut({ shortcut: "ctrl+g", action: () => this.item_group_field.set_focus(), - condition: () => this.$component.is(':visible'), + condition: () => this.$component.is(":visible"), description: __("Focus on Item Group filter"), ignore_inputs: true, - page: cur_page.page.page + page: cur_page.page.page, }); // for selecting the last filtered item on search frappe.ui.keys.on("enter", () => { - const selector_is_visible = this.$component.is(':visible'); + const selector_is_visible = this.$component.is(":visible"); if (!selector_is_visible || this.search_field.get_value() === "") return; if (this.items.length == 1) { this.$items_container.find(".item-wrapper").click(); frappe.utils.play_sound("submit"); - this.set_search_value(''); + this.set_search_value(""); } else if (this.items.length == 0 && this.barcode_scanned) { // only show alert of barcode is scanned and enter is pressed frappe.show_alert({ message: __("No items found. Scan barcode again."), - indicator: 'orange' + indicator: "orange", }); frappe.utils.play_sound("error"); this.barcode_scanned = false; - this.set_search_value(''); + this.set_search_value(""); } }); } - filter_items({ search_term='' }={}) { + filter_items({ search_term = "" } = {}) { if (search_term) { search_term = search_term.toLowerCase(); @@ -342,44 +339,47 @@ erpnext.PointOfSale.ItemSelector = class { } } - this.get_items({ search_term }) - .then(({ message }) => { - // eslint-disable-next-line no-unused-vars - const { items, serial_no, batch_no, barcode } = message; - if (search_term && !barcode) { - this.search_index[search_term] = items; - } - this.items = items; - this.render_item_list(items); - this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); - }); + this.get_items({ search_term }).then(({ message }) => { + // eslint-disable-next-line no-unused-vars + const { items, serial_no, batch_no, barcode } = message; + if (search_term && !barcode) { + this.search_index[search_term] = items; + } + this.items = items; + this.render_item_list(items); + this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); + }); } add_filtered_item_to_cart() { this.$items_container.find(".item-wrapper").click(); - this.set_search_value(''); + this.set_search_value(""); } resize_selector(minimize) { - minimize ? - this.$component.find('.filter-section').css('grid-template-columns', 'repeat(1, minmax(0, 1fr))') : - this.$component.find('.filter-section').css('grid-template-columns', 'repeat(12, minmax(0, 1fr))'); + minimize + ? this.$component + .find(".filter-section") + .css("grid-template-columns", "repeat(1, minmax(0, 1fr))") + : this.$component + .find(".filter-section") + .css("grid-template-columns", "repeat(12, minmax(0, 1fr))"); - minimize ? - this.$component.find('.search-field').css('margin', 'var(--margin-sm) 0px') : - this.$component.find('.search-field').css('margin', '0px var(--margin-sm)'); + minimize + ? this.$component.find(".search-field").css("margin", "var(--margin-sm) 0px") + : this.$component.find(".search-field").css("margin", "0px var(--margin-sm)"); - minimize ? - this.$component.css('grid-column', 'span 2 / span 2') : - this.$component.css('grid-column', 'span 6 / span 6'); + minimize + ? this.$component.css("grid-column", "span 2 / span 2") + : this.$component.css("grid-column", "span 6 / span 6"); - minimize ? - this.$items_container.css('grid-template-columns', 'repeat(1, minmax(0, 1fr))') : - this.$items_container.css('grid-template-columns', 'repeat(4, minmax(0, 1fr))'); + minimize + ? this.$items_container.css("grid-template-columns", "repeat(1, minmax(0, 1fr))") + : this.$items_container.css("grid-template-columns", "repeat(4, minmax(0, 1fr))"); } toggle_component(show) { - this.set_search_value(''); - this.$component.css('display', show ? 'flex': 'none'); + this.set_search_value(""); + this.$component.css("display", show ? "flex" : "none"); } }; diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js index f27b0d55ef6..c77f206308d 100644 --- a/erpnext/selling/page/point_of_sale/pos_number_pad.js +++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js @@ -20,28 +20,40 @@ erpnext.PointOfSale.NumberPad = class { function get_keys() { return keys.reduce((a, row, i) => { - return a + row.reduce((a2, number, j) => { - const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : ''; - const fieldname = fieldnames && fieldnames[number] ? - fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number; + return ( + a + + row.reduce((a2, number, j) => { + const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : ""; + const fieldname = + fieldnames && fieldnames[number] + ? fieldnames[number] + : typeof number === "string" + ? frappe.scrub(number) + : number; - return a2 + `
              ${__(number)}
              `; - }, ''); - }, ''); + return ( + a2 + + `
              ${__( + number + )}
              ` + ); + }, "") + ); + }, ""); } this.wrapper.html( `
              ${get_keys()}
              ` - ) + ); } bind_events() { const me = this; - this.wrapper.on('click', '.numpad-btn', function() { + this.wrapper.on("click", ".numpad-btn", function () { const $btn = $(this); me.events.numpad_event($btn); }); } -} +}; diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js index a0475c70d0d..c450d8a109a 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js @@ -16,7 +16,7 @@ erpnext.PointOfSale.PastOrderList = class { this.wrapper.append( `
              -
              ${__('Recent Orders')}
              +
              ${__("Recent Orders")}
              @@ -24,12 +24,12 @@ erpnext.PointOfSale.PastOrderList = class {
              ` ); - this.$component = this.wrapper.find('.past-order-list'); - this.$invoices_container = this.$component.find('.invoices-container'); + this.$component = this.wrapper.find(".past-order-list"); + this.$invoices_container = this.$component.find(".invoices-container"); } bind_events() { - this.search_field.$input.on('input', (e) => { + this.search_field.$input.on("input", (e) => { clearTimeout(this.last_search); this.last_search = setTimeout(() => { const search_term = e.target.value; @@ -37,8 +37,8 @@ erpnext.PointOfSale.PastOrderList = class { }, 300); }); const me = this; - this.$invoices_container.on('click', '.invoice-wrapper', function() { - const invoice_name = unescape($(this).attr('data-invoice-name')); + this.$invoices_container.on("click", ".invoice-wrapper", function () { + const invoice_name = unescape($(this).attr("data-invoice-name")); me.events.open_invoice_data(invoice_name); }); @@ -48,29 +48,29 @@ erpnext.PointOfSale.PastOrderList = class { const me = this; this.search_field = frappe.ui.form.make_control({ df: { - label: __('Search'), - fieldtype: 'Data', - placeholder: __('Search by invoice id or customer name') + label: __("Search"), + fieldtype: "Data", + placeholder: __("Search by invoice id or customer name"), }, - parent: this.$component.find('.search-field'), + parent: this.$component.find(".search-field"), render_input: true, }); this.status_field = frappe.ui.form.make_control({ df: { - label: __('Invoice Status'), - fieldtype: 'Select', + label: __("Invoice Status"), + fieldtype: "Select", options: `Draft\nPaid\nConsolidated\nReturn`, - placeholder: __('Filter by invoice status'), - onchange: function() { - if (me.$component.is(':visible')) me.refresh_list(); - } + placeholder: __("Filter by invoice status"), + onchange: function () { + if (me.$component.is(":visible")) me.refresh_list(); + }, }, - parent: this.$component.find('.status-field'), + parent: this.$component.find(".status-field"), render_input: true, }); this.search_field.toggle_label(false); this.status_field.toggle_label(false); - this.status_field.set_value('Draft'); + this.status_field.set_value("Draft"); } refresh_list() { @@ -79,7 +79,7 @@ erpnext.PointOfSale.PastOrderList = class { const search_term = this.search_field.get_value(); const status = this.status_field.get_value(); - this.$invoices_container.html(''); + this.$invoices_container.html(""); return frappe.call({ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_past_order_list", @@ -87,18 +87,19 @@ erpnext.PointOfSale.PastOrderList = class { args: { search_term, status }, callback: (response) => { frappe.dom.unfreeze(); - response.message.forEach(invoice => { + response.message.forEach((invoice) => { const invoice_html = this.get_invoice_html(invoice); this.$invoices_container.append(invoice_html); }); - } + }, }); } get_invoice_html(invoice) { - const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma"); - return ( - `
              + const posting_datetime = moment(invoice.posting_date + " " + invoice.posting_time).format( + "Do MMMM, h:mma" + ); + return `
              ${invoice.name}
              @@ -113,11 +114,12 @@ erpnext.PointOfSale.PastOrderList = class {
              ${posting_datetime}
              -
              ` - ); +
              `; } toggle_component(show) { - show ? this.$component.css('display', 'flex') && this.refresh_list() : this.$component.css('display', 'none'); + show + ? this.$component.css("display", "flex") && this.refresh_list() + : this.$component.css("display", "none"); } }; diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index eeb8523f19c..96b2c051e72 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -17,16 +17,16 @@ erpnext.PointOfSale.PastOrderSummary = class { this.wrapper.append( `
              - ${__('Select an invoice to load summary data')} + ${__("Select an invoice to load summary data")}
              -
              ${__('Items')}
              +
              ${__("Items")}
              -
              ${__('Totals')}
              +
              ${__("Totals")}
              -
              ${__('Payments')}
              +
              ${__("Payments")}
              @@ -34,55 +34,53 @@ erpnext.PointOfSale.PastOrderSummary = class {
              ` ); - this.$component = this.wrapper.find('.past-order-summary'); - this.$summary_wrapper = this.$component.find('.invoice-summary-wrapper'); - this.$summary_container = this.$component.find('.abs-container'); - this.$upper_section = this.$summary_container.find('.upper-section'); - this.$items_container = this.$summary_container.find('.items-container'); - this.$totals_container = this.$summary_container.find('.totals-container'); - this.$payment_container = this.$summary_container.find('.payments-container'); - this.$summary_btns = this.$summary_container.find('.summary-btns'); + this.$component = this.wrapper.find(".past-order-summary"); + this.$summary_wrapper = this.$component.find(".invoice-summary-wrapper"); + this.$summary_container = this.$component.find(".abs-container"); + this.$upper_section = this.$summary_container.find(".upper-section"); + this.$items_container = this.$summary_container.find(".items-container"); + this.$totals_container = this.$summary_container.find(".totals-container"); + this.$payment_container = this.$summary_container.find(".payments-container"); + this.$summary_btns = this.$summary_container.find(".summary-btns"); } init_email_print_dialog() { const email_dialog = new frappe.ui.Dialog({ - title: 'Email Receipt', + title: "Email Receipt", fields: [ - {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID'}, + { fieldname: "email_id", fieldtype: "Data", options: "Email", label: "Email ID" }, // {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'} ], primary_action: () => { this.send_email(); }, - primary_action_label: __('Send'), + primary_action_label: __("Send"), }); this.email_dialog = email_dialog; const print_dialog = new frappe.ui.Dialog({ - title: 'Print Receipt', - fields: [ - {fieldname: 'print', fieldtype: 'Data', label: 'Print Preview'} - ], + title: "Print Receipt", + fields: [{ fieldname: "print", fieldtype: "Data", label: "Print Preview" }], primary_action: () => { this.print_receipt(); }, - primary_action_label: __('Print'), + primary_action_label: __("Print"), }); this.print_dialog = print_dialog; } get_upper_section_html(doc) { const { status } = doc; - let indicator_color = ''; + let indicator_color = ""; - in_list(['Paid', 'Consolidated'], status) && (indicator_color = 'green'); - status === 'Draft' && (indicator_color = 'red'); - status === 'Return' && (indicator_color = 'grey'); + in_list(["Paid", "Consolidated"], status) && (indicator_color = "green"); + status === "Draft" && (indicator_color = "red"); + status === "Return" && (indicator_color = "grey"); return `
              ${doc.customer}
              ${this.customer_email}
              -
              ${__('Sold by')}: ${doc.owner}
              +
              ${__("Sold by")}: ${doc.owner}
              @@ -103,7 +101,10 @@ erpnext.PointOfSale.PastOrderSummary = class { return `(${item_data.discount_percentage}% off)
              ${format_currency(item_data.rate, doc.currency)}
              `; } else { - return `
              ${format_currency(item_data.price_list_rate || item_data.rate, doc.currency)}
              `; + return `
              ${format_currency( + item_data.price_list_rate || item_data.rate, + doc.currency + )}
              `; } } } @@ -121,30 +122,34 @@ erpnext.PointOfSale.PastOrderSummary = class { get_net_total_html(doc) { return `
              -
              ${__('Net Total')}
              +
              ${__("Net Total")}
              ${format_currency(doc.net_total, doc.currency)}
              `; } get_taxes_html(doc) { - if (!doc.taxes.length) return ''; + if (!doc.taxes.length) return ""; - let taxes_html = doc.taxes.map(t => { - const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; - return ` + let taxes_html = doc.taxes + .map((t) => { + const description = /[0-9]+/.test(t.description) + ? t.description + : `${t.description} @ ${t.rate}%`; + return `
              ${description}
              ${format_currency(t.tax_amount_after_discount_amount, doc.currency)}
              `; - }).join(''); + }) + .join(""); return `
              ${taxes_html}
              `; } get_grand_total_html(doc) { return `
              -
              ${__('Grand Total')}
              +
              ${__("Grand Total")}
              ${format_currency(doc.grand_total, doc.currency)}
              `; } @@ -157,26 +162,26 @@ erpnext.PointOfSale.PastOrderSummary = class { } bind_events() { - this.$summary_container.on('click', '.return-btn', () => { + this.$summary_container.on("click", ".return-btn", () => { this.events.process_return(this.doc.name); this.toggle_component(false); - this.$component.find('.no-summary-placeholder').css('display', 'flex'); - this.$summary_wrapper.css('display', 'none'); + this.$component.find(".no-summary-placeholder").css("display", "flex"); + this.$summary_wrapper.css("display", "none"); }); - this.$summary_container.on('click', '.edit-btn', () => { + this.$summary_container.on("click", ".edit-btn", () => { this.events.edit_order(this.doc.name); this.toggle_component(false); - this.$component.find('.no-summary-placeholder').css('display', 'flex'); - this.$summary_wrapper.css('display', 'none'); + this.$component.find(".no-summary-placeholder").css("display", "flex"); + this.$summary_wrapper.css("display", "none"); }); - this.$summary_container.on('click', '.delete-btn', () => { + this.$summary_container.on("click", ".delete-btn", () => { this.events.delete_order(this.doc.name); this.show_summary_placeholder(); }); - this.$summary_container.on('click', '.delete-btn', () => { + this.$summary_container.on("click", ".delete-btn", () => { this.events.delete_order(this.doc.name); this.show_summary_placeholder(); // this.toggle_component(false); @@ -184,19 +189,19 @@ erpnext.PointOfSale.PastOrderSummary = class { // this.$summary_wrapper.addClass('d-none'); }); - this.$summary_container.on('click', '.new-btn', () => { + this.$summary_container.on("click", ".new-btn", () => { this.events.new_order(); this.toggle_component(false); - this.$component.find('.no-summary-placeholder').css('display', 'flex'); - this.$summary_wrapper.css('display', 'none'); + this.$component.find(".no-summary-placeholder").css("display", "flex"); + this.$summary_wrapper.css("display", "none"); }); - this.$summary_container.on('click', '.email-btn', () => { + this.$summary_container.on("click", ".email-btn", () => { this.email_dialog.fields_dict.email_id.set_value(this.customer_email); this.email_dialog.show(); }); - this.$summary_container.on('click', '.print-btn', () => { + this.$summary_container.on("click", ".print-btn", () => { this.print_receipt(); }); } @@ -213,29 +218,31 @@ erpnext.PointOfSale.PastOrderSummary = class { } attach_shortcuts() { - const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; - this.$summary_container.find('.print-btn').attr("title", `${ctrl_label}+P`); + const ctrl_label = frappe.utils.is_mac() ? "⌘" : "Ctrl"; + this.$summary_container.find(".print-btn").attr("title", `${ctrl_label}+P`); frappe.ui.keys.add_shortcut({ shortcut: "ctrl+p", - action: () => this.$summary_container.find('.print-btn').click(), - condition: () => this.$component.is(':visible') && this.$summary_container.find('.print-btn').is(":visible"), + action: () => this.$summary_container.find(".print-btn").click(), + condition: () => + this.$component.is(":visible") && this.$summary_container.find(".print-btn").is(":visible"), description: __("Print Receipt"), - page: cur_page.page.page + page: cur_page.page.page, }); - this.$summary_container.find('.new-btn').attr("title", `${ctrl_label}+Enter`); + this.$summary_container.find(".new-btn").attr("title", `${ctrl_label}+Enter`); frappe.ui.keys.on("ctrl+enter", () => { const summary_is_visible = this.$component.is(":visible"); - if (summary_is_visible && this.$summary_container.find('.new-btn').is(":visible")) { - this.$summary_container.find('.new-btn').click(); + if (summary_is_visible && this.$summary_container.find(".new-btn").is(":visible")) { + this.$summary_container.find(".new-btn").click(); } }); - this.$summary_container.find('.edit-btn').attr("title", `${ctrl_label}+E`); + this.$summary_container.find(".edit-btn").attr("title", `${ctrl_label}+E`); frappe.ui.keys.add_shortcut({ shortcut: "ctrl+e", - action: () => this.$summary_container.find('.edit-btn').click(), - condition: () => this.$component.is(':visible') && this.$summary_container.find('.edit-btn').is(":visible"), + action: () => this.$summary_container.find(".edit-btn").click(), + condition: () => + this.$component.is(":visible") && this.$summary_container.find(".edit-btn").is(":visible"), description: __("Edit Receipt"), - page: cur_page.page.page + page: cur_page.page.page, }); } @@ -249,42 +256,44 @@ erpnext.PointOfSale.PastOrderSummary = class { method: "frappe.core.doctype.communication.email.make", args: { recipients: recipients, - subject: __(frm.meta.name) + ': ' + doc.name, + subject: __(frm.meta.name) + ": " + doc.name, doctype: doc.doctype, name: doc.name, + content: "", send_email: 1, print_format, sender_full_name: frappe.user.full_name(), - _lang: doc.language + _lang: doc.language, }, - callback: r => { + callback: (r) => { if (!r.exc) { frappe.utils.play_sound("email"); if (r.message["emails_not_sent_to"]) { - frappe.msgprint(__( - "Email not sent to {0} (unsubscribed / disabled)", - [ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ] - )); + frappe.msgprint( + __("Email not sent to {0} (unsubscribed / disabled)", [ + frappe.utils.escape_html(r.message["emails_not_sent_to"]), + ]) + ); } else { frappe.show_alert({ - message: __('Email sent successfully.'), - indicator: 'green' + message: __("Email sent successfully."), + indicator: "green", }); } this.email_dialog.hide(); } else { frappe.msgprint(__("There were errors while sending email. Please try again.")); } - } + }, }); } add_summary_btns(map) { - this.$summary_btns.html(''); - map.forEach(m => { + this.$summary_btns.html(""); + map.forEach((m) => { if (m.condition) { - m.visible_btns.forEach(b => { - const class_name = b.split(' ')[0].toLowerCase(); + m.visible_btns.forEach((b) => { + const class_name = b.split(" ")[0].toLowerCase(); const btn = __(b); this.$summary_btns.append( `
              ${btn}
              ` @@ -292,34 +301,40 @@ erpnext.PointOfSale.PastOrderSummary = class { }); } }); - this.$summary_btns.children().last().removeClass('mr-4'); + this.$summary_btns.children().last().removeClass("mr-4"); } toggle_summary_placeholder(show) { if (show) { - this.$summary_wrapper.css('display', 'none'); - this.$component.find('.no-summary-placeholder').css('display', 'flex'); + this.$summary_wrapper.css("display", "none"); + this.$component.find(".no-summary-placeholder").css("display", "flex"); } else { - this.$summary_wrapper.css('display', 'flex'); - this.$component.find('.no-summary-placeholder').css('display', 'none'); + this.$summary_wrapper.css("display", "flex"); + this.$component.find(".no-summary-placeholder").css("display", "none"); } } get_condition_btn_map(after_submission) { if (after_submission) - return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }]; + return [{ condition: true, visible_btns: ["Print Receipt", "Email Receipt", "New Order"] }]; return [ - { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order', 'Delete Order'] }, - { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']}, - { condition: this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt']} + { condition: this.doc.docstatus === 0, visible_btns: ["Edit Order", "Delete Order"] }, + { + condition: !this.doc.is_return && this.doc.docstatus === 1, + visible_btns: ["Print Receipt", "Email Receipt", "Return"], + }, + { + condition: this.doc.is_return && this.doc.docstatus === 1, + visible_btns: ["Print Receipt", "Email Receipt"], + }, ]; } - load_summary_of(doc, after_submission=false) { - after_submission ? - this.$component.css('grid-column', 'span 10 / span 10') : - this.$component.css('grid-column', 'span 6 / span 6'); + load_summary_of(doc, after_submission = false) { + after_submission + ? this.$component.css("grid-column", "span 10 / span 10") + : this.$component.css("grid-column", "span 6 / span 6"); this.toggle_summary_placeholder(false); @@ -339,16 +354,16 @@ erpnext.PointOfSale.PastOrderSummary = class { } attach_document_info(doc) { - frappe.db.get_value('Customer', this.doc.customer, 'email_id').then(({ message }) => { - this.customer_email = message.email_id || ''; + frappe.db.get_value("Customer", this.doc.customer, "email_id").then(({ message }) => { + this.customer_email = message.email_id || ""; const upper_section_dom = this.get_upper_section_html(doc); this.$upper_section.html(upper_section_dom); }); } attach_items_info(doc) { - this.$items_container.html(''); - doc.items.forEach(item => { + this.$items_container.html(""); + doc.items.forEach((item) => { const item_dom = this.get_item_html(doc, item); this.$items_container.append(item_dom); this.set_dynamic_rate_header_width(); @@ -359,8 +374,7 @@ erpnext.PointOfSale.PastOrderSummary = class { const rate_cols = Array.from(this.$items_container.find(".item-rate-disc")); this.$items_container.find(".item-rate-disc").css("width", ""); let max_width = rate_cols.reduce((max_width, elm) => { - if ($(elm).width() > max_width) - max_width = $(elm).width(); + if ($(elm).width() > max_width) max_width = $(elm).width(); return max_width; }, 0); @@ -371,8 +385,8 @@ erpnext.PointOfSale.PastOrderSummary = class { } attach_payments_info(doc) { - this.$payment_container.html(''); - doc.payments.forEach(p => { + this.$payment_container.html(""); + doc.payments.forEach((p) => { if (p.amount) { const payment_dom = this.get_payment_html(doc, p); this.$payment_container.append(payment_dom); @@ -380,7 +394,7 @@ erpnext.PointOfSale.PastOrderSummary = class { }); if (doc.redeem_loyalty_points && doc.loyalty_amount) { const payment_dom = this.get_payment_html(doc, { - mode_of_payment: 'Loyalty Points', + mode_of_payment: "Loyalty Points", amount: doc.loyalty_amount, }); this.$payment_container.append(payment_dom); @@ -388,7 +402,7 @@ erpnext.PointOfSale.PastOrderSummary = class { } attach_totals_info(doc) { - this.$totals_container.html(''); + this.$totals_container.html(""); const net_total_dom = this.get_net_total_html(doc); const taxes_dom = this.get_taxes_html(doc); @@ -401,6 +415,6 @@ erpnext.PointOfSale.PastOrderSummary = class { } toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); + show ? this.$component.css("display", "flex") : this.$component.css("display", "none"); } }; diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 89ce61ab168..232b6a02123 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -12,17 +12,16 @@ erpnext.PointOfSale.Payment = class { this.initialize_numpad(); this.bind_events(); this.attach_shortcuts(); - } prepare_dom() { this.wrapper.append( `
              - +
              - +
              @@ -33,12 +32,12 @@ erpnext.PointOfSale.Payment = class {
              ${__("Complete Order")}
              ` ); - this.$component = this.wrapper.find('.payment-container'); - this.$payment_modes = this.$component.find('.payment-modes'); - this.$totals_section = this.$component.find('.totals-section'); - this.$totals = this.$component.find('.totals'); - this.$numpad = this.$component.find('.number-pad'); - this.$invoice_fields_section = this.$component.find('.fields-section'); + this.$component = this.wrapper.find(".payment-container"); + this.$payment_modes = this.$component.find(".payment-modes"); + this.$totals_section = this.$component.find(".totals-section"); + this.$totals = this.$component.find(".totals"); + this.$numpad = this.$component.find(".number-pad"); + this.$invoice_fields_section = this.$component.find(".fields-section"); } make_invoice_fields_control() { @@ -46,33 +45,33 @@ erpnext.PointOfSale.Payment = class { const fields = doc.invoice_fields; if (!fields.length) return; - this.$invoice_fields = this.$invoice_fields_section.find('.invoice-fields'); - this.$invoice_fields.html(''); + this.$invoice_fields = this.$invoice_fields_section.find(".invoice-fields"); + this.$invoice_fields.html(""); const frm = this.events.get_frm(); - fields.forEach(df => { + fields.forEach((df) => { this.$invoice_fields.append( `
              ` ); let df_events = { - onchange: function() { + onchange: function () { frm.set_value(this.df.fieldname, this.get_value()); - } + }, }; if (df.fieldtype == "Button") { df_events = { - click: function() { + click: function () { if (frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype)) { frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname); } - } + }, }; } this[`${df.fieldname}_field`] = frappe.ui.form.make_control({ df: { ...df, - ...df_events + ...df_events, }, parent: this.$invoice_fields.find(`.${df.fieldname}-field`), render_input: true, @@ -87,34 +86,35 @@ erpnext.PointOfSale.Payment = class { this.number_pad = new erpnext.PointOfSale.NumberPad({ wrapper: this.$numpad, events: { - numpad_event: function($btn) { + numpad_event: function ($btn) { me.on_numpad_clicked($btn); - } + }, }, cols: 3, keys: [ - [ 1, 2, 3 ], - [ 4, 5, 6 ], - [ 7, 8, 9 ], - [ '.', 0, 'Delete' ] + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [".", 0, "Delete"], ], }); - this.numpad_value = ''; + this.numpad_value = ""; } on_numpad_clicked($btn) { - const button_value = $btn.attr('data-button-value'); + const button_value = $btn.attr("data-button-value"); highlight_numpad_btn($btn); - this.numpad_value = button_value === 'delete' ? this.numpad_value.slice(0, -1) : this.numpad_value + button_value; + this.numpad_value = + button_value === "delete" ? this.numpad_value.slice(0, -1) : this.numpad_value + button_value; this.selected_mode.$input.get(0).focus(); this.selected_mode.set_value(this.numpad_value); function highlight_numpad_btn($btn) { - $btn.addClass('shadow-base-inner bg-selected'); + $btn.addClass("shadow-base-inner bg-selected"); setTimeout(() => { - $btn.removeClass('shadow-base-inner bg-selected'); + $btn.removeClass("shadow-base-inner bg-selected"); }, 100); } } @@ -122,36 +122,37 @@ erpnext.PointOfSale.Payment = class { bind_events() { const me = this; - this.$payment_modes.on('click', '.mode-of-payment', function(e) { + this.$payment_modes.on("click", ".mode-of-payment", function (e) { const mode_clicked = $(this); // if clicked element doesn't have .mode-of-payment class then return if (!$(e.target).is(mode_clicked)) return; - const scrollLeft = mode_clicked.offset().left - me.$payment_modes.offset().left + me.$payment_modes.scrollLeft(); + const scrollLeft = + mode_clicked.offset().left - me.$payment_modes.offset().left + me.$payment_modes.scrollLeft(); me.$payment_modes.animate({ scrollLeft }); - const mode = mode_clicked.attr('data-mode'); + const mode = mode_clicked.attr("data-mode"); // hide all control fields and shortcuts - $(`.mode-of-payment-control`).css('display', 'none'); - $(`.cash-shortcuts`).css('display', 'none'); - me.$payment_modes.find(`.pay-amount`).css('display', 'inline'); - me.$payment_modes.find(`.loyalty-amount-name`).css('display', 'none'); + $(`.mode-of-payment-control`).css("display", "none"); + $(`.cash-shortcuts`).css("display", "none"); + me.$payment_modes.find(`.pay-amount`).css("display", "inline"); + me.$payment_modes.find(`.loyalty-amount-name`).css("display", "none"); // remove highlight from all mode-of-payments - $('.mode-of-payment').removeClass('border-primary'); + $(".mode-of-payment").removeClass("border-primary"); - if (mode_clicked.hasClass('border-primary')) { + if (mode_clicked.hasClass("border-primary")) { // clicked one is selected then unselect it - mode_clicked.removeClass('border-primary'); - me.selected_mode = ''; + mode_clicked.removeClass("border-primary"); + me.selected_mode = ""; } else { // clicked one is not selected then select it - mode_clicked.addClass('border-primary'); - mode_clicked.find('.mode-of-payment-control').css('display', 'flex'); - mode_clicked.find('.cash-shortcuts').css('display', 'grid'); - me.$payment_modes.find(`.${mode}-amount`).css('display', 'none'); - me.$payment_modes.find(`.${mode}-name`).css('display', 'inline'); + mode_clicked.addClass("border-primary"); + mode_clicked.find(".mode-of-payment-control").css("display", "flex"); + mode_clicked.find(".cash-shortcuts").css("display", "grid"); + me.$payment_modes.find(`.${mode}-amount`).css("display", "none"); + me.$payment_modes.find(`.${mode}-name`).css("display", "inline"); me.selected_mode = me[`${mode}_control`]; me.selected_mode && me.selected_mode.$input.get(0).focus(); @@ -159,33 +160,33 @@ erpnext.PointOfSale.Payment = class { } }); - frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => { + frappe.ui.form.on("POS Invoice", "contact_mobile", (frm) => { const contact = frm.doc.contact_mobile; const request_button = $(this.request_for_payment_field?.$input[0]); if (contact) { - request_button.removeClass('btn-default').addClass('btn-primary'); + request_button.removeClass("btn-default").addClass("btn-primary"); } else { - request_button.removeClass('btn-primary').addClass('btn-default'); + request_button.removeClass("btn-primary").addClass("btn-default"); } }); - frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { + frappe.ui.form.on("POS Invoice", "coupon_code", (frm) => { if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { if (!frm.doc.ignore_pricing_rule) { frm.applying_pos_coupon_code = true; frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.trigger('apply_pricing_rule'), + () => (frm.doc.ignore_pricing_rule = 1), + () => frm.trigger("ignore_pricing_rule"), + () => (frm.doc.ignore_pricing_rule = 0), + () => frm.trigger("apply_pricing_rule"), () => frm.save(), () => this.update_totals_section(frm.doc), - () => (frm.applying_pos_coupon_code = false) + () => (frm.applying_pos_coupon_code = false), ]); } else if (frm.doc.ignore_pricing_rule) { frappe.show_alert({ message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), - indicator: "orange" + indicator: "orange", }); } } @@ -193,18 +194,20 @@ erpnext.PointOfSale.Payment = class { this.setup_listener_for_payments(); - this.$payment_modes.on('click', '.shortcut', function() { - const value = $(this).attr('data-value'); + this.$payment_modes.on("click", ".shortcut", function () { + const value = $(this).attr("data-value"); me.selected_mode.set_value(value); }); - this.$component.on('click', '.submit-order-btn', () => { + this.$component.on("click", ".submit-order-btn", () => { const doc = this.events.get_frm().doc; const paid_amount = doc.paid_amount; const items = doc.items; if (paid_amount == 0 || !items.length) { - const message = items.length ? __("You cannot submit the order without payment.") : __("You cannot submit empty order."); + const message = items.length + ? __("You cannot submit the order without payment.") + : __("You cannot submit empty order."); frappe.show_alert({ message, indicator: "orange" }); frappe.utils.play_sound("error"); return; @@ -213,17 +216,18 @@ erpnext.PointOfSale.Payment = class { this.events.submit_invoice(); }); - frappe.ui.form.on('POS Invoice', 'paid_amount', (frm) => { + frappe.ui.form.on("POS Invoice", "paid_amount", (frm) => { this.update_totals_section(frm.doc); // need to re calculate cash shortcuts after discount is applied - const is_cash_shortcuts_invisible = !this.$payment_modes.find('.cash-shortcuts').is(':visible'); + const is_cash_shortcuts_invisible = !this.$payment_modes.find(".cash-shortcuts").is(":visible"); this.attach_cash_shortcuts(frm.doc); - !is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').css('display', 'grid'); + !is_cash_shortcuts_invisible && + this.$payment_modes.find(".cash-shortcuts").css("display", "grid"); this.render_payment_mode_dom(); }); - frappe.ui.form.on('POS Invoice', 'loyalty_amount', (frm) => { + frappe.ui.form.on("POS Invoice", "loyalty_amount", (frm) => { const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency); this.$payment_modes.find(`.loyalty-amount-amount`).html(formatted_currency); }); @@ -246,28 +250,36 @@ erpnext.PointOfSale.Payment = class { if (success) { title = __("Payment Received"); - const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) + ? doc.grand_total + : doc.rounded_total; if (amount >= grand_total) { frappe.dom.unfreeze(); - message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]); + message = __("Payment of {0} received successfully.", [ + format_currency(amount, doc.currency, 0), + ]); this.events.submit_invoice(); cur_frm.reload_doc(); - } else { - message = __("Payment of {0} received successfully. Waiting for other requests to complete...", [format_currency(amount, doc.currency, 0)]); + message = __( + "Payment of {0} received successfully. Waiting for other requests to complete...", + [format_currency(amount, doc.currency, 0)] + ); } } else if (failure_message) { message = failure_message; title = __("Payment Failed"); } - frappe.msgprint({ "message": message, "title": title }); + frappe.msgprint({ message: message, title: title }); }); } auto_set_remaining_amount() { const doc = this.events.get_frm().doc; - const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) + ? doc.grand_total + : doc.rounded_total; const remaining_amount = grand_total - doc.paid_amount; const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined; if (!current_value && remaining_amount > 0 && this.selected_mode) { @@ -276,13 +288,13 @@ erpnext.PointOfSale.Payment = class { } attach_shortcuts() { - const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; - this.$component.find('.submit-order-btn').attr("title", `${ctrl_label}+Enter`); + const ctrl_label = frappe.utils.is_mac() ? "⌘" : "Ctrl"; + this.$component.find(".submit-order-btn").attr("title", `${ctrl_label}+Enter`); frappe.ui.keys.on("ctrl+enter", () => { const payment_is_visible = this.$component.is(":visible"); const active_mode = this.$payment_modes.find(".border-primary"); if (payment_is_visible && active_mode.length) { - this.$component.find('.submit-order-btn').click(); + this.$component.find(".submit-order-btn").click(); } }); @@ -295,19 +307,24 @@ erpnext.PointOfSale.Payment = class { if (!active_mode) return; - const mode_of_payments = Array.from(this.$payment_modes.find(".mode-of-payment")).map(m => $(m).attr("data-mode")); + const mode_of_payments = Array.from(this.$payment_modes.find(".mode-of-payment")).map((m) => + $(m).attr("data-mode") + ); const mode_index = mode_of_payments.indexOf(active_mode); const next_mode_index = (mode_index + 1) % mode_of_payments.length; - const next_mode_to_be_clicked = this.$payment_modes.find(`.mode-of-payment[data-mode="${mode_of_payments[next_mode_index]}"]`); + const next_mode_to_be_clicked = this.$payment_modes.find( + `.mode-of-payment[data-mode="${mode_of_payments[next_mode_index]}"]` + ); if (payment_is_visible && mode_index != next_mode_index) { next_mode_to_be_clicked.click(); } }, - condition: () => this.$component.is(':visible') && this.$payment_modes.find(".border-primary").length, + condition: () => + this.$component.is(":visible") && this.$payment_modes.find(".border-primary").length, description: __("Switch Between Payment Modes"), ignore_inputs: true, - page: cur_page.page.page + page: cur_page.page.page, }); } @@ -341,20 +358,20 @@ erpnext.PointOfSale.Payment = class { } toggle_remarks_control() { - if (this.$remarks.find('.frappe-control').length) { - this.$remarks.html('+ Add Remark'); + if (this.$remarks.find(".frappe-control").length) { + this.$remarks.html("+ Add Remark"); } else { - this.$remarks.html(''); + this.$remarks.html(""); this[`remark_control`] = frappe.ui.form.make_control({ df: { - label: __('Remark'), - fieldtype: 'Data', - onchange: function() {} + label: __("Remark"), + fieldtype: "Data", + onchange: function () {}, }, parent: this.$totals_section.find(`.remarks`), render_input: true, }); - this[`remark_control`].set_value(''); + this[`remark_control`].set_value(""); } } @@ -363,14 +380,15 @@ erpnext.PointOfSale.Payment = class { const payments = doc.payments; const currency = doc.currency; - this.$payment_modes.html(`${ - payments.map((p, i) => { - const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); - const payment_type = p.type; - const margin = i % 2 === 0 ? 'pr-2' : 'pl-2'; - const amount = p.amount > 0 ? format_currency(p.amount, currency) : ''; + this.$payment_modes.html( + `${payments + .map((p, i) => { + const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); + const payment_type = p.type; + const margin = i % 2 === 0 ? "pr-2" : "pl-2"; + const amount = p.amount > 0 ? format_currency(p.amount, currency) : ""; - return (` + return `
              ${p.mode_of_payment} @@ -378,29 +396,30 @@ erpnext.PointOfSale.Payment = class {
              - `); - }).join('') - }`); + `; + }) + .join("")}` + ); - payments.forEach(p => { + payments.forEach((p) => { const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); const me = this; this[`${mode}_control`] = frappe.ui.form.make_control({ df: { label: p.mode_of_payment, - fieldtype: 'Currency', - placeholder: __('Enter {0} amount.', [p.mode_of_payment]), - onchange: function() { - const current_value = frappe.model.get_value(p.doctype, p.name, 'amount'); + fieldtype: "Currency", + placeholder: __("Enter {0} amount.", [p.mode_of_payment]), + onchange: function () { + const current_value = frappe.model.get_value(p.doctype, p.name, "amount"); if (current_value != this.value) { frappe.model - .set_value(p.doctype, p.name, 'amount', flt(this.value)) - .then(() => me.update_totals_section()) + .set_value(p.doctype, p.name, "amount", flt(this.value)) + .then(() => me.update_totals_section()); const formatted_currency = format_currency(this.value, currency); me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency); } - } + }, }, parent: this.$payment_modes.find(`.${mode}.mode-of-payment-control`), render_input: true, @@ -417,7 +436,7 @@ erpnext.PointOfSale.Payment = class { focus_on_default_mop() { const doc = this.events.get_frm().doc; const payments = doc.payments; - payments.forEach(p => { + payments.forEach((p) => { const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); if (p.default) { setTimeout(() => { @@ -428,17 +447,23 @@ erpnext.PointOfSale.Payment = class { } attach_cash_shortcuts(doc) { - const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) + ? doc.grand_total + : doc.rounded_total; const currency = doc.currency; const shortcuts = this.get_cash_shortcuts(flt(grand_total)); - this.$payment_modes.find('.cash-shortcuts').remove(); - let shortcuts_html = shortcuts.map(s => { - return `
              ${format_currency(s, currency, 0)}
              `; - }).join(''); + this.$payment_modes.find(".cash-shortcuts").remove(); + let shortcuts_html = shortcuts + .map((s) => { + return `
              ${format_currency(s, currency, 0)}
              `; + }) + .join(""); - this.$payment_modes.find('[data-payment-type="Cash"]').find('.mode-of-payment-control') + this.$payment_modes + .find('[data-payment-type="Cash"]') + .find(".mode-of-payment-control") .after(`
              ${shortcuts_html}
              `); } @@ -446,10 +471,10 @@ erpnext.PointOfSale.Payment = class { let steps = [1, 5, 10]; const digits = String(Math.round(grand_total)).length; - steps = steps.map(x => x * (10 ** (digits - 2))); + steps = steps.map((x) => x * 10 ** (digits - 2)); const get_nearest = (amount, x) => { - let nearest_x = Math.ceil((amount / x)) * x; + let nearest_x = Math.ceil(amount / x) * x; return nearest_x === amount ? nearest_x + x : nearest_x; }; @@ -474,13 +499,16 @@ erpnext.PointOfSale.Payment = class { description = __("You don't have enough points to redeem."); read_only = true; } else { - max_redeemable_amount = flt(flt(loyalty_points) * flt(conversion_factor), precision("loyalty_amount", doc)); + max_redeemable_amount = flt( + flt(loyalty_points) * flt(conversion_factor), + precision("loyalty_amount", doc) + ); description = __("You can redeem upto {0}.", [format_currency(max_redeemable_amount)]); read_only = false; } - const margin = this.$payment_modes.children().length % 2 === 0 ? 'pr-2' : 'pl-2'; - const amount = doc.loyalty_amount > 0 ? format_currency(doc.loyalty_amount, doc.currency) : ''; + const margin = this.$payment_modes.children().length % 2 === 0 ? "pr-2" : "pl-2"; + const amount = doc.loyalty_amount > 0 ? format_currency(doc.loyalty_amount, doc.currency) : ""; this.$payment_modes.append( `
              @@ -492,35 +520,47 @@ erpnext.PointOfSale.Payment = class {
              ` ); - this['loyalty-amount_control'] = frappe.ui.form.make_control({ + this["loyalty-amount_control"] = frappe.ui.form.make_control({ df: { label: __("Redeem Loyalty Points"), - fieldtype: 'Currency', + fieldtype: "Currency", placeholder: __("Enter amount to be redeemed."), - options: 'company:currency', + options: "company:currency", read_only, - onchange: async function() { + onchange: async function () { if (!loyalty_points) return; if (this.value > max_redeemable_amount) { frappe.show_alert({ - message: __("You cannot redeem more than {0}.", [format_currency(max_redeemable_amount)]), - indicator: "red" + message: __("You cannot redeem more than {0}.", [ + format_currency(max_redeemable_amount), + ]), + indicator: "red", }); frappe.utils.play_sound("submit"); - me['loyalty-amount_control'].set_value(0); + me["loyalty-amount_control"].set_value(0); return; } const redeem_loyalty_points = this.value > 0 ? 1 : 0; - await frappe.model.set_value(doc.doctype, doc.name, 'redeem_loyalty_points', redeem_loyalty_points); - frappe.model.set_value(doc.doctype, doc.name, 'loyalty_points', parseInt(this.value / conversion_factor)); + await frappe.model.set_value( + doc.doctype, + doc.name, + "redeem_loyalty_points", + redeem_loyalty_points + ); + frappe.model.set_value( + doc.doctype, + doc.name, + "loyalty_points", + parseInt(this.value / conversion_factor) + ); }, - description + description, }, parent: this.$payment_modes.find(`.loyalty-amount.mode-of-payment-control`), render_input: true, }); - this['loyalty-amount_control'].toggle_label(false); + this["loyalty-amount_control"].toggle_label(false); // this.render_add_payment_method_dom(); } @@ -538,20 +578,22 @@ erpnext.PointOfSale.Payment = class { update_totals_section(doc) { if (!doc) doc = this.events.get_frm().doc; const paid_amount = doc.paid_amount; - const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) + ? doc.grand_total + : doc.rounded_total; const remaining = grand_total - doc.paid_amount; const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined; const currency = doc.currency; - const label = change ? __('Change') : __('To Be Paid'); + const label = change ? __("Change") : __("To Be Paid"); this.$totals.html( `
              -
              ${__('Grand Total')}
              +
              ${__("Grand Total")}
              ${format_currency(grand_total, currency)}
              -
              ${__('Paid Amount')}
              +
              ${__("Paid Amount")}
              ${format_currency(paid_amount, currency)}
              @@ -563,6 +605,6 @@ erpnext.PointOfSale.Payment = class { } toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); + show ? this.$component.css("display", "flex") : this.$component.css("display", "none"); } }; diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.js b/erpnext/selling/page/sales_funnel/sales_funnel.js index e3d0a55c3a0..d8546ba51ce 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.js +++ b/erpnext/selling/page/sales_funnel/sales_funnel.js @@ -1,23 +1,23 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.pages['sales-funnel'].on_page_load = function(wrapper) { +frappe.pages["sales-funnel"].on_page_load = function (wrapper) { frappe.ui.make_app_page({ parent: wrapper, - title: __('Sales Funnel'), - single_column: true + title: __("Sales Funnel"), + single_column: true, }); wrapper.sales_funnel = new erpnext.SalesFunnel(wrapper); frappe.breadcrumbs.add("Selling"); -} +}; erpnext.SalesFunnel = class SalesFunnel { constructor(wrapper) { var me = this; // 0 setTimeout hack - this gives time for canvas to get width and height - setTimeout(function() { + setTimeout(function () { me.setup(wrapper); me.get_data(); }, 0); @@ -26,50 +26,65 @@ erpnext.SalesFunnel = class SalesFunnel { setup(wrapper) { var me = this; - this.company_field = wrapper.page.add_field({"fieldtype": "Link", "fieldname": "company", "options": "Company", - "label": __("Company"), "reqd": 1, "default": frappe.defaults.get_user_default('company'), - change: function() { - me.company = this.value || frappe.defaults.get_user_default('company'); + (this.company_field = wrapper.page.add_field({ + fieldtype: "Link", + fieldname: "company", + options: "Company", + label: __("Company"), + reqd: 1, + default: frappe.defaults.get_user_default("company"), + change: function () { + me.company = this.value || frappe.defaults.get_user_default("company"); me.get_data(); - } - }), + }, + })), + (this.elements = { + layout: $(wrapper).find(".layout-main"), + from_date: wrapper.page.add_date(__("From Date")), + to_date: wrapper.page.add_date(__("To Date")), + chart: wrapper.page.add_select(__("Chart"), [ + { value: "sales_funnel", label: __("Sales Funnel") }, + { value: "sales_pipeline", label: __("Sales Pipeline") }, + { value: "opp_by_lead_source", label: __("Opportunities by lead source") }, + ]), + refresh_btn: wrapper.page.set_primary_action( + __("Refresh"), + function () { + me.get_data(); + }, + "fa fa-refresh" + ), + }); - this.elements = { - layout: $(wrapper).find(".layout-main"), - from_date: wrapper.page.add_date(__("From Date")), - to_date: wrapper.page.add_date(__("To Date")), - chart: wrapper.page.add_select(__("Chart"), [{value: 'sales_funnel', label:__("Sales Funnel")}, - {value: 'sales_pipeline', label:__("Sales Pipeline")}, - {value: 'opp_by_lead_source', label:__("Opportunities by lead source")}]), - refresh_btn: wrapper.page.set_primary_action(__("Refresh"), - function() { me.get_data(); }, "fa fa-refresh"), - }; - - this.elements.no_data = $('
              ' + __("No Data") + '
              ') + this.elements.no_data = $('
              ' + __("No Data") + "
              ") .toggle(false) .appendTo(this.elements.layout); - this.elements.funnel_wrapper = $('
              ') - .appendTo(this.elements.layout); + this.elements.funnel_wrapper = $('
              ').appendTo( + this.elements.layout + ); - this.company = frappe.defaults.get_user_default('company'); + this.company = frappe.defaults.get_user_default("company"); this.options = { from_date: frappe.datetime.add_months(frappe.datetime.get_today(), -1), to_date: frappe.datetime.get_today(), - chart: 'sales_funnel' + chart: "sales_funnel", }; // set defaults and bind on change - $.each(this.options, function(k, v) { - if (['from_date', 'to_date'].includes(k)) { + $.each(this.options, function (k, v) { + if (["from_date", "to_date"].includes(k)) { me.elements[k].val(frappe.datetime.str_to_user(v)); } else { me.elements[k].val(v); } - me.elements[k].on("change", function() { - if (['from_date', 'to_date'].includes(k)) { - me.options[k] = frappe.datetime.user_to_str($(this).val()) != 'Invalid date' ? frappe.datetime.user_to_str($(this).val()) : frappe.datetime.get_today(); + me.elements[k].on("change", function () { + if (["from_date", "to_date"].includes(k)) { + me.options[k] = + frappe.datetime.user_to_str($(this).val()) != "Invalid date" + ? frappe.datetime.user_to_str($(this).val()) + : frappe.datetime.get_today(); } else { me.options.chart = $(this).val(); } @@ -78,12 +93,12 @@ erpnext.SalesFunnel = class SalesFunnel { }); // bind refresh - this.elements.refresh_btn.on("click", function() { + this.elements.refresh_btn.on("click", function () { me.get_data(this); }); // bind resize - $(window).resize(function() { + $(window).resize(function () { me.render(); }); } @@ -95,39 +110,39 @@ erpnext.SalesFunnel = class SalesFunnel { } const method_map = { - "sales_funnel": "erpnext.selling.page.sales_funnel.sales_funnel.get_funnel_data", - "opp_by_lead_source": "erpnext.selling.page.sales_funnel.sales_funnel.get_opp_by_lead_source", - "sales_pipeline": "erpnext.selling.page.sales_funnel.sales_funnel.get_pipeline_data" + sales_funnel: "erpnext.selling.page.sales_funnel.sales_funnel.get_funnel_data", + opp_by_lead_source: "erpnext.selling.page.sales_funnel.sales_funnel.get_opp_by_lead_source", + sales_pipeline: "erpnext.selling.page.sales_funnel.sales_funnel.get_pipeline_data", }; frappe.call({ method: method_map[this.options.chart], args: { from_date: this.options.from_date, to_date: this.options.to_date, - company: this.company + company: this.company, }, btn: btn, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { me.options.data = r.message; - if (me.options.data=='empty') { + if (me.options.data == "empty") { const $parent = me.elements.funnel_wrapper; - $parent.html(__('No data for this period')); + $parent.html(__("No data for this period")); } else { me.render(); } } - } + }, }); } render() { let me = this; - if (me.options.chart == 'sales_funnel'){ + if (me.options.chart == "sales_funnel") { me.render_funnel(); - } else if (me.options.chart == 'opp_by_lead_source'){ + } else if (me.options.chart == "opp_by_lead_source") { me.render_chart("Sales Opportunities by Source"); - } else if (me.options.chart == 'sales_pipeline'){ + } else if (me.options.chart == "sales_pipeline") { me.render_chart("Sales Pipeline by Stage"); } } @@ -143,12 +158,12 @@ erpnext.SalesFunnel = class SalesFunnel { y = 0, y_old = 0.0; - if(this.options.total_value === 0) { + if (this.options.total_value === 0) { this.elements.no_data.toggle(true); return; } - this.options.data.forEach(function(d) { + this.options.data.forEach(function (d) { context.fillStyle = d.color; context.strokeStyle = d.color; me.draw_triangle(x_start, x_mid, x_end, y, me.options.height); @@ -175,21 +190,22 @@ erpnext.SalesFunnel = class SalesFunnel { this.elements.no_data.toggle(false); // calculate width and height options - this.options.width = $(this.elements.funnel_wrapper).width() * 2.0 / 3.0; + this.options.width = ($(this.elements.funnel_wrapper).width() * 2.0) / 3.0; this.options.height = (Math.sqrt(3) * this.options.width) / 2.0; // calculate total weightage // as height decreases, area decreases by the square of the reduction // hence, compensating by squaring the index value - this.options.total_weightage = this.options.data.reduce( - function(prev, curr, i) { return prev + Math.pow(i+1, 2) * curr.value; }, 0.0); + this.options.total_weightage = this.options.data.reduce(function (prev, curr, i) { + return prev + Math.pow(i + 1, 2) * curr.value; + }, 0.0); // calculate height for each data - $.each(this.options.data, function(i, d) { - d.height = me.options.height * d.value * Math.pow(i+1, 2) / me.options.total_weightage; + $.each(this.options.data, function (i, d) { + d.height = (me.options.height * d.value * Math.pow(i + 1, 2)) / me.options.total_weightage; }); - this.elements.canvas = $('') + this.elements.canvas = $("") .appendTo(this.elements.funnel_wrapper.empty()) .attr("width", $(this.elements.funnel_wrapper).width()) .attr("height", this.options.height); @@ -211,7 +227,7 @@ erpnext.SalesFunnel = class SalesFunnel { draw_legend(x_mid, y_mid, width, height, title) { var context = this.elements.context; - if(y_mid == 0) { + if (y_mid == 0) { y_mid = 7; } @@ -229,7 +245,7 @@ erpnext.SalesFunnel = class SalesFunnel { context.fill(); // draw text - context.fillStyle = "black"; + context.fillStyle = ""; context.textBaseline = "middle"; context.font = "1.1em sans-serif"; context.fillText(__(title), width + 20, y_mid); @@ -246,13 +262,13 @@ erpnext.SalesFunnel = class SalesFunnel { title: title, height: 400, data: chart_data, - type: 'bar', + type: "bar", barOptions: { - stacked: 1 + stacked: 1, }, tooltipOptions: { - formatTooltipY: d => format_currency(d, currency), - } + formatTooltipY: (d) => format_currency(d, currency), + }, }); } }; diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index 6b33a717531..24bb0d2fe71 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -85,7 +85,7 @@ def get_opp_by_lead_source(from_date, to_date, company): * x["probability"] / 100 ) - } + }, ) for x in opportunities ] @@ -100,7 +100,7 @@ def get_opp_by_lead_source(from_date, to_date, company): pivot_table = [] for sales_stage in sales_stages: row = [] - for source, sales_stage_values in summary.items(): + for sales_stage_values in summary.values(): row.append(flt(sales_stage_values.get(sales_stage))) pivot_table.append({"chartType": "bar", "name": sales_stage, "values": row}) @@ -137,7 +137,7 @@ def get_pipeline_data(from_date, to_date, company): * x["probability"] / 100 ) - } + }, ) for x in opportunities ] 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 ef87586f66e..26265480416 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.js +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.js @@ -3,32 +3,32 @@ /* eslint-disable */ frappe.query_reports["Address And Contacts"] = { - "filters": [ + filters: [ { - "reqd": 1, - "fieldname":"party_type", - "label": __("Party Type"), - "fieldtype": "Link", - "options": "DocType", - "get_query": function() { + reqd: 1, + fieldname: "party_type", + label: __("Party Type"), + fieldtype: "Link", + options: "DocType", + get_query: function () { return { - "filters": { - "name": ["in","Customer,Supplier,Sales Partner"], - } - } - } + filters: { + name: ["in", "Customer,Supplier,Sales Partner"], + }, + }; + }, }, { - "fieldname":"party_name", - "label": __("Party Name"), - "fieldtype": "Dynamic Link", - "get_options": function() { - let party_type = frappe.query_report.get_filter_value('party_type'); - if(!party_type) { + fieldname: "party_name", + label: __("Party Name"), + fieldtype: "Dynamic Link", + get_options: function () { + let party_type = frappe.query_report.get_filter_value("party_type"); + if (!party_type) { frappe.throw(__("Please select Party Type first")); } return party_type; - } - } - ] -} + }, + }, + ], +}; 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 9a1cfda8474..5ae00f37311 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -27,8 +27,8 @@ def get_columns(filters): party_type = filters.get("party_type") party_type_value = get_party_group(party_type) return [ - "{party_type}:Link/{party_type}".format(party_type=party_type), - "{party_value_type}::150".format(party_value_type=frappe.unscrub(str(party_type_value))), + f"{party_type}:Link/{party_type}", + f"{frappe.unscrub(str(party_type_value))}::150", "Address Line 1", "Address Line 2", "City", @@ -109,7 +109,7 @@ def get_party_details(party_type, party_list, doctype, party_details): ["Dynamic Link", "link_doctype", "=", party_type], ["Dynamic Link", "link_name", "in", party_list], ] - fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) + fields = ["`tabDynamic Link`.link_name", *field_map.get(doctype, [])] records = frappe.get_list(doctype, filters=filters, fields=fields, as_list=True) for d in records: diff --git a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.js b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.js index 1e83acdeb18..a9ee0cbf067 100644 --- a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.js +++ b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.js @@ -2,7 +2,5 @@ // For license information, please see license.txt frappe.query_reports["Available Stock for Packing Items"] = { - "filters": [ - - ] -} + filters: [], +}; diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js index 744b5d982b5..745550b57c6 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js @@ -2,43 +2,43 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Customer Acquisition and Loyalty"] = { - "filters": [ + filters: [ { - "fieldname": "view_type", - "label": __("View Type"), - "fieldtype": "Select", - "options": ["Monthly", "Territory Wise"], - "default": "Monthly", - "reqd": 1 + fieldname: "view_type", + label: __("View Type"), + fieldtype: "Select", + options: ["Monthly", "Territory Wise"], + default: "Monthly", + reqd: 1, }, { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], - "reqd": 1 + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + reqd: 1, }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], - "reqd": 1 - } + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + reqd: 1, + }, ], - 'formatter': function(value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (data && data.bold) { value = value.bold(); } return value; - } -} + }, +}; diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 3e4bfb2ef71..dcdd2525d8e 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -77,10 +77,8 @@ def get_data_by_time(filters, common_columns): out = [] for year in range(from_year, to_year + 1): - for month in range( - from_month if year == from_year else 1, (to_month + 1) if year == to_year else 13 - ): - key = "{year}-{month:02d}".format(year=year, month=month) + for month in range(from_month if year == from_year else 1, (to_month + 1) if year == to_year else 13): + key = f"{year}-{month:02d}" data = customers_in.get(key) new = data["new"] if data else [0, 0.0] repeat = data["repeat"] if data else [0, 0.0] @@ -147,7 +145,7 @@ def get_data_by_territory(filters, common_columns): for ld in loop_data: if ld["parent_territory"]: - parent_data = [x for x in data if x["territory"] == ld["parent_territory"]][0] + parent_data = next(x for x in data if x["territory"] == ld["parent_territory"]) for key in parent_data.keys(): if key not in ["indent", "territory", "parent_territory", "bold"]: parent_data[key] += ld[key] @@ -165,15 +163,12 @@ def get_customer_stats(filters, tree_view=False): customers_in = {} for si in frappe.db.sql( - """select territory, posting_date, customer, base_grand_total from `tabSales Invoice` + f"""select territory, posting_date, customer, base_grand_total from `tabSales Invoice` where docstatus=1 and posting_date <= %(to_date)s - {company_condition} order by posting_date""".format( - company_condition=company_condition - ), + {company_condition} order by posting_date""", filters, as_dict=1, ): - key = si.territory if tree_view else si.posting_date.strftime("%Y-%m") new_or_repeat = "new" if si.customer not in customers else "repeat" customers_in.setdefault(key, {"new": [0, 0.0], "repeat": [0, 0.0]}) diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.js b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.js index 3a99eb0891d..9902abedfa3 100644 --- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.js +++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.js @@ -2,20 +2,20 @@ // For license information, please see license.txt frappe.query_reports["Customer Credit Balance"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"customer", - "label": __("Customer"), - "fieldtype": "Link", - "options": "Customer" - } - ] -} + fieldname: "customer", + label: __("Customer"), + fieldtype: "Link", + options: "Customer", + }, + ], +}; diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py index 98633cb7198..3fd37f73d84 100644 --- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py +++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py @@ -77,7 +77,6 @@ def get_columns(customer_naming_type): def get_details(filters): - sql_query = """SELECT c.name, c.customer_name, ccl.bypass_credit_limit_check, diff --git a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.js b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.js index d333c8be65e..6d937a8eede 100644 --- a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.js +++ b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.js @@ -3,25 +3,25 @@ /* eslint-disable */ frappe.query_reports["Customer-wise Item Price"] = { - "filters": [ + filters: [ { - "label": __("Customer"), - "fieldname": "customer", - "fieldtype": "Link", - "options": "Customer", - "reqd": 1 + label: __("Customer"), + fieldname: "customer", + fieldtype: "Link", + options: "Customer", + reqd: 1, }, { - "label": __("Item"), - "fieldname": "item", - "fieldtype": "Link", - "options": "Item", - "get_query": () => { + label: __("Item"), + fieldname: "item", + fieldtype: "Link", + options: "Item", + get_query: () => { return { query: "erpnext.controllers.queries.item_query", - filters: { 'is_sales_item': 1 } - } - } - } - ] -} + filters: { is_sales_item: 1 }, + }; + }, + }, + ], +}; diff --git a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py index a58f40362ba..84da765d930 100644 --- a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py +++ b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py @@ -3,11 +3,11 @@ import frappe -from frappe import _ +from frappe import _, qb +from frappe.query_builder import Criterion from erpnext import get_default_company from erpnext.accounts.party import get_party_details -from erpnext.stock.get_item_details import get_price_list_rate_for def execute(filters=None): @@ -50,6 +50,45 @@ def get_columns(filters=None): ] +def fetch_item_prices( + customer: str | None = None, + price_list: str | None = None, + selling_price_list: str | None = None, + items: list | None = None, +): + price_list_map = frappe._dict() + ip = qb.DocType("Item Price") + and_conditions = [] + or_conditions = [] + if items: + and_conditions.append(ip.item_code.isin([x.item_code for x in items])) + and_conditions.append(ip.selling.eq(True)) + + or_conditions.append(ip.customer.isnull()) + or_conditions.append(ip.price_list.isnull()) + + if customer: + or_conditions.append(ip.customer == customer) + + if price_list: + or_conditions.append(ip.price_list == price_list) + + if selling_price_list: + or_conditions.append(ip.price_list == selling_price_list) + + res = ( + qb.from_(ip) + .select(ip.item_code, ip.price_list, ip.price_list_rate) + .where(Criterion.all(and_conditions)) + .where(Criterion.any(or_conditions)) + .run(as_dict=True) + ) + for x in res: + price_list_map.update({(x.item_code, x.price_list): x.price_list_rate}) + + return price_list_map + + def get_data(filters=None): data = [] customer_details = get_customer_details(filters) @@ -59,9 +98,17 @@ def get_data(filters=None): "Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code" ) item_stock_map = {item.item_code: item.available for item in item_stock_map} + price_list_map = fetch_item_prices( + customer_details.customer, + customer_details.price_list, + customer_details.selling_price_list, + items, + ) for item in items: - price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0 + price_list_rate = price_list_map.get( + (item.item_code, customer_details.price_list or customer_details.selling_price_list), 0.0 + ) available_stock = item_stock_map.get(item.item_code) data.append( diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.js b/erpnext/selling/report/inactive_customers/inactive_customers.js index 804d17e2b0b..a191d564fee 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.js +++ b/erpnext/selling/report/inactive_customers/inactive_customers.js @@ -2,19 +2,19 @@ // For license information, please see license.txt frappe.query_reports["Inactive Customers"] = { - "filters": [ + filters: [ { - "fieldname":"days_since_last_order", - "label": __("Days Since Last Order"), - "fieldtype": "Int", - "default": 60 + fieldname: "days_since_last_order", + label: __("Days Since Last Order"), + fieldtype: "Int", + default: 60, }, { - "fieldname":"doctype", - "label": __("Doctype"), - "fieldtype": "Select", - "default": "Sales Order", - "options": "Sales Order\nSales Invoice" - } - ] -} + fieldname: "doctype", + label: __("Doctype"), + fieldtype: "Select", + default: "Sales Order", + options: "Sales Order\nSales Invoice", + }, + ], +}; diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index a1660853272..7e4ddc128ac 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -40,19 +40,17 @@ def get_sales_details(doctype): DATEDIFF(CURRENT_DATE, max(so.transaction_date)) as 'days_since_last_order'""" return frappe.db.sql( - """select + f"""select cust.name, cust.customer_name, cust.territory, cust.customer_group, count(distinct(so.name)) as 'num_of_order', - sum(base_net_total) as 'total_order_value', {0} - from `tabCustomer` cust, `tab{1}` so + sum(base_net_total) as 'total_order_value', {cond} + from `tabCustomer` cust, `tab{doctype}` so where cust.name = so.customer and so.docstatus = 1 group by cust.name - order by 'days_since_last_order' desc """.format( - cond, doctype - ), + order by 'days_since_last_order' desc """, as_list=1, ) @@ -62,11 +60,9 @@ def get_last_sales_amt(customer, doctype): if doctype == "Sales Order": cond = "transaction_date" res = frappe.db.sql( - """select base_net_total from `tab{0}` - where customer = %s and docstatus = 1 order by {1} desc - limit 1""".format( - doctype, cond - ), + f"""select base_net_total from `tab{doctype}` + where customer = %s and docstatus = 1 order by {cond} desc + limit 1""", customer, ) diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js index 073be789791..1db05643b4b 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js @@ -3,55 +3,55 @@ /* eslint-disable */ frappe.query_reports["Item-wise Sales History"] = { - "filters": [ + filters: [ { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { - fieldname:"from_date", + fieldname: "from_date", reqd: 1, label: __("From Date"), fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - fieldname:"to_date", + fieldname: "to_date", reqd: 1, default: frappe.datetime.get_today(), label: __("To Date"), fieldtype: "Date", }, { - fieldname:"item_group", + fieldname: "item_group", label: __("Item Group"), fieldtype: "Link", - options: "Item Group" + options: "Item Group", }, { - fieldname:"item_code", + fieldname: "item_code", label: __("Item"), fieldtype: "Link", options: "Item", get_query: () => { return { - query: "erpnext.controllers.queries.item_query" - } - } + query: "erpnext.controllers.queries.item_query", + }; + }, }, { - fieldname:"customer", + fieldname: "customer", label: __("Customer"), fieldtype: "Link", - options: "Customer" - } + options: "Customer", + }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); let format_fields = ["delivered_quantity", "billed_amount"]; @@ -59,5 +59,5 @@ frappe.query_reports["Item-wise Sales History"] = { value = "" + value + ""; } return value; - } + }, }; diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 2624db3191d..9cdb14caf46 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -128,7 +128,6 @@ def get_columns(filters): def get_data(filters): - data = [] company_list = get_descendants_of("Company", filters.get("company")) @@ -181,9 +180,7 @@ def get_item_details(): details = frappe.db.get_all("Item", fields=["name", "item_name", "item_group"]) item_details = {} for d in details: - item_details.setdefault( - d.name, frappe._dict({"item_name": d.item_name, "item_group": d.item_group}) - ) + item_details.setdefault(d.name, frappe._dict({"item_name": d.item_name, "item_group": d.item_group})) return item_details @@ -240,14 +237,13 @@ def get_chart_data(data): for row in data: item_key = row.get("item_code") - if not item_key in item_wise_sales_map: + if item_key not in item_wise_sales_map: item_wise_sales_map[item_key] = 0 item_wise_sales_map[item_key] = flt(item_wise_sales_map[item_key]) + flt(row.get("amount")) item_wise_sales_map = { - item: value - for item, value in (sorted(item_wise_sales_map.items(), key=lambda i: i[1], reverse=True)) + item: value for item, value in (sorted(item_wise_sales_map.items(), key=lambda i: i[1], reverse=True)) } for key in item_wise_sales_map: diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.py b/erpnext/selling/report/lost_quotations/lost_quotations.py index 7c0bfbdd525..a48e945172c 100644 --- a/erpnext/selling/report/lost_quotations/lost_quotations.py +++ b/erpnext/selling/report/lost_quotations/lost_quotations.py @@ -53,9 +53,7 @@ def get_columns(group_by: Literal["Lost Reason", "Competitor"]): ] -def get_data( - company: str, from_date: str, to_date: str, group_by: Literal["Lost Reason", "Competitor"] -): +def get_data(company: str, from_date: str, to_date: str, group_by: Literal["Lost Reason", "Competitor"]): """Return quotation value grouped by lost reason or competitor""" if group_by == "Lost Reason": fieldname = "lost_reason" diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 990d736baa4..32c84b2538b 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -5,140 +5,135 @@ function get_filters() { let filters = [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"period_start_date", - "label": __("Start Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + fieldname: "period_start_date", + label: __("Start Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - "fieldname":"period_end_date", - "label": __("End Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.get_today() + fieldname: "period_end_date", + label: __("End Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.get_today(), }, { - "fieldname":"customer_group", - "label": __("Customer Group"), - "fieldtype": "Link", - "width": 100, - "options": "Customer Group", + fieldname: "customer_group", + label: __("Customer Group"), + fieldtype: "Link", + width: 100, + options: "Customer Group", }, { - "fieldname":"customer", - "label": __("Customer"), - "fieldtype": "Link", - "width": 100, - "options": "Customer", - "get_query": () => { - var customer_group = frappe.query_report.get_filter_value('customer_group'); - return{ - "query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items", - "filters": [ - ['Customer', 'disabled', '=', '0'], - ['Customer Group','name', '=', customer_group] - ] - } - } + fieldname: "customer", + label: __("Customer"), + fieldtype: "Link", + width: 100, + options: "Customer", + get_query: () => { + var customer_group = frappe.query_report.get_filter_value("customer_group"); + return { + query: "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items", + filters: [ + ["Customer", "disabled", "=", "0"], + ["Customer Group", "name", "=", customer_group], + ], + }; + }, }, { - "fieldname":"item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "width": 100, - "options": "Item Group", - + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + width: 100, + options: "Item Group", }, { - "fieldname":"item", - "label": __("Item"), - "fieldtype": "Link", - "width": 100, - "options": "Item", - "get_query": () => { - var item_group = frappe.query_report.get_filter_value('item_group'); - return{ - "query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items", - "filters": [ - ['Item', 'disabled', '=', '0'], - ['Item Group','name', '=', item_group] - ] - } - } + fieldname: "item", + label: __("Item"), + fieldtype: "Link", + width: 100, + options: "Item", + get_query: () => { + var item_group = frappe.query_report.get_filter_value("item_group"); + return { + query: "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items", + filters: [ + ["Item", "disabled", "=", "0"], + ["Item Group", "name", "=", item_group], + ], + }; + }, }, { - "fieldname":"from_due_date", - "label": __("From Due Date"), - "fieldtype": "Date", + fieldname: "from_due_date", + label: __("From Due Date"), + fieldtype: "Date", }, { - "fieldname":"to_due_date", - "label": __("To Due Date"), - "fieldtype": "Date", + fieldname: "to_due_date", + label: __("To Due Date"), + fieldtype: "Date", }, { - "fieldname":"status", - "label": __("Status"), - "fieldtype": "MultiSelectList", - "width": 100, - get_data: function(txt) { - let status = ["Overdue", "Unpaid", "Completed", "Partly Paid"] - let options = [] - for (let option of status){ + fieldname: "status", + label: __("Status"), + fieldtype: "MultiSelectList", + width: 100, + get_data: function (txt) { + let status = ["Overdue", "Unpaid", "Completed", "Partly Paid"]; + let options = []; + for (let option of status) { options.push({ - "value": option, - "label": __(option), - "description": "" - }) + value: option, + label: __(option), + description: "", + }); } - return options - } + return options; + }, }, { - "fieldname":"only_immediate_upcoming_term", - "label": __("Show only the Immediate Upcoming Term"), - "fieldtype": "Check", + fieldname: "only_immediate_upcoming_term", + label: __("Show only the Immediate Upcoming Term"), + fieldtype: "Check", }, - ] + ]; return filters; } frappe.query_reports["Payment Terms Status for Sales Order"] = { - "filters": get_filters(), - "formatter": function(value, row, column, data, default_formatter){ - if(column.fieldname == 'invoices' && value) { - invoices = value.split(','); + filters: get_filters(), + formatter: function (value, row, column, data, default_formatter) { + if (column.fieldname == "invoices" && value) { + invoices = value.split(","); const invoice_formatter = (prev_value, curr_value) => { - if(prev_value != "") { + if (prev_value != "") { return prev_value + ", " + default_formatter(curr_value, row, column, data); - } - else { + } else { return default_formatter(curr_value, row, column, data); } - } - return invoices.reduce(invoice_formatter, "") - } - else if (column.fieldname == 'paid_amount' && value){ + }; + return invoices.reduce(invoice_formatter, ""); + } else if (column.fieldname == "paid_amount" && value) { formatted_value = default_formatter(value, row, column, data); - if(value > 0) { - formatted_value = "" + formatted_value + "" + if (value > 0) { + formatted_value = "" + formatted_value + ""; } return formatted_value; - } - else if (column.fieldname == 'status' && value == 'Completed'){ + } else if (column.fieldname == "status" && value == "Completed") { return "" + default_formatter(value, row, column, data) + ""; } return default_formatter(value, row, column, data); }, - }; diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 3682c5fd62e..cf61a0e35f3 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -82,9 +82,7 @@ def get_descendants_of(doctype, group_name): ).run()[0] # get all children of group node - query = ( - qb.from_(group_doc).select(group_doc.name).where((group_doc.lft >= lft) & (group_doc.rgt <= rgt)) - ) + query = qb.from_(group_doc).select(group_doc.name).where((group_doc.lft >= lft) & (group_doc.rgt <= rgt)) child_nodes = [] for x in query.run(): @@ -108,7 +106,9 @@ def get_customers_or_items(doctype, txt, searchfield, start, page_len, filters): ) elif item[0] == "Item Group": if item[3] != "": - filter_list.append([doctype, "item_group", "in", get_descendants_of("Item Group", item[3])]) + filter_list.append( + [doctype, "item_group", "in", get_descendants_of("Item Group", item[3])] + ) if searchfield and txt: filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt]) @@ -132,9 +132,7 @@ def get_conditions(filters): conditions.company = filters.company or frappe.defaults.get_user_default("company") conditions.end_date = filters.period_end_date or frappe.utils.today() - conditions.start_date = filters.period_start_date or frappe.utils.add_months( - conditions.end_date, -1 - ) + conditions.start_date = filters.period_start_date or frappe.utils.add_months(conditions.end_date, -1) return conditions @@ -210,7 +208,6 @@ def get_so_with_invoices(filters): .where( (so.docstatus == 1) & (so.status.isin(["To Deliver and Bill", "To Bill"])) - & (so.payment_terms_template != "NULL") & (so.company == conditions.company) & (so.transaction_date[conditions.start_date : conditions.end_date]) ) diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.js b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.js index 37634efb6cd..484fdc508b0 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.js +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.js @@ -2,5 +2,4 @@ // For license information, please see license.txt /* eslint-disable */ -frappe.query_reports["Pending SO Items For Purchase Request"] = { -} +frappe.query_reports["Pending SO Items For Purchase Request"] = {}; diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py index 928ed80d5c9..7f9f3fb999e 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py @@ -81,9 +81,7 @@ def get_data(): bundled_item_map = get_packed_items(sales_orders) - item_with_product_bundle = get_items_with_product_bundle( - [row.item_code for row in sales_order_entry] - ) + item_with_product_bundle = get_items_with_product_bundle([row.item_code for row in sales_order_entry]) materials_request_dict = {} @@ -129,7 +127,9 @@ def get_data(): "description": item.description, "sales_order_no": so.name, "date": so.transaction_date, - "material_request": ",".join(material_requests_against_so.get("material_requests", [])), + "material_request": ",".join( + material_requests_against_so.get("material_requests", []) + ), "customer": so.customer, "territory": so.territory, "so_qty": item.qty, diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.js b/erpnext/selling/report/quotation_trends/quotation_trends.js index 97a19315ec3..8ffeda47b64 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.js +++ b/erpnext/selling/report/quotation_trends/quotation_trends.js @@ -1,8 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.require("assets/erpnext/js/sales_trends_filters.js", function() { +frappe.require("assets/erpnext/js/sales_trends_filters.js", function () { frappe.query_reports["Quotation Trends"] = { - filters: erpnext.get_sales_trends_filters() - } + filters: erpnext.get_sales_trends_filters(), + }; }); diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py index 4d71ce77c4f..bcb8fe9297e 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.py +++ b/erpnext/selling/report/quotation_trends/quotation_trends.py @@ -48,9 +48,7 @@ def get_chart_data(data, conditions, filters): return { "data": { "labels": labels, - "datasets": [ - {"name": _(filters.get("period")) + " " + _("Quoted Amount"), "values": datapoints} - ], + "datasets": [{"name": _(filters.get("period")) + " " + _("Quoted Amount"), "values": datapoints}], }, "type": "line", "lineOptions": {"regionFill": 1}, diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js index e56fe8b5732..0f87923949f 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.js +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -3,47 +3,55 @@ /* eslint-disable */ frappe.query_reports["Sales Analytics"] = { - "filters": [ + filters: [ { fieldname: "tree_type", label: __("Tree Type"), fieldtype: "Select", - options: ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Order Type", "Project"], + options: [ + "Customer Group", + "Customer", + "Item Group", + "Item", + "Territory", + "Order Type", + "Project", + ], default: "Customer", - reqd: 1 + reqd: 1, }, { fieldname: "doc_type", label: __("based_on"), fieldtype: "Select", - options: ["Sales Order","Delivery Note","Sales Invoice"], + options: ["Sales Order", "Delivery Note", "Sales Invoice"], default: "Sales Invoice", - reqd: 1 + reqd: 1, }, { fieldname: "value_quantity", label: __("Value Or Qty"), fieldtype: "Select", options: [ - { "value": "Value", "label": __("Value") }, - { "value": "Quantity", "label": __("Quantity") }, + { value: "Value", label: __("Value") }, + { value: "Quantity", label: __("Quantity") }, ], default: "Value", - reqd: 1 + reqd: 1, }, { fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], - reqd: 1 + reqd: 1, }, { fieldname: "company", @@ -51,21 +59,21 @@ frappe.query_reports["Sales Analytics"] = { fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { fieldname: "range", label: __("Range"), fieldtype: "Select", options: [ - { "value": "Weekly", "label": __("Weekly") }, - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Yearly", "label": __("Yearly") } + { value: "Weekly", label: __("Weekly") }, + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Yearly", label: __("Yearly") }, ], default: "Monthly", - reqd: 1 - } + reqd: 1, + }, ], get_datatable_options(options) { return Object.assign(options, { @@ -73,32 +81,26 @@ frappe.query_reports["Sales Analytics"] = { events: { onCheckRow: function (data) { if (!data) return; - const data_doctype = $( - data[2].html - )[0].attributes.getNamedItem("data-doctype").value; + const data_doctype = $(data[2].html)[0].attributes.getNamedItem("data-doctype").value; const tree_type = frappe.query_report.filters[0].value; if (data_doctype != tree_type) return; const row_name = data[2].content; const raw_data = frappe.query_report.chart.data; const new_datasets = raw_data.datasets; - const element_found = new_datasets.some( - (element, index, array) => { - if (element.name == row_name) { - array.splice(index, 1); - return true; - } - return false; + const element_found = new_datasets.some((element, index, array) => { + if (element.name == row_name) { + array.splice(index, 1); + return true; } - ); + return false; + }); const slice_at = { Customer: 4, Item: 5 }[tree_type] || 3; if (!element_found) { new_datasets.push({ name: row_name, - values: data - .slice(slice_at, data.length - 1) - .map(column => column.content), + values: data.slice(slice_at, data.length - 1).map((column) => column.content), }); } @@ -106,7 +108,9 @@ frappe.query_reports["Sales Analytics"] = { labels: raw_data.labels, datasets: new_datasets, }; - const new_options = Object.assign({}, frappe.query_report.chart_options, {data: new_data}); + const new_options = Object.assign({}, frappe.query_report.chart_options, { + data: new_data, + }); frappe.query_report.render_chart(new_options); frappe.query_report.raw_chart_data = new_data; diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 605d2fac44f..27d2e6e555e 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -13,7 +13,7 @@ def execute(filters=None): return Analytics(filters).run() -class Analytics(object): +class Analytics: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.date_field = ( @@ -87,9 +87,7 @@ class Analytics(object): {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} ) - self.columns.append( - {"label": _("Total"), "fieldname": "total", "fieldtype": "Float", "width": 120} - ) + self.columns.append({"label": _("Total"), "fieldname": "total", "fieldtype": "Float", "width": 120}) def get_data(self): if self.filters.tree_type in ["Customer", "Supplier"]: @@ -129,9 +127,7 @@ class Analytics(object): """ select s.order_type as entity, s.{value_field} as value_field, s.{date_field} from `tab{doctype}` s where s.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s and ifnull(s.order_type, '') != '' order by s.order_type - """.format( - date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type - ), + """.format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1, ) @@ -166,7 +162,6 @@ class Analytics(object): self.entity_names.setdefault(d.entity, d.entity_name) def get_sales_transactions_based_on_items(self): - if self.filters["value_quantity"] == "Value": value_field = "base_net_amount" else: @@ -178,9 +173,7 @@ class Analytics(object): from `tab{doctype} Item` i , `tab{doctype}` s where s.name = i.parent and i.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s - """.format( - date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type - ), + """.format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1, ) @@ -221,14 +214,12 @@ class Analytics(object): value_field = "qty" self.entries = frappe.db.sql( - """ - select i.item_group as entity, i.{value_field} as value_field, s.{date_field} - from `tab{doctype} Item` i , `tab{doctype}` s + f""" + select i.item_group as entity, i.{value_field} as value_field, s.{self.date_field} + from `tab{self.filters.doc_type} Item` i , `tab{self.filters.doc_type}` s where s.name = i.parent and i.docstatus = 1 and s.company = %s - and s.{date_field} between %s and %s - """.format( - date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type - ), + and s.{self.date_field} between %s and %s + """, (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1, ) @@ -294,7 +285,7 @@ class Analytics(object): total += amount row["total"] = total - out = [row] + out + out = [row, *out] self.data = out @@ -330,9 +321,7 @@ class Analytics(object): from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) - increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get( - self.filters.range, 1 - ) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(self.filters.range, 1) if self.filters.range in ["Monthly", "Quarterly"]: from_date = from_date.replace(day=1) @@ -342,7 +331,7 @@ class Analytics(object): from_date = from_date + relativedelta(from_date, weekday=MO(-1)) self.periodic_daterange = [] - for dummy in range(1, 53): + for _dummy in range(1, 53): if self.filters.range == "Weekly": period_end_date = add_days(from_date, 6) else: @@ -370,10 +359,8 @@ class Analytics(object): self.depth_map = frappe._dict() self.group_entries = frappe.db.sql( - """select name, lft, rgt , {parent} as parent - from `tab{tree}` order by lft""".format( - tree=self.filters.tree_type, parent=parent - ), + f"""select name, lft, rgt , {parent} as parent + from `tab{self.filters.tree_type}` order by lft""", as_dict=1, ) @@ -387,12 +374,10 @@ class Analytics(object): self.depth_map = frappe._dict() self.group_entries = frappe.db.sql( - """ select * from (select "Order Types" as name, 0 as lft, + f""" select * from (select "Order Types" as name, 0 as lft, 2 as rgt, '' as parent union select distinct order_type as name, 1 as lft, 1 as rgt, "Order Types" as parent - from `tab{doctype}` where ifnull(order_type, '') != '') as b order by lft, name - """.format( - doctype=self.filters.doc_type - ), + from `tab{self.filters.doc_type}` where ifnull(order_type, '') != '') as b order by lft, name + """, as_dict=1, ) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js index 91748bc7be2..25089c4b870 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js @@ -3,74 +3,74 @@ /* eslint-disable */ frappe.query_reports["Sales Order Analysis"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "width": "80", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_default("company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + width: "80", + options: "Company", + reqd: 1, + default: frappe.defaults.get_default("company"), }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.get_today(), }, { - "fieldname": "sales_order", - "label": __("Sales Order"), - "fieldtype": "MultiSelectList", - "width": "80", - "options": "Sales Order", - "get_data": function(txt) { + fieldname: "sales_order", + label: __("Sales Order"), + fieldtype: "MultiSelectList", + width: "80", + options: "Sales Order", + get_data: function (txt) { return frappe.db.get_link_options("Sales Order", txt); }, - "get_query": () =>{ + get_query: () => { return { - filters: { "docstatus": 1 } - } - } + filters: { docstatus: 1 }, + }; + }, }, { - "fieldname": "status", - "label": __("Status"), - "fieldtype": "MultiSelectList", - "width": "80", - get_data: function(txt) { - let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"] - let options = [] - for (let option of status){ + fieldname: "status", + label: __("Status"), + fieldtype: "MultiSelectList", + width: "80", + get_data: function (txt) { + let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"]; + let options = []; + for (let option of status) { options.push({ - "value": option, - "label": __(option), - "description": "" - }) + value: option, + label: __(option), + description: "", + }); } - return options - } + return options; + }, }, { - "fieldname": "group_by_so", - "label": __("Group by Sales Order"), - "fieldtype": "Check", - "default": 0 - } + fieldname: "group_by_so", + label: __("Group by Sales Order"), + fieldtype: "Check", + default: 0, + }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); let format_fields = ["delivered_qty", "billed_amount"]; @@ -82,5 +82,5 @@ frappe.query_reports["Sales Order Analysis"] = { value = "" + value + ""; } return value; - } + }, }; diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 720aa41982a..812c21ed750 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -58,7 +58,7 @@ def get_conditions(filters): def get_data(conditions, filters): data = frappe.db.sql( - """ + f""" SELECT so.transaction_date as date, soi.delivery_date as delivery_date, @@ -88,9 +88,7 @@ def get_data(conditions, filters): {conditions} GROUP BY soi.name ORDER BY so.transaction_date ASC, soi.item_code ASC - """.format( - conditions=conditions - ), + """, filters, as_dict=1, ) @@ -164,7 +162,7 @@ def prepare_data(data, so_elapsed_time, filters): if filters.get("group_by_so"): so_name = row["sales_order"] - if not so_name in sales_order_map: + if so_name not in sales_order_map: # create an entry row_copy = copy.deepcopy(row) sales_order_map[so_name] = row_copy diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.js b/erpnext/selling/report/sales_order_trends/sales_order_trends.js index b22ea8fa2ca..fe38804ed45 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.js +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.js @@ -1,8 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.require("assets/erpnext/js/sales_trends_filters.js", function() { +frappe.require("assets/erpnext/js/sales_trends_filters.js", function () { frappe.query_reports["Sales Order Trends"] = { - filters: erpnext.get_sales_trends_filters() - } + filters: erpnext.get_sales_trends_filters(), + }; }); diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js index 63d930c1452..2a67fa35ddf 100644 --- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js +++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js @@ -3,20 +3,19 @@ /* eslint-disable */ frappe.query_reports["Sales Partner Commission Summary"] = { - "filters": [ - + filters: [ { fieldname: "sales_partner", label: __("Sales Partner"), fieldtype: "Link", - options: "Sales Partner" + options: "Sales Partner", }, { fieldname: "doctype", label: __("Document Type"), fieldtype: "Select", options: "Sales Order\nDelivery Note\nSales Invoice", - default: "Sales Order" + default: "Sales Order", }, { fieldname: "from_date", @@ -25,30 +24,29 @@ frappe.query_reports["Sales Partner Commission Summary"] = { default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.datetime.get_today() + default: frappe.datetime.get_today(), }, { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", - default: frappe.defaults.get_user_default("Company") + default: frappe.defaults.get_user_default("Company"), }, { - fieldname:"customer", + fieldname: "customer", label: __("Customer"), fieldtype: "Link", options: "Customer", }, { - fieldname:"territory", + fieldname: "territory", label: __("Territory"), fieldtype: "Link", options: "Territory", }, - - ] -} + ], +}; diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py index cf9ea219c1b..844aa86b52e 100644 --- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py +++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py @@ -75,16 +75,14 @@ def get_entries(filters): entries = frappe.db.sql( """ SELECT - name, customer, territory, {0} as posting_date, base_net_total as amount, + name, customer, territory, {} as posting_date, base_net_total as amount, sales_partner, commission_rate, total_commission FROM - `tab{1}` + `tab{}` WHERE - {2} and docstatus = 1 and sales_partner is not null + {} and docstatus = 1 and sales_partner is not null and sales_partner != '' order by name desc, sales_partner - """.format( - date_field, filters.get("doctype"), conditions - ), + """.format(date_field, filters.get("doctype"), conditions), filters, as_dict=1, ) @@ -97,15 +95,15 @@ def get_conditions(filters, date_field): for field in ["company", "customer", "territory"]: if filters.get(field): - conditions += " and {0} = %({1})s".format(field, field) + conditions += f" and {field} = %({field})s" if filters.get("sales_partner"): conditions += " and sales_partner = %(sales_partner)s" if filters.get("from_date"): - conditions += " and {0} >= %(from_date)s".format(date_field) + conditions += f" and {date_field} >= %(from_date)s" if filters.get("to_date"): - conditions += " and {0} <= %(to_date)s".format(date_field) + conditions += f" and {date_field} <= %(to_date)s" return conditions diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index 7d28f2b90d2..5046ec52c95 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -94,8 +94,8 @@ def get_columns(filters, period_list, partner_doctype): ] for period in period_list: - target_key = "target_{}".format(period.key) - variance_key = "variance_{}".format(period.key) + target_key = f"target_{period.key}" + variance_key = f"variance_{period.key}" columns.extend( [ @@ -169,9 +169,7 @@ def prepare_data( for d in sales_users_data: key = (d.parent, d.item_group) - dist_data = get_periodwise_distribution_data( - d.distribution_id, period_list, filters.get("period") - ) + dist_data = get_periodwise_distribution_data(d.distribution_id, period_list, filters.get("period")) if key not in rows: rows.setdefault(key, {"total_target": 0, "total_achieved": 0, "total_variance": 0}) @@ -182,8 +180,8 @@ def prepare_data( if p_key not in details: details[p_key] = 0 - target_key = "target_{}".format(p_key) - variance_key = "variance_{}".format(p_key) + target_key = f"target_{p_key}" + variance_key = f"variance_{p_key}" details[target_key] = (d.get(target_qty_amt_field) * dist_data.get(p_key)) / 100 details[variance_key] = 0 details["total_target"] += details[target_key] @@ -197,6 +195,8 @@ def prepare_data( ): details[p_key] += r.get(qty_or_amount_field, 0) details[variance_key] = details.get(p_key) - details.get(target_key) + else: + details[variance_key] = details.get(p_key) - details.get(target_key) details["total_achieved"] += details.get(p_key) details["total_variance"] = details.get("total_achieved") - details.get("total_target") @@ -206,43 +206,38 @@ def prepare_data( def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_field): fiscal_year = get_fiscal_year(fiscal_year=filters.get("fiscal_year"), as_dict=1) - dates = [fiscal_year.year_start_date, fiscal_year.year_end_date] - select_field = "`tab{0}`.{1}".format(filters.get("doctype"), sales_field) - child_table = "`tab{0}`".format(filters.get("doctype") + " Item") + parent_doc = frappe.qb.DocType(filters.get("doctype")) + child_doc = frappe.qb.DocType(filters.get("doctype") + " Item") + + query = frappe.qb.from_(parent_doc).inner_join(child_doc).on(child_doc.parent == parent_doc.name) if sales_field == "sales_person": - select_field = "`tabSales Team`.sales_person" - child_table = "`tab{0}`, `tabSales Team`".format(filters.get("doctype") + " Item") - cond = """`tabSales Team`.parent = `tab{0}`.name and - `tabSales Team`.sales_person in ({1}) """.format( - filters.get("doctype"), ",".join(["%s"] * len(sales_users_or_territory_data)) - ) - else: - cond = "`tab{0}`.{1} in ({2})".format( - filters.get("doctype"), sales_field, ",".join(["%s"] * len(sales_users_or_territory_data)) - ) + sales_team = frappe.qb.DocType("Sales Team") + stock_qty = child_doc.stock_qty * sales_team.allocated_percentage / 100 + net_amount = child_doc.base_net_amount * sales_team.allocated_percentage / 100 + sales_field_col = sales_team[sales_field] - return frappe.db.sql( - """ SELECT `tab{child_doc}`.item_group, - `tab{child_doc}`.stock_qty, `tab{child_doc}`.base_net_amount, - {select_field}, `tab{parent_doc}`.{date_field} - FROM `tab{parent_doc}`, {child_table} - WHERE - `tab{child_doc}`.parent = `tab{parent_doc}`.name - and `tab{parent_doc}`.docstatus = 1 and {cond} - and `tab{parent_doc}`.{date_field} between %s and %s""".format( - cond=cond, - date_field=date_field, - select_field=select_field, - child_table=child_table, - parent_doc=filters.get("doctype"), - child_doc=filters.get("doctype") + " Item", - ), - tuple(sales_users_or_territory_data + dates), - as_dict=1, + query = query.inner_join(sales_team).on(sales_team.parent == parent_doc.name) + else: + stock_qty = child_doc.stock_qty + net_amount = child_doc.base_net_amount + sales_field_col = parent_doc[sales_field] + + query = query.select( + child_doc.item_group, + parent_doc[date_field], + (stock_qty).as_("stock_qty"), + (net_amount).as_("base_net_amount"), + sales_field_col, + ).where( + (parent_doc.docstatus == 1) + & (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date)) + & (sales_field_col.isin(sales_users_or_territory_data)) ) + return query.run(as_dict=True) + def get_parents_data(filters, partner_doctype): filters_dict = {"parenttype": partner_doctype} diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js index adae47b87da..f8c950763e1 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js @@ -3,61 +3,59 @@ /* eslint-disable */ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = { - "filters": [ + filters: [ { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", - default: frappe.defaults.get_user_default("Company") + default: frappe.defaults.get_user_default("Company"), }, { fieldname: "fiscal_year", label: __("Fiscal Year"), fieldtype: "Link", options: "Fiscal Year", - default: frappe.sys_defaults.fiscal_year + default: frappe.sys_defaults.fiscal_year, }, { fieldname: "doctype", label: __("Document Type"), fieldtype: "Select", options: "Sales Order\nDelivery Note\nSales Invoice", - default: "Sales Order" + default: "Sales Order", }, { fieldname: "period", label: __("Period"), fieldtype: "Select", options: [ - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Half-Yearly", "label": __("Half-Yearly") }, - { "value": "Yearly", "label": __("Yearly") } + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, ], - default: "Monthly" + default: "Monthly", }, { fieldname: "target_on", label: __("Target On"), fieldtype: "Select", options: "Quantity\nAmount", - default: "Quantity" + default: "Quantity", }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.fieldname.includes('variance')) { - + if (column.fieldname.includes("variance")) { if (data[column.fieldname] < 0) { value = "" + value + ""; - } - else if (data[column.fieldname] > 0) { + } else if (data[column.fieldname] > 0) { value = "" + value + ""; } } return value; - } -} + }, +}; diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.py index f2b6a54a8a6..bad6c94e9dd 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.py @@ -8,6 +8,4 @@ from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.it def execute(filters=None): - data = [] - return get_data_column(filters, "Sales Partner") diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py new file mode 100644 index 00000000000..17186687d97 --- /dev/null +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py @@ -0,0 +1,57 @@ +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import flt, nowdate + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.utils import get_fiscal_year +from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.sales_partner_target_variance_based_on_item_group import ( + execute, +) +from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import ( + create_sales_target_doc, + create_target_distribution, +) + + +class TestSalesPartnerTargetVarianceBasedOnItemGroup(FrappeTestCase): + def setUp(self): + self.fiscal_year = get_fiscal_year(nowdate())[0] + + def tearDown(self): + frappe.db.rollback() + + def test_achieved_target_and_variance_for_partner(self): + # Create a Target Distribution + distribution = create_target_distribution(self.fiscal_year) + + # Create Sales Partner with targets for the current fiscal year + sales_partner = create_sales_target_doc( + "Sales Partner", "partner_name", "Sales Partner 1", self.fiscal_year, distribution.name + ) + + # Create a Sales Invoice for the Partner + si = create_sales_invoice( + rate=1000, + qty=20, + do_not_submit=True, + ) + si.sales_partner = sales_partner + si.commission_rate = 5 + si.submit() + + # Check Achieved Target and Variance for the Sales Partner + result = execute( + frappe._dict( + { + "fiscal_year": self.fiscal_year, + "doctype": "Sales Invoice", + "period": "Yearly", + "target_on": "Quantity", + } + ) + )[1] + row = frappe._dict(result[0]) + self.assertSequenceEqual( + [flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)], + [50, 20, -30], + ) diff --git a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.js b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.js index e404233953e..6bd8077ddd6 100644 --- a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.js +++ b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.js @@ -3,19 +3,19 @@ /* eslint-disable */ frappe.query_reports["Sales Partner Transaction Summary"] = { - "filters": [ + filters: [ { fieldname: "sales_partner", label: __("Sales Partner"), fieldtype: "Link", - options: "Sales Partner" + options: "Sales Partner", }, { fieldname: "doctype", label: __("Document Type"), fieldtype: "Select", options: "Sales Order\nDelivery Note\nSales Invoice", - default: "Sales Order" + default: "Sales Order", }, { fieldname: "from_date", @@ -24,48 +24,48 @@ frappe.query_reports["Sales Partner Transaction Summary"] = { default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.datetime.get_today() + default: frappe.datetime.get_today(), }, { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { - fieldname:"item_group", + fieldname: "item_group", label: __("Item Group"), fieldtype: "Link", options: "Item Group", }, { - fieldname:"brand", + fieldname: "brand", label: __("Brand"), fieldtype: "Link", options: "Brand", }, { - fieldname:"customer", + fieldname: "customer", label: __("Customer"), fieldtype: "Link", options: "Customer", }, { - fieldname:"territory", + fieldname: "territory", label: __("Territory"), fieldtype: "Link", options: "Territory", }, { - fieldname:"show_return_entries", + fieldname: "show_return_entries", label: __("Show Return Entries"), fieldtype: "Check", default: 0, }, - ] -} + ], +}; diff --git a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py index 2049520eadc..216adde18fd 100644 --- a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py +++ b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py @@ -110,9 +110,7 @@ def get_entries(filters): {cond} and dt.name = dt_item.parent and dt.docstatus = 1 and dt.sales_partner is not null and dt.sales_partner != '' order by dt.name desc, dt.sales_partner - """.format( - date_field=date_field, doctype=filters.get("doctype"), cond=conditions - ), + """.format(date_field=date_field, doctype=filters.get("doctype"), cond=conditions), filters, as_dict=1, ) @@ -125,13 +123,13 @@ def get_conditions(filters, date_field): for field in ["company", "customer", "territory", "sales_partner"]: if filters.get(field): - conditions += " and dt.{0} = %({1})s".format(field, field) + conditions += f" and dt.{field} = %({field})s" if filters.get("from_date"): - conditions += " and dt.{0} >= %(from_date)s".format(date_field) + conditions += f" and dt.{date_field} >= %(from_date)s" if filters.get("to_date"): - conditions += " and dt.{0} <= %(to_date)s".format(date_field) + conditions += f" and dt.{date_field} <= %(to_date)s" if not filters.get("show_return_entries"): conditions += " and dt_item.qty > 0.0" @@ -142,10 +140,7 @@ def get_conditions(filters, date_field): if filters.get("item_group"): lft, rgt = frappe.get_cached_value("Item Group", filters.get("item_group"), ["lft", "rgt"]) - conditions += """ and dt_item.item_group in (select name from - `tabItem Group` where lft >= %s and rgt <= %s)""" % ( - lft, - rgt, - ) + conditions += f""" and dt_item.item_group in (select name from + `tabItem Group` where lft >= {lft} and rgt <= {rgt})""" return conditions diff --git a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.js b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.js index 2044ff8c289..7944453a1b0 100644 --- a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.js +++ b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.js @@ -3,20 +3,19 @@ /* eslint-disable */ frappe.query_reports["Sales Person Commission Summary"] = { - "filters": [ - + filters: [ { fieldname: "sales_person", label: __("Sales Person"), fieldtype: "Link", - options: "Sales Person" + options: "Sales Person", }, { fieldname: "doc_type", label: __("Document Type"), fieldtype: "Select", options: "Sales Order\nDelivery Note\nSales Invoice", - default: "Sales Order" + default: "Sales Order", }, { fieldname: "from_date", @@ -25,30 +24,29 @@ frappe.query_reports["Sales Person Commission Summary"] = { default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.datetime.get_today() + default: frappe.datetime.get_today(), }, { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", - default: frappe.defaults.get_user_default("Company") + default: frappe.defaults.get_user_default("Company"), }, { - fieldname:"customer", + fieldname: "customer", label: __("Customer"), fieldtype: "Link", options: "Customer", }, { - fieldname:"territory", + fieldname: "territory", label: __("Territory"), fieldtype: "Link", options: "Territory", }, - - ] -} + ], +}; diff --git a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py index a8df5308036..16b2d499af2 100644 --- a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py +++ b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py @@ -103,16 +103,15 @@ def get_entries(filters): entries = frappe.db.sql( """ select - dt.name, dt.customer, dt.territory, dt.%s as posting_date,dt.base_net_total as base_net_amount, + dt.name, dt.customer, dt.territory, dt.{} as posting_date,dt.base_net_total as base_net_amount, st.commission_rate,st.sales_person, st.allocated_percentage, st.allocated_amount, st.incentives from - `tab%s` dt, `tabSales Team` st + `tab{}` dt, `tabSales Team` st where - st.parent = dt.name and st.parenttype = %s - and dt.docstatus = 1 %s order by dt.name desc,st.sales_person - """ - % (date_field, filters["doc_type"], "%s", conditions), - tuple([filters["doc_type"]] + values), + st.parent = dt.name and st.parenttype = {} + and dt.docstatus = 1 {} order by dt.name desc,st.sales_person + """.format(date_field, filters["doc_type"], "%s", conditions), + tuple([filters["doc_type"], *values]), as_dict=1, ) @@ -125,18 +124,18 @@ def get_conditions(filters, date_field): for field in ["company", "customer", "territory"]: if filters.get(field): - conditions.append("dt.{0}=%s".format(field)) + conditions.append(f"dt.{field}=%s") values.append(filters[field]) if filters.get("sales_person"): - conditions.append("st.sales_person = '{0}'".format(filters.get("sales_person"))) + conditions.append("st.sales_person = '{}'".format(filters.get("sales_person"))) if filters.get("from_date"): - conditions.append("dt.{0}>=%s".format(date_field)) + conditions.append(f"dt.{date_field}>=%s") values.append(filters["from_date"]) if filters.get("to_date"): - conditions.append("dt.{0}<=%s".format(date_field)) + conditions.append(f"dt.{date_field}<=%s") values.append(filters["to_date"]) return " and ".join(conditions), values diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js index 2b8443627d5..b3497085fb7 100644 --- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js @@ -3,61 +3,59 @@ /* eslint-disable */ frappe.query_reports["Sales Person Target Variance Based On Item Group"] = { - "filters": [ + filters: [ { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", - default: frappe.defaults.get_user_default("Company") + default: frappe.defaults.get_user_default("Company"), }, { fieldname: "fiscal_year", label: __("Fiscal Year"), fieldtype: "Link", options: "Fiscal Year", - default: frappe.sys_defaults.fiscal_year + default: frappe.sys_defaults.fiscal_year, }, { fieldname: "doctype", label: __("Document Type"), fieldtype: "Select", options: "Sales Order\nDelivery Note\nSales Invoice", - default: "Sales Order" + default: "Sales Order", }, { fieldname: "period", label: __("Period"), fieldtype: "Select", options: [ - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Half-Yearly", "label": __("Half-Yearly") }, - { "value": "Yearly", "label": __("Yearly") } + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, ], - default: "Monthly" + default: "Monthly", }, { fieldname: "target_on", label: __("Target On"), fieldtype: "Select", options: "Quantity\nAmount", - default: "Quantity" + default: "Quantity", }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.fieldname.includes('variance')) { - + if (column.fieldname.includes("variance")) { if (data[column.fieldname] < 0) { value = "" + value + ""; - } - else if (data[column.fieldname] > 0) { + } else if (data[column.fieldname] > 0) { value = "" + value + ""; } } return value; - } -} + }, +}; diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py new file mode 100644 index 00000000000..73ae6d0c852 --- /dev/null +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py @@ -0,0 +1,96 @@ +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import flt, nowdate + +from erpnext.accounts.utils import get_fiscal_year +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.sales_person_target_variance_based_on_item_group.sales_person_target_variance_based_on_item_group import ( + execute, +) + + +class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase): + def setUp(self): + self.fiscal_year = get_fiscal_year(nowdate())[0] + + def tearDown(self): + frappe.db.rollback() + + def test_achieved_target_and_variance(self): + # Create a Target Distribution + distribution = create_target_distribution(self.fiscal_year) + + # Create sales people with targets for the current fiscal year + person_1 = create_sales_target_doc( + "Sales Person", "sales_person_name", "Sales Person 1", self.fiscal_year, distribution.name + ) + person_2 = create_sales_target_doc( + "Sales Person", "sales_person_name", "Sales Person 2", self.fiscal_year, distribution.name + ) + + # Create a Sales Order with 50-50 contribution between both Sales people + so = make_sales_order( + rate=1000, + qty=20, + do_not_submit=True, + ) + so.set( + "sales_team", + [ + { + "sales_person": person_1.name, + "allocated_percentage": 50, + "allocated_amount": 10000, + }, + { + "sales_person": person_2.name, + "allocated_percentage": 50, + "allocated_amount": 10000, + }, + ], + ) + so.submit() + + # Check Achieved Target and Variance + result = execute( + frappe._dict( + { + "fiscal_year": self.fiscal_year, + "doctype": "Sales Order", + "period": "Yearly", + "target_on": "Quantity", + } + ) + )[1] + row = frappe._dict(result[0]) + self.assertSequenceEqual( + [flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)], + [50, 10, -40], + ) + + +def create_target_distribution(fiscal_year): + distribution = frappe.new_doc("Monthly Distribution") + distribution.distribution_id = "Target Report Distribution" + distribution.fiscal_year = fiscal_year + distribution.get_months() + return distribution.insert() + + +def create_sales_target_doc( + sales_field_dt, sales_field_name, sales_field_value, fiscal_year, distribution_id +): + sales_target_doc = frappe.new_doc(sales_field_dt) + sales_target_doc.set(sales_field_name, sales_field_value) + sales_target_doc.append( + "targets", + { + "fiscal_year": fiscal_year, + "target_qty": 50, + "target_amount": 30000, + "distribution_id": distribution_id, + }, + ) + if sales_field_dt == "Sales Partner": + sales_target_doc.commission_rate = 5 + return sales_target_doc.insert() diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js index 80c7b5f59d7..828e9e74f23 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js @@ -2,19 +2,19 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Sales Person-wise Transaction Summary"] = { - "filters": [ + filters: [ { fieldname: "sales_person", label: __("Sales Person"), fieldtype: "Link", - options: "Sales Person" + options: "Sales Person", }, { fieldname: "doc_type", label: __("Document Type"), fieldtype: "Select", options: "Sales Order\nDelivery Note\nSales Invoice", - default: "Sales Order" + default: "Sales Order", }, { fieldname: "from_date", @@ -23,48 +23,48 @@ frappe.query_reports["Sales Person-wise Transaction Summary"] = { default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.datetime.get_today() + default: frappe.datetime.get_today(), }, { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { - fieldname:"item_group", + fieldname: "item_group", label: __("Item Group"), fieldtype: "Link", options: "Item Group", }, { - fieldname:"brand", + fieldname: "brand", label: __("Brand"), fieldtype: "Link", options: "Brand", }, { - fieldname:"customer", + fieldname: "customer", label: __("Customer"), fieldtype: "Link", options: "Customer", }, { - fieldname:"territory", + fieldname: "territory", label: __("Territory"), fieldtype: "Link", options: "Territory", }, { - fieldname:"show_return_entries", + fieldname: "show_return_entries", label: __("Show Return Entries"), fieldtype: "Check", default: 0, }, - ] -} + ], +}; diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index 9f3ba0da8bd..f8cde141fe4 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -36,6 +36,7 @@ def execute(filters=None): d.base_net_amount, d.sales_person, d.allocated_percentage, + (d.stock_qty * d.allocated_percentage / 100), d.contribution_amt, company_currency, ] @@ -103,7 +104,7 @@ def get_columns(filters): "fieldtype": "Link", "width": 140, }, - {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140}, + {"label": _("SO Total Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140}, { "label": _("Amount"), "options": "currency", @@ -119,6 +120,12 @@ def get_columns(filters): "width": 140, }, {"label": _("Contribution %"), "fieldname": "contribution", "fieldtype": "Float", "width": 140}, + { + "label": _("Contribution Qty"), + "fieldname": "contribution_qty", + "fieldtype": "Float", + "width": 140, + }, { "label": _("Contribution Amount"), "options": "currency", @@ -149,27 +156,26 @@ def get_entries(filters): entries = frappe.db.sql( """ SELECT - dt.name, dt.customer, dt.territory, dt.%s as posting_date, dt_item.item_code, + dt.name, dt.customer, dt.territory, dt.{} as posting_date, dt_item.item_code, st.sales_person, st.allocated_percentage, dt_item.warehouse, CASE - WHEN dt.status = "Closed" THEN dt_item.%s * dt_item.conversion_factor + WHEN dt.status = "Closed" THEN dt_item.{} * dt_item.conversion_factor ELSE dt_item.stock_qty END as stock_qty, CASE - WHEN dt.status = "Closed" THEN (dt_item.base_net_rate * dt_item.%s * dt_item.conversion_factor) + WHEN dt.status = "Closed" THEN (dt_item.base_net_rate * dt_item.{} * dt_item.conversion_factor) ELSE dt_item.base_net_amount END as base_net_amount, CASE - WHEN dt.status = "Closed" THEN ((dt_item.base_net_rate * dt_item.%s * dt_item.conversion_factor) * st.allocated_percentage/100) + WHEN dt.status = "Closed" THEN ((dt_item.base_net_rate * dt_item.{} * dt_item.conversion_factor) * st.allocated_percentage/100) ELSE dt_item.base_net_amount * st.allocated_percentage/100 END as contribution_amt FROM - `tab%s` dt, `tab%s Item` dt_item, `tabSales Team` st + `tab{}` dt, `tab{} Item` dt_item, `tabSales Team` st WHERE - st.parent = dt.name and dt.name = dt_item.parent and st.parenttype = %s - and dt.docstatus = 1 %s order by st.sales_person, dt.name desc - """ - % ( + st.parent = dt.name and dt.name = dt_item.parent and st.parenttype = {} + and dt.docstatus = 1 {} order by st.sales_person, dt.name desc + """.format( date_field, qty_field, qty_field, @@ -179,7 +185,7 @@ def get_entries(filters): "%s", conditions, ), - tuple([filters["doc_type"]] + values), + tuple([filters["doc_type"], *values]), as_dict=1, ) @@ -192,23 +198,21 @@ def get_conditions(filters, date_field): for field in ["company", "customer", "territory"]: if filters.get(field): - conditions.append("dt.{0}=%s".format(field)) + conditions.append(f"dt.{field}=%s") values.append(filters[field]) if filters.get("sales_person"): lft, rgt = frappe.get_value("Sales Person", filters.get("sales_person"), ["lft", "rgt"]) conditions.append( - "exists(select name from `tabSales Person` where lft >= {0} and rgt <= {1} and name=st.sales_person)".format( - lft, rgt - ) + f"exists(select name from `tabSales Person` where lft >= {lft} and rgt <= {rgt} and name=st.sales_person)" ) if filters.get("from_date"): - conditions.append("dt.{0}>=%s".format(date_field)) + conditions.append(f"dt.{date_field}>=%s") values.append(filters["from_date"]) if filters.get("to_date"): - conditions.append("dt.{0}<=%s".format(date_field)) + conditions.append(f"dt.{date_field}<=%s") values.append(filters["to_date"]) items = get_items(filters) diff --git a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js index 9f3d255e662..988fb86ff6c 100644 --- a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js @@ -3,61 +3,59 @@ /* eslint-disable */ frappe.query_reports["Territory Target Variance Based On Item Group"] = { - "filters": [ + filters: [ { - fieldname:"company", + fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", - default: frappe.defaults.get_user_default("Company") + default: frappe.defaults.get_user_default("Company"), }, { fieldname: "fiscal_year", label: __("Fiscal Year"), fieldtype: "Link", options: "Fiscal Year", - default: frappe.sys_defaults.fiscal_year + default: frappe.sys_defaults.fiscal_year, }, { fieldname: "doctype", label: __("Document Type"), fieldtype: "Select", options: "Sales Order\nDelivery Note\nSales Invoice", - default: "Sales Order" + default: "Sales Order", }, { fieldname: "period", label: __("Period"), fieldtype: "Select", options: [ - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Half-Yearly", "label": __("Half-Yearly") }, - { "value": "Yearly", "label": __("Yearly") } + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, ], - default: "Monthly" + default: "Monthly", }, { fieldname: "target_on", label: __("Target On"), fieldtype: "Select", options: "Quantity\nAmount", - default: "Quantity" + default: "Quantity", }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.fieldname.includes('variance')) { - + if (column.fieldname.includes("variance")) { if (data[column.fieldname] < 0) { value = "" + value + ""; - } - else if (data[column.fieldname] > 0) { + } else if (data[column.fieldname] > 0) { value = "" + value + ""; } } return value; - } -} + }, +}; diff --git a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.py b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.py index eee2d42a786..527e60e6de3 100644 --- a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.py +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.py @@ -8,6 +8,4 @@ from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.it def execute(filters=None): - data = [] - return get_data_column(filters, "Territory") diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js index bef800f1040..c13c7408428 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js @@ -2,21 +2,23 @@ // For license information, please see license.txt /* eslint-disable */ - frappe.query_reports["Territory-wise Sales"] = { - "breadcrumb":"Selling", - "filters": [ + breadcrumb: "Selling", + filters: [ { - fieldname:"transaction_date", + fieldname: "transaction_date", label: __("Transaction Date"), fieldtype: "DateRange", - default: [frappe.datetime.add_months(frappe.datetime.get_today(),-1), frappe.datetime.get_today()], + default: [ + frappe.datetime.add_months(frappe.datetime.get_today(), -1), + frappe.datetime.get_today(), + ], }, { fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", - } - ] + }, + ], }; diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index ecb63d890a7..e31b259d731 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -107,7 +107,7 @@ def get_opportunities(filters): conditions = "" if filters.get("transaction_date"): - conditions = " WHERE transaction_date between {0} and {1}".format( + conditions = " WHERE transaction_date between {} and {}".format( frappe.db.escape(filters["transaction_date"][0]), frappe.db.escape(filters["transaction_date"][1]), ) @@ -120,12 +120,10 @@ def get_opportunities(filters): conditions += " company = %(company)s" return frappe.db.sql( - """ + f""" SELECT name, territory, opportunity_amount - FROM `tabOpportunity` {0} - """.format( - conditions - ), + FROM `tabOpportunity` {conditions} + """, filters, as_dict=1, ) # nosec @@ -141,10 +139,8 @@ def get_quotations(opportunities): """ SELECT `name`,`base_grand_total`, `opportunity` FROM `tabQuotation` - WHERE docstatus=1 AND opportunity in ({0}) - """.format( - ", ".join(["%s"] * len(opportunity_names)) - ), + WHERE docstatus=1 AND opportunity in ({}) + """.format(", ".join(["%s"] * len(opportunity_names))), tuple(opportunity_names), as_dict=1, ) # nosec @@ -160,10 +156,8 @@ def get_sales_orders(quotations): """ SELECT so.`name`, so.`base_grand_total`, soi.prevdoc_docname as quotation FROM `tabSales Order` so, `tabSales Order Item` soi - WHERE so.docstatus=1 AND so.name = soi.parent AND soi.prevdoc_docname in ({0}) - """.format( - ", ".join(["%s"] * len(quotation_names)) - ), + WHERE so.docstatus=1 AND so.name = soi.parent AND soi.prevdoc_docname in ({}) + """.format(", ".join(["%s"] * len(quotation_names))), tuple(quotation_names), as_dict=1, ) # nosec @@ -179,10 +173,8 @@ def get_sales_invoice(sales_orders): """ SELECT si.name, si.base_grand_total, sii.sales_order FROM `tabSales Invoice` si, `tabSales Invoice Item` sii - WHERE si.docstatus=1 AND si.name = sii.parent AND sii.sales_order in ({0}) - """.format( - ", ".join(["%s"] * len(so_names)) - ), + WHERE si.docstatus=1 AND si.name = sii.parent AND sii.sales_order in ({}) + """.format(", ".join(["%s"] * len(so_names))), tuple(so_names), as_dict=1, ) # nosec diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 80a6b7712fc..b2a64a5d461 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -200,6 +200,10 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran item.serial_no = null; } + if (doc.docstatus === 0 && doc.is_return && !doc.return_against) { + item.incoming_rate = 0.0; + } + var has_batch_no; frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => { has_batch_no = r && r.has_batch_no; @@ -428,6 +432,11 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran }) } } + + coupon_code() { + this.frm.set_value("discount_amount", 0); + this.frm.set_value("additional_discount_percentage", 0); + } }; frappe.ui.form.on(cur_frm.doctype,"project", function(frm) { diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py new file mode 100644 index 00000000000..f0253529c78 --- /dev/null +++ b/erpnext/setup/demo.py @@ -0,0 +1,227 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import json +import os +from random import randint + +import frappe +from frappe import _ +from frappe.utils import add_days, getdate + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.utils import get_fiscal_year +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice +from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice +from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account + + +def setup_demo_data(): + from frappe.utils.telemetry import capture + + capture("demo_data_creation_started", "erpnext") + try: + company = create_demo_company() + process_masters() + make_transactions(company) + frappe.cache.delete_keys("bootinfo") + frappe.publish_realtime("demo_data_complete") + except Exception: + frappe.log_error("Failed to create demo data") + capture("demo_data_creation_failed", "erpnext", properties={"exception": frappe.get_traceback()}) + raise + capture("demo_data_creation_completed", "erpnext") + + +@frappe.whitelist() +def clear_demo_data(): + from frappe.utils.telemetry import capture + + frappe.only_for("System Manager") + + capture("demo_data_erased", "erpnext") + try: + company = frappe.db.get_single_value("Global Defaults", "demo_company") + create_transaction_deletion_record(company) + clear_masters() + delete_company(company) + default_company = frappe.db.get_single_value("Global Defaults", "default_company") + frappe.db.set_default("company", default_company) + except Exception: + frappe.db.rollback() + frappe.log_error("Failed to erase demo data") + frappe.throw( + _("Failed to erase demo data, please delete the demo company manually."), + title=_("Could Not Delete Demo Data"), + ) + + +def create_demo_company(): + company = frappe.db.get_all("Company")[0].name + company_doc = frappe.get_doc("Company", company) + + # Make a dummy company + new_company = frappe.new_doc("Company") + new_company.company_name = company_doc.company_name + " (Demo)" + new_company.abbr = company_doc.abbr + "D" + new_company.enable_perpetual_inventory = 1 + new_company.default_currency = company_doc.default_currency + new_company.country = company_doc.country + new_company.chart_of_accounts_based_on = "Standard Template" + new_company.chart_of_accounts = company_doc.chart_of_accounts + new_company.insert() + + # Set Demo Company as default to + frappe.db.set_single_value("Global Defaults", "demo_company", new_company.name) + frappe.db.set_default("company", new_company.name) + + bank_account = create_bank_account({"company_name": new_company.name}) + frappe.db.set_value("Company", new_company.name, "default_bank_account", bank_account.name) + + return new_company.name + + +def process_masters(): + for doctype in frappe.get_hooks("demo_master_doctypes"): + data = read_data_file_using_hooks(doctype) + if data: + for item in json.loads(data): + create_demo_record(item) + + +def create_demo_record(doctype): + frappe.get_doc(doctype).insert(ignore_permissions=True) + + +def make_transactions(company): + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) + from erpnext.accounts.utils import FiscalYearError + + try: + start_date = get_fiscal_year(date=getdate())[1] + except FiscalYearError: + # User might have setup fiscal year for previous or upcoming years + active_fiscal_years = frappe.db.get_all("Fiscal Year", filters={"disabled": 0}, as_list=1) + if active_fiscal_years: + start_date = frappe.db.get_value("Fiscal Year", active_fiscal_years[0][0], "year_start_date") + else: + frappe.throw(_("There are no active Fiscal Years for which Demo Data can be generated.")) + + for doctype in frappe.get_hooks("demo_transaction_doctypes"): + data = read_data_file_using_hooks(doctype) + if data: + for item in json.loads(data): + create_transaction(item, company, start_date) + + convert_order_to_invoices() + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0) + + +def create_transaction(doctype, company, start_date): + document_type = doctype.get("doctype") + warehouse = get_warehouse(company) + + if document_type == "Purchase Order": + posting_date = get_random_date(start_date, 1, 25) + else: + posting_date = get_random_date(start_date, 31, 350) + + doctype.update( + { + "company": company, + "set_posting_time": 1, + "transaction_date": posting_date, + "schedule_date": posting_date, + "delivery_date": posting_date, + "set_warehouse": warehouse, + } + ) + + doc = frappe.get_doc(doctype) + doc.save(ignore_permissions=True) + doc.submit() + + +def convert_order_to_invoices(): + for document in ["Purchase Order", "Sales Order"]: + # Keep some orders intentionally unbilled/unpaid + for i, order in enumerate( + frappe.db.get_all( + document, filters={"docstatus": 1}, fields=["name", "transaction_date"], limit=6 + ) + ): + if document == "Purchase Order": + invoice = make_purchase_invoice(order.name) + elif document == "Sales Order": + invoice = make_sales_invoice(order.name) + + invoice.set_posting_time = 1 + invoice.posting_date = order.transaction_date + invoice.due_date = order.transaction_date + invoice.bill_date = order.transaction_date + + if invoice.get("payment_schedule"): + invoice.payment_schedule[0].due_date = order.transaction_date + + invoice.update_stock = 1 + invoice.submit() + + if i % 2 != 0: + payment = get_payment_entry(invoice.doctype, invoice.name) + payment.posting_date = order.transaction_date + payment.reference_no = invoice.name + payment.submit() + + +def get_random_date(start_date, start_range, end_range): + return add_days(start_date, randint(start_range, end_range)) + + +def create_transaction_deletion_record(company): + transaction_deletion_record = frappe.new_doc("Transaction Deletion Record") + transaction_deletion_record.company = company + transaction_deletion_record.process_in_single_transaction = True + transaction_deletion_record.save(ignore_permissions=True) + transaction_deletion_record.submit() + transaction_deletion_record.start_deletion_tasks() + + +def clear_masters(): + for doctype in frappe.get_hooks("demo_master_doctypes")[::-1]: + data = read_data_file_using_hooks(doctype) + if data: + for item in json.loads(data): + clear_demo_record(item) + + +def clear_demo_record(document): + document_type = document.get("doctype") + del document["doctype"] + + valid_columns = frappe.get_meta(document_type).get_valid_columns() + + filters = document + for key in list(filters): + if key not in valid_columns: + filters.pop(key, None) + + doc = frappe.get_doc(document_type, filters) + doc.delete(ignore_permissions=True) + + +def delete_company(company): + frappe.db.set_single_value("Global Defaults", "demo_company", "") + frappe.delete_doc("Company", company, ignore_permissions=True) + + +def read_data_file_using_hooks(doctype): + path = os.path.join(os.path.dirname(__file__), "demo_data") + with open(os.path.join(path, doctype + ".json")) as f: + data = f.read() + + return data + + +def get_warehouse(company): + warehouses = frappe.db.get_all("Warehouse", {"company": company, "is_group": 0}) + return warehouses[randint(0, 3)].name diff --git a/erpnext/setup/demo_data/item.json b/erpnext/setup/demo_data/item.json new file mode 100644 index 00000000000..17024341225 --- /dev/null +++ b/erpnext/setup/demo_data/item.json @@ -0,0 +1,92 @@ +[ + { + "doctype": "Item", + "item_group": "Demo Item Group", + "item_code": "SKU001", + "item_name": "T-shirt", + "valuation_rate": 400.0, + "gst_hsn_code": "999512", + "image": "https://images.pexels.com/photos/1484808/pexels-photo-1484808.jpeg" + }, + { + "doctype": "Item", + "item_group": "Demo Item Group", + "item_code": "SKU002", + "valuation_rate": 300.0, + "item_name": "Laptop", + "gst_hsn_code": "999512", + "image": "https://images.pexels.com/photos/3999538/pexels-photo-3999538.jpeg" + }, + { + "doctype": "Item", + "item_group": "Demo Item Group", + "item_code": "SKU003", + "valuation_rate": 523.0, + "item_name": "Book", + "gst_hsn_code": "999512", + "image": "https://images.pexels.com/photos/2422178/pexels-photo-2422178.jpeg" + }, + { + "doctype": "Item", + "item_group": "Demo Item Group", + "item_code": "SKU004", + "valuation_rate": 725.0, + "item_name": "Smartphone", + "gst_hsn_code": "999512", + "image": "https://images.pexels.com/photos/1647976/pexels-photo-1647976.jpeg" + }, + { + "doctype": "Item", + "item_group": "Demo Item Group", + "item_code": "SKU005", + "valuation_rate": 222.0, + "item_name": "Sneakers", + "gst_hsn_code": "999512", + "image": "https://images.pexels.com/photos/1598505/pexels-photo-1598505.jpeg" + }, + { + "doctype": "Item", + "item_group": "Demo Item Group", + "item_code": "SKU006", + "valuation_rate": 420.0, + "item_name": "Coffee Mug", + "gst_hsn_code": "999512", + "image": "https://images.pexels.com/photos/585753/pexels-photo-585753.jpeg" + }, + { + "doctype": "Item", + "item_group": "Demo Item Group", + "item_code": "SKU007", + "valuation_rate": 375.0, + "item_name": "Television", + "gst_hsn_code": "999512", + "image": "https://images.pexels.com/photos/8059376/pexels-photo-8059376.jpeg" + }, + { + "doctype": "Item", + "item_group": "Demo Item Group", + "item_code": "SKU008", + "valuation_rate": 333.0, + "item_name": "Backpack", + "gst_hsn_code": "999512", + "image": "https://images.pexels.com/photos/3731256/pexels-photo-3731256.jpeg" + }, + { + "doctype": "Item", + "item_group": "Demo Item Group", + "item_code": "SKU009", + "valuation_rate": 700.0, + "item_name": "Headphones", + "gst_hsn_code": "999512", + "image": "https://images.pexels.com/photos/3587478/pexels-photo-3587478.jpeg" + }, + { + "doctype": "Item", + "item_group": "Demo Item Group", + "item_code": "SKU010", + "valuation_rate": 500.0, + "item_name": "Camera", + "gst_hsn_code": "999512", + "image": "https://images.pexels.com/photos/51383/photo-camera-subject-photographer-51383.jpeg" + } +] diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py index 5e77c6fa816..f01c2e2feb8 100644 --- a/erpnext/setup/doctype/authorization_control/authorization_control.py +++ b/erpnext/setup/doctype/authorization_control/authorization_control.py @@ -12,7 +12,7 @@ from erpnext.utilities.transaction_base import TransactionBase class AuthorizationControl(TransactionBase): def get_appr_user_role(self, det, doctype_name, total, based_on, condition, item, company): amt_list, appr_users, appr_roles = [], [], [] - users, roles = "", "" + _users, _roles = "", "" if det: for x in det: amt_list.append(flt(x[0])) @@ -20,18 +20,20 @@ class AuthorizationControl(TransactionBase): app_dtl = frappe.db.sql( """select approving_user, approving_role from `tabAuthorization Rule` - where transaction = %s and (value = %s or value > %s) - and docstatus != 2 and based_on = %s and company = %s %s""" - % ("%s", "%s", "%s", "%s", "%s", condition), + where transaction = {} and (value = {} or value > {}) + and docstatus != 2 and based_on = {} and company = {} {}""".format( + "%s", "%s", "%s", "%s", "%s", condition + ), (doctype_name, flt(max_amount), total, based_on, company), ) if not app_dtl: app_dtl = frappe.db.sql( """select approving_user, approving_role from `tabAuthorization Rule` - where transaction = %s and (value = %s or value > %s) and docstatus != 2 - and based_on = %s and ifnull(company,'') = '' %s""" - % ("%s", "%s", "%s", "%s", condition), + where transaction = {} and (value = {} or value > {}) and docstatus != 2 + and based_on = {} and ifnull(company,'') = '' {}""".format( + "%s", "%s", "%s", "%s", condition + ), (doctype_name, flt(max_amount), total, based_on), ) @@ -54,18 +56,20 @@ class AuthorizationControl(TransactionBase): add_cond1 += " and master_name = " + frappe.db.escape(cstr(item)) itemwise_exists = frappe.db.sql( """select value from `tabAuthorization Rule` - where transaction = %s and value <= %s - and based_on = %s and company = %s and docstatus != 2 %s %s""" - % ("%s", "%s", "%s", "%s", cond, add_cond1), + where transaction = {} and value <= {} + and based_on = {} and company = {} and docstatus != 2 {} {}""".format( + "%s", "%s", "%s", "%s", cond, add_cond1 + ), (doctype_name, total, based_on, company), ) if not itemwise_exists: itemwise_exists = frappe.db.sql( """select value from `tabAuthorization Rule` - where transaction = %s and value <= %s and based_on = %s - and ifnull(company,'') = '' and docstatus != 2 %s %s""" - % ("%s", "%s", "%s", cond, add_cond1), + where transaction = {} and value <= {} and based_on = {} + and ifnull(company,'') = '' and docstatus != 2 {} {}""".format( + "%s", "%s", "%s", cond, add_cond1 + ), (doctype_name, total, based_on), ) @@ -80,18 +84,18 @@ class AuthorizationControl(TransactionBase): appr = frappe.db.sql( """select value from `tabAuthorization Rule` - where transaction = %s and value <= %s and based_on = %s - and company = %s and docstatus != 2 %s %s""" - % ("%s", "%s", "%s", "%s", cond, add_cond2), + where transaction = {} and value <= {} and based_on = {} + and company = {} and docstatus != 2 {} {}""".format("%s", "%s", "%s", "%s", cond, add_cond2), (doctype_name, total, based_on, company), ) if not appr: appr = frappe.db.sql( """select value from `tabAuthorization Rule` - where transaction = %s and value <= %s and based_on = %s - and ifnull(company,'') = '' and docstatus != 2 %s %s""" - % ("%s", "%s", "%s", cond, add_cond2), + where transaction = {} and value <= {} and based_on = {} + and ifnull(company,'') = '' and docstatus != 2 {} {}""".format( + "%s", "%s", "%s", cond, add_cond2 + ), (doctype_name, total, based_on), ) @@ -116,7 +120,7 @@ class AuthorizationControl(TransactionBase): customer = doc_obj.customer else: customer = doc_obj.customer_name - add_cond = " and master_name = {}".format(frappe.db.escape(customer)) + add_cond = f" and master_name = {frappe.db.escape(customer)}" if based_on == "Itemwise Discount": if doc_obj: for t in doc_obj.get("items"): @@ -175,11 +179,10 @@ class AuthorizationControl(TransactionBase): for x in frappe.db.sql( """select based_on from `tabAuthorization Rule` - where transaction = %s and system_role IN (%s) and based_on IN (%s) - and (company = %s or ifnull(company,'')='') + where transaction = {} and system_role IN ({}) and based_on IN ({}) + and (company = {} or ifnull(company,'')='') and docstatus != 2 - """ - % ( + """.format( "%s", "'" + "','".join(frappe.get_roles()) + "'", "'" + "','".join(final_based_on) + "'", diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.js b/erpnext/setup/doctype/authorization_rule/authorization_rule.js index 3f6afcae7f5..a98d6965d8f 100644 --- a/erpnext/setup/doctype/authorization_rule/authorization_rule.js +++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.js @@ -2,14 +2,14 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Authorization Rule", { - refresh: function(frm) { + refresh: function (frm) { frm.events.set_master_type(frm); }, - set_master_type: function(frm) { - if(frm.doc.based_on==="Customerwise Discount") { + set_master_type: function (frm) { + if (frm.doc.based_on === "Customerwise Discount") { unhide_field("master_name"); frm.set_value("customer_or_item", "Customer"); - } else if(frm.doc.based_on==="Itemwise Discount") { + } else if (frm.doc.based_on === "Itemwise Discount") { unhide_field("master_name"); frm.set_value("customer_or_item", "Item"); } else { @@ -18,77 +18,67 @@ frappe.ui.form.on("Authorization Rule", { hide_field("master_name"); } }, - based_on: function(frm) { + based_on: function (frm) { frm.events.set_master_type(frm); - if (frm.doc.based_on === 'Not Applicable') { + if (frm.doc.based_on === "Not Applicable") { frm.set_value("value", 0); - hide_field('value'); + hide_field("value"); } else { - unhide_field('value'); + unhide_field("value"); } }, - transaction: function(frm) { - unhide_field(['system_role', 'system_user', 'value', 'based_on']); - hide_field(['to_emp', 'to_designation']); - } -}) - + transaction: function (frm) { + unhide_field(["system_role", "system_user", "value", "based_on"]); + hide_field(["to_emp", "to_designation"]); + }, +}); // Settings Module -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - if (doc.based_on == 'Not Applicable') - hide_field('value'); - else - unhide_field('value'); +cur_frm.cscript.refresh = function (doc, cdt, cdn) { + if (doc.based_on == "Not Applicable") hide_field("value"); + else unhide_field("value"); - unhide_field(['system_role', 'system_user', 'value']); - hide_field(['to_emp', 'to_designation']); -} + unhide_field(["system_role", "system_user", "value"]); + hide_field(["to_emp", "to_designation"]); +}; -cur_frm.fields_dict.system_user.get_query = function(doc, cdt, cdn) { - return { query:"frappe.core.doctype.user.user.user_query" } -} +cur_frm.fields_dict.system_user.get_query = function (doc, cdt, cdn) { + return { query: "frappe.core.doctype.user.user.user_query" }; +}; -cur_frm.fields_dict.approving_user.get_query = function(doc, cdt, cdn) { - return { query:"frappe.core.doctype.user.user.user_query" } -} +cur_frm.fields_dict.approving_user.get_query = function (doc, cdt, cdn) { + return { query: "frappe.core.doctype.user.user.user_query" }; +}; -cur_frm.fields_dict['approving_role'].get_query = cur_frm.fields_dict['system_role'].get_query; +cur_frm.fields_dict["approving_role"].get_query = cur_frm.fields_dict["system_role"].get_query; // System Role Trigger // ----------------------- -cur_frm.fields_dict['system_role'].get_query = function(doc) { +cur_frm.fields_dict["system_role"].get_query = function (doc) { return { - filters:[ - ['Role', 'name', 'not in', 'Administrator, Guest, All'] - ] - } -} - + filters: [["Role", "name", "not in", "Administrator, Guest, All"]], + }; +}; // Master Name Trigger // -------------------- -cur_frm.fields_dict['master_name'].get_query = function(doc) { - if (doc.based_on == 'Customerwise Discount') +cur_frm.fields_dict["master_name"].get_query = function (doc) { + if (doc.based_on == "Customerwise Discount") return { doctype: "Customer", - filters:[ - ['Customer', 'docstatus', '!=', 2] - ] - } - else if (doc.based_on == 'Itemwise Discount') + filters: [["Customer", "docstatus", "!=", 2]], + }; + else if (doc.based_on == "Itemwise Discount") return { doctype: "Item", - query: "erpnext.controllers.queries.item_query" - } + query: "erpnext.controllers.queries.item_query", + }; else return { - filters: [ - ['Item', 'name', '=', 'cheating done to avoid null'] - ] - } -} + filters: [["Item", "name", "=", "cheating done to avoid null"]], + }; +}; -cur_frm.fields_dict.to_emp.get_query = function(doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.employee_query" } -} +cur_frm.fields_dict.to_emp.get_query = function (doc, cdt, cdn) { + return { query: "erpnext.controllers.queries.employee_query" }; +}; diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.py b/erpnext/setup/doctype/authorization_rule/authorization_rule.py index 44bd826fc6e..1bf9aa61c33 100644 --- a/erpnext/setup/doctype/authorization_rule/authorization_rule.py +++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.py @@ -48,9 +48,7 @@ class AuthorizationRule(Document): "Customerwise Discount", "Itemwise Discount", ]: - frappe.throw( - _("Cannot set authorization on basis of Discount for {0}").format(self.transaction) - ) + frappe.throw(_("Cannot set authorization on basis of Discount for {0}").format(self.transaction)) elif self.based_on == "Average Discount" and flt(self.value) > 100.00: frappe.throw(_("Discount must be less than 100")) elif self.based_on == "Customerwise Discount" and not self.master_name: diff --git a/erpnext/setup/doctype/branch/branch.js b/erpnext/setup/doctype/branch/branch.js index c8a0afced8d..6e17c781931 100644 --- a/erpnext/setup/doctype/branch/branch.js +++ b/erpnext/setup/doctype/branch/branch.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Branch', { - refresh: function(frm) { - - } +frappe.ui.form.on("Branch", { + refresh: function (frm) {}, }); diff --git a/erpnext/setup/doctype/brand/brand.js b/erpnext/setup/doctype/brand/brand.js index 0abb71a3629..5d16d734e0f 100644 --- a/erpnext/setup/doctype/brand/brand.js +++ b/erpnext/setup/doctype/brand/brand.js @@ -1,71 +1,99 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Brand', { +frappe.ui.form.on("Brand", { setup: (frm) => { - frm.fields_dict["brand_defaults"].grid.get_field("default_warehouse").get_query = function(doc, cdt, cdn) { + frm.fields_dict["brand_defaults"].grid.get_field("default_warehouse").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { - filters: { company: row.company } - } - } - - frm.fields_dict["brand_defaults"].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) { - const row = locals[cdt][cdn]; - return { - filters: { - 'report_type': 'Profit and Loss', - 'company': row.company, - "is_group": 0 - } + filters: { company: row.company }, }; - } + }; - frm.fields_dict["brand_defaults"].grid.get_field("buying_cost_center").get_query = function(doc, cdt, cdn) { + frm.fields_dict["brand_defaults"].grid.get_field("default_discount_account").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { filters: { - "is_group": 0, - "company": row.company - } - } - } + report_type: "Profit and Loss", + company: row.company, + is_group: 0, + }, + }; + }; - frm.fields_dict["brand_defaults"].grid.get_field("expense_account").get_query = function(doc, cdt, cdn) { + frm.fields_dict["brand_defaults"].grid.get_field("buying_cost_center").get_query = function ( + doc, + cdt, + cdn + ) { + const row = locals[cdt][cdn]; + return { + filters: { + is_group: 0, + company: row.company, + }, + }; + }; + + frm.fields_dict["brand_defaults"].grid.get_field("expense_account").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { query: "erpnext.controllers.queries.get_expense_account", - filters: { company: row.company } - } - } - - frm.fields_dict["brand_defaults"].grid.get_field("default_provisional_account").get_query = function(doc, cdt, cdn) { - const row = locals[cdt][cdn]; - return { - filters: { - "company": row.company, - "root_type": ["in", ["Liability", "Asset"]], - "is_group": 0 - } + filters: { company: row.company }, }; - } + }; - frm.fields_dict["brand_defaults"].grid.get_field("selling_cost_center").get_query = function(doc, cdt, cdn) { + frm.fields_dict["brand_defaults"].grid.get_field("default_provisional_account").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { filters: { - "is_group": 0, - "company": row.company - } - } - } + company: row.company, + root_type: ["in", ["Liability", "Asset"]], + is_group: 0, + }, + }; + }; - frm.fields_dict["brand_defaults"].grid.get_field("income_account").get_query = function(doc, cdt, cdn) { + frm.fields_dict["brand_defaults"].grid.get_field("selling_cost_center").get_query = function ( + doc, + cdt, + cdn + ) { + const row = locals[cdt][cdn]; + return { + filters: { + is_group: 0, + company: row.company, + }, + }; + }; + + frm.fields_dict["brand_defaults"].grid.get_field("income_account").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { query: "erpnext.controllers.queries.get_income_account", - filters: { company: row.company } - } - } - } -}); \ No newline at end of file + filters: { company: row.company }, + }; + }; + }, +}); diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 589b3fe8b98..6c237d787bb 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -4,121 +4,148 @@ frappe.provide("erpnext.company"); frappe.ui.form.on("Company", { - onload: function(frm) { + onload: function (frm) { if (frm.doc.__islocal && frm.doc.parent_company) { - frappe.db.get_value('Company', frm.doc.parent_company, 'is_group', (r) => { + frappe.db.get_value("Company", frm.doc.parent_company, "is_group", (r) => { if (!r.is_group) { - frm.set_value('parent_company', ''); + frm.set_value("parent_company", ""); } }); } - frm.call('check_if_transactions_exist').then(r => { - frm.toggle_enable("default_currency", (!r.message)); + frm.call("check_if_transactions_exist").then((r) => { + frm.toggle_enable("default_currency", !r.message); }); }, - setup: function(frm) { + setup: function (frm) { frm.__rename_queue = "long"; erpnext.company.setup_queries(frm); - frm.set_query("parent_company", function() { + frm.set_query("parent_company", function () { return { - filters: {"is_group": 1} - } + filters: { is_group: 1 }, + }; }); - frm.set_query("default_selling_terms", function() { + frm.set_query("default_selling_terms", function () { return { filters: { selling: 1 } }; }); - frm.set_query("default_buying_terms", function() { + frm.set_query("default_buying_terms", function () { return { filters: { buying: 1 } }; }); - frm.set_query("default_in_transit_warehouse", function() { + frm.set_query("default_in_transit_warehouse", function () { return { - filters:{ - 'warehouse_type' : 'Transit', - 'is_group': 0, - 'company': frm.doc.company_name - } + filters: { + warehouse_type: "Transit", + is_group: 0, + company: frm.doc.company_name, + }, }; }); }, - company_name: function(frm) { - if(frm.doc.__islocal) { + company_name: function (frm) { + if (frm.doc.__islocal) { // add missing " " arg in split method let parts = frm.doc.company_name.split(" "); let abbr = $.map(parts, function (p) { - return p? p.substr(0, 1) : null; + return p ? p.substr(0, 1) : null; }).join(""); frm.set_value("abbr", abbr); } }, - parent_company: function(frm) { + parent_company: function (frm) { var bool = frm.doc.parent_company ? true : false; - frm.set_value('create_chart_of_accounts_based_on', bool ? "Existing Company" : ""); - frm.set_value('existing_company', bool ? frm.doc.parent_company : ""); + frm.set_value("create_chart_of_accounts_based_on", bool ? "Existing Company" : ""); + frm.set_value("existing_company", bool ? frm.doc.parent_company : ""); disbale_coa_fields(frm, bool); }, - date_of_commencement: function(frm) { - if(frm.doc.date_of_commencement { + .then((r) => { frappe.model.set_value(cdt, cdn, "address", r.name); }) - .catch(err => { + .catch((err) => { console.log(err); }); - } + }, }); diff --git a/erpnext/setup/doctype/email_digest/email_digest.js b/erpnext/setup/doctype/email_digest/email_digest.js index c2c2710b025..c0a887e4b11 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.js +++ b/erpnext/setup/doctype/email_digest/email_digest.js @@ -2,30 +2,30 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Email Digest", { - refresh: function(frm) { + refresh: function (frm) { if (!frm.is_new()) { - frm.add_custom_button(__('View Now'), function() { + frm.add_custom_button(__("View Now"), function () { frappe.call({ - method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg', + method: "erpnext.setup.doctype.email_digest.email_digest.get_digest_msg", args: { - name: frm.doc.name + name: frm.doc.name, }, - callback: function(r) { + callback: function (r) { let d = new frappe.ui.Dialog({ - title: __('Email Digest: {0}', [frm.doc.name]), - width: 800 + title: __("Email Digest: {0}", [frm.doc.name]), + width: 800, }); $(d.body).html(r.message); d.show(); - } + }, }); }); - frm.add_custom_button(__('Send Now'), function() { - return frm.call('send', null, () => { - frappe.show_alert({ message: __("Message Sent"), indicator: 'green'}); + frm.add_custom_button(__("Send Now"), function () { + return frm.call("send", null, () => { + frappe.show_alert({ message: __("Message Sent"), indicator: "green" }); }); }); } - } + }, }); diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 6ed44fff686..d1f8340a4d6 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -31,7 +31,7 @@ from frappe.model.document import Document class EmailDigest(Document): def __init__(self, *args, **kwargs): - super(EmailDigest, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.from_date, self.to_date = self.get_from_to_date() self.set_dates() @@ -46,9 +46,7 @@ class EmailDigest(Document): select name, enabled from tabUser where name not in ({}) and user_type != "Website User" - order by enabled desc, name asc""".format( - ", ".join(["%s"] * len(STANDARD_USERS)) - ), + order by enabled desc, name asc""".format(", ".join(["%s"] * len(STANDARD_USERS))), STANDARD_USERS, as_dict=1, ) @@ -151,15 +149,11 @@ class EmailDigest(Document): context.text_color = "#36414C" context.h1 = "margin-bottom: 30px; margin-top: 40px; font-weight: 400; font-size: 30px;" context.h2 = "margin-bottom: 30px; margin-top: -20px; font-weight: 400; font-size: 20px;" - context.label_css = """display: inline-block; color: {text_muted}; - padding: 3px 7px; margin-right: 7px;""".format( - text_muted=context.text_muted - ) + context.label_css = f"""display: inline-block; color: {context.text_muted}; + padding: 3px 7px; margin-right: 7px;""" context.section_head = "margin-top: 60px; font-size: 16px;" context.line_item = "padding: 5px 0px; margin: 0; border-bottom: 1px solid #d1d8dd;" - context.link_css = "color: {text_color}; text-decoration: none;".format( - text_color=context.text_color - ) + context.link_css = f"color: {context.text_color}; text-decoration: none;" def get_notifications(self): """Get notifications for user""" @@ -182,7 +176,7 @@ class EmailDigest(Document): events = get_events(from_date, to_date) event_count = 0 - for i, e in enumerate(events): + for _i, e in enumerate(events): e.starts_on_label = format_time(e.starts_on) e.ends_on_label = format_time(e.ends_on) if e.ends_on else None e.date = formatdate(e.starts) @@ -298,11 +292,8 @@ class EmailDigest(Document): "new_quotations", "pending_quotations", ): - if self.get(key): - cache_key = "email_digest:card:{0}:{1}:{2}:{3}".format( - self.company, self.frequency, key, self.from_date - ) + cache_key = f"email_digest:card:{self.company}:{self.frequency}:{key}:{self.from_date}" card = cache.get(cache_key) if card: @@ -421,9 +412,7 @@ class EmailDigest(Document): return self.get_type_balance("invoiced_amount", "Receivable") def get_expenses_booked(self): - expenses, past_expenses, count = self.get_period_amounts( - self.get_roots("expense"), "expenses_booked" - ) + expenses, past_expenses, count = self.get_period_amounts(self.get_roots("expense"), "expenses_booked") expense_account = frappe.db.get_all( "Account", @@ -559,7 +548,6 @@ class EmailDigest(Document): return {"label": label, "value": value, "count": count} def get_type_balance(self, fieldname, account_type, root_type=None): - if root_type: accounts = [ d.name @@ -645,57 +633,47 @@ class EmailDigest(Document): ] def get_root_type_accounts(self, root_type): - if not root_type in self._accounts: + if root_type not in self._accounts: self._accounts[root_type] = [ d.name for d in frappe.db.get_all( - "Account", filters={"root_type": root_type.title(), "company": self.company, "is_group": 0} + "Account", + filters={"root_type": root_type.title(), "company": self.company, "is_group": 0}, ) ] return self._accounts[root_type] def get_purchase_order(self): - return self.get_summary_of_doc("Purchase Order", "purchase_order") def get_sales_order(self): - return self.get_summary_of_doc("Sales Order", "sales_order") def get_pending_purchase_orders(self): - return self.get_summary_of_pending("Purchase Order", "pending_purchase_orders", "per_received") def get_pending_sales_orders(self): - return self.get_summary_of_pending("Sales Order", "pending_sales_orders", "per_delivered") def get_sales_invoice(self): - return self.get_summary_of_doc("Sales Invoice", "sales_invoice") def get_purchase_invoice(self): - return self.get_summary_of_doc("Purchase Invoice", "purchase_invoice") def get_new_quotations(self): - return self.get_summary_of_doc("Quotation", "new_quotations") def get_pending_quotations(self): - return self.get_summary_of_pending_quotations("pending_quotations") def get_summary_of_pending(self, doc_type, fieldname, getfield): - value, count, billed_value, delivered_value = frappe.db.sql( """select ifnull(sum(grand_total),0), count(*), - ifnull(sum(grand_total*per_billed/100),0), ifnull(sum(grand_total*{0}/100),0) from `tab{1}` + ifnull(sum(grand_total*per_billed/100),0), ifnull(sum(grand_total*{}/100),0) from `tab{}` where (transaction_date <= %(to_date)s) and status not in ('Closed','Cancelled', 'Completed') - and company = %(company)s """.format( - getfield, doc_type - ), + and company = %(company)s """.format(getfield, doc_type), {"to_date": self.future_to_date, "company": self.company}, )[0] @@ -708,7 +686,6 @@ class EmailDigest(Document): } def get_summary_of_pending_quotations(self, fieldname): - value, count = frappe.db.sql( """select ifnull(sum(grand_total),0), count(*) from `tabQuotation` where (transaction_date <= %(to_date)s) @@ -741,19 +718,14 @@ class EmailDigest(Document): return {"label": label, "value": value, "last_value": last_value, "count": count} def get_summary_of_doc(self, doc_type, fieldname): - date_field = ( "posting_date" if doc_type in ["Sales Invoice", "Purchase Invoice"] else "transaction_date" ) - value = flt( - self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].grand_total - ) + value = flt(self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].grand_total) count = self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].count - last_value = flt( - self.get_total_on(doc_type, self.past_from_date, self.past_to_date)[0].grand_total - ) + last_value = flt(self.get_total_on(doc_type, self.past_from_date, self.past_to_date)[0].grand_total) filters = { date_field: [[">=", self.future_from_date], ["<=", self.future_to_date]], @@ -772,7 +744,6 @@ class EmailDigest(Document): return {"label": label, "value": value, "last_value": last_value, "count": count} def get_total_on(self, doc_type, from_date, to_date): - date_field = ( "posting_date" if doc_type in ["Sales Invoice", "Purchase Invoice"] else "transaction_date" ) @@ -853,20 +824,16 @@ class EmailDigest(Document): "received_qty, qty - received_qty as missing_qty, rate, amount" ) - sql_po = """select {fields} from `tabPurchase Order Item` + sql_po = f"""select {fields_po} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date and received_qty < qty order by `tabPurchase Order Item`.parent DESC, - `tabPurchase Order Item`.schedule_date DESC""".format( - fields=fields_po - ) + `tabPurchase Order Item`.schedule_date DESC""" - sql_poi = """select {fields} from `tabPurchase Order Item` + sql_poi = f"""select {fields_poi} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date - and received_qty < qty order by `tabPurchase Order Item`.idx""".format( - fields=fields_poi - ) + and received_qty < qty order by `tabPurchase Order Item`.idx""" purchase_order_list = frappe.db.sql(sql_po, as_dict=True) purchase_order_items_overdue_list = frappe.db.sql(sql_poi, as_dict=True) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index efc3fd1d33d..7a1efa82fa0 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -4,74 +4,64 @@ frappe.provide("erpnext.setup"); erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.form.Controller { setup() { - this.frm.fields_dict.user_id.get_query = function(doc, cdt, cdn) { + this.frm.fields_dict.user_id.get_query = function (doc, cdt, cdn) { return { query: "frappe.core.doctype.user.user.user_query", - filters: {ignore_user_type: 1} - } - } - this.frm.fields_dict.reports_to.get_query = function(doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.employee_query"} } + filters: { ignore_user_type: 1 }, + }; + }; + this.frm.fields_dict.reports_to.get_query = function (doc, cdt, cdn) { + return { query: "erpnext.controllers.queries.employee_query" }; + }; } refresh() { erpnext.toggle_naming_series(); } - - salutation() { - if (this.frm.doc.salutation) { - this.frm.set_value("gender", { - "Mr": "Male", - "Ms": "Female" - }[this.frm.doc.salutation]); - } - } - }; frappe.ui.form.on("Employee", { onload: function (frm) { - frm.set_query("department", function() { + frm.set_query("department", function () { return { - "filters": { - "company": frm.doc.company, - } + filters: { + company: frm.doc.company, + }, }; }); }, - prefered_contact_email: function(frm) { + prefered_contact_email: function (frm) { frm.events.update_contact(frm); }, - personal_email: function(frm) { + personal_email: function (frm) { frm.events.update_contact(frm); }, - company_email: function(frm) { + company_email: function (frm) { frm.events.update_contact(frm); }, - user_id: function(frm) { + user_id: function (frm) { frm.events.update_contact(frm); }, - update_contact: function(frm) { - var prefered_email_fieldname = frappe.model.scrub(frm.doc.prefered_contact_email) || 'user_id'; - frm.set_value("prefered_email", - frm.fields_dict[prefered_email_fieldname].value); + update_contact: function (frm) { + var prefered_email_fieldname = frappe.model.scrub(frm.doc.prefered_contact_email) || "user_id"; + frm.set_value("prefered_email", frm.fields_dict[prefered_email_fieldname].value); }, - status: function(frm) { + status: function (frm) { return frm.call({ method: "deactivate_sales_person", args: { employee: frm.doc.employee, - status: frm.doc.status - } + status: frm.doc.status, + }, }); }, - create_user: function(frm) { + create_user: function (frm) { if (!frm.doc.prefered_email) { frappe.throw(__("Please enter Preferred Contact Email")); } @@ -79,46 +69,53 @@ frappe.ui.form.on("Employee", { method: "erpnext.setup.doctype.employee.employee.create_user", args: { employee: frm.doc.name, - email: frm.doc.prefered_email + email: frm.doc.prefered_email, }, freeze: true, freeze_message: __("Creating User..."), callback: function (r) { frm.reload_doc(); - } + }, }); - } + }, }); cur_frm.cscript = new erpnext.setup.EmployeeController({ - frm: cur_frm + frm: cur_frm, }); - -frappe.tour['Employee'] = [ +frappe.tour["Employee"] = [ { fieldname: "first_name", title: "First Name", - description: __("Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched.") + description: __( + "Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched." + ), }, { fieldname: "company", title: "Company", - description: __("Select a Company this Employee belongs to.") + description: __("Select a Company this Employee belongs to."), }, { fieldname: "date_of_birth", title: "Date of Birth", - description: __("Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff.") + description: __( + "Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff." + ), }, { fieldname: "date_of_joining", title: "Date of Joining", - description: __("Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases.") + description: __( + "Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases." + ), }, { fieldname: "reports_to", title: "Reports To", - description: __("Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated.") + description: __( + "Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated." + ), }, ]; diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 1143ccb7b10..daf2df5a590 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -616,8 +616,8 @@ "fieldname": "relieving_date", "fieldtype": "Date", "label": "Relieving Date", - "no_copy": 1, "mandatory_depends_on": "eval:doc.status == \"Left\"", + "no_copy": 1, "oldfieldname": "relieving_date", "oldfieldtype": "Date" }, @@ -822,12 +822,14 @@ "icon": "fa fa-user", "idx": 24, "image_field": "image", + "is_tree": 1, "links": [], - "modified": "2023-10-04 10:57:05.174592", + "modified": "2024-01-03 17:36:20.984421", "modified_by": "Administrator", "module": "Setup", "name": "Employee", "naming_rule": "By \"Naming Series\" field", + "nsm_parent_field": "reports_to", "owner": "Administrator", "permissions": [ { @@ -860,7 +862,6 @@ "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 1, "share": 1, "write": 1 } @@ -871,4 +872,4 @@ "sort_order": "DESC", "states": [], "title_field": "employee_name" -} +} \ No newline at end of file diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index a632c90f51e..9f8f3861cf3 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -265,16 +265,12 @@ def validate_employee_role(doc, method=None, ignore_emp_check=False): user_roles = [d.role for d in doc.get("roles")] if "Employee" in user_roles: - frappe.msgprint( - _("User {0}: Removed Employee role as there is no mapped employee.").format(doc.name) - ) + frappe.msgprint(_("User {0}: Removed Employee role as there is no mapped employee.").format(doc.name)) doc.get("roles").remove(doc.get("roles", {"role": "Employee"})[0]) if "Employee Self Service" in user_roles: frappe.msgprint( - _("User {0}: Removed Employee Self Service role as there is no mapped employee.").format( - doc.name - ) + _("User {0}: Removed Employee Self Service role as there is no mapped employee.").format(doc.name) ) doc.get("roles").remove(doc.get("roles", {"role": "Employee Self Service"})[0]) @@ -290,17 +286,13 @@ def update_user_permissions(doc, method): def get_employee_email(employee_doc): return ( - employee_doc.get("user_id") - or employee_doc.get("personal_email") - or employee_doc.get("company_email") + employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email") ) def get_holiday_list_for_employee(employee, raise_exception=True): if employee: - holiday_list, company = frappe.get_cached_value( - "Employee", employee, ["holiday_list", "company"] - ) + holiday_list, company = frappe.get_cached_value("Employee", employee, ["holiday_list", "company"]) else: holiday_list = "" company = frappe.db.get_single_value("Global Defaults", "default_company") @@ -316,9 +308,7 @@ def get_holiday_list_for_employee(employee, raise_exception=True): return holiday_list -def is_holiday( - employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False -): +def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False): """ Returns True if given Employee has an holiday on the given date :param employee: Employee `name` @@ -428,7 +418,6 @@ def get_employee_emails(employee_list): @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): - filters = [["status", "=", "Active"]] if company and company != "All Companies": filters.append(["company", "=", company]) diff --git a/erpnext/setup/doctype/employee/employee_list.js b/erpnext/setup/doctype/employee/employee_list.js index d37e1496ca3..0c97b626954 100644 --- a/erpnext/setup/doctype/employee/employee_list.js +++ b/erpnext/setup/doctype/employee/employee_list.js @@ -1,9 +1,9 @@ -frappe.listview_settings['Employee'] = { - add_fields: ["status", "branch", "department", "designation","image"], - filters: [["status","=", "Active"]], - get_indicator: function(doc) { +frappe.listview_settings["Employee"] = { + add_fields: ["status", "branch", "department", "designation", "image"], + filters: [["status", "=", "Active"]], + get_indicator: function (doc) { var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status]; + indicator[1] = { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status]; return indicator; - } + }, }; diff --git a/erpnext/setup/doctype/employee/employee_tree.js b/erpnext/setup/doctype/employee/employee_tree.js index 8f52cff8c2c..26a89719d7f 100644 --- a/erpnext/setup/doctype/employee/employee_tree.js +++ b/erpnext/setup/doctype/employee/employee_tree.js @@ -1,13 +1,13 @@ -frappe.treeview_settings['Employee'] = { +frappe.treeview_settings["Employee"] = { get_tree_nodes: "erpnext.setup.doctype.employee.employee.get_children", filters: [ { fieldname: "company", - fieldtype:"Select", - options: ['All Companies'].concat(erpnext.utils.get_tree_options("company")), + fieldtype: "Select", + options: ["All Companies"].concat(erpnext.utils.get_tree_options("company")), label: __("Company"), - default: erpnext.utils.get_tree_default("company") - } + default: erpnext.utils.get_tree_default("company"), + }, ], breadcrumb: "Hr", disable_add_node: true, @@ -15,22 +15,22 @@ frappe.treeview_settings['Employee'] = { toolbar: [ { toggle_btn: true }, { - label:__("Edit"), - condition: function(node) { + label: __("Edit"), + condition: function (node) { return !node.is_root; }, - click: function(node) { + click: function (node) { frappe.set_route("Form", "Employee", node.data.value); - } - } + }, + }, ], menu_items: [ { label: __("New Employee"), - action: function() { + action: function () { frappe.new_doc("Employee", true); }, - condition: 'frappe.boot.user.can_create.indexOf("Employee") !== -1' - } + condition: 'frappe.boot.user.can_create.indexOf("Employee") !== -1', + }, ], }; diff --git a/erpnext/setup/doctype/employee_group/employee_group.js b/erpnext/setup/doctype/employee_group/employee_group.js index 3db57181f8a..0ffa877ee7e 100644 --- a/erpnext/setup/doctype/employee_group/employee_group.js +++ b/erpnext/setup/doctype/employee_group/employee_group.js @@ -1,6 +1,4 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Employee Group', { - -}); +frappe.ui.form.on("Employee Group", {}); diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.js b/erpnext/setup/doctype/global_defaults/global_defaults.js index 942dd5989ea..20ac6dafde5 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.js +++ b/erpnext/setup/doctype/global_defaults/global_defaults.js @@ -1,14 +1,14 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Global Defaults', { - onload: function(frm) { - frm.trigger('get_distance_uoms'); +frappe.ui.form.on("Global Defaults", { + onload: function (frm) { + frm.trigger("get_distance_uoms"); }, - validate: function(frm) { - frm.call('get_defaults', null, r => { + validate: function (frm) { + frm.call("get_defaults", null, (r) => { frappe.sys_defaults = r.message; - }) + }); }, get_distance_uoms: function (frm) { let units = []; @@ -17,16 +17,16 @@ frappe.ui.form.on('Global Defaults', { method: "frappe.client.get_list", args: { doctype: "UOM Conversion Factor", - filters: { "category": __("Length") }, + filters: { category: __("Length") }, fields: ["to_uom"], - limit_page_length: 500 + limit_page_length: 500, }, callback: function (r) { - r.message.forEach(row => units.push(row.to_uom)); - } + r.message.forEach((row) => units.push(row.to_uom)); + }, }); frm.set_query("default_distance_unit", function () { - return { filters: { "name": ["IN", units] } }; + return { filters: { name: ["IN", units] } }; }); - } + }, }); diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.js b/erpnext/setup/doctype/holiday_list/holiday_list.js index 90d9f1b6f50..1f95a73c242 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.js +++ b/erpnext/setup/doctype/holiday_list/holiday_list.js @@ -2,12 +2,12 @@ // For license information, please see license.txt frappe.ui.form.on("Holiday List", { - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.holidays) { frm.set_value("total_holidays", frm.doc.holidays.length); } - frm.call("get_supported_countries").then(r => { + 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)) @@ -18,20 +18,20 @@ frappe.ui.form.on("Holiday List", { } }); }, - from_date: function(frm) { + 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) { + country: function (frm) { frm.set_value("subdivision", ""); if (frm.doc.country) { frm.trigger("set_subdivisions"); } }, - set_subdivisions: function(frm) { + 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); @@ -67,11 +67,15 @@ frappe.tour["Holiday List"] = [ { fieldname: "get_weekly_off_dates", title: "Add Holidays", - description: __("Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays"), + description: __( + "Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays" + ), }, { fieldname: "holidays", title: "Holidays", - description: __("Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually.") + description: __( + "Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually." + ), }, ]; diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 526bc2ba4ac..94ec0b95675 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -63,7 +63,7 @@ class HolidayList(Document): for holiday_date, holiday_name in country_holidays( self.country, subdiv=self.subdivision, - years=[from_date.year, to_date.year], + years=list(range(from_date.year, to_date.year + 1)), language=frappe.local.lang, ).items(): if holiday_date in existing_holidays: @@ -161,9 +161,7 @@ def is_holiday(holiday_list, date=None): if date is None: date = today() if holiday_list: - return bool( - frappe.db.exists("Holiday", {"parent": holiday_list, "holiday_date": date}, cache=True) - ) + return bool(frappe.db.exists("Holiday", {"parent": holiday_list, "holiday_date": date}, cache=True)) else: return False diff --git a/erpnext/setup/doctype/holiday_list/holiday_list_calendar.js b/erpnext/setup/doctype/holiday_list/holiday_list_calendar.js index bb6d8315467..ff830881c92 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list_calendar.js +++ b/erpnext/setup/doctype/holiday_list/holiday_list_calendar.js @@ -3,20 +3,20 @@ frappe.views.calendar["Holiday List"] = { field_map: { - "start": "holiday_date", - "end": "holiday_date", - "id": "name", - "title": "description", - "allDay": "allDay" + start: "holiday_date", + end: "holiday_date", + id: "name", + title: "description", + allDay: "allDay", }, order_by: `from_date`, get_events_method: "erpnext.setup.doctype.holiday_list.holiday_list.get_events", filters: [ { - 'fieldtype': 'Link', - 'fieldname': 'holiday_list', - 'options': 'Holiday List', - 'label': __('Holiday List') - } - ] -} + fieldtype: "Link", + fieldname: "holiday_list", + options: "Holiday List", + label: __("Holiday List"), + }, + ], +}; diff --git a/erpnext/setup/doctype/holiday_list/test_holiday_list.py b/erpnext/setup/doctype/holiday_list/test_holiday_list.py index 7eeb27d864e..0d3c35fb2cf 100644 --- a/erpnext/setup/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/test_holiday_list.py @@ -48,17 +48,58 @@ class TestHolidayList(unittest.TestCase): 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.from_date = "2022-01-01" + holiday_list.to_date = "2024-12-31" 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) + holidays = holiday_list.get_holidays() + self.assertIn(date(2022, 1, 1), holidays) + self.assertIn(date(2022, 4, 15), holidays) + self.assertIn(date(2022, 4, 18), holidays) + self.assertIn(date(2022, 5, 1), holidays) + self.assertIn(date(2022, 5, 26), holidays) + self.assertIn(date(2022, 6, 6), holidays) + self.assertIn(date(2022, 10, 3), holidays) + self.assertIn(date(2022, 10, 31), holidays) + self.assertIn(date(2022, 11, 16), holidays) + self.assertIn(date(2022, 12, 25), holidays) + self.assertIn(date(2022, 12, 26), holidays) + self.assertIn(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) + self.assertIn(date(2023, 5, 1), holidays) + self.assertIn(date(2023, 5, 18), holidays) + self.assertIn(date(2023, 5, 29), holidays) + self.assertIn(date(2023, 10, 3), holidays) + self.assertIn(date(2023, 10, 31), holidays) + self.assertIn(date(2023, 11, 22), holidays) + self.assertIn(date(2023, 12, 25), holidays) + self.assertIn(date(2023, 12, 26), holidays) + self.assertIn(date(2024, 1, 1), holidays) + self.assertIn(date(2024, 3, 29), holidays) + self.assertIn(date(2024, 4, 1), holidays) + self.assertIn(date(2024, 5, 1), holidays) + self.assertIn(date(2024, 5, 9), holidays) + self.assertIn(date(2024, 5, 20), holidays) + self.assertIn(date(2024, 10, 3), holidays) + self.assertIn(date(2024, 10, 31), holidays) + self.assertIn(date(2024, 11, 20), holidays) + self.assertIn(date(2024, 12, 25), holidays) + self.assertIn(date(2024, 12, 26), holidays) + + # check some random dates that should not be local holidays + self.assertNotIn(date(2022, 1, 2), holidays) + self.assertNotIn(date(2023, 4, 16), holidays) + self.assertNotIn(date(2024, 4, 19), holidays) + self.assertNotIn(date(2022, 5, 2), holidays) + self.assertNotIn(date(2023, 5, 27), holidays) + self.assertNotIn(date(2024, 6, 7), holidays) + self.assertNotIn(date(2022, 10, 4), holidays) + self.assertNotIn(date(2023, 10, 30), holidays) + self.assertNotIn(date(2024, 11, 17), holidays) + self.assertNotIn(date(2022, 12, 24), holidays) def test_localized_country_names(self): lang = frappe.local.lang @@ -71,9 +112,13 @@ class TestHolidayList(unittest.TestCase): frappe.local.lang = lang -def make_holiday_list( - name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None -): +def make_holiday_list(name, from_date=None, to_date=None, holiday_dates=None): + if from_date is None: + from_date = getdate() - timedelta(days=10) + + if to_date is None: + to_date = getdate() + frappe.delete_doc_if_exists("Holiday List", name, force=1) doc = frappe.get_doc( { diff --git a/erpnext/setup/doctype/incoterm/incoterm.py b/erpnext/setup/doctype/incoterm/incoterm.py index 7e2e622c24b..6b39a7b2f57 100644 --- a/erpnext/setup/doctype/incoterm/incoterm.py +++ b/erpnext/setup/doctype/incoterm/incoterm.py @@ -14,7 +14,7 @@ def create_incoterms(): import os from csv import DictReader - with open(os.path.join(os.path.dirname(__file__), "incoterms.csv"), "r") as f: + with open(os.path.join(os.path.dirname(__file__), "incoterms.csv")) as f: for incoterm in DictReader(f): if frappe.db.exists("Incoterm", incoterm["code"]): continue diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js index 4b04ac1d5ef..ec5ba2a77e0 100644 --- a/erpnext/setup/doctype/item_group/item_group.js +++ b/erpnext/setup/doctype/item_group/item_group.js @@ -2,99 +2,116 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Item Group", { - onload: function(frm) { + onload: function (frm) { frm.list_route = "Tree/Item Group"; //get query select item group - frm.fields_dict['parent_item_group'].get_query = function(doc,cdt,cdn) { - return{ - filters:[ - ['Item Group', 'is_group', '=', 1], - ['Item Group', 'name', '!=', doc.item_group_name] - ] - } - } - frm.fields_dict['item_group_defaults'].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) { - const row = locals[cdt][cdn]; + frm.fields_dict["parent_item_group"].get_query = function (doc, cdt, cdn) { return { - filters: { - 'report_type': 'Profit and Loss', - 'company': row.company, - "is_group": 0 - } + filters: [ + ["Item Group", "is_group", "=", 1], + ["Item Group", "name", "!=", doc.item_group_name], + ], }; - } - frm.fields_dict["item_group_defaults"].grid.get_field("expense_account").get_query = function(doc, cdt, cdn) { + }; + frm.fields_dict["item_group_defaults"].grid.get_field("default_discount_account").get_query = + function (doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + filters: { + report_type: "Profit and Loss", + company: row.company, + is_group: 0, + }, + }; + }; + frm.fields_dict["item_group_defaults"].grid.get_field("expense_account").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { query: "erpnext.controllers.queries.get_expense_account", - filters: { company: row.company } - } - } - frm.fields_dict["item_group_defaults"].grid.get_field("income_account").get_query = function(doc, cdt, cdn) { + filters: { company: row.company }, + }; + }; + frm.fields_dict["item_group_defaults"].grid.get_field("income_account").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { query: "erpnext.controllers.queries.get_income_account", - filters: { company: row.company } - } - } + filters: { company: row.company }, + }; + }; - frm.fields_dict["item_group_defaults"].grid.get_field("buying_cost_center").get_query = function(doc, cdt, cdn) { + frm.fields_dict["item_group_defaults"].grid.get_field("buying_cost_center").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { filters: { - "is_group": 0, - "company": row.company - } - } - } + is_group: 0, + company: row.company, + }, + }; + }; - frm.fields_dict["item_group_defaults"].grid.get_field("selling_cost_center").get_query = function(doc, cdt, cdn) { + frm.fields_dict["item_group_defaults"].grid.get_field("selling_cost_center").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { filters: { - "is_group": 0, - "company": row.company - } - } - } + is_group: 0, + company: row.company, + }, + }; + }; }, - refresh: function(frm) { + refresh: function (frm) { frm.trigger("set_root_readonly"); - frm.add_custom_button(__("Item Group Tree"), function() { + frm.add_custom_button(__("Item Group Tree"), function () { frappe.set_route("Tree", "Item Group"); }); - if(!frm.is_new()) { - frm.add_custom_button(__("Items"), function() { - frappe.set_route("List", "Item", {"item_group": frm.doc.name}); + if (!frm.is_new()) { + frm.add_custom_button(__("Items"), function () { + frappe.set_route("List", "Item", { item_group: frm.doc.name }); }); } - frappe.model.with_doctype('Website Item', () => { - const web_item_meta = frappe.get_meta('Website Item'); + frappe.model.with_doctype("Website Item", () => { + const web_item_meta = frappe.get_meta("Website Item"); - const valid_fields = web_item_meta.fields.filter(df => - ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden - ).map(df => - ({ label: df.label, value: df.fieldname }) - ); + const valid_fields = web_item_meta.fields + .filter((df) => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden) + .map((df) => ({ label: df.label, value: df.fieldname })); frm.get_field("filter_fields").grid.update_docfield_property( - 'fieldname', 'options', valid_fields + "fieldname", + "options", + valid_fields ); }); }, - set_root_readonly: function(frm) { + set_root_readonly: function (frm) { // read-only for root item group frm.set_intro(""); - if(!frm.doc.parent_item_group && !frm.doc.__islocal) { + if (!frm.doc.parent_item_group && !frm.doc.__islocal) { frm.set_read_only(); frm.set_intro(__("This is a root item group and cannot be edited."), true); } }, - page_name: frappe.utils.warn_page_name_change + page_name: frappe.utils.warn_page_name_change, }); diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index 2986087277c..71dfdb4f91a 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_guest_to_view": 1, "allow_import": 1, "allow_rename": 1, "autoname": "field:item_group_name", @@ -227,13 +228,14 @@ "label": "Include Descendants" } ], + "has_web_view": 1, "icon": "fa fa-sitemap", "idx": 1, "image_field": "image", "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2023-01-05 12:21:30.458628", + "modified": "2024-02-22 16:23:46.936496", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index cc67c696b43..cb43f949000 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -25,7 +25,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): ) def validate(self): - super(ItemGroup, self).validate() + super().validate() if not self.parent_item_group and not frappe.flags.in_test: if frappe.db.exists("Item Group", _("All Item Groups")): @@ -45,7 +45,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): frappe.throw( _("{0} entered twice {1} in Item Taxes").format( frappe.bold(d.item_tax_template), - "for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "", + f"for tax category {frappe.bold(d.tax_category)}" if d.tax_category else "", ) ) else: diff --git a/erpnext/setup/doctype/item_group/item_group_tree.js b/erpnext/setup/doctype/item_group/item_group_tree.js index b2628f4f4f8..a7c2b6360cf 100644 --- a/erpnext/setup/doctype/item_group/item_group_tree.js +++ b/erpnext/setup/doctype/item_group/item_group_tree.js @@ -1,3 +1,3 @@ frappe.treeview_settings["Item Group"] = { - ignore_fields:["parent_item_group"] -} + ignore_fields: ["parent_item_group"], +}; diff --git a/erpnext/setup/doctype/item_group/templates/item_group.html b/erpnext/setup/doctype/item_group/templates/item_group.html new file mode 100644 index 00000000000..db123090aae --- /dev/null +++ b/erpnext/setup/doctype/item_group/templates/item_group.html @@ -0,0 +1,7 @@ +{% extends "templates/web.html" %} + +{% block page_content %} +

              {{ title }}

              +{% endblock %} + + \ No newline at end of file diff --git a/erpnext/setup/doctype/item_group/templates/item_group_row.html b/erpnext/setup/doctype/item_group/templates/item_group_row.html new file mode 100644 index 00000000000..d7014b453ab --- /dev/null +++ b/erpnext/setup/doctype/item_group/templates/item_group_row.html @@ -0,0 +1,4 @@ + + diff --git a/erpnext/setup/doctype/item_group/test_item_group.py b/erpnext/setup/doctype/item_group/test_item_group.py index 11bc9b92c12..a579fb703da 100644 --- a/erpnext/setup/doctype/item_group/test_item_group.py +++ b/erpnext/setup/doctype/item_group/test_item_group.py @@ -124,9 +124,7 @@ class TestItem(unittest.TestCase): def print_tree(self): import json - print( - json.dumps(frappe.db.sql("select name, lft, rgt from `tabItem Group` order by lft"), indent=1) - ) + print(json.dumps(frappe.db.sql("select name, lft, rgt from `tabItem Group` order by lft"), indent=1)) def test_move_leaf_into_another_group(self): # before move @@ -156,17 +154,13 @@ class TestItem(unittest.TestCase): def test_delete_leaf(self): # for checking later - parent_item_group = frappe.db.get_value( - "Item Group", "_Test Item Group B - 3", "parent_item_group" - ) - rgt = frappe.db.get_value("Item Group", parent_item_group, "rgt") + parent_item_group = frappe.db.get_value("Item Group", "_Test Item Group B - 3", "parent_item_group") + frappe.db.get_value("Item Group", parent_item_group, "rgt") ancestors = get_ancestors_of("Item Group", "_Test Item Group B - 3") ancestors = frappe.db.sql( """select name, rgt from `tabItem Group` - where name in ({})""".format( - ", ".join(["%s"] * len(ancestors)) - ), + where name in ({})""".format(", ".join(["%s"] * len(ancestors))), tuple(ancestors), as_dict=True, ) @@ -188,9 +182,7 @@ class TestItem(unittest.TestCase): def test_delete_group(self): # cannot delete group with child, but can delete leaf - self.assertRaises( - NestedSetChildExistsError, frappe.delete_doc, "Item Group", "_Test Item Group B" - ) + self.assertRaises(NestedSetChildExistsError, frappe.delete_doc, "Item Group", "_Test Item Group B") def test_merge_groups(self): frappe.rename_doc("Item Group", "_Test Item Group B", "_Test Item Group C", merge=True) @@ -207,7 +199,6 @@ class TestItem(unittest.TestCase): """select name from `tabItem Group` where parent_item_group='_Test Item Group C'""" ): - doc = frappe.get_doc("Item Group", name) doc.parent_item_group = "_Test Item Group B" doc.save() diff --git a/erpnext/setup/doctype/party_type/party_type.js b/erpnext/setup/doctype/party_type/party_type.js index 7a96dc55ee9..9bbfd7d8832 100644 --- a/erpnext/setup/doctype/party_type/party_type.js +++ b/erpnext/setup/doctype/party_type/party_type.js @@ -1,15 +1,15 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Party Type', { - setup: function(frm) { - frm.fields_dict["party_type"].get_query = function(frm) { +frappe.ui.form.on("Party Type", { + setup: function (frm) { + frm.fields_dict["party_type"].get_query = function (frm) { return { filters: { - "istable": 0, - "is_submittable": 0 - } - } - } - } + istable: 0, + is_submittable: 0, + }, + }; + }; + }, }); diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index cf7cba84528..e14287237f8 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -19,10 +19,8 @@ def get_party_type(doctype, txt, searchfield, start, page_len, filters): cond = "and account_type = '%s'" % account_type return frappe.db.sql( - """select name from `tabParty Type` - where `{key}` LIKE %(txt)s {cond} - order by name limit %(page_len)s offset %(start)s""".format( - key=searchfield, cond=cond - ), + f"""select name from `tabParty Type` + where `{searchfield}` LIKE %(txt)s {cond} + order by name limit %(page_len)s offset %(start)s""", {"txt": "%" + txt + "%", "start": start, "page_len": page_len}, ) diff --git a/erpnext/setup/doctype/print_heading/print_heading.js b/erpnext/setup/doctype/print_heading/print_heading.js index 3680906057f..273e30fd197 100644 --- a/erpnext/setup/doctype/print_heading/print_heading.js +++ b/erpnext/setup/doctype/print_heading/print_heading.js @@ -1,13 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - - //--------- ONLOAD ------------- -cur_frm.cscript.onload = function(doc, cdt, cdn) { +cur_frm.cscript.onload = function (doc, cdt, cdn) {}; -} - -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} +cur_frm.cscript.refresh = function (doc, cdt, cdn) {}; diff --git a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.js b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.js index 3680906057f..273e30fd197 100644 --- a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.js +++ b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.js @@ -1,13 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - - //--------- ONLOAD ------------- -cur_frm.cscript.onload = function(doc, cdt, cdn) { +cur_frm.cscript.onload = function (doc, cdt, cdn) {}; -} - -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} +cur_frm.cscript.refresh = function (doc, cdt, cdn) {}; diff --git a/erpnext/setup/doctype/sales_partner/sales_partner.js b/erpnext/setup/doctype/sales_partner/sales_partner.js index 5656d43e852..cb957941dc8 100644 --- a/erpnext/setup/doctype/sales_partner/sales_partner.js +++ b/erpnext/setup/doctype/sales_partner/sales_partner.js @@ -1,34 +1,33 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Sales Partner', { - refresh: function(frm) { - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Sales Partner'} +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']); + if (frm.doc.__islocal) { + hide_field(["address_html", "contact_html", "address_contacts"]); frappe.contacts.clear_address_and_contact(frm); - } - else{ - unhide_field(['address_html', 'contact_html', 'address_contacts']); + } else { + unhide_field(["address_html", "contact_html", "address_contacts"]); frappe.contacts.render_address_and_contact(frm); } }, - setup: function(frm) { - frm.fields_dict["targets"].grid.get_field("distribution_id").get_query = function(doc, cdt, cdn){ + setup: function (frm) { + frm.fields_dict["targets"].grid.get_field("distribution_id").get_query = function (doc, cdt, cdn) { var row = locals[cdt][cdn]; return { filters: { - 'fiscal_year': row.fiscal_year - } - } + fiscal_year: row.fiscal_year, + }, + }; }; }, - referral_code:function(frm){ + referral_code: function (frm) { if (frm.doc.referral_code) { - frm.doc.referral_code=frm.doc.referral_code.toUpperCase(); - frm.refresh_field('referral_code'); + frm.doc.referral_code = frm.doc.referral_code.toUpperCase(); + frm.refresh_field("referral_code"); } - } + }, }); diff --git a/erpnext/setup/doctype/sales_partner/sales_partner.py b/erpnext/setup/doctype/sales_partner/sales_partner.py index c3136715fe5..e72773e7132 100644 --- a/erpnext/setup/doctype/sales_partner/sales_partner.py +++ b/erpnext/setup/doctype/sales_partner/sales_partner.py @@ -25,7 +25,7 @@ class SalesPartner(WebsiteGenerator): def validate(self): if not self.route: self.route = "partners/" + self.scrub(self.partner_name) - super(SalesPartner, self).validate() + super().validate() if self.partner_website and not self.partner_website.startswith("http"): self.partner_website = "http://" + self.partner_website diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js index d86a8f3d984..89f794e0955 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.js +++ b/erpnext/setup/doctype/sales_person/sales_person.js @@ -1,59 +1,69 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Sales Person', { - refresh: function(frm) { - if(frm.doc.__onload && frm.doc.__onload.dashboard_info) { +frappe.ui.form.on("Sales Person", { + refresh: function (frm) { + if (frm.doc.__onload && frm.doc.__onload.dashboard_info) { let info = frm.doc.__onload.dashboard_info; - frm.dashboard.add_indicator(__('Total Contribution Amount Against Orders: {0}', - [format_currency(info.allocated_amount_against_order, info.currency)]), 'blue'); + frm.dashboard.add_indicator( + __("Total Contribution Amount Against Orders: {0}", [ + format_currency(info.allocated_amount_against_order, info.currency), + ]), + "blue" + ); - frm.dashboard.add_indicator(__('Total Contribution Amount Against Invoices: {0}', - [format_currency(info.allocated_amount_against_invoice, info.currency)]), 'blue'); + frm.dashboard.add_indicator( + __("Total Contribution Amount Against Invoices: {0}", [ + format_currency(info.allocated_amount_against_invoice, info.currency), + ]), + "blue" + ); } }, - setup: function(frm) { - frm.fields_dict["targets"].grid.get_field("distribution_id").get_query = function(doc, cdt, cdn){ + setup: function (frm) { + frm.fields_dict["targets"].grid.get_field("distribution_id").get_query = function (doc, cdt, cdn) { var row = locals[cdt][cdn]; return { filters: { - 'fiscal_year': row.fiscal_year - } - } + fiscal_year: row.fiscal_year, + }, + }; }; frm.make_methods = { - 'Sales Order': () => frappe.new_doc("Sales Order") - .then(() => frm.add_child("sales_team", {"sales_person": frm.doc.name})) - } - } + "Sales Order": () => + frappe + .new_doc("Sales Order") + .then(() => frm.add_child("sales_team", { sales_person: frm.doc.name })), + }; + }, }); -cur_frm.cscript.refresh = function(doc, cdt, cdn) { +cur_frm.cscript.refresh = function (doc, cdt, cdn) { cur_frm.cscript.set_root_readonly(doc); -} +}; -cur_frm.cscript.set_root_readonly = function(doc) { +cur_frm.cscript.set_root_readonly = function (doc) { // read-only for root - if(!doc.parent_sales_person && !doc.__islocal) { + if (!doc.parent_sales_person && !doc.__islocal) { cur_frm.set_read_only(); cur_frm.set_intro(__("This is a root sales person and cannot be edited.")); } else { cur_frm.set_intro(null); } -} +}; //get query select sales person -cur_frm.fields_dict['parent_sales_person'].get_query = function(doc, cdt, cdn) { - return{ +cur_frm.fields_dict["parent_sales_person"].get_query = function (doc, cdt, cdn) { + return { filters: [ - ['Sales Person', 'is_group', '=', 1], - ['Sales Person', 'name', '!=', doc.sales_person_name] - ] - } -} + ["Sales Person", "is_group", "=", 1], + ["Sales Person", "name", "!=", doc.sales_person_name], + ], + }; +}; -cur_frm.fields_dict.employee.get_query = function(doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.employee_query" } -} +cur_frm.fields_dict.employee.get_query = function (doc, cdt, cdn) { + return { query: "erpnext.controllers.queries.employee_query" }; +}; diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index 0082c700758..91a7008fabd 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -52,7 +52,7 @@ class SalesPerson(NestedSet): self.set_onload("dashboard_info", info) def on_update(self): - super(SalesPerson, self).on_update() + super().on_update() self.validate_one_root() def get_email_id(self): @@ -78,7 +78,6 @@ def on_doctype_update(): def get_timeline_data(doctype, name): - out = {} out.update( diff --git a/erpnext/setup/doctype/sales_person/sales_person_tree.js b/erpnext/setup/doctype/sales_person/sales_person_tree.js index 00056fde869..0fcd592aea3 100644 --- a/erpnext/setup/doctype/sales_person/sales_person_tree.js +++ b/erpnext/setup/doctype/sales_person/sales_person_tree.js @@ -1,12 +1,18 @@ - frappe.treeview_settings["Sales Person"] = { fields: [ - {fieldtype:'Data', fieldname: 'sales_person_name', - label:__('New Sales Person Name'), reqd:true}, - {fieldtype:'Link', fieldname:'employee', - label:__('Employee'), options:'Employee', - description: __("Please enter Employee Id of this sales person")}, - {fieldtype:'Check', fieldname:'is_group', label:__('Group Node'), - description: __("Further nodes can be only created under 'Group' type nodes")} + { fieldtype: "Data", fieldname: "sales_person_name", label: __("New Sales Person Name"), reqd: true }, + { + fieldtype: "Link", + fieldname: "employee", + label: __("Employee"), + options: "Employee", + description: __("Please enter Employee Id of this sales person"), + }, + { + fieldtype: "Check", + fieldname: "is_group", + label: __("Group Node"), + description: __("Further nodes can be only created under 'Group' type nodes"), + }, ], -} +}; diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.js b/erpnext/setup/doctype/supplier_group/supplier_group.js index e75030d4414..cd58d23889d 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.js +++ b/erpnext/setup/doctype/supplier_group/supplier_group.js @@ -1,14 +1,14 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -cur_frm.cscript.refresh = function(doc) { +cur_frm.cscript.refresh = function (doc) { cur_frm.set_intro(doc.__islocal ? "" : __("There is nothing to edit.")); cur_frm.cscript.set_root_readonly(doc); }; -cur_frm.cscript.set_root_readonly = function(doc) { +cur_frm.cscript.set_root_readonly = function (doc) { // read-only for root customer group - if(!doc.parent_supplier_group && !doc.__islocal) { + if (!doc.parent_supplier_group && !doc.__islocal) { cur_frm.set_read_only(); cur_frm.set_intro(__("This is a root supplier group and cannot be edited.")); } else { @@ -17,22 +17,22 @@ cur_frm.cscript.set_root_readonly = function(doc) { }; // get query select Customer Group -cur_frm.fields_dict['parent_supplier_group'].get_query = function() { +cur_frm.fields_dict["parent_supplier_group"].get_query = function () { return { filters: { - 'is_group': 1, - 'name': ['!=', cur_frm.doc.supplier_group_name] - } + is_group: 1, + name: ["!=", cur_frm.doc.supplier_group_name], + }, }; }; -cur_frm.fields_dict['accounts'].grid.get_field('account').get_query = function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; +cur_frm.fields_dict["accounts"].grid.get_field("account").get_query = function (doc, cdt, cdn) { + var d = locals[cdt][cdn]; return { filters: { - 'account_type': 'Payable', - 'company': d.company, - "is_group": 0 - } + account_type: "Payable", + company: d.company, + is_group: 0, + }, }; }; diff --git a/erpnext/setup/doctype/supplier_group/supplier_group_tree.js b/erpnext/setup/doctype/supplier_group/supplier_group_tree.js index 728793eb25f..9210dc1f002 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group_tree.js +++ b/erpnext/setup/doctype/supplier_group/supplier_group_tree.js @@ -1,4 +1,4 @@ frappe.treeview_settings["Supplier Group"] = { breadcrumbs: "Buying", - ignore_fields:["parent_supplier_group"] + ignore_fields: ["parent_supplier_group"], }; diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py index 344f6c6a19d..c795401d399 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py @@ -15,12 +15,7 @@ class TermsandConditions(Document): def validate(self): if self.terms: validate_template(self.terms) - if ( - not cint(self.buying) - and not cint(self.selling) - and not cint(self.hr) - and not cint(self.disabled) - ): + if not cint(self.buying) and not cint(self.selling) and not cint(self.hr) and not cint(self.disabled): throw(_("At least one of the Applicable Modules should be selected")) diff --git a/erpnext/setup/doctype/territory/territory.js b/erpnext/setup/doctype/territory/territory.js index 3caf814c90b..744c7fe01e2 100644 --- a/erpnext/setup/doctype/territory/territory.js +++ b/erpnext/setup/doctype/territory/territory.js @@ -2,38 +2,38 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Territory", { - setup: function(frm) { - frm.fields_dict["targets"].grid.get_field("distribution_id").get_query = function(doc, cdt, cdn){ + setup: function (frm) { + frm.fields_dict["targets"].grid.get_field("distribution_id").get_query = function (doc, cdt, cdn) { var row = locals[cdt][cdn]; return { filters: { - 'fiscal_year': row.fiscal_year - } - } + fiscal_year: row.fiscal_year, + }, + }; }; - } + }, }); -cur_frm.cscript.refresh = function(doc, cdt, cdn) { +cur_frm.cscript.refresh = function (doc, cdt, cdn) { cur_frm.cscript.set_root_readonly(doc); -} +}; -cur_frm.cscript.set_root_readonly = function(doc) { +cur_frm.cscript.set_root_readonly = function (doc) { // read-only for root territory - if(!doc.parent_territory && !doc.__islocal) { + if (!doc.parent_territory && !doc.__islocal) { cur_frm.set_read_only(); cur_frm.set_intro(__("This is a root territory and cannot be edited.")); } else { cur_frm.set_intro(null); } -} +}; //get query select territory -cur_frm.fields_dict['parent_territory'].get_query = function(doc,cdt,cdn) { - return{ - filters:[ - ['Territory', 'is_group', '=', 1], - ['Territory', 'name', '!=', doc.territory_name] - ] - } -} +cur_frm.fields_dict["parent_territory"].get_query = function (doc, cdt, cdn) { + return { + filters: [ + ["Territory", "is_group", "=", 1], + ["Territory", "name", "!=", doc.territory_name], + ], + }; +}; diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 9bb5569de5b..630dfa16809 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -20,7 +20,7 @@ class Territory(NestedSet): frappe.throw(_("Either target qty or target amount is mandatory")) def on_update(self): - super(Territory, self).on_update() + super().on_update() self.validate_one_root() diff --git a/erpnext/setup/doctype/territory/territory_tree.js b/erpnext/setup/doctype/territory/territory_tree.js index dadeeef09e9..4892b8c45fc 100644 --- a/erpnext/setup/doctype/territory/territory_tree.js +++ b/erpnext/setup/doctype/territory/territory_tree.js @@ -1,3 +1,3 @@ frappe.treeview_settings["Territory"] = { - ignore_fields:["parent_territory"] -} + ignore_fields: ["parent_territory"], +}; diff --git a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py index 319d435ca69..2f32fe0eee4 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py @@ -25,9 +25,10 @@ class TestTransactionDeletionRecord(unittest.TestCase): self.assertTrue(contains_company) def test_no_of_docs_is_correct(self): - for i in range(5): + for _i in range(5): create_task("Dunder Mifflin Paper Co") tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") + tdr.reload() for doctype in tdr.doctypes: if doctype.doctype_name == "Task": self.assertEqual(doctype.no_of_docs, 5) @@ -40,16 +41,16 @@ class TestTransactionDeletionRecord(unittest.TestCase): def create_company(company_name): - company = frappe.get_doc( - {"doctype": "Company", "company_name": company_name, "default_currency": "INR"} - ) + company = frappe.get_doc({"doctype": "Company", "company_name": company_name, "default_currency": "INR"}) company.insert(ignore_if_duplicate=True) def create_transaction_deletion_request(company): tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() + tdr.process_in_single_transaction = True tdr.submit() + tdr.start_deletion_tasks() return tdr diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js index 6a50ef8bbd9..9aa02784165 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js @@ -1,39 +1,42 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Transaction Deletion Record', { - onload: function(frm) { +frappe.ui.form.on("Transaction Deletion Record", { + onload: function (frm) { if (frm.doc.docstatus == 0) { let doctypes_to_be_ignored_array; frappe.call({ - method: 'erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.get_doctypes_to_be_ignored', - callback: function(r) { + method: "erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.get_doctypes_to_be_ignored", + callback: function (r) { doctypes_to_be_ignored_array = r.message; populate_doctypes_to_be_ignored(doctypes_to_be_ignored_array, frm); - frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('no_of_docs', false); - frm.refresh_field('doctypes_to_be_ignored'); - } + frm.refresh_field("doctypes_to_be_ignored"); + }, }); } - - frm.get_field('doctypes_to_be_ignored').grid.cannot_add_rows = true; - frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('no_of_docs', false); - frm.refresh_field('doctypes_to_be_ignored'); }, - refresh: function(frm) { - frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('no_of_docs', false); - frm.refresh_field('doctypes_to_be_ignored'); - } + refresh: function (frm) { + if (frm.doc.docstatus == 1 && ["Queued", "Failed"].find((x) => x == frm.doc.status)) { + let execute_btn = frm.doc.status == "Queued" ? __("Start Deletion") : __("Retry"); + frm.add_custom_button(execute_btn, () => { + // Entry point for chain of events + frm.call({ + method: "start_deletion_tasks", + doc: frm.doc, + }); + }); + } + }, }); function populate_doctypes_to_be_ignored(doctypes_to_be_ignored_array, frm) { - if (!(frm.doc.doctypes_to_be_ignored)) { + if (frm.doc.doctypes_to_be_ignored.length === 0) { var i; for (i = 0; i < doctypes_to_be_ignored_array.length; i++) { - frm.add_child('doctypes_to_be_ignored', { - doctype_name: doctypes_to_be_ignored_array[i] + frm.add_child("doctypes_to_be_ignored", { + doctype_name: doctypes_to_be_ignored_array[i], }); } } diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index 23e59472a6d..b9f911dbe8c 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -7,10 +7,21 @@ "engine": "InnoDB", "field_order": [ "company", + "section_break_qpwb", + "status", + "error_log", + "tasks_section", + "delete_bin_data", + "delete_leads_and_addresses", + "reset_company_default_values", + "clear_notifications", + "initialize_doctypes_table", + "delete_transactions", + "section_break_tbej", "doctypes", "doctypes_to_be_ignored", "amended_from", - "status" + "process_in_single_transaction" ], "fields": [ { @@ -25,14 +36,16 @@ "fieldname": "doctypes", "fieldtype": "Table", "label": "Summary", - "options": "Transaction Deletion Record Item", + "no_copy": 1, + "options": "Transaction Deletion Record Details", "read_only": 1 }, { "fieldname": "doctypes_to_be_ignored", "fieldtype": "Table", "label": "Excluded DocTypes", - "options": "Transaction Deletion Record Item" + "options": "Transaction Deletion Record Item", + "read_only": 1 }, { "fieldname": "amended_from", @@ -46,18 +59,96 @@ { "fieldname": "status", "fieldtype": "Select", - "hidden": 1, "label": "Status", - "options": "Draft\nCompleted" + "no_copy": 1, + "options": "Queued\nRunning\nFailed\nCompleted\nCancelled", + "read_only": 1 + }, + { + "fieldname": "section_break_tbej", + "fieldtype": "Section Break" + }, + { + "fieldname": "tasks_section", + "fieldtype": "Section Break", + "label": "Tasks" + }, + { + "default": "0", + "fieldname": "delete_bin_data", + "fieldtype": "Check", + "label": "Delete Bins", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "delete_leads_and_addresses", + "fieldtype": "Check", + "label": "Delete Leads and Addresses", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "clear_notifications", + "fieldtype": "Check", + "label": "Clear Notifications", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "reset_company_default_values", + "fieldtype": "Check", + "label": "Reset Company Default Values", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "delete_transactions", + "fieldtype": "Check", + "label": "Delete Transactions", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "initialize_doctypes_table", + "fieldtype": "Check", + "label": "Initialize Summary Table", + "no_copy": 1, + "read_only": 1 + }, + { + "depends_on": "eval: doc.error_log", + "fieldname": "error_log", + "fieldtype": "Long Text", + "label": "Error Log" + }, + { + "fieldname": "section_break_qpwb", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "process_in_single_transaction", + "fieldtype": "Check", + "hidden": 1, + "label": "Process in Single Transaction", + "no_copy": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-08-04 20:15:59.071493", + "modified": "2024-03-21 10:29:19.456413", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -76,5 +167,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index d266285b29a..2b4d0754759 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -1,18 +1,31 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from collections import OrderedDict import frappe from frappe import _, qb from frappe.desk.notifications import clear_notifications from frappe.model.document import Document -from frappe.utils import cint, create_batch +from frappe.utils import cint, comma_and, create_batch, get_link_to_form +from frappe.utils.background_jobs import create_job_id, is_job_enqueued class TransactionDeletionRecord(Document): def __init__(self, *args, **kwargs): - super(TransactionDeletionRecord, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.batch_size = 5000 + # Tasks are listed by their execution order + self.task_to_internal_method_map = OrderedDict( + { + "Delete Bins": "delete_bins", + "Delete Leads and Addresses": "delete_lead_addresses", + "Reset Company Values": "reset_company_values", + "Clear Notifications": "delete_notifications", + "Initialize Summary Table": "initialize_doctypes_to_be_deleted_table", + "Delete Transactions": "delete_company_transactions", + } + ) def validate(self): frappe.only_for("System Manager") @@ -29,104 +42,264 @@ class TransactionDeletionRecord(Document): title=_("Not Allowed"), ) + def generate_job_name_for_task(self, task=None): + method = self.task_to_internal_method_map[task] + return f"{self.name}_{method}" + + def generate_job_name_for_next_tasks(self, task=None): + job_names = [] + current_task_idx = list(self.task_to_internal_method_map).index(task) + for idx, task in enumerate(self.task_to_internal_method_map.keys(), 0): + # generate job_name for next tasks + if idx > current_task_idx: + job_names.append(self.generate_job_name_for_task(task)) + return job_names + + def generate_job_name_for_all_tasks(self): + job_names = [] + for task in self.task_to_internal_method_map.keys(): + job_names.append(self.generate_job_name_for_task(task)) + return job_names + def before_submit(self): + if queued_docs := frappe.db.get_all( + "Transaction Deletion Record", + filters={"company": self.company, "status": ("in", ["Running", "Queued"]), "docstatus": 1}, + pluck="name", + ): + frappe.throw( + _( + "Cannot enqueue multi docs for one company. {0} is already queued/running for company: {1}" + ).format( + comma_and([get_link_to_form("Transaction Deletion Record", x) for x in queued_docs]), + frappe.bold(self.company), + ) + ) + if not self.doctypes_to_be_ignored: self.populate_doctypes_to_be_ignored_table() - self.delete_bins() - self.delete_lead_addresses() - self.reset_company_values() - clear_notifications() - self.delete_company_transactions() + def reset_task_flags(self): + self.clear_notifications = 0 + self.delete_bin_data = 0 + self.delete_leads_and_addresses = 0 + self.delete_transactions = 0 + self.initialize_doctypes_table = 0 + self.reset_company_default_values = 0 + + def before_save(self): + self.status = "" + self.doctypes.clear() + self.reset_task_flags() + + def on_submit(self): + self.db_set("status", "Queued") + + def on_cancel(self): + self.db_set("status", "Cancelled") + + def enqueue_task(self, task: str | None = None): + if task and task in self.task_to_internal_method_map: + # make sure that none of next tasks are already running + job_names = self.generate_job_name_for_next_tasks(task=task) + self.validate_running_task_for_doc(job_names=job_names) + + # Generate Job Id to uniquely identify each task for this document + job_id = self.generate_job_name_for_task(task) + + if self.process_in_single_transaction: + self.execute_task(task_to_execute=task) + else: + frappe.enqueue( + "frappe.utils.background_jobs.run_doc_method", + doctype=self.doctype, + name=self.name, + doc_method="execute_task", + job_id=job_id, + queue="long", + enqueue_after_commit=True, + task_to_execute=task, + ) + + def execute_task(self, task_to_execute: str | None = None): + if task_to_execute: + method = self.task_to_internal_method_map[task_to_execute] + if task := getattr(self, method, None): + try: + task() + except Exception: + frappe.db.rollback() + traceback = frappe.get_traceback(with_context=True) + if traceback: + message = "Traceback:
              " + traceback + frappe.db.set_value(self.doctype, self.name, "error_log", message) + frappe.db.set_value(self.doctype, self.name, "status", "Failed") + + def delete_notifications(self): + self.validate_doc_status() + if not self.clear_notifications: + clear_notifications() + self.db_set("clear_notifications", 1) + self.enqueue_task(task="Initialize Summary Table") def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in doctypes_to_be_ignored_list: self.append("doctypes_to_be_ignored", {"doctype_name": doctype}) + def validate_running_task_for_doc(self, job_names: list | None = None): + # at most only one task should be runnning + running_tasks = [] + for x in job_names: + if is_job_enqueued(x): + running_tasks.append(create_job_id(x)) + + if running_tasks: + frappe.throw( + _("{0} is already running for {1}").format( + comma_and([get_link_to_form("RQ Job", x) for x in running_tasks]), self.name + ) + ) + + def validate_doc_status(self): + if self.status != "Running": + frappe.throw( + _("{0} is not running. Cannot trigger events for this Document").format( + get_link_to_form("Transaction Deletion Record", self.name) + ) + ) + + @frappe.whitelist() + def start_deletion_tasks(self): + # This method is the entry point for the chain of events that follow + self.db_set("status", "Running") + self.enqueue_task(task="Delete Bins") + def delete_bins(self): - frappe.db.sql( - """delete from `tabBin` where warehouse in - (select name from tabWarehouse where company=%s)""", - self.company, - ) + self.validate_doc_status() + if not self.delete_bin_data: + frappe.db.sql( + """delete from `tabBin` where warehouse in + (select name from tabWarehouse where company=%s)""", + self.company, + ) + self.db_set("delete_bin_data", 1) + self.enqueue_task(task="Delete Leads and Addresses") def delete_lead_addresses(self): """Delete addresses to which leads are linked""" - leads = frappe.get_all("Lead", filters={"company": self.company}) - leads = ["'%s'" % row.get("name") for row in leads] - addresses = [] - if leads: - addresses = frappe.db.sql_list( - """select parent from `tabDynamic Link` where link_name - in ({leads})""".format( - leads=",".join(leads) + self.validate_doc_status() + if not self.delete_leads_and_addresses: + leads = frappe.get_all("Lead", filters={"company": self.company}) + leads = ["'%s'" % row.get("name") for row in leads] + addresses = [] + if leads: + addresses = frappe.db.sql_list( + """select parent from `tabDynamic Link` where link_name + in ({leads})""".format(leads=",".join(leads)) ) - ) - if addresses: - addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] + if addresses: + addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] - frappe.db.sql( - """delete from `tabAddress` where name in ({addresses}) and - name not in (select distinct dl1.parent from `tabDynamic Link` dl1 - inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent - and dl1.link_doctype<>dl2.link_doctype)""".format( - addresses=",".join(addresses) + frappe.db.sql( + """delete from `tabAddress` where name in ({addresses}) and + name not in (select distinct dl1.parent from `tabDynamic Link` dl1 + inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent + and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)) + ) + + frappe.db.sql( + """delete from `tabDynamic Link` where link_doctype='Lead' + and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)) ) - ) frappe.db.sql( - """delete from `tabDynamic Link` where link_doctype='Lead' - and parenttype='Address' and link_name in ({leads})""".format( + """update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format( leads=",".join(leads) ) ) - - frappe.db.sql( - """update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format( - leads=",".join(leads) - ) - ) + self.db_set("delete_leads_and_addresses", 1) + self.enqueue_task(task="Reset Company Values") def reset_company_values(self): - company_obj = frappe.get_doc("Company", self.company) - company_obj.total_monthly_sales = 0 - company_obj.sales_monthly_history = None - company_obj.save() + self.validate_doc_status() + if not self.reset_company_default_values: + company_obj = frappe.get_doc("Company", self.company) + company_obj.total_monthly_sales = 0 + company_obj.sales_monthly_history = None + company_obj.save() + self.db_set("reset_company_default_values", 1) + self.enqueue_task(task="Clear Notifications") + + def initialize_doctypes_to_be_deleted_table(self): + self.validate_doc_status() + if not self.initialize_doctypes_table: + doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() + docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) + tables = self.get_all_child_doctypes() + for docfield in docfields: + if docfield["parent"] != self.doctype: + no_of_docs = self.get_number_of_docs_linked_with_specified_company( + docfield["parent"], docfield["fieldname"] + ) + if no_of_docs > 0: + # Initialize + self.populate_doctypes_table(tables, docfield["parent"], docfield["fieldname"], 0) + self.db_set("initialize_doctypes_table", 1) + self.enqueue_task(task="Delete Transactions") def delete_company_transactions(self): - doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() - docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) + self.validate_doc_status() + if not self.delete_transactions: + doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() + self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) - tables = self.get_all_child_doctypes() - for docfield in docfields: - if docfield["parent"] != self.doctype: - no_of_docs = self.get_number_of_docs_linked_with_specified_company( - docfield["parent"], docfield["fieldname"] - ) - - if no_of_docs > 0: - self.delete_version_log(docfield["parent"], docfield["fieldname"]) - - reference_docs = frappe.get_all( - docfield["parent"], filters={docfield["fieldname"]: self.company} + self.get_all_child_doctypes() + for docfield in self.doctypes: + if docfield.doctype_name != self.doctype and not docfield.done: + no_of_docs = self.get_number_of_docs_linked_with_specified_company( + docfield.doctype_name, docfield.docfield_name ) - reference_doc_names = [r.name for r in reference_docs] + if no_of_docs > 0: + reference_docs = frappe.get_all( + docfield.doctype_name, + filters={docfield.docfield_name: self.company}, + limit=self.batch_size, + ) + reference_doc_names = [r.name for r in reference_docs] - self.delete_communications(docfield["parent"], reference_doc_names) - self.delete_comments(docfield["parent"], reference_doc_names) - self.unlink_attachments(docfield["parent"], reference_doc_names) + self.delete_version_log(docfield.doctype_name, reference_doc_names) + self.delete_communications(docfield.doctype_name, reference_doc_names) + self.delete_comments(docfield.doctype_name, reference_doc_names) + self.unlink_attachments(docfield.doctype_name, reference_doc_names) + self.delete_child_tables(docfield.doctype_name, reference_doc_names) + self.delete_docs_linked_with_specified_company( + docfield.doctype_name, reference_doc_names + ) + processed = int(docfield.no_of_docs) + len(reference_doc_names) + frappe.db.set_value(docfield.doctype, docfield.name, "no_of_docs", processed) + else: + # reset naming series + naming_series = frappe.db.get_value("DocType", docfield.doctype_name, "autoname") + if naming_series: + if "#" in naming_series: + self.update_naming_series(naming_series, docfield.doctype_name) + frappe.db.set_value(docfield.doctype, docfield.name, "done", 1) - self.populate_doctypes_table(tables, docfield["parent"], no_of_docs) - - self.delete_child_tables(docfield["parent"], docfield["fieldname"]) - self.delete_docs_linked_with_specified_company(docfield["parent"], docfield["fieldname"]) - - naming_series = frappe.db.get_value("DocType", docfield["parent"], "autoname") - if naming_series: - if "#" in naming_series: - self.update_naming_series(naming_series, docfield["parent"]) + pending_doctypes = frappe.db.get_all( + "Transaction Deletion Record Details", + filters={"parent": self.name, "done": 0}, + pluck="doctype_name", + ) + if pending_doctypes: + # as method is enqueued after commit, calling itself will not make validate_doc_status to throw + # recursively call this task to delete all transactions + self.enqueue_task(task="Delete Transactions") + else: + self.db_set("status", "Completed") + self.db_set("delete_transactions", 1) + self.db_set("error_log", None) def get_doctypes_to_be_ignored_list(self): singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name") @@ -155,25 +328,24 @@ class TransactionDeletionRecord(Document): def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname): return frappe.db.count(doctype, {company_fieldname: self.company}) - def populate_doctypes_table(self, tables, doctype, no_of_docs): + def populate_doctypes_table(self, tables, doctype, fieldname, no_of_docs): + self.flags.ignore_validate_update_after_submit = True if doctype not in tables: - self.append("doctypes", {"doctype_name": doctype, "no_of_docs": no_of_docs}) - - def delete_child_tables(self, doctype, company_fieldname): - parent_docs_to_be_deleted = frappe.get_all( - doctype, {company_fieldname: self.company}, pluck="name" - ) + self.append( + "doctypes", {"doctype_name": doctype, "docfield_name": fieldname, "no_of_docs": no_of_docs} + ) + self.save(ignore_permissions=True) + def delete_child_tables(self, doctype, reference_doc_names): child_tables = frappe.get_all( "DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options" ) - for batch in create_batch(parent_docs_to_be_deleted, self.batch_size): - for table in child_tables: - frappe.db.delete(table, {"parent": ["in", batch]}) + for table in child_tables: + frappe.db.delete(table, {"parent": ["in", reference_doc_names]}) - def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): - frappe.db.delete(doctype, {company_fieldname: self.company}) + def delete_docs_linked_with_specified_company(self, doctype, reference_doc_names): + frappe.db.delete(doctype, {"name": ("in", reference_doc_names)}) def update_naming_series(self, naming_series, doctype_name): if "." in naming_series: @@ -181,10 +353,8 @@ class TransactionDeletionRecord(Document): else: prefix, hashes = naming_series.rsplit("{", 1) last = frappe.db.sql( - """select max(name) from `tab{0}` - where name like %s""".format( - doctype_name - ), + f"""select max(name) from `tab{doctype_name}` + where name like %s""", prefix + "%", ) if last and last[0][0]: @@ -194,17 +364,11 @@ class TransactionDeletionRecord(Document): frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix)) - def delete_version_log(self, doctype, company_fieldname): - dt = qb.DocType(doctype) - names = qb.from_(dt).select(dt.name).where(dt[company_fieldname] == self.company).run(as_list=1) - names = [x[0] for x in names] - - if names: - versions = qb.DocType("Version") - for batch in create_batch(names, self.batch_size): - qb.from_(versions).delete().where( - (versions.ref_doctype == doctype) & (versions.docname.isin(batch)) - ).run() + def delete_version_log(self, doctype, docnames): + versions = qb.DocType("Version") + qb.from_(versions).delete().where( + (versions.ref_doctype == doctype) & (versions.docname.isin(docnames)) + ).run() def delete_communications(self, doctype, reference_doc_names): communications = frappe.get_all( @@ -220,17 +384,11 @@ class TransactionDeletionRecord(Document): frappe.delete_doc("Communication", batch, ignore_permissions=True) def delete_comments(self, doctype, reference_doc_names): - comments = frappe.get_all( - "Comment", - filters={"reference_doctype": doctype, "reference_name": ["in", reference_doc_names]}, - ) - comment_names = [c.name for c in comments] - - if not comment_names: - return - - for batch in create_batch(comment_names, self.batch_size): - frappe.delete_doc("Comment", batch, ignore_permissions=True) + if reference_doc_names: + comment = qb.DocType("Comment") + qb.from_(comment).delete().where( + (comment.reference_doctype == doctype) & (comment.reference_name.isin(reference_doc_names)) + ).run() def unlink_attachments(self, doctype, reference_doc_names): files = frappe.get_all( @@ -267,6 +425,7 @@ def get_doctypes_to_be_ignored(): "Bank Account", "Item Tax Template", "Mode of Payment", + "Mode of Payment Account", "Item Default", "Customer", "Supplier", @@ -275,3 +434,29 @@ def get_doctypes_to_be_ignored(): doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or []) return doctypes_to_be_ignored + + +@frappe.whitelist() +def is_deletion_doc_running(company: str | None = None, err_msg: str | None = None): + if company: + if running_deletion_jobs := frappe.db.get_all( + "Transaction Deletion Record", + filters={"docstatus": 1, "company": company, "status": "Running"}, + ): + if not err_msg: + err_msg = "" + frappe.throw( + title=_("Deletion in Progress!"), + msg=_("Transaction Deletion Document: {0} is running for this Company. {1}").format( + get_link_to_form("Transaction Deletion Record", running_deletion_jobs[0].name), err_msg + ), + ) + + +def check_for_running_deletion_job(doc, method=None): + # Check if DocType has 'company' field + df = qb.DocType("DocField") + if qb.from_(df).select(df.parent).where((df.fieldname == "company") & (df.parent == doc.doctype)).run(): + is_deletion_doc_running( + doc.company, _("Cannot make any transactions until the deletion job is completed") + ) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js index c238f18abad..285cb6dd221 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js @@ -1,12 +1,16 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.listview_settings['Transaction Deletion Record'] = { - get_indicator: function(doc) { - if (doc.docstatus == 0) { - return [__("Draft"), "red"]; - } else { - return [__("Completed"), "green"]; - } - } +frappe.listview_settings["Transaction Deletion Record"] = { + add_fields: ["status"], + get_indicator: function (doc) { + let colors = { + Queued: "orange", + Completed: "green", + Running: "blue", + Failed: "red", + }; + let status = doc.status; + return [__(status), colors[status], "status,=," + status]; + }, }; diff --git a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json index be0be945c4e..89db63694c2 100644 --- a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json +++ b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json @@ -5,8 +5,7 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "doctype_name", - "no_of_docs" + "doctype_name" ], "fields": [ { @@ -16,18 +15,12 @@ "label": "DocType", "options": "DocType", "reqd": 1 - }, - { - "fieldname": "no_of_docs", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Number of Docs" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-08 23:10:46.166744", + "modified": "2024-02-04 10:56:27.413691", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record Item", @@ -35,5 +28,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/uom/uom.js b/erpnext/setup/doctype/uom/uom.js index 3680906057f..273e30fd197 100644 --- a/erpnext/setup/doctype/uom/uom.js +++ b/erpnext/setup/doctype/uom/uom.js @@ -1,13 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - - //--------- ONLOAD ------------- -cur_frm.cscript.onload = function(doc, cdt, cdn) { +cur_frm.cscript.onload = function (doc, cdt, cdn) {}; -} - -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} +cur_frm.cscript.refresh = function (doc, cdt, cdn) {}; diff --git a/erpnext/setup/doctype/uom_conversion_factor/uom_conversion_factor.js b/erpnext/setup/doctype/uom_conversion_factor/uom_conversion_factor.js index e734d83500f..e975729dca7 100644 --- a/erpnext/setup/doctype/uom_conversion_factor/uom_conversion_factor.js +++ b/erpnext/setup/doctype/uom_conversion_factor/uom_conversion_factor.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('UOM Conversion Factor', { - refresh: function() { - - } +frappe.ui.form.on("UOM Conversion Factor", { + refresh: function () {}, }); diff --git a/erpnext/setup/doctype/vehicle/vehicle.js b/erpnext/setup/doctype/vehicle/vehicle.js index f12c4346852..9d598a9883b 100644 --- a/erpnext/setup/doctype/vehicle/vehicle.js +++ b/erpnext/setup/doctype/vehicle/vehicle.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Vehicle', { - refresh: function(frm) { - - } +frappe.ui.form.on("Vehicle", { + refresh: function (frm) {}, }); diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 71b1ca7c058..2e26117ef33 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -19,7 +19,9 @@ default_mail_footer = """
              ').appendTo(wrapper); parent.html(frappe.render_template("welcome_to_erpnext", {})); - parent.find(".video-placeholder").on("click", function() { + parent.find(".video-placeholder").on("click", function () { window.erpnext_welcome_video_started = true; parent.find(".video-placeholder").addClass("hidden"); - parent.find(".embed-responsive").append('') + parent + .find(".embed-responsive") + .append( + '' + ); }); // pause video on page change - $(document).on("page-change", function() { + $(document).on("page-change", function () { if (window.erpnext_welcome_video_started && parent) { - parent.find(".video-playlist").each(function() { - this.contentWindow.postMessage('{"event":"command","func":"' + 'pauseVideo' + '","args":""}', '*'); + parent.find(".video-playlist").each(function () { + this.contentWindow.postMessage( + '{"event":"command","func":"' + "pauseVideo" + '","args":""}', + "*" + ); }); } }); -} +}; diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py index eed8f73cb48..daceafa94b6 100644 --- a/erpnext/setup/setup_wizard/operations/defaults_setup.py +++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py @@ -30,9 +30,7 @@ def set_default_settings(args): stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value( - "Warehouse", {"warehouse_name": _("Stores")} - ) + stock_settings.default_warehouse = frappe.db.get_value("Warehouse", {"warehouse_name": _("Stores")}) stock_settings.stock_uom = _("Nos") stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 081a51573e0..0fe2b56a944 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -276,9 +276,7 @@ def install(country=None): records += [{"doctype": doctype, title_field: title} for title in read_lines(filename)] base_path = frappe.get_app_path("erpnext", "stock", "doctype") - response = frappe.read_file( - os.path.join(base_path, "delivery_trip/dispatch_notification_template.html") - ) + response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html")) records += [ { @@ -478,9 +476,7 @@ def update_stock_settings(): stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value( - "Warehouse", {"warehouse_name": _("Stores")} - ) + stock_settings.default_warehouse = frappe.db.get_value("Warehouse", {"warehouse_name": _("Stores")}) stock_settings.stock_uom = _("Nos") stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 49ba78c63a4..14fcb800ae7 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -2,8 +2,8 @@ # License: GNU General Public License v3. See license.txt -import os import json +import os import frappe from frappe import _ @@ -14,7 +14,7 @@ def setup_taxes_and_charges(company_name: str, country: str): frappe.throw(_("Company {} does not exist yet. Taxes setup aborted.").format(company_name)) file_path = os.path.join(os.path.dirname(__file__), "..", "data", "country_wise_tax.json") - with open(file_path, "r") as json_file: + with open(file_path) as json_file: tax_data = json.load(json_file) country_wise_tax = tax_data.get(country) @@ -54,7 +54,12 @@ def simple_to_detailed(templates): { "title": title, "taxes": [ - {"tax_type": {"account_name": data.get("account_name"), "tax_rate": data.get("tax_rate")}} + { + "tax_type": { + "account_name": data.get("account_name"), + "tax_rate": data.get("tax_rate"), + } + } ], } for title, data in templates.items() @@ -110,11 +115,9 @@ def update_regional_tax_settings(country, company): path = frappe.get_app_path("erpnext", "regional", frappe.scrub(country)) if os.path.exists(path.encode("utf-8")): try: - module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format( - frappe.scrub(country) - ) + module_name = f"erpnext.regional.{frappe.scrub(country)}.setup.update_regional_tax_settings" frappe.get_attr(module_name)(country, company) - except Exception as e: + except Exception: # Log error and ignore if failed to setup regional tax settings frappe.log_error("Unable to setup regional tax settings") pass @@ -140,7 +143,7 @@ def make_taxes_and_charges_template(company_name, doctype, template): # if account_head is a dict, search or create the account and get it's name if isinstance(account_data, dict): - tax_row_defaults["description"] = "{0} @ {1}".format( + tax_row_defaults["description"] = "{} @ {}".format( account_data.get("account_name"), account_data.get("tax_rate") ) tax_row_defaults["rate"] = account_data.get("tax_rate") diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index bab57fe267a..705fb1f2fcf 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.utils import add_days, flt, get_datetime_str, nowdate from frappe.utils.data import now_datetime -from frappe.utils.nestedset import get_ancestors_of, get_root_of # noqa +from frappe.utils.nestedset import get_root_of from erpnext import get_default_company @@ -81,14 +81,12 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if entries: return flt(entries[0].exchange_rate) - if frappe.get_cached_value( - "Currency Exchange Settings", "Currency Exchange Settings", "disabled" - ): + if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"): return 0.00 try: cache = frappe.cache() - key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency) + key = f"currency_exchange_rate_{transaction_date}:{from_currency}:{to_currency}" value = cache.get(key) if not value: diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index bc4b6141bf0..6d101b7e6ec 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -15,9 +15,7 @@ def boot_session(bootinfo): update_page_info(bootinfo) bootinfo.sysdefaults.territory = frappe.db.get_single_value("Selling Settings", "territory") - bootinfo.sysdefaults.customer_group = frappe.db.get_single_value( - "Selling Settings", "customer_group" - ) + bootinfo.sysdefaults.customer_group = frappe.db.get_single_value("Selling Settings", "customer_group") bootinfo.sysdefaults.allow_stale = cint( frappe.db.get_single_value("Accounts Settings", "allow_stale") ) @@ -30,9 +28,7 @@ def boot_session(bootinfo): ) bootinfo.sysdefaults.allow_sales_order_creation_for_expired_quotation = cint( - frappe.db.get_single_value( - "Selling Settings", "allow_sales_order_creation_for_expired_quotation" - ) + frappe.db.get_single_value("Selling Settings", "allow_sales_order_creation_for_expired_quotation") ) # if no company, show a dialog box to create a new company @@ -56,9 +52,7 @@ def boot_session(bootinfo): update={"doctype": ":Company"}, ) - party_account_types = frappe.db.sql( - """ select name, ifnull(account_type, '') from `tabParty Type`""" - ) + party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""") bootinfo.party_account_types = frappe._dict(party_account_types) diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index 5a60d2ff967..06f61fd961e 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -93,7 +93,7 @@ def get_all_items(date_range, company, field, limit=None): select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)" results = frappe.db.get_all( "Bin", - fields=["item_code as name", "{0} as value".format(select_field)], + fields=["item_code as name", f"{select_field} as value"], group_by="item_code", order_by="value desc", limit=limit, @@ -224,9 +224,7 @@ def get_date_condition(date_range, field): if date_range: date_range = frappe.parse_json(date_range) from_date, to_date = date_range - date_condition = "and {0} between {1} and {2}".format( - field, frappe.db.escape(from_date), frappe.db.escape(to_date) - ) + date_condition = f"and {field} between {frappe.db.escape(from_date)} and {frappe.db.escape(to_date)}" return date_condition diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py index 45bf012be85..242bdcf8b55 100644 --- a/erpnext/stock/__init__.py +++ b/erpnext/stock/__init__.py @@ -17,9 +17,9 @@ install_docs = [ def get_warehouse_account_map(company=None): - company_warehouse_account_map = company and frappe.flags.setdefault( - "warehouse_account_map", {} - ).get(company) + company_warehouse_account_map = company and frappe.flags.setdefault("warehouse_account_map", {}).get( + company + ) warehouse_account_map = frappe.flags.warehouse_account_map if not warehouse_account_map or not company_warehouse_account_map or frappe.flags.in_test: diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index bef438f9fd7..29db6e3c01d 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -1,4 +1,4 @@ -frappe.provide('erpnext.stock'); +frappe.provide("erpnext.stock"); erpnext.stock.ItemDashboard = class ItemDashboard { constructor(opts) { @@ -9,46 +9,51 @@ erpnext.stock.ItemDashboard = class ItemDashboard { var me = this; this.start = 0; if (!this.sort_by) { - this.sort_by = 'projected_qty'; - this.sort_order = 'asc'; + this.sort_by = "projected_qty"; + this.sort_order = "asc"; } - this.content = $(frappe.render_template('item_dashboard')).appendTo(this.parent); - this.result = this.content.find('.result'); + this.content = $(frappe.render_template("item_dashboard")).appendTo(this.parent); + this.result = this.content.find(".result"); - this.content.on('click', '.btn-move', function () { + this.content.on("click", ".btn-move", function () { handle_move_add($(this), "Move"); }); - this.content.on('click', '.btn-add', function () { + this.content.on("click", ".btn-add", function () { handle_move_add($(this), "Add"); }); - this.content.on('click', '.btn-edit', function () { - let item = unescape($(this).attr('data-item')); - let warehouse = unescape($(this).attr('data-warehouse')); - let company = unescape($(this).attr('data-company')); - frappe.db.get_value('Putaway Rule', { - 'item_code': item, - 'warehouse': warehouse, - 'company': company - }, 'name', (r) => { - frappe.set_route("Form", "Putaway Rule", r.name); - }); + this.content.on("click", ".btn-edit", function () { + let item = unescape($(this).attr("data-item")); + let warehouse = unescape($(this).attr("data-warehouse")); + let company = unescape($(this).attr("data-company")); + frappe.db.get_value( + "Putaway Rule", + { + item_code: item, + warehouse: warehouse, + company: company, + }, + "name", + (r) => { + frappe.set_route("Form", "Putaway Rule", r.name); + } + ); }); function handle_move_add(element, action) { - let item = unescape(element.attr('data-item')); - let warehouse = unescape(element.attr('data-warehouse')); - let actual_qty = unescape(element.attr('data-actual_qty')); - let disable_quick_entry = Number(unescape(element.attr('data-disable_quick_entry'))); + let item = unescape(element.attr("data-item")); + let warehouse = unescape(element.attr("data-warehouse")); + let actual_qty = unescape(element.attr("data-actual_qty")); + let disable_quick_entry = Number(unescape(element.attr("data-disable_quick_entry"))); let entry_type = action === "Move" ? "Material Transfer" : "Material Receipt"; if (disable_quick_entry) { open_stock_entry(item, warehouse, entry_type); } else { if (action === "Add") { - let rate = unescape($(this).attr('data-rate')); + let rate = unescape($(this).attr("data-rate")); erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function () { me.refresh(); }); @@ -61,35 +66,33 @@ erpnext.stock.ItemDashboard = class ItemDashboard { } function open_stock_entry(item, warehouse, entry_type) { - frappe.model.with_doctype('Stock Entry', function () { - var doc = frappe.model.get_new_doc('Stock Entry'); + frappe.model.with_doctype("Stock Entry", function () { + var doc = frappe.model.get_new_doc("Stock Entry"); if (entry_type) { doc.stock_entry_type = entry_type; } - var row = frappe.model.add_child(doc, 'items'); + var row = frappe.model.add_child(doc, "items"); row.item_code = item; if (entry_type === "Material Transfer") { row.s_warehouse = warehouse; - } - else { + } else { row.t_warehouse = warehouse; } - frappe.set_route('Form', doc.doctype, doc.name); + frappe.set_route("Form", doc.doctype, doc.name); }); } // more - this.content.find('.btn-more').on('click', function () { + this.content.find(".btn-more").on("click", function () { me.start += me.page_length; me.refresh(); }); - } refresh() { - if(this.before_refresh) { + if (this.before_refresh) { this.before_refresh(); } @@ -101,7 +104,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard { company: this.company, start: this.start, sort_by: this.sort_by, - sort_order: this.sort_order + sort_order: this.sort_order, }; var me = this; @@ -110,11 +113,11 @@ erpnext.stock.ItemDashboard = class ItemDashboard { args: args, callback: function (r) { me.render(r.message); - } + }, }); } render(data) { - if (this.start===0) { + if (this.start === 0) { this.max_count = 0; this.result.empty(); } @@ -129,22 +132,22 @@ erpnext.stock.ItemDashboard = class ItemDashboard { this.max_count = this.max_count; // show more button - if (data && data.length === (this.page_length + 1)) { - this.content.find('.more').removeClass('hidden'); + if (data && data.length === this.page_length + 1) { + this.content.find(".more").removeClass("hidden"); // remove the last element data.splice(-1); } else { - this.content.find('.more').addClass('hidden'); + this.content.find(".more").addClass("hidden"); } // If not any stock in any warehouses provide a message to end user if (context.data.length > 0) { - this.content.find('.result').css('text-align', 'unset'); + this.content.find(".result").css("text-align", "unset"); $(frappe.render_template(this.template, context)).appendTo(this.result); } else { var message = __("No Stock Available Currently"); - this.content.find('.result').css('text-align', 'center'); + this.content.find(".result").css("text-align", "center"); $(`
              ${message}
              `).appendTo(this.result); @@ -152,19 +155,23 @@ erpnext.stock.ItemDashboard = class ItemDashboard { } get_item_dashboard_data(data, max_count, show_item) { - if(!max_count) max_count = 0; - if(!data) data = []; + if (!max_count) max_count = 0; + if (!data) data = []; data.forEach(function (d) { - d.actual_or_pending = d.projected_qty + d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract; + d.actual_or_pending = + d.projected_qty + + d.reserved_qty + + d.reserved_qty_for_production + + d.reserved_qty_for_sub_contract; d.pending_qty = 0; - d.total_reserved = d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract; + d.total_reserved = + d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract; if (d.actual_or_pending > d.actual_qty) { d.pending_qty = d.actual_or_pending - d.actual_qty; } - max_count = Math.max(d.actual_or_pending, d.actual_qty, - d.total_reserved, max_count); + max_count = Math.max(d.actual_or_pending, d.actual_qty, d.total_reserved, max_count); }); let can_write = 0; @@ -176,7 +183,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard { data: data, max_count: max_count, can_write: can_write, - show_item: show_item || false + show_item: show_item || false, }; } @@ -201,73 +208,74 @@ erpnext.stock.ItemDashboard = class ItemDashboard { erpnext.stock.move_item = function (item, source, target, actual_qty, rate, callback) { var dialog = new frappe.ui.Dialog({ - title: target ? __('Add Item') : __('Move Item'), - fields: [{ - fieldname: 'item_code', - label: __('Item'), - fieldtype: 'Link', - options: 'Item', - read_only: 1 - }, - { - fieldname: 'source', - label: __('Source Warehouse'), - fieldtype: 'Link', - options: 'Warehouse', - read_only: 1 - }, - { - fieldname: 'target', - label: __('Target Warehouse'), - fieldtype: 'Link', - options: 'Warehouse', - reqd: 1, - get_query() { - return { - filters: { - is_group: 0 - } - } - } - }, - { - fieldname: 'qty', - label: __('Quantity'), - reqd: 1, - fieldtype: 'Float', - description: __('Available {0}', [actual_qty]) - }, - { - fieldname: 'rate', - label: __('Rate'), - fieldtype: 'Currency', - hidden: 1 - }, + title: target ? __("Add Item") : __("Move Item"), + fields: [ + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + read_only: 1, + }, + { + fieldname: "source", + label: __("Source Warehouse"), + fieldtype: "Link", + options: "Warehouse", + read_only: 1, + }, + { + fieldname: "target", + label: __("Target Warehouse"), + fieldtype: "Link", + options: "Warehouse", + reqd: 1, + get_query() { + return { + filters: { + is_group: 0, + }, + }; + }, + }, + { + fieldname: "qty", + label: __("Quantity"), + reqd: 1, + fieldtype: "Float", + description: __("Available {0}", [actual_qty]), + }, + { + fieldname: "rate", + label: __("Rate"), + fieldtype: "Currency", + hidden: 1, + }, ], }); dialog.show(); - dialog.get_field('item_code').set_input(item); + dialog.get_field("item_code").set_input(item); if (source) { - dialog.get_field('source').set_input(source); + dialog.get_field("source").set_input(source); } else { - dialog.get_field('source').df.hidden = 1; - dialog.get_field('source').refresh(); + dialog.get_field("source").df.hidden = 1; + dialog.get_field("source").refresh(); } if (rate) { - dialog.get_field('rate').set_value(rate); - dialog.get_field('rate').df.hidden = 0; - dialog.get_field('rate').refresh(); + dialog.get_field("rate").set_value(rate); + dialog.get_field("rate").df.hidden = 0; + dialog.get_field("rate").refresh(); } if (target) { - dialog.get_field('target').df.read_only = 1; - dialog.get_field('target').value = target; - dialog.get_field('target').refresh(); + dialog.get_field("target").df.read_only = 1; + dialog.get_field("target").value = target; + dialog.get_field("target").refresh(); } - dialog.set_primary_action(__('Create Stock Entry'), function () { + dialog.set_primary_action(__("Create Stock Entry"), function () { if (source && (dialog.get_value("qty") == 0 || dialog.get_value("qty") > actual_qty)) { frappe.msgprint(__("Quantity must be greater than zero, and less or equal to {0}", [actual_qty])); return; @@ -278,20 +286,20 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call return; } - frappe.model.with_doctype('Stock Entry', function () { - let doc = frappe.model.get_new_doc('Stock Entry'); - doc.from_warehouse = dialog.get_value('source'); - doc.to_warehouse = dialog.get_value('target'); + frappe.model.with_doctype("Stock Entry", function () { + let doc = frappe.model.get_new_doc("Stock Entry"); + doc.from_warehouse = dialog.get_value("source"); + doc.to_warehouse = dialog.get_value("target"); doc.stock_entry_type = doc.from_warehouse ? "Material Transfer" : "Material Receipt"; - let row = frappe.model.add_child(doc, 'items'); - row.item_code = dialog.get_value('item_code'); - row.s_warehouse = dialog.get_value('source'); - row.t_warehouse = dialog.get_value('target'); - row.qty = dialog.get_value('qty'); + let row = frappe.model.add_child(doc, "items"); + row.item_code = dialog.get_value("item_code"); + row.s_warehouse = dialog.get_value("source"); + row.t_warehouse = dialog.get_value("target"); + row.qty = dialog.get_value("qty"); row.conversion_factor = 1; - row.transfer_qty = dialog.get_value('qty'); - row.basic_rate = dialog.get_value('rate'); - frappe.set_route('Form', doc.doctype, doc.name); + row.transfer_qty = dialog.get_value("qty"); + row.basic_rate = dialog.get_value("rate"); + frappe.set_route("Form", doc.doctype, doc.name); }); }); }; diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js index 2b9d46e4ab0..a550174d656 100644 --- a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js @@ -1,4 +1,4 @@ -frappe.provide('frappe.dashboards.chart_sources'); +frappe.provide("frappe.dashboards.chart_sources"); frappe.dashboards.chart_sources["Warehouse wise Stock Value"] = { method: "erpnext.stock.dashboard_chart_source.warehouse_wise_stock_value.warehouse_wise_stock_value.get", @@ -8,7 +8,7 @@ frappe.dashboards.chart_sources["Warehouse wise Stock Value"] = { label: __("Company"), fieldtype: "Link", options: "Company", - default: frappe.defaults.get_user_default("Company") - } - ] + default: frappe.defaults.get_user_default("Company"), + }, + ], }; diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py index d488150eef3..cbc4fc76ecf 100644 --- a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py @@ -27,9 +27,7 @@ def get( if filters and filters.get("company"): warehouse_filters.append(["company", "=", filters.get("company")]) - warehouses = frappe.get_list( - "Warehouse", pluck="name", filters=warehouse_filters, order_by="name" - ) + warehouses = frappe.get_list("Warehouse", pluck="name", filters=warehouse_filters, order_by="name") warehouses = frappe.get_list( "Bin", diff --git a/erpnext/stock/doctype/batch/batch.js b/erpnext/stock/doctype/batch/batch.js index 3b07e4e80c1..fc160fa3b65 100644 --- a/erpnext/stock/doctype/batch/batch.js +++ b/erpnext/stock/doctype/batch/batch.js @@ -1,62 +1,72 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Batch', { +frappe.ui.form.on("Batch", { setup: (frm) => { - frm.fields_dict['item'].get_query = function(doc, cdt, cdn) { + frm.fields_dict["item"].get_query = function (doc, cdt, cdn) { return { query: "erpnext.controllers.queries.item_query", - filters:{ - 'is_stock_item': 1, - 'has_batch_no': 1 - } - } - } + filters: { + is_stock_item: 1, + has_batch_no: 1, + }, + }; + }; }, refresh: (frm) => { - if(!frm.is_new()) { + if (!frm.is_new()) { frm.add_custom_button(__("View Ledger"), () => { frappe.route_options = { - batch_no: frm.doc.name + batch_no: frm.doc.name, }; frappe.set_route("query-report", "Stock Ledger"); }); - frm.trigger('make_dashboard'); + frm.trigger("make_dashboard"); } }, item: (frm) => { // frappe.db.get_value('Item', {name: frm.doc.item}, 'has_expiry_date', (r) => { // frm.toggle_reqd('expiry_date', r.has_expiry_date); // }); - frappe.db.get_value('Item', {name: frm.doc.item}, ['shelf_life_in_days', 'has_expiry_date'], (r) => { - if (r.has_expiry_date && r.shelf_life_in_days) { - // Calculate expiry date based on shelf_life_in_days - frm.set_value('expiry_date', frappe.datetime.add_days(frm.doc.manufacturing_date, r.shelf_life_in_days)); - }else if(r.has_expiry_date){ - frm.toggle_reqd('expiry_date', r.has_expiry_date); + frappe.db.get_value( + "Item", + { name: frm.doc.item }, + ["shelf_life_in_days", "has_expiry_date"], + (r) => { + if (r.has_expiry_date && r.shelf_life_in_days) { + // Calculate expiry date based on shelf_life_in_days + frm.set_value( + "expiry_date", + frappe.datetime.add_days(frm.doc.manufacturing_date, r.shelf_life_in_days) + ); + } else if (r.has_expiry_date) { + frm.toggle_reqd("expiry_date", r.has_expiry_date); + } } - }) + ); }, make_dashboard: (frm) => { - if(!frm.is_new()) { + if (!frm.is_new()) { frappe.call({ - method: 'erpnext.stock.doctype.batch.batch.get_batch_qty', - args: {batch_no: frm.doc.name}, + method: "erpnext.stock.doctype.batch.batch.get_batch_qty", + args: { batch_no: frm.doc.name }, callback: (r) => { - if(!r.message) { + if (!r.message) { return; } - const section = frm.dashboard.add_section('', __("Stock Levels")); + const section = frm.dashboard.add_section("", __("Stock Levels")); // sort by qty - r.message.sort(function(a, b) { a.qty > b.qty ? 1 : -1 }); + r.message.sort(function (a, b) { + a.qty > b.qty ? 1 : -1; + }); - var rows = $('
              ').appendTo(section); + const rows = $("
              ").appendTo(section); // show - (r.message || []).forEach(function(d) { - if(d.qty > 0) { + (r.message || []).forEach(function (d) { + if (d.qty > 0) { $(`
              ${d.warehouse}
              ${d.qty}
              @@ -64,101 +74,110 @@ frappe.ui.form.on('Batch', { + ${__("Move")} + ${__("Split")}
              `).appendTo(rows); } }); // move - ask for target warehouse and make stock entry - rows.find('.btn-move').on('click', function() { - var $btn = $(this); + rows.find(".btn-move").on("click", function () { + const $btn = $(this); const fields = [ { - fieldname: 'to_warehouse', - label: __('To Warehouse'), - fieldtype: 'Link', - options: 'Warehouse' - } + fieldname: "to_warehouse", + label: __("To Warehouse"), + fieldtype: "Link", + options: "Warehouse", + }, ]; frappe.prompt( fields, (data) => { frappe.call({ - method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry', + method: "erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry", args: { item_code: frm.doc.item, batch_no: frm.doc.name, - qty: $btn.attr('data-qty'), - from_warehouse: $btn.attr('data-warehouse'), + qty: $btn.attr("data-qty"), + from_warehouse: $btn.attr("data-warehouse"), to_warehouse: data.to_warehouse, source_document: frm.doc.reference_name, - reference_doctype: frm.doc.reference_doctype + reference_doctype: frm.doc.reference_doctype, }, callback: (r) => { - frappe.show_alert(__('Stock Entry {0} created', - ['' + r.message.name+ ''])); + frappe.show_alert( + __("Stock Entry {0} created", [ + '' + + r.message.name + + "", + ]) + ); frm.refresh(); }, }); }, - __('Select Target Warehouse'), - __('Move') + __("Select Target Warehouse"), + __("Move") ); }); // split - ask for new qty and batch ID (optional) // and make stock entry via batch.batch_split - rows.find('.btn-split').on('click', function() { - var $btn = $(this); - frappe.prompt([{ - fieldname: 'qty', - label: __('New Batch Qty'), - fieldtype: 'Float', - 'default': $btn.attr('data-qty') - }, - { - fieldname: 'new_batch_id', - label: __('New Batch ID (Optional)'), - fieldtype: 'Data', - }], - (data) => { - frappe.call({ - method: 'erpnext.stock.doctype.batch.batch.split_batch', - args: { - item_code: frm.doc.item, - batch_no: frm.doc.name, - qty: data.qty, - warehouse: $btn.attr('data-warehouse'), - new_batch_id: data.new_batch_id + rows.find(".btn-split").on("click", function () { + const $btn = $(this); + frappe.prompt( + [ + { + fieldname: "qty", + label: __("New Batch Qty"), + fieldtype: "Float", + default: $btn.attr("data-qty"), }, - callback: (r) => { - frm.refresh(); + { + fieldname: "new_batch_id", + label: __("New Batch ID (Optional)"), + fieldtype: "Data", }, - }); - }, - __('Split Batch'), - __('Split') + ], + (data) => { + frappe + .xcall("erpnext.stock.doctype.batch.batch.split_batch", { + item_code: frm.doc.item, + batch_no: frm.doc.name, + qty: data.qty, + warehouse: $btn.attr("data-warehouse"), + new_batch_id: data.new_batch_id, + }) + .then(() => frm.reload_doc()); + }, + __("Split Batch"), + __("Split") ); - }) + }); frm.dashboard.show(); - } + }, }); } - } -}) + }, +}); -frappe.ui.form.on('Batch', 'manufacturing_date', function (frm){ - frappe.db.get_value('Item', {name: frm.doc.item}, ['shelf_life_in_days', 'has_expiry_date'], (r) => { +frappe.ui.form.on("Batch", "manufacturing_date", function (frm) { + frappe.db.get_value("Item", { name: frm.doc.item }, ["shelf_life_in_days", "has_expiry_date"], (r) => { if (r.has_expiry_date && r.shelf_life_in_days) { // Calculate expiry date based on shelf_life_in_days - frm.set_value('expiry_date', frappe.datetime.add_days(frm.doc.manufacturing_date, r.shelf_life_in_days)); + frm.set_value( + "expiry_date", + frappe.datetime.add_days(frm.doc.manufacturing_date, r.shelf_life_in_days) + ); } - }) -}) + }); +}); diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index f377f94a8cf..6ddc2abbc73 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -165,9 +165,7 @@ class Batch(Document): @frappe.whitelist() -def get_batch_qty( - batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None -): +def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None): """Returns batch actual qty if warehouse is passed, or returns dict of qty by warehouse if warehouse is None @@ -221,9 +219,7 @@ def get_batch_qty( def get_batches_by_oldest(item_code, warehouse): """Returns the oldest batch and qty for the given item_code and warehouse""" batches = get_batch_qty(item_code=item_code, warehouse=warehouse) - batches_dates = [ - [batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches - ] + batches_dates = [[batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches] batches_dates.sort(key=lambda tup: tup[1]) return batches_dates diff --git a/erpnext/stock/doctype/batch/batch_list.js b/erpnext/stock/doctype/batch/batch_list.js index 0de9fd01503..2060d6e8763 100644 --- a/erpnext/stock/doctype/batch/batch_list.js +++ b/erpnext/stock/doctype/batch/batch_list.js @@ -1,14 +1,21 @@ -frappe.listview_settings['Batch'] = { +frappe.listview_settings["Batch"] = { add_fields: ["item", "expiry_date", "batch_qty", "disabled"], get_indicator: (doc) => { if (doc.disabled) { return [__("Disabled"), "gray", "disabled,=,1"]; } else if (!doc.batch_qty) { return [__("Empty"), "gray", "batch_qty,=,0|disabled,=,0"]; - } else if (doc.expiry_date && frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0) { - return [__("Expired"), "red", "expiry_date,not in,|expiry_date,<=,Today|batch_qty,>,0|disabled,=,0"] + } else if ( + doc.expiry_date && + frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0 + ) { + return [ + __("Expired"), + "red", + "expiry_date,not in,|expiry_date,<=,Today|batch_qty,>,0|disabled,=,0", + ]; } else { return [__("Active"), "green", "batch_qty,>,0|disabled,=,0"]; - }; - } + } + }, }; diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 271e2e02984..51a5dac4d4b 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -81,9 +81,7 @@ class TestBatch(FrappeTestCase): stock_entry.submit() self.assertTrue(stock_entry.items[0].batch_no) - self.assertEqual( - get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90 - ) + self.assertEqual(get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90) def test_delivery_note(self): """Test automatic batch selection for outgoing items""" @@ -159,9 +157,7 @@ class TestBatch(FrappeTestCase): receipt = self.test_purchase_receipt() from erpnext.stock.doctype.batch.batch import split_batch - new_batch = split_batch( - receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22 - ) + new_batch = split_batch(receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22) self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), 78) self.assertEqual(get_batch_qty(new_batch, receipt.items[0].warehouse), 22) @@ -359,9 +355,7 @@ class TestBatch(FrappeTestCase): self.make_batch_item(item_code) def assertValuation(expected): - actual = get_valuation_rate( - item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no - ) + actual = get_valuation_rate(item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no) self.assertAlmostEqual(actual, expected) se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse) diff --git a/erpnext/stock/doctype/bin/bin.js b/erpnext/stock/doctype/bin/bin.js index 40411b68b48..02ff8b62396 100644 --- a/erpnext/stock/doctype/bin/bin.js +++ b/erpnext/stock/doctype/bin/bin.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Bin', { - refresh: function(frm) { - - } +frappe.ui.form.on("Bin", { + refresh: function (frm) {}, }); diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index 02684a72419..5284bc410c1 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -51,8 +51,7 @@ "oldfieldtype": "Link", "options": "Item", "read_only": 1, - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "default": "0.00", diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index bd6d7241c2b..75753c8b0f5 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -58,9 +58,7 @@ class Bin(Document): get_reserved_qty_for_sub_assembly, ) - reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly( - self.item_code, self.warehouse - ) + reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(self.item_code, self.warehouse) if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan: return @@ -81,9 +79,7 @@ class Bin(Document): in open work orders""" from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production - self.reserved_qty_for_production = get_reserved_qty_for_production( - self.item_code, self.warehouse - ) + self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse) self.db_set( "reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True @@ -131,9 +127,7 @@ class Bin(Document): se_item = frappe.qb.DocType("Stock Entry Detail") if frappe.db.field_exists("Stock Entry", "is_return"): - qty_field = ( - Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty) - ) + qty_field = Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty) else: qty_field = se_item.transfer_qty diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py index b79dee81e21..e4f5565cd75 100644 --- a/erpnext/stock/doctype/bin/test_bin.py +++ b/erpnext/stock/doctype/bin/test_bin.py @@ -31,4 +31,4 @@ class TestBin(FrappeTestCase): def test_index_exists(self): indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1) if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes): - self.fail(f"Expected unique index on item-warehouse") + self.fail("Expected unique index on item-warehouse") diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js index 5c807a80a04..0f0221fa562 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js @@ -16,9 +16,9 @@ frappe.ui.form.on("Closing Stock Balance", { freeze: true, callback: () => { frm.reload_doc(); - } - }) - }) + }, + }); + }); } }, @@ -31,9 +31,9 @@ frappe.ui.form.on("Closing Stock Balance", { freeze: true, callback: () => { frm.reload_doc(); - } - }) - }) + }, + }); + }); } - } + }, }); diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py index 295d979b835..70aa57f4c12 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -44,7 +44,7 @@ class ClosingStockBalance(Document): & ( (table.from_date.between(self.from_date, self.to_date)) | (table.to_date.between(self.from_date, self.to_date)) - | (table.from_date >= self.from_date and table.to_date <= self.to_date) + | ((table.from_date >= self.from_date) & (table.to_date >= self.to_date)) ) ) ) @@ -126,8 +126,6 @@ def prepare_closing_stock_balance(name): try: doc.create_closing_stock_balance_entries() doc.db_set("status", "Completed") - except Exception as e: + except Exception: doc.db_set("status", "Failed") - traceback = frappe.get_traceback() - - frappe.log_error("Closing Stock Balance Failed", traceback, doc.doctype, doc.name) + doc.log_error(title="Closing Stock Balance Failed") diff --git a/erpnext/stock/doctype/customs_tariff_number/customs_tariff_number.js b/erpnext/stock/doctype/customs_tariff_number/customs_tariff_number.js index a39414c8363..bb60ee7e368 100644 --- a/erpnext/stock/doctype/customs_tariff_number/customs_tariff_number.js +++ b/erpnext/stock/doctype/customs_tariff_number/customs_tariff_number.js @@ -1,8 +1,6 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Customs Tariff Number', { - refresh: function(frm) { - - } +frappe.ui.form.on("Customs Tariff Number", { + refresh: function (frm) {}, }); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 5731bda495e..60e69599dae 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -680,7 +680,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1401,7 +1401,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2023-12-18 17:19:39.368239", + "modified": "2024-03-20 16:05:02.854990", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 4216ca0cdc3..2ad3f485d08 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -10,7 +10,7 @@ from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values from frappe.utils import cint, flt -from erpnext.controllers.accounts_controller import get_taxes_and_charges +from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes from erpnext.controllers.selling_controller import SellingController from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no @@ -20,7 +20,7 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class DeliveryNote(SellingController): def __init__(self, *args, **kwargs): - super(DeliveryNote, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.status_updater = [ { "source_dt": "Delivery Note Item", @@ -108,7 +108,7 @@ class DeliveryNote(SellingController): for f in fieldname: toggle_print_hide(self.meta if key == "parent" else item_meta, f) - super(DeliveryNote, self).before_print(settings) + super().before_print(settings) def set_actual_qty(self): for d in self.get("items"): @@ -129,7 +129,8 @@ class DeliveryNote(SellingController): def validate(self): self.validate_posting_time() - super(DeliveryNote, self).validate() + super().validate() + self.validate_references() self.set_status() self.so_required() self.validate_proj_cust() @@ -158,11 +159,16 @@ class DeliveryNote(SellingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): - super(DeliveryNote, self).validate_with_previous_doc( + super().validate_with_previous_doc( { "Sales Order": { "ref_dn_field": "against_sales_order", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + "compare_fields": [ + ["customer", "="], + ["company", "="], + ["project", "="], + ["currency", "="], + ], }, "Sales Order Item": { "ref_dn_field": "so_detail", @@ -172,7 +178,12 @@ class DeliveryNote(SellingController): }, "Sales Invoice": { "ref_dn_field": "against_sales_invoice", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + "compare_fields": [ + ["customer", "="], + ["company", "="], + ["project", "="], + ["currency", "="], + ], }, "Sales Invoice Item": { "ref_dn_field": "si_detail", @@ -195,6 +206,58 @@ class DeliveryNote(SellingController): ] ) + def validate_references(self): + self.validate_sales_order_references() + self.validate_sales_invoice_references() + + def validate_sales_order_references(self): + err_msg = "" + for item in self.items: + if (item.against_sales_order and not item.so_detail) or ( + not item.against_sales_order and item.so_detail + ): + if not item.against_sales_order: + err_msg += ( + _("'Sales Order' reference ({1}) is missing in row {0}").format( + frappe.bold(item.idx), frappe.bold("against_sales_order") + ) + + "
              " + ) + else: + err_msg += ( + _("'Sales Order Item' reference ({1}) is missing in row {0}").format( + frappe.bold(item.idx), frappe.bold("so_detail") + ) + + "
              " + ) + + if err_msg: + frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete")) + + def validate_sales_invoice_references(self): + err_msg = "" + for item in self.items: + if (item.against_sales_invoice and not item.si_detail) or ( + not item.against_sales_invoice and item.si_detail + ): + if not item.against_sales_invoice: + err_msg += ( + _("'Sales Invoice' reference ({1}) is missing in row {0}").format( + frappe.bold(item.idx), frappe.bold("against_sales_invoice") + ) + + "
              " + ) + else: + err_msg += ( + _("'Sales Invoice Item' reference ({1}) is missing in row {0}").format( + frappe.bold(item.idx), frappe.bold("si_detail") + ) + + "
              " + ) + + if err_msg: + frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete")) + def validate_proj_cust(self): """check for does customer belong to same project as entered..""" if self.project and self.customer: @@ -210,7 +273,7 @@ class DeliveryNote(SellingController): ) def validate_warehouse(self): - super(DeliveryNote, self).validate_warehouse() + super().validate_warehouse() for d in self.get_item_list(): if not d["warehouse"] and frappe.get_cached_value("Item", d["item_code"], "is_stock_item") == 1: @@ -258,7 +321,7 @@ class DeliveryNote(SellingController): self.repost_future_sle_and_gle() def on_cancel(self): - super(DeliveryNote, self).on_cancel() + super().on_cancel() self.check_sales_order_on_hold_or_close("against_sales_order") self.check_next_docstatus() @@ -570,7 +633,7 @@ def get_returned_qty_map(delivery_note): @frappe.whitelist() -def make_sales_invoice(source_name, target_doc=None): +def make_sales_invoice(source_name, target_doc=None, args=None): doc = frappe.get_doc("Delivery Note", source_name) to_make_invoice_qty_map = {} @@ -584,6 +647,9 @@ def make_sales_invoice(source_name, target_doc=None): if len(target.get("items")) == 0: frappe.throw(_("All these items have already been Invoiced/Returned")) + if args and args.get("merge_taxes"): + merge_taxes(source.get("taxes") or [], target) + target.run_method("calculate_taxes_and_totals") # set company address @@ -648,7 +714,11 @@ def make_sales_invoice(source_name, target_doc=None): if not doc.get("is_return") else get_pending_qty(d) > 0, }, - "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Taxes and Charges": { + "doctype": "Sales Taxes and Charges", + "add_if_empty": True, + "ignore": args.get("merge_taxes") if args else 0, + }, "Sales Team": { "doctype": "Sales Team", "field_map": {"incentives": "incentives"}, @@ -702,7 +772,7 @@ def make_delivery_trip(source_name, target_doc=None): @frappe.whitelist() -def make_installation_note(source_name, target_doc=None): +def make_installation_note(source_name, target_doc=None, kwargs=None): def update_item(obj, target, source_parent): target.qty = flt(obj.qty) - flt(obj.installed_qty) target.serial_no = obj.serial_no @@ -791,7 +861,7 @@ def make_shipment(source_name, target_doc=None): "User", frappe.session.user, ["email", "full_name", "phone", "mobile_no"], as_dict=1 ) target.pickup_contact_email = user.email - pickup_contact_display = "{}".format(user.full_name) + pickup_contact_display = f"{user.full_name}" if user: if user.email: pickup_contact_display += "
              " + user.email @@ -807,7 +877,7 @@ def make_shipment(source_name, target_doc=None): contact = frappe.db.get_value( "Contact", source.contact_person, ["email_id", "phone", "mobile_no"], as_dict=1 ) - delivery_contact_display = "{}".format(source.contact_display) + delivery_contact_display = f"{source.contact_display}" if contact: if contact.email_id: delivery_contact_display += "
              " + contact.email_id @@ -912,6 +982,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")): target.append("taxes", tax) + if not target.get("items"): + frappe.throw(_("All items have already been received")) + def update_details(source_doc, target_doc, source_parent): target_doc.inter_company_invoice_reference = source_doc.name if target_doc.doctype == "Purchase Receipt": @@ -967,6 +1040,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): shipping_address_name=target_doc.shipping_address_name, ) + def update_item(source, target, source_parent): + if source_parent.doctype == "Delivery Note" and source.received_qty: + target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty) + doclist = get_mapped_doc( doctype, source_name, @@ -976,8 +1053,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "postprocess": update_details, "field_no_map": ["taxes_and_charges", "set_warehouse"], }, - doctype - + " Item": { + doctype + " Item": { "doctype": target_doctype + " Item", "field_map": { source_document_warehouse_field: target_document_warehouse_field, @@ -990,6 +1066,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "Material_request_item": "material_request_item", }, "field_no_map": ["warehouse"], + "condition": lambda item: item.received_qty < item.qty + item.returned_qty, + "postprocess": update_item, }, }, target_doc, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index d4a574da73f..2440701af98 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -8,6 +8,7 @@ def get_data(): "Stock Entry": "delivery_note_no", "Quality Inspection": "reference_name", "Auto Repeat": "reference_document", + "Purchase Receipt": "inter_company_reference", }, "internal_links": { "Sales Order": ["items", "against_sales_order"], @@ -22,6 +23,9 @@ def get_data(): {"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]}, {"label": _("Returns"), "items": ["Stock Entry"]}, {"label": _("Subscription"), "items": ["Auto Repeat"]}, - {"label": _("Internal Transfer"), "items": ["Material Request", "Purchase Order"]}, + { + "label": _("Internal Transfer"), + "items": ["Material Request", "Purchase Order", "Purchase Receipt"], + }, ], } diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index 6ff3ed3e8e5..c6b98c4134c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -1,8 +1,18 @@ -frappe.listview_settings['Delivery Note'] = { - add_fields: ["customer", "customer_name", "base_grand_total", "per_installed", "per_billed", - "transporter_name", "grand_total", "is_return", "status", "currency"], - get_indicator: function(doc) { - if(cint(doc.is_return)==1) { +frappe.listview_settings["Delivery Note"] = { + add_fields: [ + "customer", + "customer_name", + "base_grand_total", + "per_installed", + "per_billed", + "transporter_name", + "grand_total", + "is_return", + "status", + "currency", + ], + get_indicator: function (doc) { + if (cint(doc.is_return) == 1) { return [__("Return"), "gray", "is_return,=,Yes"]; } else if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; @@ -24,46 +34,45 @@ frappe.listview_settings['Delivery Note'] = { if (!doc.docstatus) { frappe.throw(__("Cannot create a Delivery Trip from Draft documents.")); } - }; + } - frappe.new_doc("Delivery Trip") - .then(() => { - // Empty out the child table before inserting new ones - cur_frm.set_value("delivery_stops", []); + frappe.new_doc("Delivery Trip").then(() => { + // Empty out the child table before inserting new ones + cur_frm.set_value("delivery_stops", []); - // We don't want to use `map_current_doc` since it brings up - // the dialog to select more items. We just want the mapper - // function to be called. - frappe.call({ - type: "POST", - method: "frappe.model.mapper.map_docs", - args: { - "method": "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip", - "source_names": docnames, - "target_doc": cur_frm.doc - }, - callback: function (r) { - if (!r.exc) { - frappe.model.sync(r.message); - cur_frm.dirty(); - cur_frm.refresh(); - } + // We don't want to use `map_current_doc` since it brings up + // the dialog to select more items. We just want the mapper + // function to be called. + frappe.call({ + type: "POST", + method: "frappe.model.mapper.map_docs", + args: { + method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip", + source_names: docnames, + target_doc: cur_frm.doc, + }, + callback: function (r) { + if (!r.exc) { + frappe.model.sync(r.message); + cur_frm.dirty(); + cur_frm.refresh(); } - }); - }) - }; + }, + }); + }); + } }; // doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); - doclist.page.add_action_item(__('Create Delivery Trip'), action); + doclist.page.add_action_item(__("Create Delivery Trip"), action); - doclist.page.add_action_item(__("Sales Invoice"), ()=>{ + doclist.page.add_action_item(__("Sales Invoice"), () => { erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice"); }); - doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{ + doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), () => { erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip"); }); - } + }, }; diff --git a/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py index 8fe4ffb58f1..cc29e67fa7b 100644 --- a/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py +++ b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py @@ -1,15 +1,27 @@ +import click import frappe +UNUSED_INDEXES = [ + ("Delivery Note", ["customer", "is_return", "return_against"]), + ("Sales Invoice", ["customer", "is_return", "return_against"]), + ("Purchase Invoice", ["supplier", "is_return", "return_against"]), + ("Purchase Receipt", ["supplier", "is_return", "return_against"]), +] + def execute(): - """Drop unused return_against index""" + for doctype, index_fields in UNUSED_INDEXES: + table = f"tab{doctype}" + index_name = frappe.db.get_index_name(index_fields) + drop_index_if_exists(table, index_name) + + +def drop_index_if_exists(table: str, index: str): + if not frappe.db.has_index(table, index): + return try: - frappe.db.sql_ddl( - "ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`" - ) - frappe.db.sql_ddl( - "ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`" - ) + frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`") + click.echo(f"✓ dropped {index} index from {table}") except Exception: - frappe.log_error("Failed to drop unused index") + frappe.log_error("Failed to drop index") diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 65828f3a4ac..e9b07e76b2d 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -54,7 +54,7 @@ class TestDeliveryNote(FrappeTestCase): self.assertRaises(frappe.ValidationError, frappe.get_doc(si).insert) def test_delivery_note_no_gl_entry(self): - company = frappe.db.get_value("Warehouse", "_Test Warehouse - _TC", "company") + frappe.db.get_value("Warehouse", "_Test Warehouse - _TC", "company") make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100) stock_queue = json.loads( @@ -71,16 +71,14 @@ class TestDeliveryNote(FrappeTestCase): dn = create_delivery_note() - sle = frappe.get_doc( - "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name} - ) + sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name}) self.assertEqual(sle.stock_value_difference, flt(-1 * stock_queue[0][1], 2)) self.assertFalse(get_gl_entries("Delivery Note", dn.name)) def test_delivery_note_gl_entry_packing_item(self): - company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") + frappe.db.get_value("Warehouse", "Stores - TCP1", "company") make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=10, basic_rate=100) make_stock_entry( @@ -127,7 +125,7 @@ class TestDeliveryNote(FrappeTestCase): stock_in_hand_account: [0.0, stock_value_diff], "Cost of Goods Sold - TCP1": [stock_value_diff, 0.0], } - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) # check stock in hand balance @@ -159,9 +157,7 @@ class TestDeliveryNote(FrappeTestCase): serial_no = get_serial_nos(se.get("items")[0].serial_no) serial_no = "\n".join(serial_no) - dn = create_delivery_note( - item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no - ) + dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no) si = make_sales_invoice(dn.name) si.items[0].qty = 1 @@ -694,7 +690,7 @@ class TestDeliveryNote(FrappeTestCase): "Stock In Hand - TCP1": [0.0, stock_value_difference], target_warehouse: [stock_value_difference, 0.0], } - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) # tear down @@ -723,6 +719,15 @@ class TestDeliveryNote(FrappeTestCase): dn.cancel() self.assertEqual(dn.status, "Cancelled") + def test_sales_order_reference_validation(self): + so = make_sales_order(po_no="12345") + dn = create_dn_against_so(so.name, delivered_qty=2, do_not_submit=True) + dn.items[0].against_sales_order = None + self.assertRaises(frappe.ValidationError, dn.save) + dn.reload() + dn.items[0].so_detail = None + self.assertRaises(frappe.ValidationError, dn.save) + def test_dn_billing_status_case1(self): # SO -> DN -> SI so = make_sales_order(po_no="12345") @@ -893,7 +898,7 @@ class TestDeliveryNote(FrappeTestCase): "Cost of Goods Sold - TCP1": {"cost_center": cost_center}, stock_in_hand_account: {"cost_center": cost_center}, } - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) def test_delivery_note_cost_center_with_balance_sheet_account(self): @@ -922,7 +927,7 @@ class TestDeliveryNote(FrappeTestCase): "Cost of Goods Sold - TCP1": {"cost_center": cost_center}, stock_in_hand_account: {"cost_center": cost_center}, } - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) def test_make_sales_invoice_from_dn_for_returned_qty(self): @@ -988,9 +993,7 @@ class TestDeliveryNote(FrappeTestCase): }, ) make_product_bundle(parent=batched_bundle.name, items=[batched_item.name]) - make_stock_entry( - item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42 - ) + make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42) try: dn = create_delivery_note(item_code=batched_bundle.name, qty=1) @@ -999,9 +1002,7 @@ class TestDeliveryNote(FrappeTestCase): self.fail("Batch numbers not getting added to bundled items in DN.") raise e - self.assertTrue( - "TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item" - ) + self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( @@ -1176,9 +1177,7 @@ class TestDeliveryNote(FrappeTestCase): warehouse=warehouse, target_warehouse=target, ) - self.assertFalse( - frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype}) - ) + self.assertFalse(frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype})) def test_batch_expiry_for_delivery_note(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -1253,11 +1252,9 @@ class TestDeliveryNote(FrappeTestCase): ).name # Step - 2: Inward Stock - se1 = make_stock_entry(item_code=batch_item, target="_Test Warehouse - _TC", qty=3) + make_stock_entry(item_code=batch_item, target="_Test Warehouse - _TC", qty=3) serial_nos = ( - make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=3) - .items[0] - .serial_no + make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=3).items[0].serial_no ) # Step - 3: Create a Product Bundle @@ -1332,6 +1329,126 @@ class TestDeliveryNote(FrappeTestCase): dn.reload() self.assertFalse(dn.items[0].target_warehouse) + def test_sales_return_valuation_for_moving_average(self): + item_code = make_item( + "_Test Item Sales Return with MA", {"is_stock_item": 1, "valuation_method": "Moving Average"} + ).name + + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=100.0, + posting_date=add_days(nowdate(), -5), + ) + dn = create_delivery_note(item_code=item_code, qty=5, rate=500, posting_date=add_days(nowdate(), -4)) + self.assertEqual(dn.items[0].incoming_rate, 100.0) + + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=200.0, + posting_date=add_days(nowdate(), -3), + ) + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=300.0, + posting_date=add_days(nowdate(), -2), + ) + + dn1 = create_delivery_note( + is_return=1, + item_code=item_code, + return_against=dn.name, + qty=-5, + rate=500, + company=dn.company, + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + do_not_submit=1, + posting_date=add_days(nowdate(), -1), + ) + + # (300 * 5) + (200 * 5) = 2500 + # 2500 / 10 = 250 + + self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0) + + def test_sales_return_valuation_for_moving_average_case2(self): + # Make DN return + # Make Bakcdated Purchase Receipt and check DN return valuation rate + # The rate should be recalculate based on the backdated purchase receipt + frappe.flags.print_debug_messages = False + item_code = make_item( + "_Test Item Sales Return with MA Case2", + {"is_stock_item": 1, "valuation_method": "Moving Average", "stock_uom": "Nos"}, + ).name + + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=100.0, + posting_date=add_days(nowdate(), -5), + ) + + dn = create_delivery_note( + item_code=item_code, + warehouse="_Test Warehouse - _TC", + qty=5, + rate=500, + posting_date=add_days(nowdate(), -4), + ) + + returned_dn = create_delivery_note( + is_return=1, + item_code=item_code, + return_against=dn.name, + qty=-5, + rate=500, + company=dn.company, + warehouse="_Test Warehouse - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + posting_date=add_days(nowdate(), -1), + ) + + self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 100.0) + + # Make backdated purchase receipt + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=200.0, + posting_date=add_days(nowdate(), -3), + ) + + returned_dn.reload() + self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.0) + + def test_internal_transfer_for_non_stock_item(self): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + + item = make_item(properties={"is_stock_item": 0}).name + warehouse = "_Test Warehouse - _TC" + target = "Stores - _TC" + company = "_Test Company" + customer = create_internal_customer(represents_company=company) + rate = 100 + + so = make_sales_order(item_code=item, qty=1, rate=rate, customer=customer, warehouse=warehouse) + dn = make_delivery_note(so.name) + dn.items[0].target_warehouse = target + dn.save().submit() + + self.assertEqual(so.items[0].rate, rate) + self.assertEqual(dn.items[0].rate, so.items[0].rate) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index b85bfe5036d..25788966780 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -809,7 +809,8 @@ "label": "Purchase Order", "options": "Purchase Order", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "column_break_82", @@ -870,7 +871,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:38.638144", + "modified": "2024-03-21 18:15:07.603672", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/delivery_settings/delivery_settings.js b/erpnext/stock/doctype/delivery_settings/delivery_settings.js index 03aa1922ff5..4328158e6d9 100644 --- a/erpnext/stock/doctype/delivery_settings/delivery_settings.js +++ b/erpnext/stock/doctype/delivery_settings/delivery_settings.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Delivery Settings', { - refresh: function(frm) { - - } +frappe.ui.form.on("Delivery Settings", { + refresh: function (frm) {}, }); diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index 4ba5b32ca62..4f8649c0bfa 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -1,15 +1,15 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Delivery Trip', { +frappe.ui.form.on("Delivery Trip", { setup: function (frm) { - frm.set_indicator_formatter('customer', (stop) => (stop.visited) ? "green" : "orange"); + frm.set_indicator_formatter("customer", (stop) => (stop.visited ? "green" : "orange")); frm.set_query("driver", function () { return { filters: { - "status": "Active" - } + status: "Active", + }, }; }); @@ -17,58 +17,71 @@ frappe.ui.form.on('Delivery Trip', { var row = locals[cdt][cdn]; if (row.customer) { return { - query: 'frappe.contacts.doctype.address.address.address_query', + query: "frappe.contacts.doctype.address.address.address_query", filters: { link_doctype: "Customer", - link_name: row.customer - } + link_name: row.customer, + }, }; } - }) + }); frm.set_query("contact", "delivery_stops", function (doc, cdt, cdn) { var row = locals[cdt][cdn]; if (row.customer) { return { - query: 'frappe.contacts.doctype.contact.contact.contact_query', + query: "frappe.contacts.doctype.contact.contact.contact_query", filters: { link_doctype: "Customer", - link_name: row.customer - } + link_name: row.customer, + }, }; } - }) + }); }, refresh: function (frm) { if (frm.doc.docstatus == 1 && frm.doc.delivery_stops.length > 0) { frm.add_custom_button(__("Notify Customers via Email"), function () { - frm.trigger('notify_customers'); + frm.trigger("notify_customers"); }); } if (frm.doc.docstatus === 0) { - frm.add_custom_button(__('Delivery Note'), () => { - erpnext.utils.map_current_doc({ - method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip", - source_doctype: "Delivery Note", - target: frm, - date_field: "posting_date", - setters: { - company: frm.doc.company, - }, - get_query_filters: { - docstatus: 1, - company: frm.doc.company, - } - }) - }, __("Get stops from")); - } - frm.add_custom_button(__("Delivery Notes"), function () { - frappe.set_route("List", "Delivery Note", - {'name': ["in", frm.doc.delivery_stops.map((stop) => {return stop.delivery_note;})]} + frm.add_custom_button( + __("Delivery Note"), + () => { + erpnext.utils.map_current_doc({ + method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip", + source_doctype: "Delivery Note", + target: frm, + date_field: "posting_date", + setters: { + company: frm.doc.company, + }, + get_query_filters: { + docstatus: 1, + company: frm.doc.company, + }, + }); + }, + __("Get stops from") ); - }, __("View")); + } + frm.add_custom_button( + __("Delivery Notes"), + function () { + frappe.set_route("List", "Delivery Note", { + name: [ + "in", + frm.doc.delivery_stops.map((stop) => { + return stop.delivery_note; + }), + ], + }); + }, + __("View") + ); }, calculate_arrival_time: function (frm) { @@ -77,13 +90,17 @@ frappe.ui.form.on('Delivery Trip', { } frappe.show_alert({ message: "Calculating Arrival Times", - indicator: 'orange' - }); - frm.call("process_route", { - optimize: false, - }, () => { - frm.reload_doc(); + indicator: "orange", }); + frm.call( + "process_route", + { + optimize: false, + }, + () => { + frm.reload_doc(); + } + ); }, driver: function (frm) { @@ -91,13 +108,13 @@ frappe.ui.form.on('Delivery Trip', { frappe.call({ method: "erpnext.stock.doctype.delivery_trip.delivery_trip.get_driver_email", args: { - driver: frm.doc.driver + driver: frm.doc.driver, }, callback: (data) => { frm.set_value("driver_email", data.message.email); - } + }, }); - }; + } }, optimize_route: function (frm) { @@ -106,23 +123,27 @@ frappe.ui.form.on('Delivery Trip', { } frappe.show_alert({ message: "Optimizing Route", - indicator: 'orange' - }); - frm.call("process_route", { - optimize: true, - }, () => { - frm.reload_doc(); + indicator: "orange", }); + frm.call( + "process_route", + { + optimize: true, + }, + () => { + frm.reload_doc(); + } + ); }, notify_customers: function (frm) { $.each(frm.doc.delivery_stops || [], function (i, delivery_stop) { if (!delivery_stop.delivery_note) { frappe.msgprint({ - "message": __("No Delivery Note selected for Customer {}", [delivery_stop.customer]), - "title": __("Warning"), - "indicator": "orange", - "alert": 1 + message: __("No Delivery Note selected for Customer {}", [delivery_stop.customer]), + title: __("Warning"), + indicator: "orange", + alert: 1, }); } }); @@ -135,48 +156,45 @@ frappe.ui.form.on('Delivery Trip', { frappe.call({ method: "erpnext.stock.doctype.delivery_trip.delivery_trip.notify_customers", args: { - "delivery_trip": frm.doc.name + delivery_trip: frm.doc.name, }, callback: function (r) { if (!r.exc) { frm.doc.email_notification_sent = true; - frm.refresh_field('email_notification_sent'); + frm.refresh_field("email_notification_sent"); } - } + }, }); }); } }); - } + }, }); -frappe.ui.form.on('Delivery Stop', { +frappe.ui.form.on("Delivery Stop", { customer: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; if (row.customer) { frappe.call({ method: "erpnext.stock.doctype.delivery_trip.delivery_trip.get_contact_and_address", - args: { "name": row.customer }, + args: { name: row.customer }, callback: function (r) { if (r.message) { if (r.message["shipping_address"]) { frappe.model.set_value(cdt, cdn, "address", r.message["shipping_address"].parent); - } - else { - frappe.model.set_value(cdt, cdn, "address", ''); + } else { + frappe.model.set_value(cdt, cdn, "address", ""); } if (r.message["contact_person"]) { frappe.model.set_value(cdt, cdn, "contact", r.message["contact_person"].parent); + } else { + frappe.model.set_value(cdt, cdn, "contact", ""); } - else { - frappe.model.set_value(cdt, cdn, "contact", ''); - } + } else { + frappe.model.set_value(cdt, cdn, "address", ""); + frappe.model.set_value(cdt, cdn, "contact", ""); } - else { - frappe.model.set_value(cdt, cdn, "address", ''); - frappe.model.set_value(cdt, cdn, "contact", ''); - } - } + }, }); } }, @@ -186,12 +204,12 @@ frappe.ui.form.on('Delivery Stop', { if (row.address) { frappe.call({ method: "frappe.contacts.doctype.address.address.get_address_display", - args: { "address_dict": row.address }, + args: { address_dict: row.address }, callback: function (r) { if (r.message) { frappe.model.set_value(cdt, cdn, "customer_address", r.message); } - } + }, }); } else { frappe.model.set_value(cdt, cdn, "customer_address", ""); @@ -203,15 +221,15 @@ frappe.ui.form.on('Delivery Stop', { if (row.contact) { frappe.call({ method: "erpnext.stock.doctype.delivery_trip.delivery_trip.get_contact_display", - args: { "contact": row.contact }, + args: { contact: row.contact }, callback: function (r) { if (r.message) { frappe.model.set_value(cdt, cdn, "customer_contact", r.message); } - } + }, }); } else { frappe.model.set_value(cdt, cdn, "customer_contact", ""); } - } + }, }); diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index c531a8769c0..a81dffddfb3 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -13,7 +13,7 @@ from frappe.utils import cint, get_datetime, get_link_to_form class DeliveryTrip(Document): def __init__(self, *args, **kwargs): - super(DeliveryTrip, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # Google Maps returns distances in meters by default self.default_distance_uom = ( @@ -67,9 +67,7 @@ class DeliveryTrip(Document): delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`. """ - delivery_notes = list( - set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note) - ) + delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note)) update_fields = { "driver": self.driver, @@ -315,14 +313,11 @@ def get_contact_display(contact): "Contact", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1 ) - contact_info.html = ( - """ %(first_name)s %(last_name)s
              %(phone)s
              %(mobile_no)s""" - % { - "first_name": contact_info.first_name, - "last_name": contact_info.last_name or "", - "phone": contact_info.phone or "", - "mobile_no": contact_info.mobile_no or "", - } + contact_info.html = """ {first_name} {last_name}
              {phone}
              {mobile_no}""".format( + first_name=contact_info.first_name, + last_name=contact_info.last_name or "", + phone=contact_info.phone or "", + mobile_no=contact_info.mobile_no or "", ) return contact_info.html diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js b/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js index 1d198b733fb..230107caadb 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js @@ -1,4 +1,4 @@ -frappe.listview_settings['Delivery Trip'] = { +frappe.listview_settings["Delivery Trip"] = { add_fields: ["status"], get_indicator: function (doc) { if (in_list(["Cancelled", "Draft"], doc.status)) { @@ -8,5 +8,5 @@ frappe.listview_settings['Delivery Trip'] = { } else if (doc.status === "Completed") { return [__(doc.status), "green", "status,=," + doc.status]; } - } + }, }; diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py index 9b8b46e6e0a..09f5b2c2c89 100644 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py @@ -1,7 +1,6 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe from frappe.tests.utils import FrappeTestCase diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index 35d1c02719c..c819d17b1e7 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -1,43 +1,59 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Inventory Dimension', { +frappe.ui.form.on("Inventory Dimension", { setup(frm) { - frm.trigger('set_query_on_fields'); + frm.trigger("set_query_on_fields"); }, set_query_on_fields(frm) { - frm.set_query('reference_document', () => { + frm.set_query("reference_document", () => { let invalid_doctypes = frappe.model.core_doctypes_list; - invalid_doctypes.push('Batch', 'Serial No', 'Warehouse', 'Item', 'Inventory Dimension', - 'Accounting Dimension', 'Accounting Dimension Filter'); + invalid_doctypes.push( + "Batch", + "Serial No", + "Warehouse", + "Item", + "Inventory Dimension", + "Accounting Dimension", + "Accounting Dimension Filter" + ); return { filters: { - 'istable': 0, - 'issingle': 0, - 'name': ['not in', invalid_doctypes] - } + istable: 0, + issingle: 0, + name: ["not in", invalid_doctypes], + }, }; }); - frm.set_query('document_type', () => { + frm.set_query("document_type", () => { return { - query: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_inventory_documents', + query: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_inventory_documents", }; }); }, onload(frm) { - frm.trigger('render_traget_field'); + frm.trigger("render_traget_field"); frm.trigger("set_parent_fields"); }, refresh(frm) { - if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger - && frm.doc.__onload.has_stock_ledger.length) { - let allow_to_edit_fields = ['disabled', 'fetch_from_parent', - 'type_of_transaction', 'condition', 'mandatory_depends_on', 'validate_negative_stock']; + if ( + frm.doc.__onload && + frm.doc.__onload.has_stock_ledger && + frm.doc.__onload.has_stock_ledger.length + ) { + let allow_to_edit_fields = [ + "disabled", + "fetch_from_parent", + "type_of_transaction", + "condition", + "mandatory_depends_on", + "validate_negative_stock", + ]; frm.fields.forEach((field) => { if (!in_list(allow_to_edit_fields, field.df.fieldname)) { @@ -47,8 +63,8 @@ frappe.ui.form.on('Inventory Dimension', { } if (!frm.is_new()) { - frm.add_custom_button(__('Delete Dimension'), () => { - frm.trigger('delete_dimension'); + frm.add_custom_button(__("Delete Dimension"), () => { + frm.trigger("delete_dimension"); }); } }, @@ -62,39 +78,38 @@ frappe.ui.form.on('Inventory Dimension', { frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document); } else if (frm.doc.document_type && frm.doc.istable) { frappe.call({ - method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields', + method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields", args: { child_doctype: frm.doc.document_type, - dimension_name: frm.doc.reference_document + dimension_name: frm.doc.reference_document, }, callback: (r) => { if (r.message && r.message.length) { - frm.set_df_property("fetch_from_parent", "options", - [""].concat(r.message)); + frm.set_df_property("fetch_from_parent", "options", [""].concat(r.message)); } else { frm.set_df_property("fetch_from_parent", "hidden", 1); } - } + }, }); } }, delete_dimension(frm) { - let msg = (` + let msg = ` Custom fields related to this dimension will be deleted on deletion of dimension.
              Do you want to delete {0} dimension? - `); + `; frappe.confirm(__(msg, [frm.doc.name.bold()]), () => { frappe.call({ - method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.delete_dimension', + method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.delete_dimension", args: { - dimension: frm.doc.name + dimension: frm.doc.name, + }, + callback: function () { + frappe.set_route("List", "Inventory Dimension"); }, - callback: function() { - frappe.set_route('List', 'Inventory Dimension'); - } }); }); - } + }, }); diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 257d18fc33a..5fe5042ffd6 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -281,7 +281,7 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None): dimensions = get_document_wise_inventory_dimensions(doc.doctype) filter_dimensions = [] for row in dimensions: - if row.type_of_transaction: + if row.type_of_transaction and row.type_of_transaction != "Both": if ( row.type_of_transaction == "Inward" if doc.docstatus == 1 @@ -360,9 +360,7 @@ def delete_dimension(dimension): @frappe.whitelist() def get_parent_fields(child_doctype, dimension_name): - parent_doctypes = frappe.get_all( - "DocField", fields=["parent"], filters={"options": child_doctype} - ) + parent_doctypes = frappe.get_all("DocField", fields=["parent"], filters={"options": child_doctype}) fields = [] diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 531bc3f109f..57b37801cb5 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -210,9 +210,7 @@ class TestInventoryDimension(FrappeTestCase): ) self.assertFalse( - frappe.db.get_value( - "Custom Field", {"fieldname": "project", "dt": "Stock Ledger Entry"}, "name" - ) + frappe.db.get_value("Custom Field", {"fieldname": "project", "dt": "Stock Ledger Entry"}, "name") ) def test_check_mandatory_dimensions(self): @@ -296,9 +294,7 @@ class TestInventoryDimension(FrappeTestCase): se_doc.save() se_doc.submit() - entries = get_voucher_sl_entries( - se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"] - ) + entries = get_voucher_sl_entries(se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"]) for entry in entries: self.assertEqual(entry.warehouse, warehouse) @@ -429,6 +425,14 @@ class TestInventoryDimension(FrappeTestCase): ) warehouse = create_warehouse("Negative Stock Warehouse") + + doc = make_stock_entry(item_code=item_code, source=warehouse, qty=10, do_not_submit=True) + doc.items[0].inv_site = "Site 1" + self.assertRaises(frappe.ValidationError, doc.submit) + doc.reload() + if doc.docstatus == 1: + doc.cancel() + doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True) doc.items[0].to_inv_site = "Site 1" @@ -480,7 +484,14 @@ def create_store_dimension(): "autoname": "field:store_name", "fields": [{"label": "Store Name", "fieldname": "store_name", "fieldtype": "Data"}], "permissions": [ - {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + { + "role": "System Manager", + "permlevel": 0, + "read": 1, + "write": 1, + "create": 1, + "delete": 1, + } ], } ).insert(ignore_permissions=True) @@ -502,7 +513,14 @@ def prepare_test_data(): "autoname": "field:shelf_name", "fields": [{"label": "Shelf Name", "fieldname": "shelf_name", "fieldtype": "Data"}], "permissions": [ - {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + { + "role": "System Manager", + "permlevel": 0, + "read": 1, + "write": 1, + "create": 1, + "delete": 1, + } ], } ).insert(ignore_permissions=True) @@ -524,7 +542,14 @@ def prepare_test_data(): "autoname": "field:rack_name", "fields": [{"label": "Rack Name", "fieldname": "rack_name", "fieldtype": "Data"}], "permissions": [ - {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + { + "role": "System Manager", + "permlevel": 0, + "read": 1, + "write": 1, + "create": 1, + "delete": 1, + } ], } ).insert(ignore_permissions=True) @@ -546,7 +571,14 @@ def prepare_test_data(): "autoname": "field:pallet_name", "fields": [{"label": "Pallet Name", "fieldname": "pallet_name", "fieldtype": "Data"}], "permissions": [ - {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + { + "role": "System Manager", + "permlevel": 0, + "read": 1, + "write": 1, + "create": 1, + "delete": 1, + } ], } ).insert(ignore_permissions=True) @@ -562,7 +594,14 @@ def prepare_test_data(): "autoname": "field:site_name", "fields": [{"label": "Site Name", "fieldname": "site_name", "fieldtype": "Data"}], "permissions": [ - {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + { + "role": "System Manager", + "permlevel": 0, + "read": 1, + "write": 1, + "create": 1, + "delete": 1, + } ], } ).insert(ignore_permissions=True) @@ -615,9 +654,7 @@ def prepare_data_for_internal_transfer(): to_warehouse = create_warehouse("_Test Internal Warehouse GIT A", company=company) - pr_doc = make_purchase_receipt( - company=company, warehouse=warehouse, qty=10, rate=100, do_not_submit=True - ) + pr_doc = make_purchase_receipt(company=company, warehouse=warehouse, qty=10, rate=100, do_not_submit=True) pr_doc.items[0].store = "Inter Transfer Store 1" pr_doc.submit() @@ -643,9 +680,7 @@ def prepare_data_for_internal_transfer(): expense_account = frappe.db.get_value( "Company", company, "stock_adjustment_account" - ) or frappe.db.get_value( - "Account", {"company": company, "account_type": "Expense Account"}, "name" - ) + ) or frappe.db.get_value("Account", {"company": company, "account_type": "Expense Account"}, "name") return frappe._dict( { diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index b306a41bb83..29d41fe7625 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -3,48 +3,47 @@ frappe.provide("erpnext.item"); -const SALES_DOCTYPES = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']; -const PURCHASE_DOCTYPES = ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']; +const SALES_DOCTYPES = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]; +const PURCHASE_DOCTYPES = ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]; frappe.ui.form.on("Item", { - setup: function(frm) { - frm.add_fetch('attribute', 'numeric_values', 'numeric_values'); - frm.add_fetch('attribute', 'from_range', 'from_range'); - frm.add_fetch('attribute', 'to_range', 'to_range'); - frm.add_fetch('attribute', 'increment', 'increment'); - frm.add_fetch('tax_type', 'tax_rate', 'tax_rate'); + setup: function (frm) { + frm.add_fetch("attribute", "numeric_values", "numeric_values"); + frm.add_fetch("attribute", "from_range", "from_range"); + frm.add_fetch("attribute", "to_range", "to_range"); + frm.add_fetch("attribute", "increment", "increment"); + frm.add_fetch("tax_type", "tax_rate", "tax_rate"); frm.make_methods = { - 'Sales Order': () => { + "Sales Order": () => { open_form(frm, "Sales Order", "Sales Order Item", "items"); }, - 'Delivery Note': () => { + "Delivery Note": () => { open_form(frm, "Delivery Note", "Delivery Note Item", "items"); }, - 'Sales Invoice': () => { + "Sales Invoice": () => { open_form(frm, "Sales Invoice", "Sales Invoice Item", "items"); }, - 'Purchase Order': () => { + "Purchase Order": () => { open_form(frm, "Purchase Order", "Purchase Order Item", "items"); }, - 'Purchase Receipt': () => { + "Purchase Receipt": () => { open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items"); }, - 'Purchase Invoice': () => { + "Purchase Invoice": () => { open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items"); }, - 'Material Request': () => { + "Material Request": () => { open_form(frm, "Material Request", "Material Request Item", "items"); }, - 'Stock Entry': () => { + "Stock Entry": () => { open_form(frm, "Stock Entry", "Stock Entry Detail", "items"); }, }; - }, - onload: function(frm) { + onload: function (frm) { erpnext.item.setup_queries(frm); - if (frm.doc.variant_of){ + if (frm.doc.variant_of) { frm.fields_dict["attributes"].grid.set_column_disp("attribute_value", true); } @@ -53,106 +52,161 @@ frappe.ui.form.on("Item", { } }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.is_stock_item) { - frm.add_custom_button(__("Stock Balance"), function() { - frappe.route_options = { - "item_code": frm.doc.name - } - frappe.set_route("query-report", "Stock Balance"); - }, __("View")); - frm.add_custom_button(__("Stock Ledger"), function() { - frappe.route_options = { - "item_code": frm.doc.name - } - frappe.set_route("query-report", "Stock Ledger"); - }, __("View")); - frm.add_custom_button(__("Stock Projected Qty"), function() { - frappe.route_options = { - "item_code": frm.doc.name - } - frappe.set_route("query-report", "Stock Projected Qty"); - }, __("View")); + frm.add_custom_button( + __("Stock Balance"), + function () { + frappe.route_options = { + item_code: frm.doc.name, + }; + frappe.set_route("query-report", "Stock Balance"); + }, + __("View") + ); + frm.add_custom_button( + __("Stock Ledger"), + function () { + frappe.route_options = { + item_code: frm.doc.name, + }; + frappe.set_route("query-report", "Stock Ledger"); + }, + __("View") + ); + frm.add_custom_button( + __("Stock Projected Qty"), + function () { + frappe.route_options = { + item_code: frm.doc.name, + }; + frappe.set_route("query-report", "Stock Projected Qty"); + }, + __("View") + ); } - if (frm.doc.is_fixed_asset) { - frm.trigger('is_fixed_asset'); - frm.trigger('auto_create_assets'); + frm.trigger("is_fixed_asset"); + frm.trigger("auto_create_assets"); } // clear intro frm.set_intro(); if (frm.doc.has_variants) { - frm.set_intro(__("This Item is a Template and cannot be used in transactions. Item attributes will be copied over into the variants unless 'No Copy' is set"), true); + frm.set_intro( + __( + "This Item is a Template and cannot be used in transactions. Item attributes will be copied over into the variants unless 'No Copy' is set" + ), + true + ); - frm.add_custom_button(__("Show Variants"), function() { - frappe.set_route("List", "Item", {"variant_of": frm.doc.name}); - }, __("View")); + frm.add_custom_button( + __("Show Variants"), + function () { + frappe.set_route("List", "Item", { variant_of: frm.doc.name }); + }, + __("View") + ); - frm.add_custom_button(__("Item Variant Settings"), function() { - frappe.set_route("Form", "Item Variant Settings"); - }, __("View")); + frm.add_custom_button( + __("Item Variant Settings"), + function () { + frappe.set_route("Form", "Item Variant Settings"); + }, + __("View") + ); - frm.add_custom_button(__("Variant Details Report"), function() { - frappe.set_route("query-report", "Item Variant Details", {"item": frm.doc.name}); - }, __("View")); + frm.add_custom_button( + __("Variant Details Report"), + function () { + frappe.set_route("query-report", "Item Variant Details", { item: frm.doc.name }); + }, + __("View") + ); - if(frm.doc.variant_based_on==="Item Attribute") { - frm.add_custom_button(__("Single Variant"), function() { - erpnext.item.show_single_variant_dialog(frm); - }, __('Create')); - frm.add_custom_button(__("Multiple Variants"), function() { - erpnext.item.show_multiple_variants_dialog(frm); - }, __('Create')); + if (frm.doc.variant_based_on === "Item Attribute") { + frm.add_custom_button( + __("Single Variant"), + function () { + erpnext.item.show_single_variant_dialog(frm); + }, + __("Create") + ); + frm.add_custom_button( + __("Multiple Variants"), + function () { + erpnext.item.show_multiple_variants_dialog(frm); + }, + __("Create") + ); } else { - frm.add_custom_button(__("Variant"), function() { - erpnext.item.show_modal_for_manufacturers(frm); - }, __('Create')); + frm.add_custom_button( + __("Variant"), + function () { + erpnext.item.show_modal_for_manufacturers(frm); + }, + __("Create") + ); } // frm.page.set_inner_btn_group_as_primary(__('Create')); } if (frm.doc.variant_of) { - frm.set_intro(__('This Item is a Variant of {0} (Template).', - [`${frm.doc.variant_of}`]), true); + frm.set_intro( + __("This Item is a Variant of {0} (Template).", [ + `${frm.doc.variant_of}`, + ]), + true + ); } - if (frappe.defaults.get_default("item_naming_by")!="Naming Series" || frm.doc.variant_of) { + if (frappe.defaults.get_default("item_naming_by") != "Naming Series" || frm.doc.variant_of) { frm.toggle_display("naming_series", false); } else { erpnext.toggle_naming_series(); } if (!frm.doc.published_in_website) { - frm.add_custom_button(__("Publish in Website"), function() { - frappe.call({ - method: "erpnext.e_commerce.doctype.website_item.website_item.make_website_item", - args: {doc: frm.doc}, - freeze: true, - freeze_message: __("Publishing Item ..."), - callback: function(result) { - frappe.msgprint({ - message: __("Website Item {0} has been created.", - [repl('%(item)s', { - item_encoded: encodeURIComponent(result.message[0]), - item: result.message[1] - })] - ), - title: __("Published"), - indicator: "green" - }); - } - }); - }, __('Actions')); + frm.add_custom_button( + __("Publish in Website"), + function () { + frappe.call({ + method: "erpnext.e_commerce.doctype.website_item.website_item.make_website_item", + args: { doc: frm.doc }, + freeze: true, + freeze_message: __("Publishing Item ..."), + callback: function (result) { + frappe.msgprint({ + message: __("Website Item {0} has been created.", [ + repl( + '%(item)s', + { + item_encoded: encodeURIComponent(result.message[0]), + item: result.message[1], + } + ), + ]), + title: __("Published"), + indicator: "green", + }); + }, + }); + }, + __("Actions") + ); } else { - frm.add_custom_button(__("Website Item"), function() { - frappe.db.get_value("Website Item", {item_code: frm.doc.name}, "name", (d) => { - if (!d.name) frappe.throw(__("Website Item not found")); - frappe.set_route("Form", "Website Item", d.name); - }); - }, __("View")); + frm.add_custom_button( + __("Website Item"), + function () { + frappe.db.get_value("Website Item", { item_code: frm.doc.name }, "name", (d) => { + if (!d.name) frappe.throw(__("Website Item not found")); + frappe.set_route("Form", "Website Item", d.name); + }); + }, + __("View") + ); } erpnext.item.edit_prices_button(frm); @@ -162,341 +216,374 @@ frappe.ui.form.on("Item", { erpnext.item.make_dashboard(frm); } - frm.add_custom_button(__('Duplicate'), function() { + frm.add_custom_button(__("Duplicate"), function () { var new_item = frappe.model.copy_doc(frm.doc); // Duplicate item could have different name, causing "copy paste" error. - if (new_item.item_name===new_item.item_code) { + if (new_item.item_name === new_item.item_code) { new_item.item_name = null; } - if (new_item.item_code===new_item.description || new_item.item_code===new_item.description) { + if (new_item.item_code === new_item.description || new_item.item_code === new_item.description) { new_item.description = null; } - frappe.set_route('Form', 'Item', new_item.name); + frappe.set_route("Form", "Item", new_item.name); }); - const stock_exists = (frm.doc.__onload - && frm.doc.__onload.stock_exists) ? 1 : 0; + const stock_exists = frm.doc.__onload && frm.doc.__onload.stock_exists ? 1 : 0; - ['is_stock_item', 'has_serial_no', 'has_batch_no', 'has_variants'].forEach((fieldname) => { - frm.set_df_property(fieldname, 'read_only', stock_exists); + ["is_stock_item", "has_serial_no", "has_batch_no", "has_variants"].forEach((fieldname) => { + frm.set_df_property(fieldname, "read_only", stock_exists); }); - frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0); + frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0); }, - validate: function(frm){ + validate: function (frm) { erpnext.item.weight_to_validate(frm); }, - image: function() { + image: function () { refresh_field("image_view"); }, - is_customer_provided_item: function(frm) { - frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0); + is_customer_provided_item: function (frm) { + frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0); }, - is_fixed_asset: function(frm) { + is_fixed_asset: function (frm) { // set serial no to false & toggles its visibility - frm.set_value('has_serial_no', 0); - frm.set_value('has_batch_no', 0); - frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); + frm.set_value("has_serial_no", 0); + frm.set_value("has_batch_no", 0); + frm.toggle_enable(["has_serial_no", "serial_no_series"], !frm.doc.is_fixed_asset); frappe.call({ method: "erpnext.stock.doctype.item.item.get_asset_naming_series", - callback: function(r) { + callback: function (r) { frm.set_value("is_stock_item", frm.doc.is_fixed_asset ? 0 : 1); frm.events.set_asset_naming_series(frm, r.message); - } + }, }); - frm.trigger('auto_create_assets'); + frm.trigger("auto_create_assets"); }, - set_asset_naming_series: function(frm, asset_naming_series) { + set_asset_naming_series: function (frm, asset_naming_series) { if ((frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series) { - let naming_series = (frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series; + let naming_series = + (frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series; frm.set_df_property("asset_naming_series", "options", naming_series); } }, - auto_create_assets: function(frm) { - frm.toggle_reqd(['asset_naming_series'], frm.doc.auto_create_assets); - frm.toggle_display(['asset_naming_series'], frm.doc.auto_create_assets); + auto_create_assets: function (frm) { + frm.toggle_reqd(["asset_naming_series"], frm.doc.auto_create_assets); + frm.toggle_display(["asset_naming_series"], frm.doc.auto_create_assets); }, page_name: frappe.utils.warn_page_name_change, - item_code: function(frm) { - if(!frm.doc.item_name) - frm.set_value("item_name", frm.doc.item_code); + item_code: function (frm) { + if (!frm.doc.item_name) frm.set_value("item_name", frm.doc.item_code); }, - is_stock_item: function(frm) { - if(!frm.doc.is_stock_item) { + is_stock_item: function (frm) { + if (!frm.doc.is_stock_item) { frm.set_value("has_batch_no", 0); frm.set_value("create_new_batch", 0); frm.set_value("has_serial_no", 0); } }, - has_variants: function(frm) { + has_variants: function (frm) { erpnext.item.toggle_attributes(frm); - } + }, }); -frappe.ui.form.on('Item Reorder', { - reorder_levels_add: function(frm, cdt, cdn) { +frappe.ui.form.on("Item Reorder", { + reorder_levels_add: function (frm, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); - var type = frm.doc.default_material_request_type - row.material_request_type = (type == 'Material Transfer')? 'Transfer' : type; - } -}) - -frappe.ui.form.on('Item Customer Detail', { - customer_items_add: function(frm, cdt, cdn) { - frappe.model.set_value(cdt, cdn, 'customer_group', ""); + var type = frm.doc.default_material_request_type; + row.material_request_type = type == "Material Transfer" ? "Transfer" : type; }, - customer_name: function(frm, cdt, cdn) { +}); + +frappe.ui.form.on("Item Customer Detail", { + customer_items_add: function (frm, cdt, cdn) { + frappe.model.set_value(cdt, cdn, "customer_group", ""); + }, + customer_name: function (frm, cdt, cdn) { set_customer_group(frm, cdt, cdn); }, - customer_group: function(frm, cdt, cdn) { - if(set_customer_group(frm, cdt, cdn)){ + customer_group: function (frm, cdt, cdn) { + if (set_customer_group(frm, cdt, cdn)) { frappe.msgprint(__("Changing Customer Group for the selected Customer is not allowed.")); } - } + }, }); -var set_customer_group = function(frm, cdt, cdn) { +var set_customer_group = function (frm, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); if (!row.customer_name) { return false; } - frappe.model.with_doc("Customer", row.customer_name, function() { + frappe.model.with_doc("Customer", row.customer_name, function () { var customer = frappe.model.get_doc("Customer", row.customer_name); row.customer_group = customer.customer_group; refresh_field("customer_group", cdn, "customer_items"); }); return true; -} +}; $.extend(erpnext.item, { - setup_queries: function(frm) { - frm.fields_dict["item_defaults"].grid.get_field("expense_account").get_query = function(doc, cdt, cdn) { + setup_queries: function (frm) { + frm.fields_dict["item_defaults"].grid.get_field("expense_account").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { query: "erpnext.controllers.queries.get_expense_account", - filters: { company: row.company } - } - } - - frm.fields_dict["item_defaults"].grid.get_field("income_account").get_query = function(doc, cdt, cdn) { - const row = locals[cdt][cdn]; - return { - query: "erpnext.controllers.queries.get_income_account", - filters: { company: row.company } - } - } - - frm.fields_dict["item_defaults"].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) { - const row = locals[cdt][cdn]; - return { - filters: { - 'report_type': 'Profit and Loss', - 'company': row.company, - "is_group": 0 - } + filters: { company: row.company }, }; }; - frm.fields_dict["item_defaults"].grid.get_field("buying_cost_center").get_query = function(doc, cdt, cdn) { + frm.fields_dict["item_defaults"].grid.get_field("income_account").get_query = function ( + doc, + cdt, + cdn + ) { + const row = locals[cdt][cdn]; + return { + query: "erpnext.controllers.queries.get_income_account", + filters: { company: row.company }, + }; + }; + + frm.fields_dict["item_defaults"].grid.get_field("default_discount_account").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { filters: { - "is_group": 0, - "company": row.company - } - } - } + report_type: "Profit and Loss", + company: row.company, + is_group: 0, + }, + }; + }; - frm.fields_dict["item_defaults"].grid.get_field("selling_cost_center").get_query = function(doc, cdt, cdn) { + frm.fields_dict["item_defaults"].grid.get_field("buying_cost_center").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { filters: { - "is_group": 0, - "company": row.company - } - } - } + is_group: 0, + company: row.company, + }, + }; + }; + frm.fields_dict["item_defaults"].grid.get_field("selling_cost_center").get_query = function ( + doc, + cdt, + cdn + ) { + const row = locals[cdt][cdn]; + return { + filters: { + is_group: 0, + company: row.company, + }, + }; + }; - frm.fields_dict['taxes'].grid.get_field("tax_type").get_query = function(doc, cdt, cdn) { + frm.fields_dict["taxes"].grid.get_field("tax_type").get_query = function (doc, cdt, cdn) { return { filters: [ - ['Account', 'account_type', 'in', - 'Tax, Chargeable, Income Account, Expense Account'], - ['Account', 'docstatus', '!=', 2] - ] - } - } + ["Account", "account_type", "in", "Tax, Chargeable, Income Account, Expense Account"], + ["Account", "docstatus", "!=", 2], + ], + }; + }; - frm.fields_dict['item_group'].get_query = function(doc, cdt, cdn) { + frm.fields_dict["item_group"].get_query = function (doc, cdt, cdn) { return { - filters: [ - ['Item Group', 'docstatus', '!=', 2] - ] - } - } + filters: [["Item Group", "docstatus", "!=", 2]], + }; + }; - frm.fields_dict["item_defaults"].grid.get_field("deferred_revenue_account").get_query = function(doc, cdt, cdn) { + frm.fields_dict["item_defaults"].grid.get_field("deferred_revenue_account").get_query = function ( + doc, + cdt, + cdn + ) { return { filters: { - "company": locals[cdt][cdn].company, - 'root_type': 'Liability', - "is_group": 0 - } - } - } + company: locals[cdt][cdn].company, + root_type: "Liability", + is_group: 0, + }, + }; + }; - frm.fields_dict["item_defaults"].grid.get_field("deferred_expense_account").get_query = function(doc, cdt, cdn) { + frm.fields_dict["item_defaults"].grid.get_field("deferred_expense_account").get_query = function ( + doc, + cdt, + cdn + ) { return { filters: { - "company": locals[cdt][cdn].company, - 'root_type': 'Asset', - "is_group": 0 - } - } - } + company: locals[cdt][cdn].company, + root_type: "Asset", + is_group: 0, + }, + }; + }; - frm.fields_dict.customer_items.grid.get_field("customer_name").get_query = function(doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.customer_query" } - } + frm.fields_dict.customer_items.grid.get_field("customer_name").get_query = function (doc, cdt, cdn) { + return { query: "erpnext.controllers.queries.customer_query" }; + }; - frm.fields_dict.supplier_items.grid.get_field("supplier").get_query = function(doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.supplier_query" } - } + frm.fields_dict.supplier_items.grid.get_field("supplier").get_query = function (doc, cdt, cdn) { + return { query: "erpnext.controllers.queries.supplier_query" }; + }; - frm.fields_dict["item_defaults"].grid.get_field("default_warehouse").get_query = function(doc, cdt, cdn) { + frm.fields_dict["item_defaults"].grid.get_field("default_warehouse").get_query = function ( + doc, + cdt, + cdn + ) { const row = locals[cdt][cdn]; return { filters: { - "is_group": 0, - "company": row.company - } - } - } + is_group: 0, + company: row.company, + }, + }; + }; - frm.fields_dict.reorder_levels.grid.get_field("warehouse_group").get_query = function(doc, cdt, cdn) { + frm.fields_dict.reorder_levels.grid.get_field("warehouse_group").get_query = function ( + doc, + cdt, + cdn + ) { return { - filters: { "is_group": 1 } - } - } + filters: { is_group: 1 }, + }; + }; - frm.fields_dict.reorder_levels.grid.get_field("warehouse").get_query = function(doc, cdt, cdn) { + frm.fields_dict.reorder_levels.grid.get_field("warehouse").get_query = function (doc, cdt, cdn) { var d = locals[cdt][cdn]; var filters = { - "is_group": 0 - } + is_group: 0, + }; if (d.parent_warehouse) { - filters.extend({"parent_warehouse": d.warehouse_group}) + filters.extend({ parent_warehouse: d.warehouse_group }); } return { - filters: filters - } - } + filters: filters, + }; + }; - frm.set_query('default_provisional_account', 'item_defaults', (doc, cdt, cdn) => { + frm.set_query("default_provisional_account", "item_defaults", (doc, cdt, cdn) => { let row = locals[cdt][cdn]; return { filters: { - "company": row.company, - "root_type": ["in", ["Liability", "Asset"]], - "is_group": 0 - } + company: row.company, + root_type: ["in", ["Liability", "Asset"]], + is_group: 0, + }, }; }); - }, - make_dashboard: function(frm) { - if(frm.doc.__islocal) - return; + make_dashboard: function (frm) { + if (frm.doc.__islocal) return; // Show Stock Levels only if is_stock_item if (frm.doc.is_stock_item) { - frappe.require('item-dashboard.bundle.js', function() { - const section = frm.dashboard.add_section('', __("Stock Levels")); + frappe.require("item-dashboard.bundle.js", function () { + const section = frm.dashboard.add_section("", __("Stock Levels")); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, item_code: frm.doc.name, page_length: 20, - method: 'erpnext.stock.dashboard.item_dashboard.get_data', - template: 'item_dashboard_list' + method: "erpnext.stock.dashboard.item_dashboard.get_data", + template: "item_dashboard_list", }); erpnext.item.item_dashboard.refresh(); }); } }, - edit_prices_button: function(frm) { - frm.add_custom_button(__("Add / Edit Prices"), function() { - frappe.set_route("List", "Item Price", {"item_code": frm.doc.name}); - }, __("Actions")); + edit_prices_button: function (frm) { + frm.add_custom_button( + __("Add / Edit Prices"), + function () { + frappe.set_route("List", "Item Price", { item_code: frm.doc.name }); + }, + __("Actions") + ); }, - weight_to_validate: function(frm) { + weight_to_validate: function (frm) { if (frm.doc.weight_per_unit && !frm.doc.weight_uom) { frappe.msgprint({ message: __("Please mention 'Weight UOM' along with Weight."), - title: __("Note") + title: __("Note"), }); } }, - show_modal_for_manufacturers: function(frm) { + show_modal_for_manufacturers: function (frm) { var dialog = new frappe.ui.Dialog({ fields: [ { - fieldtype: 'Link', - fieldname: 'manufacturer', - options: 'Manufacturer', - label: 'Manufacturer', + fieldtype: "Link", + fieldname: "manufacturer", + options: "Manufacturer", + label: "Manufacturer", reqd: 1, }, { - fieldtype: 'Data', - label: 'Manufacturer Part Number', - fieldname: 'manufacturer_part_no' + fieldtype: "Data", + label: "Manufacturer Part Number", + fieldname: "manufacturer_part_no", }, - ] + ], }); - dialog.set_primary_action(__('Create'), function() { + dialog.set_primary_action(__("Create"), function () { var data = dialog.get_values(); - if(!data) return; + if (!data) return; // call the server to make the variant data.template = frm.doc.name; frappe.call({ method: "erpnext.controllers.item_variant.get_variant", args: data, - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); dialog.hide(); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } + }, }); - }) + }); dialog.show(); }, - show_multiple_variants_dialog: function(frm) { + show_multiple_variants_dialog: function (frm) { var me = this; let promises = []; @@ -505,27 +592,26 @@ $.extend(erpnext.item, { function make_fields_from_attribute_values(attr_dict) { let fields = []; Object.keys(attr_dict).forEach((name, i) => { - if(i % 3 === 0){ - fields.push({fieldtype: 'Section Break'}); + if (i % 3 === 0) { + fields.push({ fieldtype: "Section Break" }); } - fields.push({fieldtype: 'Column Break', label: name}); - attr_dict[name].forEach(value => { + fields.push({ fieldtype: "Column Break", label: name }); + attr_dict[name].forEach((value) => { fields.push({ - fieldtype: 'Check', + fieldtype: "Check", label: value, fieldname: value, default: 0, - onchange: function() { + onchange: function () { let selected_attributes = get_selected_attributes(); let lengths = []; - Object.keys(selected_attributes).map(key => { + Object.keys(selected_attributes).map((key) => { lengths.push(selected_attributes[key].length); }); - if(lengths.includes(0)) { - me.multiple_variant_dialog.get_primary_btn().html(__('Create Variants')); + if (lengths.includes(0)) { + me.multiple_variant_dialog.get_primary_btn().html(__("Create Variants")); me.multiple_variant_dialog.disable_primary_action(); } else { - let no_of_combinations = lengths.reduce((a, b) => a * b, 1); let msg; if (no_of_combinations === 1) { @@ -536,7 +622,7 @@ $.extend(erpnext.item, { me.multiple_variant_dialog.get_primary_btn().html(msg); me.multiple_variant_dialog.enable_primary_action(); } - } + }, }); }); }); @@ -553,38 +639,40 @@ $.extend(erpnext.item, { options: ``, - } - ].concat(fields) + }, + ].concat(fields), }); - me.multiple_variant_dialog.set_primary_action(__('Create Variants'), () => { + me.multiple_variant_dialog.set_primary_action(__("Create Variants"), () => { let selected_attributes = get_selected_attributes(); me.multiple_variant_dialog.hide(); frappe.call({ method: "erpnext.controllers.item_variant.enqueue_multiple_variant_creation", args: { - "item": frm.doc.name, - "args": selected_attributes + item: frm.doc.name, + args: selected_attributes, }, - callback: function(r) { - if (r.message==='queued') { + callback: function (r) { + if (r.message === "queued") { frappe.show_alert({ message: __("Variant creation has been queued."), - indicator: 'orange' + indicator: "orange", }); } else { frappe.show_alert({ message: __("{0} variants created.", [r.message]), - indicator: 'green' + indicator: "green", }); } - } + }, }); }); - $($(me.multiple_variant_dialog.$wrapper.find('.form-column')) - .find('.frappe-control')).css('margin-bottom', '0px'); + $($(me.multiple_variant_dialog.$wrapper.find(".form-column")).find(".frappe-control")).css( + "margin-bottom", + "0px" + ); me.multiple_variant_dialog.disable_primary_action(); me.multiple_variant_dialog.clear(); @@ -593,14 +681,14 @@ $.extend(erpnext.item, { function get_selected_attributes() { let selected_attributes = {}; - me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => { - if(i===0) return; - let attribute_name = $(col).find('.control-label').html().trim(); + me.multiple_variant_dialog.$wrapper.find(".form-column").each((i, col) => { + if (i === 0) return; + let attribute_name = $(col).find(".control-label").html().trim(); selected_attributes[attribute_name] = []; - let checked_opts = $(col).find('.checkbox input'); + let checked_opts = $(col).find(".checkbox input"); checked_opts.each((i, opt) => { - if($(opt).is(':checked')) { - selected_attributes[attribute_name].push($(opt).attr('data-fieldname')); + if ($(opt).is(":checked")) { + selected_attributes[attribute_name].push($(opt).attr("data-fieldname")); } }); }); @@ -608,112 +696,108 @@ $.extend(erpnext.item, { return selected_attributes; } - frm.doc.attributes.forEach(function(d) { - let p = new Promise(resolve => { - if(!d.numeric_values) { - frappe.call({ - method: "frappe.client.get_list", - args: { - doctype: "Item Attribute Value", - filters: [ - ["parent","=", d.attribute] - ], - fields: ["attribute_value"], - limit_page_length: 0, - parent: "Item Attribute", - order_by: "idx" - } - }).then((r) => { - if(r.message) { - attr_val_fields[d.attribute] = r.message.map(function(d) { return d.attribute_value; }); - resolve(); - } - }); - } else { - frappe.call({ - method: "frappe.client.get", - args: { - doctype: "Item Attribute", - name: d.attribute - } - }).then((r) => { - if(r.message) { - const from = r.message.from_range; - const to = r.message.to_range; - const increment = r.message.increment; - - let values = []; - for(var i = from; i <= to; i = flt(i + increment, 6)) { - values.push(i); + frm.doc.attributes.forEach(function (d) { + let p = new Promise((resolve) => { + if (!d.numeric_values) { + frappe + .call({ + method: "frappe.client.get_list", + args: { + doctype: "Item Attribute Value", + filters: [["parent", "=", d.attribute]], + fields: ["attribute_value"], + limit_page_length: 0, + parent: "Item Attribute", + order_by: "idx", + }, + }) + .then((r) => { + if (r.message) { + attr_val_fields[d.attribute] = r.message.map(function (d) { + return d.attribute_value; + }); + resolve(); } - attr_val_fields[d.attribute] = values; - resolve(); - } - }); + }); + } else { + let values = []; + for (var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) { + values.push(i); + } + attr_val_fields[d.attribute] = values; + resolve(); } }); promises.push(p); - }, this); Promise.all(promises).then(() => { let fields = make_fields_from_attribute_values(attr_val_fields); make_and_show_dialog(fields); - }) - + }); }, - show_single_variant_dialog: function(frm) { - var fields = [] + show_single_variant_dialog: function (frm) { + var fields = []; - for(var i=0;i< frm.doc.attributes.length;i++){ + for (var i = 0; i < frm.doc.attributes.length; i++) { var fieldtype, desc; var row = frm.doc.attributes[i]; - if (row.numeric_values){ + if (row.numeric_values) { fieldtype = "Float"; - desc = "Min Value: "+ row.from_range +" , Max Value: "+ row.to_range +", in Increments of: "+ row.increment - } - else { + desc = + "Min Value: " + + row.from_range + + " , Max Value: " + + row.to_range + + ", in Increments of: " + + row.increment; + } else { fieldtype = "Data"; - desc = "" + desc = ""; } fields = fields.concat({ - "label": row.attribute, - "fieldname": row.attribute, - "fieldtype": fieldtype, - "reqd": 0, - "description": desc - }) + label: row.attribute, + fieldname: row.attribute, + fieldtype: fieldtype, + reqd: 0, + description: desc, + }); } var d = new frappe.ui.Dialog({ - title: __('Create Variant'), - fields: fields + title: __("Create Variant"), + fields: fields, }); - d.set_primary_action(__('Create'), function() { + d.set_primary_action(__("Create"), function () { var args = d.get_values(); - if(!args) return; + if (!args) return; frappe.call({ method: "erpnext.controllers.item_variant.get_variant", btn: d.get_primary_btn(), args: { - "template": frm.doc.name, - "args": d.get_values() + template: frm.doc.name, + args: d.get_values(), }, - callback: function(r) { + callback: function (r) { // returns variant item if (r.message) { var variant = r.message; - frappe.msgprint_dialog = frappe.msgprint(__("Item Variant {0} already exists with same attributes", - [repl('%(item)s', { - item_encoded: encodeURIComponent(variant), - item: variant - })] - )); + frappe.msgprint_dialog = frappe.msgprint( + __("Item Variant {0} already exists with same attributes", [ + repl( + '%(item)s', + { + item_encoded: encodeURIComponent(variant), + item: variant, + } + ), + ]) + ); frappe.msgprint_dialog.hide_on_page_refresh = true; - frappe.msgprint_dialog.$wrapper.find(".variant-click").on("click", function() { + frappe.msgprint_dialog.$wrapper.find(".variant-click").on("click", function () { d.hide(); }); } else { @@ -721,24 +805,23 @@ $.extend(erpnext.item, { frappe.call({ method: "erpnext.controllers.item_variant.create_variant", args: { - "item": frm.doc.name, - "args": d.get_values() + item: frm.doc.name, + args: d.get_values(), }, - callback: function(r) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } + }, }); } - } + }, }); }); d.show(); - $.each(d.fields_dict, function(i, field) { - - if(field.df.fieldtype !== "Data") { + $.each(d.fields_dict, function (i, field) { + if (field.df.fieldtype !== "Data") { return; } @@ -754,32 +837,34 @@ $.extend(erpnext.item, { input.field = field; field.$input - .on('input', function(e) { + .on("input", function (e) { var term = e.target.value; frappe.call({ method: "erpnext.stock.doctype.item.item.get_item_attribute", args: { parent: i, - attribute_value: term + attribute_value: term, }, - callback: function(r) { + callback: function (r) { if (r.message) { - e.target.awesomplete.list = r.message.map(function(d) { return d.attribute_value; }); + e.target.awesomplete.list = r.message.map(function (d) { + return d.attribute_value; + }); } - } + }, }); }) - .on('focus', function(e) { - $(e.target).val('').trigger('input'); + .on("focus", function (e) { + $(e.target).val("").trigger("input"); }) .on("awesomplete-open", () => { - let modal = field.$input.parents('.modal-dialog')[0]; + let modal = field.$input.parents(".modal-dialog")[0]; if (modal) { $(modal).removeClass("modal-dialog-scrollable"); } }) .on("awesomplete-close", () => { - let modal = field.$input.parents('.modal-dialog')[0]; + let modal = field.$input.parents(".modal-dialog")[0]; if (modal) { $(modal).addClass("modal-dialog-scrollable"); } @@ -787,14 +872,13 @@ $.extend(erpnext.item, { }); }, - toggle_attributes: function(frm) { - if((frm.doc.has_variants || frm.doc.variant_of) - && frm.doc.variant_based_on==='Item Attribute') { + toggle_attributes: function (frm) { + if ((frm.doc.has_variants || frm.doc.variant_of) && frm.doc.variant_based_on === "Item Attribute") { frm.toggle_display("attributes", true); var grid = frm.fields_dict.attributes.grid; - if(frm.doc.variant_of) { + if (frm.doc.variant_of) { // variant // value column is displayed but not editable @@ -818,78 +902,87 @@ $.extend(erpnext.item, { // enable the grid so you can add more attributes grid.toggle_enable("attribute", true); } - } else { // nothing to do with attributes, hide it frm.toggle_display("attributes", false); } frm.layout.refresh_sections(); - } + }, }); frappe.ui.form.on("UOM Conversion Detail", { - uom: function(frm, cdt, cdn) { + uom: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; if (row.uom) { frappe.call({ method: "erpnext.stock.doctype.item.item.get_uom_conv_factor", args: { - "uom": row.uom, - "stock_uom": frm.doc.stock_uom + uom: row.uom, + stock_uom: frm.doc.stock_uom, }, - callback: function(r) { + callback: function (r) { if (!r.exc && r.message) { frappe.model.set_value(cdt, cdn, "conversion_factor", r.message); } - } + }, }); } - } + }, }); -frappe.tour['Item'] = [ +frappe.tour["Item"] = [ { fieldname: "item_code", title: "Item Code", - description: __("Enter an Item Code, the name will be auto-filled the same as Item Code on clicking inside the Item Name field.") + description: __( + "Enter an Item Code, the name will be auto-filled the same as Item Code on clicking inside the Item Name field." + ), }, { fieldname: "item_group", title: "Item Group", - description: __("Select an Item Group.") + description: __("Select an Item Group."), }, { fieldname: "is_stock_item", title: "Maintain Stock", - description: __("If you are maintaining stock of this Item in your Inventory, ERPNext will make a stock ledger entry for each transaction of this item.") + description: __( + "If you are maintaining stock of this Item in your Inventory, ERPNext will make a stock ledger entry for each transaction of this item." + ), }, { fieldname: "include_item_in_manufacturing", title: "Include Item in Manufacturing", - description: __("This is for raw material Items that'll be used to create finished goods. If the Item is an additional service like 'washing' that'll be used in the BOM, keep this unchecked.") + description: __( + "This is for raw material Items that'll be used to create finished goods. If the Item is an additional service like 'washing' that'll be used in the BOM, keep this unchecked." + ), }, { fieldname: "opening_stock", title: "Opening Stock", - description: __("Enter the opening stock units.") + description: __("Enter the opening stock units."), }, { fieldname: "valuation_rate", title: "Valuation Rate", - description: __("There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit Item Valuation, FIFO and Moving Average.") + description: __( + "There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit Item Valuation, FIFO and Moving Average." + ), }, { fieldname: "standard_rate", title: "Standard Selling Rate", - description: __("When creating an Item, entering a value for this field will automatically create an Item Price at the backend.") + description: __( + "When creating an Item, entering a value for this field will automatically create an Item Price at the backend." + ), }, { fieldname: "item_defaults", title: "Item Defaults", - description: __("In this section, you can define Company-wide transaction-related defaults for this Item. Eg. Default Warehouse, Default Price List, Supplier, etc.") - } - - + description: __( + "In this section, you can define Company-wide transaction-related defaults for this Item. Eg. Default Warehouse, Default Price List, Supplier, etc." + ), + }, ]; function open_form(frm, doctype, child_doctype, parentfield) { @@ -916,7 +1009,7 @@ function open_form(frm, doctype, child_doctype, parentfield) { () => { frappe.flags.ignore_company_party_validation = true; frappe.model.trigger("item_code", frm.doc.name, new_child_doc); - } - ]) + }, + ]); }); } diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 7afc031d87d..b9256c8ee56 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -203,6 +203,7 @@ "label": "Allow Alternative Item" }, { + "allow_in_quick_entry": 1, "bold": 1, "default": "1", "depends_on": "eval:!doc.is_fixed_asset", @@ -240,6 +241,7 @@ "label": "Standard Selling Rate" }, { + "allow_in_quick_entry": 1, "default": "0", "fieldname": "is_fixed_asset", "fieldtype": "Check", @@ -247,6 +249,7 @@ "set_only_once": 1 }, { + "allow_in_quick_entry": 1, "depends_on": "is_fixed_asset", "fieldname": "asset_category", "fieldtype": "Link", @@ -897,7 +900,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2023-09-18 15:41:32.688051", + "modified": "2024-01-08 18:09:30.225085", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 96cec1dd0a2..b2b06e05871 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -3,7 +3,6 @@ import copy import json -from typing import Dict, List, Optional import frappe from frappe import _ @@ -300,9 +299,9 @@ class Item(Document): for d in self.get("uoms"): if cstr(d.uom) in check_list: frappe.throw( - _("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format( - d.uom - ) + _( + "Unit of Measure {0} has been entered more than once in Conversion Factor Table" + ).format(d.uom) ) else: check_list.append(cstr(d.uom)) @@ -354,7 +353,7 @@ class Item(Document): frappe.throw( _("{0} entered twice {1} in Item Taxes").format( frappe.bold(d.item_tax_template), - "for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "", + f"for tax category {frappe.bold(d.tax_category)}" if d.tax_category else "", ) ) else: @@ -373,7 +372,9 @@ class Item(Document): ) if duplicate: frappe.throw( - _("Barcode {0} already used in Item {1}").format(item_barcode.barcode, duplicate[0][0]) + _("Barcode {0} already used in Item {1}").format( + item_barcode.barcode, duplicate[0][0] + ) ) item_barcode.barcode_type = ( @@ -403,9 +404,9 @@ class Item(Document): warehouse_material_request_type += [(d.get("warehouse"), d.get("material_request_type"))] else: frappe.throw( - _("Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.").format( - d.idx, d.warehouse, d.material_request_type - ), + _( + "Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}." + ).format(d.idx, d.warehouse, d.material_request_type), DuplicateReorderRows, ) @@ -477,20 +478,21 @@ class Item(Document): for dt in ("Sales Taxes and Charges", "Purchase Taxes and Charges"): for d in frappe.db.sql( - """select name, item_wise_tax_detail from `tab{0}` - where ifnull(item_wise_tax_detail, '') != ''""".format( - dt - ), + f"""select name, item_wise_tax_detail from `tab{dt}` + where ifnull(item_wise_tax_detail, '') != ''""", as_dict=1, ): - item_wise_tax_detail = json.loads(d.item_wise_tax_detail) if isinstance(item_wise_tax_detail, dict) and old_name in item_wise_tax_detail: item_wise_tax_detail[new_name] = item_wise_tax_detail[old_name] item_wise_tax_detail.pop(old_name) frappe.db.set_value( - dt, d.name, "item_wise_tax_detail", json.dumps(item_wise_tax_detail), update_modified=False + dt, + d.name, + "item_wise_tax_detail", + json.dumps(item_wise_tax_detail), + update_modified=False, ) def delete_old_bins(self, old_name): @@ -517,9 +519,7 @@ class Item(Document): ) msg += "
              " - msg += ( - ", ".join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "

              " - ) + msg += ", ".join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "

              " msg += _( "Note: To merge the items, create a separate Stock Reconciliation for the old item {0}" @@ -542,12 +542,8 @@ class Item(Document): def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): "Block merge if both old and new items have product bundles." - old_bundle = frappe.get_value( - "Product Bundle", filters={"new_item_code": old_name, "disabled": 0} - ) - new_bundle = frappe.get_value( - "Product Bundle", filters={"new_item_code": new_name, "disabled": 0} - ) + old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name, "disabled": 0}) + new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name, "disabled": 0}) if old_bundle and new_bundle: bundle_link = get_link_to_form("Product Bundle", old_bundle) @@ -572,7 +568,7 @@ class Item(Document): if len(web_items) <= 1: return - old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0] + old_web_item = next(d.get("name") for d in web_items if d.get("item_code") == old_name) web_item_link = get_link_to_form("Website Item", old_web_item) old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) @@ -586,9 +582,7 @@ class Item(Document): def recalculate_bin_qty(self, new_name): from erpnext.stock.stock_balance import repost_stock - existing_allow_negative_stock = frappe.db.get_value( - "Stock Settings", None, "allow_negative_stock" - ) + existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) repost_stock_for_warehouses = frappe.get_all( @@ -605,9 +599,7 @@ class Item(Document): for warehouse in repost_stock_for_warehouses: repost_stock(new_name, warehouse) - frappe.db.set_value( - "Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock - ) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) def update_bom_item_desc(self): if self.is_new(): @@ -768,16 +760,14 @@ class Item(Document): return "
              ".join(docnames) def table_row(title, body): - return """ - {0} - {1} - """.format( - title, body - ) + return f""" + {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 = _( @@ -785,17 +775,15 @@ class Item(Document): ) message = """ -
              {0}

              +
              {}

              - - + + - {3} + {}
              {1}{2}{}{}
              - """.format( - error_description, _("Variant Items"), _("Attributes"), rows - ) + """.format(error_description, _("Variant Items"), _("Attributes"), rows) frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True) @@ -925,7 +913,7 @@ class Item(Document): frappe.throw(msg, title=_("Linked with submitted documents")) - def _get_linked_submitted_documents(self, changed_fields: List[str]) -> Optional[Dict[str, str]]: + def _get_linked_submitted_documents(self, changed_fields: list[str]) -> dict[str, str] | None: linked_doctypes = [ "Delivery Note Item", "Sales Invoice Item", @@ -1047,6 +1035,7 @@ def validate_cancelled_item(item_code, docstatus=None): frappe.throw(_("Item {0} is cancelled").format(item_code)) +@frappe.request_cache def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): """returns last purchase details in stock uom""" # get last purchase order item details @@ -1087,17 +1076,13 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): last_purchase_receipt and last_purchase_receipt[0].posting_date or "1900-01-01" ) - if last_purchase_order and ( - purchase_order_date >= purchase_receipt_date or not last_purchase_receipt - ): + if last_purchase_order and (purchase_order_date >= purchase_receipt_date or not last_purchase_receipt): # use purchase order last_purchase = last_purchase_order[0] purchase_date = purchase_order_date - elif last_purchase_receipt and ( - purchase_receipt_date > purchase_order_date or not last_purchase_order - ): + elif last_purchase_receipt and (purchase_receipt_date > purchase_order_date or not last_purchase_order): # use purchase receipt last_purchase = last_purchase_receipt[0] purchase_date = purchase_receipt_date @@ -1304,7 +1289,7 @@ def set_item_tax_from_hsn_code(item): pass -def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None: +def validate_item_default_company_links(item_defaults: list[ItemDefault]) -> None: for item_default in item_defaults: for doctype, field in [ ["Warehouse", "default_warehouse"], diff --git a/erpnext/stock/doctype/item/item_list.js b/erpnext/stock/doctype/item/item_list.js index 22d38e88935..aef0b2a648b 100644 --- a/erpnext/stock/doctype/item/item_list.js +++ b/erpnext/stock/doctype/item/item_list.js @@ -1,9 +1,8 @@ -frappe.listview_settings['Item'] = { - add_fields: ["item_name", "stock_uom", "item_group", "image", - "has_variants", "end_of_life", "disabled"], +frappe.listview_settings["Item"] = { + add_fields: ["item_name", "stock_uom", "item_group", "image", "has_variants", "end_of_life", "disabled"], filters: [["disabled", "=", "0"]], - get_indicator: function(doc) { + get_indicator: function (doc) { if (doc.disabled) { return [__("Disabled"), "grey", "disabled,=,Yes"]; } else if (doc.end_of_life && doc.end_of_life < frappe.datetime.get_today()) { @@ -17,24 +16,23 @@ frappe.listview_settings['Item'] = { reports: [ { - name: 'Stock Summary', - report_type: 'Page', - route: 'stock-balance' + name: "Stock Summary", + report_type: "Page", + route: "stock-balance", }, { - name: 'Stock Ledger', - report_type: 'Script Report' + name: "Stock Ledger", + report_type: "Script Report", }, { - name: 'Stock Balance', - report_type: 'Script Report' + name: "Stock Balance", + report_type: "Script Report", }, { - name: 'Stock Projected Qty', - report_type: 'Script Report' - } - - ] + name: "Stock Projected Qty", + report_type: "Script Report", + }, + ], }; frappe.help.youtube_id["Item"] = "qXaEwld4_Ps"; diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 22015e8a65d..d486e972771 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -32,7 +32,7 @@ test_ignore = ["BOM"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] -def make_item(item_code=None, properties=None, uoms=None): +def make_item(item_code=None, properties=None, uoms=None, barcode=None): if not item_code: item_code = frappe.generate_hash(length=16) @@ -61,6 +61,14 @@ def make_item(item_code=None, properties=None, uoms=None): for uom in uoms: item.append("uoms", uom) + if barcode: + item.append( + "barcodes", + { + "barcode": barcode, + }, + ) + item.insert() return item @@ -315,7 +323,6 @@ class TestItem(FrappeTestCase): self.assertEqual(value, purchase_item_details.get(key)) def test_item_default_validations(self): - with self.assertRaises(frappe.ValidationError) as ve: make_item( "Bad Item defaults", @@ -469,9 +476,7 @@ class TestItem(FrappeTestCase): self.assertFalse(frappe.db.exists("Item", old)) - self.assertTrue( - frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse - _TC"}) - ) + self.assertTrue(frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse - _TC"})) self.assertTrue( frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"}) ) @@ -524,7 +529,7 @@ class TestItem(FrappeTestCase): def test_item_variant_by_manufacturer(self): template = make_item( "_Test Item Variant By Manufacturer", {"has_variants": 1, "variant_based_on": "Manufacturer"} - ) + ).name for manufacturer in ["DFSS", "DASA", "ASAAS"]: if not frappe.db.exists("Manufacturer", manufacturer): @@ -715,9 +720,7 @@ class TestItem(FrappeTestCase): @change_settings("Stock Settings", {"sample_retention_warehouse": "_Test Warehouse - _TC"}) def test_retain_sample(self): - item = make_item( - "_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1} - ) + item = make_item("_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1}) self.assertEqual(item.has_batch_no, 1) self.assertEqual(item.retain_sample, 1) @@ -790,7 +793,7 @@ class TestItem(FrappeTestCase): def test_customer_codes_length(self): """Check if item code with special characters are allowed.""" item = make_item(properties={"item_code": "Test Item Code With Special Characters"}) - for row in range(3): + for _row in range(3): item.append("customer_items", {"ref_code": frappe.generate_hash("", 120)}) item.save() self.assertTrue(len(item.customer_code) > 140) @@ -831,9 +834,7 @@ class TestItem(FrappeTestCase): make_property_setter("Item", None, "search_fields", "item_name", "Data", for_doctype="Doctype") item = make_item(properties={"item_name": "Test Item", "description": "Test Description"}) - data = item_query( - "Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True - ) + data = item_query("Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True) self.assertEqual(data[0].name, item.name) self.assertEqual(data[0].item_name, item.item_name) self.assertTrue("description" not in data[0]) @@ -841,9 +842,7 @@ class TestItem(FrappeTestCase): make_property_setter( "Item", None, "search_fields", "item_name, description", "Data", for_doctype="Doctype" ) - data = item_query( - "Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True - ) + data = item_query("Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True) self.assertEqual(data[0].name, item.name) self.assertEqual(data[0].item_name, item.item_name) self.assertEqual(data[0].description, item.description) diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.js b/erpnext/stock/doctype/item_alternative/item_alternative.js index ef0a88b9c81..e384e576bd2 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.js +++ b/erpnext/stock/doctype/item_alternative/item_alternative.js @@ -1,14 +1,14 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Item Alternative', { - setup: function(frm) { +frappe.ui.form.on("Item Alternative", { + setup: function (frm) { frm.fields_dict.item_code.get_query = () => { return { filters: { - 'allow_alternative_item': 1 - } + allow_alternative_item: 1, + }, }; }; - } + }, }); diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index 0c24d3c780f..3ef7553853f 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -30,9 +30,7 @@ class ItemAlternative(Document): "allow_alternative_item", ] item_data = frappe.db.get_value("Item", self.item_code, fields, as_dict=1) - alternative_item_data = frappe.db.get_value( - "Item", self.alternative_item_code, fields, as_dict=1 - ) + alternative_item_data = frappe.db.get_value("Item", self.alternative_item_code, fields, as_dict=1) for field in fields: if item_data.get(field) != alternative_item_data.get(field): @@ -72,14 +70,12 @@ class ItemAlternative(Document): @frappe.validate_and_sanitize_search_inputs def get_alternative_items(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( - """ (select alternative_item_code from `tabItem Alternative` + f""" (select alternative_item_code from `tabItem Alternative` where item_code = %(item_code)s and alternative_item_code like %(txt)s) union (select item_code from `tabItem Alternative` where alternative_item_code = %(item_code)s and item_code like %(txt)s - and two_way = 1) limit {1} offset {0} - """.format( - start, page_len - ), + and two_way = 1) limit {page_len} offset {start} + """, {"item_code": filters.get("item_code"), "txt": "%" + txt + "%"}, ) diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index 199641803ed..11e45ee06ca 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -54,9 +54,7 @@ class TestItemAlternative(FrappeTestCase): "fg_item_qty": 5, }, ] - sco = get_subcontracting_order( - service_items=service_items, supplier_warehouse=supplier_warehouse - ) + sco = get_subcontracting_order(service_items=service_items, supplier_warehouse=supplier_warehouse) rm_items = [ { "item_code": "Test Finished Goods - A", @@ -106,9 +104,7 @@ class TestItemAlternative(FrappeTestCase): "reserved_qty_for_sub_contract", ) - self.assertEqual( - after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5) - ) + self.assertEqual(after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5)) scr = make_subcontracting_receipt(sco.name) scr.save() @@ -159,9 +155,7 @@ class TestItemAlternative(FrappeTestCase): "reserved_qty_for_production", ) - self.assertEqual( - reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5) - ) + self.assertEqual(reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5)) ste1 = frappe.get_doc(make_stock_entry(pro_order.name, "Manufacture", 5)) status = False diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.js b/erpnext/stock/doctype/item_attribute/item_attribute.js index f253e22327f..22c7978ac3c 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.js +++ b/erpnext/stock/doctype/item_attribute/item_attribute.js @@ -1,6 +1,4 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Item Attribute', { - -}); +frappe.ui.form.on("Item Attribute", {}); diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index ac4c313e28a..e714cd27504 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -75,7 +75,9 @@ class ItemAttribute(Document): values, abbrs = [], [] for d in self.item_attribute_values: if d.attribute_value.lower() in map(str.lower, values): - frappe.throw(_("Attribute value: {0} must appear only once").format(d.attribute_value.title())) + frappe.throw( + _("Attribute value: {0} must appear only once").format(d.attribute_value.title()) + ) values.append(d.attribute_value) if d.abbr.lower() in map(str.lower, abbrs): diff --git a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.js b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.js index a4df923f039..21803720f1f 100644 --- a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.js +++ b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Item Manufacturer', { +frappe.ui.form.on("Item Manufacturer", { // refresh: function(frm) { - // } }); diff --git a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py index b65ba98a8bf..09f3ca0374c 100644 --- a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py +++ b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py @@ -41,7 +41,9 @@ class ItemManufacturer(Document): # if unchecked and default in Item master, clear it. if default_manufacturer == self.manufacturer and default_part_no == self.manufacturer_part_no: frappe.db.set_value( - "Item", item.name, {"default_item_manufacturer": None, "default_manufacturer_part_no": None} + "Item", + item.name, + {"default_item_manufacturer": None, "default_manufacturer_part_no": None}, ) elif self.is_default: diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js index 8a4b4eef0ae..a5599e340a6 100644 --- a/erpnext/stock/doctype/item_price/item_price.js +++ b/erpnext/stock/doctype/item_price/item_price.js @@ -3,11 +3,11 @@ frappe.ui.form.on("Item Price", { setup(frm) { - frm.set_query("item_code", function() { + frm.set_query("item_code", function () { return { filters: { - "has_variants": 0 - } + has_variants: 0, + }, }; }); }, @@ -23,15 +23,18 @@ frappe.ui.form.on("Item Price", { frm.add_fetch("item_code", "description", "item_description"); frm.add_fetch("item_code", "stock_uom", "uom"); - frm.set_df_property("bulk_import_help", "options", - '' + __("Import in Bulk") + ''); + frm.set_df_property( + "bulk_import_help", + "options", + '' + __("Import in Bulk") + "" + ); - frm.set_query('batch_no', function() { + frm.set_query("batch_no", function () { return { filters: { - 'item': frm.doc.item_code - } + item: frm.doc.item_code, + }, }; }); - } + }, }); diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index f4d9bb0742d..e83730fa23d 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -104,7 +104,8 @@ "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "bold": 1, @@ -220,7 +221,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-11-15 08:26:04.041861", + "modified": "2024-03-13 12:23:39.630290", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 54d1ae634f5..a4df337af93 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -40,7 +40,7 @@ class ItemPrice(Document): if not price_list_details: link = frappe.utils.get_link_to_form("Price List", self.price_list) - frappe.throw("The price list {0} does not exist or is disabled".format(link)) + frappe.throw(f"The price list {link} does not exist or is disabled") self.buying, self.selling, self.currency = price_list_details @@ -57,7 +57,6 @@ class ItemPrice(Document): frappe.throw(_(msg)) def check_duplicates(self): - item_price = frappe.qb.DocType("Item Price") query = ( diff --git a/erpnext/stock/doctype/item_price/item_price_list.js b/erpnext/stock/doctype/item_price/item_price_list.js index 48158393f67..94c232e2e65 100644 --- a/erpnext/stock/doctype/item_price/item_price_list.js +++ b/erpnext/stock/doctype/item_price/item_price_list.js @@ -1,3 +1,3 @@ -frappe.listview_settings['Item Price'] = { +frappe.listview_settings["Item Price"] = { hide_name_column: true, }; diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js index 058783c4ae3..4c49ce99510 100644 --- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js +++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js @@ -1,24 +1,39 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Item Variant Settings', { - refresh: function(frm) { +frappe.ui.form.on("Item Variant Settings", { + refresh: function (frm) { const allow_fields = []; - const existing_fields = frm.doc.fields.map(row => row.field_name); - const exclude_fields = [...existing_fields, "naming_series", "item_code", "item_name", - "published_in_website", "standard_rate", "opening_stock", "image", - "variant_of", "valuation_rate", "barcodes", "has_variants", "attributes"]; + const existing_fields = frm.doc.fields.map((row) => row.field_name); + const exclude_fields = [ + ...existing_fields, + "naming_series", + "item_code", + "item_name", + "published_in_website", + "standard_rate", + "opening_stock", + "image", + "variant_of", + "valuation_rate", + "barcodes", + "has_variants", + "attributes", + ]; - const exclude_field_types = ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only']; + const exclude_field_types = ["HTML", "Section Break", "Column Break", "Button", "Read Only"]; - frappe.model.with_doctype('Item', () => { + frappe.model.with_doctype("Item", () => { const field_label_map = {}; - frappe.get_meta('Item').fields.forEach(d => { + frappe.get_meta("Item").fields.forEach((d) => { field_label_map[d.fieldname] = __(d.label) + ` (${d.fieldname})`; - if (!in_list(exclude_field_types, d.fieldtype) - && !d.no_copy && !in_list(exclude_fields, d.fieldname)) { + if ( + !in_list(exclude_field_types, d.fieldtype) && + !d.no_copy && + !in_list(exclude_fields, d.fieldname) + ) { allow_fields.push({ label: field_label_map[d.fieldname], value: d.fieldname, @@ -33,9 +48,7 @@ frappe.ui.form.on('Item Variant Settings', { }); } - frm.fields_dict.fields.grid.update_docfield_property( - 'field_name', 'options', allow_fields - ); + frm.fields_dict.fields.grid.update_docfield_property("field_name", "options", allow_fields); }); - } + }, }); diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py index cec5e218cca..7a6d39045c4 100644 --- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py +++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py @@ -1,6 +1,6 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - +import typing import frappe from frappe import _ @@ -8,7 +8,7 @@ from frappe.model.document import Document class ItemVariantSettings(Document): - invalid_fields_for_copy_fields_in_variants = ["barcodes"] + invalid_fields_for_copy_fields_in_variants: typing.ClassVar[list] = ["barcodes"] def set_default_fields(self): self.fields = [] @@ -24,7 +24,6 @@ class ItemVariantSettings(Document): "description", "variant_of", "valuation_rate", - "description", "barcodes", "has_variants", "attributes", @@ -50,4 +49,6 @@ class ItemVariantSettings(Document): def validate(self): for d in self.fields: if d.field_name in self.invalid_fields_for_copy_fields_in_variants: - frappe.throw(_("Cannot set the field {0} for copying in variants").format(d.field_name)) + frappe.throw( + _("Cannot set the field {0} for copying in variants").format(d.field_name) + ) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 7f0dc2df9f3..f6b7e0d8435 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -38,6 +38,7 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() self.validate_receipt_documents() + self.validate_line_items() init_landed_taxes_and_totals(self) self.set_total_taxes_and_charges() if not self.get("items"): @@ -45,6 +46,26 @@ class LandedCostVoucher(Document): self.set_applicable_charges_on_item() + def validate_line_items(self): + for d in self.get("items"): + if ( + d.docstatus == 0 + and d.purchase_receipt_item + and not frappe.db.exists( + d.receipt_document_type + " Item", + {"name": d.purchase_receipt_item, "parent": d.receipt_document}, + ) + ): + frappe.throw( + _("Row {0}: {2} Item {1} does not exist in {2} {3}").format( + d.idx, + frappe.bold(d.purchase_receipt_item), + d.receipt_document_type, + frappe.bold(d.receipt_document), + ), + title=_("Incorrect Reference Document (Purchase Receipt Item)"), + ) + def check_mandatory(self): if not self.get("purchase_receipts"): frappe.throw(_("Please enter Receipt Document")) @@ -55,13 +76,13 @@ class LandedCostVoucher(Document): for d in self.get("purchase_receipts"): docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") if docstatus != 1: - msg = ( - f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" - ) + msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" frappe.throw(_(msg), title=_("Invalid Document")) if d.receipt_document_type == "Purchase Invoice": - update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock") + update_stock = frappe.db.get_value( + d.receipt_document_type, d.receipt_document, "update_stock" + ) if not update_stock: msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format( d.idx, frappe.bold(d.receipt_document) @@ -111,7 +132,8 @@ class LandedCostVoucher(Document): ) item.applicable_charges = flt( - flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), + flt(item.get(based_on_field)) + * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), item.precision("applicable_charges"), ) total_charges += item.applicable_charges @@ -122,6 +144,13 @@ class LandedCostVoucher(Document): self.get("items")[item_count - 1].applicable_charges += diff def validate_applicable_charges_for_item(self): + if self.distribute_charges_based_on == "Distribute Manually" and len(self.taxes) > 1: + frappe.throw( + _( + "Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher." + ) + ) + based_on = self.distribute_charges_based_on.lower() if based_on != "distribute manually": @@ -167,7 +196,8 @@ class LandedCostVoucher(Document): for d in self.get("purchase_receipts"): doc = frappe.get_doc(d.receipt_document_type, d.receipt_document) # check if there are {qty} assets created and linked to this receipt document - self.validate_asset_qty_and_status(d.receipt_document_type, doc) + if self.docstatus != 2: + self.validate_asset_qty_and_status(d.receipt_document_type, doc) # set landed cost voucher amount in pr item doc.set_landed_cost_voucher_amount() @@ -201,28 +231,28 @@ class LandedCostVoucher(Document): for item in self.get("items"): if item.is_fixed_asset: receipt_document_type = ( - "purchase_invoice" if item.receipt_document_type == "Purchase Invoice" else "purchase_receipt" + "purchase_invoice" + if item.receipt_document_type == "Purchase Invoice" + else "purchase_receipt" ) docs = frappe.db.get_all( "Asset", filters={receipt_document_type: item.receipt_document, "item_code": item.item_code}, fields=["name", "docstatus"], ) - if not docs or len(docs) != item.qty: + if not docs or len(docs) < item.qty: frappe.throw( _( - "There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document." - ).format(item.receipt_document, item.qty) + "There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document." + ).format(len(docs), item.receipt_document, item.qty) ) if docs: for d in docs: if d.docstatus == 1: frappe.throw( _( - "{2} {0} has submitted Assets. Remove Item {1} from table to continue." - ).format( - item.receipt_document, item.item_code, item.receipt_document_type - ) + "{0} {1} has submitted Assets. Remove Item {2} from table to continue." + ).format(item.receipt_document_type, item.receipt_document, item.item_code) ) def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): @@ -231,10 +261,10 @@ class LandedCostVoucher(Document): serial_nos = get_serial_nos(item.serial_no) if serial_nos: frappe.db.sql( - "update `tabSerial No` set purchase_rate=%s where name in ({0})".format( + "update `tabSerial No` set purchase_rate=%s where name in ({})".format( ", ".join(["%s"] * len(serial_nos)) ), - tuple([item.valuation_rate] + serial_nos), + tuple([item.valuation_rate, *serial_nos]), ) diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 00fa1686c0d..51c44f60676 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -62,9 +62,7 @@ class TestLandedCostVoucher(FrappeTestCase): as_dict=1, ) - self.assertEqual( - last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction - ) + self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0) # assert after submit @@ -87,7 +85,6 @@ class TestLandedCostVoucher(FrappeTestCase): self.assertPurchaseReceiptLCVGLEntries(pr) def assertPurchaseReceiptLCVGLEntries(self, pr): - gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) @@ -170,9 +167,7 @@ class TestLandedCostVoucher(FrappeTestCase): as_dict=1, ) - self.assertEqual( - last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction - ) + self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) def test_landed_cost_voucher_for_zero_purchase_rate(self): @@ -229,7 +224,6 @@ class TestLandedCostVoucher(FrappeTestCase): ) def test_landed_cost_voucher_against_purchase_invoice(self): - pi = make_purchase_invoice( update_stock=1, posting_date=frappe.utils.nowdate(), @@ -274,9 +268,7 @@ class TestLandedCostVoucher(FrappeTestCase): as_dict=1, ) - self.assertEqual( - last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction - ) + self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) @@ -365,9 +357,7 @@ class TestLandedCostVoucher(FrappeTestCase): new_purchase_rate = serial_no_rate + charges - serial_no = frappe.db.get_value( - "Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1 - ) + serial_no = frappe.db.get_value("Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1) self.assertEqual(serial_no.purchase_rate, new_purchase_rate) @@ -392,7 +382,7 @@ class TestLandedCostVoucher(FrappeTestCase): do_not_save=True, ) pr.items[0].cost_center = "Main - TCP1" - for x in range(2): + for _x in range(2): pr.append( "items", { diff --git a/erpnext/stock/doctype/manufacturer/manufacturer.js b/erpnext/stock/doctype/manufacturer/manufacturer.js index bb7e314e14e..1af6e4cb26d 100644 --- a/erpnext/stock/doctype/manufacturer/manufacturer.js +++ b/erpnext/stock/doctype/manufacturer/manufacturer.js @@ -1,16 +1,15 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Manufacturer', { - refresh: function(frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Manufacturer' }; +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']); + hide_field(["address_html", "contact_html"]); frappe.contacts.clear_address_and_contact(frm); - } - else { - unhide_field(['address_html','contact_html']); + } else { + unhide_field(["address_html", "contact_html"]); frappe.contacts.render_address_and_contact(frm); } - } + }, }); diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index ec075bb6bad..20bfbef8f49 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -199,9 +199,9 @@ frappe.ui.form.on('Material Request', { get_item_data: function(frm, item, overwrite_warehouse=false) { if (item && !item.item_code) { return; } - frm.call({ + + frappe.call({ method: "erpnext.stock.get_item_details.get_item_details", - child: item, args: { args: { item_code: item.item_code, @@ -226,12 +226,22 @@ frappe.ui.form.on('Material Request', { }, callback: function(r) { const d = item; - const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty']; + const allow_to_change_fields = ['actual_qty', 'projected_qty', 'min_order_qty', 'item_name', 'description', 'stock_uom', 'uom', 'conversion_factor', 'stock_qty']; if(!r.exc) { - $.each(r.message, function(k, v) { - if(!d[k] || in_list(qty_fields, k)) d[k] = v; + $.each(r.message, function(key, value) { + if(!d[key] || allow_to_change_fields.includes(key)) { + d[key] = value; + } }); + + if (d.price_list_rate != r.message.price_list_rate) { + d.rate = 0.0; + d.price_list_rate = r.message.price_list_rate; + frappe.model.set_value(d.doctype, d.name, "rate", d.price_list_rate); + } + + refresh_field("items"); } } }); @@ -243,7 +253,7 @@ frappe.ui.form.on('Material Request', { fields: [ {"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"), options:"BOM", reqd: 1, get_query: function() { - return {filters: { docstatus:1 }}; + return {filters: { docstatus:1, "is_active": 1 }}; }}, {"fieldname":"warehouse", "fieldtype":"Link", "label":__("For Warehouse"), options:"Warehouse", reqd: 1}, @@ -428,9 +438,11 @@ frappe.ui.form.on("Material Request Item", { frm.events.get_item_data(frm, item, false); }, - rate: function(frm, doctype, name) { + rate(frm, doctype, name) { const item = locals[doctype][name]; - frm.events.get_item_data(frm, item, false); + item.amount = flt(item.qty) * flt(item.rate); + frappe.model.set_value(doctype, name, "amount", item.amount); + refresh_field("amount", item.name, item.parentfield); }, item_code: function(frm, doctype, name) { @@ -450,7 +462,12 @@ frappe.ui.form.on("Material Request Item", { set_schedule_date(frm); } } - } + }, + + conversion_factor: function(frm, doctype, name) { + const item = locals[doctype][name]; + frm.events.get_item_data(frm, item, false); + }, }); erpnext.buying.MaterialRequestController = class MaterialRequestController extends erpnext.buying.BuyingController { @@ -515,6 +532,13 @@ erpnext.buying.MaterialRequestController = class MaterialRequestController exten schedule_date() { set_schedule_date(this.frm); } + + qty(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + row.amount = flt(row.qty) * flt(row.rate); + frappe.model.set_value(cdt, cdn, "amount", row.amount); + refresh_field("amount", row.name, row.parentfield); + } }; // for backward compatibility: combine new and previous states diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 659bc42f0a6..9381bbb4706 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -33,10 +33,10 @@ class MaterialRequest(BuyingController): so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}} for d in self.get("items"): if d.sales_order: - if not d.sales_order in so_items: + if d.sales_order not in so_items: so_items[d.sales_order] = {d.item_code: flt(d.qty)} else: - if not d.item_code in so_items[d.sales_order]: + if d.item_code not in so_items[d.sales_order]: so_items[d.sales_order][d.item_code] = flt(d.qty) else: so_items[d.sales_order][d.item_code] += flt(d.qty) @@ -61,13 +61,13 @@ class MaterialRequest(BuyingController): if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty): frappe.throw( - _("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format( - actual_so_qty - already_indented, item, so_no - ) + _( + "Material Request of maximum {0} can be made for Item {1} against Sales Order {2}" + ).format(actual_so_qty - already_indented, item, so_no) ) def validate(self): - super(MaterialRequest, self).validate() + super().validate() self.validate_schedule_date() self.check_for_on_hold_or_closed_status("Sales Order", "sales_order") @@ -123,7 +123,9 @@ class MaterialRequest(BuyingController): def on_submit(self): self.update_requested_qty_in_production_plan() self.update_requested_qty() - if self.material_request_type == "Purchase": + if self.material_request_type == "Purchase" and frappe.db.exists( + "Budget", {"applicable_on_material_request": 1, "docstatus": 1} + ): self.validate_budget() def before_save(self): @@ -139,12 +141,8 @@ class MaterialRequest(BuyingController): self.set_status(update=True, status="Cancelled") def check_modified_date(self): - mod_db = frappe.db.sql( - """select modified from `tabMaterial Request` where name = %s""", self.name - ) - date_diff = frappe.db.sql( - """select TIMEDIFF('%s', '%s')""" % (mod_db[0][0], cstr(self.modified)) - ) + mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""", self.name) + date_diff = frappe.db.sql(f"""select TIMEDIFF('{mod_db[0][0]}', '{cstr(self.modified)}')""") if date_diff and date_diff[0][0]: frappe.throw(_("{0} {1} has been modified. Please refresh.").format(_(self.doctype), self.name)) @@ -324,9 +322,7 @@ def update_completed_and_requested_qty(stock_entry, method): def set_missing_values(source, target_doc): - if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate( - nowdate() - ): + if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(nowdate()): target_doc.schedule_date = None target_doc.run_method("set_missing_values") target_doc.run_method("calculate_taxes_and_totals") @@ -415,6 +411,7 @@ def make_purchase_order(source_name, target_doc=None, args=None): postprocess, ) + doclist.set_onload("load_after_mapping", False) return doclist @@ -455,9 +452,7 @@ def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=Non target_doc.schedule_date = None target_doc.set( "items", - [ - d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0 - ], + [d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0], ) set_missing_values(source, target_doc) @@ -509,7 +504,7 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa if filters.get("transaction_date"): date = filters.get("transaction_date")[1] - conditions += "and mr.transaction_date between '{0}' and '{1}' ".format(date[0], date[1]) + conditions += f"and mr.transaction_date between '{date[0]}' and '{date[1]}' " supplier = filters.get("supplier") supplier_items = get_items_based_on_default_supplier(supplier) @@ -521,18 +516,18 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa """select distinct mr.name, transaction_date,company from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item where mr.name = mr_item.parent - and mr_item.item_code in ({0}) + and mr_item.item_code in ({}) and mr.material_request_type = 'Purchase' and mr.per_ordered < 99.99 and mr.docstatus = 1 and mr.status != 'Stopped' and mr.company = %s - {1} + {} order by mr_item.item_code ASC - limit {2} offset {3} """.format( + limit {} offset {} """.format( ", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start) ), - tuple(supplier_items) + (filters.get("company"),), + (*tuple(supplier_items), filters.get("company")), as_dict=1, ) @@ -547,17 +542,28 @@ def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filte for d in doc.items: item_list.append(d.item_code) - return frappe.db.sql( - """select default_supplier - from `tabItem Default` - where parent in ({0}) and - default_supplier IS NOT NULL - """.format( - ", ".join(["%s"] * len(item_list)) - ), - tuple(item_list), + supplier = frappe.qb.DocType("Supplier") + item_default = frappe.qb.DocType("Item Default") + query = ( + frappe.qb.from_(supplier) + .left_join(item_default) + .on(supplier.name == item_default.default_supplier) + .select(item_default.default_supplier) + .where( + (item_default.parent.isin(item_list)) + & (item_default.default_supplier.notnull()) + & (supplier[searchfield].like(f"%{txt}%")) + ) + .offset(start) + .limit(page_len) ) + meta = frappe.get_meta("Supplier") + if meta.show_title_field_in_link and meta.title_field: + query = query.select(supplier[meta.title_field]) + + return query.run(as_dict=False) + @frappe.whitelist() def make_supplier_quotation(source_name, target_doc=None): @@ -649,7 +655,10 @@ def make_stock_entry(source_name, target_doc=None): "doctype": "Stock Entry", "validation": { "docstatus": ["=", 1], - "material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]], + "material_request_type": [ + "in", + ["Material Transfer", "Material Issue", "Customer Provided"], + ], }, }, "Material Request Item": { @@ -679,9 +688,7 @@ def raise_work_orders(material_request): mr = frappe.get_doc("Material Request", material_request) errors = [] work_orders = [] - default_wip_warehouse = frappe.db.get_single_value( - "Manufacturing Settings", "default_wip_warehouse" - ) + default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") for d in mr.items: if (d.stock_qty - d.ordered_qty) > 0: diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index de7a3d05bf5..ee5c9e7b86c 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -1,8 +1,8 @@ -frappe.listview_settings['Material Request'] = { +frappe.listview_settings["Material Request"] = { add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"], - get_indicator: function(doc) { + get_indicator: function (doc) { var precision = frappe.defaults.get_default("float_precision"); - if (doc.status=="Stopped") { + if (doc.status == "Stopped") { return [__("Stopped"), "red", "status,=,Stopped"]; } else if (doc.transfer_status && doc.docstatus != 2) { if (doc.transfer_status == "Not Started") { @@ -12,12 +12,16 @@ frappe.listview_settings['Material Request'] = { } else if (doc.transfer_status == "Completed") { return [__("Completed"), "green"]; } - } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 0) { + } else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) { return [__("Pending"), "orange", "per_ordered,=,0"]; - } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) < 100) { + } else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) < 100) { return [__("Partially ordered"), "yellow", "per_ordered,<,100"]; - } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 100) { - if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) < 100 && flt(doc.per_received, precision) > 0) { + } else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 100) { + if ( + doc.material_request_type == "Purchase" && + flt(doc.per_received, precision) < 100 && + flt(doc.per_received, precision) > 0 + ) { return [__("Partially Received"), "yellow", "per_received,<,100"]; } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) { return [__("Received"), "green", "per_received,=,100"]; @@ -33,5 +37,5 @@ frappe.listview_settings['Material Request'] = { return [__("Manufactured"), "green", "per_ordered,=,100"]; } } - } + }, }; diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 03f58c664d3..b87413e67da 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -744,9 +744,7 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.per_ordered, 100) def test_customer_provided_parts_mr(self): - create_item( - "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 - ) + create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0) existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC") mr = make_material_request(item_code="CUST-0987", material_request_type="Customer Provided") @@ -762,6 +760,62 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.per_ordered, 100) self.assertEqual(existing_requested_qty, current_requested_qty) + def test_auto_email_users_with_company_user_permissions(self): + from erpnext.stock.reorder_item import get_email_list + + comapnywise_users = { + "_Test Company": "test_auto_email_@example.com", + "_Test Company 1": "test_auto_email_1@example.com", + } + + permissions = [] + + for company, user in comapnywise_users.items(): + if not frappe.db.exists("User", user): + frappe.get_doc( + { + "doctype": "User", + "email": user, + "first_name": user, + "send_notifications": 0, + "enabled": 1, + "user_type": "System User", + "roles": [{"role": "Purchase Manager"}], + } + ).insert(ignore_permissions=True) + + if not frappe.db.exists( + "User Permission", {"user": user, "allow": "Company", "for_value": company} + ): + perm_doc = frappe.get_doc( + { + "doctype": "User Permission", + "user": user, + "allow": "Company", + "for_value": company, + "apply_to_all_doctypes": 1, + } + ).insert(ignore_permissions=True) + + permissions.append(perm_doc) + + comapnywise_mr_list = frappe._dict({}) + mr1 = make_material_request() + comapnywise_mr_list.setdefault(mr1.company, []).append(mr1.name) + + mr2 = make_material_request( + company="_Test Company 1", warehouse="Stores - _TC1", cost_center="Main - _TC1" + ) + comapnywise_mr_list.setdefault(mr2.company, []).append(mr2.name) + + for company, _mr_list in comapnywise_mr_list.items(): + emails = get_email_list(company) + + self.assertTrue(comapnywise_users[company] in emails) + + for perm in permissions: + perm.delete() + def get_in_transit_warehouse(company): if not frappe.db.exists("Warehouse Type", "Transit"): @@ -772,9 +826,7 @@ def get_in_transit_warehouse(company): } ).insert() - in_transit_warehouse = frappe.db.exists( - "Warehouse", {"warehouse_type": "Transit", "company": company} - ) + in_transit_warehouse = frappe.db.exists("Warehouse", {"warehouse_type": "Transit", "company": company}) if not in_transit_warehouse: in_transit_warehouse = ( diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index ed4a7e7cf64..d03a356c28a 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -35,6 +35,7 @@ "received_qty", "rate_and_amount_section_break", "rate", + "price_list_rate", "col_break3", "amount", "accounting_details_section", @@ -474,13 +475,22 @@ "fieldtype": "Link", "label": "WIP Composite Asset", "options": "Asset" + }, + { + "fieldname": "price_list_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Price List Rate", + "options": "currency", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:59.599115", + "modified": "2024-02-08 16:30:56.137858", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 35701c90deb..a157606a37a 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -23,9 +23,7 @@ def make_packing_list(doc): return parent_items_price, reset = {}, False - set_price_from_children = frappe.db.get_single_value( - "Selling Settings", "editable_bundle_item_rates" - ) + set_price_from_children = frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates") stale_packed_items_table = get_indexed_packed_items_table(doc) @@ -244,9 +242,7 @@ def get_packed_item_bin_qty(item, warehouse): def get_cancelled_doc_packed_item_details(old_packed_items): prev_doc_packed_items_map = {} for items in old_packed_items: - prev_doc_packed_items_map.setdefault((items.item_code, items.parent_item), []).append( - items.as_dict() - ) + prev_doc_packed_items_map.setdefault((items.item_code, items.parent_item), []).append(items.as_dict()) return prev_doc_packed_items_map diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index ad06732bc3e..24a302f009f 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -1,7 +1,6 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from typing import List, Optional, Tuple import frappe from frappe.tests.utils import FrappeTestCase, change_settings @@ -15,8 +14,8 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry def create_product_bundle( - quantities: Optional[List[int]] = None, warehouse: Optional[str] = None -) -> Tuple[str, List[str]]: + quantities: list[int] | None = None, warehouse: str | None = None +) -> tuple[str, list[str]]: """Get a new product_bundle for use in tests. Create 10x required stock if warehouse is specified. @@ -169,9 +168,7 @@ class TestPackedItem(FrappeTestCase): # backdated stock entry for item in self.bundle_items: - make_stock_entry( - item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday - ) + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday) # assert correct reposting gles = get_gl_entries(dn.doctype, dn.name) @@ -182,14 +179,15 @@ class TestPackedItem(FrappeTestCase): def assertReturns(self, original, returned): self.assertEqual(len(original), len(returned)) - sort_function = lambda p: (p.parent_item, p.item_code, p.qty) + def sort_function(p): + return p.parent_item, p.item_code, p.qty - for sent, returned in zip( - sorted(original, key=sort_function), sorted(returned, key=sort_function) + for sent_item, returned_item in zip( + sorted(original, key=sort_function), sorted(returned, key=sort_function), strict=False ): - self.assertEqual(sent.item_code, returned.item_code) - self.assertEqual(sent.parent_item, returned.parent_item) - self.assertEqual(sent.qty, -1 * returned.qty) + self.assertEqual(sent_item.item_code, returned_item.item_code) + self.assertEqual(sent_item.parent_item, returned_item.parent_item) + self.assertEqual(sent_item.qty, -1 * returned_item.qty) def test_returning_full_bundles(self): from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index 95e5ea309f8..682631f1b74 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -1,45 +1,45 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Packing Slip', { - setup: (frm) => { - frm.set_query('delivery_note', () => { - return { - filters: { - docstatus: 0, - } - } - }); +frappe.ui.form.on("Packing Slip", { + setup: (frm) => { + frm.set_query("delivery_note", () => { + return { + filters: { + docstatus: 0, + }, + }; + }); - frm.set_query('item_code', 'items', (doc, cdt, cdn) => { - if (!doc.delivery_note) { - frappe.throw(__('Please select a Delivery Note')); - } else { - let d = locals[cdt][cdn]; - return { - query: 'erpnext.stock.doctype.packing_slip.packing_slip.item_details', - filters: { - delivery_note: doc.delivery_note, - } - } - } - }); + frm.set_query("item_code", "items", (doc, cdt, cdn) => { + if (!doc.delivery_note) { + frappe.throw(__("Please select a Delivery Note")); + } else { + let d = locals[cdt][cdn]; + return { + query: "erpnext.stock.doctype.packing_slip.packing_slip.item_details", + filters: { + delivery_note: doc.delivery_note, + }, + }; + } + }); }, refresh: (frm) => { - frm.toggle_display('misc_details', frm.doc.amended_from); + frm.toggle_display("misc_details", frm.doc.amended_from); }, delivery_note: (frm) => { - frm.set_value('items', null); + frm.set_value("items", null); if (frm.doc.delivery_note) { erpnext.utils.map_current_doc({ - method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip', + method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip", source_name: frm.doc.delivery_note, target_doc: frm, freeze: true, - freeze_message: __('Creating Packing Slip ...'), + freeze_message: __("Creating Packing Slip ..."), }); } }, diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index 6ea5938917a..df66b98c337 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -11,7 +11,7 @@ from erpnext.controllers.status_updater import StatusUpdater class PackingSlip(StatusUpdater): def __init__(self, *args, **kwargs) -> None: - super(PackingSlip, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.status_updater = [ { "target_dt": "Delivery Note Item", @@ -64,9 +64,7 @@ class PackingSlip(StatusUpdater): """Validate if case nos overlap. If they do, recommend next case no.""" if cint(self.from_case_no) <= 0: - frappe.throw( - _("The 'From Package No.' field must neither be empty nor it's value less than 1.") - ) + frappe.throw(_("The 'From Package No.' field must neither be empty nor it's value less than 1.")) elif not self.to_case_no: self.to_case_no = self.from_case_no elif cint(self.to_case_no) < cint(self.from_case_no): @@ -189,9 +187,8 @@ def item_details(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( """select name, item_name, description from `tabItem` where name in ( select item_code FROM `tabDelivery Note Item` - where parent= %s) - and %s like "%s" %s - limit %s offset %s """ - % ("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"), + where parent= {}) + and {} like "{}" {} + limit {} offset {} """.format("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"), ((filters or {}).get("delivery_note"), "%%%s%%" % txt, page_len, start), ) diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py index 96da23db4a8..08c70bf8e95 100644 --- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py @@ -1,7 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe from frappe.tests.utils import FrappeTestCase diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 8213adb89bf..38b1ca7f76e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -1,61 +1,62 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Pick List', { +frappe.ui.form.on("Pick List", { setup: (frm) => { - frm.set_indicator_formatter('item_code', - function(doc) { return (doc.stock_qty === 0) ? "red" : "green"; }); + frm.set_indicator_formatter("item_code", function (doc) { + return doc.stock_qty === 0 ? "red" : "green"; + }); frm.custom_make_buttons = { - 'Delivery Note': 'Delivery Note', - 'Stock Entry': 'Stock Entry', + "Delivery Note": "Delivery Note", + "Stock Entry": "Stock Entry", }; - frm.set_query('parent_warehouse', () => { + frm.set_query("parent_warehouse", () => { return { filters: { - 'is_group': 1, - 'company': frm.doc.company - } + is_group: 1, + company: frm.doc.company, + }, }; }); - frm.set_query('work_order', () => { + frm.set_query("work_order", () => { return { - query: 'erpnext.stock.doctype.pick_list.pick_list.get_pending_work_orders', + query: "erpnext.stock.doctype.pick_list.pick_list.get_pending_work_orders", filters: { - 'company': frm.doc.company - } + company: frm.doc.company, + }, }; }); - frm.set_query('material_request', () => { + frm.set_query("material_request", () => { return { filters: { - 'material_request_type': ['=', frm.doc.purpose] - } + material_request_type: ["=", frm.doc.purpose], + }, }; }); - frm.set_query('item_code', 'locations', () => { - return erpnext.queries.item({ "is_stock_item": 1 }); + frm.set_query("item_code", "locations", () => { + return erpnext.queries.item({ is_stock_item: 1 }); }); - frm.set_query('batch_no', 'locations', (frm, cdt, cdn) => { + frm.set_query("batch_no", "locations", (frm, cdt, cdn) => { const row = locals[cdt][cdn]; return { - query: 'erpnext.controllers.queries.get_batch_no', + query: "erpnext.controllers.queries.get_batch_no", filters: { item_code: row.item_code, - warehouse: row.warehouse + warehouse: row.warehouse, }, }; }); }, - set_item_locations:(frm, save) => { + set_item_locations: (frm, save) => { if (!(frm.doc.locations && frm.doc.locations.length)) { - frappe.msgprint(__('Add items in the Item Locations table')); + frappe.msgprint(__("Add items in the Item Locations table")); } else { frappe.call({ method: "set_item_locations", doc: frm.doc, args: { - "save": save, + save: save, }, freeze: 1, freeze_message: __("Setting Item Locations..."), @@ -67,155 +68,172 @@ frappe.ui.form.on('Pick List', { frm.events.set_item_locations(frm, false); }, refresh: (frm) => { - frm.trigger('add_get_items_button'); + frm.trigger("add_get_items_button"); if (frm.doc.docstatus === 1) { - frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.target_document_exists', { - 'pick_list_name': frm.doc.name, - 'purpose': frm.doc.purpose - }).then(target_document_exists => { - frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1); + frappe + .xcall("erpnext.stock.doctype.pick_list.pick_list.target_document_exists", { + pick_list_name: frm.doc.name, + purpose: frm.doc.purpose, + }) + .then((target_document_exists) => { + frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1); - if (target_document_exists) return; + if (target_document_exists) return; - frm.add_custom_button(__('Update Current Stock'), () => frm.trigger('update_pick_list_stock')); + frm.add_custom_button(__("Update Current Stock"), () => + frm.trigger("update_pick_list_stock") + ); - if (frm.doc.purpose === 'Delivery') { - frm.add_custom_button(__('Delivery Note'), () => frm.trigger('create_delivery_note'), __('Create')); - } else { - frm.add_custom_button(__('Stock Entry'), () => frm.trigger('create_stock_entry'), __('Create')); - } - }); + if (frm.doc.purpose === "Delivery") { + frm.add_custom_button( + __("Delivery Note"), + () => frm.trigger("create_delivery_note"), + __("Create") + ); + } else { + frm.add_custom_button( + __("Stock Entry"), + () => frm.trigger("create_stock_entry"), + __("Create") + ); + } + }); } }, work_order: (frm) => { - frappe.db.get_value('Work Order', - frm.doc.work_order, - ['qty', 'material_transferred_for_manufacturing'] - ).then(data => { - let qty_data = data.message; - let max = qty_data.qty - qty_data.material_transferred_for_manufacturing; - frappe.prompt({ - fieldtype: 'Float', - label: __('Qty of Finished Goods Item'), - fieldname: 'qty', - description: __('Max: {0}', [max]), - default: max - }, (data) => { - frm.set_value('for_qty', data.qty); - if (data.qty > max) { - frappe.msgprint(__('Quantity must not be more than {0}', [max])); - return; - } - frm.clear_table('locations'); - erpnext.utils.map_current_doc({ - method: 'erpnext.manufacturing.doctype.work_order.work_order.create_pick_list', - target: frm, - source_name: frm.doc.work_order - }); - }, __('Select Quantity'), __('Get Items')); - }); + frappe.db + .get_value("Work Order", frm.doc.work_order, ["qty", "material_transferred_for_manufacturing"]) + .then((data) => { + let qty_data = data.message; + let max = qty_data.qty - qty_data.material_transferred_for_manufacturing; + frappe.prompt( + { + fieldtype: "Float", + label: __("Qty of Finished Goods Item"), + fieldname: "qty", + description: __("Max: {0}", [max]), + default: max, + }, + (data) => { + frm.set_value("for_qty", data.qty); + if (data.qty > max) { + frappe.msgprint(__("Quantity must not be more than {0}", [max])); + return; + } + frm.clear_table("locations"); + erpnext.utils.map_current_doc({ + method: "erpnext.manufacturing.doctype.work_order.work_order.create_pick_list", + target: frm, + source_name: frm.doc.work_order, + }); + }, + __("Select Quantity"), + __("Get Items") + ); + }); }, material_request: (frm) => { erpnext.utils.map_current_doc({ - method: 'erpnext.stock.doctype.material_request.material_request.create_pick_list', + method: "erpnext.stock.doctype.material_request.material_request.create_pick_list", target: frm, - source_name: frm.doc.material_request + source_name: frm.doc.material_request, }); }, purpose: (frm) => { - frm.clear_table('locations'); - frm.trigger('add_get_items_button'); + frm.clear_table("locations"); + frm.trigger("add_get_items_button"); }, create_delivery_note: (frm) => { frappe.model.open_mapped_doc({ - method: 'erpnext.stock.doctype.pick_list.pick_list.create_delivery_note', - frm: frm + method: "erpnext.stock.doctype.pick_list.pick_list.create_delivery_note", + frm: frm, }); - }, create_stock_entry: (frm) => { - frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.create_stock_entry', { - 'pick_list': frm.doc, - }).then(stock_entry => { - frappe.model.sync(stock_entry); - frappe.set_route("Form", 'Stock Entry', stock_entry.name); - }); + frappe + .xcall("erpnext.stock.doctype.pick_list.pick_list.create_stock_entry", { + pick_list: frm.doc, + }) + .then((stock_entry) => { + frappe.model.sync(stock_entry); + frappe.set_route("Form", "Stock Entry", stock_entry.name); + }); }, update_pick_list_stock: (frm) => { frm.events.set_item_locations(frm, true); }, add_get_items_button: (frm) => { let purpose = frm.doc.purpose; - if (purpose != 'Delivery' || frm.doc.docstatus !== 0) return; + if (purpose != "Delivery" || frm.doc.docstatus !== 0) return; let get_query_filters = { docstatus: 1, - per_delivered: ['<', 100], - status: ['!=', ''], - customer: frm.doc.customer + per_delivered: ["<", 100], + status: ["!=", ""], + customer: frm.doc.customer, }; - frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => { + frm.get_items_btn = frm.add_custom_button(__("Get Items"), () => { erpnext.utils.map_current_doc({ - method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list', - source_doctype: 'Sales Order', + method: "erpnext.selling.doctype.sales_order.sales_order.create_pick_list", + source_doctype: "Sales Order", target: frm, setters: { company: frm.doc.company, - customer: frm.doc.customer + customer: frm.doc.customer, }, - date_field: 'transaction_date', - get_query_filters: get_query_filters + date_field: "transaction_date", + get_query_filters: get_query_filters, }); }); }, scan_barcode: (frm) => { const opts = { frm, - items_table_name: 'locations', - qty_field: 'picked_qty', - max_qty_field: 'qty', + items_table_name: "locations", + qty_field: "picked_qty", + max_qty_field: "qty", dont_allow_new_row: true, prompt_qty: frm.doc.prompt_qty, - serial_no_field: "not_supported", // doesn't make sense for picklist without a separate field. + serial_no_field: "not_supported", // doesn't make sense for picklist without a separate field. }; const barcode_scanner = new erpnext.utils.BarcodeScanner(opts); barcode_scanner.process_scan(); - } + }, }); -frappe.ui.form.on('Pick List Item', { +frappe.ui.form.on("Pick List Item", { item_code: (frm, cdt, cdn) => { let row = frappe.get_doc(cdt, cdn); if (row.item_code) { - get_item_details(row.item_code).then(data => { - frappe.model.set_value(cdt, cdn, 'uom', data.stock_uom); - frappe.model.set_value(cdt, cdn, 'stock_uom', data.stock_uom); - frappe.model.set_value(cdt, cdn, 'conversion_factor', 1); + get_item_details(row.item_code).then((data) => { + frappe.model.set_value(cdt, cdn, "uom", data.stock_uom); + frappe.model.set_value(cdt, cdn, "stock_uom", data.stock_uom); + frappe.model.set_value(cdt, cdn, "conversion_factor", 1); }); } }, uom: (frm, cdt, cdn) => { let row = frappe.get_doc(cdt, cdn); if (row.uom) { - get_item_details(row.item_code, row.uom).then(data => { - frappe.model.set_value(cdt, cdn, 'conversion_factor', data.conversion_factor); + get_item_details(row.item_code, row.uom).then((data) => { + frappe.model.set_value(cdt, cdn, "conversion_factor", data.conversion_factor); }); } }, qty: (frm, cdt, cdn) => { let row = frappe.get_doc(cdt, cdn); - frappe.model.set_value(cdt, cdn, 'stock_qty', row.qty * row.conversion_factor); + frappe.model.set_value(cdt, cdn, "stock_qty", row.qty * row.conversion_factor); }, conversion_factor: (frm, cdt, cdn) => { let row = frappe.get_doc(cdt, cdn); - frappe.model.set_value(cdt, cdn, 'stock_qty', row.qty * row.conversion_factor); - } + frappe.model.set_value(cdt, cdn, "stock_qty", row.qty * row.conversion_factor); + }, }); -function get_item_details(item_code, uom=null) { +function get_item_details(item_code, uom = null) { if (item_code) { - return frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.get_item_details', { + return frappe.xcall("erpnext.stock.doctype.pick_list.pick_list.get_item_details", { item_code, - uom + uom, }); } } diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index 7259dc00a81..948011cce69 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -16,6 +16,7 @@ "for_qty", "column_break_4", "parent_warehouse", + "consider_rejected_warehouses", "get_item_locations", "section_break_6", "scan_barcode", @@ -184,11 +185,18 @@ "report_hide": 1, "reqd": 1, "search_index": 1 + }, + { + "default": "0", + "description": "Enable it if users want to consider rejected materials to dispatch.", + "fieldname": "consider_rejected_warehouses", + "fieldtype": "Check", + "label": "Consider Rejected Warehouses" } ], "is_submittable": 1, "links": [], - "modified": "2023-01-24 10:33:43.244476", + "modified": "2024-01-24 17:05:20.317180", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 3c5384d4a88..ef7dc710b13 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -4,7 +4,6 @@ import json from collections import OrderedDict, defaultdict from itertools import groupby -from typing import Dict, List import frappe from frappe import _ @@ -32,6 +31,10 @@ class PickList(Document): self.update_status() self.set_item_locations() + if self.get("locations"): + self.validate_sales_order_percentage() + + def validate_sales_order_percentage(self): # set percentage picked in SO for location in self.get("locations"): if ( @@ -184,9 +187,9 @@ class PickList(Document): picked_items_details = self.get_picked_items_details(items) self.item_location_map = frappe._dict() - from_warehouses = None + from_warehouses = [self.parent_warehouse] if self.parent_warehouse else [] if self.parent_warehouse: - from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse) + from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse)) # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") @@ -205,12 +208,11 @@ class PickList(Document): self.item_count_map.get(item_code), self.company, picked_item_details=picked_items_details.get(item_code), + consider_rejected_warehouses=self.consider_rejected_warehouses, ), ) - locations = get_items_with_location_and_quantity( - item_doc, self.item_location_map, self.docstatus - ) + locations = get_items_with_location_and_quantity(item_doc, self.item_location_map, self.docstatus) item_doc.idx = None item_doc.name = None @@ -264,12 +266,10 @@ class PickList(Document): item_map = OrderedDict() for item in locations: if not item.item_code: - frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) + frappe.throw(f"Row #{item.idx}: Item Code is Mandatory") if not cint( frappe.get_cached_value("Item", item.item_code, "is_stock_item") - ) and not frappe.db.exists( - "Product Bundle", {"new_item_code": item.item_code, "disabled": 0} - ): + ) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}): continue item_code = item.item_code reference = item.sales_order_item or item.material_request_item @@ -390,7 +390,7 @@ class PickList(Document): return picked_items - def _get_product_bundles(self) -> Dict[str, str]: + def _get_product_bundles(self) -> dict[str, str]: # Dict[so_item_row: item_code] product_bundles = {} for item in self.locations: @@ -403,13 +403,11 @@ class PickList(Document): ) return product_bundles - def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]: + def _get_product_bundle_qty_map(self, bundles: list[str]) -> dict[str, dict[str, float]]: # bundle_item_code: Dict[component, qty] product_bundle_qty_map = {} for bundle_item_code in bundles: - bundle = frappe.get_last_doc( - "Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0} - ) + bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}) product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} return product_bundle_qty_map @@ -435,7 +433,7 @@ def update_pick_list_status(pick_list): doc.run_method("update_status") -def get_picked_items_qty(items) -> List[Dict]: +def get_picked_items_qty(items) -> list[dict]: pi_item = frappe.qb.DocType("Pick List Item") return ( frappe.qb.from_(pi_item) @@ -465,17 +463,13 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) locations = [] # if stock qty is zero on submitted entry, show positive remaining qty to recalculate in case of restock. - remaining_stock_qty = ( - item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty - ) + remaining_stock_qty = item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty while flt(remaining_stock_qty) > 0 and available_locations: item_location = available_locations.pop(0) item_location = frappe._dict(item_location) - stock_qty = ( - remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty - ) + stock_qty = remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty qty = stock_qty / (item_doc.conversion_factor or 1) uom_must_be_whole_number = frappe.get_cached_value("UOM", item_doc.uom, "must_be_whole_number") @@ -510,7 +504,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) if item_location.serial_no: # set remaining serial numbers item_location.serial_no = item_location.serial_no[-int(qty_diff) :] - available_locations = [item_location] + available_locations + available_locations = [item_location, *available_locations] # update available locations for the item item_location_map[item_doc.item_code] = available_locations @@ -524,6 +518,7 @@ def get_available_item_locations( company, ignore_validation=False, picked_item_details=None, + consider_rejected_warehouses=False, ): locations = [] total_picked_qty = ( @@ -534,19 +529,39 @@ def get_available_item_locations( if has_batch_no and has_serial_no: locations = get_available_item_locations_for_serial_and_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) elif has_serial_no: locations = get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) elif has_batch_no: locations = get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) else: locations = get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) total_qty_available = sum(location.get("qty") for location in locations) @@ -597,7 +612,12 @@ def get_available_item_locations( def get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): sn = frappe.qb.DocType("Serial No") query = ( @@ -613,6 +633,10 @@ def get_available_item_locations_for_serialized_item( else: query = query.where(Coalesce(sn.warehouse, "") != "") + if not consider_rejected_warehouses: + if rejected_warehouses := get_rejected_warehouses(): + query = query.where(sn.warehouse.notin(rejected_warehouses)) + serial_nos = query.run(as_list=True) warehouse_serial_nos_map = frappe._dict() @@ -627,7 +651,12 @@ def get_available_item_locations_for_serialized_item( def get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): sle = frappe.qb.DocType("Stock Ledger Entry") batch = frappe.qb.DocType("Batch") @@ -653,15 +682,28 @@ def get_available_item_locations_for_batched_item( if from_warehouses: query = query.where(sle.warehouse.isin(from_warehouses)) + if not consider_rejected_warehouses: + if rejected_warehouses := get_rejected_warehouses(): + query = query.where(sle.warehouse.notin(rejected_warehouses)) + return query.run(as_dict=True) def get_available_item_locations_for_serial_and_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): # Get batch nos by FIFO locations = get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company + item_code, + from_warehouses, + required_qty, + company, + consider_rejected_warehouses=consider_rejected_warehouses, ) if locations: @@ -691,7 +733,12 @@ def get_available_item_locations_for_serial_and_batched_item( def get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): bin = frappe.qb.DocType("Bin") query = ( @@ -708,6 +755,10 @@ def get_available_item_locations_for_other_item( wh = frappe.qb.DocType("Warehouse") query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company)) + if not consider_rejected_warehouses: + if rejected_warehouses := get_rejected_warehouses(): + query = query.where(bin.warehouse.notin(rejected_warehouses)) + item_locations = query.run(as_dict=True) return item_locations @@ -768,8 +819,7 @@ def create_dn_with_so(sales_dict, pick_list): "name": "so_detail", "parent": "against_sales_order", }, - "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) - and doc.delivered_by_supplier != 1, + "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1, } for customer in sales_dict: @@ -790,7 +840,6 @@ def create_dn_with_so(sales_dict, pick_list): def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): - for location in pick_list.locations: if location.sales_order != sales_order or location.product_bundle_item: continue @@ -821,9 +870,7 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer") -def add_product_bundles_to_delivery_note( - pick_list: "PickList", delivery_note, item_mapper -) -> None: +def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, item_mapper) -> None: """Add product bundles found in pick list to delivery note. When mapping pick list items, the bundle item itself isn't part of the @@ -901,7 +948,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte & (wo.qty > wo.material_transferred_for_manufacturing) & (wo.docstatus == 1) & (wo.company == filters.get("company")) - & (wo.name.like("%{0}%".format(txt))) + & (wo.name.like(f"%{txt}%")) ) .orderby(Case().when(Locate(txt, wo.name) > 0, Locate(txt, wo.name)).else_(99999)) .orderby(wo.name) @@ -968,9 +1015,7 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): stock_entry.use_multi_level_bom = work_order.use_multi_level_bom stock_entry.fg_completed_qty = pick_list.for_qty if work_order.bom_no: - stock_entry.inspection_required = frappe.db.get_value( - "BOM", work_order.bom_no, "inspection_required" - ) + stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required") is_wip_warehouse_group = frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group") if not (is_wip_warehouse_group and work_order.skip_transfer): @@ -1028,3 +1073,15 @@ def update_common_item_properties(item, location): item.serial_no = location.serial_no item.batch_no = location.batch_no item.material_request_item = location.material_request_item + + +def get_rejected_warehouses(): + if not hasattr(frappe.local, "rejected_warehouses"): + frappe.local.rejected_warehouses = [] + + if not frappe.local.rejected_warehouses: + frappe.local.rejected_warehouses = frappe.get_all( + "Warehouse", filters={"is_rejected_warehouse": 1}, pluck="name" + ) + + return frappe.local.rejected_warehouses diff --git a/erpnext/stock/doctype/pick_list/pick_list_list.js b/erpnext/stock/doctype/pick_list/pick_list_list.js index ad88b0a682f..9cdbfe18720 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list_list.js @@ -1,14 +1,14 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.listview_settings['Pick List'] = { +frappe.listview_settings["Pick List"] = { get_indicator: function (doc) { const status_colors = { - "Draft": "grey", - "Open": "orange", - "Completed": "green", - "Cancelled": "red", + Draft: "grey", + Open: "orange", + Completed: "green", + Cancelled: "red", }; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; }, -}; \ No newline at end of file +}; diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 1254fe3927f..4a9a1a50ac6 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -139,7 +139,6 @@ class TestPickList(FrappeTestCase): self.assertEqual(pick_list.locations[1].qty, 10) def test_pick_list_shows_serial_no_for_serialized_item(self): - stock_reconciliation = frappe.get_doc( { "doctype": "Stock Reconciliation", @@ -274,7 +273,6 @@ class TestPickList(FrappeTestCase): pr2.cancel() def test_pick_list_for_items_from_multiple_sales_orders(self): - item_code = make_item().name try: frappe.get_doc( @@ -418,9 +416,7 @@ class TestPickList(FrappeTestCase): self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty) self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty) - self.assertEqual( - sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor - ) + self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor) pick_list.cancel() sales_order.cancel() @@ -469,7 +465,7 @@ class TestPickList(FrappeTestCase): _dict(item_code="A", warehouse="X", qty=8, picked_qty=3), _dict(item_code="B", warehouse="Y", qty=6, picked_qty=4), ] - for expected_item, created_item in zip(expected_items, pl.locations): + for expected_item, created_item in zip(expected_items, pl.locations, strict=False): _compare_dicts(expected_item, created_item) def test_multiple_dn_creation(self): @@ -579,9 +575,7 @@ class TestPickList(FrappeTestCase): pick_list_1.set_item_locations() pick_list_1.submit() create_delivery_note(pick_list_1.name) - for dn in frappe.get_all( - "Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"} - ): + for dn in frappe.get_all("Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"}): for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): if dn_item.item_code == "_Test Item": self.assertEqual(dn_item.qty, 1) @@ -609,7 +603,7 @@ class TestPickList(FrappeTestCase): quantities = [5, 2] bundle, components = create_product_bundle(quantities, warehouse=warehouse) - bundle_items = dict(zip(components, quantities)) + bundle_items = dict(zip(components, quantities, strict=False)) so = make_sales_order(item_code=bundle, qty=3, rate=42) @@ -715,7 +709,7 @@ class TestPickList(FrappeTestCase): for item in items: for warehouse in warehouses: - se = make_stock_entry( + make_stock_entry( item=item.get("item_code"), to_warehouse=warehouse, qty=5, diff --git a/erpnext/stock/doctype/price_list/price_list.js b/erpnext/stock/doctype/price_list/price_list.js index 9291498e863..85a3a391187 100644 --- a/erpnext/stock/doctype/price_list/price_list.js +++ b/erpnext/stock/doctype/price_list/price_list.js @@ -2,13 +2,17 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Price List", { - refresh: function(frm) { + refresh: function (frm) { let me = this; - frm.add_custom_button(__("Add / Edit Prices"), function() { - frappe.route_options = { - "price_list": frm.doc.name - }; - frappe.set_route("Report", "Item Price"); - }, "fa fa-money"); - } + frm.add_custom_button( + __("Add / Edit Prices"), + function () { + frappe.route_options = { + price_list: frm.doc.name, + }; + frappe.set_route("Report", "Item Price"); + }, + "fa fa-money" + ); + }, }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 8966fbcbb3c..314b65ee4f8 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -78,6 +78,20 @@ frappe.ui.form.on("Purchase Receipt", { }, __('Create')); } + if (frm.doc.docstatus === 0) { + if (!frm.doc.is_return) { + frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => { + if (value) { + frm.doc.items.forEach((item) => { + frm.fields_dict.items.grid.update_docfield_property( + "rate", "read_only", (item.purchase_order && item.purchase_order_item) + ); + }); + } + }); + } + } + frm.events.add_custom_buttons(frm); }, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index de1263d8f66..28c54dab9b2 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -649,7 +649,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1242,7 +1242,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2023-12-18 17:26:41.279663", + "modified": "2024-03-20 16:05:31.713453", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index f35dc136990..a6fd929ea34 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -14,6 +14,7 @@ import erpnext from erpnext.accounts.utils import get_account_currency from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.buying.utils import check_on_hold_or_closed_status +from erpnext.controllers.accounts_controller import merge_taxes from erpnext.controllers.buying_controller import BuyingController from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction @@ -22,7 +23,7 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class PurchaseReceipt(BuyingController): def __init__(self, *args, **kwargs): - super(PurchaseReceipt, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.status_updater = [ { "target_dt": "Purchase Order Item", @@ -115,7 +116,7 @@ class PurchaseReceipt(BuyingController): def validate(self): self.validate_posting_time() - super(PurchaseReceipt, self).validate() + super().validate() if self._action == "submit": self.make_batches("warehouse") @@ -125,8 +126,7 @@ class PurchaseReceipt(BuyingController): self.po_required() self.validate_items_quality_inspection() self.validate_with_previous_doc() - self.validate_uom_is_integer("uom", ["qty", "received_qty"]) - self.validate_uom_is_integer("stock_uom", "stock_qty") + self.validate_uom_is_integer() self.validate_cwip_accounts() self.validate_provisional_expense_account() @@ -140,6 +140,10 @@ class PurchaseReceipt(BuyingController): self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def validate_uom_is_integer(self): + super().validate_uom_is_integer("uom", ["qty", "received_qty"], "Purchase Receipt Item") + super().validate_uom_is_integer("stock_uom", "stock_qty", "Purchase Receipt Item") + def validate_cwip_accounts(self): for item in self.get("items"): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): @@ -147,15 +151,15 @@ class PurchaseReceipt(BuyingController): # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account self.get_company_default("asset_received_but_not_billed") get_asset_account( - "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company + "capital_work_in_progress_account", + asset_category=item.asset_category, + company=self.company, ) break def validate_provisional_expense_account(self): provisional_accounting_for_non_stock_items = cint( - frappe.db.get_value( - "Company", self.company, "enable_provisional_accounting_for_non_stock_items" - ) + frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items") ) if not provisional_accounting_for_non_stock_items: @@ -167,7 +171,7 @@ class PurchaseReceipt(BuyingController): item.provisional_expense_account = default_provisional_account def validate_with_previous_doc(self): - super(PurchaseReceipt, self).validate_with_previous_doc( + super().validate_with_previous_doc( { "Purchase Order": { "ref_dn_field": "purchase_order", @@ -228,24 +232,20 @@ class PurchaseReceipt(BuyingController): return qty and flt(qty[0][0]) or 0.0 def get_po_qty_and_warehouse(self, po_detail): - po_qty, po_warehouse = frappe.db.get_value( - "Purchase Order Item", po_detail, ["qty", "warehouse"] - ) + po_qty, po_warehouse = frappe.db.get_value("Purchase Order Item", po_detail, ["qty", "warehouse"]) return po_qty, po_warehouse # Check for Closed status def check_on_hold_or_closed_status(self): check_list = [] for d in self.get("items"): - if ( - d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list - ): + if d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list: check_list.append(d.purchase_order) check_on_hold_or_closed_status("Purchase Order", d.purchase_order) # on submit def on_submit(self): - super(PurchaseReceipt, self).on_submit() + super().on_submit() # Check for Approving Authority frappe.get_doc("Authorization Control").validate_approving_authority( @@ -282,7 +282,7 @@ class PurchaseReceipt(BuyingController): frappe.throw(_("Purchase Invoice {0} is already submitted").format(self.submit_rv[0][0])) def on_cancel(self): - super(PurchaseReceipt, self).on_cancel() + super().on_cancel() self.check_on_hold_or_closed_status() # Check if Purchase Invoice has been submitted against current Purchase Order @@ -324,9 +324,7 @@ class PurchaseReceipt(BuyingController): ) provisional_accounting_for_non_stock_items = cint( - frappe.db.get_value( - "Company", self.company, "enable_provisional_accounting_for_non_stock_items" - ) + frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items") ) exchange_rate_map, net_rate_map = get_purchase_document_details(self) @@ -395,7 +393,6 @@ class PurchaseReceipt(BuyingController): and self.conversion_rate != exchange_rate_map[item.purchase_invoice] and item.net_rate == net_rate_map[item.purchase_invoice_item] ): - discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * ( exchange_rate_map[item.purchase_invoice] - self.conversion_rate ) @@ -572,17 +569,17 @@ class PurchaseReceipt(BuyingController): ) stock_value_diff = ( - flt(d.base_net_amount) - + flt(d.item_tax_amount / self.conversion_rate) - + flt(d.landed_cost_voucher_amount) + flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount) ) elif warehouse_account.get(d.warehouse): stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) stock_asset_account_name = warehouse_account[d.warehouse]["account"] - supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") - supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( - "account_currency" + supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get( + "account" ) + supplier_warehouse_account_currency = warehouse_account.get( + self.supplier_warehouse, {} + ).get("account_currency") # If PR is sub-contracted and fg item rate is zero # in that case if account for source and target warehouse are same, @@ -606,7 +603,7 @@ class PurchaseReceipt(BuyingController): ): warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse) - if d.is_fixed_asset: + if d.is_fixed_asset and d.landed_cost_voucher_amount: self.update_assets(d, d.valuation_rate) if warehouse_with_no_account: @@ -617,16 +614,19 @@ class PurchaseReceipt(BuyingController): ) def add_provisional_gl_entry( - self, item, gl_entries, posting_date, provisional_account, reverse=0 + self, item, gl_entries, posting_date, provisional_account, reverse=0, item_amount=None ): credit_currency = get_account_currency(provisional_account) expense_account = item.expense_account debit_currency = get_account_currency(item.expense_account) remarks = self.get("remarks") or _("Accounting Entry for Service") multiplication_factor = 1 + amount = item.base_amount if reverse: multiplication_factor = -1 + # Post reverse entry for previously posted amount + amount = item_amount expense_account = frappe.db.get_value( "Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"] ) @@ -636,7 +636,7 @@ class PurchaseReceipt(BuyingController): account=provisional_account, cost_center=item.cost_center, debit=0.0, - credit=multiplication_factor * item.base_amount, + credit=multiplication_factor * amount, remarks=remarks, against_account=expense_account, account_currency=credit_currency, @@ -650,7 +650,7 @@ class PurchaseReceipt(BuyingController): gl_entries=gl_entries, account=expense_account, cost_center=item.cost_center, - debit=multiplication_factor * item.base_amount, + debit=multiplication_factor * amount, credit=0.0, remarks=remarks, against_account=provisional_account, @@ -738,11 +738,14 @@ class PurchaseReceipt(BuyingController): ) for asset in assets: + purchase_amount = flt(valuation_rate) * asset.asset_quantity frappe.db.set_value( - "Asset", asset.name, "gross_purchase_amount", flt(valuation_rate) * asset.asset_quantity - ) - frappe.db.set_value( - "Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate) * asset.asset_quantity + "Asset", + asset.name, + { + "gross_purchase_amount": purchase_amount, + "purchase_receipt_amount": purchase_amount, + }, ) def update_status(self, status): @@ -971,7 +974,7 @@ def get_item_wise_returned_qty(pr_doc): @frappe.whitelist() -def make_purchase_invoice(source_name, target_doc=None): +def make_purchase_invoice(source_name, target_doc=None, args=None): from erpnext.accounts.party import get_payment_terms_template doc = frappe.get_doc("Purchase Receipt", source_name) @@ -983,19 +986,19 @@ def make_purchase_invoice(source_name, target_doc=None): frappe.throw(_("All items have already been Invoiced/Returned")) doc = frappe.get_doc(target) - doc.payment_terms_template = get_payment_terms_template( - source.supplier, "Supplier", source.company - ) + doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company) doc.run_method("onload") doc.run_method("set_missing_values") + + if args and args.get("merge_taxes"): + merge_taxes(source.get("taxes") or [], doc) + doc.run_method("calculate_taxes_and_totals") doc.set_payment_schedule() def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) - if frappe.db.get_single_value( - "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" - ): + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): target_doc.rejected_qty = 0 target_doc.stock_qty = flt(target_doc.qty) * flt( target_doc.conversion_factor, target_doc.precision("conversion_factor") @@ -1004,9 +1007,7 @@ def make_purchase_invoice(source_name, target_doc=None): def get_pending_qty(item_row): qty = item_row.qty - if frappe.db.get_single_value( - "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" - ): + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): qty = item_row.received_qty pending_qty = qty - invoiced_qty_map.get(item_row.name, 0) returned_qty = flt(returned_qty_map.get(item_row.name, 0)) @@ -1039,6 +1040,7 @@ def make_purchase_invoice(source_name, target_doc=None): "field_map": { "name": "pr_detail", "parent": "purchase_receipt", + "qty": "received_qty", "purchase_order_item": "po_detail", "purchase_order": "purchase_order", "is_fixed_asset": "is_fixed_asset", @@ -1051,7 +1053,11 @@ def make_purchase_invoice(source_name, target_doc=None): if not doc.get("is_return") else get_pending_qty(d)[0] > 0, }, - "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True}, + "Purchase Taxes and Charges": { + "doctype": "Purchase Taxes and Charges", + "add_if_empty": True, + "ignore": args.get("merge_taxes") if args else 0, + }, }, target_doc, set_missing_values, @@ -1157,16 +1163,16 @@ def get_item_account_wise_additional_cost(purchase_document): for lcv in landed_cost_vouchers: landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) + based_on_field = None # Use amount field for total item cost for manually cost distributed LCVs - if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually": - based_on_field = "amount" - else: + if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually": based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) total_item_cost = 0 - for item in landed_cost_voucher_doc.items: - total_item_cost += item.get(based_on_field) + if based_on_field: + for item in landed_cost_voucher_doc.items: + total_item_cost += item.get(based_on_field) for item in landed_cost_voucher_doc.items: if item.receipt_document == purchase_document: @@ -1179,15 +1185,11 @@ def get_item_account_wise_additional_cost(purchase_document): if total_item_cost > 0: item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ account.expense_account - ]["amount"] += ( - account.amount * item.get(based_on_field) / total_item_cost - ) + ]["amount"] += account.amount * item.get(based_on_field) / total_item_cost item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ account.expense_account - ]["base_amount"] += ( - account.base_amount * item.get(based_on_field) / total_item_cost - ) + ]["base_amount"] += account.base_amount * item.get(based_on_field) / total_item_cost else: item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ account.expense_account diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index 4029f0c127b..ddc9bb026fb 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -1,8 +1,17 @@ -frappe.listview_settings['Purchase Receipt'] = { - add_fields: ["supplier", "supplier_name", "base_grand_total", "is_subcontracted", - "transporter_name", "is_return", "status", "per_billed", "currency"], - get_indicator: function(doc) { - if(cint(doc.is_return)==1) { +frappe.listview_settings["Purchase Receipt"] = { + add_fields: [ + "supplier", + "supplier_name", + "base_grand_total", + "is_subcontracted", + "transporter_name", + "is_return", + "status", + "per_billed", + "currency", + ], + get_indicator: function (doc) { + if (cint(doc.is_return) == 1) { return [__("Return"), "gray", "is_return,=,Yes"]; } else if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; @@ -15,11 +24,9 @@ frappe.listview_settings['Purchase Receipt'] = { } }, - onload: function(listview) { - - listview.page.add_action_item(__("Purchase Invoice"), ()=>{ + onload: function (listview) { + listview.page.add_action_item(__("Purchase Invoice"), () => { erpnext.bulk_transaction_processing.create(listview, "Purchase Receipt", "Purchase Invoice"); }); - } - + }, }; diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 404758ce94f..b185fd471f9 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, cint, cstr, flt, today +from frappe.utils import add_days, cint, cstr, flt, nowtime, today from pypika import functions as fn import erpnext @@ -11,9 +11,8 @@ from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice -from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse -from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction class TestPurchaseReceipt(FrappeTestCase): @@ -38,7 +37,6 @@ class TestPurchaseReceipt(FrappeTestCase): pr.delete() def test_reverse_purchase_receipt_sle(self): - pr = make_purchase_receipt(qty=0.5, item_code="_Test Item Home Desktop 200") sl_entry = frappe.db.get_all( @@ -125,9 +123,7 @@ class TestPurchaseReceipt(FrappeTestCase): pi.delete() # draft PI pr.cancel() frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier) - frappe.get_doc( - "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" - ).delete() + frappe.get_doc("Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice").delete() def test_purchase_receipt_no_gl_entry(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -194,87 +190,8 @@ class TestPurchaseReceipt(FrappeTestCase): batch_no = pr.items[0].batch_no pr.cancel() - self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no})) - def test_duplicate_serial_nos(self): - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - - item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"}) - if not item: - item = create_item("Test Serialized Item 123") - item.has_serial_no = 1 - item.serial_no_series = "TSI123-.####" - item.save() - else: - item = frappe.get_doc("Item", {"item_name": "Test Serialized Item 123"}) - - # First make purchase receipt - pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500) - pr.load_from_db() - - serial_nos = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name}, - "serial_no", - ) - - serial_nos = get_serial_nos(serial_nos) - - self.assertEquals(get_serial_nos(pr.items[0].serial_no), serial_nos) - - # Then tried to receive same serial nos in difference company - pr_different_company = make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - serial_no="\n".join(serial_nos), - company="_Test Company 1", - do_not_submit=True, - warehouse="Stores - _TC1", - ) - - self.assertRaises(SerialNoDuplicateError, pr_different_company.submit) - - # Then made delivery note to remove the serial nos from stock - dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos)) - dn.load_from_db() - self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos) - - posting_date = add_days(today(), -3) - - # Try to receive same serial nos again in the same company with backdated. - pr1 = make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - posting_date=posting_date, - serial_no="\n".join(serial_nos), - do_not_submit=True, - ) - - self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit) - - # Try to receive same serial nos with different company with backdated. - pr2 = make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - posting_date=posting_date, - serial_no="\n".join(serial_nos), - company="_Test Company 1", - do_not_submit=True, - warehouse="Stores - _TC1", - ) - - self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit) - - # Receive the same serial nos after the delivery note posting date and time - make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos)) - - # Raise the error for backdated deliver note entry cancel - self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel) - def test_purchase_receipt_gl_entry(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", @@ -353,7 +270,8 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(len(rejected_serial_nos), 2) for serial_no in rejected_serial_nos: self.assertEqual( - frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].rejected_warehouse + frappe.db.get_value("Serial No", serial_no, "warehouse"), + pr.get("items")[0].rejected_warehouse, ) pr.cancel() @@ -693,7 +611,7 @@ class TestPurchaseReceipt(FrappeTestCase): item_code = "Test Manual Created Serial No" if not frappe.db.exists("Item", item_code): - item = make_item(item_code, dict(has_serial_no=1)) + make_item(item_code, dict(has_serial_no=1)) serial_no = "12903812901" pr_doc = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no) @@ -839,7 +757,7 @@ class TestPurchaseReceipt(FrappeTestCase): "Stock Received But Not Billed - TCP1": {"cost_center": cost_center}, stock_in_hand_account: {"cost_center": cost_center}, } - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) pr.cancel() @@ -864,7 +782,7 @@ class TestPurchaseReceipt(FrappeTestCase): "Stock Received But Not Billed - TCP1": {"cost_center": cost_center}, stock_in_hand_account: {"cost_center": cost_center}, } - for i, gle in enumerate(gl_entries): + for _i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) pr.cancel() @@ -1125,9 +1043,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr.submit() # Get exchnage gain and loss account - exchange_gain_loss_account = frappe.db.get_value( - "Company", pr.company, "exchange_gain_loss_account" - ) + exchange_gain_loss_account = frappe.db.get_value("Company", pr.company, "exchange_gain_loss_account") # fetching the latest GL Entry with exchange gain and loss account account amount = frappe.db.get_value( @@ -1184,9 +1100,7 @@ class TestPurchaseReceipt(FrappeTestCase): account = "Stock Received But Not Billed - TCP1" make_item(item_code) - se = make_stock_entry( - item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0 - ) + se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0) se.items[0].allow_zero_valuation_rate = 1 se.save() se.submit() @@ -1301,9 +1215,7 @@ class TestPurchaseReceipt(FrappeTestCase): from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) - rejected_warehouse = create_warehouse( - "_Test Rejected Internal To Warehouse New", company=company - ) + rejected_warehouse = create_warehouse("_Test Rejected Internal To Warehouse New", company=company) item_doc = make_item( "Test Internal Transfer Item DS", { @@ -1674,9 +1586,10 @@ class TestPurchaseReceipt(FrappeTestCase): make_stock_entry( purpose="Material Receipt", item_code=item.name, - qty=15, + qty=20, company=company, to_warehouse=from_warehouse, + posting_date=add_days(today(), -3), ) # Step 3: Create Delivery Note with Internal Customer @@ -1695,17 +1608,18 @@ class TestPurchaseReceipt(FrappeTestCase): ) # Step 4: Create Internal Purchase Receipt - from erpnext.controllers.status_updater import OverAllowanceError from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt pr = make_inter_company_purchase_receipt(dn.name) + pr.set_posting_time = 1 + pr.posting_date = today() pr.items[0].qty = 15 pr.items[0].from_warehouse = target_warehouse pr.items[0].warehouse = to_warehouse pr.items[0].rejected_warehouse = from_warehouse pr.save() - self.assertRaises(OverAllowanceError, pr.submit) + self.assertRaises(frappe.ValidationError, pr.submit) # Step 5: Test Over Receipt Allowance frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50) @@ -1717,8 +1631,10 @@ class TestPurchaseReceipt(FrappeTestCase): company=company, from_warehouse=from_warehouse, to_warehouse=target_warehouse, + posting_date=add_days(pr.posting_date, -1), ) + pr.reload() pr.submit() frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) @@ -1841,7 +1757,6 @@ class TestPurchaseReceipt(FrappeTestCase): ) # Step 4: Create Internal Purchase Receipt - from erpnext.controllers.status_updater import OverAllowanceError from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt pr = make_inter_company_purchase_receipt(dn.name) @@ -2147,9 +2062,7 @@ class TestPurchaseReceipt(FrappeTestCase): ) # Step - 4: Create a Material Issue Stock Entry (Qty = 100, Basic Rate = 18.18 [Auto Fetched]) - make_stock_entry( - purpose="Material Issue", item_code=item_code, from_warehouse=warehouse, qty=100 - ) + make_stock_entry(purpose="Material Issue", item_code=item_code, from_warehouse=warehouse, qty=100) # Step - 5: Create a Return Purchase Return (Qty = -8, Rate = 100 [Auto fetched]) return_pr = make_purchase_receipt( @@ -2221,9 +2134,9 @@ class TestPurchaseReceipt(FrappeTestCase): gl_entries = get_gl_entries(pr_return.doctype, pr_return.name) # Test - 1: SLE Stock Value Difference should be equal to Qty * Average Rate - average_rate = ( - (se.items[0].qty * se.items[0].basic_rate) + (pr.items[0].qty * pr.items[0].rate) - ) / (se.items[0].qty + pr.items[0].qty) + average_rate = ((se.items[0].qty * se.items[0].basic_rate) + (pr.items[0].qty * pr.items[0].rate)) / ( + se.items[0].qty + pr.items[0].qty + ) expected_stock_value_difference = pr_return.items[0].qty * average_rate self.assertEqual( flt(sl_entries[0].stock_value_difference, 2), flt(expected_stock_value_difference, 2) @@ -2251,6 +2164,143 @@ class TestPurchaseReceipt(FrappeTestCase): pr_doc.reload() self.assertFalse(pr_doc.items[0].from_warehouse) + def test_do_not_delete_batch_implicitly(self): + item = make_item( + "_Test Item With Delete Batch", + {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TBWDB.#####"}, + ).name + + pr = make_purchase_receipt(item_code=item, qty=10, rate=100) + batch_no = pr.items[0].batch_no + self.assertTrue(frappe.db.exists("Batch", batch_no)) + + pr.cancel() + self.assertTrue(frappe.db.exists("Batch", batch_no)) + + def test_pr_billed_amount_against_return_entry(self): + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_debit_note + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as make_pi_from_pr, + ) + + # Create a Purchase Receipt and Fully Bill it + pr = make_purchase_receipt(qty=10) + pi = make_pi_from_pr(pr.name) + pi.insert() + pi.submit() + + # Debit Note - 50% Qty & enable updating PR billed amount + pi_return = make_debit_note(pi.name) + pi_return.items[0].qty = -5 + pi_return.update_billed_amount_in_purchase_receipt = 1 + pi_return.submit() + + # Check if the billed amount reduced + pr.reload() + self.assertEqual(pr.per_billed, 50) + + pi_return.reload() + pi_return.cancel() + + # Debit Note - 50% Qty & disable updating PR billed amount + pi_return = make_debit_note(pi.name) + pi_return.items[0].qty = -5 + pi_return.update_billed_amount_in_purchase_receipt = 0 + pi_return.submit() + + # Check if the billed amount stayed the same + pr.reload() + self.assertEqual(pr.per_billed, 100) + + def test_sle_qty_after_transaction(self): + item = make_item( + "_Test Item Qty After Transaction", + properties={"is_stock_item": 1, "valuation_method": "FIFO"}, + ).name + + posting_date = today() + posting_time = nowtime() + + # Step 1: Create Purchase Receipt + pr = make_purchase_receipt( + item_code=item, + qty=1, + rate=100, + posting_date=posting_date, + posting_time=posting_time, + do_not_save=1, + ) + + for _i in range(9): + pr.append( + "items", + { + "item_code": item, + "qty": 1, + "rate": 100, + "warehouse": pr.items[0].warehouse, + "cost_center": pr.items[0].cost_center, + "expense_account": pr.items[0].expense_account, + "uom": pr.items[0].uom, + "stock_uom": pr.items[0].stock_uom, + "conversion_factor": pr.items[0].conversion_factor, + }, + ) + + self.assertEqual(len(pr.items), 10) + pr.save() + pr.submit() + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["qty_after_transaction", "creation", "posting_datetime"], + filters={"voucher_no": pr.name, "is_cancelled": 0}, + order_by="creation", + ) + + for index, d in enumerate(data): + self.assertEqual(d.qty_after_transaction, 1 + index) + + # Step 2: Create Purchase Receipt + pr = make_purchase_receipt( + item_code=item, + qty=1, + rate=100, + posting_date=posting_date, + posting_time=posting_time, + do_not_save=1, + ) + + for _i in range(9): + pr.append( + "items", + { + "item_code": item, + "qty": 1, + "rate": 100, + "warehouse": pr.items[0].warehouse, + "cost_center": pr.items[0].cost_center, + "expense_account": pr.items[0].expense_account, + "uom": pr.items[0].uom, + "stock_uom": pr.items[0].stock_uom, + "conversion_factor": pr.items[0].conversion_factor, + }, + ) + + self.assertEqual(len(pr.items), 10) + pr.save() + pr.submit() + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["qty_after_transaction", "creation", "posting_datetime"], + filters={"voucher_no": pr.name, "is_cancelled": 0}, + order_by="creation", + ) + + for index, d in enumerate(data): + self.assertEqual(d.qty_after_transaction, 11 + index) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier @@ -2308,7 +2358,6 @@ def get_gl_entries(voucher_type, voucher_no): def get_taxes(**args): - args = frappe._dict(args) return [ @@ -2428,15 +2477,14 @@ def make_purchase_receipt(**args): "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", - "rate": args.rate if args.rate != None else 50, + "rate": args.rate if args.rate is not None else 50, "conversion_factor": args.conversion_factor or 1.0, "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, "batch_no": args.batch_no, "stock_uom": args.stock_uom or "_Test UOM", "uom": uom, - "cost_center": args.cost_center - or frappe.get_cached_value("Company", pr.company, "cost_center"), + "cost_center": args.cost_center or frappe.get_cached_value("Company", pr.company, "cost_center"), "asset_location": args.location or "Test Location", }, ) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 6d89e9897af..1a2bb8e4c8e 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -352,7 +352,6 @@ "oldfieldtype": "Currency", "options": "currency", "print_width": "100px", - "read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)", "width": "100px" }, { @@ -1055,7 +1054,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-30 16:12:02.364608", + "modified": "2023-12-25 22:32:09.801965", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js index e0569206ef9..351be16d7cb 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -1,41 +1,41 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Putaway Rule', { - setup: function(frm) { - frm.set_query("warehouse", function() { +frappe.ui.form.on("Putaway Rule", { + setup: function (frm) { + frm.set_query("warehouse", function () { return { - "filters": { - "company": frm.doc.company, - "is_group": 0 - } + filters: { + company: frm.doc.company, + is_group: 0, + }, }; }); }, - uom: function(frm) { + uom: function (frm) { if (frm.doc.item_code && frm.doc.uom) { return frm.call({ method: "erpnext.stock.get_item_details.get_conversion_factor", args: { item_code: frm.doc.item_code, - uom: frm.doc.uom + uom: frm.doc.uom, }, - callback: function(r) { + callback: function (r) { if (!r.exc) { let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor); - frm.set_value('conversion_factor', r.message.conversion_factor); - frm.set_value('stock_capacity', stock_capacity); + frm.set_value("conversion_factor", r.message.conversion_factor); + frm.set_value("stock_capacity", stock_capacity); } - } + }, }); } }, - capacity: function(frm) { + capacity: function (frm) { let stock_capacity = flt(frm.doc.capacity) * flt(frm.doc.conversion_factor); - frm.set_value('stock_capacity', stock_capacity); - } + frm.set_value("stock_capacity", stock_capacity); + }, // refresh: function(frm) { diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 623fbde2b0b..2795c576c91 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -109,9 +109,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table) continue - at_capacity, rules = get_ordered_putaway_rules( - item_code, company, source_warehouse=source_warehouse - ) + at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) if not rules: warehouse = source_warehouse or item.get("warehouse") @@ -204,7 +202,7 @@ def _items_changed(old, new, doctype: str) -> bool: new_sorted = sorted(new, key=sort_key) # Once sorted by all relevant keys both tables should align if they are same. - for old_item, new_item in zip(old_sorted, new_sorted): + for old_item, new_item in zip(old_sorted, new_sorted, strict=False): for key in compare_keys: if old_item.get(key) != new_item.get(key): return True @@ -253,9 +251,7 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N if item.doctype == "Stock Entry Detail": new_updated_table_row.t_warehouse = warehouse - new_updated_table_row.transfer_qty = flt(to_allocate) * flt( - new_updated_table_row.conversion_factor - ) + new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) else: new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) new_updated_table_row.warehouse = warehouse @@ -277,24 +273,20 @@ def show_unassigned_items_message(items_not_accomodated): for entry in items_not_accomodated: item_link = frappe.utils.get_link_to_form("Item", entry[0]) - formatted_item_rows += """ - {0} - {1} - """.format( - item_link, frappe.bold(entry[1]) - ) + formatted_item_rows += f""" + {item_link} + {frappe.bold(entry[1])} + """ msg += """ - - + + - {2} + {}
              {0}{1}{}{}
              - """.format( - _("Item"), _("Unassigned Qty"), formatted_item_rows - ) + """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js index 725e91ee8d9..c5c85908b8f 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -1,4 +1,4 @@ -frappe.listview_settings['Putaway Rule'] = { +frappe.listview_settings["Putaway Rule"] = { add_fields: ["disable"], get_indicator: (doc) => { if (doc.disable) { @@ -10,9 +10,9 @@ frappe.listview_settings['Putaway Rule'] = { reports: [ { - name: 'Warehouse Capacity Summary', - report_type: 'Page', - route: 'warehouse-capacity-summary' - } - ] + name: "Warehouse Capacity Summary", + report_type: "Page", + route: "warehouse-capacity-summary", + }, + ], }; diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index ab0ca106a8b..7253f767e92 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -46,9 +46,7 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_priority(self): """Test if rule is applied by priority, irrespective of free space.""" - rule_1 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" - ) + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg") rule_2 = create_putaway_rule( item_code="_Rice", warehouse=self.warehouse_2, capacity=300, uom="Kg", priority=2 ) @@ -69,17 +67,11 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_with_same_priority(self): """Test if rule with more free space is applied, among two rules with same priority and capacity.""" - rule_1 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_1, capacity=500, uom="Kg" - ) - rule_2 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_2, capacity=500, uom="Kg" - ) + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=500, uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500, uom="Kg") # out of 500 kg capacity, occupy 100 kg in warehouse_1 - stock_receipt = make_stock_entry( - item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50 - ) + stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50) pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) @@ -97,12 +89,8 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_with_insufficient_capacity(self): """Test if qty exceeding capacity, is handled.""" - rule_1 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_1, capacity=100, uom="Kg" - ) - rule_2 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_2, capacity=200, uom="Kg" - ) + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=100, uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=200, uom="Kg") pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) @@ -123,19 +111,13 @@ class TestPutawayRule(FrappeTestCase): item.append("uoms", {"uom": "Bag", "conversion_factor": 1000}) item.save() - rule_1 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_1, capacity=3, uom="Bag" - ) + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=3, uom="Bag") self.assertEqual(rule_1.stock_capacity, 3000) - rule_2 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_2, capacity=4, uom="Bag" - ) + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=4, uom="Bag") self.assertEqual(rule_2.stock_capacity, 4000) # populate 'Rack 1' with 1 Bag, making the free space 2 Bags - stock_receipt = make_stock_entry( - item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50 - ) + stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50) pr = make_purchase_receipt( item_code="_Rice", @@ -167,9 +149,7 @@ class TestPutawayRule(FrappeTestCase): frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) # Putaway Rule in different UOM - rule_1 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_1, capacity=1, uom="Bag" - ) + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=1, uom="Bag") self.assertEqual(rule_1.stock_capacity, 1000) # Putaway Rule in Stock UOM rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500) @@ -199,9 +179,7 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_with_reoccurring_item(self): """Test rules on same item entered multiple times with different rate.""" - rule_1 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" - ) + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg") # total capacity is 200 Kg pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, do_not_submit=1) @@ -237,9 +215,7 @@ class TestPutawayRule(FrappeTestCase): def test_validate_over_receipt_in_warehouse(self): """Test if overreceipt is blocked in the presence of putaway rules.""" - rule_1 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" - ) + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg") pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 1) @@ -291,9 +267,7 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rule_on_stock_entry_material_transfer_reoccuring_item(self): """Test if reoccuring item is correctly considered.""" - rule_1 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_1, capacity=300, uom="Kg" - ) + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=300, uom="Kg") rule_2 = create_putaway_rule( item_code="_Rice", warehouse=self.warehouse_2, capacity=600, uom="Kg", priority=2 ) @@ -428,9 +402,7 @@ class TestPutawayRule(FrappeTestCase): rule_1 = create_putaway_rule( item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" ) # more capacity - rule_2 = create_putaway_rule( - item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg" - ) + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg") stock_entry = make_stock_entry( item_code="_Rice", @@ -480,9 +452,7 @@ def create_putaway_rule(**args): putaway.capacity = args.capacity or 1 putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom") putaway.uom = args.uom or putaway.stock_uom - putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)[ - "conversion_factor" - ] + putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)["conversion_factor"] if not args.do_not_save: putaway.save() diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index 05fa2324dd4..fc487514a2c 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -4,88 +4,85 @@ cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; frappe.ui.form.on("Quality Inspection", { - - setup: function(frm) { - frm.set_query("reference_name", function() { + setup: function (frm) { + frm.set_query("reference_name", function () { return { filters: { - "docstatus": ["!=", 2], - } - } + docstatus: ["!=", 2], + }, + }; }); - frm.set_query("batch_no", function() { + frm.set_query("batch_no", function () { return { filters: { - "item": frm.doc.item_code - } + item: frm.doc.item_code, + }, }; }); // Serial No based on item_code - frm.set_query("item_serial_no", function() { + frm.set_query("item_serial_no", function () { let filters = {}; if (frm.doc.item_code) { filters = { - 'item_code': frm.doc.item_code + item_code: frm.doc.item_code, }; } return { filters: filters }; }); // item code based on GRN/DN - frm.set_query("item_code", function(doc) { + frm.set_query("item_code", function (doc) { let doctype = doc.reference_type; if (doc.reference_type !== "Job Card") { - doctype = (doc.reference_type == "Stock Entry") ? - "Stock Entry Detail" : doc.reference_type + " Item"; + doctype = + doc.reference_type == "Stock Entry" ? "Stock Entry Detail" : doc.reference_type + " Item"; } if (doc.reference_type && doc.reference_name) { let filters = { - "from": doctype, - "inspection_type": doc.inspection_type + from: doctype, + inspection_type: doc.inspection_type, }; - if (doc.reference_type == doctype) - filters["reference_name"] = doc.reference_name; - else - filters["parent"] = doc.reference_name; + if (doc.reference_type == doctype) filters["reference_name"] = doc.reference_name; + else filters["parent"] = doc.reference_name; return { query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query", - filters: filters + filters: filters, }; } }); }, - refresh: function(frm) { + refresh: function (frm) { // Ignore cancellation of reference doctype on cancel all. frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type]; }, - item_code: function(frm) { + item_code: function (frm) { if (frm.doc.item_code && !frm.doc.quality_inspection_template) { return frm.call({ method: "get_quality_inspection_template", doc: frm.doc, - callback: function() { - refresh_field(['quality_inspection_template', 'readings']); - } + callback: function () { + refresh_field(["quality_inspection_template", "readings"]); + }, }); } }, - quality_inspection_template: function(frm) { + quality_inspection_template: function (frm) { if (frm.doc.quality_inspection_template) { return frm.call({ method: "get_item_specification_details", doc: frm.doc, - callback: function() { - refresh_field('readings'); - } + callback: function () { + refresh_field("readings"); + }, }); } }, diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 2a9f091bd09..feb2844aec0 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -68,6 +68,9 @@ class QualityInspection(Document): def on_cancel(self): self.update_qc_reference() + def on_trash(self): + self.update_qc_reference() + def validate_readings_status_mandatory(self): for reading in self.readings: if not reading.status: @@ -79,13 +82,11 @@ class QualityInspection(Document): if self.reference_type == "Job Card": if self.reference_name: frappe.db.sql( - """ - UPDATE `tab{doctype}` + f""" + UPDATE `tab{self.reference_type}` SET quality_inspection = %s, modified = %s WHERE name = %s and production_item = %s - """.format( - doctype=self.reference_type - ), + """, (quality_inspection, self.modified, self.reference_name, self.item_code), ) @@ -107,9 +108,9 @@ class QualityInspection(Document): args.append(self.name) frappe.db.sql( - """ + f""" UPDATE - `tab{child_doc}` t1, `tab{parent_doc}` t2 + `tab{doctype}` t1, `tab{self.reference_type}` t2 SET t1.quality_inspection = %s, t2.modified = %s WHERE @@ -117,9 +118,7 @@ class QualityInspection(Document): and t1.item_code = %s and t1.parent = t2.name {conditions} - """.format( - parent_doc=self.reference_type, child_doc=doctype, conditions=conditions - ), + """, args, ) @@ -177,9 +176,9 @@ class QualityInspection(Document): except NameError as e: field = frappe.bold(e.args[0].split()[1]) frappe.throw( - _("Row #{0}: {1} is not a valid reading field. Please refer to the field description.").format( - reading.idx, field - ), + _( + "Row #{0}: {1} is not a valid reading field. Please refer to the field description." + ).format(reading.idx, field), title=_("Invalid Formula"), ) except Exception: @@ -248,40 +247,26 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): qi_condition = "" return frappe.db.sql( - """ + f""" SELECT item_code - FROM `tab{doc}` + FROM `tab{from_doctype}` WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s {qi_condition} {cond} {mcond} - ORDER BY item_code limit {page_len} offset {start} - """.format( - doc=from_doctype, - cond=cond, - mcond=mcond, - start=cint(start), - page_len=cint(page_len), - qi_condition=qi_condition, - ), + ORDER BY item_code limit {cint(page_len)} offset {cint(start)} + """, {"parent": filters.get("parent"), "txt": "%%%s%%" % txt}, ) elif filters.get("reference_name"): return frappe.db.sql( - """ + f""" SELECT production_item - FROM `tab{doc}` + FROM `tab{from_doctype}` WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s {qi_condition} {cond} {mcond} ORDER BY production_item - limit {page_len} offset {start} - """.format( - doc=from_doctype, - cond=cond, - mcond=mcond, - start=cint(start), - page_len=cint(page_len), - qi_condition=qi_condition, - ), + limit {cint(page_len)} offset {cint(start)} + """, {"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt}, ) diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 4f19643ad52..40feaccf08c 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -159,9 +159,7 @@ class TestQualityInspection(FrappeTestCase): do_not_submit=True, ) - readings = [ - {"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "1.0"} - ] + readings = [{"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "1.0"}] qa = create_quality_inspection( reference_type="Stock Entry", reference_name=se.name, readings=readings, status="Rejected" @@ -216,6 +214,33 @@ class TestQualityInspection(FrappeTestCase): qa.save() self.assertEqual(qa.status, "Accepted") + def test_delete_quality_inspection_linked_with_stock_entry(self): + item_code = create_item("_Test Cicuular Dependecy Item with QA").name + + se = make_stock_entry( + item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, do_not_submit=True + ) + + se.inspection_required = 1 + se.save() + + qa = create_quality_inspection( + item_code=item_code, reference_type="Stock Entry", reference_name=se.name, do_not_submit=True + ) + + se.reload() + se.items[0].quality_inspection = qa.name + se.save() + + qa.delete() + + se.reload() + + qc = se.items[0].quality_inspection + self.assertFalse(qc) + + se.delete() + def create_quality_inspection(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js index 47c7e11d237..ecf84c91506 100644 --- a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js @@ -1,8 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Inspection Parameter', { +frappe.ui.form.on("Quality Inspection Parameter", { // refresh: function(frm) { - // } }); diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js index 8716a298716..796ab89d2b5 100644 --- a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js +++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js @@ -1,8 +1,7 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Inspection Parameter Group', { +frappe.ui.form.on("Quality Inspection Parameter Group", { // refresh: function(frm) { - // } }); diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.js b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.js index fa57a3dcc34..053809d5fe0 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.js +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Quality Inspection Template', { - refresh: function() { - - } +frappe.ui.form.on("Quality Inspection Template", { + refresh: function () {}, }); diff --git a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.js b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.js index f261fd99790..5cfd1f5843a 100644 --- a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.js +++ b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.js @@ -1,91 +1,90 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Quick Stock Balance', { - +frappe.ui.form.on("Quick Stock Balance", { setup: (frm) => { - frm.set_query('item', () => { + frm.set_query("item", () => { if (!(frm.doc.warehouse && frm.doc.date)) { - frm.trigger('check_warehouse_and_date'); + frm.trigger("check_warehouse_and_date"); } }); }, make_custom_stock_report_button: (frm) => { if (frm.doc.item) { - frm.add_custom_button(__('Stock Balance Report'), () => { - frappe.set_route('query-report', 'Stock Balance', - { 'item_code': frm.doc.item, 'warehouse': frm.doc.warehouse }); + frm.add_custom_button(__("Stock Balance Report"), () => { + frappe.set_route("query-report", "Stock Balance", { + item_code: frm.doc.item, + warehouse: frm.doc.warehouse, + }); }); } }, refresh: (frm) => { frm.disable_save(); - frm.trigger('make_custom_stock_report_button'); + frm.trigger("make_custom_stock_report_button"); }, check_warehouse_and_date: (frm) => { - frappe.msgprint(__('Please enter Warehouse and Date')); - frm.doc.item = ''; + frappe.msgprint(__("Please enter Warehouse and Date")); + frm.doc.item = ""; frm.refresh(); }, warehouse: (frm) => { if (frm.doc.item || frm.doc.item_barcode) { - frm.trigger('get_stock_and_item_details'); + frm.trigger("get_stock_and_item_details"); } }, date: (frm) => { if (frm.doc.item || frm.doc.item_barcode) { - frm.trigger('get_stock_and_item_details'); + frm.trigger("get_stock_and_item_details"); } }, item: (frm) => { - frappe.flags.last_updated_element = 'item'; - frm.trigger('get_stock_and_item_details'); - frm.trigger('make_custom_stock_report_button'); + frappe.flags.last_updated_element = "item"; + frm.trigger("get_stock_and_item_details"); + frm.trigger("make_custom_stock_report_button"); }, item_barcode: (frm) => { - frappe.flags.last_updated_element = 'item_barcode'; - frm.trigger('get_stock_and_item_details'); - frm.trigger('make_custom_stock_report_button'); + frappe.flags.last_updated_element = "item_barcode"; + frm.trigger("get_stock_and_item_details"); + frm.trigger("make_custom_stock_report_button"); }, get_stock_and_item_details: (frm) => { if (!(frm.doc.warehouse && frm.doc.date)) { - frm.trigger('check_warehouse_and_date'); - } - else if (frm.doc.item || frm.doc.item_barcode) { + frm.trigger("check_warehouse_and_date"); + } else if (frm.doc.item || frm.doc.item_barcode) { let filters = { warehouse: frm.doc.warehouse, date: frm.doc.date, }; - if (frappe.flags.last_updated_element === 'item') { - filters = { ...filters, ...{ item: frm.doc.item }}; - } - else { - filters = { ...filters, ...{ barcode: frm.doc.item_barcode }}; + if (frappe.flags.last_updated_element === "item") { + filters = { ...filters, ...{ item: frm.doc.item } }; + } else { + filters = { ...filters, ...{ barcode: frm.doc.item_barcode } }; } frappe.call({ - method: 'erpnext.stock.doctype.quick_stock_balance.quick_stock_balance.get_stock_item_details', + method: "erpnext.stock.doctype.quick_stock_balance.quick_stock_balance.get_stock_item_details", args: filters, callback: (r) => { if (r.message) { - let fields = ['item', 'qty', 'value', 'image']; - if (!r.message['barcodes'].includes(frm.doc.item_barcode)) { - frm.doc.item_barcode = ''; + let fields = ["item", "qty", "value", "image"]; + if (!r.message["barcodes"].includes(frm.doc.item_barcode)) { + frm.doc.item_barcode = ""; frm.refresh(); } fields.forEach(function (field) { frm.set_value(field, r.message[field]); }); } - } + }, }); } - } + }, }); diff --git a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py index 846be0b9bdc..15c692f3a07 100644 --- a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py +++ b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py @@ -17,17 +17,13 @@ class QuickStockBalance(Document): def get_stock_item_details(warehouse, date, item=None, barcode=None): out = {} if barcode: - out["item"] = frappe.db.get_value( - "Item Barcode", filters={"barcode": barcode}, fieldname=["parent"] - ) + out["item"] = frappe.db.get_value("Item Barcode", filters={"barcode": barcode}, fieldname=["parent"]) if not out["item"]: frappe.throw(_("Invalid Barcode. There is no Item attached to this barcode.")) else: out["item"] = item - barcodes = frappe.db.get_values( - "Item Barcode", filters={"parent": out["item"]}, fieldname=["barcode"] - ) + barcodes = frappe.db.get_values("Item Barcode", filters={"parent": out["item"]}, fieldname=["barcode"]) out["barcodes"] = [x[0] for x in barcodes] out["qty"] = get_stock_balance(out["item"], warehouse, date) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index 8aec5328476..38241ae0609 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -1,22 +1,32 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Repost Item Valuation', { - setup: function(frm) { +frappe.ui.form.on("Repost Item Valuation", { + setup: function (frm) { frm.set_query("warehouse", () => { let filters = { - 'is_group': 0 + is_group: 0, }; - if (frm.doc.company) filters['company'] = frm.doc.company; - return {filters: filters}; + if (frm.doc.company) filters["company"] = frm.doc.company; + return { filters: filters }; }); frm.set_query("voucher_type", () => { return { filters: { - name: ['in', ['Purchase Receipt', 'Purchase Invoice', 'Delivery Note', - 'Sales Invoice', 'Stock Entry', 'Stock Reconciliation', 'Subcontracting Receipt']] - } + name: [ + "in", + [ + "Purchase Receipt", + "Purchase Invoice", + "Delivery Note", + "Sales Invoice", + "Stock Entry", + "Stock Reconciliation", + "Subcontracting Receipt", + ], + ], + }, }; }); @@ -25,97 +35,97 @@ frappe.ui.form.on('Repost Item Valuation', { return { filters: { company: frm.doc.company, - docstatus: 1 - } + docstatus: 1, + }, }; }); } - frm.trigger('setup_realtime_progress'); + frm.trigger("setup_realtime_progress"); }, - based_on: function(frm) { + based_on: function (frm) { var fields_to_reset = []; - if (frm.doc.based_on == 'Transaction') { - fields_to_reset = ['item_code', 'warehouse']; - } else if (frm.doc.based_on == 'Item and Warehouse') { - fields_to_reset = ['voucher_type', 'voucher_no']; + if (frm.doc.based_on == "Transaction") { + fields_to_reset = ["item_code", "warehouse"]; + } else if (frm.doc.based_on == "Item and Warehouse") { + fields_to_reset = ["voucher_type", "voucher_no"]; } if (fields_to_reset) { - fields_to_reset.forEach(field => { + fields_to_reset.forEach((field) => { frm.set_value(field, undefined); }); } }, - setup_realtime_progress: function(frm) { - frappe.realtime.on('item_reposting_progress', data => { + setup_realtime_progress: function (frm) { + frappe.realtime.on("item_reposting_progress", (data) => { if (frm.doc.name !== data.name) { return; } - if (frm.doc.status == 'In Progress') { + if (frm.doc.status == "In Progress") { frm.doc.current_index = data.current_index; frm.doc.items_to_be_repost = data.items_to_be_repost; frm.dashboard.reset(); - frm.trigger('show_reposting_progress'); + frm.trigger("show_reposting_progress"); } }); }, - refresh: function(frm) { - if (frm.doc.status == "Failed" && frm.doc.docstatus==1) { - frm.add_custom_button(__('Restart'), function () { + refresh: function (frm) { + if (frm.doc.status == "Failed" && frm.doc.docstatus == 1) { + frm.add_custom_button(__("Restart"), function () { frm.trigger("restart_reposting"); }).addClass("btn-primary"); } - frm.trigger('show_reposting_progress'); + frm.trigger("show_reposting_progress"); - if (frm.doc.status === 'Queued' && frm.doc.docstatus === 1) { - frm.trigger('execute_reposting'); + if (frm.doc.status === "Queued" && frm.doc.docstatus === 1) { + frm.trigger("execute_reposting"); } }, execute_reposting(frm) { frm.add_custom_button(__("Start Reposting"), () => { frappe.call({ - method: 'erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.execute_repost_item_valuation', - callback: function() { - frappe.msgprint(__('Reposting has been started in the background.')); - } + method: "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.execute_repost_item_valuation", + callback: function () { + frappe.msgprint(__("Reposting has been started in the background.")); + }, }); }); }, - show_reposting_progress: function(frm) { + show_reposting_progress: function (frm) { var bars = []; let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0; - let progress = flt(cint(frm.doc.current_index) / total_count * 100, 2) || 0.5; - var title = __('Reposting Completed {0}%', [progress]); + let progress = flt((cint(frm.doc.current_index) / total_count) * 100, 2) || 0.5; + var title = __("Reposting Completed {0}%", [progress]); bars.push({ - 'title': title, - 'width': progress + '%', - 'progress_class': 'progress-bar-success' + title: title, + width: progress + "%", + progress_class: "progress-bar-success", }); - frm.dashboard.add_progress(__('Reposting Progress'), bars); + frm.dashboard.add_progress(__("Reposting Progress"), bars); }, - restart_reposting: function(frm) { + restart_reposting: function (frm) { frappe.call({ method: "restart_reposting", doc: frm.doc, - callback: function(r) { + callback: function (r) { if (!r.exc) { frm.refresh(); } - } + }, }); - } + }, }); diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 9f3074a24ca..e47c1f8aba7 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -117,15 +117,12 @@ class RepostItemValuation(Document): if not acc_settings.acc_frozen_upto: return if getdate(self.posting_date) <= getdate(acc_settings.acc_frozen_upto): - if ( + if acc_settings.frozen_accounts_modifier and frappe.session.user in get_users_with_role( acc_settings.frozen_accounts_modifier - and frappe.session.user in get_users_with_role(acc_settings.frozen_accounts_modifier) ): frappe.msgprint(_("Caution: This might alter frozen accounts.")) return - frappe.throw( - _("You cannot repost item valuation before {}").format(acc_settings.acc_frozen_upto) - ) + frappe.throw(_("You cannot repost item valuation before {}").format(acc_settings.acc_frozen_upto)) def reset_field_values(self): if self.based_on == "Transaction": @@ -173,14 +170,9 @@ class RepostItemValuation(Document): if self.status not in ("Queued", "In Progress"): return - if not (self.voucher_no and self.voucher_no): - return - - transaction_status = frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") - if transaction_status == 2: - msg = _("Cannot cancel as processing of cancelled documents is pending.") - msg += "
              " + _("Please try again in an hour.") - frappe.throw(msg, title=_("Pending processing")) + msg = _("Cannot cancel as processing of cancelled documents is pending.") + msg += "
              " + _("Please try again in an hour.") + frappe.throw(msg, title=_("Pending processing")) @frappe.whitelist() def restart_reposting(self): @@ -226,6 +218,7 @@ def on_doctype_update(): def repost(doc): try: + frappe.flags.through_repost_item_valuation = True if not frappe.db.exists("Repost Item Valuation", doc.name): return @@ -248,7 +241,7 @@ def repost(doc): raise frappe.db.rollback() - traceback = frappe.get_traceback() + traceback = frappe.get_traceback(with_context=True) doc.log_error("Unable to repost item valuation") message = frappe.message_log.pop() if frappe.message_log else "" diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index aa90ff03a82..56b960fb59b 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -179,7 +179,6 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): riv3.set_status("Skipped") def test_stock_freeze_validation(self): - today = nowdate() riv = frappe.get_doc( diff --git a/erpnext/stock/doctype/serial_no/serial_no.js b/erpnext/stock/doctype/serial_no/serial_no.js index 9d5555ed631..88ed7abe2bd 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.js +++ b/erpnext/stock/doctype/serial_no/serial_no.js @@ -1,20 +1,20 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.add_fetch("customer", "customer_name", "customer_name") -cur_frm.add_fetch("supplier", "supplier_name", "supplier_name") +cur_frm.add_fetch("customer", "customer_name", "customer_name"); +cur_frm.add_fetch("supplier", "supplier_name", "supplier_name"); -cur_frm.add_fetch("item_code", "item_name", "item_name") -cur_frm.add_fetch("item_code", "description", "description") -cur_frm.add_fetch("item_code", "item_group", "item_group") -cur_frm.add_fetch("item_code", "brand", "brand") +cur_frm.add_fetch("item_code", "item_name", "item_name"); +cur_frm.add_fetch("item_code", "description", "description"); +cur_frm.add_fetch("item_code", "item_group", "item_group"); +cur_frm.add_fetch("item_code", "brand", "brand"); -cur_frm.cscript.onload = function() { - cur_frm.set_query("item_code", function() { - return erpnext.queries.item({"is_stock_item": 1, "has_serial_no": 1}) +cur_frm.cscript.onload = function () { + cur_frm.set_query("item_code", function () { + return erpnext.queries.item({ is_stock_item: 1, has_serial_no: 1 }); }); }; -frappe.ui.form.on("Serial No", "refresh", function(frm) { +frappe.ui.form.on("Serial No", "refresh", function (frm) { frm.toggle_enable("item_code", frm.doc.__islocal); }); diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 71b2faa41de..50f47687828 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -3,7 +3,6 @@ import json -from typing import List, Optional, Union import frappe from frappe import ValidationError, _ @@ -66,7 +65,7 @@ class SerialNoDuplicateError(ValidationError): class SerialNo(StockController): def __init__(self, *args, **kwargs): - super(SerialNo, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.via_stock_ledger = False def validate(self): @@ -123,9 +122,7 @@ class SerialNo(StockController): """ item = frappe.get_cached_doc("Item", self.item_code) if item.has_serial_no != 1: - frappe.throw( - _("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code) - ) + frappe.throw(_("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code)) self.item_group = item.item_group self.description = item.description @@ -301,7 +298,8 @@ def validate_serial_no(sle, item_det): if len(serial_nos) != len(set(serial_nos)): frappe.throw( - _("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError + _("Duplicate Serial No entered for Item {0}").format(sle.item_code), + SerialNoDuplicateError, ) allow_existing_serial_no = cint( @@ -337,7 +335,9 @@ def validate_serial_no(sle, item_det): if sr.item_code != sle.item_code: if not allow_serial_nos_with_different_item(serial_no, sle): frappe.throw( - _("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code), + _("Serial No {0} does not belong to Item {1}").format( + serial_no, sle.item_code + ), SerialNoItemError, ) @@ -362,7 +362,9 @@ def validate_serial_no(sle, item_det): frappe.throw(_(msg), SerialNoDuplicateError) if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle): - doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no)) + doc_name = frappe.bold( + get_link_to_form(sr.purchase_document_type, sr.purchase_document_no) + ) frappe.throw( _("Serial No {0} has already been received in the {1} #{2}").format( frappe.bold(serial_no), sr.purchase_document_type, doc_name @@ -375,25 +377,32 @@ def validate_serial_no(sle, item_det): and sle.voucher_type not in ["Stock Entry", "Stock Reconciliation"] and sle.voucher_type == sr.delivery_document_type ): - return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, "return_against") + return_against = frappe.db.get_value( + sle.voucher_type, sle.voucher_no, "return_against" + ) if return_against and return_against != sr.delivery_document_no: frappe.throw(_("Serial no {0} has been already returned").format(sr.name)) if cint(sle.actual_qty) < 0: if sr.warehouse != sle.warehouse: frappe.throw( - _("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse), + _("Serial No {0} does not belong to Warehouse {1}").format( + serial_no, sle.warehouse + ), SerialNoWarehouseError, ) if not sr.purchase_document_no: - frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError) + frappe.throw( + _("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError + ) if sle.voucher_type in ("Delivery Note", "Sales Invoice"): - if sr.batch_no and sr.batch_no != sle.batch_no: frappe.throw( - _("Serial No {0} does not belong to Batch {1}").format(serial_no, sle.batch_no), + _("Serial No {0} does not belong to Batch {1}").format( + serial_no, sle.batch_no + ), SerialNoBatchError, ) @@ -408,7 +417,11 @@ def validate_serial_no(sle, item_det): if sle.voucher_type == "Sales Invoice": if not frappe.db.exists( "Sales Invoice Item", - {"parent": sle.voucher_no, "item_code": sle.item_code, "sales_order": sr.sales_order}, + { + "parent": sle.voucher_no, + "item_code": sle.item_code, + "sales_order": sr.sales_order, + }, ): frappe.throw( _( @@ -431,7 +444,11 @@ def validate_serial_no(sle, item_det): ) if not invoice or frappe.db.exists( "Sales Invoice Item", - {"parent": invoice, "item_code": sle.item_code, "sales_order": sr.sales_order}, + { + "parent": invoice, + "item_code": sle.item_code, + "sales_order": sr.sales_order, + }, ): frappe.throw( _( @@ -467,7 +484,9 @@ def validate_serial_no(sle, item_det): {"parent": sales_invoice, "item_code": sle.item_code}, "sales_order", ) - if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): + if sales_order and get_reserved_qty_for_so( + sales_order, sle.item_code + ): validate_so_serial_no(sr, sales_order) elif cint(sle.actual_qty) < 0: # transfer out @@ -483,9 +502,7 @@ def validate_serial_no(sle, item_det): def check_serial_no_validity_on_cancel(serial_no, sle): - sr = frappe.db.get_value( - "Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1 - ) + sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1) sr_link = frappe.utils.get_link_to_form("Serial No", serial_no) doc_link = frappe.utils.get_link_to_form(sle.voucher_type, sle.voucher_no) actual_qty = cint(sle.actual_qty) @@ -538,9 +555,7 @@ def validate_so_serial_no(sr, sales_order): def has_serial_no_exists(sn, sle): - if ( - sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation" - ): + if sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation": return True if sn.company != sle.company: @@ -584,7 +599,7 @@ def update_serial_nos(sle, item_det): def get_auto_serial_nos(serial_no_series, qty): serial_nos = [] - for i in range(cint(qty)): + for _i in range(cint(qty)): serial_nos.append(get_new_serial_number(serial_no_series)) return "\n".join(serial_nos) @@ -634,13 +649,11 @@ def auto_make_serial_nos(args): def get_items_html(serial_nos, item_code): body = ", ".join(serial_nos) - return """
              - {0}: {1} Serial Numbers + return f"""
              + {item_code}: {len(serial_nos)} Serial Numbers -
              {2}
              - """.format( - item_code, len(serial_nos), body - ) +
              {body}
              + """ def get_item_details(item_code): @@ -657,9 +670,7 @@ def get_serial_nos(serial_no): if isinstance(serial_no, list): return serial_no - return [ - s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip() - ] + return [s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()] def clean_serial_no_string(serial_no: str) -> str: @@ -779,11 +790,9 @@ def update_maintenance_status(): def get_delivery_note_serial_no(item_code, qty, delivery_note): serial_nos = "" dn_serial_nos = frappe.db.sql_list( - """ select name from `tabSerial No` + f""" select name from `tabSerial No` where item_code = %(item_code)s and delivery_document_no = %(delivery_note)s - and sales_invoice is null limit {0}""".format( - cint(qty) - ), + and sales_invoice is null limit {cint(qty)}""", {"item_code": item_code, "delivery_note": delivery_note}, ) @@ -798,12 +807,11 @@ def auto_fetch_serial_number( qty: int, item_code: str, warehouse: str, - posting_date: Optional[str] = None, - batch_nos: Optional[Union[str, List[str]]] = None, - for_doctype: Optional[str] = None, + posting_date: str | None = None, + batch_nos: str | list[str] | None = None, + for_doctype: str | None = None, exclude_sr_nos=None, -) -> List[str]: - +) -> list[str]: filters = frappe._dict({"item_code": item_code, "warehouse": warehouse}) if exclude_sr_nos is None: diff --git a/erpnext/stock/doctype/serial_no/serial_no_list.js b/erpnext/stock/doctype/serial_no/serial_no_list.js index 7526d1d8a5c..e085c2a9312 100644 --- a/erpnext/stock/doctype/serial_no/serial_no_list.js +++ b/erpnext/stock/doctype/serial_no/serial_no_list.js @@ -1,14 +1,21 @@ -frappe.listview_settings['Serial No'] = { +frappe.listview_settings["Serial No"] = { add_fields: ["item_code", "warehouse", "warranty_expiry_date", "delivery_document_type"], get_indicator: (doc) => { if (doc.delivery_document_type) { return [__("Delivered"), "green", "delivery_document_type,is,set"]; - } else if (doc.warranty_expiry_date && frappe.datetime.get_diff(doc.warranty_expiry_date, frappe.datetime.nowdate()) <= 0) { - return [__("Expired"), "red", "warranty_expiry_date,not in,|warranty_expiry_date,<=,Today|delivery_document_type,is,not set"]; + } else if ( + doc.warranty_expiry_date && + frappe.datetime.get_diff(doc.warranty_expiry_date, frappe.datetime.nowdate()) <= 0 + ) { + return [ + __("Expired"), + "red", + "warranty_expiry_date,not in,|warranty_expiry_date,<=,Today|delivery_document_type,is,not set", + ]; } else if (!doc.warehouse) { return [__("Inactive"), "grey", "warehouse,is,not set"]; } else { return [__("Active"), "green", "delivery_document_type,is,not set"]; } - } + }, }; diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index 68623fba11e..612905e4074 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -248,16 +248,12 @@ class TestSerialNo(FrappeTestCase): warehouse = "_Test Warehouse - _TC" serial_nos = ["LOWVALUATION", "HIGHVALUATION"] - in1 = make_stock_entry( - item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0] - ) - in2 = make_stock_entry( + make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0]) + make_stock_entry( item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, serial_no=serial_nos[1] ) - out = create_delivery_note( - item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True - ) + out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True) # change serial no out.items[0].serial_no = serial_nos[1] diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js index 13a17a25913..8843d383531 100644 --- a/erpnext/stock/doctype/shipment/shipment.js +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -1,34 +1,44 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Shipment', { - address_query: function(frm, link_doctype, link_name, is_your_company_address) { +frappe.ui.form.on("Shipment", { + address_query: function (frm, link_doctype, link_name, is_your_company_address) { return { - query: 'frappe.contacts.doctype.address.address.address_query', + query: "frappe.contacts.doctype.address.address.address_query", filters: { link_doctype: link_doctype, link_name: link_name, - is_your_company_address: is_your_company_address - } + is_your_company_address: is_your_company_address, + }, }; }, - contact_query: function(frm, link_doctype, link_name) { + contact_query: function (frm, link_doctype, link_name) { return { - query: 'frappe.contacts.doctype.contact.contact.contact_query', + query: "frappe.contacts.doctype.contact.contact.contact_query", filters: { link_doctype: link_doctype, - link_name: link_name - } + link_name: link_name, + }, }; }, - onload: function(frm) { + onload: function (frm) { frm.set_query("delivery_address_name", () => { let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; - return frm.events.address_query(frm, frm.doc.delivery_to_type, frm.doc[delivery_to], frm.doc.delivery_to_type === 'Company' ? 1 : 0); + return frm.events.address_query( + frm, + frm.doc.delivery_to_type, + frm.doc[delivery_to], + frm.doc.delivery_to_type === "Company" ? 1 : 0 + ); }); frm.set_query("pickup_address_name", () => { let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; - return frm.events.address_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from], frm.doc.pickup_from_type === 'Company' ? 1 : 0); + return frm.events.address_query( + frm, + frm.doc.pickup_from_type, + frm.doc[pickup_from], + frm.doc.pickup_from_type === "Company" ? 1 : 0 + ); }); frm.set_query("delivery_contact_name", () => { let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; @@ -38,8 +48,8 @@ frappe.ui.form.on('Shipment', { let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; return frm.events.contact_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from]); }); - frm.set_query("delivery_note", "shipment_delivery_note", function() { - let customer = ''; + frm.set_query("delivery_note", "shipment_delivery_note", function () { + let customer = ""; if (frm.doc.delivery_to_type == "Customer") { customer = frm.doc.delivery_customer; } @@ -51,305 +61,329 @@ frappe.ui.form.on('Shipment', { filters: { customer: customer, docstatus: 1, - status: ["not in", ["Cancelled"]] - } + status: ["not in", ["Cancelled"]], + }, }; } }); }, - refresh: function() { - $('div[data-fieldname=pickup_address] > div > .clearfix').hide(); - $('div[data-fieldname=pickup_contact] > div > .clearfix').hide(); - $('div[data-fieldname=delivery_address] > div > .clearfix').hide(); - $('div[data-fieldname=delivery_contact] > div > .clearfix').hide(); + refresh: function () { + $("div[data-fieldname=pickup_address] > div > .clearfix").hide(); + $("div[data-fieldname=pickup_contact] > div > .clearfix").hide(); + $("div[data-fieldname=delivery_address] > div > .clearfix").hide(); + $("div[data-fieldname=delivery_contact] > div > .clearfix").hide(); }, - before_save: function(frm) { + before_save: function (frm) { let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; frm.set_value("delivery_to", frm.doc[delivery_to]); let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; frm.set_value("pickup", frm.doc[pickup_from]); }, - set_pickup_company_address: function(frm) { - frappe.db.get_value('Address', { - address_title: frm.doc.pickup_company, - is_your_company_address: 1 - }, 'name', (r) => { - frm.set_value("pickup_address_name", r.name); - }); + set_pickup_company_address: function (frm) { + frappe.db.get_value( + "Address", + { + address_title: frm.doc.pickup_company, + is_your_company_address: 1, + }, + "name", + (r) => { + frm.set_value("pickup_address_name", r.name); + } + ); }, - set_delivery_company_address: function(frm) { - frappe.db.get_value('Address', { - address_title: frm.doc.delivery_company, - is_your_company_address: 1 - }, 'name', (r) => { - frm.set_value("delivery_address_name", r.name); - }); + set_delivery_company_address: function (frm) { + frappe.db.get_value( + "Address", + { + address_title: frm.doc.delivery_company, + is_your_company_address: 1, + }, + "name", + (r) => { + frm.set_value("delivery_address_name", r.name); + } + ); }, - pickup_from_type: function(frm) { - if (frm.doc.pickup_from_type == 'Company') { - frm.set_value("pickup_company", frappe.defaults.get_default('company')); - frm.set_value("pickup_customer", ''); - frm.set_value("pickup_supplier", ''); + pickup_from_type: function (frm) { + if (frm.doc.pickup_from_type == "Company") { + frm.set_value("pickup_company", frappe.defaults.get_default("company")); + frm.set_value("pickup_customer", ""); + frm.set_value("pickup_supplier", ""); } else { - frm.trigger('clear_pickup_fields'); + frm.trigger("clear_pickup_fields"); } - if (frm.doc.pickup_from_type == 'Customer') { - frm.set_value("pickup_company", ''); - frm.set_value("pickup_supplier", ''); + if (frm.doc.pickup_from_type == "Customer") { + frm.set_value("pickup_company", ""); + frm.set_value("pickup_supplier", ""); } - if (frm.doc.pickup_from_type == 'Supplier') { - frm.set_value("pickup_customer", ''); - frm.set_value("pickup_company", ''); + if (frm.doc.pickup_from_type == "Supplier") { + frm.set_value("pickup_customer", ""); + frm.set_value("pickup_company", ""); } }, - delivery_to_type: function(frm) { - if (frm.doc.delivery_to_type == 'Company') { - frm.set_value("delivery_company", frappe.defaults.get_default('company')); - frm.set_value("delivery_customer", ''); - frm.set_value("delivery_supplier", ''); + delivery_to_type: function (frm) { + if (frm.doc.delivery_to_type == "Company") { + frm.set_value("delivery_company", frappe.defaults.get_default("company")); + frm.set_value("delivery_customer", ""); + frm.set_value("delivery_supplier", ""); } else { - frm.trigger('clear_delivery_fields'); + frm.trigger("clear_delivery_fields"); } - if (frm.doc.delivery_to_type == 'Customer') { - frm.set_value("delivery_company", ''); - frm.set_value("delivery_supplier", ''); + if (frm.doc.delivery_to_type == "Customer") { + frm.set_value("delivery_company", ""); + frm.set_value("delivery_supplier", ""); } - if (frm.doc.delivery_to_type == 'Supplier') { - frm.set_value("delivery_customer", ''); - frm.set_value("delivery_company", ''); + if (frm.doc.delivery_to_type == "Supplier") { + frm.set_value("delivery_customer", ""); + frm.set_value("delivery_company", ""); frm.toggle_display("shipment_delivery_note", false); } else { frm.toggle_display("shipment_delivery_note", true); } }, - delivery_address_name: function(frm) { - if (frm.doc.delivery_to_type == 'Company') { - erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', true); + delivery_address_name: function (frm) { + if (frm.doc.delivery_to_type == "Company") { + erpnext.utils.get_address_display(frm, "delivery_address_name", "delivery_address", true); } else { - erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', false); + erpnext.utils.get_address_display(frm, "delivery_address_name", "delivery_address", false); } }, - pickup_address_name: function(frm) { - if (frm.doc.pickup_from_type == 'Company') { - erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', true); + pickup_address_name: function (frm) { + if (frm.doc.pickup_from_type == "Company") { + erpnext.utils.get_address_display(frm, "pickup_address_name", "pickup_address", true); } else { - erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', false); + erpnext.utils.get_address_display(frm, "pickup_address_name", "pickup_address", false); } }, - get_contact_display: function(frm, contact_name, contact_type) { + get_contact_display: function (frm, contact_name, contact_type) { frappe.call({ method: "frappe.contacts.doctype.contact.contact.get_contact_details", args: { contact: contact_name }, - callback: function(r) { + callback: function (r) { if (r.message) { if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) { - if (contact_type == 'Delivery') { - frm.set_value('delivery_contact_name', ''); - frm.set_value('delivery_contact', ''); + if (contact_type == "Delivery") { + frm.set_value("delivery_contact_name", ""); + frm.set_value("delivery_contact", ""); } else { - frm.set_value('pickup_contact_name', ''); - frm.set_value('pickup_contact', ''); + frm.set_value("pickup_contact_name", ""); + frm.set_value("pickup_contact", ""); } - frappe.throw(__("Email or Phone/Mobile of the Contact are mandatory to continue.") - + "
              " + __("Please set Email/Phone for the contact") - + ` ${contact_name}`); + frappe.throw( + __("Email or Phone/Mobile of the Contact are mandatory to continue.") + + "
              " + + __("Please set Email/Phone for the contact") + + ` ${contact_name}` + ); } let contact_display = r.message.contact_display; if (r.message.contact_email) { - contact_display += '
              ' + r.message.contact_email; + contact_display += "
              " + r.message.contact_email; } if (r.message.contact_phone) { - contact_display += '
              ' + r.message.contact_phone; + contact_display += "
              " + r.message.contact_phone; } if (r.message.contact_mobile && !r.message.contact_phone) { - contact_display += '
              ' + r.message.contact_mobile; + contact_display += "
              " + r.message.contact_mobile; } - if (contact_type == 'Delivery') { - frm.set_value('delivery_contact', contact_display); + if (contact_type == "Delivery") { + frm.set_value("delivery_contact", contact_display); if (r.message.contact_email) { - frm.set_value('delivery_contact_email', r.message.contact_email); + frm.set_value("delivery_contact_email", r.message.contact_email); } } else { - frm.set_value('pickup_contact', contact_display); + frm.set_value("pickup_contact", contact_display); if (r.message.contact_email) { - frm.set_value('pickup_contact_email', r.message.contact_email); + frm.set_value("pickup_contact_email", r.message.contact_email); } } } - } + }, }); }, - delivery_contact_name: function(frm) { + delivery_contact_name: function (frm) { if (frm.doc.delivery_contact_name) { - frm.events.get_contact_display(frm, frm.doc.delivery_contact_name, 'Delivery'); + frm.events.get_contact_display(frm, frm.doc.delivery_contact_name, "Delivery"); } }, - pickup_contact_name: function(frm) { + pickup_contact_name: function (frm) { if (frm.doc.pickup_contact_name) { - frm.events.get_contact_display(frm, frm.doc.pickup_contact_name, 'Pickup'); + frm.events.get_contact_display(frm, frm.doc.pickup_contact_name, "Pickup"); } }, - pickup_contact_person: function(frm) { + pickup_contact_person: function (frm) { if (frm.doc.pickup_contact_person) { frappe.call({ method: "erpnext.stock.doctype.shipment.shipment.get_company_contact", args: { user: frm.doc.pickup_contact_person }, - callback: function({ message }) { + callback: function ({ message }) { const r = message; let contact_display = `${r.first_name} ${r.last_name}`; if (r.email) { - contact_display += `
              ${ r.email }`; - frm.set_value('pickup_contact_email', r.email); + contact_display += `
              ${r.email}`; + frm.set_value("pickup_contact_email", r.email); } if (r.phone) { - contact_display += `
              ${ r.phone }`; + contact_display += `
              ${r.phone}`; } if (r.mobile_no && !r.phone) { - contact_display += `
              ${ r.mobile_no }`; + contact_display += `
              ${r.mobile_no}`; } - frm.set_value('pickup_contact', contact_display); - } + frm.set_value("pickup_contact", contact_display); + }, }); } else { - if (frm.doc.pickup_from_type === 'Company') { + if (frm.doc.pickup_from_type === "Company") { frappe.call({ method: "erpnext.stock.doctype.shipment.shipment.get_company_contact", args: { user: frappe.session.user }, - callback: function({ message }) { + callback: function ({ message }) { const r = message; let contact_display = `${r.first_name} ${r.last_name}`; if (r.email) { - contact_display += `
              ${ r.email }`; - frm.set_value('pickup_contact_email', r.email); + contact_display += `
              ${r.email}`; + frm.set_value("pickup_contact_email", r.email); } if (r.phone) { - contact_display += `
              ${ r.phone }`; + contact_display += `
              ${r.phone}`; } if (r.mobile_no && !r.phone) { - contact_display += `
              ${ r.mobile_no }`; + contact_display += `
              ${r.mobile_no}`; } - frm.set_value('pickup_contact', contact_display); - } + frm.set_value("pickup_contact", contact_display); + }, }); } } }, - set_company_contact: function(frm, delivery_type) { - frappe.db.get_value('User', { name: frappe.session.user }, ['full_name', 'last_name', 'email', 'phone', 'mobile_no'], (r) => { - if (!(r.last_name && r.email && (r.phone || r.mobile_no))) { - if (delivery_type == 'Delivery') { - frm.set_value('delivery_company', ''); - frm.set_value('delivery_contact', ''); + set_company_contact: function (frm, delivery_type) { + frappe.db.get_value( + "User", + { name: frappe.session.user }, + ["full_name", "last_name", "email", "phone", "mobile_no"], + (r) => { + if (!(r.last_name && r.email && (r.phone || r.mobile_no))) { + if (delivery_type == "Delivery") { + frm.set_value("delivery_company", ""); + frm.set_value("delivery_contact", ""); + } else { + frm.set_value("pickup_company", ""); + frm.set_value("pickup_contact", ""); + } + frappe.throw( + __("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + + "
              " + + __("Please first set Last Name, Email and Phone for the user") + + ` ${frappe.session.user}` + ); + } + let contact_display = r.full_name; + if (r.email) { + contact_display += "
              " + r.email; + } + if (r.phone) { + contact_display += "
              " + r.phone; + } + if (r.mobile_no && !r.phone) { + contact_display += "
              " + r.mobile_no; + } + if (delivery_type == "Delivery") { + frm.set_value("delivery_contact", contact_display); + if (r.email) { + frm.set_value("delivery_contact_email", r.email); + } } else { - frm.set_value('pickup_company', ''); - frm.set_value('pickup_contact', ''); - } - frappe.throw(__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + "
              " - + __("Please first set Last Name, Email and Phone for the user") - + ` ${frappe.session.user}`); - } - let contact_display = r.full_name; - if (r.email) { - contact_display += '
              ' + r.email; - } - if (r.phone) { - contact_display += '
              ' + r.phone; - } - if (r.mobile_no && !r.phone) { - contact_display += '
              ' + r.mobile_no; - } - if (delivery_type == 'Delivery') { - frm.set_value('delivery_contact', contact_display); - if (r.email) { - frm.set_value('delivery_contact_email', r.email); - } - } else { - frm.set_value('pickup_contact', contact_display); - if (r.email) { - frm.set_value('pickup_contact_email', r.email); + frm.set_value("pickup_contact", contact_display); + if (r.email) { + frm.set_value("pickup_contact_email", r.email); + } } } - }); - frm.set_value('pickup_contact_person', frappe.session.user); + ); + frm.set_value("pickup_contact_person", frappe.session.user); }, - pickup_company: function(frm) { - if (frm.doc.pickup_from_type == 'Company' && frm.doc.pickup_company) { - frm.trigger('set_pickup_company_address'); - frm.events.set_company_contact(frm, 'Pickup'); + pickup_company: function (frm) { + if (frm.doc.pickup_from_type == "Company" && frm.doc.pickup_company) { + frm.trigger("set_pickup_company_address"); + frm.events.set_company_contact(frm, "Pickup"); } }, - delivery_company: function(frm) { - if (frm.doc.delivery_to_type == 'Company' && frm.doc.delivery_company) { - frm.trigger('set_delivery_company_address'); - frm.events.set_company_contact(frm, 'Delivery'); + delivery_company: function (frm) { + if (frm.doc.delivery_to_type == "Company" && frm.doc.delivery_company) { + frm.trigger("set_delivery_company_address"); + frm.events.set_company_contact(frm, "Delivery"); } }, - delivery_customer: function(frm) { - frm.trigger('clear_delivery_fields'); + delivery_customer: function (frm) { + frm.trigger("clear_delivery_fields"); if (frm.doc.delivery_customer) { - frm.events.set_address_name(frm, 'Customer', frm.doc.delivery_customer, 'Delivery'); - frm.events.set_contact_name(frm, 'Customer', frm.doc.delivery_customer, 'Delivery'); + frm.events.set_address_name(frm, "Customer", frm.doc.delivery_customer, "Delivery"); + frm.events.set_contact_name(frm, "Customer", frm.doc.delivery_customer, "Delivery"); } }, - delivery_supplier: function(frm) { - frm.trigger('clear_delivery_fields'); + delivery_supplier: function (frm) { + frm.trigger("clear_delivery_fields"); if (frm.doc.delivery_supplier) { - frm.events.set_address_name(frm, 'Supplier', frm.doc.delivery_supplier, 'Delivery'); - frm.events.set_contact_name(frm, 'Supplier', frm.doc.delivery_supplier, 'Delivery'); + frm.events.set_address_name(frm, "Supplier", frm.doc.delivery_supplier, "Delivery"); + frm.events.set_contact_name(frm, "Supplier", frm.doc.delivery_supplier, "Delivery"); } }, - pickup_customer: function(frm) { + pickup_customer: function (frm) { if (frm.doc.pickup_customer) { - frm.events.set_address_name(frm, 'Customer', frm.doc.pickup_customer, 'Pickup'); - frm.events.set_contact_name(frm, 'Customer', frm.doc.pickup_customer, 'Pickup'); + frm.events.set_address_name(frm, "Customer", frm.doc.pickup_customer, "Pickup"); + frm.events.set_contact_name(frm, "Customer", frm.doc.pickup_customer, "Pickup"); } }, - pickup_supplier: function(frm) { + pickup_supplier: function (frm) { if (frm.doc.pickup_supplier) { - frm.events.set_address_name(frm, 'Supplier', frm.doc.pickup_supplier, 'Pickup'); - frm.events.set_contact_name(frm, 'Supplier', frm.doc.pickup_supplier, 'Pickup'); + frm.events.set_address_name(frm, "Supplier", frm.doc.pickup_supplier, "Pickup"); + frm.events.set_contact_name(frm, "Supplier", frm.doc.pickup_supplier, "Pickup"); } }, - set_address_name: function(frm, ref_doctype, ref_docname, delivery_type) { + set_address_name: function (frm, ref_doctype, ref_docname, delivery_type) { frappe.call({ method: "erpnext.stock.doctype.shipment.shipment.get_address_name", args: { ref_doctype: ref_doctype, - docname: ref_docname + docname: ref_docname, }, - callback: function(r) { + callback: function (r) { if (r.message) { - if (delivery_type == 'Delivery') { - frm.set_value('delivery_address_name', r.message); + if (delivery_type == "Delivery") { + frm.set_value("delivery_address_name", r.message); } else { - frm.set_value('pickup_address_name', r.message); + frm.set_value("pickup_address_name", r.message); } } - } + }, }); }, - set_contact_name: function(frm, ref_doctype, ref_docname, delivery_type) { + set_contact_name: function (frm, ref_doctype, ref_docname, delivery_type) { frappe.call({ method: "erpnext.stock.doctype.shipment.shipment.get_contact_name", args: { ref_doctype: ref_doctype, - docname: ref_docname + docname: ref_docname, }, - callback: function(r) { + callback: function (r) { if (r.message) { - if (delivery_type == 'Delivery') { - frm.set_value('delivery_contact_name', r.message); + if (delivery_type == "Delivery") { + frm.set_value("delivery_contact_name", r.message); } else { - frm.set_value('pickup_contact_name', r.message); + frm.set_value("pickup_contact_name", r.message); } } - } + }, }); }, - add_template: function(frm) { + add_template: function (frm) { if (frm.doc.parcel_template) { frappe.model.with_doc("Shipment Parcel Template", frm.doc.parcel_template, () => { - let parcel_template = frappe.model.get_doc("Shipment Parcel Template", frm.doc.parcel_template); + let parcel_template = frappe.model.get_doc( + "Shipment Parcel Template", + frm.doc.parcel_template + ); let row = frappe.model.add_child(frm.doc, "Shipment Parcel", "shipment_parcel"); row.length = parcel_template.length; row.width = parcel_template.width; @@ -359,56 +393,71 @@ frappe.ui.form.on('Shipment', { }); } }, - pickup_date: function(frm) { + pickup_date: function (frm) { if (frm.doc.pickup_date < frappe.datetime.get_today()) { frappe.throw(__("Pickup Date cannot be before this day")); } }, - clear_pickup_fields: function(frm) { - let fields = ["pickup_address_name", "pickup_contact_name", "pickup_address", "pickup_contact", "pickup_contact_email", "pickup_contact_person"]; + clear_pickup_fields: function (frm) { + let fields = [ + "pickup_address_name", + "pickup_contact_name", + "pickup_address", + "pickup_contact", + "pickup_contact_email", + "pickup_contact_person", + ]; for (let field of fields) { - frm.set_value(field, ''); + frm.set_value(field, ""); } }, - clear_delivery_fields: function(frm) { - let fields = ["delivery_address_name", "delivery_contact_name", "delivery_address", "delivery_contact", "delivery_contact_email"]; + clear_delivery_fields: function (frm) { + let fields = [ + "delivery_address_name", + "delivery_contact_name", + "delivery_address", + "delivery_contact", + "delivery_contact_email", + ]; for (let field of fields) { - frm.set_value(field, ''); + frm.set_value(field, ""); } }, - remove_email_row: function(frm, table, fieldname) { - $.each(frm.doc[table] || [], function(i, detail) { + remove_email_row: function (frm, table, fieldname) { + $.each(frm.doc[table] || [], function (i, detail) { if (detail.email === fieldname) { cur_frm.get_field(table).grid.grid_rows[i].remove(); } }); - } + }, }); -frappe.ui.form.on('Shipment Delivery Note', { - delivery_note: function(frm, cdt, cdn) { +frappe.ui.form.on("Shipment Delivery Note", { + delivery_note: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; if (row.delivery_note) { let row_index = row.idx - 1; - if (validate_duplicate(frm, 'shipment_delivery_note', row.delivery_note, row_index)) { - frappe.throw(__("You have entered a duplicate Delivery Note on Row") + ` ${row.idx}. ` + __("Please rectify and try again.")); + if (validate_duplicate(frm, "shipment_delivery_note", row.delivery_note, row_index)) { + frappe.throw( + __("You have entered a duplicate Delivery Note on Row") + + ` ${row.idx}. ` + + __("Please rectify and try again.") + ); } } }, - grand_total: function(frm, cdt, cdn) { + grand_total: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; if (row.grand_total) { - var value_of_goods = parseFloat(frm.doc.value_of_goods)+parseFloat(row.grand_total); + var value_of_goods = parseFloat(frm.doc.value_of_goods) + parseFloat(row.grand_total); frm.set_value("value_of_goods", Math.round(value_of_goods)); frm.refresh_fields("value_of_goods"); } }, }); -var validate_duplicate = function(frm, table, fieldname, index) { - return ( - table === 'shipment_delivery_note' - ? frm.doc[table].some((detail, i) => detail.delivery_note === fieldname && !(index === i)) - : frm.doc[table].some((detail, i) => detail.email === fieldname && !(index === i)) - ); +var validate_duplicate = function (frm, table, fieldname, index) { + return table === "shipment_delivery_note" + ? frm.doc[table].some((detail, i) => detail.delivery_note === fieldname && !(index === i)) + : frm.doc[table].some((detail, i) => detail.email === fieldname && !(index === i)); }; diff --git a/erpnext/stock/doctype/shipment/shipment_list.js b/erpnext/stock/doctype/shipment/shipment_list.js index ae6a3c154e8..b333ccc817f 100644 --- a/erpnext/stock/doctype/shipment/shipment_list.js +++ b/erpnext/stock/doctype/shipment/shipment_list.js @@ -1,8 +1,8 @@ -frappe.listview_settings['Shipment'] = { +frappe.listview_settings["Shipment"] = { add_fields: ["status"], - get_indicator: function(doc) { - if (doc.status=='Booked') { + get_indicator: function (doc) { + if (doc.status == "Booked") { return [__("Booked"), "green"]; } - } + }, }; diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index 726dff4f313..4d4eadc339b 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -76,9 +76,7 @@ def create_test_shipment(delivery_notes=None): shipment.description_of_content = "unit test entry" for delivery_note in delivery_notes: shipment.append("shipment_delivery_note", {"delivery_note": delivery_note.name}) - shipment.append( - "shipment_parcel", {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5} - ) + shipment.append("shipment_parcel", {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5}) shipment.insert() return shipment @@ -96,9 +94,7 @@ def get_shipment_customer_contact(customer_name): def get_shipment_customer_address(customer_name): address_title = customer_name + " address 123" - customer_address = frappe.get_all( - "Address", fields=["name"], filters={"address_title": address_title} - ) + customer_address = frappe.get_all("Address", fields=["name"], filters={"address_title": address_title}) if len(customer_address): return customer_address[0] else: @@ -160,9 +156,7 @@ def create_customer_contact(fname, lname): customer.is_primary_contact = 1 customer.is_billing_contact = 1 customer.append("email_ids", {"email_id": "randomme@email.com", "is_primary": 1}) - customer.append( - "phone_nos", {"phone": "123123123", "is_primary_phone": 1, "is_primary_mobile_no": 1} - ) + customer.append("phone_nos", {"phone": "123123123", "is_primary_phone": 1, "is_primary_mobile_no": 1}) customer.status = "Passive" customer.insert() return customer diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json index 6943edcdc91..0dc4fd113fc 100644 --- a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json +++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json @@ -16,22 +16,19 @@ "fieldname": "length", "fieldtype": "Int", "in_list_view": 1, - "label": "Length (cm)", - "reqd": 1 + "label": "Length (cm)" }, { "fieldname": "width", "fieldtype": "Int", "in_list_view": 1, - "label": "Width (cm)", - "reqd": 1 + "label": "Width (cm)" }, { "fieldname": "height", "fieldtype": "Int", "in_list_view": 1, - "label": "Height (cm)", - "reqd": 1 + "label": "Height (cm)" }, { "fieldname": "weight", @@ -52,7 +49,7 @@ ], "istable": 1, "links": [], - "modified": "2020-07-09 12:54:14.847170", + "modified": "2024-03-06 16:48:57.355757", "modified_by": "Administrator", "module": "Stock", "name": "Shipment Parcel", @@ -61,5 +58,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js index 785a3b304de..3ef90fa81f3 100644 --- a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js @@ -1,8 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Shipment Parcel Template', { +frappe.ui.form.on("Shipment Parcel Template", { // refresh: function(frm) { - // } }); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 70771e77461..b6459ba1e28 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -517,7 +517,12 @@ frappe.ui.form.on('Stock Entry', { }, callback: function(r) { if (!r.exc) { - ["actual_qty", "basic_rate"].forEach((field) => { + let fields = ["actual_qty", "basic_rate"]; + if (frm.doc.purpose == "Material Receipt") { + fields = ["actual_qty"]; + } + + fields.forEach((field) => { frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0)); }); frm.events.calculate_basic_amount(frm, child); @@ -543,7 +548,9 @@ frappe.ui.form.on('Stock Entry', { let fields = [ {"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"), - options:"BOM", reqd: 1, get_query: filters()}, + options:"BOM", reqd: 1, get_query: () => { + return {filters: { docstatus:1, "is_active": 1 }}; + }}, {"fieldname":"source_warehouse", "fieldtype":"Link", "label":__("Source Warehouse"), options:"Warehouse"}, {"fieldname":"target_warehouse", "fieldtype":"Link", "label":__("Target Warehouse"), diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 564c380017b..d45296f1310 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -104,7 +104,8 @@ "in_standard_filter": 1, "label": "Stock Entry Type", "options": "Stock Entry Type", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "depends_on": "eval:doc.purpose == 'Material Transfer'", @@ -546,7 +547,8 @@ "label": "Job Card", "options": "Job Card", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "amended_from", @@ -679,7 +681,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-06-19 18:23:40.748114", + "modified": "2024-01-12 11:56:58.644882", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5031625a6b8..2a8bb383a9c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -9,22 +9,17 @@ import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum -from frappe.utils import ( - cint, - comma_or, - cstr, - flt, - format_time, - formatdate, - getdate, - month_diff, - nowdate, -) +from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate import erpnext from erpnext.accounts.general_ledger import process_gl_map from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals -from erpnext.manufacturing.doctype.bom.bom import add_additional_cost, validate_bom_no +from erpnext.manufacturing.doctype.bom.bom import ( + add_additional_cost, + get_op_cost_from_sub_assemblies, + get_scrap_items_from_sub_assemblies, + validate_bom_no, +) from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.batch.batch import get_batch_no, get_batch_qty, set_batch_nos @@ -37,6 +32,7 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( OpeningEntryAccountError, ) from erpnext.stock.get_item_details import ( + get_barcode_data, get_bin_details, get_conversion_factor, get_default_cost_center, @@ -73,7 +69,7 @@ form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"} class StockEntry(StockController): def __init__(self, *args, **kwargs): - super(StockEntry, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if self.purchase_order: self.subcontract_data = frappe._dict( { @@ -103,9 +99,7 @@ class StockEntry(StockController): def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule - apply_rule = self.apply_putaway_rule and ( - self.purpose in ["Material Transfer", "Material Receipt"] - ) + apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) if self.get("items") and apply_rule: apply_putaway_rule(self.doctype, self.get("items"), self.company, purpose=self.purpose) @@ -154,7 +148,6 @@ class StockEntry(StockController): set_batch_nos(self, "s_warehouse") self.validate_serialized_batch() - self.set_actual_qty() self.calculate_rate_and_amount() self.validate_putaway_capacity() @@ -164,41 +157,6 @@ class StockEntry(StockController): self.reset_default_field_value("from_warehouse", "items", "s_warehouse") self.reset_default_field_value("to_warehouse", "items", "t_warehouse") - def submit(self): - if self.is_enqueue_action(): - frappe.msgprint( - _( - "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage" - ) - ) - self.queue_action("submit", timeout=2000) - else: - self._submit() - - def cancel(self): - if self.is_enqueue_action(): - frappe.msgprint( - _( - "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage" - ) - ) - self.queue_action("cancel", timeout=2000) - else: - self._cancel() - - def is_enqueue_action(self, force=False) -> bool: - if force: - return True - - if frappe.flags.in_test: - return False - - # If line items are more than 100 or record is older than 6 months - if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6: - return True - - return False - def on_submit(self): self.update_stock_ledger() @@ -309,7 +267,11 @@ class StockEntry(StockController): if self.purpose == "Send to Warehouse": for d in frappe.get_all( "Stock Entry", - filters={"docstatus": 0, "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}, + filters={ + "docstatus": 0, + "outgoing_stock_entry": self.name, + "purpose": "Receive at Warehouse", + }, ): frappe.delete_doc("Stock Entry", d.name) @@ -393,7 +355,14 @@ class StockEntry(StockController): for field in reset_fields: item.set(field, item_details.get(field)) - update_fields = ("uom", "description", "expense_account", "cost_center", "conversion_factor") + update_fields = ( + "uom", + "description", + "expense_account", + "cost_center", + "conversion_factor", + "barcode", + ) for field in update_fields: if not item.get(field): @@ -463,7 +432,7 @@ class StockEntry(StockController): return precision = frappe.get_precision("Stock Entry Detail", "qty") - fg_item = list(fg_qty.keys())[0] + fg_item = next(iter(fg_qty.keys())) fg_item_qty = flt(fg_qty[fg_item], precision) fg_completed_qty = flt(self.fg_completed_qty, precision) @@ -646,14 +615,13 @@ class StockEntry(StockController): production_item, qty = frappe.db.get_value( "Work Order", self.work_order, ["production_item", "qty"] ) - args = other_ste + [production_item] + args = [*other_ste, production_item] fg_qty_already_entered = frappe.db.sql( """select sum(transfer_qty) from `tabStock Entry Detail` - where parent in (%s) - and item_code = %s - and ifnull(s_warehouse,'')='' """ - % (", ".join(["%s" * len(other_ste)]), "%s"), + where parent in ({}) + and item_code = {} + and ifnull(s_warehouse,'')='' """.format(", ".join(["%s" * len(other_ste)]), "%s"), args, )[0][0] if fg_qty_already_entered and fg_qty_already_entered >= qty: @@ -731,9 +699,7 @@ class StockEntry(StockController): Set rate for outgoing, scrapped and finished items """ # Set rate for outgoing items - outgoing_items_cost = self.set_rate_for_outgoing_items( - reset_outgoing_rate, raise_error_if_no_rate - ) + outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) items = [] @@ -897,8 +863,6 @@ class StockEntry(StockController): self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose") def validate_duplicate_serial_no(self): - warehouse_wise_serial_nos = {} - # In case of repack the source and target serial nos could be same for warehouse in ["s_warehouse", "t_warehouse"]: serial_nos = [] @@ -936,13 +900,17 @@ class StockEntry(StockController): item_code = se_item.original_item or se_item.item_code precision = cint(frappe.db.get_default("float_precision")) or 3 required_qty = sum( - [flt(d.required_qty) for d in subcontract_order.supplied_items if d.rm_item_code == item_code] + [ + flt(d.required_qty) + for d in subcontract_order.supplied_items + if d.rm_item_code == item_code + ] ) total_allowed = required_qty + (required_qty * (qty_allowance / 100)) if not required_qty: - bom_no = frappe.db.get_value( + frappe.db.get_value( f"{self.subcontract_data.order_doctype} Item", { "parent": self.get(self.subcontract_data.order_field), @@ -988,7 +956,10 @@ class StockEntry(StockController): & (se.docstatus == 1) & (se_detail.item_code == se_item.item_code) & ( - ((se.purchase_order == self.purchase_order) & (se_detail.po_detail == se_item.po_detail)) + ( + (se.purchase_order == self.purchase_order) + & (se_detail.po_detail == se_item.po_detail) + ) if self.subcontract_data.order_doctype == "Purchase Order" else ( (se.subcontracting_order == self.subcontracting_order) @@ -1041,7 +1012,9 @@ class StockEntry(StockController): else: if not se_item.allow_alternative_item: frappe.throw( - _("Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}").format( + _( + "Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" + ).format( se_item.idx, se_item.item_code, self.subcontract_data.order_doctype, @@ -1228,12 +1201,18 @@ class StockEntry(StockController): for d in self.get("items"): if cstr(d.s_warehouse): sle = self.get_sl_entries( - d, {"warehouse": cstr(d.s_warehouse), "actual_qty": -flt(d.transfer_qty), "incoming_rate": 0} + d, + { + "warehouse": cstr(d.s_warehouse), + "actual_qty": -flt(d.transfer_qty), + "incoming_rate": 0, + }, ) if cstr(d.t_warehouse): sle.dependant_sle_voucher_detail_no = d.name elif finished_item_row and ( - finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse + finished_item_row.item_code != d.item_code + or finished_item_row.t_warehouse != d.s_warehouse ): sle.dependant_sle_voucher_detail_no = finished_item_row.name @@ -1256,7 +1235,7 @@ class StockEntry(StockController): sl_entries.append(sle) def get_gl_entries(self, warehouse_account): - gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account) + gl_entries = super().get_gl_entries(warehouse_account) if self.purpose in ("Repack", "Manufacture"): total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item) @@ -1289,9 +1268,9 @@ class StockEntry(StockController): flt(t.amount * multiply_based_on) / divide_based_on ) - item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += ( - flt(t.base_amount * multiply_based_on) / divide_based_on - ) + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account][ + "base_amount" + ] += flt(t.base_amount * multiply_based_on) / divide_based_on if item_account_wise_additional_cost: for d in self.get("items"): @@ -1323,7 +1302,9 @@ class StockEntry(StockController): "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "credit": -1 - * amount["base_amount"], # put it as negative credit instead of debit purposefully + * amount[ + "base_amount" + ], # put it as negative credit instead of debit purposefully }, item=d, ) @@ -1421,11 +1402,7 @@ class StockEntry(StockController): ret.update(get_uom_details(args.get("item_code"), args.get("uom"), args.get("qty"))) if self.purpose == "Material Issue": - ret["expense_account"] = ( - item.get("expense_account") - or item_group_defaults.get("expense_account") - or frappe.get_cached_value("Company", self.company, "default_expense_account") - ) + ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account") for company_field, field in { "stock_adjustment_account": "expense_account", @@ -1456,13 +1433,20 @@ class StockEntry(StockController): ): subcontract_items = frappe.get_all( self.subcontract_data.order_supplied_items_field, - {"parent": self.get(self.subcontract_data.order_field), "rm_item_code": args.get("item_code")}, + { + "parent": self.get(self.subcontract_data.order_field), + "rm_item_code": args.get("item_code"), + }, "main_item_code", ) if subcontract_items and len(subcontract_items) == 1: ret["subcontracted_item"] = subcontract_items[0].main_item_code + barcode_data = get_barcode_data(item_code=item.name) + if barcode_data and len(barcode_data.get(item.name)) == 1: + ret["barcode"] = barcode_data.get(item.name)[0] + return ret @frappe.whitelist() @@ -1506,7 +1490,6 @@ class StockEntry(StockController): ) if self.bom_no: - backflush_based_on = frappe.db.get_single_value( "Manufacturing Settings", "backflush_raw_materials_based_on" ) @@ -1520,7 +1503,6 @@ class StockEntry(StockController): "Material Transfer for Manufacture", "Material Consumption for Manufacture", ]: - if self.work_order and self.purpose == "Material Transfer for Manufacture": item_dict = self.get_pending_raw_materials(backflush_based_on) if self.to_warehouse and self.pro_doc: @@ -1530,7 +1512,10 @@ class StockEntry(StockController): elif ( self.work_order - and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") + and ( + self.purpose == "Manufacture" + or self.purpose == "Material Consumption for Manufacture" + ) and not self.pro_doc.skip_transfer and self.flags.backflush_based_on == "Material Transferred for Manufacture" ): @@ -1538,7 +1523,10 @@ class StockEntry(StockController): elif ( self.work_order - and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") + and ( + self.purpose == "Manufacture" + or self.purpose == "Material Consumption for Manufacture" + ) and self.flags.backflush_based_on == "BOM" and frappe.db.get_single_value("Manufacturing Settings", "material_consumption") == 1 ): @@ -1551,7 +1539,10 @@ class StockEntry(StockController): item_dict = self.get_bom_raw_materials(self.fg_completed_qty) # Get Subcontract Order Supplied Items Details - if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor": + if ( + self.get(self.subcontract_data.order_field) + and self.purpose == "Send to Subcontractor" + ): # Get Subcontract Order Supplied Items Details parent = frappe.qb.DocType(self.subcontract_data.order_doctype) child = frappe.qb.DocType(self.subcontract_data.order_supplied_items_field) @@ -1570,9 +1561,14 @@ class StockEntry(StockController): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): item["from_warehouse"] = self.pro_doc.wip_warehouse # Get Reserve Warehouse from Subcontract Order - if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor": + if ( + self.get(self.subcontract_data.order_field) + and self.purpose == "Send to Subcontractor" + ): item["from_warehouse"] = item_wh.get(item.item_code) - item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else "" + item["to_warehouse"] = ( + self.to_warehouse if self.purpose == "Send to Subcontractor" else "" + ) self.add_to_stock_entry_detail(item_dict) @@ -1767,11 +1763,20 @@ class StockEntry(StockController): def get_bom_scrap_material(self, qty): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict - # item dict = { item_code: {qty, description, stock_uom} } - item_dict = ( - get_bom_items_as_dict(self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1) - or {} - ) + if ( + frappe.db.get_single_value("Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies") + and self.work_order + and frappe.get_cached_value("Work Order", self.work_order, "use_multi_level_bom") + ): + item_dict = get_scrap_items_from_sub_assemblies(self.bom_no, self.company, qty) + else: + # item dict = { item_code: {qty, description, stock_uom} } + item_dict = ( + get_bom_items_as_dict( + self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1 + ) + or {} + ) for item in item_dict.values(): item.from_warehouse = "" @@ -1915,7 +1920,7 @@ class StockEntry(StockController): as_dict=1, ) - for key, row in available_materials.items(): + for _key, row in available_materials.items(): remaining_qty_to_produce = flt(wo_data.trans_qty) - flt(wo_data.produced_qty) if remaining_qty_to_produce <= 0 and not self.is_return: continue @@ -2059,9 +2064,7 @@ class StockEntry(StockController): continue transfer_pending = flt(d.required_qty) > flt(d.transferred_qty) - can_transfer = transfer_pending or ( - backflush_based_on == "Material Transferred for Manufacture" - ) + can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture") if not can_transfer: continue @@ -2076,7 +2079,9 @@ class StockEntry(StockController): ) item_row["job_card_item"] = job_card_item or None - if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"): + if d.source_warehouse and not frappe.db.get_value( + "Warehouse", d.source_warehouse, "is_group" + ): item_row["from_warehouse"] = d.source_warehouse item_row["to_warehouse"] = wip_warehouse @@ -2131,9 +2136,9 @@ class StockEntry(StockController): if item_row.get(field): se_child.set(field, item_row.get(field)) - if se_child.s_warehouse == None: + if se_child.s_warehouse is None: se_child.s_warehouse = self.from_warehouse - if se_child.t_warehouse == None: + if se_child.t_warehouse is None: se_child.t_warehouse = self.to_warehouse # in stock uom @@ -2189,15 +2194,20 @@ class StockEntry(StockController): expiry_date = frappe.db.get_value("Batch", item.batch_no, "expiry_date") if expiry_date: if getdate(self.posting_date) > getdate(expiry_date): - frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code)) + frappe.throw( + _("Batch {0} of Item {1} has expired.").format( + item.batch_no, item.item_code + ) + ) else: - frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code)) + frappe.throw( + _("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code) + ) def update_subcontract_order_supplied_items(self): if self.get(self.subcontract_data.order_field) and ( self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return ): - # Get Subcontract Order Supplied Items Details order_supplied_items = frappe.db.get_all( self.subcontract_data.order_supplied_items_field, @@ -2298,8 +2308,8 @@ class StockEntry(StockController): cond = "" for data, transferred_qty in stock_entries.items(): - cond += """ WHEN (parent = %s and name = %s) THEN %s - """ % ( + cond += """ WHEN (parent = {} and name = {}) THEN {} + """.format( frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty, @@ -2355,7 +2365,9 @@ class StockEntry(StockController): material_request = item.material_request or None if self.purpose == "Material Transfer" and material_request not in material_requests: if self.outgoing_stock_entry and parent_se: - material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, "material_request") + material_request = frappe.get_value( + "Stock Entry Detail", item.ste_detail, "material_request" + ) if material_request and material_request not in material_requests: material_requests.append(material_request) @@ -2527,6 +2539,15 @@ def get_work_order_details(work_order, company): def get_operating_cost_per_unit(work_order=None, bom_no=None): operating_cost_per_unit = 0 if work_order: + if ( + bom_no + and frappe.db.get_single_value( + "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies" + ) + and frappe.get_cached_value("Work Order", work_order, "use_multi_level_bom") + ): + return get_op_cost_from_sub_assemblies(bom_no) + if not bom_no: bom_no = work_order.bom_no @@ -2551,9 +2572,7 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): ) ) ): - operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt( - work_order.produced_qty - ) + operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty) return operating_cost_per_unit @@ -2566,24 +2585,20 @@ def get_used_alternative_items( if subcontract_order: cond = f"and ste.purpose = 'Send to Subcontractor' and ste.{subcontract_order_field} = '{subcontract_order}'" elif work_order: - cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format( - work_order - ) + cond = f"and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{work_order}'" if not cond: return {} used_alternative_items = {} data = frappe.db.sql( - """ select sted.original_item, sted.uom, sted.conversion_factor, + f""" select sted.original_item, sted.uom, sted.conversion_factor, sted.item_code, sted.item_name, sted.conversion_factor,sted.stock_uom, sted.description from `tabStock Entry` ste, `tabStock Entry Detail` sted where sted.parent = ste.name and ste.docstatus = 1 and sted.original_item != sted.item_code - {0} """.format( - cond - ), + {cond} """, as_dict=1, ) @@ -2621,9 +2636,7 @@ def get_uom_details(item_code, uom, qty): conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor") if not conversion_factor: - frappe.msgprint( - _("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) - ) + frappe.msgprint(_("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code)) ret = {"uom": ""} else: ret = { @@ -2733,9 +2746,7 @@ def get_supplied_items( else: supplied_item.supplied_qty += row.transfer_qty - supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt( - supplied_item.returned_qty - ) + supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty) return supplied_item_details @@ -2825,7 +2836,11 @@ def get_stock_entry_data(work_order): & (stock_entry_detail.s_warehouse.isnotnull()) & ( stock_entry.purpose.isin( - ["Manufacture", "Material Consumption for Manufacture", "Material Transfer for Manufacture"] + [ + "Manufacture", + "Material Consumption for Manufacture", + "Material Transfer for Manufacture", + ] ) ) ) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_list.js b/erpnext/stock/doctype/stock_entry/stock_entry_list.js index af29d495ff7..8a1c808cd91 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_list.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry_list.js @@ -1,19 +1,25 @@ -frappe.listview_settings['Stock Entry'] = { - add_fields: ["`tabStock Entry`.`from_warehouse`", "`tabStock Entry`.`to_warehouse`", - "`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`", - "`tabStock Entry`.`is_return`"], +frappe.listview_settings["Stock Entry"] = { + add_fields: [ + "`tabStock Entry`.`from_warehouse`", + "`tabStock Entry`.`to_warehouse`", + "`tabStock Entry`.`purpose`", + "`tabStock Entry`.`work_order`", + "`tabStock Entry`.`bom_no`", + "`tabStock Entry`.`is_return`", + ], get_indicator: function (doc) { - if(doc.is_return===1 && doc.purpose === "Material Transfer for Manufacture") { - return [__("Material Returned from WIP"), "orange", - "is_return,=,1|purpose,=,Material Transfer for Manufacture|docstatus,<,2"]; + if (doc.is_return === 1 && doc.purpose === "Material Transfer for Manufacture") { + return [ + __("Material Returned from WIP"), + "orange", + "is_return,=,1|purpose,=,Material Transfer for Manufacture|docstatus,<,2", + ]; } else if (doc.docstatus === 0) { return [__("Draft"), "red", "docstatus,=,0"]; - - } else if (doc.purpose === 'Send to Warehouse' && doc.per_transferred < 100) { + } else if (doc.purpose === "Send to Warehouse" && doc.per_transferred < 100) { // not delivered & overdue return [__("Goods In Transit"), "grey", "per_transferred,<,100"]; - - } else if (doc.purpose === 'Send to Warehouse' && doc.per_transferred === 100) { + } else if (doc.purpose === "Send to Warehouse" && doc.per_transferred === 100) { return [__("Goods Transferred"), "green", "per_transferred,=,100"]; } else if (doc.docstatus === 2) { return [__("Canceled"), "red", "docstatus,=,2"]; @@ -22,21 +28,30 @@ frappe.listview_settings['Stock Entry'] = { } }, column_render: { - "from_warehouse": function(doc) { + from_warehouse: function (doc) { var html = ""; - if(doc.from_warehouse) { - html += '' - +doc.from_warehouse+' '; + if (doc.from_warehouse) { + html += + '' + + doc.from_warehouse + + " "; } // if(doc.from_warehouse || doc.to_warehouse) { // html += ' '; // } - if(doc.to_warehouse) { - html += ''+doc.to_warehouse+''; + if (doc.to_warehouse) { + html += + '' + + doc.to_warehouse + + ""; } return html; - } - } + }, + }, }; diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 0f9001392df..c3b8e3bc0b9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -2,7 +2,7 @@ # See license.txt -from typing import TYPE_CHECKING, Optional, overload +from typing import TYPE_CHECKING, overload import frappe from frappe.utils import cint, flt @@ -18,15 +18,15 @@ def make_stock_entry( *, item_code: str, qty: float, - company: Optional[str] = None, - from_warehouse: Optional[str] = None, - to_warehouse: Optional[str] = None, - rate: Optional[float] = None, - serial_no: Optional[str] = None, - batch_no: Optional[str] = None, - posting_date: Optional[str] = None, - posting_time: Optional[str] = None, - purpose: Optional[str] = None, + company: str | None = None, + from_warehouse: str | None = None, + to_warehouse: str | None = None, + rate: float | None = None, + serial_no: str | None = None, + batch_no: str | None = None, + posting_date: str | None = None, + posting_time: str | None = None, + purpose: str | None = None, do_not_save: bool = False, do_not_submit: bool = False, inspection_required: bool = False, diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c4f26c4baaf..085f6b36f29 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2,10 +2,9 @@ # License: GNU General Public License v3. See license.txt -import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today +from frappe.utils import add_days, flt, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import ( @@ -14,7 +13,7 @@ from erpnext.stock.doctype.item.test_item import ( make_item_variant, set_item_variant_settings, ) -from erpnext.stock.doctype.serial_no.serial_no import * # noqa +from erpnext.stock.doctype.serial_no.serial_no import * from erpnext.stock.doctype.stock_entry.stock_entry import ( FinishedGoodError, make_stock_in_entry, @@ -35,7 +34,7 @@ def get_sle(**args): condition, values = "", [] for key, value in args.items(): condition += " and " if condition else " where " - condition += "`{0}`=%s".format(key) + condition += f"`{key}`=%s" values.append(value) return frappe.db.sql( @@ -96,6 +95,12 @@ class TestStockEntry(FrappeTestCase): self._test_auto_material_request("_Test Item") self._test_auto_material_request("_Test Item", material_request_type="Transfer") + def test_barcode_item_stock_entry(self): + item_code = make_item("_Test Item Stock Entry For Barcode", barcode="BDD-1234567890") + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + self.assertEqual(se.items[0].barcode, "BDD-1234567890") + def test_auto_material_request_for_variant(self): fields = [{"field_name": "reorder_levels"}] set_item_variant_settings(fields) @@ -136,8 +141,7 @@ class TestStockEntry(FrappeTestCase): ) projected_qty = ( - frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty") - or 0 + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty") or 0 ) frappe.db.set_value("Stock Settings", None, "auto_indent", 1) @@ -519,10 +523,10 @@ class TestStockEntry(FrappeTestCase): self.assertTrue(sle) sle.sort(key=lambda x: x[1]) - for i, sle in enumerate(sle): - self.assertEqual(expected_sle[i][0], sle[0]) - self.assertEqual(expected_sle[i][1], sle[1]) - self.assertEqual(expected_sle[i][2], sle[2]) + for i, sle_value in enumerate(sle): + self.assertEqual(expected_sle[i][0], sle_value[0]) + self.assertEqual(expected_sle[i][1], sle_value[1]) + self.assertEqual(expected_sle[i][2], sle_value[2]) def check_gl_entries(self, voucher_type, voucher_no, expected_gl_entries): expected_gl_entries.sort(key=lambda x: x[0]) @@ -664,14 +668,10 @@ class TestStockEntry(FrappeTestCase): se.set_stock_entry_type() se.insert() se.submit() - self.assertTrue( - frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC" - ) + self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC") se.cancel() - self.assertTrue( - frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC" - ) + self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC") def test_serial_warehouse_error(self): make_serialized_item(target_warehouse="_Test Warehouse 1 - _TC") @@ -719,9 +719,7 @@ class TestStockEntry(FrappeTestCase): else: item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) - se = make_stock_entry( - item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100 - ) + se = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) batch_no = se.items[0].batch_no serial_no = get_serial_nos(se.items[0].serial_no)[0] batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) @@ -737,7 +735,7 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(batch_in_serial_no, None) self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive") - self.assertEqual(frappe.db.exists("Batch", batch_no), None) + self.assertTrue(frappe.db.exists("Batch", batch_no)) def test_serial_batch_item_qty_deduction(self): """ @@ -797,7 +795,7 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive") def test_warehouse_company_validation(self): - company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company") + frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company") frappe.get_doc("User", "test2@example.com").add_roles( "Sales User", "Sales Manager", "Stock User", "Stock Manager" ) @@ -903,12 +901,8 @@ class TestStockEntry(FrappeTestCase): for d in stock_entry.get("items"): if d.item_code != "_Test FG Item 2": rm_cost += flt(d.amount) - fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item 2", stock_entry.get("items")))[ - 0 - ].amount - self.assertEqual( - fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2) - ) + fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item 2", stock_entry.get("items"))).amount + self.assertEqual(fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2)) @change_settings("Manufacturing Settings", {"material_consumption": 1}) def test_work_order_manufacture_with_material_consumption(self): @@ -951,8 +945,8 @@ class TestStockEntry(FrappeTestCase): for d in s.get("items"): if d.s_warehouse: rm_cost += d.amount - fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount - scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount + fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item", s.get("items"))).amount + scrap_cost = next(filter(lambda x: x.is_scrap_item, s.get("items"))).amount self.assertEqual(fg_cost, flt(rm_cost - scrap_cost, 2)) # When Stock Entry has only FG + Scrap @@ -966,13 +960,11 @@ class TestStockEntry(FrappeTestCase): rm_cost += d.amount self.assertEqual(rm_cost, 0) expected_fg_cost = s.get_basic_rate_for_manufactured_item(1) - fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount + fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item", s.get("items"))).amount self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2)) def test_variant_work_order(self): - bom_no = frappe.db.get_value( - "BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1} - ) + bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1}) make_item_variant() # make variant of _Test Variant Item if absent @@ -1073,9 +1065,7 @@ class TestStockEntry(FrappeTestCase): receipt_entry.insert() receipt_entry.submit() - retention_data = move_sample_to_retention_warehouse( - receipt_entry.company, receipt_entry.get("items") - ) + retention_data = move_sample_to_retention_warehouse(receipt_entry.company, receipt_entry.get("items")) retention_entry = frappe.new_doc("Stock Entry") retention_entry.company = retention_data.company retention_entry.purpose = retention_data.purpose @@ -1126,9 +1116,7 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(frappe.ValidationError, repack.submit) def test_customer_provided_parts_se(self): - create_item( - "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 - ) + create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0) se = make_stock_entry( item_code="CUST-0987", purpose="Material Receipt", qty=4, to_warehouse="_Test Warehouse - _TC" ) @@ -1243,9 +1231,7 @@ class TestStockEntry(FrappeTestCase): self.check_gl_entries( "Stock Entry", se.name, - sorted( - [["Stock Adjustment - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]] - ), + sorted([["Stock Adjustment - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]]), ) def test_conversion_factor_change(self): @@ -1409,7 +1395,7 @@ class TestStockEntry(FrappeTestCase): ) batch_nos.append(se2.items[0].batch_no) - with self.assertRaises(NegativeStockError) as nse: + with self.assertRaises(NegativeStockError): make_stock_entry( item_code=item_code, qty=1, @@ -1455,7 +1441,7 @@ class TestStockEntry(FrappeTestCase): item_code=item_code, qty=1, from_warehouse=warehouse, purpose="Material Issue", do_not_save=True ) issue.append("items", frappe.copy_doc(issue.items[0], ignore_no_copy=False)) - for row, batch_no in zip(issue.items, batch_nos): + for row, batch_no in zip(issue.items, batch_nos, strict=False): row.batch_no = batch_no issue.save() issue.submit() @@ -1663,32 +1649,27 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(BatchExpiredError, se.save) def test_negative_stock_reco(self): - from erpnext.controllers.stock_controller import BatchExpiredError - from erpnext.stock.doctype.batch.test_batch import make_new_batch - frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0) item_code = "Test Negative Item - 001" - item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10) + create_item(item_code=item_code, is_stock_item=1, valuation_rate=10) make_stock_entry( item_code=item_code, posting_date=add_days(today(), -3), posting_time="00:00:00", - purpose="Material Receipt", + target="_Test Warehouse - _TC", qty=10, to_warehouse="_Test Warehouse - _TC", - do_not_save=True, ) make_stock_entry( item_code=item_code, posting_date=today(), posting_time="00:00:00", - purpose="Material Receipt", + source="_Test Warehouse - _TC", qty=8, from_warehouse="_Test Warehouse - _TC", - do_not_save=True, ) sr_doc = create_stock_reconciliation( @@ -1704,35 +1685,47 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(frappe.ValidationError, sr_doc.submit) - def test_enqueue_action(self): - frappe.flags.in_test = False - item_code = "Test Enqueue Item - 001" - create_item(item_code=item_code, is_stock_item=1, valuation_rate=10) + def test_auto_reorder_level(self): + from erpnext.stock.reorder_item import reorder_item - doc = make_stock_entry( - item_code=item_code, - posting_date=add_to_date(today(), months=-7), - posting_time="00:00:00", - purpose="Material Receipt", - qty=10, - to_warehouse="_Test Warehouse - _TC", - do_not_submit=True, + item_doc = make_item( + "Test Auto Reorder Item - 001", + properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1}, + uoms=[{"uom": "Nos", "conversion_factor": 5}], ) - self.assertTrue(doc.is_enqueue_action()) + if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}): + item_doc.append( + "reorder_levels", + { + "warehouse_reorder_level": 0, + "warehouse_reorder_qty": 10, + "warehouse": "_Test Warehouse - _TC", + "material_request_type": "Purchase", + }, + ) - doc = make_stock_entry( - item_code=item_code, - posting_date=today(), - posting_time="00:00:00", - purpose="Material Receipt", - qty=10, - to_warehouse="_Test Warehouse - _TC", - do_not_submit=True, + item_doc.save(ignore_permissions=True) + + frappe.db.set_single_value("Stock Settings", "auto_indent", 1) + + mr_list = reorder_item() + + frappe.db.set_single_value("Stock Settings", "auto_indent", 0) + mrs = frappe.get_all( + "Material Request Item", + fields=["qty", "stock_uom", "stock_qty"], + filters={"item_code": item_doc.name, "uom": "Nos"}, ) - self.assertFalse(doc.is_enqueue_action()) - frappe.flags.in_test = True + for mri in mrs: + self.assertEqual(mri.stock_uom, "Kg") + self.assertEqual(mri.stock_qty, 10) + self.assertEqual(mri.qty, 2) + + for mr in mr_list: + mr.cancel() + mr.delete() def make_serialized_item(**args): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 6b1a8efc997..5e523aef999 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -557,7 +557,8 @@ "label": "Job Card Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "0", @@ -572,7 +573,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-09 12:41:18.210864", + "modified": "2024-01-12 11:56:04.626103", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.js b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.js index c554278334b..21e60fbcae4 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.js +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Stock Entry Type', { +frappe.ui.form.on("Stock Entry Type", { // refresh: function(frm) { - // } }); diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.js b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.js index 23018aa615b..7196dc9f1e3 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.js +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.js @@ -1,8 +1,8 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Stock Ledger Entry', { - refresh: function(frm) { - frm.page.btn_secondary.hide() - } +frappe.ui.form.on("Stock Ledger Entry", { + refresh: function (frm) { + frm.page.btn_secondary.hide(); + }, }); diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index b07c0876a25..835002f0e16 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -11,6 +11,7 @@ "warehouse", "posting_date", "posting_time", + "posting_datetime", "is_adjustment_entry", "column_break_6", "voucher_type", @@ -96,7 +97,6 @@ "oldfieldtype": "Date", "print_width": "100px", "read_only": 1, - "search_index": 1, "width": "100px" }, { @@ -226,7 +226,7 @@ }, { "fieldname": "stock_queue", - "fieldtype": "Text", + "fieldtype": "Long Text", "label": "FIFO Stock Queue (qty, rate)", "oldfieldname": "fcfs_stack", "oldfieldtype": "Text", @@ -249,7 +249,6 @@ "options": "Company", "print_width": "150px", "read_only": 1, - "search_index": 1, "width": "150px" }, { @@ -316,6 +315,11 @@ "fieldname": "is_adjustment_entry", "fieldtype": "Check", "label": "Is Adjustment Entry" + }, + { + "fieldname": "posting_datetime", + "fieldtype": "Datetime", + "label": "Posting Datetime" } ], "hide_toolbar": 1, @@ -324,7 +328,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-23 18:07:42.063615", + "modified": "2024-02-07 09:18:13.999231", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 921b04aab8c..523a1d15cee 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -52,7 +52,16 @@ class StockLedgerEntry(Document): self.validate_with_last_transaction_posting_time() self.validate_inventory_dimension_negative_stock() + def set_posting_datetime(self): + from erpnext.stock.utils import get_combine_datetime + + self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time) + self.db_set("posting_datetime", self.posting_datetime) + def validate_inventory_dimension_negative_stock(self): + if self.is_cancelled: + return + extra_cond = "" kwargs = {} @@ -71,16 +80,20 @@ class StockLedgerEntry(Document): "posting_date": self.posting_date, "posting_time": self.posting_time, "company": self.company, + "sle": self.name, } ) sle = get_previous_sle(kwargs, extra_cond=extra_cond) + qty_after_transaction = 0.0 + flt_precision = cint(frappe.db.get_default("float_precision")) or 2 if sle: - flt_precision = cint(frappe.db.get_default("float_precision")) or 2 - diff = sle.qty_after_transaction + flt(self.actual_qty) - diff = flt(diff, flt_precision) - if diff < 0 and abs(diff) > 0.0001: - self.throw_validation_error(diff, dimensions) + qty_after_transaction = sle.qty_after_transaction + + diff = qty_after_transaction + flt(self.actual_qty) + diff = flt(diff, flt_precision) + if diff < 0 and abs(diff) > 0.0001: + self.throw_validation_error(diff, dimensions) def throw_validation_error(self, diff, dimensions): dimension_msg = _(", with the inventory {0}: {1}").format( @@ -115,6 +128,7 @@ class StockLedgerEntry(Document): return inv_dimension_dict def on_submit(self): + self.set_posting_datetime() self.check_stock_frozen_date() self.calculate_batch_qty() @@ -208,7 +222,9 @@ class StockLedgerEntry(Document): ) if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles(): frappe.throw( - _("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), + _("Not allowed to update stock transactions older than {0}").format( + stock_frozen_upto_days + ), StockFreezeError, ) @@ -226,7 +242,9 @@ class StockLedgerEntry(Document): expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date") if expiry_date: if getdate(self.posting_date) > getdate(expiry_date): - frappe.throw(_("Batch {0} of Item {1} has expired.").format(self.batch_no, self.item_code)) + frappe.throw( + _("Batch {0} of Item {1} has expired.").format(self.batch_no, self.item_code) + ) def validate_and_set_fiscal_year(self): if not self.fiscal_year: @@ -259,7 +277,7 @@ class StockLedgerEntry(Document): (self.item_code, self.warehouse), )[0][0] - cur_doc_posting_datetime = "%s %s" % ( + cur_doc_posting_datetime = "{} {}".format( self.posting_date, self.get("posting_time") or "00:00:00", ) @@ -268,7 +286,9 @@ class StockLedgerEntry(Document): last_transaction_time ): msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format( - frappe.bold(self.item_code), frappe.bold(self.warehouse), frappe.bold(last_transaction_time) + frappe.bold(self.item_code), + frappe.bold(self.warehouse), + frappe.bold(last_transaction_time), ) msg += "

              " + _( @@ -286,9 +306,7 @@ class StockLedgerEntry(Document): def on_doctype_update(): - frappe.db.add_index( - "Stock Ledger Entry", fields=["posting_date", "posting_time"], index_name="posting_sort_index" - ) frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse") + frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"]) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 6c341d9e9ec..2b01ada1092 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -2,12 +2,12 @@ # See license.txt import json +import time from uuid import uuid4 import frappe from frappe.core.page.permission_manager.permission_manager import reset from frappe.custom.doctype.property_setter.property_setter import make_property_setter -from frappe.query_builder.functions import CombineDatetime from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, add_to_date, flt, today @@ -34,8 +34,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): # delete SLE and BINs for all items frappe.db.sql( - "delete from `tabStock Ledger Entry` where item_code in (%s)" - % (", ".join(["%s"] * len(items))), + "delete from `tabStock Ledger Entry` where item_code in (%s)" % (", ".join(["%s"] * len(items))), items, ) frappe.db.sql( @@ -121,7 +120,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): self.assertEqual(finished_item_sle.get("valuation_rate"), 540) # Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150 - sr = create_stock_reconciliation( + create_stock_reconciliation( item_code="_Test Item for Reposting", warehouse="Stores - _TC", qty=50, @@ -483,7 +482,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): expected_incoming_rates = expected_abs_svd = [75, 125, 75, 125] self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values") - for dn, incoming_rate in zip(dns, expected_incoming_rates): + for dn, incoming_rate in zip(dns, expected_incoming_rates, strict=False): self.assertEqual( dn.items[0].incoming_rate, incoming_rate, @@ -555,9 +554,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): ], ) - reciept = make_stock_entry( - item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10 - ) + reciept = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10) self.assertSLEs( reciept, [ @@ -679,9 +676,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): ], ) - consume_today = make_stock_entry( - item_code=item_code, source=warehouse, batch_no=batches[0], qty=1 - ) + consume_today = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0], qty=1) self.assertSLEs( consume_today, [ @@ -714,8 +709,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): item, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0) def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns): - for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)): - for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals): + for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details, strict=False)): + for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals, strict=False): if col == "stock_queue": sle_val = get_stock_value_from_q(sle_val) ex_sle_val = get_stock_value_from_q(ex_sle_val) @@ -745,9 +740,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): se_entry_list_mi = [ (item, warehouses[0], None, batches[1], 1, None, "2021-01-29"), ] - ses = create_stock_entry_entries_for_batchwise_item_valuation_test( - se_entry_list_mi, "Material Issue" - ) + ses = create_stock_entry_entries_for_batchwise_item_valuation_test(se_entry_list_mi, "Material Issue") sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0) expected_sle_details = [(-50.0, 100.0, -1.0, 1.0, "[[1, 100.0]]")] details_list.append((sle_details, expected_sle_details, "Material Issue Entries", columns)) @@ -770,9 +763,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): sle["qty_after_transaction"] = state["qty"] return exp_sles - old1 = make_stock_entry( - item_code=item_code, target=warehouse, batch_no=batches[0], qty=10, rate=10 - ) + old1 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], qty=10, rate=10) self.assertSLEs( old1, update_invariants( @@ -781,20 +772,20 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): ] ), ) - old2 = make_stock_entry( - item_code=item_code, target=warehouse, batch_no=batches[1], qty=10, rate=20 - ) + old2 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1], qty=10, rate=20) self.assertSLEs( old2, update_invariants( [ - {"actual_qty": 10, "stock_value_difference": 10 * 20, "stock_queue": [[10, 10], [10, 20]]}, + { + "actual_qty": 10, + "stock_value_difference": 10 * 20, + "stock_queue": [[10, 10], [10, 20]], + }, ] ), ) - old3 = make_stock_entry( - item_code=item_code, target=warehouse, batch_no=batches[0], qty=5, rate=15 - ) + old3 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], qty=5, rate=15) self.assertSLEs( old3, @@ -841,9 +832,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): ) # consume old batch as per FIFO - consume_old1 = make_stock_entry( - item_code=item_code, source=warehouse, qty=15, batch_no=batches[0] - ) + consume_old1 = make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0]) self.assertSLEs( consume_old1, update_invariants( @@ -858,22 +847,22 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): ) # consume new batch as per batch - consume_new2 = make_stock_entry( - item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1] - ) + consume_new2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1]) self.assertSLEs( consume_new2, update_invariants( [ - {"actual_qty": -10, "stock_value_difference": -10 * 42, "stock_queue": [[5, 20], [5, 15]]}, + { + "actual_qty": -10, + "stock_value_difference": -10 * 42, + "stock_queue": [[5, 20], [5, 15]], + }, ] ), ) # finish all old batches - consume_old2 = make_stock_entry( - item_code=item_code, source=warehouse, qty=10, batch_no=batches[1] - ) + consume_old2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1]) self.assertSLEs( consume_old2, update_invariants( @@ -884,9 +873,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): ) # finish all new batches - consume_new1 = make_stock_entry( - item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2] - ) + consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2]) self.assertSLEs( consume_new1, update_invariants( @@ -913,14 +900,14 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): receipt.submit() expected_queues = [] - for idx, rate in enumerate(rates, start=1): + for idx in range(1, len(rates) + 1): expected_queues.append({"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}) self.assertSLEs(receipt, expected_queues) transfer = make_stock_entry( item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10 ) - for rate in rates[1:]: + for _ in rates[1:]: row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False) transfer.append("items", row) @@ -937,9 +924,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): rates = [10 * i for i in range(1, 5)] - receipt = make_stock_entry( - item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10 - ) + receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10) for rate in rates[1:]: row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False) row.basic_rate = rate @@ -951,7 +936,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): repack = make_stock_entry( item_code=rm.name, source=warehouse, qty=10, do_not_save=True, rate=10, purpose="Repack" ) - for rate in rates[1:]: + for _ in rates[1:]: row = frappe.copy_doc(repack.items[0], ignore_no_copy=False) repack.append("items", row) @@ -969,9 +954,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): repack.submit() # same exact queue should be transferred - self.assertSLEs( - repack, [{"incoming_rate": sum(rates) * 10}], sle_filters={"item_code": packed.name} - ) + self.assertSLEs(repack, [{"incoming_rate": sum(rates) * 10}], sle_filters={"item_code": packed.name}) def test_negative_fifo_valuation(self): """ @@ -981,7 +964,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): item = make_item(properties={"allow_negative_stock": 1}).name warehouse = "_Test Warehouse - _TC" - receipt = make_stock_entry(item_code=item, target=warehouse, qty=10, rate=10) + make_stock_entry(item_code=item, target=warehouse, qty=10, rate=10) consume1 = make_stock_entry(item_code=item, source=warehouse, qty=15) self.assertSLEs(consume1, [{"stock_value": -5 * 10, "stock_queue": [[-5, 10]]}]) @@ -1021,27 +1004,21 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): self.assertEqual(50, _get_stock_credit(depdendent_consumption)) # backdated receipt - should trigger GL repost of all previous stock entries - bd_receipt = make_stock_entry( - item_code=item, to_warehouse=A, qty=5, rate=20, posting_date=_day(-1) - ) + bd_receipt = make_stock_entry(item_code=item, to_warehouse=A, qty=5, rate=20, posting_date=_day(-1)) self.assertEqual(100, _get_stock_credit(depdendent_consumption)) # cancelling receipt should reset it back bd_receipt.cancel() self.assertEqual(50, _get_stock_credit(depdendent_consumption)) - bd_receipt2 = make_stock_entry( - item_code=item, to_warehouse=A, qty=2, rate=20, posting_date=_day(-2) - ) + bd_receipt2 = make_stock_entry(item_code=item, to_warehouse=A, qty=2, rate=20, posting_date=_day(-2)) # total as per FIFO -> 2 * 20 + 3 * 10 = 70 self.assertEqual(70, _get_stock_credit(depdendent_consumption)) # transfer WIP material to final destination and consume it all depdendent_consumption.cancel() make_stock_entry(item_code=item, from_warehouse=B, to_warehouse=C, qty=5, posting_date=_day(3)) - final_consumption = make_stock_entry( - item_code=item, from_warehouse=C, qty=5, posting_date=_day(4) - ) + final_consumption = make_stock_entry(item_code=item, from_warehouse=C, qty=5, posting_date=_day(4)) # exact amount gets consumed self.assertEqual(70, _get_stock_credit(final_consumption)) @@ -1066,7 +1043,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): frappe.qb.from_(sle) .select("qty_after_transaction") .where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0)) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.posting_datetime) .orderby(sle.creation) ).run(pluck=True) @@ -1108,11 +1085,10 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): self.assertEqual([1], ordered_qty_after_transaction()) def test_timestamp_clash(self): - item = make_item().name warehouse = "_Test Warehouse - _TC" - reciept = make_stock_entry( + make_stock_entry( item_code=item, to_warehouse=warehouse, qty=100, @@ -1121,7 +1097,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): posting_time="01:00:00", ) - consumption = make_stock_entry( + make_stock_entry( item_code=item, from_warehouse=warehouse, qty=50, @@ -1140,9 +1116,90 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): try: backdated_receipt.cancel() - except Exception as e: + except Exception: self.fail("Double processing of qty for clashing timestamp.") + def test_previous_sle_with_clashed_timestamp(self): + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + reciept1 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=100, + rate=10, + posting_date="2021-01-01", + posting_time="02:00:00", + ) + + time.sleep(3) + + reciept2 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=5, + posting_date="2021-01-01", + rate=10, + posting_time="02:00:00.1234", + ) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept1.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 100) + self.assertEqual(sle[0].actual_qty, 100) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept2.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 105) + self.assertEqual(sle[0].actual_qty, 5) + + def test_backdated_sle_with_same_timestamp(self): + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + reciept1 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=5, + posting_date="2021-01-01", + rate=10, + posting_time="02:00:00.1234", + ) + + time.sleep(3) + + # backdated entry with same timestamp but different ms part + reciept2 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=100, + rate=10, + posting_date="2021-01-01", + posting_time="02:00:00", + ) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept1.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 5) + self.assertEqual(sle[0].actual_qty, 5) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept2.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 105) + self.assertEqual(sle[0].actual_qty, 100) + @change_settings("System Settings", {"float_precision": 3, "currency_precision": 2}) def test_transfer_invariants(self): """Extact stock value should be transferred.""" @@ -1345,12 +1402,13 @@ def create_items(items=None, uoms=None): def setup_item_valuation_test( - valuation_method="FIFO", suffix=None, use_batchwise_valuation=1, batches_list=["X", "Y"] + valuation_method="FIFO", suffix=None, use_batchwise_valuation=1, batches_list=None ): - from erpnext.stock.doctype.batch.batch import make_batch from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + if batches_list is None: + batches_list = ["X", "Y"] if not suffix: suffix = get_unique_suffix() @@ -1364,7 +1422,7 @@ def setup_item_valuation_test( for i, batch_id in enumerate(batches): if not frappe.db.exists("Batch", batch_id): ubw = use_batchwise_valuation - if isinstance(use_batchwise_valuation, (list, tuple)): + if isinstance(use_batchwise_valuation, list | tuple): ubw = use_batchwise_valuation[i] batch = frappe.get_doc( frappe._dict( @@ -1395,9 +1453,7 @@ def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list dns = [] for item, warehouse, batch_no, qty, rate in dn_entry_list: - so = make_sales_order( - rate=rate, qty=qty, item=item, warehouse=warehouse, against_blanket_order=0 - ) + so = make_sales_order(rate=rate, qty=qty, item=item, warehouse=warehouse, against_blanket_order=0) dn = make_delivery_note(so.name) dn.items[0].batch_no = batch_no diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 1415aa36164..6ba2f0ac051 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -5,29 +5,29 @@ frappe.provide("erpnext.stock"); frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Stock Reconciliation", { - onload: function(frm) { + onload: function (frm) { frm.add_fetch("item_code", "item_name", "item_name"); // end of life - frm.set_query("item_code", "items", function(doc, cdt, cdn) { + frm.set_query("item_code", "items", function (doc, cdt, cdn) { return { query: "erpnext.controllers.queries.item_query", - filters:{ - "is_stock_item": 1 - } - } + filters: { + is_stock_item: 1, + }, + }; }); - frm.set_query("batch_no", "items", function(doc, cdt, cdn) { + frm.set_query("batch_no", "items", function (doc, cdt, cdn) { var item = locals[cdt][cdn]; return { filters: { - 'item': item.item_code - } + item: item.item_code, + }, }; }); if (frm.doc.company) { - erpnext.queries.setup_queries(frm, "Warehouse", function() { + erpnext.queries.setup_queries(frm, "Warehouse", function () { return erpnext.queries.warehouse(frm.doc); }); } @@ -39,56 +39,68 @@ frappe.ui.form.on("Stock Reconciliation", { erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, - company: function(frm) { + company: function (frm) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, - refresh: function(frm) { - if(frm.doc.docstatus < 1) { - frm.add_custom_button(__("Fetch Items from Warehouse"), function() { + refresh: function (frm) { + if (frm.doc.docstatus < 1) { + frm.add_custom_button(__("Fetch Items from Warehouse"), function () { frm.events.get_items(frm); }); } - if(frm.doc.company) { + if (frm.doc.company) { frm.trigger("toggle_display_account_head"); } + + frm.events.set_fields_onload_for_line_item(frm); }, - scan_barcode: function(frm) { - const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:frm}); - barcode_scanner.process_scan(); - }, - - scan_mode: function(frm) { - if (frm.doc.scan_mode) { - frappe.show_alert({ - message: __("Scan mode enabled, existing quantity will not be fetched."), - indicator: "green" + set_fields_onload_for_line_item(frm) { + if (frm.is_new() && frm.doc?.items && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) { + frm.doc.items.forEach((item) => { + if (!item.serial_and_batch_bundle) { + frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1); + } }); } }, - set_warehouse: function(frm) { - let transaction_controller = new erpnext.TransactionController({frm:frm}); + scan_barcode: function (frm) { + const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm }); + barcode_scanner.process_scan(); + }, + + scan_mode: function (frm) { + if (frm.doc.scan_mode) { + frappe.show_alert({ + message: __("Scan mode enabled, existing quantity will not be fetched."), + indicator: "green", + }); + } + }, + + set_warehouse: function (frm) { + let transaction_controller = new erpnext.TransactionController({ frm: frm }); transaction_controller.autofill_warehouse(frm.doc.items, "warehouse", frm.doc.set_warehouse); }, - get_items: function(frm) { + get_items: function (frm) { let fields = [ { - label: 'Warehouse', - fieldname: 'warehouse', - fieldtype: 'Link', - options: 'Warehouse', + label: "Warehouse", + fieldname: "warehouse", + fieldtype: "Link", + options: "Warehouse", reqd: 1, - "get_query": function() { + get_query: function () { return { - "filters": { - "company": frm.doc.company, - } + filters: { + company: frm.doc.company, + }, }; - } + }, }, { label: "Item Code", @@ -99,57 +111,65 @@ frappe.ui.form.on("Stock Reconciliation", { { label: __("Ignore Empty Stock"), fieldname: "ignore_empty_stock", - fieldtype: "Check" - } + fieldtype: "Check", + }, ]; - frappe.prompt(fields, function(data) { - frappe.call({ - method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", - args: { - warehouse: data.warehouse, - posting_date: frm.doc.posting_date, - posting_time: frm.doc.posting_time, - company: frm.doc.company, - item_code: data.item_code, - ignore_empty_stock: data.ignore_empty_stock - }, - callback: function(r) { - if (r.exc || !r.message || !r.message.length) return; + frappe.prompt( + fields, + function (data) { + frappe.call({ + method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", + args: { + warehouse: data.warehouse, + posting_date: frm.doc.posting_date, + posting_time: frm.doc.posting_time, + company: frm.doc.company, + item_code: data.item_code, + ignore_empty_stock: data.ignore_empty_stock, + }, + callback: function (r) { + if (r.exc || !r.message || !r.message.length) return; - frm.clear_table("items"); + frm.clear_table("items"); - r.message.forEach((row) => { - let item = frm.add_child("items"); - $.extend(item, row); + r.message.forEach((row) => { + let item = frm.add_child("items"); + $.extend(item, row); - item.qty = item.qty || 0; - item.valuation_rate = item.valuation_rate || 0; - }); - frm.refresh_field("items"); - } - }); - }, __("Get Items"), __("Update")); + item.qty = item.qty || 0; + item.valuation_rate = item.valuation_rate || 0; + item.use_serial_batch_fields = cint( + frappe.user_defaults?.use_serial_batch_fields + ); + }); + frm.refresh_field("items"); + }, + }); + }, + __("Get Items"), + __("Update") + ); }, - posting_date: function(frm) { + posting_date: function (frm) { frm.trigger("set_valuation_rate_and_qty_for_all_items"); }, - posting_time: function(frm) { + posting_time: function (frm) { frm.trigger("set_valuation_rate_and_qty_for_all_items"); }, - set_valuation_rate_and_qty_for_all_items: function(frm) { - frm.doc.items.forEach(row => { + set_valuation_rate_and_qty_for_all_items: function (frm) { + frm.doc.items.forEach((row) => { frm.events.set_valuation_rate_and_qty(frm, row.doctype, row.name); }); }, - set_valuation_rate_and_qty: function(frm, cdt, cdn) { + set_valuation_rate_and_qty: function (frm, cdt, cdn) { var d = frappe.model.get_doc(cdt, cdn); - if(d.item_code && d.warehouse) { + if (d.item_code && d.warehouse) { frappe.call({ method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_stock_balance_for", args: { @@ -157,9 +177,9 @@ frappe.ui.form.on("Stock Reconciliation", { warehouse: d.warehouse, posting_date: frm.doc.posting_date, posting_time: frm.doc.posting_time, - batch_no: d.batch_no + batch_no: d.batch_no, }, - callback: function(r) { + callback: function (r) { const row = frappe.model.get_doc(cdt, cdn); if (!frm.doc.scan_mode) { frappe.model.set_value(cdt, cdn, "qty", r.message.qty); @@ -174,12 +194,12 @@ frappe.ui.form.on("Stock Reconciliation", { if (frm.doc.purpose == "Stock Reconciliation" && !frm.doc.scan_mode) { frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos); } - } + }, }); } }, - set_amount_quantity: function(doc, cdt, cdn) { + set_amount_quantity: function (doc, cdt, cdn) { var d = frappe.model.get_doc(cdt, cdn); if (d.qty & d.valuation_rate) { frappe.model.set_value(cdt, cdn, "amount", flt(d.qty) * flt(d.valuation_rate)); @@ -187,37 +207,38 @@ frappe.ui.form.on("Stock Reconciliation", { frappe.model.set_value(cdt, cdn, "amount_difference", flt(d.amount) - flt(d.current_amount)); } }, - company: function(frm) { + company: function (frm) { frm.trigger("toggle_display_account_head"); }, - toggle_display_account_head: function(frm) { - frm.toggle_display(['expense_account', 'cost_center'], - erpnext.is_perpetual_inventory_enabled(frm.doc.company)); + toggle_display_account_head: function (frm) { + frm.toggle_display( + ["expense_account", "cost_center"], + erpnext.is_perpetual_inventory_enabled(frm.doc.company) + ); }, - purpose: function(frm) { + purpose: function (frm) { frm.trigger("set_expense_account"); }, - set_expense_account: function(frm) { + set_expense_account: function (frm) { if (frm.doc.company && erpnext.is_perpetual_inventory_enabled(frm.doc.company)) { return frm.call({ method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_difference_account", args: { - "purpose": frm.doc.purpose, - "company": frm.doc.company + purpose: frm.doc.purpose, + company: frm.doc.company, }, - callback: function(r) { + callback: function (r) { if (!r.exc) { frm.set_value("expense_account", r.message); } - } + }, }); } - } + }, }); frappe.ui.form.on("Stock Reconciliation Item", { - - warehouse: function(frm, cdt, cdn) { + warehouse: function (frm, cdt, cdn) { var child = locals[cdt][cdn]; if (child.batch_no && !frm.doc.scan_mode) { frappe.model.set_value(child.cdt, child.cdn, "batch_no", ""); @@ -226,7 +247,7 @@ frappe.ui.form.on("Stock Reconciliation Item", { frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, - item_code: function(frm, cdt, cdn) { + item_code: function (frm, cdt, cdn) { var child = locals[cdt][cdn]; if (child.batch_no && !frm.doc.scan_mode) { frappe.model.set_value(cdt, cdn, "batch_no", ""); @@ -235,34 +256,37 @@ frappe.ui.form.on("Stock Reconciliation Item", { frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, - batch_no: function(frm, cdt, cdn) { + batch_no: function (frm, cdt, cdn) { frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, - qty: function(frm, cdt, cdn) { + qty: function (frm, cdt, cdn) { frm.events.set_amount_quantity(frm, cdt, cdn); }, - valuation_rate: function(frm, cdt, cdn) { + valuation_rate: function (frm, cdt, cdn) { frm.events.set_amount_quantity(frm, cdt, cdn); }, - serial_no: function(frm, cdt, cdn) { + serial_no: function (frm, cdt, cdn) { var child = locals[cdt][cdn]; if (child.serial_no) { - const serial_nos = child.serial_no.trim().split('\n'); + const serial_nos = child.serial_no.trim().split("\n"); frappe.model.set_value(cdt, cdn, "qty", serial_nos.length); } }, - items_add: function(frm, cdt, cdn) { + items_add: function (frm, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); if (!item.warehouse && frm.doc.set_warehouse) { frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.set_warehouse); } - }, + if (item.docstatus === 0 && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) { + frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1); + } + }, }); erpnext.stock.StockReconciliation = class StockReconciliation extends erpnext.stock.StockController { @@ -274,37 +298,36 @@ erpnext.stock.StockReconciliation = class StockReconciliation extends erpnext.st if (me.frm.doc.company && erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) { this.frm.add_fetch("company", "cost_center", "cost_center"); } - this.frm.fields_dict["expense_account"].get_query = function() { - if(erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) { + this.frm.fields_dict["expense_account"].get_query = function () { + if (erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) { return { - "filters": { - 'company': me.frm.doc.company, - "is_group": 0 - } - } + filters: { + company: me.frm.doc.company, + is_group: 0, + }, + }; } - } - this.frm.fields_dict["cost_center"].get_query = function() { - if(erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) { + }; + this.frm.fields_dict["cost_center"].get_query = function () { + if (erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) { return { - "filters": { - 'company': me.frm.doc.company, - "is_group": 0 - } - } + filters: { + company: me.frm.doc.company, + is_group: 0, + }, + }; } - } + }; } refresh() { - if(this.frm.doc.docstatus > 0) { + if (this.frm.doc.docstatus > 0) { this.show_stock_ledger(); if (erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { this.show_general_ledger(); } } } - }; -cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm}); +cur_frm.cscript = new erpnext.stock.StockReconciliation({ frm: cur_frm }); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index dd39f103cd7..d78e3c14048 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -1,7 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from typing import Optional import frappe from frappe import _, bold, msgprint @@ -27,7 +26,7 @@ class EmptyStockReconciliationItemsError(frappe.ValidationError): class StockReconciliation(StockController): def __init__(self, *args, **kwargs): - super(StockReconciliation, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"] def validate(self): @@ -87,7 +86,9 @@ class StockReconciliation(StockController): if not item.batch_no and not item.serial_no: for dimension in get_inventory_dimensions(): if item.get(dimension.get("fieldname")): - inventory_dimensions_dict[dimension.get("fieldname")] = item.get(dimension.get("fieldname")) + inventory_dimensions_dict[dimension.get("fieldname")] = item.get( + dimension.get("fieldname") + ) item_dict = get_stock_balance_for( item.item_code, @@ -189,19 +190,27 @@ class StockReconciliation(StockController): # do not allow negative valuation if flt(row.valuation_rate) < 0: - self.validation_messages.append(_get_msg(row_num, _("Negative Valuation Rate is not allowed"))) + self.validation_messages.append( + _get_msg(row_num, _("Negative Valuation Rate is not allowed")) + ) if row.batch_no and frappe.get_cached_value("Batch", row.batch_no, "item") != row.item_code: self.validation_messages.append( _get_msg( row_num, - _("Batch {0} does not belong to item {1}").format(bold(row.batch_no), bold(row.item_code)), + _("Batch {0} does not belong to item {1}").format( + bold(row.batch_no), bold(row.item_code) + ), ) ) if row.qty and row.valuation_rate in ["", None]: row.valuation_rate = get_stock_balance( - row.item_code, row.warehouse, self.posting_date, self.posting_time, with_valuation_rate=True + row.item_code, + row.warehouse, + self.posting_date, + self.posting_time, + with_valuation_rate=True, )[1] if not row.valuation_rate: # try if there is a buying price list in default currency @@ -263,11 +272,10 @@ class StockReconciliation(StockController): sl_entries = [] has_serial_no = False - has_batch_no = False for row in self.items: item = frappe.get_doc("Item", row.item_code) if item.has_batch_no: - has_batch_no = True + pass if not row.qty and not row.valuation_rate and not row.current_qty: self.make_adjustment_entry(row, sl_entries) @@ -542,18 +550,14 @@ class StockReconciliation(StockController): data.incoming_rate = (data.total_amount) / data.actual_qty - for key, value in merge_similar_entries.items(): - new_sl_entries.append(value) - + new_sl_entries.extend(merge_similar_entries.values()) return new_sl_entries def get_gl_entries(self, warehouse_account=None): if not self.cost_center: msgprint(_("Please enter Cost Center"), raise_exception=1) - return super(StockReconciliation, self).get_gl_entries( - warehouse_account, self.expense_account, self.cost_center - ) + return super().get_gl_entries(warehouse_account, self.expense_account, self.cost_center) def validate_expense_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -634,30 +638,53 @@ class StockReconciliation(StockController): if voucher_detail_no != row.name: continue - current_qty = get_batch_qty_for_stock_reco( - row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name - ) + if row.serial_no: + item_dict = get_stock_balance_for( + row.item_code, + row.warehouse, + self.posting_date, + self.posting_time, + voucher_no=self.name, + ) + + current_qty = item_dict.get("qty") + row.current_serial_no = item_dict.get("serial_nos") + row.current_valuation_rate = item_dict.get("rate") + else: + current_qty = get_batch_qty_for_stock_reco( + row.item_code, + row.warehouse, + row.batch_no, + self.posting_date, + self.posting_time, + self.name, + ) precesion = row.precision("current_qty") if flt(current_qty, precesion) != flt(row.current_qty, precesion): - val_rate = get_valuation_rate( - row.item_code, - row.warehouse, - self.doctype, - self.name, - company=self.company, - batch_no=row.batch_no, - ) + if not row.serial_no: + val_rate = get_valuation_rate( + row.item_code, + row.warehouse, + self.doctype, + self.name, + company=self.company, + batch_no=row.batch_no, + ) + + row.current_valuation_rate = val_rate - row.current_valuation_rate = val_rate row.current_qty = current_qty - row.db_set( - { - "current_qty": row.current_qty, - "current_valuation_rate": row.current_valuation_rate, - "current_amount": flt(row.current_qty * row.current_valuation_rate), - } - ) + values_to_update = { + "current_qty": row.current_qty, + "current_valuation_rate": row.current_valuation_rate, + "current_amount": flt(row.current_qty * row.current_valuation_rate), + } + + if row.current_serial_no: + values_to_update["current_serial_no"] = row.current_serial_no + + row.db_set(values_to_update) if ( add_new_sle @@ -681,16 +708,16 @@ class StockReconciliation(StockController): def has_negative_stock_allowed(self): allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + if allow_negative_stock: + return True - if all(d.batch_no and flt(d.qty) == flt(d.current_qty) for d in self.items): + if any((d.batch_no and flt(d.qty) == flt(d.current_qty)) for d in self.items): allow_negative_stock = True return allow_negative_stock -def get_batch_qty_for_stock_reco( - item_code, warehouse, batch_no, posting_date, posting_time, voucher_no -): +def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no, posting_date, posting_time, voucher_no): ledger = frappe.qb.DocType("Stock Ledger Entry") query = ( @@ -720,11 +747,11 @@ def get_batch_qty_for_stock_reco( @frappe.whitelist() -def get_items( - warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False -): +def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False): ignore_empty_stock = cint(ignore_empty_stock) - items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})] + items = [] + if item_code and warehouse: + items = get_item_and_warehouses(item_code, warehouse) if not item_code: items = get_items_for_stock_reco(warehouse, company) @@ -769,6 +796,20 @@ def get_items( return res +def get_item_and_warehouses(item_code, warehouse): + from frappe.utils.nestedset import get_descendants_of + + items = [] + if frappe.get_cached_value("Warehouse", warehouse, "is_group"): + childrens = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft") + for ch_warehouse in childrens: + items.append(frappe._dict({"item_code": item_code, "warehouse": ch_warehouse})) + else: + items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})] + + return items + + def get_items_for_stock_reco(warehouse, company): lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) items = frappe.db.sql( @@ -783,7 +824,7 @@ def get_items_for_stock_reco(warehouse, company): and i.is_stock_item = 1 and i.has_variants = 0 and exists( - select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse + select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse and is_group = 0 ) """, as_dict=1, @@ -798,7 +839,7 @@ def get_items_for_stock_reco(warehouse, company): where i.name = id.parent and exists( - select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse + select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse and is_group = 0 ) and i.is_stock_item = 1 and i.has_variants = 0 @@ -860,7 +901,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None): frappe._dict( { "item_code": row[0], - "warehouse": warehouse, + "warehouse": row[3], "qty": row[8], "item_name": row[1], "batch_no": row[4], @@ -877,15 +918,14 @@ def get_stock_balance_for( warehouse: str, posting_date, posting_time, - batch_no: Optional[str] = None, + batch_no: str | None = None, with_valuation_rate: bool = True, inventory_dimensions_dict=None, + voucher_no=None, ): frappe.has_permission("Stock Reconciliation", "write", throw=True) - item_dict = frappe.get_cached_value( - "Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1 - ) + item_dict = frappe.get_cached_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1) if not item_dict: # In cases of data upload to Items table @@ -910,6 +950,7 @@ def get_stock_balance_for( with_serial_no=has_serial_no, inventory_dimensions_dict=inventory_dimensions_dict, batch_no=batch_no, + voucher_no=voucher_no, ) if has_serial_no: @@ -918,9 +959,7 @@ def get_stock_balance_for( qty, rate = data if item_dict.get("has_batch_no"): - qty = ( - get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 - ) + qty = get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 return {"qty": qty, "rate": rate, "serial_nos": serial_nos} diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 05c60175f51..8f3c9891334 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -101,7 +101,8 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): # no gl entries self.assertTrue( frappe.db.get_value( - "Stock Ledger Entry", {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name} + "Stock Ledger Entry", + {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name}, ) ) @@ -147,7 +148,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): def test_stock_reco_for_serialized_item(self): to_delete_records = [] - to_delete_serial_nos = [] # Add new serial nos serial_item_code = "Stock-Reco-Serial-Item-1" @@ -215,7 +215,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): purpose="Opening Stock", ) - for i in range(3): + for _i in range(3): sr.append( "items", { @@ -312,7 +312,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): sr.cancel() self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive") - self.assertEqual(frappe.db.exists("Batch", batch_no), None) + self.assertTrue(frappe.db.exists("Batch", batch_no)) def test_stock_reco_balance_qty_for_serial_and_batch_item(self): from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -394,9 +394,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): def test_customer_provided_items(self): item_code = "Stock-Reco-customer-Item-100" - create_item( - item_code, is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 - ) + create_item(item_code, is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0) sr = create_stock_reconciliation(item_code=item_code, qty=10, rate=420) @@ -579,18 +577,18 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): item_code = self.make_item().name warehouse = "_Test Warehouse - _TC" - sr = create_stock_reconciliation( + create_stock_reconciliation( item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10) ) - dn = create_delivery_note( + create_delivery_note( item_code=item_code, warehouse=warehouse, qty=5, rate=120, posting_date=add_days(nowdate(), 12) ) old_bin_qty = frappe.db.get_value( "Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty" ) - sr2 = create_stock_reconciliation( + create_stock_reconciliation( item_code=item_code, warehouse=warehouse, qty=11, rate=100, posting_date=add_days(nowdate(), 11) ) new_bin_qty = frappe.db.get_value( @@ -830,7 +828,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): warehouse = "_Test Warehouse - _TC" # Stock Value => 100 * 100 = 10000 - se = make_stock_entry( + make_stock_entry( item_code=item_code, target=warehouse, qty=100, @@ -1055,6 +1053,72 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(sr.items[0].current_qty, se2.items[0].qty) self.assertEqual(len(sr.items[0].current_serial_no.split("\n")), sr.items[0].current_qty) + def test_backdated_purchase_receipt_with_stock_reco(self): + item_code = self.make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "TEST-SERIAL-.###", + } + ).name + + warehouse = "_Test Warehouse - _TC" + + # Step - 1: Create a Backdated Purchase Receipt + + pr1 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) + ) + pr1.reload() + + serial_nos = sorted(get_serial_nos(pr1.items[0].serial_no))[:5] + + # Step - 2: Create a Stock Reconciliation + sr1 = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=5, + serial_no="\n".join(serial_nos), + ) + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["serial_no", "actual_qty", "stock_value_difference"], + filters={"voucher_no": sr1.name, "is_cancelled": 0}, + order_by="creation", + ) + + for d in data: + if d.actual_qty < 0: + self.assertEqual(d.actual_qty, -10.0) + self.assertAlmostEqual(d.stock_value_difference, -1000.0) + else: + self.assertEqual(d.actual_qty, 5.0) + self.assertAlmostEqual(d.stock_value_difference, 500.0) + + # Step - 3: Create a Purchase Receipt before the first Purchase Receipt + make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5) + ) + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["serial_no", "actual_qty", "stock_value_difference"], + filters={"voucher_no": sr1.name, "is_cancelled": 0}, + order_by="creation", + ) + + for d in data: + if d.actual_qty < 0: + self.assertEqual(d.actual_qty, -20.0) + self.assertAlmostEqual(d.stock_value_difference, -3000.0) + else: + self.assertEqual(d.actual_qty, 5.0) + self.assertAlmostEqual(d.stock_value_difference, 500.0) + + active_serial_no = frappe.get_all("Serial No", filters={"status": "Active", "item_code": item_code}) + self.assertEqual(len(active_serial_no), 5) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) @@ -1145,9 +1209,7 @@ def create_stock_reconciliation(**args): ) ) if frappe.get_all("Stock Ledger Entry", {"company": sr.company}) - else frappe.get_cached_value( - "Account", {"account_type": "Temporary", "company": sr.company}, "name" - ) + else frappe.get_cached_value("Account", {"account_type": "Temporary", "company": sr.company}, "name") ) sr.cost_center = ( args.cost_center diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index a04309ad48e..e3cd34725b8 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -10,8 +10,9 @@ "has_item_scanned", "item_code", "item_name", - "warehouse", + "item_group", "column_break_6", + "warehouse", "qty", "valuation_rate", "amount", @@ -49,6 +50,7 @@ "reqd": 1 }, { + "fetch_from": "item_code.item_name", "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, @@ -186,11 +188,18 @@ "fieldtype": "Data", "label": "Has Item Scanned", "read_only": 1 + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" } ], "istable": 1, "links": [], - "modified": "2023-07-25 11:58:44.992419", + "modified": "2024-01-14 10:04:23.599951", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", @@ -201,4 +210,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js index 5f81679bade..e8c14372416 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js @@ -1,23 +1,23 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Stock Reposting Settings', { - refresh: function(frm) { - frm.trigger('convert_to_item_based_reposting'); +frappe.ui.form.on("Stock Reposting Settings", { + refresh: function (frm) { + frm.trigger("convert_to_item_based_reposting"); }, - convert_to_item_based_reposting: function(frm) { - frm.add_custom_button(__('Convert to Item Based Reposting'), function() { + convert_to_item_based_reposting: function (frm) { + frm.add_custom_button(__("Convert to Item Based Reposting"), function () { frm.call({ - method: 'convert_to_item_wh_reposting', + method: "convert_to_item_wh_reposting", frezz: true, doc: frm.doc, - callback: function(r) { + callback: function (r) { if (!r.exc) { frm.reload_doc(); } - } - }) - }) - } + }, + }); + }); + }, }); diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 89ac4b5fc90..1972b193732 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -1,29 +1,31 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Stock Settings', { - refresh: function(frm) { - let filters = function() { +frappe.ui.form.on("Stock Settings", { + refresh: function (frm) { + let filters = function () { return { - filters : { - is_group : 0 - } + filters: { + is_group: 0, + }, }; }; frm.set_query("default_warehouse", filters); frm.set_query("sample_retention_warehouse", filters); }, - allow_negative_stock: function(frm) { + allow_negative_stock: function (frm) { if (!frm.doc.allow_negative_stock) { return; } - let msg = __("Using negative stock disables FIFO/Moving average valuation when inventory is negative."); + let msg = __( + "Using negative stock disables FIFO/Moving average valuation when inventory is negative." + ); msg += " "; - msg += __("This is considered dangerous from accounting point of view.") + msg += __("This is considered dangerous from accounting point of view."); msg += "
              "; - msg += ("Do you still want to enable negative inventory?"); + msg += "Do you still want to enable negative inventory?"; frappe.confirm( msg, @@ -32,5 +34,5 @@ frappe.ui.form.on('Stock Settings', { frm.set_value("allow_negative_stock", 0); } ); - } + }, }); diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 305459116b5..963b0650fae 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -61,9 +61,9 @@ class StockSettings(Document): for field in warehouse_fields: if frappe.db.get_value("Warehouse", self.get(field), "is_group"): frappe.throw( - _("Group Warehouses cannot be used in transactions. Please change the value of {0}").format( - frappe.bold(self.meta.get_field(field).label) - ), + _( + "Group Warehouses cannot be used in transactions. Please change the value of {0}" + ).format(frappe.bold(self.meta.get_field(field).label)), title=_("Incorrect Warehouse"), ) diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.py b/erpnext/stock/doctype/stock_settings/test_stock_settings.py index 974e16339b7..b9e9f75fced 100644 --- a/erpnext/stock/doctype/stock_settings/test_stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/test_stock_settings.py @@ -1,7 +1,6 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe from frappe.tests.utils import FrappeTestCase diff --git a/erpnext/stock/doctype/uom_category/uom_category.js b/erpnext/stock/doctype/uom_category/uom_category.js index 48026da1bed..b53190f7482 100644 --- a/erpnext/stock/doctype/uom_category/uom_category.js +++ b/erpnext/stock/doctype/uom_category/uom_category.js @@ -1,8 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('UOM Category', { - refresh: function() { - - } +frappe.ui.form.on("UOM Category", { + refresh: function () {}, }); diff --git a/erpnext/stock/doctype/variant_field/variant_field.js b/erpnext/stock/doctype/variant_field/variant_field.js index 13db3f9272d..8455d912531 100644 --- a/erpnext/stock/doctype/variant_field/variant_field.js +++ b/erpnext/stock/doctype/variant_field/variant_field.js @@ -1,8 +1,6 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Variant Field', { - refresh: function() { - - } +frappe.ui.form.on("Variant Field", { + refresh: function () {}, }); diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 5a7228a5068..02d64cadfe6 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -58,7 +58,7 @@ class TestWarehouse(FrappeTestCase): warehouse_ids.append(warehouse_id) item_names = [f"_Test Item {i} for Unlinking" for i in range(2)] - for item, warehouse in zip(item_names, warehouse_ids): + for item, warehouse in zip(item_names, warehouse_ids, strict=False): create_item(item, warehouse=warehouse, company=company) # Delete warehouses @@ -78,7 +78,6 @@ class TestWarehouse(FrappeTestCase): ) def test_group_non_group_conversion(self): - warehouse = frappe.get_doc("Warehouse", create_warehouse("TestGroupConversion")) convert_to_group_or_ledger(warehouse.name) diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 746a1cbaf17..9457c25b22d 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -35,23 +35,21 @@ frappe.ui.form.on("Warehouse", { refresh: function (frm) { frm.toggle_display("warehouse_name", frm.doc.__islocal); - frm.toggle_display( - ["address_html", "contact_html"], - !frm.doc.__islocal - ); + frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal); if (!frm.is_new()) { frappe.contacts.render_address_and_contact(frm); let enable_toggle = frm.doc.disabled ? "Enable" : "Disable"; frm.add_custom_button(__(enable_toggle), () => { - frm.set_value('disabled', 1 - frm.doc.disabled); - frm.save() + frm.set_value("disabled", 1 - frm.doc.disabled); + frm.save(); }); frm.add_custom_button(__("Stock Balance"), function () { frappe.set_route("query-report", "Stock Balance", { warehouse: frm.doc.name, + company: frm.doc.company, }); }); @@ -61,25 +59,20 @@ frappe.ui.form.on("Warehouse", { : __("Convert to Group", null, "Warehouse"), function () { convert_to_group_or_ledger(frm); - }, + } ); - } else { frappe.contacts.clear_address_and_contact(frm); } - if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) { - frm.add_custom_button( - __("General Ledger", null, "Warehouse"), - function () { - frappe.route_options = { - account: frm.doc.__onload.account, - company: frm.doc.company, - }; - frappe.set_route("query-report", "General Ledger"); - } - ); + frm.add_custom_button(__("General Ledger", null, "Warehouse"), function () { + frappe.route_options = { + account: frm.doc.__onload.account, + company: frm.doc.company, + }; + frappe.set_route("query-report", "General Ledger"); + }); } frm.toggle_enable(["is_group", "company"], false); diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 43b2ad2a69b..7b0cade3ca4 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -13,6 +13,7 @@ "column_break_3", "is_group", "parent_warehouse", + "is_rejected_warehouse", "column_break_4", "account", "company", @@ -249,13 +250,20 @@ { "fieldname": "column_break_qajx", "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If yes, then this warehouse will be used to store rejected materials", + "fieldname": "is_rejected_warehouse", + "fieldtype": "Check", + "label": "Is Rejected Warehouse" } ], "icon": "fa fa-building", "idx": 1, "is_tree": 1, "links": [], - "modified": "2023-05-29 13:10:43.333160", + "modified": "2024-01-24 16:27:28.299520", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 430a8d17e01..e980c53670d 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -26,9 +26,7 @@ class Warehouse(NestedSet): def onload(self): """load account name for General Ledger Report""" - if self.company and cint( - frappe.db.get_value("Company", self.company, "enable_perpetual_inventory") - ): + if self.company and cint(frappe.db.get_value("Company", self.company, "enable_perpetual_inventory")): account = self.account or get_warehouse_account(self) if account: @@ -190,7 +188,7 @@ def get_child_warehouses(warehouse): from frappe.utils.nestedset import get_descendants_of children = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft") - return children + [warehouse] # append self for backward compatibility + return [*children, warehouse] # append self for backward compatibility def get_warehouses_based_on_account(account, company=None): diff --git a/erpnext/stock/doctype/warehouse/warehouse_tree.js b/erpnext/stock/doctype/warehouse/warehouse_tree.js index eb635e6757d..d9070b33d10 100644 --- a/erpnext/stock/doctype/warehouse/warehouse_tree.js +++ b/erpnext/stock/doctype/warehouse/warehouse_tree.js @@ -1,20 +1,25 @@ -frappe.treeview_settings['Warehouse'] = { +frappe.treeview_settings["Warehouse"] = { get_tree_nodes: "erpnext.stock.doctype.warehouse.warehouse.get_children", add_tree_node: "erpnext.stock.doctype.warehouse.warehouse.add_node", get_tree_root: false, root_label: "Warehouses", - filters: [{ - fieldname: "company", - fieldtype:"Select", - options: erpnext.utils.get_tree_options("company"), - label: __("Company"), - default: erpnext.utils.get_tree_default("company") - }], - fields:[ - {fieldtype:'Data', fieldname: 'warehouse_name', - label:__('New Warehouse Name'), reqd:true}, - {fieldtype:'Check', fieldname:'is_group', label:__('Is Group'), - description: __("Child nodes can be only created under 'Group' type nodes")} + filters: [ + { + fieldname: "company", + fieldtype: "Select", + options: erpnext.utils.get_tree_options("company"), + label: __("Company"), + default: erpnext.utils.get_tree_default("company"), + }, ], - ignore_fields:["parent_warehouse"], -} + fields: [ + { fieldtype: "Data", fieldname: "warehouse_name", label: __("New Warehouse Name"), reqd: true }, + { + fieldtype: "Check", + fieldname: "is_group", + label: __("Is Group"), + description: __("Child nodes can be only created under 'Group' type nodes"), + }, + ], + ignore_fields: ["parent_warehouse"], +}; diff --git a/erpnext/stock/doctype/warehouse_type/warehouse_type.js b/erpnext/stock/doctype/warehouse_type/warehouse_type.js index 4c4b89ce301..521b07a33d7 100644 --- a/erpnext/stock/doctype/warehouse_type/warehouse_type.js +++ b/erpnext/stock/doctype/warehouse_type/warehouse_type.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Warehouse Type', { +frappe.ui.form.on("Warehouse Type", { // refresh: function(frm) { - // } }); diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 10d3ef4b7a9..64715d28ce8 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -87,7 +87,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru get_party_item_code(args, item, out) - set_valuation_rate(out, args) + if args.get("doctype") in ["Sales Order", "Quotation"]: + set_valuation_rate(out, args) update_party_blanket_order(args, out) @@ -103,22 +104,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) - if ( - args.get("doctype") == "Material Request" - and args.get("material_request_type") == "Material Transfer" - ): - out.update(get_bin_details(args.item_code, args.get("from_warehouse"))) - - elif out.get("warehouse"): - if doc and doc.get("doctype") == "Purchase Order": - # calculate company_total_stock only for po - bin_details = get_bin_details( - args.item_code, out.warehouse, args.company, include_child_warehouses=True - ) - else: - bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True) - - out.update(bin_details) + if item.is_stock_item: + update_bin_details(args, out, doc) # update args with out, if key or value not exists for key, value in out.items(): @@ -201,6 +188,19 @@ def set_valuation_rate(out, args): out.update(get_valuation_rate(args.item_code, args.company, out.get("warehouse"))) +def update_bin_details(args, out, doc): + if args.get("doctype") == "Material Request" and args.get("material_request_type") == "Material Transfer": + out.update(get_bin_details(args.item_code, args.get("from_warehouse"))) + + elif out.get("warehouse"): + company = args.company if (doc and doc.get("doctype") == "Purchase Order") else None + + # calculate company_total_stock only for po + bin_details = get_bin_details(args.item_code, out.warehouse, company, include_child_warehouses=True) + + out.update(bin_details) + + def process_args(args): if isinstance(args, str): args = json.loads(args) @@ -303,7 +303,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): if not item: item = frappe.get_doc("Item", args.get("item_code")) - if item.variant_of and not item.taxes: + if item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of}): item.update_template_tables() item_defaults = get_item_defaults(item.name, args.company) @@ -348,9 +348,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): # Set stock UOM in args, so that it can be used while fetching item price args.stock_uom = item.stock_uom - if args.get("batch_no") and item.name != frappe.get_cached_value( - "Batch", args.get("batch_no"), "item" - ): + if args.get("batch_no") and item.name != frappe.get_cached_value("Batch", args.get("batch_no"), "item"): args["batch_no"] = "" out = frappe._dict( @@ -371,9 +369,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "provisional_expense_account": get_provisional_account( args, item_defaults, item_group_defaults, brand_defaults ), - "cost_center": get_default_cost_center( - args, item_defaults, item_group_defaults, brand_defaults - ), + "cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults), "has_serial_no": item.has_serial_no, "has_batch_no": item.has_batch_no, "batch_no": args.get("batch_no"), @@ -399,9 +395,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0, "is_fixed_asset": item.is_fixed_asset, - "last_purchase_rate": item.last_purchase_rate - if args.get("doctype") in ["Purchase Order"] - else 0, + "last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0, "transaction_date": args.get("transaction_date"), "against_blanket_order": args.get("against_blanket_order"), "bom_no": item.get("default_bom"), @@ -530,17 +524,24 @@ def update_barcode_value(out): out["barcode"] = barcode_data.get(out.item_code)[0] -def get_barcode_data(items_list): +def get_barcode_data(items_list=None, item_code=None): # get itemwise batch no data # exmaple: {'LED-GRE': [Batch001, Batch002]} # where LED-GRE is item code, SN0001 is serial no and Pune is warehouse itemwise_barcode = {} - for item in items_list: - barcodes = frappe.db.get_all( - "Item Barcode", filters={"parent": item.item_code}, fields="barcode" + if not items_list and item_code: + _dict_item_code = frappe._dict( + { + "item_code": item_code, + } ) + items_list = [frappe._dict(_dict_item_code)] + + for item in items_list: + barcodes = frappe.db.get_all("Item Barcode", filters={"parent": item.item_code}, fields="barcode") + for barcode in barcodes: if item.item_code not in itemwise_barcode: itemwise_barcode.setdefault(item.item_code, []) @@ -559,13 +560,13 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t if item_rates is None: item_rates = {} - if isinstance(item_codes, (str,)): + if isinstance(item_codes, str): item_codes = json.loads(item_codes) - if isinstance(item_rates, (str,)): + if isinstance(item_rates, str): item_rates = json.loads(item_rates) - if isinstance(item_tax_templates, (str,)): + if isinstance(item_tax_templates, str): item_tax_templates = json.loads(item_tax_templates) for item_code in item_codes: @@ -633,9 +634,7 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity.append(tax) if taxes_with_validity: - taxes = sorted( - taxes_with_validity, key=lambda i: i.valid_from or tax.maximum_net_rate, reverse=True - ) + taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from or tax.maximum_net_rate, reverse=True) else: taxes = taxes_with_no_validity @@ -788,7 +787,7 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan elif not cost_center and args.get("item_code") and company: for method in ["get_item_defaults", "get_item_group_defaults", "get_brand_defaults"]: - path = "erpnext.stock.get_item_details.{0}".format(method) + path = f"erpnext.stock.get_item_details.{method}" data = frappe.get_attr(path)(args.get("item_code"), company) if data and (data.selling_cost_center or data.buying_cost_center): @@ -803,11 +802,7 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan if not cost_center and args.get("cost_center"): cost_center = args.get("cost_center") - if ( - company - and cost_center - and frappe.get_cached_value("Cost Center", cost_center, "company") != company - ): + if company and cost_center and frappe.get_cached_value("Cost Center", cost_center, "company") != company: return None if not cost_center and company: @@ -817,11 +812,7 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan def get_default_supplier(args, item, item_group, brand): - return ( - item.get("default_supplier") - or item_group.get("default_supplier") - or brand.get("default_supplier") - ) + return item.get("default_supplier") or item_group.get("default_supplier") or brand.get("default_supplier") def get_price_list_rate(args, item_doc, out=None): @@ -847,14 +838,16 @@ def get_price_list_rate(args, item_doc, out=None): price_list_rate = get_price_list_rate_for(args, item_doc.variant_of) # insert in database - if price_list_rate is None: + if price_list_rate is None or frappe.db.get_single_value( + "Stock Settings", "update_existing_price_list_rate" + ): if args.price_list and args.rate: insert_item_price(args) - return out - out.price_list_rate = ( - flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) - ) + if not price_list_rate: + return out + + out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"): return out @@ -869,9 +862,7 @@ def get_price_list_rate(args, item_doc, out=None): def insert_item_price(args): """Insert Item Price if Price List and Price List Rate are specified and currency is the same""" - if frappe.db.get_value( - "Price List", args.price_list, "currency", cache=True - ) == args.currency and cint( + if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint( frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing") ): if frappe.has_permission("Item Price", "write"): @@ -893,7 +884,9 @@ def insert_item_price(args): ): frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate) frappe.msgprint( - _("Item Price updated for {0} in Price List {1}").format(args.item_code, args.price_list), + _("Item Price updated for {0} in Price List {1}").format( + args.item_code, args.price_list + ), alert=True, ) else: @@ -1033,11 +1026,7 @@ def validate_conversion_rate(args, meta): if not args.conversion_rate and args.currency == company_currency: args.conversion_rate = 1.0 - if ( - not args.ignore_conversion_rate - and args.conversion_rate == 1 - and args.currency != company_currency - ): + if not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency != company_currency: args.conversion_rate = ( get_exchange_rate(args.currency, company_currency, args.transaction_date, "for_buying") or 1.0 ) @@ -1072,7 +1061,9 @@ def validate_conversion_rate(args, meta): if meta.get_field("plc_conversion_rate"): args.plc_conversion_rate = flt( args.plc_conversion_rate, - get_field_precision(meta.get_field("plc_conversion_rate"), frappe._dict({"fields": args})), + get_field_precision( + meta.get_field("plc_conversion_rate"), frappe._dict({"fields": args}) + ), ) @@ -1253,9 +1244,7 @@ def get_serial_no_details(item_code, warehouse, stock_qty, serial_no): @frappe.whitelist() -def get_bin_details_and_serial_nos( - item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None -): +def get_bin_details_and_serial_nos(item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None): bin_details_and_serial_nos = {} bin_details_and_serial_nos.update(get_bin_details(item_code, warehouse)) if flt(stock_qty) > 0: @@ -1265,9 +1254,7 @@ def get_bin_details_and_serial_nos( bin_details_and_serial_nos.update({"serial_no": serial_no}) return bin_details_and_serial_nos - bin_details_and_serial_nos.update( - get_serial_no_details(item_code, warehouse, stock_qty, serial_no) - ) + bin_details_and_serial_nos.update(get_serial_no_details(item_code, warehouse, stock_qty, serial_no)) return bin_details_and_serial_nos @@ -1379,9 +1366,7 @@ def get_price_list_currency_and_exchange_rate(args): company_currency = get_company_currency(args.company) if (not plc_conversion_rate) or ( - price_list_currency - and args.price_list_currency - and price_list_currency != args.price_list_currency + price_list_currency and args.price_list_currency and price_list_currency != args.price_list_currency ): # cksgb 19/09/2016: added args.transaction_date as posting_date argument for get_exchange_rate plc_conversion_rate = ( @@ -1403,9 +1388,7 @@ def get_price_list_currency_and_exchange_rate(args): @frappe.whitelist() def get_default_bom(item_code=None): def _get_bom(item): - bom = frappe.get_all( - "BOM", dict(item=item, is_active=True, is_default=True, docstatus=1), limit=1 - ) + bom = frappe.get_all("BOM", dict(item=item, is_active=True, is_default=True, docstatus=1), limit=1) return bom[0].name if bom else None if not item_code: @@ -1445,7 +1428,7 @@ def get_valuation_rate(item_code, company, warehouse=None): pi_item = frappe.qb.DocType("Purchase Invoice Item") valuation_rate = ( frappe.qb.from_(pi_item) - .select((Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor))) + .select(Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor)) .where((pi_item.docstatus == 1) & (pi_item.item_code == item_code)) ).run() diff --git a/erpnext/stock/landed_taxes_and_charges_common.js b/erpnext/stock/landed_taxes_and_charges_common.js index 1d76a3d95a1..48cf773f627 100644 --- a/erpnext/stock/landed_taxes_and_charges_common.js +++ b/erpnext/stock/landed_taxes_and_charges_common.js @@ -1,61 +1,74 @@ -let document_list = ['Landed Cost Voucher', 'Stock Entry', 'Subcontracting Order', 'Subcontracting Receipt']; +let document_list = ["Landed Cost Voucher", "Stock Entry", "Subcontracting Order", "Subcontracting Receipt"]; document_list.forEach((doctype) => { frappe.ui.form.on(doctype, { - refresh: function(frm) { - let tax_field = frm.doc.doctype == 'Landed Cost Voucher' ? 'taxes' : 'additional_costs'; - frm.set_query("expense_account", tax_field, function() { + refresh: function (frm) { + let tax_field = frm.doc.doctype == "Landed Cost Voucher" ? "taxes" : "additional_costs"; + frm.set_query("expense_account", tax_field, function () { return { filters: { - "account_type": ['in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]], - "company": frm.doc.company - } + account_type: [ + "in", + [ + "Tax", + "Chargeable", + "Income Account", + "Expenses Included In Valuation", + "Expenses Included In Asset Valuation", + ], + ], + company: frm.doc.company, + }, }; }); }, - set_account_currency: function(frm, cdt, cdn) { + set_account_currency: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; if (row.expense_account) { - frappe.db.get_value('Account', row.expense_account, 'account_currency', function(value) { + frappe.db.get_value("Account", row.expense_account, "account_currency", function (value) { frappe.model.set_value(cdt, cdn, "account_currency", value.account_currency); frm.events.set_exchange_rate(frm, cdt, cdn); }); } }, - set_exchange_rate: function(frm, cdt, cdn) { + set_exchange_rate: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; if (row.account_currency == company_currency) { row.exchange_rate = 1; - frm.set_df_property('taxes', 'hidden', 1, row.name, 'exchange_rate'); + frm.set_df_property("taxes", "hidden", 1, row.name, "exchange_rate"); } else if (!row.exchange_rate || row.exchange_rate == 1) { - frm.set_df_property('taxes', 'hidden', 0, row.name, 'exchange_rate'); + frm.set_df_property("taxes", "hidden", 0, row.name, "exchange_rate"); frappe.call({ method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_exchange_rate", args: { posting_date: frm.doc.posting_date, account: row.expense_account, account_currency: row.account_currency, - company: frm.doc.company + company: frm.doc.company, }, - callback: function(r) { + callback: function (r) { if (r.message) { frappe.model.set_value(cdt, cdn, "exchange_rate", r.message); } - } + }, }); } - frm.refresh_field('taxes'); + frm.refresh_field("taxes"); }, - set_base_amount: function(frm, cdt, cdn) { + set_base_amount: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; - frappe.model.set_value(cdt, cdn, "base_amount", - flt(flt(row.amount)*row.exchange_rate, precision("base_amount", row))); - } + frappe.model.set_value( + cdt, + cdn, + "base_amount", + flt(flt(row.amount) * row.exchange_rate, precision("base_amount", row)) + ); + }, }); }); diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js index 90b8d453420..a5fba9f98f3 100644 --- a/erpnext/stock/page/stock_balance/stock_balance.js +++ b/erpnext/stock/page/stock_balance/stock_balance.js @@ -1,103 +1,101 @@ -frappe.pages['stock-balance'].on_page_load = function(wrapper) { +frappe.pages["stock-balance"].on_page_load = function (wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, - title: __('Stock Summary'), - single_column: true + title: __("Stock Summary"), + single_column: true, }); page.start = 0; page.warehouse_field = page.add_field({ - fieldname: 'warehouse', - label: __('Warehouse'), - fieldtype:'Link', - options:'Warehouse', + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", default: frappe.route_options && frappe.route_options.warehouse, - change: function() { + change: function () { page.item_dashboard.start = 0; page.item_dashboard.refresh(); - } + }, }); page.item_field = page.add_field({ - fieldname: 'item_code', - label: __('Item'), - fieldtype:'Link', - options:'Item', + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", default: frappe.route_options && frappe.route_options.item_code, - change: function() { + change: function () { page.item_dashboard.start = 0; page.item_dashboard.refresh(); - } + }, }); page.item_group_field = page.add_field({ - fieldname: 'item_group', - label: __('Item Group'), - fieldtype:'Link', - options:'Item Group', + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", default: frappe.route_options && frappe.route_options.item_group, - change: function() { + change: function () { page.item_dashboard.start = 0; page.item_dashboard.refresh(); - } + }, }); page.sort_selector = new frappe.ui.SortSelector({ - parent: page.wrapper.find('.page-form'), + parent: page.wrapper.find(".page-form"), args: { - sort_by: 'projected_qty', - sort_order: 'asc', + sort_by: "projected_qty", + sort_order: "asc", options: [ - {fieldname: 'projected_qty', label: __('Projected qty')}, - {fieldname: 'reserved_qty', label: __('Reserved for sale')}, - {fieldname: 'reserved_qty_for_production', label: __('Reserved for manufacturing')}, - {fieldname: 'reserved_qty_for_sub_contract', label: __('Reserved for sub contracting')}, - {fieldname: 'actual_qty', label: __('Actual qty in stock')}, - ] + { fieldname: "projected_qty", label: __("Projected qty") }, + { fieldname: "reserved_qty", label: __("Reserved for sale") }, + { fieldname: "reserved_qty_for_production", label: __("Reserved for manufacturing") }, + { fieldname: "reserved_qty_for_sub_contract", label: __("Reserved for sub contracting") }, + { fieldname: "actual_qty", label: __("Actual qty in stock") }, + ], }, - change: function(sort_by, sort_order) { + change: function (sort_by, sort_order) { page.item_dashboard.sort_by = sort_by; page.item_dashboard.sort_order = sort_order; page.item_dashboard.start = 0; page.item_dashboard.refresh(); - } + }, }); // page.sort_selector.wrapper.css({'margin-right': '15px', 'margin-top': '4px'}); - frappe.require('item-dashboard.bundle.js', function() { + frappe.require("item-dashboard.bundle.js", function () { page.item_dashboard = new erpnext.stock.ItemDashboard({ parent: page.main, page_length: 20, - method: 'erpnext.stock.dashboard.item_dashboard.get_data', - template: 'item_dashboard_list' - }) + method: "erpnext.stock.dashboard.item_dashboard.get_data", + template: "item_dashboard_list", + }); - page.item_dashboard.before_refresh = function() { + page.item_dashboard.before_refresh = function () { this.item_code = page.item_field.get_value(); this.warehouse = page.warehouse_field.get_value(); this.item_group = page.item_group_field.get_value(); - } + }; page.item_dashboard.refresh(); // item click - var setup_click = function(doctype) { - page.main.on('click', 'a[data-type="'+ doctype.toLowerCase() +'"]', function() { - var name = $(this).attr('data-name'); - var field = page[doctype.toLowerCase() + '_field']; - if(field.get_value()===name) { - frappe.set_route('Form', doctype, name) + var setup_click = function (doctype) { + page.main.on("click", 'a[data-type="' + doctype.toLowerCase() + '"]', function () { + var name = $(this).attr("data-name"); + var field = page[doctype.toLowerCase() + "_field"]; + if (field.get_value() === name) { + frappe.set_route("Form", doctype, name); } else { field.set_input(name); page.item_dashboard.refresh(); } }); - } + }; - setup_click('Item'); - setup_click('Warehouse'); + setup_click("Item"); + setup_click("Warehouse"); }); - - -} +}; diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js index 61927f51a82..f03c5ca65e8 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -1,98 +1,98 @@ -frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { +frappe.pages["warehouse-capacity-summary"].on_page_load = function (wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, - title: 'Warehouse Capacity Summary', - single_column: true + title: "Warehouse Capacity Summary", + single_column: true, }); - page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'refresh'); + page.set_secondary_action("Refresh", () => page.capacity_dashboard.refresh(), "refresh"); page.start = 0; page.company_field = page.add_field({ - fieldname: 'company', - label: __('Company'), - fieldtype: 'Link', - options: 'Company', + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", reqd: 1, default: frappe.defaults.get_default("company"), - change: function() { + change: function () { page.capacity_dashboard.start = 0; page.capacity_dashboard.refresh(); - } + }, }); page.warehouse_field = page.add_field({ - fieldname: 'warehouse', - label: __('Warehouse'), - fieldtype: 'Link', - options: 'Warehouse', - change: function() { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + change: function () { page.capacity_dashboard.start = 0; page.capacity_dashboard.refresh(); - } + }, }); page.item_field = page.add_field({ - fieldname: 'item_code', - label: __('Item'), - fieldtype: 'Link', - options: 'Item', - change: function() { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + change: function () { page.capacity_dashboard.start = 0; page.capacity_dashboard.refresh(); - } + }, }); page.parent_warehouse_field = page.add_field({ - fieldname: 'parent_warehouse', - label: __('Parent Warehouse'), - fieldtype: 'Link', - options: 'Warehouse', - get_query: function() { + fieldname: "parent_warehouse", + label: __("Parent Warehouse"), + fieldtype: "Link", + options: "Warehouse", + get_query: function () { return { filters: { - "is_group": 1 - } + is_group: 1, + }, }; }, - change: function() { + change: function () { page.capacity_dashboard.start = 0; page.capacity_dashboard.refresh(); - } + }, }); page.sort_selector = new frappe.ui.SortSelector({ - parent: page.wrapper.find('.page-form'), + parent: page.wrapper.find(".page-form"), args: { - sort_by: 'stock_capacity', - sort_order: 'desc', + sort_by: "stock_capacity", + sort_order: "desc", options: [ - {fieldname: 'stock_capacity', label: __('Capacity (Stock UOM)')}, - {fieldname: 'percent_occupied', label: __('% Occupied')}, - {fieldname: 'actual_qty', label: __('Balance Qty (Stock)')} - ] + { fieldname: "stock_capacity", label: __("Capacity (Stock UOM)") }, + { fieldname: "percent_occupied", label: __("% Occupied") }, + { fieldname: "actual_qty", label: __("Balance Qty (Stock)") }, + ], }, - change: function(sort_by, sort_order) { + change: function (sort_by, sort_order) { page.capacity_dashboard.sort_by = sort_by; page.capacity_dashboard.sort_order = sort_order; page.capacity_dashboard.start = 0; page.capacity_dashboard.refresh(); - } + }, }); - frappe.require('item-dashboard.bundle.js', function() { - $(frappe.render_template('warehouse_capacity_summary_header')).appendTo(page.main); + frappe.require("item-dashboard.bundle.js", function () { + $(frappe.render_template("warehouse_capacity_summary_header")).appendTo(page.main); page.capacity_dashboard = new erpnext.stock.ItemDashboard({ page_name: "warehouse-capacity-summary", page_length: 10, parent: page.main, - sort_by: 'stock_capacity', - sort_order: 'desc', - method: 'erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data', - template: 'warehouse_capacity_summary' + sort_by: "stock_capacity", + sort_order: "desc", + method: "erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data", + template: "warehouse_capacity_summary", }); - page.capacity_dashboard.before_refresh = function() { + page.capacity_dashboard.before_refresh = function () { this.item_code = page.item_field.get_value(); this.warehouse = page.warehouse_field.get_value(); this.parent_warehouse = page.parent_warehouse_field.get_value(); @@ -101,12 +101,12 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.capacity_dashboard.refresh(); - let setup_click = function(doctype) { - page.main.on('click', 'a[data-type="'+ doctype.toLowerCase() +'"]', function() { - var name = $(this).attr('data-name'); - var field = page[doctype.toLowerCase() + '_field']; - if (field.get_value()===name) { - frappe.set_route('Form', doctype, name); + let setup_click = function (doctype) { + page.main.on("click", 'a[data-type="' + doctype.toLowerCase() + '"]', function () { + var name = $(this).attr("data-name"); + var field = page[doctype.toLowerCase() + "_field"]; + if (field.get_value() === name) { + frappe.set_route("Form", doctype, name); } else { field.set_input(name); page.capacity_dashboard.refresh(); @@ -114,7 +114,7 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { }); }; - setup_click('Item'); - setup_click('Warehouse'); + setup_click("Item"); + setup_click("Warehouse"); }); }; diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 136c78ff586..c859506ce8a 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -34,83 +34,164 @@ def _reorder_item(): erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0] ) - items_to_consider = frappe.db.sql_list( - """select name from `tabItem` item - where is_stock_item=1 and has_variants=0 - and disabled=0 - and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s) - and (exists (select name from `tabItem Reorder` ir where ir.parent=item.name) - or (variant_of is not null and variant_of != '' - and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of)) - )""", - {"today": nowdate()}, - ) + items_to_consider = get_items_for_reorder() if not items_to_consider: return item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider) - def add_to_material_request( - item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None - ): - if warehouse not in warehouse_company: + def add_to_material_request(**kwargs): + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + if kwargs.warehouse not in warehouse_company: # a disabled warehouse return - reorder_level = flt(reorder_level) - reorder_qty = flt(reorder_qty) + reorder_level = flt(kwargs.reorder_level) + reorder_qty = flt(kwargs.reorder_qty) # projected_qty will be 0 if Bin does not exist - if warehouse_group: - projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse_group)) + if kwargs.warehouse_group: + projected_qty = flt( + item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group) + ) else: - projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse)) + projected_qty = flt(item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse)) if (reorder_level or reorder_qty) and projected_qty < reorder_level: deficiency = reorder_level - projected_qty if deficiency > reorder_qty: reorder_qty = deficiency - company = warehouse_company.get(warehouse) or default_company + company = warehouse_company.get(kwargs.warehouse) or default_company - material_requests[material_request_type].setdefault(company, []).append( - {"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty} + material_requests[kwargs.material_request_type].setdefault(company, []).append( + { + "item_code": kwargs.item_code, + "warehouse": kwargs.warehouse, + "reorder_qty": reorder_qty, + "item_details": kwargs.item_details, + } ) - for item_code in items_to_consider: - item = frappe.get_doc("Item", item_code) + for item_code, reorder_levels in items_to_consider.items(): + for d in reorder_levels: + if d.has_variants: + continue - if item.variant_of and not item.get("reorder_levels"): - item.update_template_tables() - - if item.get("reorder_levels"): - for d in item.get("reorder_levels"): - add_to_material_request( - item_code, - d.warehouse, - d.warehouse_reorder_level, - d.warehouse_reorder_qty, - d.material_request_type, - warehouse_group=d.warehouse_group, - ) + add_to_material_request( + item_code=item_code, + warehouse=d.warehouse, + reorder_level=d.warehouse_reorder_level, + reorder_qty=d.warehouse_reorder_qty, + material_request_type=d.material_request_type, + warehouse_group=d.warehouse_group, + item_details=frappe._dict( + { + "item_code": item_code, + "name": item_code, + "item_name": d.item_name, + "item_group": d.item_group, + "brand": d.brand, + "description": d.description, + "stock_uom": d.stock_uom, + "purchase_uom": d.purchase_uom, + } + ), + ) if material_requests: return create_material_request(material_requests) +def get_items_for_reorder() -> dict[str, list]: + reorder_table = frappe.qb.DocType("Item Reorder") + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(reorder_table) + .inner_join(item_table) + .on(reorder_table.parent == item_table.name) + .select( + reorder_table.warehouse, + reorder_table.warehouse_group, + reorder_table.material_request_type, + reorder_table.warehouse_reorder_level, + reorder_table.warehouse_reorder_qty, + item_table.name, + item_table.stock_uom, + item_table.purchase_uom, + item_table.description, + item_table.item_name, + item_table.item_group, + item_table.brand, + item_table.variant_of, + item_table.has_variants, + ) + .where( + (item_table.disabled == 0) + & (item_table.is_stock_item == 1) + & ( + (item_table.end_of_life.isnull()) + | (item_table.end_of_life > nowdate()) + | (item_table.end_of_life == "0000-00-00") + ) + ) + ) + + data = query.run(as_dict=True) + itemwise_reorder = frappe._dict({}) + for d in data: + itemwise_reorder.setdefault(d.name, []).append(d) + + itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder) + + return itemwise_reorder + + +def get_reorder_levels_for_variants(itemwise_reorder): + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(item_table) + .select( + item_table.name, + item_table.variant_of, + ) + .where( + (item_table.disabled == 0) + & (item_table.is_stock_item == 1) + & ( + (item_table.end_of_life.isnull()) + | (item_table.end_of_life > nowdate()) + | (item_table.end_of_life == "0000-00-00") + ) + & (item_table.variant_of.notnull()) + ) + ) + + variants_item = query.run(as_dict=True) + for row in variants_item: + if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of): + itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, [])) + + return itemwise_reorder + + def get_item_warehouse_projected_qty(items_to_consider): item_warehouse_projected_qty = {} + items_to_consider = list(items_to_consider.keys()) for item_code, warehouse, projected_qty in frappe.db.sql( """select item_code, warehouse, projected_qty - from tabBin where item_code in ({0}) + from tabBin where item_code in ({}) and (warehouse != '' and warehouse is not null)""".format( ", ".join(["%s"] * len(items_to_consider)) ), items_to_consider, ): - if item_code not in item_warehouse_projected_qty: item_warehouse_projected_qty.setdefault(item_code, {}) @@ -141,10 +222,11 @@ def create_material_request(material_requests): exceptions_list.extend(frappe.local.message_log) frappe.local.message_log = [] else: - exceptions_list.append(frappe.get_traceback()) + exceptions_list.append(frappe.get_traceback(with_context=True)) mr.log_error("Unable to create material request") + company_wise_mr = frappe._dict({}) for request_type in material_requests: for company in material_requests[request_type]: try: @@ -157,13 +239,15 @@ def create_material_request(material_requests): { "company": company, "transaction_date": nowdate(), - "material_request_type": "Material Transfer" if request_type == "Transfer" else request_type, + "material_request_type": "Material Transfer" + if request_type == "Transfer" + else request_type, } ) for d in items: d = frappe._dict(d) - item = frappe.get_doc("Item", d.item_code) + item = d.get("item_details") uom = item.stock_uom conversion_factor = 1.0 @@ -172,7 +256,9 @@ def create_material_request(material_requests): if uom != item.stock_uom: conversion_factor = ( frappe.db.get_value( - "UOM Conversion Detail", {"parent": item.name, "uom": uom}, "conversion_factor" + "UOM Conversion Detail", + {"parent": item.name, "uom": uom}, + "conversion_factor", ) or 1.0 ) @@ -189,6 +275,7 @@ def create_material_request(material_requests): "item_code": d.item_code, "schedule_date": add_days(nowdate(), cint(item.lead_time_days)), "qty": qty, + "conversion_factor": conversion_factor, "uom": uom, "stock_uom": item.stock_uom, "warehouse": d.warehouse, @@ -206,17 +293,19 @@ def create_material_request(material_requests): mr.submit() mr_list.append(mr) + company_wise_mr.setdefault(company, []).append(mr) + except Exception: _log_exception(mr) - if mr_list: + if company_wise_mr: if getattr(frappe.local, "reorder_email_notify", None) is None: frappe.local.reorder_email_notify = cint( frappe.db.get_value("Stock Settings", None, "reorder_email_notify") ) if frappe.local.reorder_email_notify: - send_email_notification(mr_list) + send_email_notification(company_wise_mr) if exceptions_list: notify_errors(exceptions_list) @@ -224,20 +313,54 @@ def create_material_request(material_requests): return mr_list -def send_email_notification(mr_list): +def send_email_notification(company_wise_mr): """Notify user about auto creation of indent""" - email_list = frappe.db.sql_list( - """select distinct r.parent - from `tabHas Role` r, tabUser p - where p.name = r.parent and p.enabled = 1 and p.docstatus < 2 - and r.role in ('Purchase Manager','Stock Manager') - and p.name not in ('Administrator', 'All', 'Guest')""" + for company, mr_list in company_wise_mr.items(): + email_list = get_email_list(company) + + if not email_list: + continue + + msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list}) + + frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg) + + +def get_email_list(company): + users = get_comapny_wise_users(company) + user_table = frappe.qb.DocType("User") + role_table = frappe.qb.DocType("Has Role") + + query = ( + frappe.qb.from_(user_table) + .inner_join(role_table) + .on(user_table.name == role_table.parent) + .select(user_table.email) + .where( + (role_table.role.isin(["Purchase Manager", "Stock Manager"])) + & (user_table.name.notin(["Administrator", "All", "Guest"])) + & (user_table.enabled == 1) + & (user_table.docstatus < 2) + ) ) - msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list}) + if users: + query = query.where(user_table.name.isin(users)) - frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg) + emails = query.run(as_dict=True) + + return list(set([email.email for email in emails])) + + +def get_comapny_wise_users(company): + users = frappe.get_all( + "User Permission", + filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1}, + fields=["user"], + ) + + return [user.user for user in users] def notify_errors(exceptions_list): @@ -254,7 +377,7 @@ def notify_errors(exceptions_list): for exception in exceptions_list: try: exception = json.loads(exception) - error_message = """
              {0}

              """.format( + error_message = """
              {}

              """.format( _(exception.get("message")) ) content += error_message diff --git a/erpnext/stock/report/available_batch_report/__init__.py b/erpnext/stock/report/available_batch_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.js b/erpnext/stock/report/available_batch_report/available_batch_report.js new file mode 100644 index 00000000000..011f7e09ca2 --- /dev/null +++ b/erpnext/stock/report/available_batch_report/available_batch_report.js @@ -0,0 +1,91 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Available Batch Report"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + width: "80", + options: "Company", + default: frappe.defaults.get_default("company"), + }, + { + fieldname: "to_date", + label: __("On This Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + width: "80", + options: "Item", + get_query: () => { + return { + filters: { + has_batch_no: 1, + disabled: 0, + }, + }; + }, + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + width: "80", + options: "Warehouse", + get_query: () => { + let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); + let company = frappe.query_report.get_filter_value("company"); + + return { + filters: { + ...(warehouse_type && { warehouse_type }), + ...(company && { company }), + }, + }; + }, + }, + { + fieldname: "warehouse_type", + label: __("Warehouse Type"), + fieldtype: "Link", + width: "80", + options: "Warehouse Type", + }, + { + fieldname: "batch_no", + label: __("Batch No"), + fieldtype: "Link", + width: "80", + options: "Batch", + get_query: () => { + let item = frappe.query_report.get_filter_value("item_code"); + + return { + filters: { + ...(item && { item }), + }, + }; + }, + }, + { + fieldname: "include_expired_batches", + label: __("Include Expired Batches"), + fieldtype: "Check", + width: "80", + }, + { + fieldname: "show_item_name", + label: __("Show Item Name"), + fieldtype: "Check", + width: "80", + }, + ], +}; diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.json b/erpnext/stock/report/available_batch_report/available_batch_report.json new file mode 100644 index 00000000000..0125a96fe70 --- /dev/null +++ b/erpnext/stock/report/available_batch_report/available_batch_report.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2024-04-11 17:03:32.253275", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "letter_head": "", + "modified": "2024-04-23 17:09:54.595566", + "modified_by": "Administrator", + "module": "Stock", + "name": "Available Batch Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Available Batch Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.py b/erpnext/stock/report/available_batch_report/available_batch_report.py new file mode 100644 index 00000000000..cb651dd7972 --- /dev/null +++ b/erpnext/stock/report/available_batch_report/available_batch_report.py @@ -0,0 +1,144 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from collections import defaultdict + +import frappe +from frappe import _ +from frappe.query_builder.functions import Sum +from frappe.utils import flt, today + + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + return columns, data + + +def get_columns(filters): + columns = [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 200, + } + ] + + if filters.show_item_name: + columns.append( + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Link", + "options": "Item", + "width": 200, + } + ) + + columns.extend( + [ + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 200, + }, + { + "label": _("Batch No"), + "fieldname": "batch_no", + "fieldtype": "Link", + "width": 150, + "options": "Batch", + }, + {"label": _("Balance Qty"), "fieldname": "balance_qty", "fieldtype": "Float", "width": 150}, + ] + ) + + return columns + + +def get_data(filters): + data = [] + batchwise_data = get_batchwise_data_from_stock_ledger(filters) + + data = parse_batchwise_data(batchwise_data) + + return data + + +def parse_batchwise_data(batchwise_data): + data = [] + for key in batchwise_data: + d = batchwise_data[key] + if d.balance_qty == 0: + continue + + data.append(d) + + return data + + +def get_batchwise_data_from_stock_ledger(filters): + batchwise_data = frappe._dict({}) + + table = frappe.qb.DocType("Stock Ledger Entry") + batch = frappe.qb.DocType("Batch") + + query = ( + frappe.qb.from_(table) + .inner_join(batch) + .on(table.batch_no == batch.name) + .select( + table.item_code, + table.batch_no, + table.warehouse, + Sum(table.actual_qty).as_("balance_qty"), + ) + .where(table.is_cancelled == 0) + .groupby(table.batch_no, table.item_code, table.warehouse) + ) + + query = get_query_based_on_filters(query, batch, table, filters) + + for d in query.run(as_dict=True): + key = (d.item_code, d.warehouse, d.batch_no) + batchwise_data.setdefault(key, d) + + return batchwise_data + + +def get_query_based_on_filters(query, batch, table, filters): + if filters.item_code: + query = query.where(table.item_code == filters.item_code) + + if filters.batch_no: + query = query.where(batch.name == filters.batch_no) + + if not filters.include_expired_batches: + query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull())) + if filters.to_date == today(): + query = query.where(batch.batch_qty > 0) + + if filters.warehouse: + lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"]) + warehouses = frappe.get_all( + "Warehouse", filters={"lft": (">=", lft), "rgt": ("<=", rgt), "is_group": 0}, pluck="name" + ) + + query = query.where(table.warehouse.isin(warehouses)) + + elif filters.warehouse_type: + warehouses = frappe.get_all( + "Warehouse", filters={"warehouse_type": filters.warehouse_type, "is_group": 0}, pluck="name" + ) + + query = query.where(table.warehouse.isin(warehouses)) + + if filters.show_item_name: + query = query.select(batch.item_name) + + return query 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 48a72a2bfe5..a15b69b112a 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 @@ -2,34 +2,34 @@ // For license information, please see license.txt frappe.query_reports["Batch Item Expiry Status"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "width": "80", - "default": frappe.sys_defaults.year_start_date, - "reqd": 1, + fieldname: "from_date", + label: __("From Date"), + 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(), - "reqd": 1, + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + width: "80", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"item", - "label": __("Item"), - "fieldtype": "Link", - "options": "Item", - "width": "100", - "get_query": function () { + fieldname: "item", + label: __("Item"), + fieldtype: "Link", + options: "Item", + width: "100", + get_query: function () { return { - filters: {"has_batch_no": 1} - } - } - } - ] -} + 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 5661e8b2609..3820b3e1566 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 @@ -28,15 +28,15 @@ def validate_filters(filters): 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"] - ) + 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_data(filters): @@ -76,9 +76,7 @@ def get_batch_details(filters): .where( (batch.disabled == 0) & (batch.batch_qty > 0) - & ( - (Date(batch.creation) >= filters["from_date"]) & (Date(batch.creation) <= filters["to_date"]) - ) + & ((Date(batch.creation) >= filters["from_date"]) & (Date(batch.creation) <= filters["to_date"])) ) .orderby(batch.creation) ) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js index 74b5a5ae36e..401ebe43028 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js @@ -2,87 +2,100 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Batch-Wise Balance History"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "width": "80", - "default": frappe.sys_defaults.year_start_date, - "reqd": 1 + fieldname: "from_date", + label: __("From Date"), + 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(), - "reqd": 1 + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + width: "80", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"item_code", - "label": __("Item Code"), - "fieldtype": "Link", - "options": "Item", - "get_query": function() { + fieldname: "item_code", + label: __("Item Code"), + fieldtype: "Link", + options: "Item", + get_query: function () { return { filters: { - "has_batch_no": 1 - } + has_batch_no: 1, + }, }; - } + }, }, { - "fieldname":"warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse", - "get_query": function() { - let company = frappe.query_report.get_filter_value('company'); - return { - filters: { - "company": company - } - }; - } + fieldname: "warehouse_type", + label: __("Warehouse Type"), + fieldtype: "Link", + width: "80", + options: "Warehouse Type", }, { - "fieldname":"batch_no", - "label": __("Batch No"), - "fieldtype": "Link", - "options": "Batch", - "get_query": function() { - let item_code = frappe.query_report.get_filter_value('item_code'); + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + get_query: function () { + let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); + const company = frappe.query_report.get_filter_value("company"); + return { filters: { - "item": item_code - } + ...(warehouse_type && { warehouse_type }), + ...(company && { company }), + }, }; - } + }, + }, + { + fieldname: "batch_no", + label: __("Batch No"), + fieldtype: "Link", + options: "Batch", + get_query: function () { + let item_code = frappe.query_report.get_filter_value("item_code"); + return { + filters: { + item: item_code, + }, + }; + }, }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { if (column.fieldname == "Batch" && data && !!data["Batch"]) { value = data["Batch"]; - column.link_onclick = "frappe.query_reports['Batch-Wise Balance History'].set_batch_route_to_stock_ledger(" + JSON.stringify(data) + ")"; + column.link_onclick = + "frappe.query_reports['Batch-Wise Balance History'].set_batch_route_to_stock_ledger(" + + JSON.stringify(data) + + ")"; } value = default_formatter(value, row, column, data); return value; }, - "set_batch_route_to_stock_ledger": function (data) { + set_batch_route_to_stock_ledger: function (data) { frappe.route_options = { - "batch_no": data["Batch"] + batch_no: data["Batch"], }; frappe.set_route("query-report", "Stock Ledger"); - } -} + }, +}; 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 b38dba8bb17..c8c26fd66cb 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 @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.utils import cint, flt, getdate +from frappe.utils import cint, flt, get_table_name, getdate from pypika import functions as fn from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter @@ -12,11 +12,22 @@ from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter SLE_COUNT_LIMIT = 10_000 +def _estimate_table_row_count(doctype: str): + table = get_table_name(doctype) + return cint( + frappe.db.sql( + f"""select table_rows + from information_schema.tables + where table_name = '{table}' ;""" + )[0][0] + ) + + def execute(filters=None): if not filters: filters = {} - sle_count = frappe.db.count("Stock Ledger Entry") + sle_count = _estimate_table_row_count("Stock Ledger Entry") 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.")) @@ -58,18 +69,18 @@ def execute(filters=None): 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"] - + [_("Opening Qty") + ":Float:90"] - + [_("In Qty") + ":Float:80"] - + [_("Out Qty") + ":Float:80"] - + [_("Balance Qty") + ":Float:90"] - + [_("UOM") + "::90"] - ) + columns = [ + _("Item") + ":Link/Item:100", + _("Item Name") + "::150", + _("Description") + "::150", + _("Warehouse") + ":Link/Warehouse:100", + _("Batch") + ":Link/Batch:100", + _("Opening Qty") + ":Float:90", + _("In Qty") + ":Float:80", + _("Out Qty") + ":Float:80", + _("Balance Qty") + ":Float:90", + _("UOM") + "::90", + ] return columns @@ -102,47 +113,60 @@ def get_stock_ledger_entries(filters): ) query = apply_warehouse_filter(query, sle, filters) + if filters.warehouse_type and not filters.warehouse: + warehouses = frappe.get_all( + "Warehouse", + filters={"warehouse_type": filters.warehouse_type, "is_group": 0}, + pluck="name", + ) + + if warehouses: + query = query.where(sle.warehouse.isin(warehouses)) + for field in ["item_code", "batch_no", "company"]: if filters.get(field): query = query.where(sle[field] == filters.get(field)) - return query.run(as_dict=True) + return query def get_item_warehouse_batch_map(filters, float_precision): - sle = get_stock_ledger_entries(filters) - iwb_map = {} + with frappe.db.unbuffered_cursor(): + sle = get_stock_ledger_entries(filters) + sle = sle.run(as_dict=True, as_iterator=True) - from_date = getdate(filters["from_date"]) - to_date = getdate(filters["to_date"]) + iwb_map = {} - for d in sle: - iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( - d.batch_no, frappe._dict({"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0}) - ) - qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] - if d.posting_date < from_date: - qty_dict.opening_qty = flt(qty_dict.opening_qty, float_precision) + flt( - d.actual_qty, float_precision + from_date = getdate(filters["from_date"]) + to_date = getdate(filters["to_date"]) + + for d in sle: + iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( + d.batch_no, frappe._dict({"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0}) ) - elif d.posting_date >= from_date and d.posting_date <= to_date: - if flt(d.actual_qty) > 0: - qty_dict.in_qty = flt(qty_dict.in_qty, float_precision) + flt(d.actual_qty, float_precision) - else: - qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) + abs( - flt(d.actual_qty, float_precision) + qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] + if d.posting_date < from_date: + qty_dict.opening_qty = flt(qty_dict.opening_qty, float_precision) + flt( + d.actual_qty, float_precision ) + elif d.posting_date >= from_date and d.posting_date <= to_date: + if flt(d.actual_qty) > 0: + qty_dict.in_qty = flt(qty_dict.in_qty, float_precision) + flt( + d.actual_qty, float_precision + ) + else: + qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) + abs( + flt(d.actual_qty, float_precision) + ) - qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision) + qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision) return iwb_map def get_item_details(filters): item_map = {} - for d in (frappe.qb.from_("Item").select("name", "item_name", "description", "stock_uom")).run( - as_dict=1 - ): + for d in (frappe.qb.from_("Item").select("name", "item_name", "description", "stock_uom")).run(as_dict=1): item_map.setdefault(d.name, d) return item_map diff --git a/erpnext/stock/report/bom_search/bom_search.js b/erpnext/stock/report/bom_search/bom_search.js index e9e763cb889..b6f3c9633e1 100644 --- a/erpnext/stock/report/bom_search/bom_search.js +++ b/erpnext/stock/report/bom_search/bom_search.js @@ -2,41 +2,41 @@ // For license information, please see license.txt frappe.query_reports["BOM Search"] = { - "filters": [ + filters: [ { fieldname: "item1", label: __("Item 1"), fieldtype: "Link", - options: "Item" + options: "Item", }, { fieldname: "item2", label: __("Item 2"), fieldtype: "Link", - options: "Item" + options: "Item", }, { fieldname: "item3", label: __("Item 3"), fieldtype: "Link", - options: "Item" + options: "Item", }, { fieldname: "item4", label: __("Item 4"), fieldtype: "Link", - options: "Item" + options: "Item", }, { fieldname: "item5", label: __("Item 5"), fieldtype: "Link", - options: "Item" + options: "Item", }, { fieldname: "search_sub_assemblies", label: __("Search Sub Assemblies"), fieldtype: "Check", }, - ] -} + ], +}; diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js index d7c50a66979..e61791cc8cd 100644 --- a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js @@ -2,30 +2,29 @@ // For license information, please see license.txt /* eslint-disable */ - frappe.query_reports["COGS By Item Group"] = { filters: [ - { - label: __("Company"), - fieldname: "company", - fieldtype: "Link", - options: "Company", - mandatory: true, - default: frappe.defaults.get_user_default("Company"), - }, - { - label: __("From Date"), - fieldname: "from_date", - fieldtype: "Date", - mandatory: true, - default: frappe.datetime.year_start(), - }, - { - label: __("To Date"), - fieldname: "to_date", - fieldtype: "Date", - mandatory: true, - default: frappe.datetime.get_today(), - }, - ] + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + mandatory: true, + default: frappe.defaults.get_user_default("Company"), + }, + { + label: __("From Date"), + fieldname: "from_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.year_start(), + }, + { + label: __("To Date"), + fieldname: "to_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.get_today(), + }, + ], }; diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py index 4642a535b63..07119411304 100644 --- a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py @@ -3,7 +3,6 @@ import datetime from collections import OrderedDict -from typing import Dict, List, Tuple, Union import frappe from frappe import _ @@ -13,15 +12,15 @@ from erpnext.accounts.report.general_ledger.general_ledger import get_gl_entries Filters = frappe._dict Row = frappe._dict -Data = List[Row] -Columns = List[Dict[str, str]] -DateTime = Union[datetime.date, datetime.datetime] -FilteredEntries = List[Dict[str, Union[str, float, DateTime, None]]] -ItemGroupsDict = Dict[Tuple[int, int], Dict[str, Union[str, int]]] -SVDList = List[frappe._dict] +Data = list[Row] +Columns = list[dict[str, str]] +DateTime = datetime.date | datetime.datetime +FilteredEntries = list[dict[str, str | float | DateTime | None]] +ItemGroupsDict = dict[tuple[int, int], dict[str, str | int]] +SVDList = list[frappe._dict] -def execute(filters: Filters) -> Tuple[Columns, Data]: +def execute(filters: Filters) -> tuple[Columns, Data]: update_filters_with_account(filters) validate_filters(filters) columns = get_columns() @@ -158,7 +157,7 @@ def assign_item_groups_to_svd_list(svd_list: SVDList) -> None: item.item_group = ig_map[item.get("item_code")] -def get_item_groups_map(svd_list: SVDList) -> Dict[str, str]: +def get_item_groups_map(svd_list: SVDList) -> dict[str, str]: item_codes = set(i["item_code"] for i in svd_list) ig_list = frappe.get_list( "Item", fields=["item_code", "item_group"], filters=[("item_code", "in", item_codes)] @@ -168,9 +167,7 @@ def get_item_groups_map(svd_list: SVDList) -> Dict[str, str]: def get_item_groups_dict() -> ItemGroupsDict: item_groups_list = frappe.get_all("Item Group", fields=("name", "is_group", "lft", "rgt")) - return { - (i["lft"], i["rgt"]): {"name": i["name"], "is_group": i["is_group"]} for i in item_groups_list - } + return {(i["lft"], i["rgt"]): {"name": i["name"], "is_group": i["is_group"]} for i in item_groups_list} def update_leveled_dict(leveled_dict: OrderedDict) -> None: diff --git a/erpnext/stock/report/delayed_item_report/delayed_item_report.js b/erpnext/stock/report/delayed_item_report/delayed_item_report.js index 40e6abefeb0..317fa77827a 100644 --- a/erpnext/stock/report/delayed_item_report/delayed_item_report.js +++ b/erpnext/stock/report/delayed_item_report/delayed_item_report.js @@ -3,60 +3,60 @@ /* eslint-disable */ frappe.query_reports["Delayed Item Report"] = { - "filters": [ + filters: [ { fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", default: frappe.defaults.get_default("company"), - reqd: 1 + reqd: 1, }, { - fieldname:"from_date", + fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: frappe.datetime.month_start(), - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: frappe.datetime.now_date(), - reqd: 1 + reqd: 1, }, { - fieldname:"sales_order", + fieldname: "sales_order", label: __("Sales Order"), fieldtype: "Link", options: "Sales Order", }, { - fieldname:"customer", + fieldname: "customer", label: __("Customer"), fieldtype: "Link", options: "Customer", }, { - fieldname:"customer_group", + fieldname: "customer_group", label: __("Customer Group"), fieldtype: "Link", options: "Customer Group", }, { - fieldname:"item_group", + fieldname: "item_group", label: __("Item Group"), fieldtype: "Link", options: "Item Group", }, { - fieldname:"based_on", + fieldname: "based_on", label: __("Based On"), fieldtype: "Select", options: ["Delivery Note", "Sales Invoice"], default: "Sales Invoice", - reqd: 1 + reqd: 1, }, - ] -} + ], +}; diff --git a/erpnext/stock/report/delayed_item_report/delayed_item_report.py b/erpnext/stock/report/delayed_item_report/delayed_item_report.py index 546a834da8c..88a188e0cad 100644 --- a/erpnext/stock/report/delayed_item_report/delayed_item_report.py +++ b/erpnext/stock/report/delayed_item_report/delayed_item_report.py @@ -13,7 +13,7 @@ def execute(filters=None, consolidated=False): return data, columns -class DelayedItemReport(object): +class DelayedItemReport: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -86,7 +86,11 @@ class DelayedItemReport(object): filters = {"parent": ("in", sales_orders), "name": ("in", sales_order_items)} so_data = {} - for d in frappe.get_all(doctype, filters=filters, fields=["delivery_date", "parent", "name"]): + fields = ["delivery_date", "name"] + if frappe.db.has_column(doctype, "parent"): + fields.append("parent") + + for d in frappe.get_all(doctype, filters=filters, fields=fields): key = d.name if consolidated else (d.parent, d.name) if key not in so_data: so_data.setdefault(key, d.delivery_date) diff --git a/erpnext/stock/report/delayed_order_report/delayed_order_report.js b/erpnext/stock/report/delayed_order_report/delayed_order_report.js index aab0f3d0d1f..34ea3240de0 100644 --- a/erpnext/stock/report/delayed_order_report/delayed_order_report.js +++ b/erpnext/stock/report/delayed_order_report/delayed_order_report.js @@ -3,60 +3,60 @@ /* eslint-disable */ frappe.query_reports["Delayed Order Report"] = { - "filters": [ + filters: [ { fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", default: frappe.defaults.get_default("company"), - reqd: 1 + reqd: 1, }, { - fieldname:"from_date", + fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: frappe.datetime.month_start(), - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: frappe.datetime.now_date(), - reqd: 1 + reqd: 1, }, { - fieldname:"sales_order", + fieldname: "sales_order", label: __("Sales Order"), fieldtype: "Link", options: "Sales Order", }, { - fieldname:"customer", + fieldname: "customer", label: __("Customer"), fieldtype: "Link", options: "Customer", }, { - fieldname:"customer_group", + fieldname: "customer_group", label: __("Customer Group"), fieldtype: "Link", options: "Customer Group", }, { - fieldname:"item_group", + fieldname: "item_group", label: __("Item Group"), fieldtype: "Link", options: "Item Group", }, { - fieldname:"based_on", + fieldname: "based_on", label: __("Based On"), fieldtype: "Select", options: ["Delivery Note", "Sales Invoice"], default: "Sales Invoice", - reqd: 1 + reqd: 1, }, - ] -} + ], +}; diff --git a/erpnext/stock/report/delayed_order_report/delayed_order_report.py b/erpnext/stock/report/delayed_order_report/delayed_order_report.py index 197218d7ff4..24be939f05b 100644 --- a/erpnext/stock/report/delayed_order_report/delayed_order_report.py +++ b/erpnext/stock/report/delayed_order_report/delayed_order_report.py @@ -20,7 +20,7 @@ class DelayedOrderReport(DelayedItemReport): return self.get_columns(), self.get_data(consolidated=True) or [] def get_data(self, consolidated=False): - data = super(DelayedOrderReport, self).get_data(consolidated) or [] + data = super().get_data(consolidated) or [] so_list = [] result = [] diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.js b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.js index 8a04565c197..5e7dc8b2a63 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.js +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.js @@ -1,8 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.require("assets/erpnext/js/sales_trends_filters.js", function() { +frappe.require("assets/erpnext/js/sales_trends_filters.js", function () { frappe.query_reports["Delivery Note Trends"] = { - filters: erpnext.get_sales_trends_filters() - } + filters: erpnext.get_sales_trends_filters(), + }; }); diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js index 0b8f49653dd..8fc3590c4d6 100644 --- a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js +++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js @@ -2,48 +2,45 @@ // For license information, please see license.txt /* eslint-disable */ -const DIFFERNCE_FIELD_NAMES = [ - "fifo_qty_diff", - "fifo_value_diff", -]; +const DIFFERNCE_FIELD_NAMES = ["fifo_qty_diff", "fifo_value_diff"]; frappe.query_reports["FIFO Queue vs Qty After Transaction Comparison"] = { - "filters": [ + filters: [ { - "fieldname": "item_code", - "fieldtype": "Link", - "label": "Item", - "options": "Item", - get_query: function() { + fieldname: "item_code", + fieldtype: "Link", + label: "Item", + options: "Item", + get_query: function () { return { - filters: {is_stock_item: 1, has_serial_no: 0} - } - } + filters: { is_stock_item: 1, has_serial_no: 0 }, + }; + }, }, { - "fieldname": "item_group", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group", + fieldname: "item_group", + fieldtype: "Link", + label: "Item Group", + options: "Item Group", }, { - "fieldname": "warehouse", - "fieldtype": "Link", - "label": "Warehouse", - "options": "Warehouse", + fieldname: "warehouse", + fieldtype: "Link", + label: "Warehouse", + options: "Warehouse", }, { - "fieldname": "from_date", - "fieldtype": "Date", - "label": "From Posting Date", + fieldname: "from_date", + fieldtype: "Date", + label: "From Posting Date", }, { - "fieldname": "to_date", - "fieldtype": "Date", - "label": "From Posting Date", - } + fieldname: "to_date", + fieldtype: "Date", + label: "From Posting Date", + }, ], - formatter (value, row, column, data, default_formatter) { + formatter(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) { value = "" + value + ""; diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py index 9e140336c9a..404a58b799d 100644 --- a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py +++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py @@ -40,19 +40,18 @@ def get_data(filters): def get_stock_ledger_entries(filters): - sle_filters = {"is_cancelled": 0} if filters.warehouse: children = get_descendants_of("Warehouse", filters.warehouse) - sle_filters["warehouse"] = ("in", children + [filters.warehouse]) + sle_filters["warehouse"] = ("in", [*children, filters.warehouse]) if filters.item_code: sle_filters["item_code"] = filters.item_code elif filters.get("item_group"): item_group = filters.get("item_group") children = get_descendants_of("Item Group", item_group) - item_group_filter = {"item_group": ("in", children + [item_group])} + item_group_filter = {"item_group": ("in", [*children, item_group])} sle_filters["item_code"] = ( "in", frappe.get_all("Item", filters=item_group_filter, pluck="name", order_by=None), diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js index bf11277d9c4..0e6f7a0d177 100644 --- a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js @@ -3,25 +3,25 @@ /* eslint-disable */ frappe.query_reports["Incorrect Balance Qty After Transaction"] = { - "filters": [ + filters: [ { label: __("Company"), fieldtype: "Link", fieldname: "company", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { - label: __('Item Code'), - fieldtype: 'Link', - fieldname: 'item_code', - options: 'Item' + label: __("Item Code"), + fieldtype: "Link", + fieldname: "item_code", + options: "Item", }, { - label: __('Warehouse'), - fieldtype: 'Link', - fieldname: 'warehouse' - } - ] + label: __("Warehouse"), + fieldtype: "Link", + fieldname: "warehouse", + }, + ], }; diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py index b68db356ead..163e7ab8843 100644 --- a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py @@ -27,7 +27,7 @@ def get_data(filters): def validate_data(itewise_balance_qty): res = [] - for key, data in itewise_balance_qty.items(): + for _key, data in itewise_balance_qty.items(): row = get_incorrect_data(data) if row: res.append(row) diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js index ff27fb32cd8..cde502ad738 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js @@ -3,33 +3,33 @@ /* eslint-disable */ frappe.query_reports["Incorrect Serial No Valuation"] = { - "filters": [ + filters: [ { - label: __('Item Code'), - fieldtype: 'Link', - fieldname: 'item_code', - options: 'Item', - get_query: function() { + label: __("Item Code"), + fieldtype: "Link", + fieldname: "item_code", + options: "Item", + get_query: function () { return { filters: { - 'has_serial_no': 1 - } - } - } + has_serial_no: 1, + }, + }; + }, }, { - label: __('From Date'), - fieldtype: 'Date', - fieldname: 'from_date', + label: __("From Date"), + fieldtype: "Date", + fieldname: "from_date", reqd: 1, - default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1] + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], }, { - label: __('To Date'), - fieldtype: 'Date', - fieldname: 'to_date', + label: __("To Date"), + fieldtype: "Date", + fieldname: "to_date", reqd: 1, - default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2] - } - ] + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + }, + ], }; diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py index 39d84a7d5be..4d740bd829a 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py @@ -43,11 +43,9 @@ def prepare_serial_nos(data): def get_incorrect_serial_nos(serial_nos_data): result = [] - total_value = frappe._dict( - {"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Balance"))} - ) + total_value = frappe._dict({"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Balance"))}) - for serial_no, data in serial_nos_data.items(): + for _serial_no, data in serial_nos_data.items(): total_dict = frappe._dict({"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Total"))}) if check_incorrect_serial_data(data, total_dict): diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js index ff424807e3e..65e0b928bb7 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js @@ -3,34 +3,34 @@ /* eslint-disable */ frappe.query_reports["Incorrect Stock Value Report"] = { - "filters": [ + filters: [ { - "label": __("Company"), - "fieldname": "company", - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "label": __("Account"), - "fieldname": "account", - "fieldtype": "Link", - "options": "Account", - get_query: function() { - var company = frappe.query_report.get_filter_value('company'); + label: __("Account"), + fieldname: "account", + fieldtype: "Link", + options: "Account", + get_query: function () { + var company = frappe.query_report.get_filter_value("company"); return { filters: { - "account_type": "Stock", - "company": company - } - } - } + account_type: "Stock", + company: company, + }, + }; + }, }, { - "label": __("From Date"), - "fieldname": "from_date", - "fieldtype": "Date" - } - ] + label: __("From Date"), + fieldname: "from_date", + fieldtype: "Date", + }, + ], }; diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index e4f657ca707..7be88314ffd 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.query_builder import Field -from frappe.query_builder.functions import CombineDatetime, Min +from frappe.query_builder.functions import Min from frappe.utils import add_days, getdate, today import erpnext @@ -16,9 +16,7 @@ from erpnext.stock.utils import get_stock_value_on def execute(filters=None): if not erpnext.is_perpetual_inventory_enabled(filters.company): frappe.throw( - _("Perpetual inventory required for the company {0} to view this report.").format( - filters.company - ) + _("Perpetual inventory required for the company {0} to view this report.").format(filters.company) ) data = get_data(filters) @@ -75,7 +73,7 @@ def get_data(report_filters): & (sle.company == report_filters.company) & (sle.is_cancelled == 0) ) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time), sle.creation) + .orderby(sle.posting_datetime, sle.creation) ).run(as_dict=True) for d in data: @@ -83,9 +81,7 @@ def get_data(report_filters): closing_date = add_days(from_date, -1) for key, stock_data in voucher_wise_dict.items(): - prev_stock_value = get_stock_value_on( - posting_date=closing_date, item_code=key[0], warehouses=key[1] - ) + prev_stock_value = get_stock_value_on(posting_date=closing_date, item_code=key[0], warehouses=key[1]) for data in stock_data: expected_stock_value = prev_stock_value + data.stock_value_difference if abs(data.stock_value - expected_stock_value) > 0.1: diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.js b/erpnext/stock/report/item_price_stock/item_price_stock.js index 7af1dab6a0b..aa2b7aaa4f3 100644 --- a/erpnext/stock/report/item_price_stock/item_price_stock.js +++ b/erpnext/stock/report/item_price_stock/item_price_stock.js @@ -3,12 +3,12 @@ /* eslint-disable */ frappe.query_reports["Item Price Stock"] = { - "filters": [ + filters: [ { - "fieldname":"item_code", - "label": __("Item"), - "fieldtype": "Link", - "options": "Item" - } - ] -} + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + }, + ], +}; diff --git a/erpnext/stock/report/item_prices/item_prices.js b/erpnext/stock/report/item_prices/item_prices.js index 77bca4466d0..6724c1a2909 100644 --- a/erpnext/stock/report/item_prices/item_prices.js +++ b/erpnext/stock/report/item_prices/item_prices.js @@ -2,16 +2,13 @@ // For license information, please see license.txt frappe.query_reports["Item Prices"] = { - "filters": [ + filters: [ { - "fieldname": "items", - "label": __("Items Filter"), - "fieldtype": "Select", - "options": "Enabled Items only\nDisabled Items only\nAll Items", - "default": "Enabled Items only", - "on_change": function(query_report) { - query_report.trigger_refresh(); - } - } - ] -} + fieldname: "items", + label: __("Items Filter"), + fieldtype: "Select", + options: "Enabled Items only\nDisabled Items only\nAll Items", + default: "Enabled Items only", + }, + ], +}; diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index a53a9f24f54..5c5b84f8393 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -111,9 +111,7 @@ def get_price_list(): ).run(as_dict=True) for d in price_list: - d.update( - {"price": "{0} {1} - {2}".format(d.currency, round(d.price_list_rate, 2), d.price_list)} - ) + d.update({"price": f"{d.currency} {round(d.price_list_rate, 2)} - {d.price_list}"}) d.pop("currency") d.pop("price_list_rate") d.pop("price_list") diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.js b/erpnext/stock/report/item_shortage_report/item_shortage_report.js index ca42a331e91..5a6a54734a4 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.js +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.js @@ -3,24 +3,24 @@ /* eslint-disable */ frappe.query_reports["Item Shortage Report"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "width": "80", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_default("company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + width: "80", + options: "Company", + reqd: 1, + default: frappe.defaults.get_default("company"), }, { - "fieldname": "warehouse", - "label": __("Warehouse"), - "fieldtype": "MultiSelectList", - "width": "100", - get_data: function(txt) { - return frappe.db.get_link_options('Warehouse', txt); - } - } - ] + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "MultiSelectList", + width: "100", + get_data: function (txt) { + return frappe.db.get_link_options("Warehouse", txt); + }, + }, + ], }; diff --git a/erpnext/stock/report/item_variant_details/item_variant_details.js b/erpnext/stock/report/item_variant_details/item_variant_details.js index 78eab4050c1..f61e7417724 100644 --- a/erpnext/stock/report/item_variant_details/item_variant_details.js +++ b/erpnext/stock/report/item_variant_details/item_variant_details.js @@ -3,7 +3,7 @@ /* eslint-disable */ frappe.query_reports["Item Variant Details"] = { - "filters": [ + filters: [ { reqd: 1, default: "", @@ -13,9 +13,9 @@ frappe.query_reports["Item Variant Details"] = { fieldtype: "Link", get_query: () => { return { - filters: { "has_variants": 1 } - } - } - } - ] -} + filters: { has_variants: 1 }, + }; + }, + }, + ], +}; diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js index 173aad6d5a9..3b12dab939a 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js @@ -2,31 +2,31 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Itemwise Recommended Reorder Level"] = { - "filters": [ + filters: [ { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.sys_defaults.year_start_date + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.sys_defaults.year_start_date, }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), }, { - "fieldname":"item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "options": "Item Group", - "reqd": 1 + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", + reqd: 1, }, { - "fieldname":"brand", - "label": __("Brand"), - "fieldtype": "Link", - "options": "Brand" - } - ] -} + fieldname: "brand", + label: __("Brand"), + fieldtype: "Link", + options: "Brand", + }, + ], +}; diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index c4358b809fc..dffe9486a64 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -24,9 +24,7 @@ def execute(filters=None): data = [] for item in items: - total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt( - delivered_item_map.get(item.name, 0) - ) + total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt(delivered_item_map.get(item.name, 0)) avg_daily_outgoing = flt(total_outgoing / diff, float_precision) reorder_level = (avg_daily_outgoing * flt(item.lead_time_days)) + flt(item.safety_stock) diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js index 4458a7245f3..5cef5c70341 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js @@ -2,51 +2,51 @@ // For license information, please see license.txt frappe.query_reports["Product Bundle Balance"] = { - "filters": [ + filters: [ { - "fieldname":"date", - "label": __("Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.get_today(), + fieldname: "date", + label: __("Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.get_today(), }, { - "fieldname": "item_code", - "label": __("Item"), - "fieldtype": "Link", - "width": "80", - "options": "Item", - "get_query": function() { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + width: "80", + options: "Item", + get_query: function () { return { query: "erpnext.controllers.queries.item_query", - filters: {"is_stock_item": 0} + filters: { is_stock_item: 0 }, }; - } + }, }, { - "fieldname": "item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "width": "80", - "options": "Item Group" + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + width: "80", + options: "Item Group", }, { - "fieldname":"brand", - "label": __("Brand"), - "fieldtype": "Link", - "options": "Brand" + fieldname: "brand", + label: __("Brand"), + fieldtype: "Link", + options: "Brand", }, { - "fieldname": "warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "width": "80", - "options": "Warehouse" + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + width: "80", + options: "Warehouse", }, ], - "initial_depth": 0, - "formatter": function(value, row, column, data, default_formatter) { + initial_depth: 0, + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (!data.parent_item) { value = $(`${value}`); @@ -54,5 +54,5 @@ frappe.query_reports["Product Bundle Balance"] = { value = $value.wrap("

              ").parent().html(); } return value; - } + }, }; diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 9e75201bd14..dd79e7fcaf5 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -213,13 +213,11 @@ def get_stock_ledger_entries(filters, items): query = ( frappe.qb.from_(sle) - .force_index("posting_sort_index") .left_join(sle2) .on( (sle.item_code == sle2.item_code) & (sle.warehouse == sle2.warehouse) - & (sle.posting_date < sle2.posting_date) - & (sle.posting_time < sle2.posting_time) + & (sle.posting_datetime < sle2.posting_datetime) & (sle.name < sle2.name) ) .select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company) diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.js b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.js index 695efacb694..bddfe5d7705 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.js +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.js @@ -1,8 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.require("assets/erpnext/js/purchase_trends_filters.js", function() { +frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () { frappe.query_reports["Purchase Receipt Trends"] = { - filters: erpnext.get_purchase_trends_filters() - } + filters: erpnext.get_purchase_trends_filters(), + }; }); diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js index 616312e3118..6c2c17db52c 100644 --- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js +++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js @@ -3,50 +3,50 @@ /* eslint-disable */ frappe.query_reports["Serial No Ledger"] = { - "filters": [ + filters: [ { - 'label': __('Item Code'), - 'fieldtype': 'Link', - 'fieldname': 'item_code', - 'reqd': 1, - 'options': 'Item', - get_query: function() { + label: __("Item Code"), + fieldtype: "Link", + fieldname: "item_code", + reqd: 1, + options: "Item", + get_query: function () { return { filters: { - 'has_serial_no': 1 - } - } - } + has_serial_no: 1, + }, + }; + }, }, { - 'label': __('Serial No'), - 'fieldtype': 'Link', - 'fieldname': 'serial_no', - 'options': 'Serial No', - 'reqd': 1 + label: __("Serial No"), + fieldtype: "Link", + fieldname: "serial_no", + options: "Serial No", + reqd: 1, }, { - 'label': __('Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'warehouse', - 'options': 'Warehouse', - get_query: function() { - let company = frappe.query_report.get_filter_value('company'); + label: __("Warehouse"), + fieldtype: "Link", + fieldname: "warehouse", + options: "Warehouse", + get_query: function () { + let company = frappe.query_report.get_filter_value("company"); if (company) { return { filters: { - 'company': company - } - } + company: company, + }, + }; } - } + }, }, { - 'label': __('As On Date'), - 'fieldtype': 'Date', - 'fieldname': 'posting_date', - 'default': frappe.datetime.get_today() + label: __("As On Date"), + fieldtype: "Date", + fieldname: "posting_date", + default: frappe.datetime.get_today(), }, - ] + ], }; diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.js b/erpnext/stock/report/stock_ageing/stock_ageing.js index db463b7ca09..726b507663d 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.js +++ b/erpnext/stock/report/stock_ageing/stock_ageing.js @@ -2,74 +2,84 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Stock Ageing"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"to_date", - "label": __("As On Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1 + fieldname: "to_date", + label: __("As On Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse", + fieldname: "warehouse_type", + label: __("Warehouse Type"), + fieldtype: "Link", + width: "80", + options: "Warehouse Type", + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", get_query: () => { + let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); const company = frappe.query_report.get_filter_value("company"); + return { filters: { - ...company && {company}, - } + ...(warehouse_type && { warehouse_type }), + ...(company && { company }), + }, }; - } + }, }, { - "fieldname":"item_code", - "label": __("Item"), - "fieldtype": "Link", - "options": "Item" + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", }, { - "fieldname":"brand", - "label": __("Brand"), - "fieldtype": "Link", - "options": "Brand" + fieldname: "brand", + label: __("Brand"), + fieldtype: "Link", + options: "Brand", }, { - "fieldname":"range1", - "label": __("Ageing Range 1"), - "fieldtype": "Int", - "default": "30", - "reqd": 1 + fieldname: "range1", + label: __("Ageing Range 1"), + fieldtype: "Int", + default: "30", + reqd: 1, }, { - "fieldname":"range2", - "label": __("Ageing Range 2"), - "fieldtype": "Int", - "default": "60", - "reqd": 1 + fieldname: "range2", + label: __("Ageing Range 2"), + fieldtype: "Int", + default: "60", + reqd: 1, }, { - "fieldname":"range3", - "label": __("Ageing Range 3"), - "fieldtype": "Int", - "default": "90", - "reqd": 1 + fieldname: "range3", + label: __("Ageing Range 3"), + fieldtype: "Int", + default: "90", + reqd: 1, }, { - "fieldname":"show_warehouse_wise_stock", - "label": __("Show Warehouse-wise Stock"), - "fieldtype": "Check", - "default": 0 - } - ] -} + fieldname: "show_warehouse_wise_stock", + label: __("Show Warehouse-wise Stock"), + fieldtype: "Check", + default: 0, + }, + ], +}; diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index d0929a082ce..26bf99e1ed7 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -3,7 +3,6 @@ from operator import itemgetter -from typing import Dict, List, Tuple, Union import frappe from frappe import _ @@ -14,7 +13,7 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos Filters = frappe._dict -def execute(filters: Filters = None) -> Tuple: +def execute(filters: Filters = None) -> tuple: to_date = filters["to_date"] columns = get_columns(filters) @@ -26,14 +25,14 @@ def execute(filters: Filters = None) -> Tuple: return columns, data, None, chart_data -def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> List[Dict]: +def format_report_data(filters: Filters, item_details: dict, to_date: str) -> list[dict]: "Returns ordered, formatted data with ranges." _func = itemgetter(1) data = [] precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) - for item, item_dict in item_details.items(): + for _item, item_dict in item_details.items(): if not flt(item_dict.get("total_qty"), precision): continue @@ -74,12 +73,12 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li return data -def get_average_age(fifo_queue: List, to_date: str) -> float: +def get_average_age(fifo_queue: list, to_date: str) -> float: batch_age = age_qty = total_qty = 0.0 for batch in fifo_queue: batch_age = date_diff(to_date, batch[1]) - if isinstance(batch[0], (int, float)): + if isinstance(batch[0], int | float): age_qty += batch_age * batch[0] total_qty += batch[0] else: @@ -89,8 +88,7 @@ def get_average_age(fifo_queue: List, to_date: str) -> float: return flt(age_qty / total_qty, 2) if total_qty else 0.0 -def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple: - +def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> tuple: precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) range1 = range2 = range3 = above_range3 = 0.0 @@ -111,7 +109,7 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D return range1, range2, range3, above_range3 -def get_columns(filters: Filters) -> List[Dict]: +def get_columns(filters: Filters) -> list[dict]: range_columns = [] setup_ageing_columns(filters, range_columns) columns = [ @@ -169,7 +167,7 @@ def get_columns(filters: Filters) -> List[Dict]: return columns -def get_chart_data(data: List, filters: Filters) -> Dict: +def get_chart_data(data: list, filters: Filters) -> dict: if not data: return [] @@ -193,7 +191,7 @@ def get_chart_data(data: List, filters: Filters) -> Dict: } -def setup_ageing_columns(filters: Filters, range_columns: List): +def setup_ageing_columns(filters: Filters, range_columns: list): ranges = [ f"0 - {filters['range1']}", f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}", @@ -205,23 +203,21 @@ def setup_ageing_columns(filters: Filters, range_columns: List): add_column(range_columns, label=_("Age ({0})").format(label), fieldname=fieldname) -def add_column( - range_columns: List, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140 -): +def add_column(range_columns: list, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140): range_columns.append(dict(label=label, fieldname=fieldname, fieldtype=fieldtype, width=width)) class FIFOSlots: "Returns FIFO computed slots of inwarded stock as per date." - def __init__(self, filters: Dict = None, sle: List = None): + def __init__(self, filters: dict | None = None, sle: list | None = None): self.item_details = {} self.transferred_item_details = {} self.serial_no_batch_purchase_details = {} self.filters = filters self.sle = sle - def generate(self) -> Dict: + def generate(self) -> dict: """ Returns dict of the foll.g structure: Key = Item A / (Item A, Warehouse A) @@ -231,25 +227,30 @@ class FIFOSlots: consumed/updated and maintained via FIFO. ** } """ - if self.sle is None: - self.sle = self.__get_stock_ledger_entries() + stock_ledger_entries = self.sle - for d in self.sle: - key, fifo_queue, transferred_item_key = self.__init_key_stores(d) + with frappe.db.unbuffered_cursor(): + if self.sle is None: + self.sle = self.__get_stock_ledger_entries() - if d.voucher_type == "Stock Reconciliation": - # get difference in qty shift as actual qty - prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) - d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) + for d in self.sle: + key, fifo_queue, transferred_item_key = self.__init_key_stores(d) - serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] + if d.voucher_type == "Stock Reconciliation": + # get difference in qty shift as actual qty + prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) + d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) - if d.actual_qty > 0: - self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos) - else: - self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos) + serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] - self.__update_balances(d, key) + if d.actual_qty > 0: + self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos) + else: + self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos) + + self.__update_balances(d, key) + + del stock_ledger_entries if not self.filters.get("show_warehouse_wise_stock"): # (Item 1, WH 1), (Item 1, WH 2) => (Item 1) @@ -257,7 +258,7 @@ class FIFOSlots: return self.item_details - def __init_key_stores(self, row: Dict) -> Tuple: + def __init_key_stores(self, row: dict) -> tuple: "Initialise keys and FIFO Queue." key = (row.name, row.warehouse) @@ -269,9 +270,7 @@ class FIFOSlots: return key, fifo_queue, transferred_item_key - def __compute_incoming_stock( - self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List - ): + def __compute_incoming_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list): "Update FIFO Queue on inward stock." transfer_data = self.transferred_item_details.get(transfer_key) @@ -297,9 +296,7 @@ class FIFOSlots: self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date) fifo_queue.append([serial_no, row.posting_date]) - def __compute_outgoing_stock( - self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List - ): + def __compute_outgoing_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list): "Update FIFO Queue on outward stock." if serial_nos: fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos] @@ -325,7 +322,7 @@ class FIFOSlots: self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]]) qty_to_pop = 0 - def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict): + def __adjust_incoming_transfer_qty(self, transfer_data: dict, fifo_queue: list, row: dict): "Add previously removed stock back to FIFO Queue." transfer_qty_to_pop = flt(row.actual_qty) @@ -352,7 +349,7 @@ class FIFOSlots: add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]]) transfer_qty_to_pop = 0 - def __update_balances(self, row: Dict, key: Union[Tuple, str]): + def __update_balances(self, row: dict, key: tuple | str): self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction if "total_qty" not in self.item_details[key]: @@ -362,7 +359,7 @@ class FIFOSlots: self.item_details[key]["has_serial_no"] = row.has_serial_no - def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict: + def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict: "Aggregate Item-Wh wise data into single Item entry." item_aggregated_data = {} for key, row in wh_wise_data.items(): @@ -370,7 +367,12 @@ class FIFOSlots: if not item_aggregated_data.get(item): item_aggregated_data.setdefault( item, - {"details": frappe._dict(), "fifo_queue": [], "qty_after_transaction": 0.0, "total_qty": 0.0}, + { + "details": frappe._dict(), + "fifo_queue": [], + "qty_after_transaction": 0.0, + "total_qty": 0.0, + }, ) item_row = item_aggregated_data.get(item) item_row["details"].update(row["details"]) @@ -381,7 +383,7 @@ class FIFOSlots: return item_aggregated_data - def __get_stock_ledger_entries(self) -> List[Dict]: + def __get_stock_ledger_entries(self) -> list[dict]: sle = frappe.qb.DocType("Stock Ledger Entry") item = self.__get_item_query() # used as derived table in sle query @@ -415,10 +417,19 @@ class FIFOSlots: if self.filters.get("warehouse"): sle_query = self.__get_warehouse_conditions(sle, sle_query) + elif self.filters.get("warehouse_type"): + warehouses = frappe.get_all( + "Warehouse", + filters={"warehouse_type": self.filters.get("warehouse_type"), "is_group": 0}, + pluck="name", + ) + + if warehouses: + sle_query = sle_query.where(sle.warehouse.isin(warehouses)) sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty) - return sle_query.run(as_dict=True) + return sle_query.run(as_dict=True, as_iterator=True) def __get_item_query(self) -> str: item_table = frappe.qb.DocType("Item") diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.js b/erpnext/stock/report/stock_analytics/stock_analytics.js index 071bfa22959..0cfed95ba6b 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.js +++ b/erpnext/stock/report/stock_analytics/stock_analytics.js @@ -3,38 +3,38 @@ /* eslint-disable */ frappe.query_reports["Stock Analytics"] = { - "filters": [ + filters: [ { fieldname: "item_group", label: __("Item Group"), fieldtype: "Link", - options:"Item Group", + options: "Item Group", default: "", }, { fieldname: "item_code", label: __("Item"), fieldtype: "Link", - options:"Item", + options: "Item", default: "", - get_query: () => ({filters: { 'is_stock_item': 1 }}), + get_query: () => ({ filters: { is_stock_item: 1 } }), }, { fieldname: "value_quantity", label: __("Value Or Qty"), fieldtype: "Select", options: [ - { "value": "Value", "label": __("Value") }, - { "value": "Quantity", "label": __("Quantity") } + { value: "Value", label: __("Value") }, + { value: "Quantity", label: __("Quantity") }, ], default: "Value", - reqd: 1 + reqd: 1, }, { fieldname: "brand", label: __("Brand"), fieldtype: "Link", - options:"Brand", + options: "Brand", default: "", }, { @@ -51,92 +51,91 @@ frappe.query_reports["Stock Analytics"] = { fieldtype: "Link", options: "Warehouse", default: "", - get_query: function() { - const company = frappe.query_report.get_filter_value('company'); + get_query: function () { + const company = frappe.query_report.get_filter_value("company"); return { - filters: { 'company': company } - } - } + filters: { company: company }, + }; + }, }, { fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: frappe.defaults.get_global_default("year_start_date"), - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: frappe.defaults.get_global_default("year_end_date"), - reqd: 1 + reqd: 1, }, { fieldname: "range", label: __("Range"), fieldtype: "Select", options: [ - { "value": "Weekly", "label": __("Weekly") }, - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Yearly", "label": __("Yearly") } + { value: "Weekly", label: __("Weekly") }, + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Yearly", label: __("Yearly") }, ], default: "Monthly", - reqd: 1 - } + reqd: 1, + }, ], - after_datatable_render: function(datatable_obj) { - $(datatable_obj.wrapper).find(".dt-row-0").find('input[type=checkbox]').click(); + after_datatable_render: function (datatable_obj) { + $(datatable_obj.wrapper).find(".dt-row-0").find("input[type=checkbox]").click(); }, get_datatable_options(options) { return Object.assign(options, { checkboxColumn: true, events: { - onCheckRow: function(data) { + onCheckRow: function (data) { row_name = data[2].content; row_values = data.slice(7).map(function (column) { return column.content; - }) - entry = { - 'name':row_name, - 'values':row_values - } + }); + entry = { + name: row_name, + values: row_values, + }; let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; var found = false; - for(var i=0; i < new_datasets.length;i++){ - if(new_datasets[i].name == row_name){ + for (var i = 0; i < new_datasets.length; i++) { + if (new_datasets[i].name == row_name) { found = true; - new_datasets.splice(i,1); + new_datasets.splice(i, 1); break; } } - if(!found){ + if (!found) { new_datasets.push(entry); } let new_data = { labels: raw_data.labels, - datasets: new_datasets - } + datasets: new_datasets, + }; setTimeout(() => { - frappe.query_report.chart.update(new_data) - },500) - + frappe.query_report.chart.update(new_data); + }, 500); setTimeout(() => { frappe.query_report.chart.draw(true); - }, 1000) + }, 1000); frappe.query_report.raw_chart_data = new_data; }, - } + }, }); - } -} + }, +}; diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index ab48181c48d..920c315352c 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt import datetime -from typing import List import frappe from frappe import _, scrub @@ -48,12 +47,10 @@ def get_columns(filters): ranges = get_period_date_ranges(filters) - for dummy, end_date in ranges: + for _dummy, end_date in ranges: period = get_period(end_date, filters) - columns.append( - {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} - ) + columns.append({"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120}) return columns @@ -67,7 +64,7 @@ def get_period_date_ranges(filters): increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(filters.range, 1) periodic_daterange = [] - for dummy in range(1, 53, increment): + for _dummy in range(1, 53, increment): if filters.range == "Weekly": period_end_date = from_date + relativedelta(days=6) else: @@ -116,9 +113,7 @@ def get_period(posting_date, filters): elif filters.range == "Monthly": period = _(str(months[posting_date.month - 1])) + " " + str(posting_date.year) elif filters.range == "Quarterly": - period = _("Quarter {0} {1}").format( - str(((posting_date.month - 1) // 3) + 1), str(posting_date.year) - ) + period = _("Quarter {0} {1}").format(str(((posting_date.month - 1) // 3) + 1), str(posting_date.year)) else: year = get_fiscal_year(posting_date, company=filters.company) period = str(year[2]) @@ -188,15 +183,13 @@ def get_periodic_data(entry, filters): periodic_data.setdefault(d.item_code, {}).setdefault(period, {}).setdefault(d.warehouse, 0.0) periodic_data[d.item_code]["balance"][d.warehouse] += value - periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]["balance"][ - d.warehouse - ] + periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]["balance"][d.warehouse] return periodic_data def fill_intermediate_periods( - periodic_data, item_code: str, current_period: str, all_periods: List[str] + periodic_data, item_code: str, current_period: str, all_periods: list[str] ) -> None: """There might be intermediate periods where no stock ledger entry exists, copy previous previous data. @@ -235,7 +228,7 @@ def get_data(filters): today = getdate() - for dummy, item_data in item_details.items(): + for _dummy, item_data in item_details.items(): row = { "name": item_data.name, "item_name": item_data.item_name, @@ -273,7 +266,7 @@ def get_items(filters): item_filters = {"is_stock_item": 1} if item_group := filters.get("item_group"): children = get_descendants_of("Item Group", item_group, ignore_permissions=True) - item_filters["item_group"] = ("in", children + [item_group]) + item_filters["item_group"] = ("in", [*children, item_group]) if brand := filters.get("brand"): item_filters["brand"] = brand diff --git a/erpnext/stock/report/stock_analytics/test_stock_analytics.py b/erpnext/stock/report/stock_analytics/test_stock_analytics.py index dd8f8d80385..b4f086c3631 100644 --- a/erpnext/stock/report/stock_analytics/test_stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/test_stock_analytics.py @@ -3,7 +3,7 @@ import datetime import frappe from frappe import _dict from frappe.tests.utils import FrappeTestCase -from frappe.utils.data import add_to_date, get_datetime, getdate, nowdate +from frappe.utils.data import add_to_date, getdate from erpnext.accounts.utils import get_fiscal_year from erpnext.stock.doctype.item.test_item import make_item @@ -55,7 +55,6 @@ class TestStockAnalyticsReport(FrappeTestCase): self.assertEqual(actual_buckets, expected_buckets) def test_get_period_date_ranges(self): - filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06") ranges = get_period_date_ranges(filters) @@ -69,7 +68,6 @@ class TestStockAnalyticsReport(FrappeTestCase): self.assertEqual(ranges, expected_ranges) def test_get_period_date_ranges_yearly(self): - filters = _dict(range="Yearly", from_date="2021-01-28", to_date="2021-02-06") ranges = get_period_date_ranges(filters) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js index 254f5273be2..6204c4f725f 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js @@ -3,35 +3,35 @@ /* eslint-disable */ frappe.query_reports["Stock and Account Value Comparison"] = { - "filters": [ + filters: [ { - "label": __("Company"), - "fieldname": "company", - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "label": __("Account"), - "fieldname": "account", - "fieldtype": "Link", - "options": "Account", - get_query: function() { - var company = frappe.query_report.get_filter_value('company'); + label: __("Account"), + fieldname: "account", + fieldtype: "Link", + options: "Account", + get_query: function () { + var company = frappe.query_report.get_filter_value("company"); return { filters: { - "account_type": "Stock", - "company": company - } - } - } + account_type: "Stock", + company: company, + }, + }; + }, }, { - "label": __("As On Date"), - "fieldname": "as_on_date", - "fieldtype": "Date", - "default": frappe.datetime.get_today(), + label: __("As On Date"), + fieldname: "as_on_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), }, ], @@ -42,7 +42,7 @@ frappe.query_reports["Stock and Account Value Comparison"] = { }, onload(report) { - report.page.add_inner_button(__("Create Reposting Entries"), function() { + report.page.add_inner_button(__("Create Reposting Entries"), function () { let message = `

              Reposting Entries will change the value of @@ -54,7 +54,7 @@ frappe.query_reports["Stock and Account Value Comparison"] = {

              `; let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); - let selected_rows = indexes.map(i => frappe.query_report.data[i]); + let selected_rows = indexes.map((i) => frappe.query_report.data[i]); if (!selected_rows.length) { frappe.throw(__("Please select rows to create Reposting Entries")); @@ -65,11 +65,10 @@ frappe.query_reports["Stock and Account Value Comparison"] = { method: "erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison.create_reposting_entries", args: { rows: selected_rows, - company: frappe.query_report.get_filter_values().company - } + company: frappe.query_report.get_filter_values().company, + }, }); - }); }); - } + }, }; diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 416cf48871a..67e340d0f70 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -14,9 +14,7 @@ from erpnext.stock.doctype.warehouse.warehouse import get_warehouses_based_on_ac def execute(filters=None): if not erpnext.is_perpetual_inventory_enabled(filters.company): frappe.throw( - _("Perpetual inventory required for the company {0} to view this report.").format( - filters.company - ) + _("Perpetual inventory required for the company {0} to view this report.").format(filters.company) ) data = get_data(filters) @@ -34,7 +32,7 @@ def get_data(report_filters): "posting_date": ("<=", report_filters.as_on_date), } - currency_precision = get_currency_precision() or 2 + get_currency_precision() or 2 stock_ledger_entries = get_stock_ledger_data(report_filters, filters) voucher_wise_gl_data = get_gl_data(report_filters, filters) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 6de5f00ece8..ca2c053fdb1 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -2,119 +2,118 @@ // For license information, please see license.txt frappe.query_reports["Stock Balance"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "width": "80", - "options": "Company", - "default": frappe.defaults.get_default("company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + width: "80", + options: "Company", + default: frappe.defaults.get_default("company"), }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.get_today() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.get_today(), }, { - "fieldname": "item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "width": "80", - "options": "Item Group" + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + width: "80", + options: "Item Group", }, { - "fieldname": "item_code", - "label": __("Item"), - "fieldtype": "Link", - "width": "80", - "options": "Item", - "get_query": function() { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + width: "80", + options: "Item", + get_query: function () { return { query: "erpnext.controllers.queries.item_query", }; - } + }, }, { - "fieldname": "warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "width": "80", - "options": "Warehouse", + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + width: "80", + options: "Warehouse", get_query: () => { let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); let company = frappe.query_report.get_filter_value("company"); return { filters: { - ...warehouse_type && {warehouse_type}, - ...company && {company} - } - } - } + ...(warehouse_type && { warehouse_type }), + ...(company && { company }), + }, + }; + }, }, { - "fieldname": "warehouse_type", - "label": __("Warehouse Type"), - "fieldtype": "Link", - "width": "80", - "options": "Warehouse Type" + fieldname: "warehouse_type", + label: __("Warehouse Type"), + fieldtype: "Link", + width: "80", + options: "Warehouse Type", }, { - "fieldname": "valuation_field_type", - "label": __("Valuation Field Type"), - "fieldtype": "Select", - "width": "80", - "options": "Currency\nFloat", - "default": "Currency" + fieldname: "valuation_field_type", + label: __("Valuation Field Type"), + fieldtype: "Select", + width: "80", + options: "Currency\nFloat", + default: "Currency", }, { - "fieldname":"include_uom", - "label": __("Include UOM"), - "fieldtype": "Link", - "options": "UOM" + fieldname: "include_uom", + label: __("Include UOM"), + fieldtype: "Link", + options: "UOM", }, { - "fieldname": "show_variant_attributes", - "label": __("Show Variant Attributes"), - "fieldtype": "Check" + fieldname: "show_variant_attributes", + label: __("Show Variant Attributes"), + fieldtype: "Check", }, { - "fieldname": 'show_stock_ageing_data', - "label": __('Show Stock Ageing Data'), - "fieldtype": 'Check' + fieldname: "show_stock_ageing_data", + label: __("Show Stock Ageing Data"), + fieldtype: "Check", }, { - "fieldname": 'ignore_closing_balance', - "label": __('Ignore Closing Balance'), - "fieldtype": 'Check', - "default": 1 + fieldname: "ignore_closing_balance", + label: __("Ignore Closing Balance"), + fieldtype: "Check", + default: 0, }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (column.fieldname == "out_qty" && data && data.out_qty > 0) { value = "" + value + ""; - } - else if (column.fieldname == "in_qty" && data && data.in_qty > 0) { + } else if (column.fieldname == "in_qty" && data && data.in_qty > 0) { value = "" + value + ""; } return value; - } + }, }; -erpnext.utils.add_inventory_dimensions('Stock Balance', 8); \ No newline at end of file +erpnext.utils.add_inventory_dimensions("Stock Balance", 8); diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 7a5a8615d0c..f0fddac2c64 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -3,12 +3,12 @@ from operator import itemgetter -from typing import Any, Dict, List, Optional, TypedDict +from typing import Any, TypedDict import frappe from frappe import _ from frappe.query_builder import Order -from frappe.query_builder.functions import Coalesce, CombineDatetime +from frappe.query_builder.functions import Coalesce from frappe.utils import add_days, cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of @@ -20,27 +20,27 @@ from erpnext.stock.utils import add_additional_uom_columns class StockBalanceFilter(TypedDict): - company: Optional[str] + company: str | None from_date: str to_date: str - item_group: Optional[str] - item: Optional[str] - warehouse: Optional[str] - warehouse_type: Optional[str] - include_uom: Optional[str] # include extra info in converted UOM + item_group: str | None + item: str | None + warehouse: str | None + warehouse_type: str | None + include_uom: str | None # include extra info in converted UOM show_stock_ageing_data: bool show_variant_attributes: bool -SLEntry = Dict[str, Any] +SLEntry = dict[str, Any] -def execute(filters: Optional[StockBalanceFilter] = None): +def execute(filters: StockBalanceFilter | None = None): return StockBalanceReport(filters).run() -class StockBalanceReport(object): - def __init__(self, filters: Optional[StockBalanceFilter]) -> None: +class StockBalanceReport: + def __init__(self, filters: StockBalanceFilter | None) -> None: self.filters = filters self.from_date = getdate(filters.get("from_date")) self.to_date = getdate(filters.get("to_date")) @@ -48,7 +48,7 @@ class StockBalanceReport(object): self.start_from = None self.data = [] self.columns = [] - self.sle_entries: List[SLEntry] = [] + self.sle_entries: list[SLEntry] = [] self.set_company_currency() def set_company_currency(self) -> None: @@ -90,8 +90,7 @@ class StockBalanceReport(object): self.opening_data.setdefault(group_by_key, entry) def prepare_new_data(self): - if not self.sle_entries: - return + self.item_warehouse_map = self.get_item_warehouse_map() if self.filters.get("show_stock_ageing_data"): self.filters["show_warehouse_wise_stock"] = True @@ -99,13 +98,13 @@ class StockBalanceReport(object): _func = itemgetter(1) - self.item_warehouse_map = self.get_item_warehouse_map() + del self.sle_entries variant_values = {} if self.filters.get("show_variant_attributes"): variant_values = self.get_variant_values_for() - for key, report_data in self.item_warehouse_map.items(): + for _key, report_data in self.item_warehouse_map.items(): if variant_data := variant_values.get(report_data.item_code): report_data.update(variant_data) @@ -139,15 +138,22 @@ class StockBalanceReport(object): item_warehouse_map = {} self.opening_vouchers = self.get_opening_vouchers() - for entry in self.sle_entries: - group_by_key = self.get_group_by_key(entry) - if group_by_key not in item_warehouse_map: - self.initialize_data(item_warehouse_map, group_by_key, entry) + if self.filters.get("show_stock_ageing_data"): + self.sle_entries = self.sle_query.run(as_dict=True) - self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) + with frappe.db.unbuffered_cursor(): + if not self.filters.get("show_stock_ageing_data"): + self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True) - if self.opening_data.get(group_by_key): - del self.opening_data[group_by_key] + for entry in self.sle_entries: + group_by_key = self.get_group_by_key(entry) + if group_by_key not in item_warehouse_map: + self.initialize_data(item_warehouse_map, group_by_key, entry) + + self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) + + if self.opening_data.get(group_by_key): + del self.opening_data[group_by_key] for group_by_key, entry in self.opening_data.items(): if group_by_key not in item_warehouse_map: @@ -178,7 +184,6 @@ class StockBalanceReport(object): qty_dict.opening_val += value_diff elif entry.posting_date >= self.from_date and entry.posting_date <= self.to_date: - if flt(qty_diff, self.float_precision) >= 0: qty_dict.in_qty += qty_diff qty_dict.in_val += value_diff @@ -224,7 +229,7 @@ class StockBalanceReport(object): return tuple(group_by_key) - def get_closing_balance(self) -> List[Dict[str, Any]]: + def get_closing_balance(self) -> list[dict[str, Any]]: if self.filters.get("ignore_closing_balance"): return [] @@ -236,7 +241,8 @@ class StockBalanceReport(object): .where( (table.docstatus == 1) & (table.company == self.filters.company) - & ((table.to_date <= self.from_date)) + & (table.to_date <= self.from_date) + & (table.status == "Completed") ) .orderby(table.to_date, order=Order.desc) .limit(1) @@ -276,7 +282,7 @@ class StockBalanceReport(object): item_table.item_name, ) .where((sle.docstatus < 2) & (sle.is_cancelled == 0)) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.posting_datetime) .orderby(sle.creation) .orderby(sle.actual_qty) ) @@ -289,7 +295,7 @@ class StockBalanceReport(object): if self.filters.get("company"): query = query.where(sle.company == self.filters.get("company")) - self.sle_entries = query.run(as_dict=True) + self.sle_query = query def apply_inventory_dimensions_filters(self, query, sle) -> str: inventory_dimension_fields = self.get_inventory_dimension_fields() @@ -318,7 +324,7 @@ class StockBalanceReport(object): def apply_items_filters(self, query, item_table) -> str: if item_group := self.filters.get("item_group"): children = get_descendants_of("Item Group", item_group, ignore_permissions=True) - query = query.where(item_table.item_group.isin(children + [item_group])) + query = query.where(item_table.item_group.isin([*children, item_group])) for field in ["item_code", "brand"]: if not self.filters.get(field): @@ -535,7 +541,9 @@ class StockBalanceReport(object): frappe.qb.from_(sr) .select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type")) .where( - (sr.docstatus == 1) & (sr.posting_date <= self.to_date) & (sr.purpose == "Opening Stock") + (sr.docstatus == 1) + & (sr.posting_date <= self.to_date) + & (sr.purpose == "Opening Stock") ) ) ).select("voucher_type", "name") @@ -561,7 +569,7 @@ class StockBalanceReport(object): def filter_items_with_no_transactions( - iwb_map, float_precision: float, inventory_dimensions: list = None + iwb_map, float_precision: float, inventory_dimensions: list | None = None ): pop_keys = [] for group_by_key in iwb_map: @@ -598,6 +606,6 @@ def filter_items_with_no_transactions( return iwb_map -def get_variants_attributes() -> List[str]: +def get_variants_attributes() -> list[str]: """Return all item variant attributes.""" return frappe.get_all("Item Attribute", pluck="name") diff --git a/erpnext/stock/report/stock_balance/test_stock_balance.py b/erpnext/stock/report/stock_balance/test_stock_balance.py index e963de293ab..8b3dbee7c8d 100644 --- a/erpnext/stock/report/stock_balance/test_stock_balance.py +++ b/erpnext/stock/report/stock_balance/test_stock_balance.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any import frappe from frappe import _dict @@ -32,12 +32,11 @@ class TestStockBalance(FrappeTestCase): def tearDown(self): frappe.db.rollback() - def assertPartialDictEq(self, expected: Dict[str, Any], actual: Dict[str, Any]): + def assertPartialDictEq(self, expected: dict[str, Any], actual: dict[str, Any]): for k, v in expected.items(): self.assertEqual(v, actual[k], msg=f"{expected=}\n{actual=}") def generate_stock_ledger(self, item_code: str, movements): - for movement in map(_dict, movements): if "to_warehouse" not in movement: movement.to_warehouse = "_Test Warehouse - _TC" @@ -128,7 +127,6 @@ class TestStockBalance(FrappeTestCase): self.assertPartialDictEq({"opening_qty": 6, "in_qty": 0}, rows[0]) def test_uom_converted_info(self): - self.item.append("uoms", {"conversion_factor": 5, "uom": "Box"}) self.item.save() @@ -167,8 +165,6 @@ class TestStockBalance(FrappeTestCase): variant.save() self.generate_stock_ledger(variant.name, [_dict(qty=5, rate=10)]) - rows = stock_balance( - self.filters.update({"show_variant_attributes": 1, "item_code": variant.name}) - ) + rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": variant.name})) self.assertPartialDictEq(attributes, rows[0]) self.assertInvariants(rows) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index b00b422a67a..2c670bee9c1 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -2,102 +2,101 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Stock Ledger"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - "reqd": 1 + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1 + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, }, { - "fieldname":"warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse", - "get_query": function() { - const company = frappe.query_report.get_filter_value('company'); + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + get_query: function () { + const company = frappe.query_report.get_filter_value("company"); return { - filters: { 'company': company } - } - } + filters: { company: company }, + }; + }, }, { - "fieldname":"item_code", - "label": __("Item"), - "fieldtype": "Link", - "options": "Item", - "get_query": function() { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + get_query: function () { return { - query: "erpnext.controllers.queries.item_query" - } - } + query: "erpnext.controllers.queries.item_query", + }; + }, }, { - "fieldname":"item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "options": "Item Group" + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", }, { - "fieldname":"batch_no", - "label": __("Batch No"), - "fieldtype": "Link", - "options": "Batch" + fieldname: "batch_no", + label: __("Batch No"), + fieldtype: "Link", + options: "Batch", }, { - "fieldname":"brand", - "label": __("Brand"), - "fieldtype": "Link", - "options": "Brand" + fieldname: "brand", + label: __("Brand"), + fieldtype: "Link", + options: "Brand", }, { - "fieldname":"voucher_no", - "label": __("Voucher #"), - "fieldtype": "Data" + fieldname: "voucher_no", + label: __("Voucher #"), + fieldtype: "Data", }, { - "fieldname":"project", - "label": __("Project"), - "fieldtype": "Link", - "options": "Project" + fieldname: "project", + label: __("Project"), + fieldtype: "Link", + options: "Project", }, { - "fieldname":"include_uom", - "label": __("Include UOM"), - "fieldtype": "Link", - "options": "UOM" + fieldname: "include_uom", + label: __("Include UOM"), + fieldtype: "Link", + options: "UOM", }, { - "fieldname": "valuation_field_type", - "label": __("Valuation Field Type"), - "fieldtype": "Select", - "width": "80", - "options": "Currency\nFloat", - "default": "Currency" + fieldname: "valuation_field_type", + label: __("Valuation Field Type"), + fieldtype: "Select", + width: "80", + options: "Currency\nFloat", + default: "Currency", }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (column.fieldname == "out_qty" && data && data.out_qty < 0) { value = "" + value + ""; - } - else if (column.fieldname == "in_qty" && data && data.in_qty > 0) { + } else if (column.fieldname == "in_qty" && data && data.in_qty > 0) { value = "" + value + ""; } @@ -105,4 +104,4 @@ frappe.query_reports["Stock Ledger"] = { }, }; -erpnext.utils.add_inventory_dimensions('Stock Ledger', 10); \ No newline at end of file +erpnext.utils.add_inventory_dimensions("Stock Ledger", 10); diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index eeef39641b0..a9f74149d51 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -276,7 +276,7 @@ def get_stock_ledger_entries(filters, items): frappe.qb.from_(sle) .select( sle.item_code, - CombineDatetime(sle.posting_date, sle.posting_time).as_("date"), + sle.posting_datetime.as_("date"), sle.warehouse, sle.posting_date, sle.posting_time, @@ -437,11 +437,8 @@ def get_opening_balance(filters, columns, sl_entries): def get_warehouse_condition(warehouse): warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) if warehouse_details: - return ( - " exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and warehouse = wh.name)" - % (warehouse_details.lft, warehouse_details.rgt) - ) + return f" exists (select name from `tabWarehouse` wh \ + where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)" return "" @@ -452,22 +449,17 @@ def get_item_group_condition(item_group, item_table=None): if item_table: ig = frappe.qb.DocType("Item Group") return item_table.item_group.isin( - ( - frappe.qb.from_(ig) - .select(ig.name) - .where( - (ig.lft >= item_group_details.lft) - & (ig.rgt <= item_group_details.rgt) - & (item_table.item_group == ig.name) - ) + frappe.qb.from_(ig) + .select(ig.name) + .where( + (ig.lft >= item_group_details.lft) + & (ig.rgt <= item_group_details.rgt) + & (item_table.item_group == ig.name) ) ) else: - return ( - "item.item_group in (select ig.name from `tabItem Group` ig \ - where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)" - % (item_group_details.lft, item_group_details.rgt) - ) + return f"item.item_group in (select ig.name from `tabItem Group` ig \ + where ig.lft >= {item_group_details.lft} and ig.rgt <= {item_group_details.rgt} and item.item_group = ig.name)" def check_inventory_dimension_filters_applied(filters) -> bool: diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js index 94e0b2dce3b..166837dcc78 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js @@ -3,42 +3,42 @@ /* eslint-disable */ const DIFFERENCE_FIELD_NAMES = [ - 'difference_in_qty', - 'fifo_qty_diff', - 'fifo_value_diff', - 'fifo_valuation_diff', - 'valuation_diff', - 'fifo_difference_diff', - 'diff_value_diff' + "difference_in_qty", + "fifo_qty_diff", + "fifo_value_diff", + "fifo_valuation_diff", + "valuation_diff", + "fifo_difference_diff", + "diff_value_diff", ]; -frappe.query_reports['Stock Ledger Invariant Check'] = { - 'filters': [ +frappe.query_reports["Stock Ledger Invariant Check"] = { + filters: [ { - 'fieldname': 'item_code', - 'fieldtype': 'Link', - 'label': 'Item', - 'mandatory': 1, - 'options': 'Item', - get_query: function() { + fieldname: "item_code", + fieldtype: "Link", + label: "Item", + mandatory: 1, + options: "Item", + get_query: function () { return { - filters: {is_stock_item: 1, has_serial_no: 0} - } - } + filters: { is_stock_item: 1, has_serial_no: 0 }, + }; + }, }, { - 'fieldname': 'warehouse', - 'fieldtype': 'Link', - 'label': 'Warehouse', - 'mandatory': 1, - 'options': 'Warehouse', - } + fieldname: "warehouse", + fieldtype: "Link", + label: "Warehouse", + mandatory: 1, + options: "Warehouse", + }, ], - formatter (value, row, column, data, default_formatter) { + formatter(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (DIFFERENCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) { - value = '' + value + ''; + value = '' + value + ""; } return value; }, @@ -50,7 +50,7 @@ frappe.query_reports['Stock Ledger Invariant Check'] = { }, onload(report) { - report.page.add_inner_button(__('Create Reposting Entry'), () => { + report.page.add_inner_button(__("Create Reposting Entry"), () => { let message = `

              @@ -62,23 +62,21 @@ frappe.query_reports['Stock Ledger Invariant Check'] = {

              Are you sure you want to create a Reposting Entry?

              `; let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); - let selected_rows = indexes.map(i => frappe.query_report.data[i]); + let selected_rows = indexes.map((i) => frappe.query_report.data[i]); if (!selected_rows.length) { - frappe.throw(__('Please select a row to create a Reposting Entry')); - } - else if (selected_rows.length > 1) { - frappe.throw(__('Please select only one row to create a Reposting Entry')); - } - else { + frappe.throw(__("Please select a row to create a Reposting Entry")); + } else if (selected_rows.length > 1) { + frappe.throw(__("Please select only one row to create a Reposting Entry")); + } else { frappe.confirm(__(message), () => { frappe.call({ - method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries', + method: "erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries", args: { rows: selected_rows, item_code: frappe.query_report.get_filter_values().item_code, warehouse: frappe.query_report.get_filter_values().warehouse, - } + }, }); }); } diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js index bf3a397feef..07e7b59b514 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js @@ -8,60 +8,55 @@ const DIFFERENCE_FIELD_NAMES = [ "fifo_valuation_diff", "valuation_diff", "fifo_difference_diff", - "diff_value_diff" + "diff_value_diff", ]; frappe.query_reports["Stock Ledger Variance"] = { - "filters": [ + filters: [ { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname": "item_code", - "fieldtype": "Link", - "label": __("Item"), - "options": "Item", - get_query: function() { + fieldname: "item_code", + fieldtype: "Link", + label: __("Item"), + options: "Item", + get_query: function () { return { - filters: {is_stock_item: 1, has_serial_no: 0} - } - } + filters: { is_stock_item: 1, has_serial_no: 0 }, + }; + }, }, { - "fieldname": "warehouse", - "fieldtype": "Link", - "label": __("Warehouse"), - "options": "Warehouse", - get_query: function() { + fieldname: "warehouse", + fieldtype: "Link", + label: __("Warehouse"), + options: "Warehouse", + get_query: function () { return { - filters: {is_group: 0, disabled: 0} - } - } + filters: { is_group: 0, disabled: 0 }, + }; + }, }, { - "fieldname": "difference_in", - "fieldtype": "Select", - "label": __("Difference In"), - "options": [ - "", - "Qty", - "Value", - "Valuation", - ], + fieldname: "difference_in", + fieldtype: "Select", + label: __("Difference In"), + options: ["", "Qty", "Value", "Valuation"], }, { - "fieldname": "include_disabled", - "fieldtype": "Check", - "label": __("Include Disabled"), - } + fieldname: "include_disabled", + fieldtype: "Check", + label: __("Include Disabled"), + }, ], - formatter (value, row, column, data, default_formatter) { + formatter(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (DIFFERENCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) { @@ -78,7 +73,7 @@ frappe.query_reports["Stock Ledger Variance"] = { }, onload(report) { - report.page.add_inner_button(__('Create Reposting Entries'), () => { + report.page.add_inner_button(__("Create Reposting Entries"), () => { let message = `

              @@ -90,7 +85,7 @@ frappe.query_reports["Stock Ledger Variance"] = {

              Are you sure you want to create Reposting Entries?

              `; let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); - let selected_rows = indexes.map(i => frappe.query_report.data[i]); + let selected_rows = indexes.map((i) => frappe.query_report.data[i]); if (!selected_rows.length) { frappe.throw(__("Please select rows to create Reposting Entries")); @@ -98,10 +93,10 @@ frappe.query_reports["Stock Ledger Variance"] = { frappe.confirm(__(message), () => { frappe.call({ - method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries', + method: "erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries", args: { rows: selected_rows, - } + }, }); }); }); diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index 189a90aa471..0b7e551c86f 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -228,7 +228,7 @@ def get_data(filters=None): return data -def get_item_warehouse_combinations(filters: dict = None) -> dict: +def get_item_warehouse_combinations(filters: dict | None = None) -> dict: filters = frappe._dict(filters or {}) bin = frappe.qb.DocType("Bin") @@ -284,7 +284,5 @@ def has_difference(row, precision, difference_in, valuation_method): return True elif difference_in == "Valuation" and valuation_diff: return True - elif difference_in not in ["Qty", "Value", "Valuation"] and ( - qty_diff or value_diff or valuation_diff - ): + elif difference_in not in ["Qty", "Value", "Valuation"] and (qty_diff or value_diff or valuation_diff): return True diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.js b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.js index cb109f8050d..6e333aadfa5 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.js +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.js @@ -2,55 +2,55 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Stock Projected Qty"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse", - "get_query": () => { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + get_query: () => { return { filters: { - company: frappe.query_report.get_filter_value('company') - } - } - } + company: frappe.query_report.get_filter_value("company"), + }, + }; + }, }, { - "fieldname":"item_code", - "label": __("Item"), - "fieldtype": "Link", - "options": "Item", - "get_query": function() { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + get_query: function () { return { - query: "erpnext.controllers.queries.item_query" - } - } + query: "erpnext.controllers.queries.item_query", + }; + }, }, { - "fieldname":"item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "options": "Item Group" + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", }, { - "fieldname":"brand", - "label": __("Brand"), - "fieldtype": "Link", - "options": "Brand" + fieldname: "brand", + label: __("Brand"), + fieldtype: "Link", + options: "Brand", }, { - "fieldname":"include_uom", - "label": __("Include UOM"), - "fieldtype": "Link", - "options": "UOM" - } - ] -} + fieldname: "include_uom", + label: __("Include UOM"), + fieldtype: "Link", + options: "UOM", + }, + ], +}; diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 31c756da822..743656c6472 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -250,9 +250,7 @@ def get_bin_list(filters): query = query.where(bin.item_code == filters.item_code) if filters.warehouse: - warehouse_details = frappe.db.get_value( - "Warehouse", filters.warehouse, ["lft", "rgt"], as_dict=1 - ) + warehouse_details = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"], as_dict=1) if warehouse_details: wh = frappe.qb.DocType("Warehouse") @@ -286,7 +284,9 @@ def get_item_map(item_code, include_uom): (item.is_stock_item == 1) & (item.disabled == 0) & ( - (item.end_of_life > today()) | (item.end_of_life.isnull()) | (item.end_of_life == "0000-00-00") + (item.end_of_life > today()) + | (item.end_of_life.isnull()) + | (item.end_of_life == "0000-00-00") ) & (ExistsCriterion(frappe.qb.from_(bin).select(bin.name).where(bin.item_code == item.name))) ) diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js index 2a0fd4025cc..b0f5f5ccd5a 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js @@ -3,40 +3,39 @@ /* eslint-disable */ frappe.query_reports["Stock Qty vs Serial No Count"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { - "fieldname":"warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "options": "Warehouse", - "get_query": function() { - const company = frappe.query_report.get_filter_value('company'); + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + get_query: function () { + const company = frappe.query_report.get_filter_value("company"); return { - filters: { 'company': company } - } + filters: { company: company }, + }; }, - "reqd": 1 + reqd: 1, }, ], - "formatter": function (value, row, column, data, default_formatter) { + formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (column.fieldname == "difference" && data) { if (data.difference > 0) { value = "" + value + ""; - } - else if (data.difference < 0) { + } else if (data.difference < 0) { value = "" + value + ""; } } return value; - } + }, }; diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.js b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.js index 5b006470756..92a01c4ce20 100644 --- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.js +++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.js @@ -2,27 +2,27 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Supplier-Wise Sales Analytics"] = { - "filters": [ + filters: [ { - "fieldname":"supplier", - "label": __("Supplier"), - "fieldtype": "Link", - "options": "Supplier", - "width": "80" + fieldname: "supplier", + label: __("Supplier"), + fieldtype: "Link", + options: "Supplier", + width: "80", }, { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "width": "80", - "default": frappe.datetime.month_start() + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + width: "80", + default: frappe.datetime.month_start(), }, { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "width": "80", - "default": frappe.datetime.month_end() + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + width: "80", + default: frappe.datetime.month_end(), }, - ] -} + ], +}; diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py index 8c76908cc3c..e28b2d79228 100644 --- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py +++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py @@ -20,7 +20,6 @@ def execute(filters=None): total_qty = total_amount = 0.0 if consumed_details.get(item_code): for cd in consumed_details.get(item_code): - if cd.voucher_no not in material_transfer_vouchers: if cd.voucher_type in ["Delivery Note", "Sales Invoice"]: delivered_qty += abs(flt(cd.actual_qty)) @@ -54,19 +53,19 @@ def execute(filters=None): def get_columns(filters): """return columns based on filters""" - columns = ( - [_("Item") + ":Link/Item:100"] - + [_("Item Name") + "::100"] - + [_("Description") + "::150"] - + [_("UOM") + ":Link/UOM:90"] - + [_("Consumed Qty") + ":Float:110"] - + [_("Consumed Amount") + ":Currency:130"] - + [_("Delivered Qty") + ":Float:110"] - + [_("Delivered Amount") + ":Currency:130"] - + [_("Total Qty") + ":Float:110"] - + [_("Total Amount") + ":Currency:130"] - + [_("Supplier(s)") + "::250"] - ) + columns = [ + _("Item") + ":Link/Item:100", + _("Item Name") + "::100", + _("Description") + "::150", + _("UOM") + ":Link/UOM:90", + _("Consumed Qty") + ":Float:110", + _("Consumed Amount") + ":Currency:130", + _("Delivered Qty") + ":Float:110", + _("Delivered Amount") + ":Currency:130", + _("Total Qty") + ":Float:110", + _("Total Amount") + ":Currency:130", + _("Supplier(s)") + "::250", + ] return columns @@ -173,9 +172,7 @@ def get_suppliers_details(filters): def get_material_transfer_vouchers(): se = frappe.qb.DocType("Stock Entry") query = ( - frappe.qb.from_(se) - .select(se.name) - .where((se.purpose == "Material Transfer") & (se.docstatus == 1)) + frappe.qb.from_(se).select(se.name).where((se.purpose == "Material Transfer") & (se.docstatus == 1)) ) return [r[0] for r in query.run()] diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index d118d8e5694..74c6afa204b 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -1,5 +1,4 @@ import unittest -from typing import List, Tuple import frappe @@ -14,7 +13,7 @@ DEFAULT_FILTERS = { batch = frappe.db.get_value("Batch", fieldname=["name"], as_dict=True, order_by="creation desc") -REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ +REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("Stock Ledger", {"_optional": True}), ("Stock Ledger", {"batch_no": batch}), ("Stock Ledger", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}), diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.js b/erpnext/stock/report/total_stock_summary/total_stock_summary.js index 88054aaea73..78ab31a7df7 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.js +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.js @@ -3,25 +3,25 @@ /* eslint-disable */ frappe.query_reports["Total Stock Summary"] = { - "filters": [ + filters: [ { - "fieldname":"group_by", - "label": __("Group By"), - "fieldtype": "Select", - "width": "80", - "reqd": 1, - "options": ["Warehouse", "Company"], - "default": "Warehouse", + fieldname: "group_by", + label: __("Group By"), + fieldtype: "Select", + width: "80", + reqd: 1, + options: ["Warehouse", "Company"], + default: "Warehouse", }, { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "width": "80", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company"), - "depends_on": "eval: doc.group_by != 'Company'", + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + width: "80", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), + depends_on: "eval: doc.group_by != 'Company'", }, - ] -} + ], +}; diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.py b/erpnext/stock/report/total_stock_summary/total_stock_summary.py index c3155bd1a5e..4848c216e81 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.py +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.py @@ -8,7 +8,6 @@ from frappe.query_builder.functions import Sum def execute(filters=None): - if not filters: filters = {} columns = get_columns(filters) @@ -54,8 +53,8 @@ def get_total_stock(filters): else: query = query.select(wh.company).groupby(wh.company) - query = query.select( - item.item_code, item.description, Sum(bin.actual_qty).as_("actual_qty") - ).groupby(item.item_code) + query = query.select(item.item_code, item.description, Sum(bin.actual_qty).as_("actual_qty")).groupby( + item.item_code + ) return query.run() diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js index 39cfd7274f2..8a0c1a7af6d 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js @@ -3,49 +3,49 @@ /* eslint-disable */ frappe.query_reports["Warehouse wise Item Balance Age and Value"] = { - "filters": [ -{ - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - }, - { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "width": "80", - "reqd": 1, - "default": frappe.datetime.get_today() - }, - { - "fieldname": "item_group", - "label": __("Item Group"), - "fieldtype": "Link", - "width": "80", - "options": "Item Group" - }, - { - "fieldname": "item_code", - "label": __("Item"), - "fieldtype": "Link", - "width": "80", - "options": "Item" - }, - { - "fieldname": "warehouse", - "label": __("Warehouse"), - "fieldtype": "Link", - "width": "80", - "options": "Warehouse" - }, - { - "fieldname": "filter_total_zero_qty", - "label": __("Filter Total Zero Qty"), - "fieldtype": "Check", - "default": 1 - }, - ] -} + filters: [ + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.get_today(), + }, + { + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + width: "80", + options: "Item Group", + }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + width: "80", + options: "Item", + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + width: "80", + options: "Warehouse", + }, + { + fieldname: "filter_total_zero_qty", + label: __("Filter Total Zero Qty"), + fieldtype: "Check", + default: 1, + }, + ], +}; diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index 5dbdceff247..e1cce31329e 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -40,7 +40,7 @@ def execute(filters=None): item_balance = {} item_value = {} - for (company, item, warehouse) in sorted(iwb_map): + for company, item, warehouse in sorted(iwb_map): if not item_map.get(item): continue @@ -71,7 +71,7 @@ def execute(filters=None): row += [average_age] - bal_qty = [sum(bal_qty) for bal_qty in zip(*wh_balance)] + bal_qty = [sum(bal_qty) for bal_qty in zip(*wh_balance, strict=False)] total_qty = sum(bal_qty) if len(warehouse_list) > 1: row += [total_qty] diff --git a/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.js b/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.js index 752e464e27c..808fe29237f 100644 --- a/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.js +++ b/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.js @@ -3,25 +3,24 @@ /* eslint-disable */ frappe.query_reports["Warehouse Wise Stock Balance"] = { - "filters": [ + filters: [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), }, { - "fieldname":"show_disabled_warehouses", - "label": __("Show Disabled Warehouses"), - "fieldtype": "Check", - "default": 0 - - } + fieldname: "show_disabled_warehouses", + label: __("Show Disabled Warehouses"), + fieldtype: "Check", + default: 0, + }, ], - "initial_depth": 3, - "tree": true, - "parent_field": "parent_warehouse", - "name_field": "warehouse" + initial_depth: 3, + tree: true, + parent_field: "parent_warehouse", + name_field: "warehouse", }; diff --git a/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.py b/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.py index a0e994482f8..39baa548daf 100644 --- a/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.py +++ b/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.py @@ -1,7 +1,7 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from typing import Any, Dict, List, Optional, TypedDict +from typing import Any, TypedDict import frappe from frappe import _ @@ -9,12 +9,12 @@ from frappe.query_builder.functions import Sum class StockBalanceFilter(TypedDict): - company: Optional[str] - warehouse: Optional[str] - show_disabled_warehouses: Optional[int] + company: str | None + warehouse: str | None + show_disabled_warehouses: int | None -SLEntry = Dict[str, Any] +SLEntry = dict[str, Any] def execute(filters=None): @@ -25,7 +25,7 @@ def execute(filters=None): return columns, data -def get_warehouse_wise_balance(filters: StockBalanceFilter) -> List[SLEntry]: +def get_warehouse_wise_balance(filters: StockBalanceFilter) -> list[SLEntry]: sle = frappe.qb.DocType("Stock Ledger Entry") query = ( @@ -95,7 +95,7 @@ def set_balance_in_parent(warehouses): update_balance(warehouse, warehouse.stock_balance) -def get_columns(filters: StockBalanceFilter) -> List[Dict]: +def get_columns(filters: StockBalanceFilter) -> list[dict]: columns = [ { "label": _("Warehouse"), diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 439ed7a8e09..05d21950ad2 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -15,9 +15,7 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, frappe.db.auto_commit_on_many_writes = 1 if allow_negative_stock: - existing_allow_negative_stock = frappe.db.get_value( - "Stock Settings", None, "allow_negative_stock" - ) + existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) item_warehouses = frappe.db.sql( @@ -37,9 +35,7 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, frappe.db.rollback() if allow_negative_stock: - frappe.db.set_value( - "Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock - ) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) frappe.db.auto_commit_on_many_writes = 0 @@ -51,7 +47,6 @@ def repost_stock( only_bin=False, allow_negative_stock=False, ): - if not only_bin: repost_actual_qty(item_code, warehouse, allow_zero_rate, allow_negative_stock) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8a262497926..99930768ea5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1,21 +1,21 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import copy import json -from typing import Optional, Set, Tuple import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.query_builder.functions import CombineDatetime, Sum +from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.utils import ( + get_combine_datetime, get_incoming_outgoing_rate_for_cancel, + get_incoming_rate, get_or_make_bin, get_valuation_method, ) @@ -26,10 +26,6 @@ class NegativeStockError(frappe.ValidationError): pass -class SerialNoExistsInFutureTransaction(frappe.ValidationError): - pass - - def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): """Create SL entries from SL entry dicts @@ -53,9 +49,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc future_sle_exists(args, sl_entries) for sle in sl_entries: - if sle.serial_no and not via_landed_cost_voucher: - validate_serial_no(sle) - if cancel: sle["actual_qty"] = -flt(sle.get("actual_qty")) @@ -76,6 +69,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc args = sle_doc.as_dict() args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False + args["posting_datetime"] = get_combine_datetime(args.posting_date, args.posting_time) if sle.get("voucher_type") == "Stock Reconciliation": # preserve previous_qty_after_transaction for qty reposting @@ -132,35 +126,6 @@ def get_args_for_future_sle(row): ) -def validate_serial_no(sle): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - - for sn in get_serial_nos(sle.serial_no): - args = copy.deepcopy(sle) - args.serial_no = sn - args.warehouse = "" - - vouchers = [] - for row in get_stock_ledger_entries(args, ">"): - voucher_type = frappe.bold(row.voucher_type) - voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no)) - vouchers.append(f"{voucher_type} {voucher_no}") - - if vouchers: - serial_no = frappe.bold(sn) - msg = ( - f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first. - The list of the transactions are as below.""" - + "

              • " - ) - - msg += "
              • ".join(vouchers) - msg += "
              " - - title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel" - frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction) - - def validate_cancellation(args): if args[0].get("is_cancelled"): repost_entry = frappe.db.get_value( @@ -219,9 +184,7 @@ def repost_future_sle( if not args: args = [] # set args to empty list if None to avoid enumerate error - items_to_be_repost = get_items_to_be_repost( - voucher_type=voucher_type, voucher_no=voucher_no, doc=doc - ) + items_to_be_repost = get_items_to_be_repost(voucher_type=voucher_type, voucher_no=voucher_no, doc=doc) if items_to_be_repost: args = items_to_be_repost @@ -246,12 +209,10 @@ def repost_future_sle( ) affected_transactions.update(obj.affected_transactions) - distinct_item_warehouses[ - (args[i].get("item_code"), args[i].get("warehouse")) - ].reposting_status = True + distinct_item_warehouses[(args[i].get("item_code"), args[i].get("warehouse"))].reposting_status = True if obj.new_items_found: - for item_wh, data in distinct_item_warehouses.items(): + for _item_wh, data in distinct_item_warehouses.items(): if ("args_idx" not in data and not data.reposting_status) or ( data.sle_changed and data.reposting_status ): @@ -276,9 +237,7 @@ def validate_item_warehouse(args): frappe.throw(_(validation_msg)) -def update_args_in_repost_item_valuation( - doc, index, args, distinct_item_warehouses, affected_transactions -): +def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses, affected_transactions): doc.db_set( { "items_to_be_repost": json.dumps(args, default=str), @@ -334,7 +293,7 @@ def get_distinct_item_warehouse(args=None, doc=None): return distinct_item_warehouses -def get_affected_transactions(doc) -> Set[Tuple[str, str]]: +def get_affected_transactions(doc) -> set[tuple[str, str]]: if not doc.affected_transactions: return set() @@ -347,7 +306,7 @@ def get_current_index(doc=None): return doc.current_index -class update_entries_after(object): +class update_entries_after: """ update valution rate and qty after transaction from the current time-bucket onwards @@ -389,7 +348,7 @@ class update_entries_after(object): self.new_items_found = False self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) - self.affected_transactions: Set[Tuple[str, str]] = set() + self.affected_transactions: set[tuple[str, str]] = set() self.data = frappe._dict() self.initialize_previous_data(self.args) @@ -467,12 +426,12 @@ class update_entries_after(object): self.process_sle(sle) def get_sle_against_current_voucher(self): - self.args["time_format"] = "%H:%i:%s" + self.args["posting_datetime"] = get_combine_datetime(self.args.posting_date, self.args.posting_time) return frappe.db.sql( """ select - *, timestamp(posting_date, posting_time) as "timestamp" + *, posting_datetime as "timestamp" from `tabStock Ledger Entry` where @@ -480,8 +439,7 @@ class update_entries_after(object): and warehouse = %(warehouse)s and is_cancelled = 0 and ( - posting_date = %(posting_date)s and - time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s) + posting_datetime = %(posting_datetime)s ) order by creation ASC @@ -506,9 +464,7 @@ class update_entries_after(object): if not dependant_sle: return entries_to_fix - elif ( - dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse - ): + elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: return entries_to_fix elif dependant_sle.item_code != self.item_code: self.update_distinct_item_warehouses(dependant_sle) @@ -528,9 +484,7 @@ class update_entries_after(object): self.distinct_item_warehouses[key] = val self.new_items_found = True else: - existing_sle_posting_date = ( - self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") - ) + 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) @@ -572,7 +526,12 @@ class update_entries_after(object): if not self.args.get("sle_id"): self.get_dynamic_incoming_outgoing_rate(sle) - if sle.voucher_type == "Stock Reconciliation" and sle.batch_no and sle.voucher_detail_no: + if ( + sle.voucher_type == "Stock Reconciliation" + and not self.args.get("sle_id") + and sle.voucher_detail_no + and (sle.batch_no or sle.serial_no) + ): self.reset_actual_qty_for_stock_reco(sle) if ( @@ -612,7 +571,9 @@ class update_entries_after(object): self.wh_data.valuation_rate ) if self.valuation_method != "Moving Average": - self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]] + self.wh_data.stock_queue = [ + [self.wh_data.qty_after_transaction, self.wh_data.valuation_rate] + ] else: if self.valuation_method == "Moving Average": self.get_moving_average_values(sle) @@ -650,11 +611,52 @@ class update_entries_after(object): doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty >= 0) if sle.actual_qty < 0: - sle.actual_qty = ( - flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty")) - * -1 + stock_reco_details = frappe.db.get_value( + "Stock Reconciliation Item", + sle.voucher_detail_no, + ["current_qty", "current_serial_no as sn_no"], + as_dict=True, ) + sle.actual_qty = flt(stock_reco_details.current_qty) * -1 + + if stock_reco_details.sn_no: + sle.serial_no = stock_reco_details.sn_no + sle.qty_after_transaction = 0.0 + + if sle.serial_no: + self.update_serial_no_status(sle) + + def update_serial_no_status(self, sle): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + serial_nos = get_serial_nos(sle.serial_no) + warehouse = None + status = "Delivered" + if sle.actual_qty > 0: + warehouse = sle.warehouse + status = "Active" + + sn_table = frappe.qb.DocType("Serial No") + + query = ( + frappe.qb.update(sn_table) + .set(sn_table.warehouse, warehouse) + .set(sn_table.status, status) + .where(sn_table.name.isin(serial_nos)) + ) + + if sle.actual_qty > 0: + query = query.set(sn_table.purchase_document_type, sle.voucher_type) + query = query.set(sn_table.purchase_document_no, sle.voucher_no) + query = query.set(sn_table.delivery_document_type, None) + query = query.set(sn_table.delivery_document_no, None) + else: + query = query.set(sn_table.delivery_document_type, sle.voucher_type) + query = query.set(sn_table.delivery_document_no, sle.voucher_no) + + query.run() + def validate_negative_stock(self, sle): """ validate negative stock for entries current datetime onwards @@ -701,7 +703,23 @@ class update_entries_after(object): ) if self.valuation_method == "Moving Average": - rate = flt(self.data[self.args.warehouse].previous_sle.valuation_rate) + rate = get_incoming_rate( + { + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.posting_date, + "posting_time": sle.posting_time, + "qty": sle.actual_qty, + "serial_no": sle.get("serial_no"), + "batch_no": sle.get("batch_no"), + "company": sle.company, + "voucher_type": sle.voucher_type, + "voucher_no": sle.voucher_no, + "allow_zero_valuation": self.allow_zero_rate, + "sle": sle.name, + } + ) + else: rate = get_rate_for_return( sle.voucher_type, @@ -898,9 +916,7 @@ class update_entries_after(object): self.wh_data.valuation_rate = new_stock_value / new_stock_qty if not self.wh_data.valuation_rate and sle.voucher_detail_no: - allow_zero_rate = self.check_if_allow_zero_valuation_rate( - sle.voucher_type, sle.voucher_detail_no - ) + allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_rate: self.wh_data.valuation_rate = self.get_fallback_rate(sle) @@ -1015,9 +1031,7 @@ class update_entries_after(object): stock_value_difference = stock_value - prev_stock_value self.wh_data.stock_queue = stock_queue.state - self.wh_data.stock_value = round_off_if_near_zero( - self.wh_data.stock_value + stock_value_difference - ) + self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference) if not self.wh_data.stock_queue: self.wh_data.stock_queue.append( @@ -1054,9 +1068,7 @@ class update_entries_after(object): outgoing_rate = self.get_fallback_rate(sle) stock_value_difference = outgoing_rate * actual_qty - self.wh_data.stock_value = round_off_if_near_zero( - self.wh_data.stock_value + stock_value_difference - ) + self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference) if self.wh_data.qty_after_transaction: self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction @@ -1106,7 +1118,6 @@ class update_entries_after(object): exceptions[0]["voucher_type"], exceptions[0]["voucher_no"], ) in frappe.local.flags.currently_saving: - msg = _("{0} units of {1} needed in {2} to complete this transaction.").format( abs(deficiency), frappe.get_desk_link("Item", exceptions[0]["item_code"]), @@ -1160,11 +1171,11 @@ class update_entries_after(object): def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False): """get stock ledger entries filtered by specific posting datetime conditions""" - args["time_format"] = "%H:%i:%s" if not args.get("posting_date"): - args["posting_date"] = "1900-01-01" - if not args.get("posting_time"): - args["posting_time"] = "00:00" + args["posting_datetime"] = "1900-01-01 00:00:00" + + if not args.get("posting_datetime"): + args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"]) voucher_condition = "" if exclude_current_voucher: @@ -1172,25 +1183,19 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc voucher_condition = f"and voucher_no != '{voucher_no}'" sle = frappe.db.sql( - """ - select *, timestamp(posting_date, posting_time) as "timestamp" + f""" + select *, posting_datetime as "timestamp" from `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s and is_cancelled = 0 {voucher_condition} and ( - posting_date < %(posting_date)s or - ( - posting_date = %(posting_date)s and - time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s) - ) + posting_datetime {operator} %(posting_datetime)s ) - order by timestamp(posting_date, posting_time) desc, creation desc + order by posting_datetime desc, creation desc limit 1 - for update""".format( - operator=operator, voucher_condition=voucher_condition - ), + for update""", args, as_dict=1, ) @@ -1230,9 +1235,7 @@ def get_stock_ledger_entries( extra_cond=None, ): """get stock ledger entries filtered by specific posting datetime conditions""" - conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format( - operator - ) + conditions = f" and posting_datetime {operator} %(posting_datetime)s" if previous_sle.get("warehouse"): conditions += " and warehouse = %(warehouse)s" elif previous_sle.get("warehouse_condition"): @@ -1244,45 +1247,49 @@ def get_stock_ledger_entries( conditions += ( """ and ( - serial_no = {0} - or serial_no like {1} - or serial_no like {2} - or serial_no like {3} + serial_no = {} + or serial_no like {} + or serial_no like {} + or serial_no like {} ) """ ).format( frappe.db.escape(serial_no), - frappe.db.escape("{}\n%".format(serial_no)), - frappe.db.escape("%\n{}".format(serial_no)), - frappe.db.escape("%\n{}\n%".format(serial_no)), + frappe.db.escape(f"{serial_no}\n%"), + frappe.db.escape(f"%\n{serial_no}"), + frappe.db.escape(f"%\n{serial_no}\n%"), ) if not previous_sle.get("posting_date"): - previous_sle["posting_date"] = "1900-01-01" - if not previous_sle.get("posting_time"): - previous_sle["posting_time"] = "00:00" + previous_sle["posting_datetime"] = "1900-01-01 00:00:00" + else: + previous_sle["posting_datetime"] = get_combine_datetime( + previous_sle["posting_date"], previous_sle["posting_time"] + ) if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" + if operator in (">", "<=") and previous_sle.get("voucher_no"): + conditions += " and voucher_no!=%(voucher_no)s" + if extra_cond: conditions += f"{extra_cond}" return frappe.db.sql( """ - select *, timestamp(posting_date, posting_time) as "timestamp" + select *, posting_datetime as "timestamp" from `tabStock Ledger Entry` - where item_code = %%(item_code)s + where item_code = %(item_code)s and is_cancelled = 0 - %(conditions)s - order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s - %(limit)s %(for_update)s""" - % { - "conditions": conditions, - "limit": limit or "", - "for_update": for_update and "for update" or "", - "order": order, - }, + {conditions} + order by posting_datetime {order}, creation {order} + {limit} {for_update}""".format( + conditions=conditions, + limit=limit or "", + for_update=for_update and "for update" or "", + order=order, + ), previous_sle, as_dict=1, debug=debug, @@ -1301,26 +1308,26 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): "posting_date", "posting_time", "voucher_detail_no", - "timestamp(posting_date, posting_time) as timestamp", + "posting_datetime as timestamp", ], as_dict=1, ) -def get_batch_incoming_rate( - item_code, warehouse, batch_no, posting_date, posting_time, creation=None -): +def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None): + import datetime sle = frappe.qb.DocType("Stock Ledger Entry") - timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( - posting_date, posting_time - ) + posting_datetime = get_combine_datetime(posting_date, posting_time) + if not creation: + posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1) + + timestamp_condition = sle.posting_datetime < posting_datetime if creation: - timestamp_condition |= ( - CombineDatetime(sle.posting_date, sle.posting_time) - == CombineDatetime(posting_date, posting_time) - ) & (sle.creation < creation) + timestamp_condition |= (sle.posting_datetime == get_combine_datetime(posting_date, posting_time)) & ( + sle.creation < creation + ) batch_details = ( frappe.qb.from_(sle) @@ -1349,7 +1356,6 @@ def get_valuation_rate( raise_error_if_no_rate=True, batch_no=None, ): - if not company: company = frappe.get_cached_value("Warehouse", warehouse, "company") @@ -1382,7 +1388,7 @@ def get_valuation_rate( AND valuation_rate >= 0 AND is_cancelled = 0 AND NOT (voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", + order by posting_datetime desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type), ) @@ -1425,7 +1431,7 @@ def get_valuation_rate( solutions += ( "
            • " + _("If not, you can Cancel / Submit this entry") - + " {0} ".format(frappe.bold("after")) + + " {} ".format(frappe.bold("after")) + _("performing either one below:") + "
            • " ) @@ -1443,7 +1449,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): datetime_limit_condition = "" qty_shift = args.actual_qty - args["time_format"] = "%H:%i:%s" + args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"]) # find difference/shift in qty caused by stock reconciliation if args.voucher_type == "Stock Reconciliation": @@ -1453,8 +1459,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): next_stock_reco_detail = get_next_stock_reco(args) if next_stock_reco_detail: detail = next_stock_reco_detail[0] - - # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) frappe.db.sql( @@ -1467,13 +1471,9 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): and voucher_no != %(voucher_no)s and is_cancelled = 0 and ( - posting_date > %(posting_date)s or - ( - posting_date = %(posting_date)s and - time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s) - ) + posting_datetime > %(posting_datetime)s ) - {datetime_limit_condition} + {datetime_limit_condition} """, args, ) @@ -1528,20 +1528,11 @@ def get_next_stock_reco(kwargs): & (sle.voucher_no != kwargs.get("voucher_no")) & (sle.is_cancelled == 0) & ( - ( - CombineDatetime(sle.posting_date, sle.posting_time) - > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time")) - ) - | ( - ( - CombineDatetime(sle.posting_date, sle.posting_time) - == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time")) - ) - & (sle.creation > kwargs.get("creation")) - ) + sle.posting_datetime + >= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) ) ) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.posting_datetime) .orderby(sle.creation) .limit(1) ) @@ -1553,11 +1544,13 @@ def get_next_stock_reco(kwargs): def get_datetime_limit_condition(detail): + posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time) + return f""" and - (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}') + (posting_datetime < '{posting_datetime}' or ( - timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}') + posting_datetime = '{posting_datetime}' and creation < '{detail.creation}' ) )""" @@ -1572,9 +1565,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): neg_sle = get_future_sle_with_negative_qty(args) if is_negative_with_precision(neg_sle): - message = _( - "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." - ).format( + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( abs(neg_sle[0]["qty_after_transaction"]), frappe.get_desk_link("Item", args.item_code), frappe.get_desk_link("Warehouse", args.warehouse), @@ -1590,9 +1581,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): neg_batch_sle = get_future_sle_with_negative_batch_qty(args) if is_negative_with_precision(neg_batch_sle, is_batch=True): - message = _( - "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." - ).format( + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( abs(neg_batch_sle[0]["cumulative_total"]), frappe.get_desk_link("Batch", args.batch_no), frappe.get_desk_link("Warehouse", args.warehouse), @@ -1630,14 +1619,11 @@ def get_future_sle_with_negative_qty(sle): (SLE.item_code == sle.item_code) & (SLE.warehouse == sle.warehouse) & (SLE.voucher_no != sle.voucher_no) - & ( - CombineDatetime(SLE.posting_date, SLE.posting_time) - >= CombineDatetime(sle.posting_date, sle.posting_time) - ) + & (SLE.posting_datetime >= get_combine_datetime(sle.posting_date, sle.posting_time)) & (SLE.is_cancelled == 0) & (SLE.qty_after_transaction < 0) ) - .orderby(CombineDatetime(SLE.posting_date, SLE.posting_time)) + .orderby(SLE.posting_datetime) .limit(1) ) @@ -1652,20 +1638,20 @@ def get_future_sle_with_negative_batch_qty(args): """ with batch_ledger as ( select - posting_date, posting_time, voucher_type, voucher_no, - sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total + posting_date, posting_time, posting_datetime, voucher_type, voucher_no, + sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total from `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s and batch_no=%(batch_no)s and is_cancelled = 0 - order by posting_date, posting_time, creation + order by posting_datetime, creation ) select * from batch_ledger where cumulative_total < 0.0 - and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + and posting_datetime >= %(posting_datetime)s limit 1 """, args, @@ -1673,7 +1659,7 @@ def get_future_sle_with_negative_batch_qty(args): ) -def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool: +def is_negative_stock_allowed(*, item_code: str | None = None) -> bool: if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)): return True if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)): @@ -1717,6 +1703,7 @@ def is_internal_transfer(sle): def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None): table = frappe.qb.DocType("Stock Ledger Entry") + posting_datetime = get_combine_datetime(posting_date, posting_time) query = ( frappe.qb.from_(table) @@ -1725,10 +1712,7 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, (table.is_cancelled == 0) & (table.item_code == item_code) & (table.warehouse == warehouse) - & ( - (table.posting_date < posting_date) - | ((table.posting_date == posting_date) & (table.posting_time <= posting_time)) - ) + & (table.posting_datetime <= posting_datetime) ) ) diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py index b53e29e9e8e..30f748a65e0 100644 --- a/erpnext/stock/tests/test_get_item_details.py +++ b/erpnext/stock/tests/test_get_item_details.py @@ -1,5 +1,3 @@ -import json - import frappe from frappe.test_runner import make_test_records from frappe.tests.utils import FrappeTestCase @@ -16,7 +14,6 @@ class TestGetItemDetail(FrappeTestCase): super().setUp() def test_get_item_detail_purchase_order(self): - args = frappe._dict( { "item_code": "_Test Item", @@ -29,7 +26,6 @@ class TestGetItemDetail(FrappeTestCase): "name": None, "supplier": "_Test Supplier", "transaction_date": None, - "conversion_rate": 1.0, "price_list": "_Test Buying Price List", "is_subcontracted": 0, "ignore_pricing_rule": 1, diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py index 4e93ac93cb7..bc646fae45c 100644 --- a/erpnext/stock/tests/test_utils.py +++ b/erpnext/stock/tests/test_utils.py @@ -28,7 +28,7 @@ class StockTestMixin: ) self.assertGreaterEqual(len(sles), len(expected_sles)) - for exp_sle, act_sle in zip(expected_sles, sles): + for exp_sle, act_sle in zip(expected_sles, sles, strict=False): for k, v in exp_sle.items(): act_value = act_sle[k] if k == "stock_queue": @@ -51,7 +51,7 @@ class StockTestMixin: order_by=order_by or "posting_date, creation", ) self.assertGreaterEqual(len(actual_gles), len(expected_gles)) - for exp_gle, act_gle in zip(expected_gles, actual_gles): + for exp_gle, act_gle in zip(expected_gles, actual_gles, strict=False): for k, exp_value in exp_gle.items(): act_value = act_gle[k] self.assertEqual(exp_value, act_value, msg=f"{k} doesn't match \n{exp_gle}\n{act_gle}") diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py index 05f153b4a0c..47b8f0b5cf5 100644 --- a/erpnext/stock/tests/test_valuation.py +++ b/erpnext/stock/tests/test_valuation.py @@ -28,9 +28,7 @@ class TestFIFOValuation(unittest.TestCase): self.assertAlmostEqual(sum(q for q, _ in self.queue), qty, msg=f"queue: {self.queue}", places=4) def assertTotalValue(self, value): - self.assertAlmostEqual( - sum(q * r for q, r in self.queue), value, msg=f"queue: {self.queue}", places=2 - ) + self.assertAlmostEqual(sum(q * r for q, r in self.queue), value, msg=f"queue: {self.queue}", places=2) def test_simple_addition(self): self.queue.add_stock(1, 10) @@ -195,7 +193,6 @@ class TestFIFOValuation(unittest.TestCase): total_value -= sum(q * r for q, r in consumed) self.assertTotalQty(total_qty) self.assertTotalValue(total_value) - self.assertGreaterEqual(total_value, 0) class TestLIFOValuation(unittest.TestCase): @@ -211,9 +208,7 @@ class TestLIFOValuation(unittest.TestCase): self.assertAlmostEqual(sum(q for q, _ in self.stack), qty, msg=f"stack: {self.stack}", places=4) def assertTotalValue(self, value): - self.assertAlmostEqual( - sum(q * r for q, r in self.stack), value, msg=f"stack: {self.stack}", places=2 - ) + self.assertAlmostEqual(sum(q * r for q, r in self.stack), value, msg=f"stack: {self.stack}", places=2) def test_simple_addition(self): self.stack.add_stock(1, 10) @@ -356,7 +351,6 @@ class TestLIFOValuationSLE(FrappeTestCase): self.assertEqual(stock_queue, expected_queue) def test_lifo_values(self): - in1 = self._make_stock_entry(1, 1) self.assertStockQueue(in1, [[1, 1]]) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index e019c572daf..058db2a6e20 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -3,18 +3,17 @@ import json -from typing import Dict, Optional import frappe from frappe import _ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum -from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime +from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime import erpnext from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.valuation import FIFOValuation, LIFOValuation -BarcodeScanResult = Dict[str, Optional[str]] +BarcodeScanResult = dict[str, str | None] class InvalidWarehouseCompany(frappe.ValidationError): @@ -55,7 +54,7 @@ def get_stock_value_from_bin(warehouse=None, item_code=None): def get_stock_value_on( - warehouses: list | str = None, posting_date: str = None, item_code: str = None + warehouses: list | str | None = None, posting_date: str | None = None, item_code: str | None = None ) -> float: if not posting_date: posting_date = nowdate() @@ -96,6 +95,7 @@ def get_stock_balance( with_serial_no=False, inventory_dimensions_dict=None, batch_no=None, + voucher_no=None, ): """Returns stock balance quantity at given warehouse on given posting date or current date. @@ -115,6 +115,9 @@ def get_stock_balance( "posting_time": posting_time, } + if voucher_no: + args["voucher_no"] = voucher_no + extra_cond = "" if inventory_dimensions_dict: for field, value in inventory_dimensions_dict.items(): @@ -136,9 +139,7 @@ def get_stock_balance( else (0.0, 0.0, None) ) else: - return ( - (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) - ) + return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) else: return last_entry.qty_after_transaction if last_entry else 0.0 @@ -201,10 +202,8 @@ def get_latest_stock_qty(item_code, warehouse=None): condition += " AND warehouse = %s" actual_qty = frappe.db.sql( - """select sum(actual_qty) from tabBin - where item_code=%s {0}""".format( - condition - ), + f"""select sum(actual_qty) from tabBin + where item_code=%s {condition}""", values, )[0][0] @@ -333,9 +332,7 @@ def get_valuation_method(item_code): """get valuation method from item or default""" val_method = frappe.db.get_value("Item", item_code, "valuation_method", cache=True) if not val_method: - val_method = ( - frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO" - ) + val_method = frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO" return val_method @@ -410,7 +407,6 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto if not include_uom or not conversion_factors: return - convertible_cols = {} is_dict_obj = False if isinstance(result[0], dict): is_dict_obj = True @@ -425,8 +421,8 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto columns.insert( idx + 1, { - "label": "{0} (per {1})".format(d.get("label"), include_uom), - "fieldname": "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)), + "label": "{} (per {})".format(d.get("label"), include_uom), + "fieldname": "{}_{}".format(d.get("fieldname"), frappe.scrub(include_uom)), "fieldtype": "Currency" if d.get("convertible") == "rate" else "Float", }, ) @@ -449,7 +445,7 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto if not is_dict_obj: row.insert(key + 1, new_value) else: - new_key = "{0}_{1}".format(key, frappe.scrub(include_uom)) + new_key = f"{key}_{frappe.scrub(include_uom)}" update_dict_values.append([row, new_key, new_value]) for data in update_dict_values: @@ -483,9 +479,9 @@ def add_additional_uom_columns(columns, result, include_uom, conversion_factors) {"converted_col": columns[next_col]["fieldname"], "for_type": col.get("convertible")} ) if col.get("convertible") == "rate": - columns[next_col]["label"] += " (per {})".format(include_uom) + columns[next_col]["label"] += f" (per {include_uom})" else: - columns[next_col]["label"] += " ({})".format(include_uom) + columns[next_col]["label"] += f" ({include_uom})" for row_idx, row in enumerate(result): for convertible_col, data in convertible_column_map.items(): @@ -559,7 +555,7 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: def set_cache(data: BarcodeScanResult): frappe.cache().set_value(f"erpnext:barcode_scan:{search_value}", data, expires_in_sec=120) - def get_cache() -> Optional[BarcodeScanResult]: + def get_cache() -> BarcodeScanResult | None: if data := frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}"): return data @@ -605,7 +601,7 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: return {} -def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Optional[str]]: +def _update_item_info(scan_result: dict[str, str | None]) -> dict[str, str | None]: if item_code := scan_result.get("item_code"): if item_info := frappe.get_cached_value( "Item", @@ -615,3 +611,18 @@ def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Option ): scan_result.update(item_info) return scan_result + + +def get_combine_datetime(posting_date, posting_time): + import datetime + + if isinstance(posting_date, str): + posting_date = getdate(posting_date) + + if isinstance(posting_time, str): + posting_time = get_time(posting_time) + + if isinstance(posting_time, datetime.timedelta): + posting_time = (datetime.datetime.min + posting_time).time() + + return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0) diff --git a/erpnext/stock/valuation.py b/erpnext/stock/valuation.py index 35f4f12235d..b1df982c906 100644 --- a/erpnext/stock/valuation.py +++ b/erpnext/stock/valuation.py @@ -1,9 +1,10 @@ from abc import ABC, abstractmethod, abstractproperty -from typing import Callable, List, NewType, Optional, Tuple +from collections.abc import Callable +from typing import NewType from frappe.utils import flt -StockBin = NewType("StockBin", List[float]) # [[qty, rate], ...] +StockBin = NewType("StockBin", list[float]) # [[qty, rate], ...] # Indexes of values inside FIFO bin 2-tuple QTY = 0 @@ -17,15 +18,15 @@ class BinWiseValuation(ABC): @abstractmethod def remove_stock( - self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None - ) -> List[StockBin]: + self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] | None = None + ) -> list[StockBin]: pass @abstractproperty - def state(self) -> List[StockBin]: + def state(self) -> list[StockBin]: pass - def get_total_stock_and_value(self) -> Tuple[float, float]: + def get_total_stock_and_value(self) -> tuple[float, float]: total_qty = 0.0 total_value = 0.0 @@ -62,11 +63,11 @@ class FIFOValuation(BinWiseValuation): # ref: https://docs.python.org/3/reference/datamodel.html#slots __slots__ = ["queue"] - def __init__(self, state: Optional[List[StockBin]]): - self.queue: List[StockBin] = state if state is not None else [] + def __init__(self, state: list[StockBin] | None): + self.queue: list[StockBin] = state if state is not None else [] @property - def state(self) -> List[StockBin]: + def state(self) -> list[StockBin]: """Get current state of queue.""" return self.queue @@ -95,8 +96,8 @@ class FIFOValuation(BinWiseValuation): self.queue[-1][QTY] = qty def remove_stock( - self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None - ) -> List[StockBin]: + self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] | None = None + ) -> list[StockBin]: """Remove stock from the queue and return popped bins. args: @@ -166,11 +167,11 @@ class LIFOValuation(BinWiseValuation): # ref: https://docs.python.org/3/reference/datamodel.html#slots __slots__ = ["stack"] - def __init__(self, state: Optional[List[StockBin]]): - self.stack: List[StockBin] = state if state is not None else [] + def __init__(self, state: list[StockBin] | None): + self.stack: list[StockBin] = state if state is not None else [] @property - def state(self) -> List[StockBin]: + def state(self) -> list[StockBin]: """Get current state of stack.""" return self.stack @@ -201,8 +202,8 @@ class LIFOValuation(BinWiseValuation): self.stack[-1][QTY] = qty def remove_stock( - self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None - ) -> List[StockBin]: + self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] | None = None + ) -> list[StockBin]: """Remove stock from the stack and return popped bins. args: diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index c0064996099..187bea1e9f7 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -7,6 +7,7 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created +from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.utils import get_bin @@ -14,7 +15,7 @@ from erpnext.stock.utils import get_bin class SubcontractingOrder(SubcontractingController): def __init__(self, *args, **kwargs): - super(SubcontractingOrder, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.status_updater = [ { @@ -31,10 +32,10 @@ class SubcontractingOrder(SubcontractingController): ] def before_validate(self): - super(SubcontractingOrder, self).before_validate() + super().before_validate() def validate(self): - super(SubcontractingOrder, self).validate() + super().validate() self.validate_purchase_order_for_subcontracting() self.validate_items() self.validate_service_items() @@ -135,9 +136,7 @@ class SubcontractingOrder(SubcontractingController): ): item_wh_list.append([item.item_code, item.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty( - item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)} - ) + update_bin_qty(item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)}) @staticmethod def get_ordered_qty(item_code, warehouse): @@ -211,7 +210,10 @@ class SubcontractingOrder(SubcontractingController): elif self.per_received > 0 and self.per_received < 100: status = "Partially Received" for item in self.supplied_items: - if not item.returned_qty or (item.supplied_qty - item.consumed_qty - item.returned_qty) > 0: + if ( + not item.returned_qty + or (item.supplied_qty - item.consumed_qty - item.returned_qty) > 0 + ): break else: status = "Closed" @@ -234,6 +236,9 @@ class SubcontractingOrder(SubcontractingController): "Subcontracting Order", self.name, "status", status, update_modified=update_modified ) + if status == "Closed": + update_po_status("Closed", self.purchase_order) + @frappe.whitelist() def make_subcontracting_receipt(source_name, target_doc=None): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js index 7ca12642c5f..7c12abd6722 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js @@ -1,18 +1,18 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.listview_settings['Subcontracting Order'] = { +frappe.listview_settings["Subcontracting Order"] = { get_indicator: function (doc) { const status_colors = { - "Draft": "grey", - "Open": "orange", + Draft: "grey", + Open: "orange", "Partially Received": "yellow", - "Completed": "green", + Completed: "green", "Partial Material Transferred": "purple", "Material Transferred": "blue", - "Closed": "red", - "Cancelled": "red", + Closed: "red", + Cancelled: "red", }; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; }, -}; \ No newline at end of file +}; diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index fbdea6ddbee..5762522b93f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -211,7 +211,7 @@ class TestSubcontractingOrder(FrappeTestCase): item["qty"] -= 1 itemwise_transfer_qty[item["item_code"]] += item["qty"] - ste = make_stock_transfer_entry( + make_stock_transfer_entry( sco_no=sco.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details), diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index b84cbac4843..c0235f9b85c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -12,7 +12,7 @@ from erpnext.controllers.subcontracting_controller import SubcontractingControll class SubcontractingReceipt(SubcontractingController): def __init__(self, *args, **kwargs): - super(SubcontractingReceipt, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.status_updater = [ { "target_dt": "Subcontracting Order Item", @@ -31,9 +31,7 @@ class SubcontractingReceipt(SubcontractingController): def onload(self): self.set_onload( "backflush_based_on", - frappe.db.get_single_value( - "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" - ), + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on"), ) def update_status_updater_args(self): @@ -64,7 +62,7 @@ class SubcontractingReceipt(SubcontractingController): ) def before_validate(self): - super(SubcontractingReceipt, self).before_validate() + super().before_validate() self.validate_items_qty() self.set_items_bom() self.set_items_cost_center() @@ -74,9 +72,10 @@ class SubcontractingReceipt(SubcontractingController): if ( frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") == "BOM" - ): + ) and not self.has_serial_batch_items(): self.supplied_items = [] - super(SubcontractingReceipt, self).validate() + + super().validate() self.set_missing_values() self.validate_posting_time() self.validate_rejected_warehouse() @@ -125,6 +124,14 @@ class SubcontractingReceipt(SubcontractingController): self.calculate_supplied_items_qty_and_amount() self.calculate_items_qty_and_amount() + def has_serial_batch_items(self): + if not self.get("supplied_items"): + return False + + for row in self.get("supplied_items"): + if row.serial_no or row.batch_no: + return True + def set_available_qty_for_consumption(self): supplied_items_details = {} @@ -135,7 +142,9 @@ class SubcontractingReceipt(SubcontractingController): .select( sco_supplied_item.rm_item_code, sco_supplied_item.reference_name, - (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"), + (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_( + "available_qty" + ), ) .where( (sco_supplied_item.parent == item.subcontracting_order) @@ -148,7 +157,9 @@ class SubcontractingReceipt(SubcontractingController): supplied_items_details[item.name] = {} for supplied_item in supplied_items: - supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty + supplied_items_details[item.name][ + supplied_item.rm_item_code + ] = supplied_item.available_qty else: for item in self.get("supplied_items"): item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get( @@ -224,7 +235,9 @@ class SubcontractingReceipt(SubcontractingController): for item in self.items: if not (item.qty or item.rejected_qty): frappe.throw( - _("Row {0}: Accepted Qty and Rejected Qty can't be zero at the same time.").format(item.idx) + _("Row {0}: Accepted Qty and Rejected Qty can't be zero at the same time.").format( + item.idx + ) ) def set_items_bom(self): @@ -298,8 +311,6 @@ class SubcontractingReceipt(SubcontractingController): return process_gl_map(gl_entries) def make_item_gl_entries(self, gl_entries, warehouse_account=None): - stock_rbnb = self.get_company_default("stock_received_but_not_billed") - warehouse_with_no_account = [] for item in self.items: @@ -317,29 +328,41 @@ class SubcontractingReceipt(SubcontractingController): "stock_value_difference", ) - warehouse_account_name = warehouse_account[item.warehouse]["account"] - warehouse_account_currency = warehouse_account[item.warehouse]["account_currency"] - supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") - supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( - "account_currency" + accepted_warehouse_account = warehouse_account[item.warehouse]["account"] + supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get( + "account" ) remarks = self.get("remarks") or _("Accounting Entry for Stock") - # FG Warehouse Account (Debit) + # Accepted Warehouse Account (Debit) self.add_gl_entry( gl_entries=gl_entries, - account=warehouse_account_name, + account=accepted_warehouse_account, cost_center=item.cost_center, debit=stock_value_diff, credit=0.0, remarks=remarks, - against_account=stock_rbnb, - account_currency=warehouse_account_currency, + against_account=item.expense_account, + account_currency=get_account_currency(accepted_warehouse_account), + project=item.project, + item=item, + ) + # Expense Account (Credit) + self.add_gl_entry( + gl_entries=gl_entries, + account=item.expense_account, + cost_center=item.cost_center, + debit=0.0, + credit=stock_value_diff, + remarks=remarks, + against_account=accepted_warehouse_account, + account_currency=get_account_currency(item.expense_account), + project=item.project, item=item, ) - # Supplier Warehouse Account (Credit) - if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): + if flt(item.rm_supp_cost) and supplier_warehouse_account: + # Supplier Warehouse Account (Credit) self.add_gl_entry( gl_entries=gl_entries, account=supplier_warehouse_account, @@ -347,40 +370,66 @@ class SubcontractingReceipt(SubcontractingController): debit=0.0, credit=flt(item.rm_supp_cost), remarks=remarks, - against_account=warehouse_account_name, - account_currency=supplier_warehouse_account_currency, + against_account=item.expense_account, + account_currency=get_account_currency(supplier_warehouse_account), + project=item.project, item=item, ) - - # Expense Account (Credit) - if flt(item.service_cost_per_qty): + # Expense Account (Debit) self.add_gl_entry( gl_entries=gl_entries, account=item.expense_account, cost_center=item.cost_center, - debit=0.0, - credit=flt(item.service_cost_per_qty) * flt(item.qty), + debit=flt(item.rm_supp_cost), + credit=0.0, remarks=remarks, - against_account=warehouse_account_name, + against_account=supplier_warehouse_account, account_currency=get_account_currency(item.expense_account), + project=item.project, item=item, ) - # Loss Account (Credit) - divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount")) + # Expense Account (Debit) + if item.additional_cost_per_qty: + self.add_gl_entry( + gl_entries=gl_entries, + account=item.expense_account, + cost_center=self.cost_center or self.get_company_default("cost_center"), + debit=item.qty * item.additional_cost_per_qty, + credit=0.0, + remarks=remarks, + against_account=None, + account_currency=get_account_currency(item.expense_account), + ) - if divisional_loss: - loss_account = item.expense_account + if divisional_loss := flt(item.amount - stock_value_diff, item.precision("amount")): + loss_account = self.get_company_default( + "stock_adjustment_account", ignore_validation=True + ) + # Loss Account (Credit) self.add_gl_entry( gl_entries=gl_entries, account=loss_account, cost_center=item.cost_center, + debit=0.0, + credit=divisional_loss, + remarks=remarks, + against_account=item.expense_account, + account_currency=get_account_currency(loss_account), + project=item.project, + item=item, + ) + # Expense Account (Debit) + self.add_gl_entry( + gl_entries=gl_entries, + account=item.expense_account, + cost_center=item.cost_center, debit=divisional_loss, credit=0.0, remarks=remarks, - against_account=warehouse_account_name, - account_currency=get_account_currency(loss_account), + against_account=loss_account, + account_currency=get_account_currency(item.expense_account), project=item.project, item=item, ) @@ -390,7 +439,6 @@ class SubcontractingReceipt(SubcontractingController): ): warehouse_with_no_account.append(item.warehouse) - # Additional Costs Expense Accounts (Credit) for row in self.additional_costs: credit_amount = ( flt(row.base_amount) @@ -398,6 +446,7 @@ class SubcontractingReceipt(SubcontractingController): else flt(row.amount) ) + # Additional Cost Expense Account (Credit) self.add_gl_entry( gl_entries=gl_entries, account=row.expense_account, @@ -406,6 +455,7 @@ class SubcontractingReceipt(SubcontractingController): credit=credit_amount, remarks=remarks, against_account=None, + account_currency=get_account_currency(row.expense_account), ) if warehouse_with_no_account: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js index 14a4e4ad6cb..be6c0d0b18f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js @@ -1,14 +1,14 @@ // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.listview_settings['Subcontracting Receipt'] = { +frappe.listview_settings["Subcontracting Receipt"] = { get_indicator: function (doc) { const status_colors = { - "Draft": "grey", - "Return": "gray", + Draft: "grey", + Return: "gray", "Return Issued": "grey", - "Completed": "green", + Completed: "green", }; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; }, -}; \ No newline at end of file +}; diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index b05ed755c7f..1b1d823a920 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -6,7 +6,7 @@ import copy import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, cint, cstr, flt, today +from frappe.utils import add_days, cint, flt, today import erpnext from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -43,9 +43,7 @@ class TestSubcontractingReceipt(FrappeTestCase): def test_subcontracting(self): set_backflush_based_on("BOM") - make_stock_entry( - item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100 - ) + make_stock_entry(item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100) make_stock_entry( item_code="_Test Item Home Desktop 100", qty=100, @@ -77,9 +75,7 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost)) def test_available_qty_for_consumption(self): - make_stock_entry( - item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100 - ) + make_stock_entry(item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100) make_stock_entry( item_code="_Test Item Home Desktop 100", qty=100, @@ -206,12 +202,8 @@ class TestSubcontractingReceipt(FrappeTestCase): make_stock_entry( target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100 ) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100 - ) + make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100) rm_items = [ { @@ -442,26 +434,15 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(scr.company)), 1) gl_entries = get_gl_entries("Subcontracting Receipt", scr.name) - self.assertTrue(gl_entries) fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse) - supplier_warehouse_ac = get_inventory_account(scr.company, scr.supplier_warehouse) expense_account = scr.items[0].expense_account - - if fg_warehouse_ac == supplier_warehouse_ac: - expected_values = { - fg_warehouse_ac: [2100.0, 1000.0], # FG Amount (D), RM Cost (C) - expense_account: [0.0, 1000.0], # Service Cost (C) - additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C) - } - else: - expected_values = { - fg_warehouse_ac: [2100.0, 0.0], # FG Amount (D) - supplier_warehouse_ac: [0.0, 1000.0], # RM Cost (C) - expense_account: [0.0, 1000.0], # Service Cost (C) - additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C) - } + expected_values = { + fg_warehouse_ac: [2100.0, 1000], + expense_account: [1100, 2100], + additional_costs_expense_account: [0.0, 100.0], + } for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.debit) @@ -471,14 +452,58 @@ class TestSubcontractingReceipt(FrappeTestCase): scr.cancel() self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name)) + def test_subcontracting_receipt_with_zero_service_cost(self): + warehouse = "Stores - TCP1" + service_items = [ + { + "warehouse": warehouse, + "item_code": "Subcontracted Service Item 7", + "qty": 10, + "rate": 0, + "fg_item": "Subcontracted Item SA7", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order( + company="_Test Company with perpetual inventory", + warehouse=warehouse, + supplier_warehouse="Work In Progress - TCP1", + service_items=service_items, + ) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + + gl_entries = get_gl_entries("Subcontracting Receipt", scr.name) + self.assertTrue(gl_entries) + + fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse) + expense_account = scr.items[0].expense_account + expected_values = { + fg_warehouse_ac: [1000, 1000], + expense_account: [1000, 1000], + } + + for gle in gl_entries: + self.assertEqual(expected_values[gle.account][0], gle.debit) + self.assertEqual(expected_values[gle.account][1], gle.credit) + + scr.reload() + scr.cancel() + def test_supplied_items_consumed_qty(self): # Set Backflush Based On as "Material Transferred for Subcontracting" to transfer RM's more than the required qty set_backflush_based_on("Material Transferred for Subcontract") # Create Material Receipt for RM's - make_stock_entry( - item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100 - ) + make_stock_entry(item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100) make_stock_entry( item_code="_Test Item Home Desktop 100", qty=100, @@ -732,9 +757,7 @@ class TestSubcontractingReceipt(FrappeTestCase): def make_return_subcontracting_receipt(**args): args = frappe._dict(args) return_doc = make_return_doc("Subcontracting Receipt", args.scr_name) - return_doc.supplier_warehouse = ( - args.supplier_warehouse or args.warehouse or "_Test Warehouse 1 - _TC" - ) + return_doc.supplier_warehouse = args.supplier_warehouse or args.warehouse or "_Test Warehouse 1 - _TC" if args.qty: for item in return_doc.items: diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 9f91dc1726d..03d209e99e3 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -1,40 +1,47 @@ frappe.ui.form.on("Issue", { - onload: function(frm) { + onload: function (frm) { frm.email_field = "raised_by"; - frappe.db.get_value("Support Settings", {name: "Support Settings"}, - ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { + frappe.db.get_value( + "Support Settings", + { name: "Support Settings" }, + ["allow_resetting_service_level_agreement", "track_service_level_agreement"], + (r) => { if (r && r.track_service_level_agreement == "0") { frm.set_df_property("service_level_section", "hidden", 1); } if (r && r.allow_resetting_service_level_agreement == "0") { frm.set_df_property("reset_service_level_agreement", "hidden", 1); } - }); + } + ); // buttons if (frm.doc.status !== "Closed") { - frm.add_custom_button(__("Close"), function() { + frm.add_custom_button(__("Close"), function () { frm.set_value("status", "Closed"); frm.save(); }); - frm.add_custom_button(__("Task"), function() { - frappe.model.open_mapped_doc({ - method: "erpnext.support.doctype.issue.issue.make_task", - frm: frm - }); - }, __("Create")); - + frm.add_custom_button( + __("Task"), + function () { + frappe.model.open_mapped_doc({ + method: "erpnext.support.doctype.issue.issue.make_task", + frm: frm, + }); + }, + __("Create") + ); } else { - frm.add_custom_button(__("Reopen"), function() { + frm.add_custom_button(__("Reopen"), function () { frm.set_value("status", "Open"); frm.save(); }); } }, - reset_service_level_agreement: function(frm) { + reset_service_level_agreement: function (frm) { let reset_sla = new frappe.ui.Dialog({ title: __("Reset Service Level Agreement"), fields: [ @@ -42,8 +49,8 @@ frappe.ui.form.on("Issue", { fieldtype: "Data", fieldname: "reason", label: __("Reason"), - reqd: 1 - } + reqd: 1, + }, ], primary_action_label: __("Reset"), primary_action: (values) => { @@ -53,39 +60,42 @@ frappe.ui.form.on("Issue", { frappe.show_alert({ indicator: "green", - message: __("Resetting Service Level Agreement.") + message: __("Resetting Service Level Agreement."), }); - frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", { - reason: values.reason, - user: frappe.session.user_email, - doctype: frm.doc.doctype, - docname: frm.doc.name, - }, () => { - reset_sla.enable_primary_action(); - frm.refresh(); - frappe.msgprint(__("Service Level Agreement was reset.")); - }); - } + frappe.call( + "erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", + { + reason: values.reason, + user: frappe.session.user_email, + doctype: frm.doc.doctype, + docname: frm.doc.name, + }, + () => { + reset_sla.enable_primary_action(); + frm.refresh(); + frappe.msgprint(__("Service Level Agreement was reset.")); + } + ); + }, }); reset_sla.show(); }, - - timeline_refresh: function(frm) { + timeline_refresh: function (frm) { if (!frm.timeline.wrapper.find(".btn-split-issue").length) { let split_issue_btn = $(` - ${frappe.utils.icon('branch', 'sm')} + ${frappe.utils.icon("branch", "sm")} `); let communication_box = frm.timeline.wrapper.find('.timeline-item[data-doctype="Communication"]'); - communication_box.find('.actions').prepend(split_issue_btn); + communication_box.find(".actions").prepend(split_issue_btn); if (!frm.timeline.wrapper.data("split-issue-event-attached")) { - frm.timeline.wrapper.on('click', '.btn-split-issue', (e) => { + frm.timeline.wrapper.on("click", ".btn-split-issue", (e) => { var dialog = new frappe.ui.Dialog({ title: __("Split Issue"), fields: [ @@ -94,20 +104,30 @@ frappe.ui.form.on("Issue", { fieldtype: "Data", reqd: 1, label: __("Subject"), - description: __("All communications including and above this shall be moved into the new Issue") - } + description: __( + "All communications including and above this shall be moved into the new Issue" + ), + }, ], primary_action_label: __("Split"), primary_action: () => { - frm.call("split_issue", { - subject: dialog.fields_dict.subject.value, - communication_id: e.currentTarget.closest(".timeline-item").getAttribute("data-name") - }, (r) => { - frappe.msgprint(`New issue created: ${r.message}`); - frm.reload_doc(); - dialog.hide(); - }); - } + frm.call( + "split_issue", + { + subject: dialog.fields_dict.subject.value, + communication_id: e.currentTarget + .closest(".timeline-item") + .getAttribute("data-name"), + }, + (r) => { + frappe.msgprint( + `New issue created: ${r.message}` + ); + frm.reload_doc(); + dialog.hide(); + } + ); + }, }); dialog.show(); }); diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 7f3e0cf4c21..0cb56e6375c 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -19,7 +19,7 @@ from frappe.utils.user import is_website_user class Issue(Document): def get_feed(self): - return "{0}: {1}".format(_(self.status), self.subject) + return f"{_(self.status)}: {self.subject}" def validate(self): if self.is_new() and self.via_customer_portal: @@ -72,8 +72,8 @@ class Issue(Document): "reference_name": self.name, } ) - communication.ignore_permissions = True - communication.ignore_mandatory = True + communication.flags.ignore_permissions = True + communication.flags.ignore_mandatory = True communication.save() @frappe.whitelist() @@ -122,7 +122,7 @@ class Issue(Document): "comment_type": "Info", "reference_doctype": "Issue", "reference_name": replicated_issue.name, - "content": " - Split the Issue from {1}".format( + "content": " - Split the Issue from {}".format( self.name, frappe.bold(self.name) ), } @@ -176,7 +176,6 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord @frappe.whitelist() def set_multiple_status(names, status): - for name in json.loads(names): frappe.db.set_value("Issue", name, "status", status) @@ -258,9 +257,7 @@ def set_first_response_time(communication, method): if communication.get("reference_doctype") == "Issue": issue = get_parent_doc(communication) if is_first_response(issue) and issue.service_level_agreement: - first_response_time = calculate_first_response_time( - issue, get_datetime(issue.first_responded_on) - ) + first_response_time = calculate_first_response_time(issue, get_datetime(issue.first_responded_on)) issue.db_set("first_response_time", first_response_time) diff --git a/erpnext/support/doctype/issue/issue_list.js b/erpnext/support/doctype/issue/issue_list.js index 5c9d4ed9890..8aa1650c599 100644 --- a/erpnext/support/doctype/issue/issue_list.js +++ b/erpnext/support/doctype/issue/issue_list.js @@ -1,30 +1,30 @@ -frappe.listview_settings['Issue'] = { - colwidths: {"subject": 6}, - add_fields: ['priority'], +frappe.listview_settings["Issue"] = { + colwidths: { subject: 6 }, + add_fields: ["priority"], filters: [["status", "=", "Open"]], - onload: function(listview) { + onload: function (listview) { var method = "erpnext.support.doctype.issue.issue.set_multiple_status"; - listview.page.add_action_item(__("Set as Open"), function() { - listview.call_for_selected_items(method, {"status": "Open"}); + listview.page.add_action_item(__("Set as Open"), function () { + listview.call_for_selected_items(method, { status: "Open" }); }); - listview.page.add_action_item(__("Set as Closed"), function() { - listview.call_for_selected_items(method, {"status": "Closed"}); + listview.page.add_action_item(__("Set as Closed"), function () { + listview.call_for_selected_items(method, { status: "Closed" }); }); }, - get_indicator: function(doc) { - if (doc.status === 'Open') { + get_indicator: function (doc) { + if (doc.status === "Open") { const color = { - 'Low': 'yellow', - 'Medium': 'orange', - 'High': 'red' + Low: "yellow", + Medium: "orange", + High: "red", }; - return [__(doc.status), color[doc.priority] || 'red', `status,=,Open`]; - } else if (doc.status === 'Closed') { + return [__(doc.status), color[doc.priority] || "red", `status,=,Open`]; + } else if (doc.status === "Closed") { return [__(doc.status), "green", "status,=," + doc.status]; } else { return [__(doc.status), "gray", "status,=," + doc.status]; } - } -} + }, +}; diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index a44012444ca..8cc9db9da93 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -29,23 +29,21 @@ class TestIssue(TestSetUp): creation = get_datetime("2019-03-04 12:00") # make issue with customer specific SLA - customer = create_customer("_Test Customer", "__Test SLA Customer Group", "__Test SLA Territory") + create_customer("_Test Customer", "__Test SLA Customer Group", "__Test SLA Territory") issue = make_issue(creation, "_Test Customer", 1) self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00")) self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 15:00")) # make issue with customer_group specific SLA - customer = create_customer("__Test Customer", "_Test SLA Customer Group", "__Test SLA Territory") + create_customer("__Test Customer", "_Test SLA Customer Group", "__Test SLA Territory") issue = make_issue(creation, "__Test Customer", 2) self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00")) self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 15:00")) # make issue with territory specific SLA - customer = create_customer( - "___Test Customer", "__Test SLA Customer Group", "_Test SLA Territory" - ) + create_customer("___Test Customer", "__Test SLA Customer Group", "_Test SLA Territory") issue = make_issue(creation, "___Test Customer", 3) self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00")) @@ -147,16 +145,15 @@ class TestIssue(TestSetUp): self.assertEqual(issue.agreement_status, "Fulfilled") def test_issue_open_after_closed(self): - # Created on -> 1 pm, Response Time -> 4 hrs, Resolution Time -> 6 hrs frappe.flags.current_time = get_datetime("2021-11-01 13:00") issue = make_issue( frappe.flags.current_time, index=1, issue_type="Critical" ) # Applies 24hr working time SLA create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) - self.assertEquals(issue.agreement_status, "First Response Due") - self.assertEquals(issue.response_by, get_datetime("2021-11-01 17:00")) - self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 19:00")) + self.assertEqual(issue.agreement_status, "First Response Due") + self.assertEqual(issue.response_by, get_datetime("2021-11-01 17:00")) + self.assertEqual(issue.resolution_by, get_datetime("2021-11-01 19:00")) # Replied on → 2 pm frappe.flags.current_time = get_datetime("2021-11-01 14:00") @@ -164,19 +161,19 @@ class TestIssue(TestSetUp): issue.reload() issue.status = "Replied" issue.save() - self.assertEquals(issue.agreement_status, "Resolution Due") - self.assertEquals(issue.on_hold_since, frappe.flags.current_time) - self.assertEquals(issue.first_responded_on, frappe.flags.current_time) + self.assertEqual(issue.agreement_status, "Resolution Due") + self.assertEqual(issue.on_hold_since, frappe.flags.current_time) + self.assertEqual(issue.first_responded_on, frappe.flags.current_time) # Customer Replied → 3 pm frappe.flags.current_time = get_datetime("2021-11-01 15:00") create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) issue.reload() - self.assertEquals(issue.status, "Open") + self.assertEqual(issue.status, "Open") # Hold Time + 1 Hrs - self.assertEquals(issue.total_hold_time, 3600) + self.assertEqual(issue.total_hold_time, 3600) # Resolution By should increase by one hrs - self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 20:00")) + self.assertEqual(issue.resolution_by, get_datetime("2021-11-01 20:00")) # Replied on → 4 pm, Open → 1 hr, Resolution Due → 8 pm frappe.flags.current_time = get_datetime("2021-11-01 16:00") @@ -184,37 +181,37 @@ class TestIssue(TestSetUp): issue.reload() issue.status = "Replied" issue.save() - self.assertEquals(issue.agreement_status, "Resolution Due") + self.assertEqual(issue.agreement_status, "Resolution Due") # Customer Closed → 10 pm frappe.flags.current_time = get_datetime("2021-11-01 22:00") issue.status = "Closed" issue.save() # Hold Time + 6 Hrs - self.assertEquals(issue.total_hold_time, 3600 + 21600) + self.assertEqual(issue.total_hold_time, 3600 + 21600) # Resolution By should increase by 6 hrs - self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 02:00")) - self.assertEquals(issue.agreement_status, "Fulfilled") - self.assertEquals(issue.resolution_date, frappe.flags.current_time) + self.assertEqual(issue.resolution_by, get_datetime("2021-11-02 02:00")) + self.assertEqual(issue.agreement_status, "Fulfilled") + self.assertEqual(issue.resolution_date, frappe.flags.current_time) # Customer Open → 3 am i.e after resolution by is crossed frappe.flags.current_time = get_datetime("2021-11-02 03:00") create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) issue.reload() # Since issue was Resolved, Resolution By should be increased by 5 hrs (3am - 10pm) - self.assertEquals(issue.total_hold_time, 3600 + 21600 + 18000) + self.assertEqual(issue.total_hold_time, 3600 + 21600 + 18000) # Resolution By should increase by 5 hrs - self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00")) - self.assertEquals(issue.agreement_status, "Resolution Due") + self.assertEqual(issue.resolution_by, get_datetime("2021-11-02 07:00")) + self.assertEqual(issue.agreement_status, "Resolution Due") self.assertFalse(issue.resolution_date) # We Closed → 4 am, SLA should be Fulfilled frappe.flags.current_time = get_datetime("2021-11-02 04:00") issue.status = "Closed" issue.save() - self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00")) - self.assertEquals(issue.agreement_status, "Fulfilled") - self.assertEquals(issue.resolution_date, frappe.flags.current_time) + self.assertEqual(issue.resolution_by, get_datetime("2021-11-02 07:00")) + self.assertEqual(issue.agreement_status, "Fulfilled") + self.assertEqual(issue.resolution_date, frappe.flags.current_time) def test_recording_of_assignment_on_first_reponse_failure(self): from frappe.desk.form.assign_to import add as add_assignment @@ -255,8 +252,8 @@ class TestIssue(TestSetUp): create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) issue.reload() - self.assertEquals(issue.first_responded_on, frappe.flags.current_time) - self.assertEquals(issue.agreement_status, "Resolution Due") + self.assertEqual(issue.first_responded_on, frappe.flags.current_time) + self.assertEqual(issue.agreement_status, "Resolution Due") class TestFirstResponseTime(TestSetUp): @@ -547,7 +544,7 @@ def make_issue(creation=None, customer=None, index=0, priority=None, issue_type= issue = frappe.get_doc( { "doctype": "Issue", - "subject": "Service Level Agreement Issue {0}".format(index), + "subject": f"Service Level Agreement Issue {index}", "customer": customer, "raised_by": "test@example.com", "description": "Service Level Agreement Issue", @@ -564,7 +561,6 @@ def make_issue(creation=None, customer=None, index=0, priority=None, issue_type= def create_customer(name, customer_group, territory): - create_customer_group(customer_group) create_territory(territory) @@ -580,7 +576,6 @@ def create_customer(name, customer_group, territory): def create_customer_group(customer_group): - if not frappe.db.exists("Customer Group", {"customer_group_name": customer_group}): frappe.get_doc({"doctype": "Customer Group", "customer_group_name": customer_group}).insert( ignore_permissions=True @@ -588,7 +583,6 @@ def create_customer_group(customer_group): def create_territory(territory): - if not frappe.db.exists("Territory", {"territory_name": territory}): frappe.get_doc( { diff --git a/erpnext/support/doctype/issue_priority/issue_priority.js b/erpnext/support/doctype/issue_priority/issue_priority.js index 37ce6a54bf7..e777d0690ca 100644 --- a/erpnext/support/doctype/issue_priority/issue_priority.js +++ b/erpnext/support/doctype/issue_priority/issue_priority.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Issue Priority', { +frappe.ui.form.on("Issue Priority", { // refresh: function(frm) { - // } }); diff --git a/erpnext/support/doctype/issue_type/issue_type.js b/erpnext/support/doctype/issue_type/issue_type.js index 2b3d14ef712..aa0c4565d6c 100644 --- a/erpnext/support/doctype/issue_type/issue_type.js +++ b/erpnext/support/doctype/issue_type/issue_type.js @@ -1,8 +1,6 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Issue Type', { - refresh: function(frm) { - - } +frappe.ui.form.on("Issue Type", { + refresh: function (frm) {}, }); diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index 4dbb0e7e86f..a8b85de9d6a 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -1,121 +1,145 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Service Level Agreement', { - setup: function(frm) { +frappe.ui.form.on("Service Level Agreement", { + setup: function (frm) { if (cint(frm.doc.apply_sla_for_resolution) === 1) { - frm.get_field('priorities').grid.editable_fields = [ - {fieldname: 'default_priority', columns: 1}, - {fieldname: 'priority', columns: 2}, - {fieldname: 'response_time', columns: 2}, - {fieldname: 'resolution_time', columns: 2} + frm.get_field("priorities").grid.editable_fields = [ + { fieldname: "default_priority", columns: 1 }, + { fieldname: "priority", columns: 2 }, + { fieldname: "response_time", columns: 2 }, + { fieldname: "resolution_time", columns: 2 }, ]; } else { - frm.get_field('priorities').grid.editable_fields = [ - {fieldname: 'default_priority', columns: 1}, - {fieldname: 'priority', columns: 2}, - {fieldname: 'response_time', columns: 3}, + frm.get_field("priorities").grid.editable_fields = [ + { fieldname: "default_priority", columns: 1 }, + { fieldname: "priority", columns: 2 }, + { fieldname: "response_time", columns: 3 }, ]; } }, - refresh: function(frm) { - frm.trigger('fetch_status_fields'); - frm.trigger('toggle_resolution_fields'); - frm.trigger('default_service_level_agreement'); - frm.trigger('entity'); + refresh: function (frm) { + frm.trigger("fetch_status_fields"); + frm.trigger("toggle_resolution_fields"); + frm.trigger("default_service_level_agreement"); + frm.trigger("entity"); }, - default_service_level_agreement: function(frm) { - const field = frm.get_field('default_service_level_agreement'); + default_service_level_agreement: function (frm) { + const field = frm.get_field("default_service_level_agreement"); if (frm.doc.default_service_level_agreement) { - field.set_description(__('SLA will be applied on every {0}', [frm.doc.document_type])); + field.set_description(__("SLA will be applied on every {0}", [frm.doc.document_type])); } else { - field.set_description(__('Enable to apply SLA on every {0}', [frm.doc.document_type])); + field.set_description(__("Enable to apply SLA on every {0}", [frm.doc.document_type])); } }, - document_type: function(frm) { - frm.trigger('fetch_status_fields'); - frm.trigger('default_service_level_agreement'); + document_type: function (frm) { + frm.trigger("fetch_status_fields"); + frm.trigger("default_service_level_agreement"); }, - entity_type: function(frm) { - frm.set_value('entity', undefined); + entity_type: function (frm) { + frm.set_value("entity", undefined); }, - entity: function(frm) { - const field = frm.get_field('entity'); + entity: function (frm) { + const field = frm.get_field("entity"); if (frm.doc.entity) { - const and_descendants = frm.doc.entity_type != 'Customer' ? ' ' + __('or its descendants') : ''; + const and_descendants = frm.doc.entity_type != "Customer" ? " " + __("or its descendants") : ""; field.set_description( - __('SLA will be applied if {1} is set as {2}{3}', [ - frm.doc.document_type, frm.doc.entity_type, - frm.doc.entity, and_descendants + __("SLA will be applied if {1} is set as {2}{3}", [ + frm.doc.document_type, + frm.doc.entity_type, + frm.doc.entity, + and_descendants, ]) ); } else { - field.set_description(''); + field.set_description(""); } }, - fetch_status_fields: function(frm) { + fetch_status_fields: function (frm) { let allow_statuses = []; let exclude_statuses = []; if (frm.doc.document_type) { frappe.model.with_doctype(frm.doc.document_type, () => { - let statuses = frappe.meta.get_docfield(frm.doc.document_type, 'status', frm.doc.name).options; - statuses = statuses.split('\n'); + let statuses = frappe.meta.get_docfield( + frm.doc.document_type, + "status", + frm.doc.name + ).options; + statuses = statuses.split("\n"); - exclude_statuses = ['Open', 'Closed']; + exclude_statuses = ["Open", "Closed"]; allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status)); frm.fields_dict.pause_sla_on.grid.update_docfield_property( - 'status', 'options', [''].concat(allow_statuses) + "status", + "options", + [""].concat(allow_statuses) ); - exclude_statuses = ['Open']; + exclude_statuses = ["Open"]; allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status)); frm.fields_dict.sla_fulfilled_on.grid.update_docfield_property( - 'status', 'options', [''].concat(allow_statuses) + "status", + "options", + [""].concat(allow_statuses) ); }); } - frm.refresh_field('pause_sla_on'); + frm.refresh_field("pause_sla_on"); }, - apply_sla_for_resolution: function(frm) { - frm.trigger('toggle_resolution_fields'); + apply_sla_for_resolution: function (frm) { + frm.trigger("toggle_resolution_fields"); }, - toggle_resolution_fields: function(frm) { + toggle_resolution_fields: function (frm) { if (cint(frm.doc.apply_sla_for_resolution) === 1) { - frm.fields_dict.priorities.grid.update_docfield_property('resolution_time', 'hidden', 0); - frm.fields_dict.priorities.grid.update_docfield_property('resolution_time', 'reqd', 1); + frm.fields_dict.priorities.grid.update_docfield_property("resolution_time", "hidden", 0); + frm.fields_dict.priorities.grid.update_docfield_property("resolution_time", "reqd", 1); } else { - frm.fields_dict.priorities.grid.update_docfield_property('resolution_time', 'hidden', 1); - frm.fields_dict.priorities.grid.update_docfield_property('resolution_time', 'reqd', 0); + frm.fields_dict.priorities.grid.update_docfield_property("resolution_time", "hidden", 1); + frm.fields_dict.priorities.grid.update_docfield_property("resolution_time", "reqd", 0); } - frm.refresh_field('priorities'); + frm.refresh_field("priorities"); }, - onload: function(frm) { - frm.set_query("document_type", function() { + onload: function (frm) { + frm.set_query("document_type", function () { let invalid_doctypes = frappe.model.core_doctypes_list; - invalid_doctypes.push(frm.doc.doctype, 'Cost Center', 'Company'); + invalid_doctypes.push(frm.doc.doctype, "Cost Center", "Company"); return { filters: [ - ['DocType', 'issingle', '=', 0], - ['DocType', 'istable', '=', 0], - ['DocType', 'is_submittable', '=', 0], - ['DocType', 'name', 'not in', invalid_doctypes], - ['DocType', 'module', 'not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]] - ] + ["DocType", "issingle", "=", 0], + ["DocType", "istable", "=", 0], + ["DocType", "is_submittable", "=", 0], + ["DocType", "name", "not in", invalid_doctypes], + [ + "DocType", + "module", + "not in", + [ + "Email", + "Core", + "Custom", + "Event Streaming", + "Social", + "Data Migration", + "Geo", + "Desk", + ], + ], + ], }; }); - } + }, }); 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 54d3c31cd80..e984f927fa6 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -44,22 +44,26 @@ class ServiceLevelAgreement(Document): # Check if response and resolution time is set for every priority if not priority.response_time: frappe.throw( - _("Set Response Time for Priority {0} in row {1}.").format(priority.priority, priority.idx) + _("Set Response Time for Priority {0} in row {1}.").format( + priority.priority, priority.idx + ) ) if self.apply_sla_for_resolution: if not priority.resolution_time: frappe.throw( - _("Set Response Time for Priority {0} in row {1}.").format(priority.priority, priority.idx) + _("Set Response Time for Priority {0} in row {1}.").format( + priority.priority, priority.idx + ) ) response = priority.response_time resolution = priority.resolution_time if response > resolution: frappe.throw( - _("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format( - priority.priority, priority.idx - ) + _( + "Response Time for {0} priority in row {1} can't be greater than Resolution Time." + ).format(priority.priority, priority.idx) ) priorities.append(priority.priority) @@ -323,7 +327,7 @@ def get_active_service_level_agreement_for(doc): "Service Level Agreement", "entity", "in", - [customer] + get_customer_group(customer) + get_customer_territory(customer), + [customer, *get_customer_group(customer), *get_customer_territory(customer)], ], ["Service Level Agreement", "entity_type", "is", "not set"], ] @@ -331,9 +335,7 @@ def get_active_service_level_agreement_for(doc): else: or_filters.append(["Service Level Agreement", "entity_type", "is", "not set"]) - default_sla_filter = filters + [ - ["Service Level Agreement", "default_service_level_agreement", "=", 1] - ] + default_sla_filter = [*filters, ["Service Level Agreement", "default_service_level_agreement", "=", 1]] default_sla = frappe.get_all( "Service Level Agreement", filters=default_sla_filter, @@ -374,7 +376,7 @@ def get_customer_group(customer): customer_group = frappe.db.get_value("Customer", customer, "customer_group") if customer else None if customer_group: ancestors = get_ancestors_of("Customer Group", customer_group) - customer_groups = [customer_group] + ancestors + customer_groups = [customer_group, *ancestors] return customer_groups @@ -384,7 +386,7 @@ def get_customer_territory(customer): customer_territory = frappe.db.get_value("Customer", customer, "territory") if customer else None if customer_territory: ancestors = get_ancestors_of("Territory", customer_territory) - customer_territories = [customer_territory] + ancestors + customer_territories = [customer_territory, *ancestors] return customer_territories @@ -408,7 +410,7 @@ def get_service_level_agreement_filters(doctype, name, customer=None): "Service Level Agreement", "entity", "in", - [""] + [customer] + get_customer_group(customer) + get_customer_territory(customer), + ["", customer, *get_customer_group(customer), *get_customer_territory(customer)], ] ) @@ -420,8 +422,7 @@ def get_service_level_agreement_filters(doctype, name, customer=None): ) ], "service_level_agreements": [ - d.name - for d in frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters) + d.name for d in frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters) ], } @@ -482,7 +483,6 @@ def remove_sla_if_applied(doc): def process_sla(doc, sla): - if not doc.creation: doc.creation = now_datetime(doc.get("owner")) if doc.meta.has_field("service_level_agreement_creation"): @@ -712,7 +712,6 @@ def change_service_level_agreement_and_priority(self): and frappe.db.exists("Issue", self.name) and frappe.db.get_single_value("Support Settings", "track_service_level_agreement") ): - if not self.priority == frappe.db.get_value("Issue", self.name, "priority"): self.set_response_and_resolution_time( priority=self.priority, service_level_agreement=self.service_level_agreement @@ -733,9 +732,7 @@ def change_service_level_agreement_and_priority(self): def get_response_and_resolution_duration(doc): sla = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) priority = sla.get_service_level_agreement_priority(doc.priority) - priority.update( - {"support_and_resolution": sla.support_and_resolution, "holiday_list": sla.holiday_list} - ) + priority.update({"support_and_resolution": sla.support_and_resolution, "holiday_list": sla.holiday_list}) return priority @@ -752,7 +749,7 @@ def reset_service_level_agreement(doctype: str, docname: str, reason, user): "reference_doctype": doc.doctype, "reference_name": doc.name, "comment_email": user, - "content": " resetted Service Level Agreement - {0}".format(_(reason)), + "content": f" resetted Service Level Agreement - {_(reason)}", } ).insert(ignore_permissions=True) @@ -860,7 +857,7 @@ def record_assigned_users_on_failure(doc): if assigned_users: from frappe.utils import get_fullname - assigned_users = ", ".join((get_fullname(user) for user in assigned_users)) + assigned_users = ", ".join(get_fullname(user) for user in assigned_users) message = _("First Response SLA Failed by {}").format(assigned_users) doc.add_comment(comment_type="Assigned", text=message) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 472f6bc24eb..aea3eb9fefe 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -51,9 +51,7 @@ class TestServiceLevelAgreement(unittest.TestCase): resolution_time=21600, ) - get_default_service_level_agreement = get_service_level_agreement( - default_service_level_agreement=1 - ) + get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1) self.assertEqual( create_default_service_level_agreement.name, get_default_service_level_agreement.name @@ -198,9 +196,7 @@ class TestServiceLevelAgreement(unittest.TestCase): ) # check default SLA for custom dt - default_sla = get_service_level_agreement( - default_service_level_agreement=1, doctype=doctype.name - ) + default_sla = get_service_level_agreement(default_service_level_agreement=1, doctype=doctype.name) self.assertEqual(sla.name, default_sla.name) # check SLA docfields created @@ -390,14 +386,17 @@ def create_service_level_agreement( resolution_time=0, doctype="Issue", condition="", - sla_fulfilled_on=[], - pause_sla_on=[], + sla_fulfilled_on=None, + pause_sla_on=None, apply_sla_for_resolution=1, service_level=None, start_time="10:00:00", end_time="18:00:00", ): - + if pause_sla_on is None: + pause_sla_on = [] + if sla_fulfilled_on is None: + sla_fulfilled_on = [] make_holiday_list() make_priorities() @@ -484,9 +483,7 @@ def create_service_level_agreement( if sla: frappe.delete_doc("Service Level Agreement", sla, force=1) - return frappe.get_doc(service_level_agreement).insert( - ignore_permissions=True, ignore_if_duplicate=True - ) + return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True, ignore_if_duplicate=True) def create_customer(): @@ -615,7 +612,12 @@ def create_custom_doctype(): "fields": [ {"label": "Date", "fieldname": "date", "fieldtype": "Date"}, {"label": "Description", "fieldname": "desc", "fieldtype": "Long Text"}, - {"label": "Email ID", "fieldname": "email_id", "fieldtype": "Link", "options": "Customer"}, + { + "label": "Email ID", + "fieldname": "email_id", + "fieldtype": "Link", + "options": "Customer", + }, { "label": "Status", "fieldname": "status", @@ -637,8 +639,8 @@ def make_lead(creation=None, index=0, company=None): return frappe.get_doc( { "doctype": "Lead", - "email_id": "test_lead1@example{0}.com".format(index), - "lead_name": "_Test Lead {0}".format(index), + "email_id": f"test_lead1@example{index}.com", + "lead_name": f"_Test Lead {index}", "status": "Open", "creation": creation, "service_level_agreement_creation": creation, diff --git a/erpnext/support/doctype/support_settings/support_settings.js b/erpnext/support/doctype/support_settings/support_settings.js index 78adca81ca5..3a1cb84ba85 100644 --- a/erpnext/support/doctype/support_settings/support_settings.js +++ b/erpnext/support/doctype/support_settings/support_settings.js @@ -1,8 +1,8 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Support Settings', { - refresh: function(frm) { +frappe.ui.form.on("Support Settings", { + refresh: function (frm) { // - } + }, }); diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.js b/erpnext/support/doctype/warranty_claim/warranty_claim.js index 10cb37f5124..d7b77f71c5e 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.js +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.js @@ -43,10 +43,7 @@ frappe.ui.form.on("Warranty Claim", { doctype: "Customer", }; - if ( - !frm.doc.__islocal && - ["Open", "Work In Progress"].includes(frm.doc.status) - ) { + if (!frm.doc.__islocal && ["Open", "Work In Progress"].includes(frm.doc.status)) { frm.add_custom_button(__("Maintenance Visit"), () => { frappe.model.open_mapped_doc({ method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit", diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim_list.js b/erpnext/support/doctype/warranty_claim/warranty_claim_list.js index e162e137ba6..fdafb0ec59f 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim_list.js +++ b/erpnext/support/doctype/warranty_claim/warranty_claim_list.js @@ -1,4 +1,4 @@ -frappe.listview_settings['Warranty Claim'] = { +frappe.listview_settings["Warranty Claim"] = { add_fields: ["status", "customer", "item_code"], - filters:[["status","=", "Open"]] + filters: [["status", "=", "Open"]], }; diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js index 18691fe264f..304515b1f0c 100644 --- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js @@ -3,41 +3,43 @@ /* eslint-disable */ frappe.query_reports["First Response Time for Issues"] = { - "filters": [ + filters: [ { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30) + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_days(frappe.datetime.nowdate(), -30), }, { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "reqd": 1, - "default":frappe.datetime.nowdate() - } + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.nowdate(), + }, ], - get_chart_data: function(_columns, result) { + get_chart_data: function (_columns, result) { return { data: { - labels: result.map(d => d.creation_date), - datasets: [{ - name: 'First Response Time', - values: result.map(d => d.first_response_time) - }] + labels: result.map((d) => d.creation_date), + datasets: [ + { + name: "First Response Time", + values: result.map((d) => d.first_response_time), + }, + ], }, type: "line", tooltipOptions: { - formatTooltipY: d => { + formatTooltipY: (d) => { let duration_options = { hide_days: 0, - hide_seconds: 0 + hide_seconds: 0, }; return frappe.utils.get_formatted_duration(d, duration_options); - } - } - } - } + }, + }, + }; + }, }; diff --git a/erpnext/support/report/issue_analytics/issue_analytics.js b/erpnext/support/report/issue_analytics/issue_analytics.js index 746eee025a5..4680321979f 100644 --- a/erpnext/support/report/issue_analytics/issue_analytics.js +++ b/erpnext/support/report/issue_analytics/issue_analytics.js @@ -3,14 +3,14 @@ /* eslint-disable */ frappe.query_reports["Issue Analytics"] = { - "filters": [ + filters: [ { fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { fieldname: "based_on", @@ -18,125 +18,124 @@ frappe.query_reports["Issue Analytics"] = { fieldtype: "Select", options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"], default: "Customer", - reqd: 1 + reqd: 1, }, { fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: frappe.defaults.get_global_default("year_start_date"), - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: frappe.defaults.get_global_default("year_end_date"), - reqd: 1 + reqd: 1, }, { fieldname: "range", label: __("Range"), fieldtype: "Select", options: [ - { "value": "Weekly", "label": __("Weekly") }, - { "value": "Monthly", "label": __("Monthly") }, - { "value": "Quarterly", "label": __("Quarterly") }, - { "value": "Yearly", "label": __("Yearly") } + { value: "Weekly", label: __("Weekly") }, + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Yearly", label: __("Yearly") }, ], default: "Monthly", - reqd: 1 + reqd: 1, }, { fieldname: "status", label: __("Status"), fieldtype: "Select", - options:[ + options: [ "", - {label: __('Open'), value: 'Open'}, - {label: __('Replied'), value: 'Replied'}, - {label: __('Resolved'), value: 'Resolved'}, - {label: __('Closed'), value: 'Closed'} - ] + { label: __("Open"), value: "Open" }, + { label: __("Replied"), value: "Replied" }, + { label: __("Resolved"), value: "Resolved" }, + { label: __("Closed"), value: "Closed" }, + ], }, { fieldname: "priority", label: __("Issue Priority"), fieldtype: "Link", - options: "Issue Priority" + options: "Issue Priority", }, { fieldname: "customer", label: __("Customer"), fieldtype: "Link", - options: "Customer" + options: "Customer", }, { fieldname: "project", label: __("Project"), fieldtype: "Link", - options: "Project" + options: "Project", }, { fieldname: "assigned_to", label: __("Assigned To"), fieldtype: "Link", - options: "User" - } + options: "User", + }, ], - after_datatable_render: function(datatable_obj) { - $(datatable_obj.wrapper).find(".dt-row-0").find('input[type=checkbox]').click(); + after_datatable_render: function (datatable_obj) { + $(datatable_obj.wrapper).find(".dt-row-0").find("input[type=checkbox]").click(); }, get_datatable_options(options) { return Object.assign(options, { checkboxColumn: true, events: { - onCheckRow: function(data) { + onCheckRow: function (data) { if (data && data.length) { row_name = data[2].content; - row_values = data.slice(3).map(function(column) { + row_values = data.slice(3).map(function (column) { return column.content; - }) - entry = { - 'name': row_name, - 'values': row_values - } + }); + entry = { + name: row_name, + values: row_values, + }; let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; var found = false; - for(var i=0; i < new_datasets.length; i++){ - if (new_datasets[i].name == row_name){ + for (var i = 0; i < new_datasets.length; i++) { + if (new_datasets[i].name == row_name) { found = true; - new_datasets.splice(i,1); + new_datasets.splice(i, 1); break; } } - if (!found){ + if (!found) { new_datasets.push(entry); } let new_data = { labels: raw_data.labels, - datasets: new_datasets - } + datasets: new_datasets, + }; setTimeout(() => { - frappe.query_report.chart.update(new_data) - },500) - + frappe.query_report.chart.update(new_data); + }, 500); setTimeout(() => { frappe.query_report.chart.draw(true); - }, 1000) + }, 1000); frappe.query_report.raw_chart_data = new_data; } }, - } + }, }); - } + }, }; diff --git a/erpnext/support/report/issue_analytics/issue_analytics.py b/erpnext/support/report/issue_analytics/issue_analytics.py index 00ba25a6a97..7f023723c1f 100644 --- a/erpnext/support/report/issue_analytics/issue_analytics.py +++ b/erpnext/support/report/issue_analytics/issue_analytics.py @@ -15,7 +15,7 @@ def execute(filters=None): return IssueAnalytics(filters).run() -class IssueAnalytics(object): +class IssueAnalytics: def __init__(self, filters=None): """Issue Analytics Report""" self.filters = frappe._dict(filters or {}) @@ -44,7 +44,13 @@ class IssueAnalytics(object): elif self.filters.based_on == "Assigned To": self.columns.append( - {"label": _("User"), "fieldname": "user", "fieldtype": "Link", "options": "User", "width": 200} + { + "label": _("User"), + "fieldname": "user", + "fieldtype": "Link", + "options": "User", + "width": 200, + } ) elif self.filters.based_on == "Issue Type": @@ -75,9 +81,7 @@ class IssueAnalytics(object): {"label": _(period), "fieldname": scrub(period), "fieldtype": "Int", "width": 120} ) - self.columns.append( - {"label": _("Total"), "fieldname": "total", "fieldtype": "Int", "width": 120} - ) + self.columns.append({"label": _("Total"), "fieldname": "total", "fieldtype": "Int", "width": 120}) def get_data(self): self.get_issues() @@ -109,9 +113,7 @@ class IssueAnalytics(object): from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) - increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get( - self.filters.range, 1 - ) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(self.filters.range, 1) if self.filters.range in ["Monthly", "Quarterly"]: from_date = from_date.replace(day=1) @@ -121,7 +123,7 @@ class IssueAnalytics(object): from_date = from_date + relativedelta(from_date, weekday=MO(-1)) self.periodic_daterange = [] - for dummy in range(1, 53): + for _dummy in range(1, 53): if self.filters.range == "Weekly": period_end_date = add_days(from_date, 6) else: diff --git a/erpnext/support/report/issue_summary/issue_summary.js b/erpnext/support/report/issue_summary/issue_summary.js index a5122d03ad1..15d55ba6543 100644 --- a/erpnext/support/report/issue_summary/issue_summary.js +++ b/erpnext/support/report/issue_summary/issue_summary.js @@ -3,14 +3,14 @@ /* eslint-disable */ frappe.query_reports["Issue Summary"] = { - "filters": [ + filters: [ { fieldname: "company", label: __("Company"), fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), - reqd: 1 + reqd: 1, }, { fieldname: "based_on", @@ -18,58 +18,58 @@ frappe.query_reports["Issue Summary"] = { fieldtype: "Select", options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"], default: "Customer", - reqd: 1 + reqd: 1, }, { fieldname: "from_date", label: __("From Date"), fieldtype: "Date", default: frappe.defaults.get_global_default("year_start_date"), - reqd: 1 + reqd: 1, }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", default: frappe.defaults.get_global_default("year_end_date"), - reqd: 1 + reqd: 1, }, { fieldname: "status", label: __("Status"), fieldtype: "Select", - options:[ + options: [ "", - {label: __('Open'), value: 'Open'}, - {label: __('Replied'), value: 'Replied'}, - {label: __('On Hold'), value: 'On Hold'}, - {label: __('Resolved'), value: 'Resolved'}, - {label: __('Closed'), value: 'Closed'} - ] + { label: __("Open"), value: "Open" }, + { label: __("Replied"), value: "Replied" }, + { label: __("On Hold"), value: "On Hold" }, + { label: __("Resolved"), value: "Resolved" }, + { label: __("Closed"), value: "Closed" }, + ], }, { fieldname: "priority", label: __("Issue Priority"), fieldtype: "Link", - options: "Issue Priority" + options: "Issue Priority", }, { fieldname: "customer", label: __("Customer"), fieldtype: "Link", - options: "Customer" + options: "Customer", }, { fieldname: "project", label: __("Project"), fieldtype: "Link", - options: "Project" + options: "Project", }, { fieldname: "assigned_to", label: __("Assigned To"), fieldtype: "Link", - options: "User" - } - ] + options: "User", + }, + ], }; diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py index c80ce88222d..d976c2871b2 100644 --- a/erpnext/support/report/issue_summary/issue_summary.py +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -13,7 +13,7 @@ def execute(filters=None): return IssueSummary(filters).run() -class IssueSummary(object): +class IssueSummary: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -41,7 +41,13 @@ class IssueSummary(object): elif self.filters.based_on == "Assigned To": self.columns.append( - {"label": _("User"), "fieldname": "user", "fieldtype": "Link", "options": "User", "width": 200} + { + "label": _("User"), + "fieldname": "user", + "fieldtype": "Link", + "options": "User", + "width": 200, + } ) elif self.filters.based_on == "Issue Type": @@ -84,9 +90,7 @@ class IssueSummary(object): } for label, fieldname in self.sla_status_map.items(): - self.columns.append( - {"label": _(label), "fieldname": fieldname, "fieldtype": "Int", "width": 100} - ) + self.columns.append({"label": _(label), "fieldname": fieldname, "fieldtype": "Int", "width": 100}) self.metrics = [ "Avg First Response Time", @@ -185,8 +189,12 @@ class IssueSummary(object): if d._assign: for entry in json.loads(d._assign): self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(status, 0.0) - self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(agreement_status, 0.0) - self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault("total_issues", 0.0) + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault( + agreement_status, 0.0 + ) + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault( + "total_issues", 0.0 + ) self.issue_summary_data[entry][status] += 1 self.issue_summary_data[entry][agreement_status] += 1 self.issue_summary_data[entry]["total_issues"] += 1 @@ -229,14 +237,20 @@ class IssueSummary(object): if d._assign: for entry in json.loads(d._assign): for metric in metrics_list: - self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(metric, 0.0) + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault( + metric, 0.0 + ) - self.issue_summary_data[entry]["avg_response_time"] += d.get("avg_response_time") or 0.0 + self.issue_summary_data[entry]["avg_response_time"] += ( + d.get("avg_response_time") or 0.0 + ) self.issue_summary_data[entry]["avg_first_response_time"] += ( d.get("first_response_time") or 0.0 ) self.issue_summary_data[entry]["avg_hold_time"] += d.get("total_hold_time") or 0.0 - self.issue_summary_data[entry]["avg_resolution_time"] += d.get("resolution_time") or 0.0 + self.issue_summary_data[entry]["avg_resolution_time"] += ( + d.get("resolution_time") or 0.0 + ) self.issue_summary_data[entry]["avg_user_resolution_time"] += ( d.get("user_resolution_time") or 0.0 ) @@ -251,9 +265,9 @@ class IssueSummary(object): else: data = frappe.db.sql( - """ + f""" SELECT - {0}, AVG(first_response_time) as avg_frt, + {field}, AVG(first_response_time) as avg_frt, AVG(avg_response_time) as avg_resp_time, AVG(total_hold_time) as avg_hold_time, AVG(resolution_time) as avg_resolution_time, @@ -261,10 +275,8 @@ class IssueSummary(object): FROM `tabIssue` WHERE name IN %(issues)s - GROUP BY {0} - """.format( - field - ), + GROUP BY {field} + """, {"issues": issues}, as_dict=1, ) diff --git a/erpnext/support/report/support_hour_distribution/support_hour_distribution.js b/erpnext/support/report/support_hour_distribution/support_hour_distribution.js index ae30b6a5507..8137a50d764 100644 --- a/erpnext/support/report/support_hour_distribution/support_hour_distribution.js +++ b/erpnext/support/report/support_hour_distribution/support_hour_distribution.js @@ -3,20 +3,20 @@ /* eslint-disable */ frappe.query_reports["Support Hour Distribution"] = { - "filters": [ + filters: [ { - 'lable': __("From Date"), - 'fieldname': 'from_date', - 'fieldtype': 'Date', - 'default': frappe.datetime.nowdate(), - 'reqd': 1 + lable: __("From Date"), + fieldname: "from_date", + fieldtype: "Date", + default: frappe.datetime.nowdate(), + reqd: 1, }, { - 'lable': __("To Date"), - 'fieldname': 'to_date', - 'fieldtype': 'Date', - 'default': frappe.datetime.nowdate(), - 'reqd': 1 - } - ] -} + lable: __("To Date"), + fieldname: "to_date", + fieldtype: "Date", + default: frappe.datetime.nowdate(), + reqd: 1, + }, + ], +}; diff --git a/erpnext/support/report/support_hour_distribution/support_hour_distribution.py b/erpnext/support/report/support_hour_distribution/support_hour_distribution.py index 54967213af6..2a978310ddb 100644 --- a/erpnext/support/report/support_hour_distribution/support_hour_distribution.py +++ b/erpnext/support/report/support_hour_distribution/support_hour_distribution.py @@ -37,8 +37,8 @@ def get_data(filters): hours_count = {"date": start_date} for key, value in time_slots.items(): start_time, end_time = value.split("-") - start_time = get_datetime("{0} {1}".format(start_date.strftime("%Y-%m-%d"), start_time)) - end_time = get_datetime("{0} {1}".format(start_date.strftime("%Y-%m-%d"), end_time)) + start_time = get_datetime("{} {}".format(start_date.strftime("%Y-%m-%d"), start_time)) + end_time = get_datetime("{} {}".format(start_date.strftime("%Y-%m-%d"), end_time)) hours_count[key] = get_hours_count(start_time, end_time) time_slot_wise_total_count[key] = time_slot_wise_total_count.get(key, 0) + hours_count[key] diff --git a/erpnext/support/web_form/issues/issues.js b/erpnext/support/web_form/issues/issues.js index ffc5e984253..8f56ebb353d 100644 --- a/erpnext/support/web_form/issues/issues.js +++ b/erpnext/support/web_form/issues/issues.js @@ -1,3 +1,3 @@ -frappe.ready(function() { +frappe.ready(function () { // bind events here -}) +}); diff --git a/erpnext/telephony/doctype/call_log/call_log.js b/erpnext/telephony/doctype/call_log/call_log.js index e7afa0b7d09..1fd15266003 100644 --- a/erpnext/telephony/doctype/call_log/call_log.js +++ b/erpnext/telephony/doctype/call_log/call_log.js @@ -1,21 +1,21 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Call Log', { - refresh: function(frm) { +frappe.ui.form.on("Call Log", { + refresh: function (frm) { frm.events.setup_recording_audio_control(frm); - const incoming_call = frm.doc.type == 'Incoming'; - frm.add_custom_button(incoming_call ? __('Callback'): __('Call Again'), () => { + const incoming_call = frm.doc.type == "Incoming"; + frm.add_custom_button(incoming_call ? __("Callback") : __("Call Again"), () => { const number = incoming_call ? frm.doc.from : frm.doc.to; frappe.phone_call.handler(number, frm); }); }, setup_recording_audio_control(frm) { - const recording_wrapper = frm.get_field('recording_html').$wrapper; - if (!frm.doc.recording_url || frm.doc.recording_url == 'null') { + const recording_wrapper = frm.get_field("recording_html").$wrapper; + if (!frm.doc.recording_url || frm.doc.recording_url == "null") { recording_wrapper.empty(); } else { - recording_wrapper.addClass('input-max-width'); + recording_wrapper.addClass("input-max-width"); recording_wrapper.html(`