diff --git a/.github/helper/install.sh b/.github/helper/install.sh index d1a97f87ff..915a463799 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -4,7 +4,9 @@ set -e cd ~ || exit -sudo apt update && sudo apt install redis-server libcups2-dev +sudo apt update +sudo apt remove mysql-server mysql-client +sudo apt install libcups2-dev redis-server mariadb-client-10.6 pip install frappe-bench @@ -25,14 +27,14 @@ fi if [ "$DB" == "mariadb" ];then - mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" - mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" - mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" - mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe" - mysql --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'" + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe" + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'" - mysql --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES" + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES" fi if [ "$DB" == "postgres" ];then diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml index ee60bad104..70347738f2 100644 --- a/.github/workflows/initiate_release.yml +++ b/.github/workflows/initiate_release.yml @@ -30,23 +30,3 @@ jobs: head: version-${{ matrix.version }}-hotfix env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} - - beta-release: - name: Release - runs-on: ubuntu-latest - strategy: - fail-fast: false - - steps: - - uses: octokit/request-action@v2.x - with: - route: POST /repos/{owner}/{repo}/pulls - owner: frappe - repo: erpnext - title: |- - "chore: release v15 beta" - body: "Automated beta release." - base: version-15-beta - head: develop - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 07b8de7a90..3514f0d2d9 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -28,7 +28,7 @@ jobs: MARIADB_ROOT_PASSWORD: 'root' ports: - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - name: Clone @@ -134,6 +134,7 @@ jobs: } update_to_version 14 + update_to_version 15 echo "Updating to latest version" git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 559be06993..ccdfc8c109 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -47,7 +47,7 @@ jobs: MARIADB_ROOT_PASSWORD: 'root' ports: - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - name: Clone diff --git a/.mergify.yml b/.mergify.yml index 804b27d435..53596060b1 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -17,6 +17,7 @@ pull_request_rules: - base=version-12 - base=version-14 - base=version-15 + - base=version-16 actions: close: comment: @@ -24,16 +25,6 @@ pull_request_rules: @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch. https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch - - name: Auto-close PRs on pre-release branch - conditions: - - base=version-13-pre-release - actions: - close: - comment: - message: | - @{{author}}, pre-release branch is not maintained anymore. Releases are directly done by merging hotfix branch to stable branches. - - - name: backport to develop conditions: - label="backport develop" @@ -54,13 +45,13 @@ pull_request_rules: assignees: - "{{ author }}" - - name: backport to version-14-pre-release + - name: backport to version-15-hotfix conditions: - - label="backport version-14-pre-release" + - label="backport version-15-hotfix" actions: backport: branches: - - version-14-pre-release + - version-15-hotfix assignees: - "{{ author }}" @@ -74,35 +65,6 @@ pull_request_rules: assignees: - "{{ author }}" - - name: backport to version-13-pre-release - conditions: - - label="backport version-13-pre-release" - actions: - backport: - branches: - - version-13-pre-release - assignees: - - "{{ author }}" - - - name: backport to version-12-hotfix - conditions: - - label="backport version-12-hotfix" - actions: - backport: - branches: - - version-12-hotfix - assignees: - - "{{ author }}" - - - name: backport to version-12-pre-release - conditions: - - label="backport version-12-pre-release" - actions: - backport: - branches: - - version-12-pre-release - assignees: - - "{{ author }}" - name: Automatic merge on CI success and review conditions: diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ni_catalogo_de_cuentas.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ni_catalogo_de_cuentas.json index e8402d6d7e..73ac4ab3c8 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ni_catalogo_de_cuentas.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ni_catalogo_de_cuentas.json @@ -1,6 +1,6 @@ { "country_code": "ni", - "name": "Nicaragua - Catalogo de Cuentas", + "name": "Nicaragua - Catálogo de Cuentas", "tree": { "Activo": { "Activo Corriente": { @@ -491,4 +491,4 @@ "root_type": "Liability" } } -} \ No newline at end of file +} 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 e75af7047f..d06bd833c8 100644 --- a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py +++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py @@ -37,6 +37,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date): } ) cle.flags.ignore_permissions = True + cle.flags.ignore_links = True cle.submit() diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 3a2c3cbeeb..8f76492d4e 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -302,3 +302,30 @@ def get_dimensions(with_cost_center_and_project=False): default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension return dimension_filters, default_dimensions_map + + +def create_accounting_dimensions_for_doctype(doctype): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + for d in accounting_dimensions: + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": "accounting_dimensions_section", + } + + create_custom_field(doctype, df, ignore_validate=True) + + frappe.clear_cache(doctype=doctype) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 6857ba343e..061bab320e 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -32,6 +32,7 @@ "column_break_19", "add_taxes_from_item_tax_template", "book_tax_discount_loss", + "round_row_wise_tax", "print_settings", "show_inclusive_tax_in_print", "show_taxes_as_table_in_print", @@ -414,6 +415,13 @@ "fieldname": "ignore_account_closing_balance", "fieldtype": "Check", "label": "Ignore Account Closing Balance" + }, + { + "default": "0", + "description": "Tax Amount will be rounded on a row(items) level", + "fieldname": "round_row_wise_tax", + "fieldtype": "Check", + "label": "Round Tax Amount Row-wise" } ], "icon": "icon-cog", @@ -421,7 +429,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-07-27 15:05:34.000264", + "modified": "2023-08-28 00:12:02.740633", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index c7404d11bc..ace751b4a2 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -35,13 +35,14 @@ class TestBankClearance(unittest.TestCase): from lending.loan_management.doctype.loan.test_loan import ( create_loan, create_loan_accounts, - create_loan_type, + create_loan_product, create_repayment_entry, make_loan_disbursement_entry, ) def create_loan_masters(): - create_loan_type( + create_loan_product( + "Clearance Loan", "Clearance Loan", 2000000, 13.5, 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 04af32346b..db68dfad79 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -2,6 +2,16 @@ // For license information, please see license.txt frappe.ui.form.on("Bank Statement Import", { + onload(frm) { + frm.set_query("bank_account", function (doc) { + return { + filters: { + company: doc.company, + }, + }; + }); + }, + setup(frm) { frappe.realtime.on("data_import_refresh", ({ data_import }) => { frm.import_in_progress = false; @@ -352,10 +362,11 @@ frappe.ui.form.on("Bank Statement Import", { export_errored_rows(frm) { open_url_post( - "/api/method/frappe.core.doctype.data_import.data_import.download_errored_template", + "/api/method/erpnext.accounts.doctype.bank_statement_import.bank_statement_import.download_errored_template", { data_import_name: frm.doc.name, - } + }, + true ); }, diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 0c328ff46c..4a6491d086 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -410,7 +410,7 @@ def add_vouchers(): def create_loan_and_repayment(): from lending.loan_management.doctype.loan.test_loan import ( create_loan, - create_loan_type, + create_loan_product, create_repayment_entry, make_loan_disbursement_entry, ) @@ -420,7 +420,8 @@ def create_loan_and_repayment(): from erpnext.setup.doctype.employee.test_employee import make_employee - create_loan_type( + create_loan_product( + "Personal Loan", "Personal Loan", 500000, 8.4, @@ -441,7 +442,7 @@ def create_loan_and_repayment(): "applicant_type": "Employee", "company": "_Test Company", "applicant": applicant, - "loan_type": "Personal Loan", + "loan_product": "Personal Loan", "loan_amount": 5000, "repayment_method": "Repay Fixed Amount per Period", "monthly_repayment_amount": 500, 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 c62b711f2c..df232a5848 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", + "access_key", "url", "column_break_3", "help", @@ -84,12 +85,18 @@ "fieldname": "disabled", "fieldtype": "Check", "label": "Disabled" + }, + { + "depends_on": "eval:doc.service_provider == 'exchangerate.host';", + "fieldname": "access_key", + "fieldtype": "Data", + "label": "Access Key" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-01-09 12:19:03.955906", + "modified": "2023-10-04 15:30:25.333860", "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 d618c5ca11..117d5ff21e 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -18,11 +18,21 @@ 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( + frappe.bold(self.service_provider) + ) + ) + self.set("result_key", []) self.set("req_params", []) self.api_endpoint = "https://api.exchangerate.host/convert" 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"}) self.append("req_params", {"key": "date", "value": "{transaction_date}"}) self.append("req_params", {"key": "from", "value": "{from_currency}"}) self.append("req_params", {"key": "to", "value": "{to_currency}"}) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index cdd1203d49..22b6880ad5 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -53,7 +53,15 @@ frappe.ui.form.on("Journal Entry", { erpnext.accounts.unreconcile_payments.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")); + 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)])); + } + } + }, make_inter_company_journal_entry: function(frm) { var d = new frappe.ui.Dialog({ title: __("Select Company"), 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 1e22c64c8f..02c2c6704a 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 @@ -218,6 +218,7 @@ def make_customer(customer=None): "territory": "All Territories", } ) + if not frappe.db.exists("Customer", customer_name): customer.insert(ignore_permissions=True) return customer.name diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 38a520996c..e6403fddef 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -271,16 +271,18 @@ class PaymentEntry(AccountsController): # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key latest = latest.get(d.payment_term) or latest.get(None) - # The reference has already been fully paid if not latest: frappe.throw( _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) ) # The reference has already been partly paid - elif latest.outstanding_amount < latest.invoice_amount and flt( - d.outstanding_amount, d.precision("outstanding_amount") - ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")): + elif ( + latest.outstanding_amount < latest.invoice_amount + and flt(d.outstanding_amount, d.precision("outstanding_amount")) + != flt(latest.outstanding_amount, d.precision("outstanding_amount")) + and d.payment_term == "" + ): frappe.throw( _( "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." @@ -1751,11 +1753,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company): "voucher_type": d.voucher_type, "posting_date": d.posting_date, "invoice_amount": flt(d.invoice_amount), - "outstanding_amount": flt(d.outstanding_amount), - "payment_term_outstanding": payment_term_outstanding, - "allocated_amount": payment_term_outstanding + "outstanding_amount": payment_term_outstanding if payment_term_outstanding else d.outstanding_amount, + "payment_term_outstanding": payment_term_outstanding, "payment_amount": payment_term.payment_amount, "payment_term": payment_term.payment_term, "account": d.account, diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 11d6d5f433..5f0b434c70 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -1,13 +1,9 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - import json import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt, get_url, nowdate +from frappe.utils import flt, nowdate from frappe.utils.background_jobs import enqueue from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -20,7 +16,6 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import ( from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate from erpnext.accounts.party import get_party_account, get_party_bank_account from erpnext.accounts.utils import get_account_currency -from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscription from erpnext.utilities import payment_app_import_guard @@ -249,7 +244,7 @@ class PaymentRequest(Document): if ( party_account_currency == ref_doc.company_currency and party_account_currency != self.currency ): - party_amount = ref_doc.base_grand_total + party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") else: party_amount = self.grand_total @@ -364,35 +359,11 @@ class PaymentRequest(Document): def get_payment_success_url(self): return self.payment_success_url - def on_payment_authorized(self, status=None): - if not status: - return - - shopping_cart_settings = frappe.get_doc("E Commerce Settings") - - if status in ["Authorized", "Completed"]: - redirect_to = None - self.set_as_paid() - - # if shopping cart enabled and in session - if ( - shopping_cart_settings.enabled - 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)) - - return redirect_to - def create_subscription(self, payment_provider, gateway_controller, data): if payment_provider == "stripe": + with payment_app_import_guard(): + from payments.payment_gateways.stripe_integration import create_stripe_subscription + return create_stripe_subscription(gateway_controller, data) @@ -544,13 +515,12 @@ def get_existing_payment_request_amount(ref_dt, ref_dn): def get_gateway_details(args): # nosemgrep - """return gateway and payment account of default payment gateway""" - if args.get("payment_gateway_account"): - return get_payment_gateway_account(args.get("payment_gateway_account")) - - if args.order_type == "Shopping Cart": - payment_gateway_account = frappe.get_doc("E Commerce Settings").payment_gateway_account - return get_payment_gateway_account(payment_gateway_account) + """ + Return gateway and payment account of default payment gateway + """ + gateway_account = args.get("payment_gateway_account", {"is_default": 1}) + if gateway_account: + return get_payment_gateway_account(gateway_account) gateway_account = get_payment_gateway_account({"is_default": 1}) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 00c402f97b..887f1eaeb1 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe import _ +from frappe.utils import add_days, nowdate from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile @@ -125,70 +126,64 @@ class TestPOSInvoice(unittest.TestCase): self.assertEqual(inv.grand_total, 5474.0) def test_tax_calculation_with_item_tax_template(self): - inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=1) - item_row = inv.get("items")[0] + import json - add_items = [ - (54, "_Test Account Excise Duty @ 12 - _TC"), - (288, "_Test Account Excise Duty @ 15 - _TC"), - (144, "_Test Account Excise Duty @ 20 - _TC"), - (430, "_Test Item Tax Template 1 - _TC"), + from erpnext.stock.get_item_details import get_item_details + + # set tax template in item + item = frappe.get_cached_doc("Item", "_Test Item") + item.set( + "taxes", + [ + { + "item_tax_template": "_Test Account Excise Duty @ 15 - _TC", + "valid_from": add_days(nowdate(), -5), + } + ], + ) + item.save() + + # create POS invoice with item + pos_inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=True) + item_details = get_item_details( + doc=pos_inv, + args={ + "item_code": item.item_code, + "company": pos_inv.company, + "doctype": "POS Invoice", + "conversion_rate": 1.0, + }, + ) + tax_map = json.loads(item_details.item_tax_rate) + for tax in tax_map: + pos_inv.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": tax, + "rate": tax_map[tax], + "description": "Test", + "cost_center": "_Test Cost Center - _TC", + }, + ) + pos_inv.submit() + pos_inv.load_from_db() + + # check if correct tax values are applied from tax template + self.assertEqual(pos_inv.net_total, 386.4) + + expected_taxes = [ + { + "tax_amount": 57.96, + "total": 444.36, + }, ] - for qty, item_tax_template in add_items: - item_row_copy = copy.deepcopy(item_row) - item_row_copy.qty = qty - item_row_copy.item_tax_template = item_tax_template - inv.append("items", item_row_copy) - inv.append( - "taxes", - { - "account_head": "_Test Account Excise Duty - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "rate": 11, - }, - ) - inv.append( - "taxes", - { - "account_head": "_Test Account Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 0, - }, - ) - inv.append( - "taxes", - { - "account_head": "_Test Account S&H Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "S&H Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 3, - }, - ) - inv.insert() + for i in range(len(expected_taxes)): + for key in expected_taxes[i]: + self.assertEqual(expected_taxes[i][key], pos_inv.get("taxes")[i].get(key)) - self.assertEqual(inv.net_total, 4600) - - self.assertEqual(inv.get("taxes")[0].tax_amount, 502.41) - self.assertEqual(inv.get("taxes")[0].total, 5102.41) - - self.assertEqual(inv.get("taxes")[1].tax_amount, 197.80) - self.assertEqual(inv.get("taxes")[1].total, 5300.21) - - self.assertEqual(inv.get("taxes")[2].tax_amount, 375.36) - self.assertEqual(inv.get("taxes")[2].total, 5675.57) - - self.assertEqual(inv.grand_total, 5675.57) - self.assertEqual(inv.rounding_adjustment, 0.43) - self.assertEqual(inv.rounded_total, 5676.0) + self.assertEqual(pos_inv.get("base_total_taxes_and_charges"), 57.96) def test_tax_calculation_with_multiple_items_and_discount(self): inv = create_pos_invoice(qty=1, rate=75, do_not_save=True) 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 d42b1e4cd1..c161dac33f 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 @@ -454,7 +454,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): except Exception as e: frappe.db.rollback() message_log = frappe.message_log.pop() if frappe.message_log else str(e) - error_message = safe_load_json(message_log) + error_message = get_error_message(message_log) if closing_entry: closing_entry.set_status(update=True, status="Failed") @@ -483,7 +483,7 @@ def cancel_merge_logs(merge_logs, closing_entry=None): except Exception as e: frappe.db.rollback() message_log = frappe.message_log.pop() if frappe.message_log else str(e) - error_message = safe_load_json(message_log) + error_message = get_error_message(message_log) if closing_entry: closing_entry.set_status(update=True, status="Submitted") @@ -525,10 +525,8 @@ def check_scheduler_status(): frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive")) -def safe_load_json(message): +def get_error_message(message) -> str: try: - json_message = json.loads(message).get("message") + return message["message"] except Exception: - json_message = message - - return json_message + return str(message) 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 ee1c9cd208..6c959ba2f0 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 @@ -48,6 +48,20 @@ class ProcessStatementOfAccounts(Document): def get_report_pdf(doc, consolidated=True): + statement_dict = get_statement_dict(doc) + if not bool(statement_dict): + return False + elif consolidated: + delimiter = '
' if doc.include_break else "" + result = delimiter.join(list(statement_dict.values())) + return get_pdf(result, {"orientation": doc.orientation}) + else: + for customer, statement_html in statement_dict.items(): + statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation}) + return statement_dict + + +def get_statement_dict(doc, get_statement_dict=False): statement_dict = {} ageing = "" @@ -78,18 +92,11 @@ def get_report_pdf(doc, consolidated=True): if not res: continue - statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing) + statement_dict[entry.customer] = ( + [res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing) + ) - if not bool(statement_dict): - return False - elif consolidated: - delimiter = '' if doc.include_break else "" - result = delimiter.join(list(statement_dict.values())) - return get_pdf(result, {"orientation": doc.orientation}) - else: - for customer, statement_html in statement_dict.items(): - statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation}) - return statement_dict + return statement_dict def set_ageing(doc, entry): @@ -102,7 +109,8 @@ def set_ageing(doc, entry): "range2": 60, "range3": 90, "range4": 120, - "customer": entry.customer, + "party_type": "Customer", + "party": [entry.customer], } ) col1, ageing = get_ageing(ageing_filters) @@ -146,7 +154,7 @@ def get_ar_filters(doc, entry): return { "report_date": doc.posting_date if doc.posting_date else None, "party_type": "Customer", - "party": entry.customer, + "party": [entry.customer], "customer_name": entry.customer_name if entry.customer_name else None, "payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None, "sales_partner": doc.sales_partner if doc.sales_partner else None, 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 fb0d8d152f..a3a74df402 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 @@ -4,39 +4,107 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate, today from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import ( + get_statement_dict, send_emails, ) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -class TestProcessStatementOfAccounts(unittest.TestCase): +class TestProcessStatementOfAccounts(AccountsTestMixin, FrappeTestCase): def setUp(self): + self.create_company() + self.create_customer() + self.create_customer(customer_name="Other Customer") + self.clear_old_entries() self.si = create_sales_invoice() - self.process_soa = create_process_soa() + create_sales_invoice(customer="Other Customer") + + def test_process_soa_for_gl(self): + """Tests the utils for Statement of Accounts(General Ledger)""" + process_soa = create_process_soa( + name="_Test Process SOA for GL", + customers=[{"customer": "_Test Customer"}, {"customer": "Other Customer"}], + ) + statement_dict = get_statement_dict(process_soa, get_statement_dict=True) + + # Checks if the statements are filtered based on the Customer + self.assertIn("Other Customer", statement_dict) + self.assertIn("_Test Customer", statement_dict) + + # Checks if the correct number of receivable entries exist + # 3 rows for opening and closing and 1 row for SI + receivable_entries = statement_dict["_Test Customer"][0] + self.assertEqual(len(receivable_entries), 4) + + # Checks the amount for the receivable entry + self.assertEqual(receivable_entries[1].voucher_no, self.si.name) + self.assertEqual(receivable_entries[1].balance, 100) + + def test_process_soa_for_ar(self): + """Tests the utils for Statement of Accounts(Accounts Receivable)""" + process_soa = create_process_soa(name="_Test Process SOA for AR", report="Accounts Receivable") + statement_dict = get_statement_dict(process_soa, get_statement_dict=True) + + # Checks if the statements are filtered based on the Customer + self.assertNotIn("Other Customer", statement_dict) + self.assertIn("_Test Customer", statement_dict) + + # Checks if the correct number of receivable entries exist + receivable_entries = statement_dict["_Test Customer"][0] + self.assertEqual(len(receivable_entries), 1) + + # Checks the amount for the receivable entry + self.assertEqual(receivable_entries[0].voucher_no, self.si.name) + self.assertEqual(receivable_entries[0].total_due, 100) + + # Checks the ageing summary for AR + ageing_summary = statement_dict["_Test Customer"][1][0] + expected_summary = frappe._dict( + range1=100, + range2=0, + range3=0, + range4=0, + range5=0, + ) + self.check_ageing_summary(ageing_summary, expected_summary) def test_auto_email_for_process_soa_ar(self): - send_emails(self.process_soa.name, from_scheduler=True) - self.process_soa.load_from_db() - self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7))) + process_soa = create_process_soa( + name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable" + ) + send_emails(process_soa.name, from_scheduler=True) + process_soa.load_from_db() + self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7))) + + def check_ageing_summary(self, ageing, expected_ageing): + for age_range in expected_ageing: + self.assertEqual(expected_ageing[age_range], ageing.get(age_range)) def tearDown(self): - frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA") + frappe.db.rollback() -def create_process_soa(): - frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA") +def create_process_soa(**args): + args = frappe._dict(args) + frappe.delete_doc_if_exists("Process Statement Of Accounts", args.name) process_soa = frappe.new_doc("Process Statement Of Accounts") - soa_dict = { - "name": "Test Process SOA", - "company": "_Test Company", - } + soa_dict = frappe._dict( + name=args.name, + company=args.company or "_Test Company", + customers=args.customers or [{"customer": "_Test Customer"}], + enable_auto_email=1 if args.enable_auto_email else 0, + frequency=args.frequency or "Weekly", + report=args.report or "General Ledger", + from_date=args.from_date or getdate(today()), + to_date=args.to_date or getdate(today()), + posting_date=args.posting_date or getdate(today()), + include_ageing=1, + ) process_soa.update(soa_dict) - process_soa.set("customers", [{"customer": "_Test Customer"}]) - process_soa.enable_auto_email = 1 - process_soa.frequency = "Weekly" - process_soa.report = "Accounts Receivable" process_soa.save() return process_soa diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 095617dbcf..2eaa33767c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -479,6 +479,12 @@ cur_frm.set_query("expense_account", "items", function(doc) { } }); +cur_frm.set_query("wip_composite_asset", "items", function() { + return { + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } +}); + cur_frm.cscript.expense_account = function(doc, cdt, cdn){ var d = locals[cdt][cdn]; if(d.idx == 1 && d.expense_account){ diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index f3c01816cc..2d1f4451b6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -36,6 +36,7 @@ "currency_and_price_list", "currency", "conversion_rate", + "use_transaction_date_exchange_rate", "column_break2", "buying_price_list", "price_list_currency", @@ -1385,6 +1386,7 @@ "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Supplier Warehouse", "no_copy": 1, "options": "Warehouse", @@ -1587,13 +1589,20 @@ "label": "Repost Required", "options": "Account", "read_only": 1 + }, + { + "default": "0", + "fieldname": "use_transaction_date_exchange_rate", + "fieldtype": "Check", + "label": "Use Transaction Date Exchange Rate", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-09-21 12:22:04.545106", + "modified": "2023-10-16 16:24:51.886231", "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 85ed1260d3..2f08b65ac6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -33,7 +33,7 @@ from erpnext.accounts.general_ledger import ( ) from erpnext.accounts.party import get_due_date, get_party_account from erpnext.accounts.utils import get_account_currency, get_fiscal_year -from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled +from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.accounts_controller import validate_account_head @@ -281,9 +281,6 @@ class PurchaseInvoice(BuyingController): # in case of auto inventory accounting, # expense account is always "Stock Received But Not Billed" for a stock item # except opening entry, drop-ship entry and fixed asset items - if item.item_code: - asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") - if ( auto_accounting_for_stock and item.item_code in stock_items @@ -350,22 +347,26 @@ class PurchaseInvoice(BuyingController): frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = stock_not_billed_account - - elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): + elif item.is_fixed_asset and item.pr_detail: + if not asset_received_but_not_billed: + asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") + item.expense_account = asset_received_but_not_billed + elif item.is_fixed_asset: + account_type = ( + "capital_work_in_progress_account" + if is_cwip_accounting_enabled(item.asset_category) + else "fixed_asset_account" + ) asset_category_account = get_asset_category_account( - "fixed_asset_account", item=item.item_code, company=self.company + account_type, item=item.item_code, company=self.company ) if not asset_category_account: - form_link = get_link_to_form("Asset Category", asset_category) + form_link = get_link_to_form("Asset Category", item.asset_category) throw( _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company), title=_("Missing Account"), ) item.expense_account = asset_category_account - elif item.is_fixed_asset and item.pr_detail: - if not asset_received_but_not_billed: - asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") - item.expense_account = asset_received_but_not_billed elif not item.expense_account and for_validate: throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name)) @@ -539,8 +540,9 @@ class PurchaseInvoice(BuyingController): ] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_for_repost() - self.db_set("repost_required", self.needs_repost) + if self.needs_repost: + self.validate_for_repost() + self.db_set("repost_required", self.needs_repost) def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: @@ -586,6 +588,7 @@ class PurchaseInvoice(BuyingController): if self.auto_accounting_for_stock: self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + self.asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") else: self.stock_received_but_not_billed = None self.expenses_included_in_valuation = None @@ -597,9 +600,6 @@ class PurchaseInvoice(BuyingController): self.make_item_gl_entries(gl_entries) self.make_precision_loss_gl_entry(gl_entries) - if self.check_asset_cwip_enabled(): - self.get_asset_gl_entry(gl_entries) - self.make_tax_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) @@ -701,7 +701,11 @@ class PurchaseInvoice(BuyingController): if item.item_code: asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") - if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items: + if ( + self.update_stock + and self.auto_accounting_for_stock + and (item.item_code in stock_items or item.is_fixed_asset) + ): # warehouse account warehouse_debit_amount = self.make_stock_adjustment_entry( gl_entries, item, voucher_wise_stock_value, account_currency @@ -816,9 +820,7 @@ class PurchaseInvoice(BuyingController): ) ) - elif not item.is_fixed_asset or ( - item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category) - ): + else: expense_account = ( item.expense_account if (not item.enable_deferred_expense or self.is_return) @@ -969,11 +971,16 @@ class PurchaseInvoice(BuyingController): (item.purchase_receipt, valuation_tax_accounts), ) + stock_rbnb = ( + self.asset_received_but_not_billed + if item.is_fixed_asset + else self.stock_received_but_not_billed + ) if not negative_expense_booked_in_pr: gl_entries.append( self.get_gl_dict( { - "account": self.stock_received_but_not_billed, + "account": stock_rbnb, "against": self.supplier, "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), "remarks": self.remarks or _("Accounting Entry for Stock"), @@ -988,156 +995,12 @@ class PurchaseInvoice(BuyingController): item.item_tax_amount, item.precision("item_tax_amount") ) - def get_asset_gl_entry(self, gl_entries): - arbnb_account = None - eiiav_account = None - asset_eiiav_currency = None - - for item in self.get("items"): - if item.is_fixed_asset: - asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) - base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) - - item_exp_acc_type = frappe.get_cached_value("Account", item.expense_account, "account_type") - if not item.expense_account or item_exp_acc_type not in [ - "Asset Received But Not Billed", - "Fixed Asset", - ]: - if not arbnb_account: - arbnb_account = self.get_company_default("asset_received_but_not_billed") - item.expense_account = arbnb_account - - if not self.update_stock: - arbnb_currency = get_account_currency(item.expense_account) - gl_entries.append( - self.get_gl_dict( - { - "account": item.expense_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "debit": base_asset_amount, - "debit_in_account_currency": ( - base_asset_amount if arbnb_currency == self.company_currency else asset_amount - ), - "cost_center": item.cost_center, - "project": item.project or self.project, - }, - item=item, - ) - ) - - if item.item_tax_amount: - if not eiiav_account or not asset_eiiav_currency: - eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") - asset_eiiav_currency = get_account_currency(eiiav_account) - - gl_entries.append( - self.get_gl_dict( - { - "account": eiiav_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "cost_center": item.cost_center, - "project": item.project or self.project, - "credit": item.item_tax_amount, - "credit_in_account_currency": ( - item.item_tax_amount - if asset_eiiav_currency == self.company_currency - else item.item_tax_amount / self.conversion_rate - ), - }, - item=item, - ) - ) - else: - cwip_account = get_asset_account( - "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company - ) - - cwip_account_currency = get_account_currency(cwip_account) - gl_entries.append( - self.get_gl_dict( - { - "account": cwip_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "debit": base_asset_amount, - "debit_in_account_currency": ( - base_asset_amount if cwip_account_currency == self.company_currency else asset_amount - ), - "cost_center": self.cost_center, - "project": item.project or self.project, - }, - item=item, - ) - ) - - if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)): - if not eiiav_account or not asset_eiiav_currency: - eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") - asset_eiiav_currency = get_account_currency(eiiav_account) - - gl_entries.append( - self.get_gl_dict( - { - "account": eiiav_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "cost_center": item.cost_center, - "credit": item.item_tax_amount, - "project": item.project or self.project, - "credit_in_account_currency": ( - item.item_tax_amount - if asset_eiiav_currency == self.company_currency - else item.item_tax_amount / self.conversion_rate - ), - }, - item=item, - ) - ) - - # Assets are bought through this document then it will be linked to this document - if flt(item.landed_cost_voucher_amount): - if not eiiav_account: - eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") - - gl_entries.append( - self.get_gl_dict( - { - "account": eiiav_account, - "against": cwip_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project, - }, - item=item, - ) - ) - - gl_entries.append( - self.get_gl_dict( - { - "account": cwip_account, - "against": eiiav_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project, - }, - item=item, - ) - ) - - # update gross amount of assets 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)) - - return gl_entries + 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)) def make_stock_adjustment_entry( self, gl_entries, item, voucher_wise_stock_value, account_currency diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 0aaea060b5..13593bcf9b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -5,7 +5,7 @@ import unittest import frappe -from frappe.tests.utils import change_settings +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, flt, getdate, nowdate, today import erpnext @@ -38,7 +38,7 @@ test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Templ test_ignore = ["Serial No"] -class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): +class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): @classmethod def setUpClass(self): unlink_payment_on_cancel_of_invoice() @@ -48,6 +48,9 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): def tearDownClass(self): unlink_payment_on_cancel_of_invoice(0) + def tearDown(self): + frappe.db.rollback() + def test_purchase_invoice_received_qty(self): """ 1. Test if received qty is validated against accepted + rejected @@ -422,6 +425,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertEqual(tax.tax_amount, expected_values[i][1]) self.assertEqual(tax.total, expected_values[i][2]) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_purchase_invoice_with_advance(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, @@ -476,6 +480,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): ) ) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_invoice_with_advance_and_multi_payment_terms(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, @@ -1220,6 +1225,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_gain_loss_with_advance_entry(self): unlink_enabled = frappe.db.get_value( "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" @@ -1420,6 +1426,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): ) frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_purchase_invoice_advance_taxes(self): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -1916,6 +1923,56 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): pi.load_from_db() self.assertFalse(pi.repost_required) + @change_settings("Buying Settings", {"supplier_group": None}) + def test_purchase_invoice_without_supplier_group(self): + # Create a Supplier + test_supplier_name = "_Test Supplier Without Supplier Group" + if not frappe.db.exists("Supplier", test_supplier_name): + supplier = frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": test_supplier_name, + } + ).insert(ignore_permissions=True) + + self.assertEqual(supplier.supplier_group, None) + + po = create_purchase_order( + supplier=test_supplier_name, + rate=3000, + item="_Test Non Stock Item", + posting_date="2021-09-15", + ) + + pi = make_purchase_invoice(supplier=test_supplier_name) + + self.assertEqual(po.docstatus, 1) + self.assertEqual(pi.docstatus, 1) + + def test_default_cost_center_for_purchase(self): + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + + for c_center in ["_Test Cost Center Selling", "_Test Cost Center Buying"]: + create_cost_center(cost_center_name=c_center) + + item = create_item( + "_Test Cost Center Item For Purchase", + is_stock_item=1, + buying_cost_center="_Test Cost Center Buying - _TC", + selling_cost_center="_Test Cost Center Selling - _TC", + ) + + pi = make_purchase_invoice( + item=item.name, qty=1, rate=1000, update_stock=True, do_not_submit=True, cost_center="" + ) + + pi.items[0].cost_center = "" + pi.set_missing_values() + pi.calculate_taxes_and_totals() + pi.save() + + self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC") + def set_advance_flag(company, flag, default_account): frappe.db.set_value( 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 3690142aac..424e942990 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -77,6 +77,7 @@ "manufacturer_part_no", "accounting", "expense_account", + "wip_composite_asset", "col_break5", "is_fixed_asset", "asset_location", @@ -903,12 +904,18 @@ "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-26 12:54:53.178156", + "modified": "2023-10-03 21:01:01.824892", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f380825db7..f6d9c93261 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -536,8 +536,9 @@ class SalesInvoice(SellingController): "taxes": ("account_head",), } self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_for_repost() - self.db_set("repost_required", self.needs_repost) + if self.needs_repost: + self.validate_for_repost() + self.db_set("repost_required", self.needs_repost) def set_paid_amount(self): paid_amount = 0.0 diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 84b0149942..16477324e6 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -6,7 +6,7 @@ import unittest import frappe from frappe.model.dynamic_links import get_dynamic_link_map -from frappe.tests.utils import change_settings +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, getdate, nowdate, today import erpnext @@ -26,6 +26,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched from erpnext.controllers.accounts_controller import update_invoice_status from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency +from erpnext.selling.doctype.customer.test_customer import get_customer_dict from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -44,13 +45,17 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import from erpnext.stock.utils import get_incoming_rate, get_stock_balance -class TestSalesInvoice(unittest.TestCase): +class TestSalesInvoice(FrappeTestCase): def setUp(self): from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) create_internal_parties() setup_accounts() + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + + def tearDown(self): + frappe.db.rollback() def make(self): w = frappe.copy_doc(test_records[0]) @@ -178,6 +183,7 @@ class TestSalesInvoice(unittest.TestCase): self.assertRaises(frappe.LinkExistsError, si.cancel) unlink_payment_on_cancel_of_invoice() + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_payment_entry_unlink_against_standalone_credit_note(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -510,70 +516,72 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.grand_total, 5474.0) def test_tax_calculation_with_item_tax_template(self): + import json + + from erpnext.stock.get_item_details import get_item_details + + # set tax template in item + item = frappe.get_cached_doc("Item", "_Test Item") + item.set( + "taxes", + [ + { + "item_tax_template": "_Test Item Tax Template 1 - _TC", + "valid_from": add_days(nowdate(), -5), + } + ], + ) + item.save() + + # create sales invoice with item si = create_sales_invoice(qty=84, rate=4.6, do_not_save=True) - item_row = si.get("items")[0] + item_details = get_item_details( + doc=si, + args={ + "item_code": item.item_code, + "company": si.company, + "doctype": "Sales Invoice", + "conversion_rate": 1.0, + }, + ) + tax_map = json.loads(item_details.item_tax_rate) + for tax in tax_map: + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": tax, + "rate": tax_map[tax], + "description": "Test", + "cost_center": "_Test Cost Center - _TC", + }, + ) + si.submit() + si.load_from_db() - add_items = [ - (54, "_Test Account Excise Duty @ 12 - _TC"), - (288, "_Test Account Excise Duty @ 15 - _TC"), - (144, "_Test Account Excise Duty @ 20 - _TC"), - (430, "_Test Item Tax Template 1 - _TC"), + # check if correct tax values are applied from tax template + self.assertEqual(si.net_total, 386.4) + + expected_taxes = [ + { + "tax_amount": 19.32, + "total": 405.72, + }, + { + "tax_amount": 38.64, + "total": 444.36, + }, + { + "tax_amount": 57.96, + "total": 502.32, + }, ] - for qty, item_tax_template in add_items: - item_row_copy = copy.deepcopy(item_row) - item_row_copy.qty = qty - item_row_copy.item_tax_template = item_tax_template - si.append("items", item_row_copy) - si.append( - "taxes", - { - "account_head": "_Test Account Excise Duty - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "rate": 11, - }, - ) - si.append( - "taxes", - { - "account_head": "_Test Account Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 0, - }, - ) - si.append( - "taxes", - { - "account_head": "_Test Account S&H Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "S&H Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 3, - }, - ) - si.insert() + for i in range(len(expected_taxes)): + for key in expected_taxes[i]: + self.assertEqual(expected_taxes[i][key], si.get("taxes")[i].get(key)) - self.assertEqual(si.net_total, 4600) - - self.assertEqual(si.get("taxes")[0].tax_amount, 502.41) - self.assertEqual(si.get("taxes")[0].total, 5102.41) - - self.assertEqual(si.get("taxes")[1].tax_amount, 197.80) - self.assertEqual(si.get("taxes")[1].total, 5300.21) - - self.assertEqual(si.get("taxes")[2].tax_amount, 375.36) - self.assertEqual(si.get("taxes")[2].total, 5675.57) - - self.assertEqual(si.grand_total, 5675.57) - self.assertEqual(si.rounding_adjustment, 0.43) - self.assertEqual(si.rounded_total, 5676.0) + self.assertEqual(si.get("base_total_taxes_and_charges"), 115.92) def test_tax_calculation_with_multiple_items_and_discount(self): si = create_sales_invoice(qty=1, rate=75, do_not_save=True) @@ -1299,6 +1307,7 @@ class TestSalesInvoice(unittest.TestCase): dn.submit() return dn + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_sales_invoice_with_advance(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, @@ -2774,6 +2783,13 @@ class TestSalesInvoice(unittest.TestCase): company="_Test Company", ) + tds_payable_account = create_account( + account_name="TDS Payable", + account_type="Tax", + parent_account="Duties and Taxes - _TC", + company="_Test Company", + ) + si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1) si.apply_discount_on = "Grand Total" si.additional_discount_account = additional_discount_account @@ -3072,8 +3088,8 @@ class TestSalesInvoice(unittest.TestCase): si.commission_rate = commission_rate self.assertRaises(frappe.ValidationError, si.save) + @change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)}) def test_sales_invoice_submission_post_account_freezing_date(self): - frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", add_days(getdate(), 1)) si = create_sales_invoice(do_not_save=True) si.posting_date = add_days(getdate(), 1) si.save() @@ -3082,8 +3098,6 @@ class TestSalesInvoice(unittest.TestCase): si.posting_date = getdate() si.submit() - frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) - def test_over_billing_case_against_delivery_note(self): """ Test a case where duplicating the item with qty = 1 in the invoice @@ -3112,6 +3126,13 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", over_billing_allowance) + @change_settings( + "Accounts Settings", + { + "book_deferred_entries_via_journal_entry": 1, + "submit_journal_entries": 1, + }, + ) def test_multi_currency_deferred_revenue_via_journal_entry(self): deferred_account = create_account( account_name="Deferred Revenue", @@ -3119,11 +3140,6 @@ class TestSalesInvoice(unittest.TestCase): company="_Test Company", ) - acc_settings = frappe.get_single("Accounts Settings") - acc_settings.book_deferred_entries_via_journal_entry = 1 - acc_settings.submit_journal_entries = 1 - acc_settings.save() - item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_expense = 1 item.item_defaults[0].deferred_revenue_account = deferred_account @@ -3189,13 +3205,6 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) - acc_settings = frappe.get_single("Accounts Settings") - acc_settings.book_deferred_entries_via_journal_entry = 0 - acc_settings.submit_journal_entries = 0 - acc_settings.save() - - frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) - def test_standalone_serial_no_return(self): si = create_sales_invoice( item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1 @@ -3400,6 +3409,24 @@ class TestSalesInvoice(unittest.TestCase): set_advance_flag(company="_Test Company", flag=0, default_account="") + @change_settings("Selling Settings", {"customer_group": None, "territory": None}) + def test_sales_invoice_without_customer_group_and_territory(self): + # create a customer + if not frappe.db.exists("Customer", "_Test Simple Customer"): + customer_dict = get_customer_dict("_Test Simple Customer") + customer_dict.pop("customer_group") + customer_dict.pop("territory") + customer = frappe.get_doc(customer_dict).insert(ignore_permissions=True) + + self.assertEqual(customer.customer_group, None) + self.assertEqual(customer.territory, None) + + # create a sales invoice + si = create_sales_invoice(customer="_Test Simple Customer") + self.assertEqual(si.docstatus, 1) + self.assertEqual(si.customer_group, None) + self.assertEqual(si.territory, None) + @change_settings("Selling Settings", {"allow_negative_rates_for_items": 0}) def test_sales_return_negative_rate(self): si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index abeaab1d25..5d2764b669 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -157,7 +157,6 @@ "oldfieldname": "description", "oldfieldtype": "Text", "print_width": "200px", - "reqd": 1, "width": "200px" }, { @@ -912,4 +911,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js index ae789b5424..92f8a3a097 100644 --- a/erpnext/accounts/doctype/subscription/subscription.js +++ b/erpnext/accounts/doctype/subscription/subscription.js @@ -18,6 +18,14 @@ frappe.ui.form.on('Subscription', { } }; }); + + frm.set_query('sales_tax_template', function () { + return { + filters: { + company: frm.doc.company + } + }; + }); }, refresh: function (frm) { diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 803e87900d..785fd04b82 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils.data import ( add_days, add_months, @@ -21,11 +22,15 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto test_dependencies = ("UOM", "Item Group", "Item") -class TestSubscription(unittest.TestCase): +class TestSubscription(FrappeTestCase): def setUp(self): make_plans() create_parties() reset_settings() + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + + def tearDown(self): + frappe.db.rollback() def test_create_subscription_with_trial_with_correct_period(self): subscription = create_subscription( diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index 75223c2ccc..f6e5c56cce 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -22,7 +22,7 @@ class SubscriptionPlan(Document): @frappe.whitelist() def get_plan_rate( - plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1 + plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1, party=None ): plan = frappe.get_doc("Subscription Plan", plan) if plan.price_determination == "Fixed Rate": @@ -40,6 +40,7 @@ def get_plan_rate( customer_group=customer_group, company=None, qty=quantity, + party=party, ) if not price: return 0 diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index 87c5e6d588..ac0dd5123a 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -8,7 +8,7 @@ import frappe from frappe import _ from frappe.contacts.doctype.address.address import get_default_address from frappe.model.document import Document -from frappe.utils import cint, cstr +from frappe.utils import cstr from frappe.utils.nestedset import get_root_of from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups @@ -34,7 +34,6 @@ class TaxRule(Document): self.validate_tax_template() self.validate_from_to_dates("from_date", "to_date") self.validate_filters() - self.validate_use_for_shopping_cart() def validate_tax_template(self): if self.tax_type == "Sales": @@ -106,21 +105,6 @@ class TaxRule(Document): if tax_rule[0].priority == self.priority: frappe.throw(_("Tax Rule Conflicts with {0}").format(tax_rule[0].name), ConflictingTaxRule) - def validate_use_for_shopping_cart(self): - """If shopping cart is enabled and no tax rule exists for shopping cart, enable this one""" - if ( - not self.use_for_shopping_cart - 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( - _( - "Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart" - ) - ) - @frappe.whitelist() def get_party_details(party, party_type, args=None): diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index d4967785ba..70a8470614 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -41,7 +41,7 @@ def make_gl_entries( from_repost=from_repost, ) save_entries(gl_map, adv_adj, update_outstanding, from_repost) - # Post GL Map proccess there may no be any GL Entries + # Post GL Map process there may no be any GL Entries elif gl_map: frappe.throw( _( diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b99bb83c5b..310e41208f 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -6,12 +6,7 @@ from typing import Optional import frappe from frappe import _, msgprint, scrub -from frappe.contacts.doctype.address.address import ( - get_address_display, - get_company_address, - get_default_address, -) -from frappe.contacts.doctype.contact.contact import get_contact_details +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 @@ -133,6 +128,7 @@ def _get_party_details( party_address, company_address, shipping_address, + ignore_permissions=ignore_permissions, ) set_contact_details(party_details, party, party_type) set_other_values(party_details, party, party_type) @@ -193,6 +189,8 @@ def set_address_details( party_address=None, company_address=None, shipping_address=None, + *, + ignore_permissions=False ): billing_address_field = ( "customer_address" if party_type == "Lead" else party_type.lower() + "_address" @@ -205,13 +203,17 @@ def set_address_details( get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]) ) # address display - party_details.address_display = get_address_display(party_details[billing_address_field]) + party_details.address_display = render_address( + party_details[billing_address_field], check_permissions=not ignore_permissions + ) # shipping address if party_type in ["Customer", "Lead"]: party_details.shipping_address_name = shipping_address or get_party_shipping_address( party_type, party.name ) - party_details.shipping_address = get_address_display(party_details["shipping_address_name"]) + party_details.shipping_address = render_address( + party_details["shipping_address_name"], check_permissions=not ignore_permissions + ) if doctype: party_details.update( get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name) @@ -229,7 +231,7 @@ def set_address_details( if shipping_address: party_details.update( shipping_address=shipping_address, - shipping_address_display=get_address_display(shipping_address), + shipping_address_display=render_address(shipping_address), **get_fetch_values(doctype, "shipping_address", shipping_address) ) @@ -238,7 +240,8 @@ def set_address_details( party_details.update( billing_address=party_details.company_address, billing_address_display=( - party_details.company_address_display or get_address_display(party_details.company_address) + party_details.company_address_display + or render_address(party_details.company_address, check_permissions=False) ), **get_fetch_values(doctype, "billing_address", party_details.company_address) ) @@ -290,7 +293,21 @@ def set_contact_details(party_details, party, party_type): } ) else: - party_details.update(get_contact_details(party_details.contact_person)) + fields = [ + "name as contact_person", + "full_name as contact_display", + "email_id as contact_email", + "mobile_no as contact_mobile", + "phone as contact_phone", + "designation as contact_designation", + "department as contact_department", + ] + + contact_details = frappe.db.get_value( + "Contact", party_details.contact_person, fields, as_dict=True + ) + + party_details.update(contact_details) def set_other_values(party_details, party, party_type): @@ -995,3 +1012,13 @@ def add_party_account(party_type, party, company, account): doc.append("accounts", accounts) doc.save() + + +def render_address(address, check_permissions=True): + try: + from frappe.contacts.doctype.address.address import render_address as _render + except ImportError: + # Older frappe versions where this function is not available + from frappe.contacts.doctype.address.address import get_address_display as _render + + return frappe.call(_render, address, check_permissions=check_permissions) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 484ff7fa2b..9c73cbb344 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -95,18 +95,11 @@ frappe.query_reports["Accounts Payable"] = { "options": "Payment Terms Template" }, { - "fieldname": "party_type", + "fieldname":"party_type", "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - get_query: () => { - return { - filters: { - 'account_type': 'Payable' - } - }; - }, - on_change: () => { + "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"); } @@ -114,8 +107,15 @@ frappe.query_reports["Accounts Payable"] = { { "fieldname":"party", "label": __("Party"), - "fieldtype": "Dynamic Link", - "options": "party_type", + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + 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", @@ -164,3 +164,15 @@ frappe.query_reports["Accounts Payable"] = { } 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); + }); + }); + return options; +} \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index 3cf93cc865..9f03d92cd5 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -34,7 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): filters = { "company": self.company, "party_type": "Supplier", - "party": self.supplier, + "party": [self.supplier], "report_date": today(), "range1": 30, "range2": 60, 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 8a1725c048..9e575e669d 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -72,18 +72,11 @@ frappe.query_reports["Accounts Payable Summary"] = { } }, { - "fieldname": "party_type", + "fieldname":"party_type", "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - get_query: () => { - return { - filters: { - 'account_type': 'Payable' - } - }; - }, - on_change: () => { + "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"); } @@ -91,8 +84,15 @@ frappe.query_reports["Accounts Payable Summary"] = { { "fieldname":"party", "label": __("Party"), - "fieldtype": "Dynamic Link", - "options": "party_type", + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + 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", @@ -122,3 +122,15 @@ frappe.query_reports["Accounts Payable Summary"] = { } 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); + }); + }); + return options; +} \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 67a14e7880..1073be0bdc 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -1,6 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +frappe.provide("erpnext.utils"); + frappe.query_reports["Accounts Receivable"] = { "filters": [ { @@ -38,19 +40,11 @@ frappe.query_reports["Accounts Receivable"] = { } }, { - "fieldname": "party_type", + "fieldname":"party_type", "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - "Default": "Customer", - get_query: () => { - return { - filters: { - 'account_type': 'Receivable' - } - }; - }, - on_change: () => { + "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"); } @@ -58,8 +52,15 @@ frappe.query_reports["Accounts Receivable"] = { { "fieldname":"party", "label": __("Party"), - "fieldtype": "Dynamic Link", - "options": "party_type", + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + 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", @@ -192,3 +193,16 @@ frappe.query_reports["Accounts Receivable"] = { } 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); + }); + }); + return options; +} \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 7942402365..b9c7a0bfb8 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -116,7 +116,7 @@ class ReceivablePayableReport(object): # build all keys, since we want to exclude vouchers beyond the report date for ple in self.ple_entries: # get the balance object for voucher_type - key = (ple.voucher_type, ple.voucher_no, ple.party) + key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) if not key in self.voucher_balance: self.voucher_balance[key] = frappe._dict( voucher_type=ple.voucher_type, @@ -183,7 +183,7 @@ class ReceivablePayableReport(object): ): return - key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) + key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party) # If payment is made against credit note # and credit note is made against a Sales Invoice @@ -192,13 +192,13 @@ class ReceivablePayableReport(object): if ple.against_voucher_no in self.return_entries: return_against = self.return_entries.get(ple.against_voucher_no) if return_against: - key = (ple.against_voucher_type, return_against, ple.party) + key = (ple.account, ple.against_voucher_type, return_against, ple.party) row = self.voucher_balance.get(key) if not row: # no invoice, this is an invoice / stand-alone payment / credit note - row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party)) row.party_type = ple.party_type return row @@ -801,7 +801,7 @@ class ReceivablePayableReport(object): self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type) if self.filters.get("party"): - self.qb_selection_filter.append(self.filters.party == self.ple.party) + self.qb_selection_filter.append(self.ple.party.isin(self.filters.party)) if self.filters.party_account: self.qb_selection_filter.append(self.ple.account == self.filters.party_account) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index b98916ee44..cbeb6d3106 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,6 +1,7 @@ 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 @@ -23,29 +24,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): def tearDown(self): frappe.db.rollback() - def create_usd_account(self): - name = "Debtors USD" - exists = frappe.db.get_list( - "Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"} - ) - if exists: - self.debtors_usd = exists[0].name - else: - debtors = frappe.get_doc( - "Account", - frappe.db.get_list( - "Account", filters={"company": "_Test Company 2", "account_name": "Debtors"} - )[0].name, - ) - - debtors_usd = frappe.new_doc("Account") - debtors_usd.company = debtors.company - debtors_usd.account_name = "Debtors USD" - debtors_usd.account_currency = "USD" - debtors_usd.parent_account = debtors.parent_account - debtors_usd.account_type = debtors.account_type - self.debtors_usd = debtors_usd.save().name - def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False): frappe.set_user("Administrator") si = create_sales_invoice( @@ -573,7 +551,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): filters = { "company": self.company, "party_type": "Customer", - "party": self.customer, + "party": [self.customer], "report_date": today(), "range1": 30, "range2": 60, @@ -605,3 +583,132 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): for field in expected: with self.subTest(field=field): self.assertEqual(report_output.get(field), expected.get(field)) + + def test_multi_select_party_filter(self): + self.customer1 = self.customer + self.create_customer("_Test Customer 2") + self.customer2 = self.customer + self.create_customer("_Test Customer 3") + self.customer3 = self.customer + + filters = { + "company": self.company, + "party_type": "Customer", + "party": [self.customer1, self.customer3], + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si1.customer = self.customer1 + si1.save().submit() + + si2 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si2.customer = self.customer2 + si2.save().submit() + + si3 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si3.customer = self.customer3 + si3.save().submit() + + # check invoice grand total and invoiced column's value for 3 payment terms + report = execute(filters) + + expected_output = {self.customer1, self.customer3} + self.assertEqual(len(report[1]), 2) + output_for = set([x.party for x in report[1]]) + self.assertEqual(output_for, expected_output) + + 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} + ): + additional_receivable_acc = frappe.get_doc( + { + "doctype": "Account", + "account_name": acc_name, + "parent_account": "Accounts Receivable - " + self.company_abbr, + "company": self.company, + "account_type": "Receivable", + } + ).save() + self.debtors2 = additional_receivable_acc.name + + je = frappe.new_doc("Journal Entry") + je.company = self.company + je.posting_date = today() + je.append( + "accounts", + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "debit_in_account_currency": 150, + "credit_in_account_currency": 0, + "cost_center": self.cost_center, + }, + ) + je.append( + "accounts", + { + "account": self.debtors2, + "party_type": "Customer", + "party": self.customer, + "debit_in_account_currency": 200, + "credit_in_account_currency": 0, + "cost_center": self.cost_center, + }, + ) + je.append( + "accounts", + { + "account": self.cash, + "debit_in_account_currency": 0, + "credit_in_account_currency": 350, + "cost_center": self.cost_center, + }, + ) + je.save().submit() + + # manually remove party from Payment Ledger + ple = qb.DocType("Payment Ledger Entry") + qb.update(ple).set(ple.party, None).where(ple.voucher_no == je.name).run() + + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + report_ouput = execute(filters)[1] + expected_data = [ + [self.debtors2, je.doctype, je.name, "Customer", self.customer, 200.0, 0.0, 0.0, 200.0], + [self.debit_to, je.doctype, je.name, "Customer", self.customer, 150.0, 0.0, 0.0, 150.0], + ] + self.assertEqual(len(report_ouput), 2) + # fetch only required fields + report_output = [ + [ + x.party_account, + x.voucher_type, + x.voucher_no, + "Customer", + self.customer, + x.invoiced, + x.paid, + x.credit_note, + x.outstanding, + ] + for x in report_ouput + ] + # use account name to sort + # post sorting output should be [[Additional Debtors, ...], [Debtors, ...]] + report_output = sorted(report_output, key=lambda x: x[0]) + self.assertEqual(expected_data, report_output) 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 a78fbeb030..5ad10c7890 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -72,19 +72,11 @@ frappe.query_reports["Accounts Receivable Summary"] = { } }, { - "fieldname": "party_type", + "fieldname":"party_type", "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - "Default": "Customer", - get_query: () => { - return { - filters: { - 'account_type': 'Receivable' - } - }; - }, - on_change: () => { + "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"); } @@ -92,8 +84,15 @@ frappe.query_reports["Accounts Receivable Summary"] = { { "fieldname":"party", "label": __("Party"), - "fieldtype": "Dynamic Link", - "options": "party_type", + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + 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", @@ -151,3 +150,15 @@ frappe.query_reports["Accounts Receivable Summary"] = { } 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); + }); + }); + 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 cffc87895e..60274cd8b1 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -99,13 +99,11 @@ class AccountsReceivableSummary(ReceivablePayableReport): # Add all amount columns for k in list(self.party_total[d.party]): - if k not in ["currency", "sales_person"]: - - self.party_total[d.party][k] += d.get(k, 0.0) + if isinstance(self.party_total[d.party][k], float): + self.party_total[d.party][k] += d.get(k) or 0.0 # set territory, customer_group, sales person etc self.set_party_details(d) - self.party_total[d.party].update({"party_type": d.party_type}) def init_party_total(self, row): self.party_total.setdefault( @@ -124,6 +122,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): "total_due": 0.0, "future_amount": 0.0, "sales_person": [], + "party_type": row.party_type, } ), ) @@ -133,13 +132,12 @@ class AccountsReceivableSummary(ReceivablePayableReport): for key in ("territory", "customer_group", "supplier_group"): if row.get(key): - self.party_total[row.party][key] = row.get(key) - + self.party_total[row.party][key] = row.get(key, "") if row.sales_person: - self.party_total[row.party].sales_person.append(row.sales_person) + self.party_total[row.party].sales_person.append(row.get("sales_person", "")) if self.filters.sales_partner: - self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner") + self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner", "") def get_columns(self): self.columns = [] diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js index ecc13d7dc8..c2b57f768f 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.js +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js @@ -1,25 +1,23 @@ // 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["Balance Sheet"] = $.extend( - {}, - erpnext.financial_statements - ); +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, - }); - - frappe.query_reports["Balance Sheet"]["filters"].push({ - fieldname: "include_default_book_entries", - label: __("Include Default Book Entries"), - fieldtype: "Check", - default: 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 Book Entries"), + fieldtype: "Check", + default: 1, }); diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js index a2c34c6ee2..6b8ed27e64 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -1,24 +1,24 @@ // 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.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 - // of this if you are working here. +// 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 +// of this if you are working here. - frappe.query_reports["Cash Flow"]["filters"].splice(8, 1); +frappe.query_reports["Cash Flow"]["filters"].splice(8, 1); - frappe.query_reports["Cash Flow"]["filters"].push( - { - "fieldname": "include_default_book_entries", - "label": __("Include Default Book Entries"), - "fieldtype": "Check", - "default": 1 - } - ); -}); +frappe.query_reports["Cash Flow"]["filters"].push( + { + "fieldname": "include_default_book_entries", + "label": __("Include Default Book Entries"), + "fieldtype": "Check", + "default": 1 + } +); 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 1afa8d5625..590408c6f8 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -2,152 +2,150 @@ // For license information, please see license.txt -frappe.require("assets/erpnext/js/financial_statements.js", function() { - frappe.query_reports["Consolidated Financial Statement"] = { - "filters": [ - { - "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'); - - frappe.query_report.refresh(); - } - }, - { - "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":"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 - }); - }); - } - }, - { - "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 - }); - }); - } - }, - { - "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": "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": "include_default_book_entries", - "label": __("Include Default Book Entries"), - "fieldtype": "Check", - "default": 1 - }, - { - "fieldname": "show_zero_values", - "label": __("Show zero values"), - "fieldtype": "Check" - } - ], - "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) + ")"; - column.is_tree = true; - } - - if (data && data.account && column.apply_currency_formatter) { - data.currency = erpnext.get_currency(column.company_name); - } - - value = default_formatter(value, row, column, data); - if (!data.parent_account) { - value = $(`${value}`); - - var $value = $(value).css("font-weight", "bold"); - - value = $value.wrap("").parent().html(); - } - return value; +frappe.query_reports["Consolidated Financial Statement"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 }, - onload: function() { - let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); + { + "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.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 + frappe.query_report.refresh(); + } + }, + { + "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":"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 + }); }); - }); + } + }, + { + "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 + }); + }); + } + }, + { + "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": "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": "include_default_book_entries", + "label": __("Include Default Book Entries"), + "fieldtype": "Check", + "default": 1 + }, + { + "fieldname": "show_zero_values", + "label": __("Show zero values"), + "fieldtype": "Check" } + ], + "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) + ")"; + column.is_tree = true; + } + + if (data && data.account && column.apply_currency_formatter) { + data.currency = erpnext.get_currency(column.company_name); + } + + value = default_formatter(value, row, column, data); + if (!data.parent_account) { + value = $(`${value}`); + + var $value = $(value).css("font-weight", "bold"); + + value = $value.wrap("").parent().html(); + } + return value; + }, + onload: function() { + let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); + + frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); + frappe.query_report.set_filter_value({ + period_start_date: fy.year_start_date, + period_end_date: fy.year_end_date + }); + }); } -}); +} 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 79e5a0997f..51fa8c83f3 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 -frappe.require("assets/erpnext/js/financial_statements.js", function() { - frappe.query_reports["Dimension-wise Accounts Balance Report"] = { - "filters": [ - { - "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) { - var fiscal_year = query_report.get_values().fiscal_year; - if (!fiscal_year) { - return; - } - 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 - }); - }); +frappe.query_reports["Dimension-wise Accounts Balance Report"] = { + "filters": [ + { + "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) { + var fiscal_year = query_report.get_values().fiscal_year; + if (!fiscal_year) { + return; } - }, - { - "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": "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, - }, - ], - "formatter": erpnext.financial_statements.formatter, - "tree": true, - "name_field": "account", - "parent_field": "parent_account", - "initial_depth": 3 - } + 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 + }); + }); + } + }, + { + "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": "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, + }, + ], + "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); - }); - }); + {fields:['document_type']}).then((res) => { + res.forEach((dimension) => { + options.push(dimension.document_type); + }); + }); return options } diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.py b/erpnext/accounts/report/financial_ratios/financial_ratios.py index 57421ebcb0..47b4fd0da0 100644 --- a/erpnext/accounts/report/financial_ratios/financial_ratios.py +++ b/erpnext/accounts/report/financial_ratios/financial_ratios.py @@ -177,8 +177,8 @@ def add_solvency_ratios( return_on_equity_ratio = {"ratio": "Return on Equity Ratio"} for year in years: - profit_after_tax = total_income[year] + total_expense[year] - share_holder_fund = total_asset[year] - total_liability[year] + profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year)) + share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year)) debt_equity_ratio[year] = calculate_ratio( total_liability.get(year), share_holder_fund, precision 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 553c137f02..099884a48e 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 @@ -133,15 +133,17 @@ class General_Payment_Ledger_Comparison(object): self.gle_balances = set(val.gle) | self.gle_balances self.ple_balances = set(val.ple) | self.ple_balances - self.diff1 = self.gle_balances.difference(self.ple_balances) - self.diff2 = self.ple_balances.difference(self.gle_balances) + self.variation_in_payment_ledger = self.gle_balances.difference(self.ple_balances) + self.variation_in_general_ledger = self.ple_balances.difference(self.gle_balances) self.diff = frappe._dict({}) - for x in self.diff1: + for x in self.variation_in_payment_ledger: self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]}) - for x in self.diff2: - self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]})) + for x in self.variation_in_general_ledger: + self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update( + frappe._dict({"pl_balance": x[4]}) + ) def generate_data(self): self.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 f6b0b8c3f7..40d4259da9 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 @@ -2,20 +2,15 @@ // For license information, please see license.txt -frappe.query_reports["Gross and Net Profit Report"] = { - "filters": [ +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_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 3324a73e25..38060bb5b2 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -544,6 +544,8 @@ class GrossProfitGenerator(object): new_row.qty += flt(row.qty) new_row.buying_amount += flt(row.buying_amount, self.currency_precision) new_row.base_amount += flt(row.base_amount, self.currency_precision) + if self.filters.get("group_by") == "Sales Person": + new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) 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 9fe93b9772..e5898bf69d 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,18 +1,16 @@ // 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.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: "accumulated_values", - label: __("Accumulated Values"), - fieldtype: "Check", - default: 1, - }); +frappe.query_reports["Profit and Loss Statement"]["filters"].push({ + fieldname: "accumulated_values", + label: __("Accumulated Values"), + fieldtype: "Check", + default: 1, }); diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js index c9accef7a6..4a3d9bb479 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -1,133 +1,131 @@ // 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.query_reports["Profitability Analysis"] = { - "filters": [ - { - "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){ - let based_on = query_report.get_values().based_on; - if(based_on!='Accounting Dimension'){ - frappe.query_report.set_filter_value({ - accounting_dimension: '' - }); - } - } - }, - { - "fieldname": "accounting_dimension", - "label": __("Accounting Dimension"), - "fieldtype": "Link", - "options": "Accounting Dimension", - "get_query": () =>{ - return { - filters: { - "disabled": 0 - } - } - } - }, - { - "fieldname": "fiscal_year", - "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) { - 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 - }); +frappe.query_reports["Profitability Analysis"] = { + "filters": [ + { + "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){ + let based_on = query_report.get_values().based_on; + if(based_on!='Accounting Dimension'){ + frappe.query_report.set_filter_value({ + accounting_dimension: '' }); } - }, - { - "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": "show_zero_values", - "label": __("Show zero values"), - "fieldtype": "Check" } - ], - "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) + ")"; - column.is_tree = true; - } - - value = default_formatter(value, row, column, data); - - 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) { - $value.addClass("text-danger"); + }, + { + "fieldname": "accounting_dimension", + "label": __("Accounting Dimension"), + "fieldtype": "Link", + "options": "Accounting Dimension", + "get_query": () =>{ + return { + filters: { + "disabled": 0 + } } + } + }, + { + "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) { + 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 + }); + }); + } + }, + { + "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": "show_zero_values", + "label": __("Show zero values"), + "fieldtype": "Check" + } + ], + "formatter": function(value, row, column, data, default_formatter) { + if (column.fieldname=="account") { + value = data.account_name; - value = $value.wrap("").parent().html(); + column.link_onclick = + "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') { + value = $(`${value}`); + var $value = $(value).css("font-weight", "bold"); + if (data.warn_if_negative && data[column.fieldname] < 0) { + $value.addClass("text-danger"); } - return value; - }, - "open_profit_and_loss_statement": function(data) { - if (!data.account) return; + value = $value.wrap("").parent().html(); + } - frappe.route_options = { - "company": frappe.query_report.get_filter_value('company'), - "from_fiscal_year": data.fiscal_year, - "to_fiscal_year": data.fiscal_year - }; + return value; + }, + "open_profit_and_loss_statement": function(data) { + if (!data.account) return; - if(data.based_on == 'cost_center'){ - frappe.route_options["cost_center"] = data.account - } else { - frappe.route_options["project"] = data.account - } + frappe.route_options = { + "company": frappe.query_report.get_filter_value('company'), + "from_fiscal_year": data.fiscal_year, + "to_fiscal_year": data.fiscal_year + }; - frappe.set_route("query-report", "Profit and Loss Statement"); - }, - "tree": true, - "name_field": "account", - "parent_field": "parent_account", - "initial_depth": 3 - } + if(data.based_on == 'Cost Center'){ + frappe.route_options["cost_center"] = data.account + } else { + frappe.route_options["project"] = data.account + } - erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Profitability Analysis"].filters[1].options.push(dimension["document_type"]); - }); + 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"]); }); + diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 91ad3d6873..f2ec31c70e 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -68,7 +68,11 @@ def get_result( tax_amount += entry.credit - entry.debit if net_total_map.get(name): - total_amount, grand_total, base_total = net_total_map.get(name) + if voucher_type == "Journal Entry": + # back calcalute total amount from rate and tax_amount + total_amount = grand_total = base_total = tax_amount / (rate / 100) + else: + total_amount, grand_total, base_total = net_total_map.get(name) else: total_amount += entry.credit diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index c12ab0ff91..edd40b68ef 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -1,118 +1,116 @@ // 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["Trial Balance"] = { - "filters": [ - { - "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) { - var fiscal_year = query_report.get_values().fiscal_year; - if (!fiscal_year) { - return; - } - 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 - }); +frappe.query_reports["Trial Balance"] = { + "filters": [ + { + "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) { + var fiscal_year = query_report.get_values().fiscal_year; + if (!fiscal_year) { + return; + } + 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 }); - } - }, - { - "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": "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, - } + }); + } + }, + { + "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": "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, } } - }, - { - "fieldname": "project", - "label": __("Project"), - "fieldtype": "Link", - "options": "Project" - }, - { - "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": "with_period_closing_entry", - "label": __("Period Closing Entry"), - "fieldtype": "Check", - "default": 1 - }, - { - "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": "include_default_book_entries", - "label": __("Include Default Book Entries"), - "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 - } + }, + { + "fieldname": "project", + "label": __("Project"), + "fieldtype": "Link", + "options": "Project" + }, + { + "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": "with_period_closing_entry", + "label": __("Period Closing Entry"), + "fieldtype": "Check", + "default": 1 + }, + { + "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": "include_default_book_entries", + "label": __("Include Default Book Entries"), + "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 +} - erpnext.utils.add_dimensions('Trial Balance', 6); -}); +erpnext.utils.add_dimensions('Trial Balance', 6); diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 962292b8ee..f0e4c82048 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -148,6 +148,15 @@ frappe.ui.form.on('Asset', { if (frm.doc.docstatus == 0) { 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'); + + frm.add_custom_button(__("Capitalize Asset"), function() { + frm.trigger("create_asset_capitalization"); + }); + } } }, @@ -169,7 +178,7 @@ frappe.ui.form.on('Asset', { 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) { + else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) { frm.toggle_reqd('purchase_receipt', 0); frm.toggle_reqd('purchase_invoice', 0); } @@ -239,7 +248,7 @@ frappe.ui.form.on('Asset', { datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem', 'margin-left': '0.35rem', 'margin-right': '0.35rem'}); datatable.style.setStyle(`.dt-header`, {'margin-left': '0.35rem', 'margin-right': '0.35rem'}); - datatable.style.setStyle(`.dt-cell--header`, {'color': 'var(--text-muted)'}); + datatable.style.setStyle(`.dt-cell--header .dt-cell__content`, {'color': 'var(--gray-600)', 'font-size': 'var(--text-sm)'}); datatable.style.setStyle(`.dt-cell`, {'color': 'var(--text-color)'}); datatable.style.setStyle(`.dt-cell--col-1`, {'text-align': 'center'}); datatable.style.setStyle(`.dt-cell--col-2`, {'font-weight': 600}); @@ -328,7 +337,7 @@ frappe.ui.form.on('Asset', { item_code: function(frm) { - if(frm.doc.item_code && frm.doc.calculate_depreciation) { + 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', []); @@ -340,7 +349,8 @@ frappe.ui.form.on('Asset', { method: "erpnext.assets.doctype.asset.asset.get_item_details", args: { item_code: frm.doc.item_code, - asset_category: frm.doc.asset_category + asset_category: frm.doc.asset_category, + gross_purchase_amount: frm.doc.gross_purchase_amount }, callback: function(r, rt) { if(r.message) { @@ -352,7 +362,17 @@ frappe.ui.form.on('Asset', { is_existing_asset: function(frm) { frm.trigger("toggle_reference_doc"); - // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); + }, + + 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.trigger("toggle_reference_doc"); }, make_sales_invoice: function(frm) { @@ -402,6 +422,19 @@ frappe.ui.form.on('Asset', { }); }, + create_asset_capitalization: function(frm) { + frappe.call({ + args: { + "asset": frm.doc.name, + }, + method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization", + 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'); @@ -457,7 +490,7 @@ frappe.ui.form.on('Asset', { calculate_depreciation: function(frm) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); - if (frm.doc.item_code && frm.doc.calculate_depreciation ) { + 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", []); @@ -465,9 +498,11 @@ frappe.ui.form.on('Asset', { }, gross_purchase_amount: function(frm) { - frm.doc.finance_books.forEach(d => { - frm.events.set_depreciation_rate(frm, d); - }) + if (frm.doc.finance_books) { + frm.doc.finance_books.forEach(d => { + frm.events.set_depreciation_rate(frm, d); + }) + } }, purchase_receipt: (frm) => { @@ -546,7 +581,21 @@ frappe.ui.form.on('Asset', { } }); } - } + }, + + 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); + 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); + frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false; + } + }, }); frappe.ui.form.on('Asset Finance Book', { @@ -557,9 +606,19 @@ frappe.ui.form.on('Asset Finance Book', { 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_depreciation_rate(frm, row); }, + 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); + } + }, + frequency_of_depreciation: function(frm, cdt, cdn) { const row = locals[cdt][cdn]; frm.events.set_depreciation_rate(frm, row); diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index befb5248d5..c7d08e2041 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -14,6 +14,7 @@ "asset_owner", "asset_owner_company", "is_existing_asset", + "is_composite_asset", "supplier", "customer", "image", @@ -72,7 +73,8 @@ "purchase_receipt_amount", "default_finance_book", "depr_entry_posting_status", - "amended_from" + "amended_from", + "capitalized_in" ], "fields": [ { @@ -199,7 +201,7 @@ "fieldtype": "Date", "label": "Purchase Date", "read_only": 1, - "read_only_depends_on": "eval:!doc.is_existing_asset", + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "reqd": 1 }, { @@ -237,10 +239,12 @@ "default": "0", "fieldname": "calculate_depreciation", "fieldtype": "Check", - "label": "Calculate Depreciation" + "label": "Calculate Depreciation", + "read_only_depends_on": "eval:doc.is_composite_asset && !doc.gross_purchase_amount" }, { "default": "0", + "depends_on": "eval:!doc.is_composite_asset", "fieldname": "is_existing_asset", "fieldtype": "Check", "label": "Is Existing Asset" @@ -478,7 +482,7 @@ "fieldname": "asset_quantity", "fieldtype": "Int", "label": "Asset Quantity", - "read_only_depends_on": "eval:!doc.is_existing_asset" + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" }, { "fieldname": "depr_entry_posting_status", @@ -507,6 +511,21 @@ "fieldname": "is_fully_depreciated", "fieldtype": "Check", "label": "Is Fully Depreciated" + }, + { + "default": "0", + "depends_on": "eval:!doc.is_existing_asset", + "fieldname": "is_composite_asset", + "fieldtype": "Check", + "label": "Is Composite Asset" + }, + { + "fieldname": "capitalized_in", + "fieldtype": "Link", + "hidden": 1, + "label": "Capitalized In", + "options": "Asset Capitalization", + "read_only": 1 } ], "idx": 72, @@ -545,7 +564,7 @@ "table_fieldname": "accounts" } ], - "modified": "2023-07-28 20:12:44.819616", + "modified": "2023-10-03 23:28:26.732269", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 0dbed87cf2..9d35634933 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -198,7 +198,9 @@ 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) + 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): @@ -226,7 +228,7 @@ class Asset(AccountsController): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") - if not flt(self.gross_purchase_amount): + if not flt(self.gross_purchase_amount) and not self.is_composite_asset: frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) if is_cwip_accounting_enabled(self.asset_category): @@ -766,6 +768,15 @@ def create_asset_repair(asset, asset_name): return asset_repair +@frappe.whitelist() +def create_asset_capitalization(asset): + asset_capitalization = frappe.new_doc("Asset Capitalization") + asset_capitalization.update( + {"target_asset": asset, "capitalization_method": "Choose a WIP composite asset"} + ) + return asset_capitalization + + @frappe.whitelist() def create_asset_value_adjustment(asset, asset_category, company): asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") @@ -797,7 +808,7 @@ def transfer_asset(args): @frappe.whitelist() -def get_item_details(item_code, asset_category): +def get_item_details(item_code, asset_category, gross_purchase_amount): asset_category_doc = frappe.get_doc("Asset Category", asset_category) books = [] for d in asset_category_doc.finance_books: @@ -807,7 +818,11 @@ def get_item_details(item_code, asset_category): "depreciation_method": d.depreciation_method, "total_number_of_depreciations": d.total_number_of_depreciations, "frequency_of_depreciation": d.frequency_of_depreciation, - "start_date": nowdate(), + "daily_depreciation": d.daily_depreciation, + "salvage_value_percentage": d.salvage_value_percentage, + "expected_value_after_useful_life": flt(gross_purchase_amount) + * flt(d.salvage_value_percentage / 100), + "depreciation_start_date": d.depreciation_start_date or nowdate(), } ) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 39fcb21cdb..99824b7f67 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -19,7 +19,6 @@ from frappe.utils import ( from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.assets.doctype.asset.asset import ( - get_asset_value_after_depreciation, make_sales_invoice, split_asset, update_maintenance_status, @@ -194,6 +193,7 @@ class TestAsset(AssetSetup): def test_is_fixed_asset_set(self): asset = create_asset(is_existing_asset=1) doc = frappe.new_doc("Purchase Invoice") + doc.company = "_Test Company" doc.supplier = "_Test Supplier" doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name}) @@ -534,7 +534,7 @@ class TestAsset(AssetSetup): self.assertEqual("Asset Received But Not Billed - _TC", doc.items[0].expense_account) - # CWIP: Capital Work In Progress + # Capital Work In Progress def test_cwip_accounting(self): pr = make_purchase_receipt( item_code="Macbook Pro", qty=1, rate=5000, do_not_submit=True, location="Test Location" @@ -567,7 +567,8 @@ class TestAsset(AssetSetup): pr.submit() expected_gle = ( - ("Asset Received But Not Billed - _TC", 0.0, 5250.0), + ("_Test Account Shipping Charges - _TC", 0.0, 250.0), + ("Asset Received But Not Billed - _TC", 0.0, 5000.0), ("CWIP Account - _TC", 5250.0, 0.0), ) @@ -586,9 +587,8 @@ class TestAsset(AssetSetup): expected_gle = ( ("_Test Account Service Tax - _TC", 250.0, 0.0), ("_Test Account Shipping Charges - _TC", 250.0, 0.0), - ("Asset Received But Not Billed - _TC", 5250.0, 0.0), + ("Asset Received But Not Billed - _TC", 5000.0, 0.0), ("Creditors - _TC", 0.0, 5500.0), - ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), ) pi_gle = frappe.db.sql( @@ -1744,6 +1744,7 @@ def create_asset(**args): "location": args.location or "Test Location", "asset_owner": args.asset_owner or "Company", "is_existing_asset": args.is_existing_asset or 1, + "is_composite_asset": args.is_composite_asset or 0, "asset_quantity": args.get("asset_quantity") or 1, "depr_entry_posting_status": args.depr_entry_posting_status or "", } diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.json b/erpnext/assets/doctype/asset_activity/asset_activity.json index 476fb2732e..00992e2cfc 100644 --- a/erpnext/assets/doctype/asset_activity/asset_activity.json +++ b/erpnext/assets/doctype/asset_activity/asset_activity.json @@ -75,13 +75,14 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-01 11:09:52.584482", + "modified": "2023-09-29 15:56:17.608643", "modified_by": "Administrator", "module": "Assets", "name": "Asset Activity", "owner": "Administrator", "permissions": [ { + "delete": 1, "email": 1, "read": 1, "report": 1, @@ -89,6 +90,7 @@ "share": 1 }, { + "delete": 1, "email": 1, "read": 1, "report": 1, @@ -96,6 +98,7 @@ "share": 1 }, { + "delete": 1, "email": 1, "read": 1, "report": 1, diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 6d55d7772b..be78d9ebdc 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -16,9 +16,15 @@ 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) { 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(); + } } setup_queries() { @@ -35,18 +41,9 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s }); me.frm.set_query("target_asset", function() { - var filters = {}; - - if (me.frm.doc.target_item_code) { - filters['item_code'] = me.frm.doc.target_item_code; - } - - filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]]; - filters['docstatus'] = 1; - return { - filters: filters - }; + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } }); me.frm.set_query("asset", "asset_items", function() { @@ -128,6 +125,39 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s return this.get_target_item_details(); } + target_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(); + } + } + + set_consumed_stock_items_tagged_to_wip_composite_asset(asset) { + var me = this; + + if (asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset", + args: { + asset: asset, + }, + 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); + } + + refresh_field("stock_items"); + + me.calculate_totals(); + } + } + }); + } + } + item_code(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); if (cdt === "Asset Capitalization Stock Item") { @@ -242,6 +272,26 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } } + get_target_asset_details() { + var me = this; + + if (me.frm.doc.target_asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details", + child: me.frm.doc, + args: { + asset: me.frm.doc.target_asset, + company: me.frm.doc.company, + }, + callback: function (r) { + if (!r.exc) { + me.frm.refresh_fields(); + } + } + }); + } + } + get_consumed_stock_item_details(row) { var me = this; diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index 04b0c4e513..9ddc44212f 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -8,24 +8,25 @@ "engine": "InnoDB", "field_order": [ "title", + "company", "naming_series", "entry_type", - "target_item_code", - "target_asset", "target_item_name", "target_is_fixed_asset", "target_has_batch_no", "target_has_serial_no", "column_break_9", - "target_asset_name", + "capitalization_method", + "target_item_code", "target_asset_location", + "target_asset", + "target_asset_name", "target_warehouse", "target_qty", "target_stock_uom", "target_batch_no", "target_serial_no", "column_break_5", - "company", "finance_book", "posting_date", "posting_time", @@ -57,12 +58,13 @@ "label": "Title" }, { + "depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')", "fieldname": "target_item_code", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Item Code", - "options": "Item", - "reqd": 1 + "mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'", + "options": "Item" }, { "depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code", @@ -86,16 +88,18 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')", "fieldname": "target_asset", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Asset", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'", "no_copy": 1, "options": "Asset", - "read_only": 1 + "read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')" }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", + "depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')", "fetch_from": "target_asset.asset_name", "fieldname": "target_asset_name", "fieldtype": "Data", @@ -186,12 +190,14 @@ }, { "default": "1", + "depends_on": "eval:doc.entry_type=='Decapitalization'", "fieldname": "target_qty", "fieldtype": "Float", "label": "Target Qty", "read_only_depends_on": "eval:doc.entry_type=='Capitalization'" }, { + "depends_on": "eval:doc.entry_type=='Decapitalization'", "fetch_from": "target_item_code.stock_uom", "fieldname": "target_stock_uom", "fieldtype": "Link", @@ -331,18 +337,26 @@ "read_only": 1 }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", + "depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'", "fieldname": "target_asset_location", "fieldtype": "Link", "label": "Target Asset Location", - "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'", "options": "Location" + }, + { + "depends_on": "eval:doc.entry_type=='Capitalization'", + "fieldname": "capitalization_method", + "fieldtype": "Select", + "label": "Capitalization Method", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "options": "\nCreate a new composite asset\nChoose a WIP composite asset" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-06-22 14:17:07.995120", + "modified": "2023-10-03 22:55:59.461456", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 662e4b983b..0d6f6b4da1 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -53,6 +53,7 @@ class AssetCapitalization(StockController): self.validate_posting_time() self.set_missing_values(for_validate=True) self.validate_target_item() + self.validate_target_asset() self.validate_consumed_stock_item() self.validate_consumed_asset_item() self.validate_service_item() @@ -67,12 +68,12 @@ class AssetCapitalization(StockController): def before_submit(self): self.validate_source_mandatory() - if self.entry_type == "Capitalization": - self.create_target_asset() + self.create_target_asset() def on_submit(self): self.update_stock_ledger() self.make_gl_entries() + self.update_target_asset() def on_cancel(self): self.ignore_linked_doctypes = ( @@ -94,6 +95,11 @@ class AssetCapitalization(StockController): if self.meta.has_field(k) and (not self.get(k) or k in force_fields): self.set(k, v) + target_asset_details = get_target_asset_details(self.target_asset, self.company) + for k, v in target_asset_details.items(): + if self.meta.has_field(k) and (not self.get(k) or k in force_fields): + self.set(k, v) + for d in self.stock_items: args = self.as_dict() args.update(d.as_dict()) @@ -155,6 +161,33 @@ class AssetCapitalization(StockController): self.validate_item(target_item) + def validate_target_asset(self): + if self.target_asset: + target_asset = self.get_asset_for_validation(self.target_asset) + + if not target_asset.is_composite_asset: + frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name)) + + 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) + ) + + if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"): + frappe.throw( + _("Target Asset {0} cannot be {1}").format(target_asset.name, target_asset.status) + ) + + if target_asset.docstatus == 1: + frappe.throw(_("Target Asset {0} cannot be submitted").format(target_asset.name)) + elif target_asset.docstatus == 2: + frappe.throw(_("Target Asset {0} cannot be cancelled").format(target_asset.name)) + + if target_asset.company != self.company: + frappe.throw( + _("Target Asset {0} does not belong to company {1}").format(target_asset.name, self.company) + ) + def validate_consumed_stock_item(self): for d in self.stock_items: if d.item_code: @@ -179,7 +212,23 @@ class AssetCapitalization(StockController): ) asset = self.get_asset_for_validation(d.asset) - self.validate_asset(asset) + + 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) + ) + + 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)) + + if asset.company != self.company: + frappe.throw( + _("Row #{0}: Consumed Asset {1} does not belong to company {2}").format( + d.idx, asset.name, self.company + ) + ) def validate_service_item(self): for d in self.service_items: @@ -214,21 +263,12 @@ class AssetCapitalization(StockController): def get_asset_for_validation(self, asset): return frappe.db.get_value( - "Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1 + "Asset", + asset, + ["name", "item_code", "company", "status", "docstatus", "is_composite_asset"], + as_dict=1, ) - def validate_asset(self, asset): - if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): - frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status)) - - if asset.docstatus == 0: - frappe.throw(_("Asset {0} is Draft").format(asset.name)) - if asset.docstatus == 2: - frappe.throw(_("Asset {0} is cancelled").format(asset.name)) - - if asset.company != self.company: - frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company)) - @frappe.whitelist() def set_warehouse_details(self): for d in self.get("stock_items"): @@ -495,16 +535,25 @@ class AssetCapitalization(StockController): ) def create_target_asset(self): + if ( + self.entry_type != "Capitalization" + or self.capitalization_method != "Create a new composite asset" + ): + return + total_target_asset_value = flt(self.total_value, self.precision("total_value")) + asset_doc = frappe.new_doc("Asset") asset_doc.company = self.company asset_doc.item_code = self.target_item_code - asset_doc.is_existing_asset = 1 + asset_doc.is_composite_asset = 1 asset_doc.location = self.target_asset_location asset_doc.available_for_use_date = self.posting_date asset_doc.purchase_date = self.posting_date asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.capitalized_in = self.name asset_doc.flags.ignore_validate = True asset_doc.flags.asset_created_via_asset_capitalization = True asset_doc.insert() @@ -528,6 +577,28 @@ class AssetCapitalization(StockController): ).format(get_link_to_form("Asset", asset_doc.name)) ) + def update_target_asset(self): + if ( + self.entry_type != "Capitalization" + or self.capitalization_method != "Choose a WIP composite asset" + ): + return + + total_target_asset_value = flt(self.total_value, self.precision("total_value")) + + asset_doc = frappe.get_doc("Asset", self.target_asset) + asset_doc.gross_purchase_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.capitalized_in = self.name + asset_doc.flags.ignore_validate = True + 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)) + ) + def restore_consumed_asset_items(self): for item in self.asset_items: asset = frappe.get_doc("Asset", item.asset) @@ -612,6 +683,33 @@ def get_target_item_details(item_code=None, company=None): return out +@frappe.whitelist() +def get_target_asset_details(asset=None, company=None): + out = frappe._dict() + + # Get Asset Details + asset_details = frappe._dict() + if asset: + asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1) + if not asset_details: + frappe.throw(_("Asset {0} does not exist").format(asset)) + + # Re-set item code from Asset + out.target_item_code = asset_details.item_code + + # Set Asset Details + out.asset_name = asset_details.asset_name + + if asset_details.item_code: + out.target_fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=asset_details.item_code, company=company + ) + else: + out.target_fixed_asset_account = None + + return out + + @frappe.whitelist() def get_consumed_stock_item_details(args): if isinstance(args, str): @@ -760,3 +858,30 @@ def get_service_item_details(args): ) return out + + +@frappe.whitelist() +def get_items_tagged_to_wip_composite_asset(asset): + fields = [ + "item_code", + "item_name", + "batch_no", + "serial_no", + "stock_qty", + "stock_uom", + "warehouse", + "cost_center", + "qty", + "valuation_rate", + "amount", + ] + + pi_items = frappe.get_all( + "Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields + ) + + pr_items = frappe.get_all( + "Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields + ) + + return pi_items + pr_items diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 6e0a6856f5..ac7c90d9e6 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -58,6 +58,7 @@ class TestAssetCapitalization(unittest.TestCase): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", + capitalization_method="Create a new composite asset", target_item_code="Macbook Pro", target_asset_location="Test Location", stock_qty=stock_qty, @@ -147,6 +148,7 @@ class TestAssetCapitalization(unittest.TestCase): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", + capitalization_method="Create a new composite asset", target_item_code="Macbook Pro", target_asset_location="Test Location", stock_qty=stock_qty, @@ -211,6 +213,77 @@ class TestAssetCapitalization(unittest.TestCase): self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + def test_capitalization_with_wip_composite_asset(self): + company = "_Test Company with perpetual inventory" + set_depreciation_settings_in_company(company=company) + + stock_rate = 1000 + stock_qty = 2 + stock_amount = 2000 + + total_amount = 2000 + + wip_composite_asset = create_asset( + asset_name="Asset Capitalization WIP Composite Asset", + is_composite_asset=1, + warehouse="Stores - TCP1", + company=company, + ) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + entry_type="Capitalization", + capitalization_method="Choose a WIP composite asset", + target_asset=wip_composite_asset.name, + target_asset_location="Test Location", + stock_qty=stock_qty, + stock_rate=stock_rate, + service_expense_account="Expenses Included In Asset Valuation - TCP1", + company=company, + submit=1, + ) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, "Capitalization") + self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset") + self.assertEqual(asset_capitalization.target_qty, 1) + + self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) + self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) + self.assertEqual(asset_capitalization.stock_items_total, stock_amount) + + self.assertEqual(asset_capitalization.total_value, total_amount) + self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) + + # Test Target Asset values + target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) + self.assertEqual(target_asset.gross_purchase_amount, total_amount) + self.assertEqual(target_asset.purchase_receipt_amount, total_amount) + + # Test General Ledger Entries + expected_gle = { + "_Test Fixed Asset - TCP1": 2000, + "_Test Warehouse - TCP1": -2000, + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Test Stock Ledger Entries + expected_sle = { + ("Capitalization Source Stock Item", "_Test Warehouse - TCP1"): { + "actual_qty": -stock_qty, + "stock_value_difference": -stock_amount, + } + } + actual_sle = get_actual_sle_dict(asset_capitalization.name) + self.assertEqual(actual_sle, expected_sle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + def test_decapitalization_with_depreciation(self): # Variables purchase_date = "2020-01-01" @@ -347,6 +420,7 @@ def create_asset_capitalization(**args): asset_capitalization.update( { "entry_type": args.entry_type or "Capitalization", + "capitalization_method": args.capitalization_method or None, "company": company, "posting_date": args.posting_date or now.strftime("%Y-%m-%d"), "posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"), 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 4121302c1e..2c27dc9aca 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -12,6 +12,7 @@ "column_break_5", "frequency_of_depreciation", "depreciation_start_date", + "salvage_value_percentage", "expected_value_after_useful_life", "value_after_depreciation", "rate_of_depreciation" @@ -91,12 +92,17 @@ "fieldname": "daily_depreciation", "fieldtype": "Check", "label": "Daily Depreciation" + }, + { + "fieldname": "salvage_value_percentage", + "fieldtype": "Percent", + "label": "Salvage Value Percentage" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-10 22:10:36.576199", + "modified": "2023-09-29 15:39:52.740594", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 7e95cb2a1b..9c2b8bc963 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -177,7 +177,7 @@ class AssetRepair(AccountsController): "item_code": stock_item.item_code, "qty": stock_item.consumed_quantity, "basic_rate": stock_item.valuation_rate, - "serial_no": stock_item.serial_and_batch_bundle, + "serial_and_batch_bundle": stock_item.serial_and_batch_bundle, "cost_center": self.cost_center, "project": self.project, }, diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 8c73e56a99..71cb01b188 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -24,6 +24,7 @@ "bill_for_rejected_quantity_in_purchase_invoice", "disable_last_purchase_rate", "show_pay_button", + "use_transaction_date_exchange_rate", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -164,6 +165,13 @@ "fieldname": "over_order_allowance", "fieldtype": "Float", "label": "Over Order Allowance (%)" + }, + { + "default": "0", + "description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.", + "fieldname": "use_transaction_date_exchange_rate", + "fieldtype": "Check", + "label": "Use Transaction Date Exchange Rate" } ], "icon": "fa fa-cog", @@ -171,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-03-02 17:02:14.404622", + "modified": "2023-10-16 16:22:03.201078", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 5b5cc2b021..f74df6630e 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -477,6 +477,7 @@ "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Supplier Warehouse", "options": "Warehouse" }, @@ -1274,7 +1275,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-09-13 16:21:07.361700", + "modified": "2023-10-01 20:58:07.851037", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", 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 f79b6223bf..6b29984491 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -180,7 +180,6 @@ "oldfieldname": "description", "oldfieldtype": "Small Text", "print_width": "300px", - "reqd": 1, "width": "300px" }, { @@ -916,4 +915,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json index e07f4626b8..82fcfa2713 100644 --- a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json +++ b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json @@ -84,7 +84,6 @@ "oldfieldname": "description", "oldfieldtype": "Small Text", "print_width": "300px", - "reqd": 1, "width": "300px" }, { @@ -270,4 +269,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 08dc44c71b..70d27828b4 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -88,7 +88,7 @@ frappe.ui.form.on("Supplier", { }, __("View")); frm.add_custom_button(__('Accounts Payable'), function () { - frappe.set_route('query-report', 'Accounts Payable', { supplier: frm.doc.name }); + frappe.set_route('query-report', 'Accounts Payable', { party_type: "Supplier", party: frm.doc.name }); }, __("View")); frm.add_custom_button(__('Bank Account'), function () { diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 19972ca1fa..f37db5f115 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -167,8 +167,7 @@ "label": "Supplier Group", "oldfieldname": "supplier_type", "oldfieldtype": "Link", - "options": "Supplier Group", - "reqd": 1 + "options": "Supplier Group" }, { "default": "Company", @@ -486,7 +485,7 @@ "link_fieldname": "party" } ], - "modified": "2023-09-21 12:24:20.398889", + "modified": "2023-09-25 12:48:21.869563", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py index 11bb06e0ca..3bd306e659 100644 --- a/erpnext/buying/doctype/supplier/supplier_dashboard.py +++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py @@ -8,7 +8,7 @@ def get_data(): "This is based on transactions against this Supplier. See timeline below for details" ), "fieldname": "supplier", - "non_standard_fieldnames": {"Payment Entry": "party_name", "Bank Account": "party"}, + "non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"}, "transactions": [ {"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]}, {"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]}, diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index ee2ada3b65..350a25f26e 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -207,11 +207,14 @@ def create_supplier(**args): "doctype": "Supplier", "supplier_name": args.supplier_name, "default_currency": args.default_currency, - "supplier_group": args.supplier_group or "Services", "supplier_type": args.supplier_type or "Company", "tax_withholding_category": args.tax_withholding_category, } - ).insert() + ) + if not args.without_supplier_group: + doc.supplier_group = args.supplier_group or "Services" + + doc.insert() return doc diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index 638cde01be..8d491fbc84 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -126,7 +126,6 @@ "oldfieldname": "description", "oldfieldtype": "Small Text", "print_width": "300px", - "reqd": 1, "width": "300px" }, { @@ -569,4 +568,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py index 6e22acf01a..683a12ac95 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py @@ -334,6 +334,11 @@ def make_default_records(): "variable_label": "Total Ordered", "path": "get_ordered_qty", }, + { + "param_name": "total_invoiced", + "variable_label": "Total Invoiced", + "path": "get_invoiced_qty", + }, ] install_standing_docs = [ { diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py index 4080d1fde0..6c91a049db 100644 --- a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py +++ b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py @@ -440,6 +440,23 @@ def get_ordered_qty(scorecard): ).run(as_list=True)[0][0] or 0 +def get_invoiced_qty(scorecard): + """Returns the total number of invoiced quantity (based on Purchase Invoice)""" + + pi = frappe.qb.DocType("Purchase Invoice") + + return ( + frappe.qb.from_(pi) + .select(Sum(pi.total_qty)) + .where( + (pi.supplier == scorecard.supplier) + & (pi.docstatus == 1) + & (pi.posting_date >= scorecard.get("start_date")) + & (pi.posting_date <= scorecard.get("end_date")) + ) + ).run(as_list=True)[0][0] or 0 + + def get_rfq_total_number(scorecard): """Gets the total number of RFQs sent to supplier""" supplier = frappe.get_doc("Supplier", scorecard.supplier) 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 e10c0e2fcc..b6e46302ff 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -6,7 +6,7 @@ import copy import frappe from frappe import _ -from frappe.query_builder.functions import IfNull +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import date_diff, flt, getdate @@ -57,7 +57,7 @@ def get_data(filters): po_item.qty, po_item.received_qty, (po_item.qty - po_item.received_qty).as_("pending_qty"), - IfNull(pi_item.qty, 0).as_("billed_qty"), + Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"), po_item.base_amount.as_("amount"), (po_item.received_qty * po_item.base_rate).as_("received_qty_amount"), (po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"), 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 a728290961..01ff28d810 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -35,8 +35,12 @@ def get_data(filters): sq_item.parent, sq_item.item_code, sq_item.qty, + sq.currency, sq_item.stock_qty, sq_item.amount, + sq_item.base_rate, + sq_item.base_amount, + sq.price_list_currency, sq_item.uom, sq_item.stock_uom, sq_item.request_for_quotation, @@ -105,7 +109,11 @@ def prepare_data(supplier_quotation_data, filters): "qty": data.get("qty"), "price": flt(data.get("amount") * exchange_rate, float_precision), "uom": data.get("uom"), + "price_list_currency": data.get("price_list_currency"), + "currency": data.get("currency"), "stock_uom": data.get("stock_uom"), + "base_amount": flt(data.get("base_amount"), float_precision), + "base_rate": flt(data.get("base_rate"), float_precision), "request_for_quotation": data.get("request_for_quotation"), "valid_till": data.get("valid_till"), "lead_time_days": data.get("lead_time_days"), @@ -183,6 +191,8 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): def get_columns(filters): + currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") + group_by_columns = [ { "fieldname": "supplier_name", @@ -203,11 +213,18 @@ def get_columns(filters): columns = [ {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 90}, {"fieldname": "qty", "label": _("Quantity"), "fieldtype": "Float", "width": 80}, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "width": 110, + }, { "fieldname": "price", "label": _("Price"), "fieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "width": 110, }, { @@ -221,9 +238,23 @@ def get_columns(filters): "fieldname": "price_per_unit", "label": _("Price per Unit (Stock UOM)"), "fieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "width": 120, }, + { + "fieldname": "base_amount", + "label": _("Price ({0})").format(currency), + "fieldtype": "Currency", + "options": "price_list_currency", + "width": 180, + }, + { + "fieldname": "base_rate", + "label": _("Price Per Unit ({0})").format(currency), + "fieldtype": "Currency", + "options": "price_list_currency", + "width": 180, + }, { "fieldname": "quotation", "label": _("Supplier Quotation"), diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6812940ee2..cc5d643c14 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -13,6 +13,7 @@ from frappe.utils import ( add_days, add_months, cint, + comma_and, flt, fmt_money, formatdate, @@ -181,6 +182,17 @@ class AccountsController(TransactionBase): self.validate_party_account_currency() 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 + ]: + 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")) + ) + ) + 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() @@ -572,6 +584,17 @@ class AccountsController(TransactionBase): self.currency, self.company_currency, transaction_date, args ) + if ( + self.currency + and buying_or_selling == "Buying" + and frappe.db.get_single_value("Buying Settings", "use_transaction_date_exchange_rate") + and self.doctype == "Purchase Invoice" + ): + self.use_transaction_date_exchange_rate = True + self.conversion_rate = get_exchange_rate( + self.currency, self.company_currency, transaction_date, args + ) + def set_missing_item_details(self, for_validate=False): """set missing item values""" from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index e68ee909d9..c8785a5a72 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -9,6 +9,8 @@ import frappe from frappe import _ from frappe.utils import cstr, flt +from erpnext.utilities.product import get_item_codes_by_attributes + class ItemVariantExistsError(frappe.ValidationError): pass @@ -24,7 +26,8 @@ class ItemTemplateCannotHaveStock(frappe.ValidationError): @frappe.whitelist() def get_variant(template, args=None, variant=None, manufacturer=None, manufacturer_part_no=None): - """Validates Attributes and their Values, then looks for an exactly + """ + Validates Attributes and their Values, then looks for an exactly matching Item Variant :param item: Template Item @@ -34,13 +37,14 @@ def get_variant(template, args=None, variant=None, manufacturer=None, manufactur if item_template.variant_based_on == "Manufacturer" and manufacturer: return make_variant_based_on_manufacturer(item_template, manufacturer, manufacturer_part_no) - else: - if isinstance(args, str): - args = json.loads(args) - if not args: - frappe.throw(_("Please specify at least one attribute in the Attributes table")) - return find_variant(template, args, variant) + if isinstance(args, str): + args = json.loads(args) + + if not args: + frappe.throw(_("Please specify at least one attribute in the Attributes table")) + + return find_variant(template, args, variant) def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part_no): @@ -157,17 +161,6 @@ 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( - frappe.db.escape(key), frappe.db.escape(cstr(value)) - ) - for key, value in args.items() - ] - - conditions = " or ".join(conditions) - - 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 ] diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 901466267b..d34fbeb0da 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -4,9 +4,9 @@ import frappe from frappe import _, bold, throw -from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import cint, flt, get_link_to_form, nowtime +from erpnext.accounts.party import render_address from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.stock_controller import StockController @@ -288,7 +288,9 @@ class SellingController(StockController): last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1) 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") + throw_message( + item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate (Moving Average)" + ) def get_item_list(self): il = [] @@ -600,7 +602,9 @@ class SellingController(StockController): for address_field, address_display_field in address_dict.items(): if self.get(address_field): - self.set(address_display_field, get_address_display(self.get(address_field))) + self.set( + address_display_field, render_address(self.get(address_field), check_permissions=False) + ) def validate_for_duplicate_items(self): check_list, chk_dupl_itm = [], [] diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index ae54b801f1..a40976b8dd 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -62,9 +62,12 @@ class StockController(AccountsController): ) ) + is_asset_pr = any(d.get("is_fixed_asset") for d in self.get("items")) + if ( cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items + or is_asset_pr ): warehouse_account = get_warehouse_account_map(self.company) @@ -73,10 +76,7 @@ class StockController(AccountsController): gl_entries = self.get_gl_entries(warehouse_account) make_gl_entries(gl_entries, from_repost=from_repost) - elif self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self.docstatus == 1: - gl_entries = [] - gl_entries = self.get_asset_gl_entry(gl_entries) - make_gl_entries(gl_entries, from_repost=from_repost) + update_regional_gl_entries(gl_entries, self) def validate_serialized_batch(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -692,13 +692,21 @@ class StockController(AccountsController): d.stock_uom_rate = d.rate / (d.conversion_factor or 1) def validate_internal_transfer(self): - if ( - self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt") - and self.is_internal_transfer() - ): - self.validate_in_transit_warehouses() - self.validate_multi_currency() - self.validate_packed_items() + if self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt"): + if self.is_internal_transfer(): + self.validate_in_transit_warehouses() + self.validate_multi_currency() + self.validate_packed_items() + else: + self.validate_internal_transfer_warehouse() + + def validate_internal_transfer_warehouse(self): + for row in self.items: + if row.get("target_warehouse"): + row.target_warehouse = None + + if row.get("from_warehouse"): + row.from_warehouse = None def validate_in_transit_warehouses(self): if ( @@ -855,8 +863,9 @@ class StockController(AccountsController): @frappe.whitelist() def show_accounting_ledger_preview(company, doctype, docname): - filters = {"company": company, "include_dimensions": 1} + filters = frappe._dict(company=company, include_dimensions=1) doc = frappe.get_doc(doctype, docname) + doc.run_method("before_gl_preview") gl_columns, gl_data = get_accounting_ledger_preview(doc, filters) @@ -867,8 +876,9 @@ def show_accounting_ledger_preview(company, doctype, docname): @frappe.whitelist() def show_stock_ledger_preview(company, doctype, docname): - filters = {"company": company} + filters = frappe._dict(company=company) doc = frappe.get_doc(doctype, docname) + doc.run_method("before_sl_preview") sl_columns, sl_data = get_stock_ledger_preview(doc, filters) @@ -1216,3 +1226,8 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa repost_entries.append(repost_entry) return repost_entries + + +@erpnext.allow_regional +def update_regional_gl_entries(gl_list, doc): + return diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 62d4c53868..96284d612f 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -25,6 +25,9 @@ class calculate_taxes_and_totals(object): def __init__(self, doc: Document): self.doc = doc frappe.flags.round_off_applicable_accounts = [] + frappe.flags.round_row_wise_tax = frappe.db.get_single_value( + "Accounts Settings", "round_row_wise_tax" + ) self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items") @@ -190,7 +193,9 @@ class calculate_taxes_and_totals(object): item.net_rate = item.rate - if not item.qty and self.doc.get("is_return"): + if ( + 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"): item.amount = flt(item.rate, item.precision("amount")) @@ -368,6 +373,8 @@ class calculate_taxes_and_totals(object): for i, tax in enumerate(self.doc.get("taxes")): # tax_amount represents the amount of tax for the current step current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map) + if frappe.flags.round_row_wise_tax: + current_tax_amount = flt(current_tax_amount, tax.precision("tax_amount")) # Adjust divisional loss to the last item if tax.charge_type == "Actual": @@ -478,10 +485,19 @@ class calculate_taxes_and_totals(object): # store tax breakup for each item key = item.item_code or item.item_name item_wise_tax_amount = current_tax_amount * self.doc.conversion_rate - if tax.item_wise_tax_detail.get(key): - item_wise_tax_amount += tax.item_wise_tax_detail[key][1] + if frappe.flags.round_row_wise_tax: + item_wise_tax_amount = flt(item_wise_tax_amount, tax.precision("tax_amount")) + if tax.item_wise_tax_detail.get(key): + item_wise_tax_amount += flt(tax.item_wise_tax_detail[key][1], tax.precision("tax_amount")) + tax.item_wise_tax_detail[key] = [ + tax_rate, + flt(item_wise_tax_amount, tax.precision("tax_amount")), + ] + else: + if tax.item_wise_tax_detail.get(key): + item_wise_tax_amount += tax.item_wise_tax_detail[key][1] - tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)] + tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)] def round_off_totals(self, tax): if tax.account_head in frappe.flags.round_off_applicable_accounts: diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 105c58d110..e897ba41eb 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -379,7 +379,7 @@ def get_lead_details(lead, posting_date=None, company=None): } ) - set_address_details(out, lead, "Lead") + set_address_details(out, lead, "Lead", company=company) taxes_and_charges = set_taxes( None, diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 7b8c43b2d6..98dfbec18b 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Concat_ws, Date def execute(filters=None): @@ -69,53 +70,41 @@ def get_columns(): def get_data(filters): - return frappe.db.sql( - """ - SELECT - `tabLead`.name, - `tabLead`.lead_name, - `tabLead`.status, - `tabLead`.lead_owner, - `tabLead`.territory, - `tabLead`.source, - `tabLead`.email_id, - `tabLead`.mobile_no, - `tabLead`.phone, - `tabLead`.owner, - `tabLead`.company, - concat_ws(', ', - trim(',' from `tabAddress`.address_line1), - trim(',' from tabAddress.address_line2) - ) AS address, - `tabAddress`.state, - `tabAddress`.pincode, - `tabAddress`.country - FROM - `tabLead` left join `tabDynamic Link` on ( - `tabLead`.name = `tabDynamic Link`.link_name and - `tabDynamic Link`.parenttype = 'Address') - left join `tabAddress` on ( - `tabAddress`.name=`tabDynamic Link`.parent) - WHERE - company = %(company)s - AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s - {conditions} - ORDER BY - `tabLead`.creation asc """.format( - conditions=get_conditions(filters) - ), - filters, - as_dict=1, + lead = frappe.qb.DocType("Lead") + address = frappe.qb.DocType("Address") + dynamic_link = frappe.qb.DocType("Dynamic Link") + + query = ( + frappe.qb.from_(lead) + .left_join(dynamic_link) + .on((lead.name == dynamic_link.link_name) & (dynamic_link.parenttype == "Address")) + .left_join(address) + .on(address.name == dynamic_link.parent) + .select( + lead.name, + lead.lead_name, + lead.status, + lead.lead_owner, + lead.territory, + lead.source, + lead.email_id, + lead.mobile_no, + lead.phone, + lead.owner, + lead.company, + (Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"), + address.state, + address.pincode, + address.country, + ) + .where(lead.company == filters.company) + .where(Date(lead.creation).between(filters.from_date, filters.to_date)) ) - -def get_conditions(filters): - conditions = [] - if filters.get("territory"): - conditions.append(" and `tabLead`.territory=%(territory)s") + query = query.where(lead.territory == filters.get("territory")) if filters.get("status"): - conditions.append(" and `tabLead`.status=%(status)s") + query = query.where(lead.status == filters.get("status")) - return " ".join(conditions) if conditions else "" + return query.run(as_dict=1) diff --git a/erpnext/e_commerce/__init__.py b/erpnext/e_commerce/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py deleted file mode 100644 index bfada0faa7..0000000000 --- a/erpnext/e_commerce/api.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import json - -import frappe -from frappe.utils import cint - -from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder -from erpnext.e_commerce.product_data_engine.query import ProductQuery -from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website - - -@frappe.whitelist(allow_guest=True) -def get_product_filter_data(query_args=None): - """ - Returns filtered products and discount filters. - :param query_args (dict): contains filters to get products list - - Query Args filters: - search (str): Search Term. - field_filters (dict): Keys include item_group, brand, etc. - attribute_filters(dict): Keys include Color, Size, etc. - start (int): Offset items by - item_group (str): Valid Item Group - from_filters (bool): Set as True to jump to page 1 - """ - if isinstance(query_args, str): - query_args = json.loads(query_args) - - query_args = frappe._dict(query_args) - if query_args: - search = query_args.get("search") - field_filters = query_args.get("field_filters", {}) - attribute_filters = query_args.get("attribute_filters", {}) - start = cint(query_args.start) if query_args.get("start") else 0 - item_group = query_args.get("item_group") - from_filters = query_args.get("from_filters") - else: - search, attribute_filters, item_group, from_filters = None, None, None, None - field_filters = {} - start = 0 - - # if new filter is checked, reset start to show filtered items from page 1 - if from_filters: - start = 0 - - sub_categories = [] - if item_group: - sub_categories = get_child_groups_for_website(item_group, immediate=True) - - engine = ProductQuery() - try: - result = engine.query( - attribute_filters, field_filters, search_term=search, start=start, item_group=item_group - ) - except Exception: - frappe.log_error("Product query with filter failed") - return {"exc": "Something went wrong!"} - - # discount filter data - filters = {} - discounts = result["discounts"] - - if discounts: - filter_engine = ProductFiltersBuilder() - filters["discount_filters"] = filter_engine.get_discount_filters(discounts) - - return { - "items": result["items"] or [], - "filters": filters, - "settings": engine.settings, - "sub_categories": sub_categories, - "items_count": result["items_count"], - } - - -@frappe.whitelist(allow_guest=True) -def get_guest_redirect_on_action(): - return frappe.db.get_single_value("E Commerce Settings", "redirect_on_action") diff --git a/erpnext/e_commerce/doctype/__init__.py b/erpnext/e_commerce/doctype/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/__init__.py b/erpnext/e_commerce/doctype/e_commerce_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 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 deleted file mode 100644 index c37fa2f6ea..0000000000 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -// 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) { - 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" } }; - }); - }, - refresh: function(frm) { - if (frm.doc.enabled) { - frm.get_field('store_page_docs').$wrapper.removeClass('hide-control').html( - `Redisearch is not loaded. If you want to use the advanced product search feature, refer here.
" - }, - { - "default": "0", - "depends_on": "eval:doc.show_price", - "fieldname": "hide_price_for_guest", - "fieldtype": "Check", - "label": "Hide Price for Guest" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "collapsible": 1, - "fieldname": "guest_display_settings_section", - "fieldtype": "Section Break", - "label": "Guest Display Settings" - }, - { - "description": "Link to redirect Guest on actions that need login such as add to cart, wishlist, etc. E.g.: /login", - "fieldname": "redirect_on_action", - "fieldtype": "Data", - "label": "Redirect on Action" - }, - { - "fieldname": "column_break_22", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_23", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "enable_recommendations", - "fieldtype": "Check", - "label": "Enable Recommendations" - }, - { - "default": "0", - "depends_on": "eval: doc.enable_checkout == 0", - "fieldname": "show_price_in_quotation", - "fieldtype": "Check", - "label": "Show Price in Quotation" - }, - { - "default": "0", - "fieldname": "is_redisearch_enabled", - "fieldtype": "Check", - "label": "Enable Redisearch", - "read_only_depends_on": "eval:!doc.is_redisearch_loaded" - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2022-04-01 18:35:56.106756", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "E Commerce Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 1 -} \ No newline at end of file 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 deleted file mode 100644 index c27d29a62c..0000000000 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.utils import comma_and, flt, unique - -from erpnext.e_commerce.redisearch_utils import ( - create_website_items_index, - define_autocomplete_dictionary, - get_indexable_web_fields, - is_search_module_loaded, -) - - -class ShoppingCartSetupError(frappe.ValidationError): - pass - - -class ECommerceSettings(Document): - def onload(self): - self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series") - - # flag >> if redisearch is installed and loaded - self.is_redisearch_loaded = is_search_module_loaded() - - def validate(self): - self.validate_field_filters(self.filter_fields, self.enable_field_filters) - self.validate_attribute_filters() - self.validate_checkout() - self.validate_search_index_fields() - - if self.enabled: - self.validate_price_list_exchange_rate() - - frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings") - - self.is_redisearch_enabled_pre_save = frappe.db.get_single_value( - "E Commerce Settings", "is_redisearch_enabled" - ) - - def after_save(self): - self.create_redisearch_indexes() - - def create_redisearch_indexes(self): - # if redisearch is enabled (value changed) create indexes and dictionary - value_changed = self.is_redisearch_enabled != self.is_redisearch_enabled_pre_save - if self.is_redisearch_loaded and self.is_redisearch_enabled and value_changed: - define_autocomplete_dictionary() - create_website_items_index() - - @staticmethod - def validate_field_filters(filter_fields, enable_field_filters): - if not (enable_field_filters and filter_fields): - return - - web_item_meta = frappe.get_meta("Website Item") - valid_fields = [ - df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] - ] - - for row in filter_fields: - if row.fieldname not in valid_fields: - frappe.throw( - _( - "Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'" - ).format(row.idx, frappe.bold(row.fieldname)) - ) - - def validate_attribute_filters(self): - if not (self.enable_attribute_filters and self.filter_attributes): - return - - # if attribute filters are enabled, hide_variants should be disabled - self.hide_variants = 0 - - def validate_checkout(self): - if self.enable_checkout and not self.payment_gateway_account: - self.enable_checkout = 0 - - def validate_search_index_fields(self): - if not self.search_index_fields: - return - - fields = self.search_index_fields.replace(" ", "") - fields = unique(fields.strip(",").split(",")) # Remove extra ',' and remove duplicates - - # All fields should be indexable - allowed_indexable_fields = get_indexable_web_fields() - - if not (set(fields).issubset(allowed_indexable_fields)): - invalid_fields = list(set(fields).difference(allowed_indexable_fields)) - num_invalid_fields = len(invalid_fields) - invalid_fields = comma_and(invalid_fields) - - if num_invalid_fields > 1: - frappe.throw( - _("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields)) - ) - else: - frappe.throw( - _("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields)) - ) - - self.search_index_fields = ",".join(fields) - - def validate_price_list_exchange_rate(self): - "Check if exchange rate exists for Price List currency (to Company's currency)." - from erpnext.setup.utils import get_exchange_rate - - if not self.enabled or not self.company or not self.price_list: - return # this function is also called from hooks, check values again - - company_currency = frappe.get_cached_value("Company", self.company, "default_currency") - price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency") - - if not company_currency: - msg = f"Please specify currency in Company {self.company}" - frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError) - - if not price_list_currency: - msg = f"Please specify currency in Price List {frappe.bold(self.price_list)}" - frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError) - - if price_list_currency != company_currency: - from_currency, to_currency = price_list_currency, company_currency - - # Get exchange rate checks Currency Exchange Records too - exchange_rate = get_exchange_rate(from_currency, to_currency, args="for_selling") - - if not flt(exchange_rate): - msg = f"Missing Currency Exchange Rates for {from_currency}-{to_currency}" - frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError) - - def validate_tax_rule(self): - if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"): - frappe.throw(frappe._("Set Tax Rule for shopping cart"), ShoppingCartSetupError) - - def get_tax_master(self, billing_territory): - tax_master = self.get_name_from_territory( - billing_territory, "sales_taxes_and_charges_masters", "sales_taxes_and_charges_master" - ) - return tax_master and tax_master[0] or None - - def get_shipping_rules(self, shipping_territory): - return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule") - - def on_change(self): - old_doc = self.get_doc_before_save() - - if old_doc: - old_fields = old_doc.search_index_fields - new_fields = self.search_index_fields - - # if search index fields get changed - if not (new_fields == old_fields): - create_website_items_index() - - -def validate_cart_settings(doc=None, method=None): - frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate") - - -def get_shopping_cart_settings(): - return frappe.get_cached_doc("E Commerce Settings") - - -@frappe.whitelist(allow_guest=True) -def is_cart_enabled(): - return get_shopping_cart_settings().enabled - - -def show_quantity_in_website(): - return get_shopping_cart_settings().show_quantity_in_website - - -def check_shopping_cart_enabled(): - if not get_shopping_cart_settings().enabled: - frappe.throw(_("You need to enable Shopping Cart"), ShoppingCartSetupError) - - -def show_attachments(): - return get_shopping_cart_settings().show_attachments diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py deleted file mode 100644 index 662db4d7ae..0000000000 --- a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -import unittest - -import frappe - -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( - ShoppingCartSetupError, -) - - -class TestECommerceSettings(unittest.TestCase): - def tearDown(self): - frappe.db.rollback() - - def test_tax_rule_validation(self): - frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") - frappe.db.commit() # nosemgrep - - cart_settings = frappe.get_doc("E Commerce Settings") - cart_settings.enabled = 1 - if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"): - self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule) - - frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1") - - def test_invalid_filter_fields(self): - "Check if Item fields are blocked in E Commerce Settings filter fields." - from frappe.custom.doctype.custom_field.custom_field import create_custom_field - - setup_e_commerce_settings({"enable_field_filters": 1}) - - create_custom_field( - "Item", - dict(owner="Administrator", fieldname="test_data", label="Test", fieldtype="Data"), - ) - settings = frappe.get_doc("E Commerce Settings") - settings.append("filter_fields", {"fieldname": "test_data"}) - - self.assertRaises(frappe.ValidationError, settings.save) - - -def setup_e_commerce_settings(values_dict): - "Accepts a dict of values that updates E Commerce Settings." - if not values_dict: - return - - doc = frappe.get_doc("E Commerce Settings", "E Commerce Settings") - doc.update(values_dict) - doc.save() - - -test_dependencies = ["Tax Rule"] diff --git a/erpnext/e_commerce/doctype/item_review/__init__.py b/erpnext/e_commerce/doctype/item_review/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/doctype/item_review/item_review.js b/erpnext/e_commerce/doctype/item_review/item_review.js deleted file mode 100644 index a57c370287..0000000000 --- a/erpnext/e_commerce/doctype/item_review/item_review.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Item Review', { - // refresh: function(frm) { - - // } -}); diff --git a/erpnext/e_commerce/doctype/item_review/item_review.json b/erpnext/e_commerce/doctype/item_review/item_review.json deleted file mode 100644 index 57f719fc3c..0000000000 --- a/erpnext/e_commerce/doctype/item_review/item_review.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "actions": [], - "beta": 1, - "creation": "2021-03-23 16:47:26.542226", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "website_item", - "user", - "customer", - "column_break_3", - "item", - "published_on", - "reviews_section", - "review_title", - "rating", - "comment" - ], - "fields": [ - { - "fieldname": "website_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "user", - "fieldtype": "Link", - "in_list_view": 1, - "label": "User", - "options": "User", - "read_only": 1 - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fetch_from": "website_item.item_code", - "fieldname": "item", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item", - "options": "Item", - "read_only": 1 - }, - { - "fieldname": "reviews_section", - "fieldtype": "Section Break", - "label": "Reviews" - }, - { - "fieldname": "rating", - "fieldtype": "Rating", - "in_list_view": 1, - "label": "Rating", - "read_only": 1 - }, - { - "fieldname": "comment", - "fieldtype": "Small Text", - "label": "Comment", - "read_only": 1 - }, - { - "fieldname": "review_title", - "fieldtype": "Data", - "label": "Review Title", - "read_only": 1 - }, - { - "fieldname": "customer", - "fieldtype": "Link", - "label": "Customer", - "options": "Customer", - "read_only": 1 - }, - { - "fieldname": "published_on", - "fieldtype": "Data", - "label": "Published on", - "read_only": 1 - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-08-10 12:08:58.119691", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Item Review", - "owner": "Administrator", - "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": "Website Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "report": 1, - "role": "Customer", - "share": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/item_review/item_review.py b/erpnext/e_commerce/doctype/item_review/item_review.py deleted file mode 100644 index 3e540e3885..0000000000 --- a/erpnext/e_commerce/doctype/item_review/item_review.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from datetime import datetime - -import frappe -from frappe import _ -from frappe.contacts.doctype.contact.contact import get_contact_name -from frappe.model.document import Document -from frappe.utils import cint, flt - -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( - get_shopping_cart_settings, -) - - -class UnverifiedReviewer(frappe.ValidationError): - pass - - -class ItemReview(Document): - def after_insert(self): - # regenerate cache on review creation - reviews_dict = get_queried_reviews(self.website_item) - set_reviews_in_cache(self.website_item, reviews_dict) - - def after_delete(self): - # regenerate cache on review deletion - reviews_dict = get_queried_reviews(self.website_item) - set_reviews_in_cache(self.website_item, reviews_dict) - - -@frappe.whitelist() -def get_item_reviews(web_item, start=0, end=10, data=None): - "Get Website Item Review Data." - start, end = cint(start), cint(end) - settings = get_shopping_cart_settings() - - # Get cached reviews for first page (start=0) - # avoid cache when page is different - from_cache = not bool(start) - - if not data: - data = frappe._dict() - - if settings and settings.get("enable_reviews"): - reviews_cache = frappe.cache().hget("item_reviews", web_item) - if from_cache and reviews_cache: - data = reviews_cache - else: - data = get_queried_reviews(web_item, start, end, data) - if from_cache: - set_reviews_in_cache(web_item, data) - - return data - - -def get_queried_reviews(web_item, start=0, end=10, data=None): - """ - Query Website Item wise reviews and cache if needed. - Cache stores only first page of reviews i.e. 10 reviews maximum. - Returns: - dict: Containing reviews, average ratings, % of reviews per rating and total reviews. - """ - if not data: - data = frappe._dict() - - data.reviews = frappe.db.get_all( - "Item Review", - filters={"website_item": web_item}, - fields=["*"], - limit_start=start, - limit_page_length=end, - ) - - rating_data = frappe.db.get_all( - "Item Review", - filters={"website_item": web_item}, - fields=["avg(rating) as average, count(*) as total"], - )[0] - - data.average_rating = flt(rating_data.average, 1) - data.average_whole_rating = flt(data.average_rating, 0) - - # get % of reviews per rating - reviews_per_rating = [] - for i in range(1, 6): - count = frappe.db.get_all( - "Item Review", filters={"website_item": web_item, "rating": i}, fields=["count(*) as count"] - )[0].count - - percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0 - reviews_per_rating.append(percent) - - data.reviews_per_rating = reviews_per_rating - data.total_reviews = rating_data.total - - return data - - -def set_reviews_in_cache(web_item, reviews_dict): - frappe.cache().hset("item_reviews", web_item, reviews_dict) - - -@frappe.whitelist() -def add_item_review(web_item, title, rating, comment=None): - """Add an Item Review by a user if non-existent.""" - if frappe.session.user == "Guest": - # guest user should not reach here ideally in the case they do via an API, throw error - frappe.throw(_("You are not verified to write a review yet."), exc=UnverifiedReviewer) - - if not frappe.db.exists("Item Review", {"user": frappe.session.user, "website_item": web_item}): - doc = frappe.get_doc( - { - "doctype": "Item Review", - "user": frappe.session.user, - "customer": get_customer(), - "website_item": web_item, - "item": frappe.db.get_value("Website Item", web_item, "item_code"), - "review_title": title, - "rating": rating, - "comment": comment, - } - ) - doc.published_on = datetime.today().strftime("%d %B %Y") - doc.insert() - - -def get_customer(silent=False): - """ - silent: Return customer if exists else return nothing. Dont throw error. - """ - user = frappe.session.user - contact_name = get_contact_name(user) - customer = None - - if contact_name: - contact = frappe.get_doc("Contact", contact_name) - for link in contact.links: - if link.link_doctype == "Customer": - customer = link.link_name - break - - if customer: - return frappe.db.get_value("Customer", customer) - elif silent: - return None - else: - # should not reach here unless via an API - frappe.throw( - _("You are not a verified customer yet. Please contact us to proceed."), exc=UnverifiedReviewer - ) diff --git a/erpnext/e_commerce/doctype/item_review/test_item_review.py b/erpnext/e_commerce/doctype/item_review/test_item_review.py deleted file mode 100644 index 8a4befc800..0000000000 --- a/erpnext/e_commerce/doctype/item_review/test_item_review.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- 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 erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import ( - setup_e_commerce_settings, -) -from erpnext.e_commerce.doctype.item_review.item_review import ( - UnverifiedReviewer, - add_item_review, - get_item_reviews, -) -from erpnext.e_commerce.doctype.website_item.website_item import make_website_item -from erpnext.e_commerce.shopping_cart.cart import get_party -from erpnext.stock.doctype.item.test_item import make_item - - -class TestItemReview(unittest.TestCase): - def setUp(self): - item = make_item("Test Mobile Phone") - if not frappe.db.exists("Website Item", {"item_code": "Test Mobile Phone"}): - make_website_item(item, save=True) - - setup_e_commerce_settings({"enable_reviews": 1}) - 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}) - - 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)" - # create user - web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"}) - test_user = create_user("test_reviewer@example.com", "Customer") - frappe.set_user(test_user.name) - - # create customer and contact against user - customer = get_party() - - # post review on "Test Mobile Phone" - try: - add_item_review(web_item, "Great Product", 3, "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}") - - 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) - - # tear down - frappe.set_user("Administrator") - frappe.delete_doc("Item Review", review_name) - customer.delete() - - def test_add_item_review_from_non_customer(self): - "Check if logged in user (who is not a customer yet) is blocked from posting reviews." - web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"}) - test_user = create_user("test_reviewer@example.com", "Customer") - frappe.set_user(test_user.name) - - with self.assertRaises(UnverifiedReviewer): - add_item_review(web_item, "Great Product", 3, "Would recommend this product") - - # tear down - frappe.set_user("Administrator") - - def test_add_item_reviews_from_guest_user(self): - "Check if Guest user is blocked from posting reviews." - web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"}) - frappe.set_user("Guest") - - with self.assertRaises(UnverifiedReviewer): - add_item_review(web_item, "Great Product", 3, "Would recommend this product") - - # tear down - frappe.set_user("Administrator") diff --git a/erpnext/e_commerce/doctype/recommended_items/__init__.py b/erpnext/e_commerce/doctype/recommended_items/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/doctype/recommended_items/recommended_items.json b/erpnext/e_commerce/doctype/recommended_items/recommended_items.json deleted file mode 100644 index 1821532323..0000000000 --- a/erpnext/e_commerce/doctype/recommended_items/recommended_items.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "actions": [], - "creation": "2021-07-12 20:52:12.503470", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "website_item", - "website_item_name", - "column_break_2", - "item_code", - "more_information_section", - "route", - "column_break_6", - "website_item_image", - "website_item_thumbnail" - ], - "fields": [ - { - "fieldname": "website_item", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Website Item", - "options": "Website Item" - }, - { - "fetch_from": "website_item.web_item_name", - "fieldname": "website_item_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Website Item Name", - "read_only": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "more_information_section", - "fieldtype": "Section Break", - "label": "More Information" - }, - { - "fetch_from": "website_item.route", - "fieldname": "route", - "fieldtype": "Small Text", - "label": "Route", - "read_only": 1 - }, - { - "fetch_from": "website_item.website_image", - "fieldname": "website_item_image", - "fieldtype": "Attach", - "label": "Website Item Image", - "read_only": 1 - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break" - }, - { - "fetch_from": "website_item.thumbnail", - "fieldname": "website_item_thumbnail", - "fieldtype": "Data", - "label": "Website Item Thumbnail", - "read_only": 1 - }, - { - "fetch_from": "website_item.item_code", - "fieldname": "item_code", - "fieldtype": "Data", - "label": "Item Code" - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2022-06-28 16:44:24.718728", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Recommended Items", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/recommended_items/recommended_items.py b/erpnext/e_commerce/doctype/recommended_items/recommended_items.py deleted file mode 100644 index 16b6e52047..0000000000 --- a/erpnext/e_commerce/doctype/recommended_items/recommended_items.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class RecommendedItems(Document): - pass diff --git a/erpnext/e_commerce/doctype/website_item/__init__.py b/erpnext/e_commerce/doctype/website_item/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/doctype/website_item/templates/website_item.html b/erpnext/e_commerce/doctype/website_item/templates/website_item.html deleted file mode 100644 index db123090aa..0000000000 --- a/erpnext/e_commerce/doctype/website_item/templates/website_item.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "templates/web.html" %} - -{% block page_content %} -- ${ item.item_group } | Item Code : ${ item.item_code } -
-
- {message}
-{{ subtitle }}
{%- endif -%} - {%- if action -%} - - {{ label }} - - {%- endif -%} -{{ subtitle }}
- {%- endif -%} -{{ subtitle }}
- {%- endif -%} - -| {{ __("Account Type") }} | -{{ __("Current Balance") }} | -{{ __("Available Balance") }} | -{{ __("Reserved Balance") }} | -{{ __("Uncleared Balance") }} | -
|---|---|---|---|---|
| {%= key %} | -{%= value["current_balance"] %} | -{%= value["available_balance"] %} | -{%= value["reserved_balance"] %} | -{%= value["uncleared_balance"] %} | -
Account Balance Information Not Available.
-{% endif %} diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py deleted file mode 100644 index a577e7fa69..0000000000 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py +++ /dev/null @@ -1,149 +0,0 @@ -import base64 -import datetime - -import requests -from requests.auth import HTTPBasicAuth - - -class MpesaConnector: - def __init__( - self, - env="sandbox", - app_key=None, - app_secret=None, - sandbox_url="https://sandbox.safaricom.co.ke", - live_url="https://api.safaricom.co.ke", - ): - """Setup configuration for Mpesa connector and generate new access token.""" - self.env = env - self.app_key = app_key - self.app_secret = app_secret - if env == "sandbox": - self.base_url = sandbox_url - else: - self.base_url = live_url - self.authenticate() - - def authenticate(self): - """ - This method is used to fetch the access token required by Mpesa. - - Returns: - access_token (str): This token is to be used with the Bearer header for further API calls to Mpesa. - """ - authenticate_uri = "/oauth/v1/generate?grant_type=client_credentials" - authenticate_url = "{0}{1}".format(self.base_url, authenticate_uri) - r = requests.get(authenticate_url, auth=HTTPBasicAuth(self.app_key, self.app_secret)) - self.authentication_token = r.json()["access_token"] - return r.json()["access_token"] - - def get_balance( - self, - initiator=None, - security_credential=None, - party_a=None, - identifier_type=None, - remarks=None, - queue_timeout_url=None, - result_url=None, - ): - """ - This method uses Mpesa's Account Balance API to to enquire the balance on a M-Pesa BuyGoods (Till Number). - - Args: - initiator (str): Username used to authenticate the transaction. - security_credential (str): Generate from developer portal. - command_id (str): AccountBalance. - party_a (int): Till number being queried. - identifier_type (int): Type of organization receiving the transaction. (MSISDN/Till Number/Organization short code) - remarks (str): Comments that are sent along with the transaction(maximum 100 characters). - queue_timeout_url (str): The url that handles information of timed out transactions. - result_url (str): The url that receives results from M-Pesa api call. - - Returns: - OriginatorConverstionID (str): The unique request ID for tracking a transaction. - ConversationID (str): The unique request ID returned by mpesa for each request made - ResponseDescription (str): Response Description message - """ - - payload = { - "Initiator": initiator, - "SecurityCredential": security_credential, - "CommandID": "AccountBalance", - "PartyA": party_a, - "IdentifierType": identifier_type, - "Remarks": remarks, - "QueueTimeOutURL": queue_timeout_url, - "ResultURL": result_url, - } - headers = { - "Authorization": "Bearer {0}".format(self.authentication_token), - "Content-Type": "application/json", - } - saf_url = "{0}{1}".format(self.base_url, "/mpesa/accountbalance/v1/query") - r = requests.post(saf_url, headers=headers, json=payload) - return r.json() - - def stk_push( - self, - business_shortcode=None, - passcode=None, - amount=None, - callback_url=None, - reference_code=None, - phone_number=None, - description=None, - ): - """ - This method uses Mpesa's Express API to initiate online payment on behalf of a customer. - - Args: - business_shortcode (int): The short code of the organization. - passcode (str): Get from developer portal - amount (int): The amount being transacted - callback_url (str): A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API. - reference_code(str): Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier of the transaction for CustomerPayBillOnline transaction type. - phone_number(int): The Mobile Number to receive the STK Pin Prompt. - description(str): This is any additional information/comment that can be sent along with the request from your system. MAX 13 characters - - Success Response: - CustomerMessage(str): Messages that customers can understand. - CheckoutRequestID(str): This is a global unique identifier of the processed checkout transaction request. - ResponseDescription(str): Describes Success or failure - MerchantRequestID(str): This is a global unique Identifier for any submitted payment request. - ResponseCode(int): 0 means success all others are error codes. e.g.404.001.03 - - Error Reponse: - requestId(str): This is a unique requestID for the payment request - errorCode(str): This is a predefined code that indicates the reason for request failure. - errorMessage(str): This is a predefined code that indicates the reason for request failure. - """ - - time = ( - str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "") - ) - password = "{0}{1}{2}".format(str(business_shortcode), str(passcode), time) - encoded = base64.b64encode(bytes(password, encoding="utf8")) - payload = { - "BusinessShortCode": business_shortcode, - "Password": encoded.decode("utf-8"), - "Timestamp": time, - "Amount": amount, - "PartyA": int(phone_number), - "PartyB": reference_code, - "PhoneNumber": int(phone_number), - "CallBackURL": callback_url, - "AccountReference": reference_code, - "TransactionDesc": description, - "TransactionType": "CustomerPayBillOnline" - if self.env == "sandbox" - else "CustomerBuyGoodsOnline", - } - headers = { - "Authorization": "Bearer {0}".format(self.authentication_token), - "Content-Type": "application/json", - } - - saf_url = "{0}{1}".format(self.base_url, "/mpesa/stkpush/v1/processrequest") - r = requests.post(saf_url, headers=headers, json=payload) - return r.json() diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py deleted file mode 100644 index c92edc5efa..0000000000 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py +++ /dev/null @@ -1,56 +0,0 @@ -import frappe -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - - -def create_custom_pos_fields(): - """Create custom fields corresponding to POS Settings and POS Invoice.""" - pos_field = { - "POS Invoice": [ - { - "fieldname": "request_for_payment", - "label": "Request for Payment", - "fieldtype": "Button", - "hidden": 1, - "insert_after": "contact_email", - }, - { - "fieldname": "mpesa_receipt_number", - "label": "Mpesa Receipt Number", - "fieldtype": "Data", - "read_only": 1, - "insert_after": "company", - }, - ] - } - if not frappe.get_meta("POS Invoice").has_field("request_for_payment"): - create_custom_fields(pos_field) - - record_dict = [ - { - "doctype": "POS Field", - "fieldname": "contact_mobile", - "label": "Mobile No", - "fieldtype": "Data", - "options": "Phone", - "parenttype": "POS Settings", - "parent": "POS Settings", - "parentfield": "invoice_fields", - }, - { - "doctype": "POS Field", - "fieldname": "request_for_payment", - "label": "Request for Payment", - "fieldtype": "Button", - "parenttype": "POS Settings", - "parent": "POS Settings", - "parentfield": "invoice_fields", - }, - ] - create_pos_settings(record_dict) - - -def create_pos_settings(record_dict): - for record in record_dict: - if frappe.db.exists("POS Field", {"fieldname": record.get("fieldname")}): - continue - frappe.get_doc(record).insert() diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js deleted file mode 100644 index 447d720ca2..0000000000 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Mpesa Settings', { - onload_post_render: function(frm) { - frm.events.setup_account_balance_html(frm); - }, - - refresh: function(frm) { - erpnext.utils.check_payments_app(); - - frappe.realtime.on("refresh_mpesa_dashboard", function(){ - frm.reload_doc(); - frm.events.setup_account_balance_html(frm); - }); - }, - - get_account_balance: function(frm) { - if (!frm.doc.initiator_name && !frm.doc.security_credential) { - frappe.throw(__("Please set the initiator name and the security credential")); - } - frappe.call({ - method: "get_account_balance_info", - doc: frm.doc - }); - }, - - setup_account_balance_html: function(frm) { - if (!frm.doc.account_balance) return; - $("div").remove(".form-dashboard-section.custom"); - frm.dashboard.add_section( - frappe.render_template('account_balance', { - data: JSON.parse(frm.doc.account_balance) - }) - ); - frm.dashboard.show(); - } - -}); diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json deleted file mode 100644 index 8f3b4271c1..0000000000 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "actions": [], - "autoname": "field:payment_gateway_name", - "creation": "2020-09-10 13:21:27.398088", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "payment_gateway_name", - "consumer_key", - "consumer_secret", - "initiator_name", - "till_number", - "transaction_limit", - "sandbox", - "column_break_4", - "business_shortcode", - "online_passkey", - "security_credential", - "get_account_balance", - "account_balance" - ], - "fields": [ - { - "fieldname": "payment_gateway_name", - "fieldtype": "Data", - "label": "Payment Gateway Name", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "consumer_key", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Consumer Key", - "reqd": 1 - }, - { - "fieldname": "consumer_secret", - "fieldtype": "Password", - "in_list_view": 1, - "label": "Consumer Secret", - "reqd": 1 - }, - { - "fieldname": "till_number", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Till Number", - "reqd": 1 - }, - { - "default": "0", - "fieldname": "sandbox", - "fieldtype": "Check", - "label": "Sandbox" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "online_passkey", - "fieldtype": "Password", - "label": " Online PassKey", - "reqd": 1 - }, - { - "fieldname": "initiator_name", - "fieldtype": "Data", - "label": "Initiator Name" - }, - { - "fieldname": "security_credential", - "fieldtype": "Small Text", - "label": "Security Credential" - }, - { - "fieldname": "account_balance", - "fieldtype": "Long Text", - "hidden": 1, - "label": "Account Balance", - "read_only": 1 - }, - { - "fieldname": "get_account_balance", - "fieldtype": "Button", - "label": "Get Account Balance" - }, - { - "depends_on": "eval:(doc.sandbox==0)", - "fieldname": "business_shortcode", - "fieldtype": "Data", - "label": "Business Shortcode", - "mandatory_depends_on": "eval:(doc.sandbox==0)" - }, - { - "default": "150000", - "fieldname": "transaction_limit", - "fieldtype": "Float", - "label": "Transaction Limit", - "non_negative": 1 - } - ], - "links": [], - "modified": "2021-03-02 17:35:14.084342", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "Mpesa Settings", - "owner": "Administrator", - "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": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py deleted file mode 100644 index a298e11eaf..0000000000 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py +++ /dev/null @@ -1,354 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies and contributors -# For license information, please see license.txt - - -from json import dumps, loads - -import frappe -from frappe import _ -from frappe.integrations.utils import create_request_log -from frappe.model.document import Document -from frappe.utils import call_hook_method, fmt_money, get_request_site_address - -from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_connector import MpesaConnector -from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_custom_fields import ( - create_custom_pos_fields, -) -from erpnext.erpnext_integrations.utils import create_mode_of_payment -from erpnext.utilities import payment_app_import_guard - - -class MpesaSettings(Document): - supported_currencies = ["KES"] - - def validate_transaction_currency(self, currency): - if currency not in self.supported_currencies: - frappe.throw( - _( - "Please select another payment method. Mpesa does not support transactions in currency '{0}'" - ).format(currency) - ) - - def on_update(self): - with payment_app_import_guard(): - from payments.utils import create_payment_gateway - - create_custom_pos_fields() - create_payment_gateway( - "Mpesa-" + self.payment_gateway_name, - settings="Mpesa Settings", - controller=self.payment_gateway_name, - ) - call_hook_method( - "payment_gateway_enabled", gateway="Mpesa-" + self.payment_gateway_name, payment_channel="Phone" - ) - - # required to fetch the bank account details from the payment gateway account - frappe.db.commit() - create_mode_of_payment("Mpesa-" + self.payment_gateway_name, payment_type="Phone") - - def request_for_payment(self, **kwargs): - args = frappe._dict(kwargs) - request_amounts = self.split_request_amount_according_to_transaction_limit(args) - - for i, amount in enumerate(request_amounts): - args.request_amount = amount - if frappe.flags.in_test: - from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import ( - get_payment_request_response_payload, - ) - - response = frappe._dict(get_payment_request_response_payload(amount)) - else: - response = frappe._dict(generate_stk_push(**args)) - - self.handle_api_response("CheckoutRequestID", args, response) - - def split_request_amount_according_to_transaction_limit(self, args): - request_amount = args.request_amount - if request_amount > self.transaction_limit: - # make multiple requests - request_amounts = [] - requests_to_be_made = frappe.utils.ceil( - request_amount / self.transaction_limit - ) # 480/150 = ceil(3.2) = 4 - for i in range(requests_to_be_made): - amount = self.transaction_limit - if i == requests_to_be_made - 1: - amount = request_amount - ( - self.transaction_limit * i - ) # for 4th request, 480 - (150 * 3) = 30 - request_amounts.append(amount) - else: - request_amounts = [request_amount] - - return request_amounts - - @frappe.whitelist() - def get_account_balance_info(self): - payload = dict( - reference_doctype="Mpesa Settings", reference_docname=self.name, doc_details=vars(self) - ) - - if frappe.flags.in_test: - from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import ( - get_test_account_balance_response, - ) - - response = frappe._dict(get_test_account_balance_response()) - else: - response = frappe._dict(get_account_balance(payload)) - - self.handle_api_response("ConversationID", payload, response) - - def handle_api_response(self, global_id, request_dict, response): - """Response received from API calls returns a global identifier for each transaction, this code is returned during the callback.""" - # check error response - if getattr(response, "requestId"): - req_name = getattr(response, "requestId") - error = response - else: - # global checkout id used as request name - req_name = getattr(response, global_id) - error = None - - if not frappe.db.exists("Integration Request", req_name): - create_request_log(request_dict, "Host", "Mpesa", req_name, error) - - if error: - frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error")) - - -def generate_stk_push(**kwargs): - """Generate stk push by making a API call to the stk push API.""" - args = frappe._dict(kwargs) - try: - callback_url = ( - get_request_site_address(True) - + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction" - ) - - mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:]) - env = "production" if not mpesa_settings.sandbox else "sandbox" - # for sandbox, business shortcode is same as till number - business_shortcode = ( - mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number - ) - - connector = MpesaConnector( - env=env, - app_key=mpesa_settings.consumer_key, - app_secret=mpesa_settings.get_password("consumer_secret"), - ) - - mobile_number = sanitize_mobile_number(args.sender) - - response = connector.stk_push( - business_shortcode=business_shortcode, - amount=args.request_amount, - passcode=mpesa_settings.get_password("online_passkey"), - callback_url=callback_url, - reference_code=mpesa_settings.till_number, - phone_number=mobile_number, - description="POS Payment", - ) - - return response - - except Exception: - frappe.log_error("Mpesa Express Transaction Error") - frappe.throw( - _("Issue detected with Mpesa configuration, check the error logs for more details"), - title=_("Mpesa Express Error"), - ) - - -def sanitize_mobile_number(number): - """Add country code and strip leading zeroes from the phone number.""" - return "254" + str(number).lstrip("0") - - -@frappe.whitelist(allow_guest=True) -def verify_transaction(**kwargs): - """Verify the transaction result received via callback from stk.""" - transaction_response = frappe._dict(kwargs["Body"]["stkCallback"]) - - checkout_id = getattr(transaction_response, "CheckoutRequestID", "") - if not isinstance(checkout_id, str): - frappe.throw(_("Invalid Checkout Request ID")) - - integration_request = frappe.get_doc("Integration Request", checkout_id) - transaction_data = frappe._dict(loads(integration_request.data)) - total_paid = 0 # for multiple integration request made against a pos invoice - success = False # for reporting successfull callback to point of sale ui - - if transaction_response["ResultCode"] == 0: - if integration_request.reference_doctype and integration_request.reference_docname: - try: - item_response = transaction_response["CallbackMetadata"]["Item"] - amount = fetch_param_value(item_response, "Amount", "Name") - mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") - pr = frappe.get_doc( - integration_request.reference_doctype, integration_request.reference_docname - ) - - mpesa_receipts, completed_payments = get_completed_integration_requests_info( - integration_request.reference_doctype, integration_request.reference_docname, checkout_id - ) - - total_paid = amount + sum(completed_payments) - mpesa_receipts = ", ".join(mpesa_receipts + [mpesa_receipt]) - - if total_paid >= pr.grand_total: - pr.run_method("on_payment_authorized", "Completed") - success = True - - frappe.db.set_value("POS Invoice", pr.reference_name, "mpesa_receipt_number", mpesa_receipts) - integration_request.handle_success(transaction_response) - except Exception: - integration_request.handle_failure(transaction_response) - frappe.log_error("Mpesa: Failed to verify transaction") - - else: - integration_request.handle_failure(transaction_response) - - frappe.publish_realtime( - event="process_phone_payment", - doctype="POS Invoice", - docname=transaction_data.payment_reference, - user=integration_request.owner, - message={ - "amount": total_paid, - "success": success, - "failure_message": transaction_response["ResultDesc"] - if transaction_response["ResultCode"] != 0 - else "", - }, - ) - - -def get_completed_integration_requests_info(reference_doctype, reference_docname, checkout_id): - output_of_other_completed_requests = frappe.get_all( - "Integration Request", - filters={ - "name": ["!=", checkout_id], - "reference_doctype": reference_doctype, - "reference_docname": reference_docname, - "status": "Completed", - }, - pluck="output", - ) - - mpesa_receipts, completed_payments = [], [] - - for out in output_of_other_completed_requests: - out = frappe._dict(loads(out)) - item_response = out["CallbackMetadata"]["Item"] - completed_amount = fetch_param_value(item_response, "Amount", "Name") - completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") - completed_payments.append(completed_amount) - mpesa_receipts.append(completed_mpesa_receipt) - - return mpesa_receipts, completed_payments - - -def get_account_balance(request_payload): - """Call account balance API to send the request to the Mpesa Servers.""" - try: - mpesa_settings = frappe.get_doc("Mpesa Settings", request_payload.get("reference_docname")) - env = "production" if not mpesa_settings.sandbox else "sandbox" - connector = MpesaConnector( - env=env, - app_key=mpesa_settings.consumer_key, - app_secret=mpesa_settings.get_password("consumer_secret"), - ) - - callback_url = ( - get_request_site_address(True) - + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info" - ) - - response = connector.get_balance( - mpesa_settings.initiator_name, - mpesa_settings.security_credential, - mpesa_settings.till_number, - 4, - mpesa_settings.name, - callback_url, - callback_url, - ) - return response - except Exception: - frappe.log_error("Mpesa: Failed to get account balance") - frappe.throw(_("Please check your configuration and try again"), title=_("Error")) - - -@frappe.whitelist(allow_guest=True) -def process_balance_info(**kwargs): - """Process and store account balance information received via callback from the account balance API call.""" - account_balance_response = frappe._dict(kwargs["Result"]) - - conversation_id = getattr(account_balance_response, "ConversationID", "") - if not isinstance(conversation_id, str): - frappe.throw(_("Invalid Conversation ID")) - - request = frappe.get_doc("Integration Request", conversation_id) - - if request.status == "Completed": - return - - transaction_data = frappe._dict(loads(request.data)) - - if account_balance_response["ResultCode"] == 0: - try: - result_params = account_balance_response["ResultParameters"]["ResultParameter"] - - balance_info = fetch_param_value(result_params, "AccountBalance", "Key") - balance_info = format_string_to_json(balance_info) - - ref_doc = frappe.get_doc(transaction_data.reference_doctype, transaction_data.reference_docname) - ref_doc.db_set("account_balance", balance_info) - - request.handle_success(account_balance_response) - frappe.publish_realtime( - "refresh_mpesa_dashboard", - doctype="Mpesa Settings", - docname=transaction_data.reference_docname, - user=transaction_data.owner, - ) - except Exception: - request.handle_failure(account_balance_response) - frappe.log_error( - title="Mpesa Account Balance Processing Error", message=account_balance_response - ) - else: - request.handle_failure(account_balance_response) - - -def format_string_to_json(balance_info): - """ - Format string to json. - - e.g: '''Working Account|KES|481000.00|481000.00|0.00|0.00''' - => {'Working Account': {'current_balance': '481000.00', - 'available_balance': '481000.00', - 'reserved_balance': '0.00', - 'uncleared_balance': '0.00'}} - """ - balance_dict = frappe._dict() - for account_info in balance_info.split("&"): - account_info = account_info.split("|") - balance_dict[account_info[0]] = dict( - current_balance=fmt_money(account_info[2], currency="KES"), - available_balance=fmt_money(account_info[3], currency="KES"), - reserved_balance=fmt_money(account_info[4], currency="KES"), - uncleared_balance=fmt_money(account_info[5], currency="KES"), - ) - return dumps(balance_dict) - - -def fetch_param_value(response, key, key_field): - """Fetch the specified key from list of dictionary. Key is identified via the key field.""" - for param in response: - if param[key_field] == key: - return param["Value"] diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py deleted file mode 100644 index b52662421d..0000000000 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ /dev/null @@ -1,361 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest -from json import dumps - -import frappe - -from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice -from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import ( - process_balance_info, - verify_transaction, -) -from erpnext.erpnext_integrations.utils import create_mode_of_payment - - -class TestMpesaSettings(unittest.TestCase): - def setUp(self): - # create payment gateway in setup - create_mpesa_settings(payment_gateway_name="_Test") - create_mpesa_settings(payment_gateway_name="_Account Balance") - create_mpesa_settings(payment_gateway_name="Payment") - - def tearDown(self): - frappe.db.sql("delete from `tabMpesa Settings`") - frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'") - - def test_creation_of_payment_gateway(self): - mode_of_payment = create_mode_of_payment("Mpesa-_Test", payment_type="Phone") - self.assertTrue(frappe.db.exists("Payment Gateway Account", {"payment_gateway": "Mpesa-_Test"})) - self.assertTrue(mode_of_payment.name) - self.assertEqual(mode_of_payment.type, "Phone") - - def test_processing_of_account_balance(self): - mpesa_doc = create_mpesa_settings(payment_gateway_name="_Account Balance") - mpesa_doc.get_account_balance_info() - - callback_response = get_account_balance_callback_payload() - process_balance_info(**callback_response) - integration_request = frappe.get_doc("Integration Request", "AG_20200927_00007cdb1f9fb6494315") - - # test integration request creation and successful update of the status on receiving callback response - self.assertTrue(integration_request) - self.assertEqual(integration_request.status, "Completed") - - # test formatting of account balance received as string to json with appropriate currency symbol - mpesa_doc.reload() - self.assertEqual( - mpesa_doc.account_balance, - dumps( - { - "Working Account": { - "current_balance": "Sh 481,000.00", - "available_balance": "Sh 481,000.00", - "reserved_balance": "Sh 0.00", - "uncleared_balance": "Sh 0.00", - } - } - ), - ) - - integration_request.delete() - - def test_processing_of_callback_payload(self): - mpesa_account = frappe.db.get_value( - "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" - ) - frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") - frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") - - pos_invoice = create_pos_invoice(do_not_submit=1) - pos_invoice.append( - "payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 500} - ) - pos_invoice.contact_mobile = "093456543894" - pos_invoice.currency = "KES" - pos_invoice.save() - - pr = pos_invoice.create_payment_request() - # test payment request creation - self.assertEqual(pr.payment_gateway, "Mpesa-Payment") - - # submitting payment request creates integration requests with random id - integration_req_ids = frappe.get_all( - "Integration Request", - filters={ - "reference_doctype": pr.doctype, - "reference_docname": pr.name, - }, - pluck="name", - ) - - callback_response = get_payment_callback_payload( - Amount=500, CheckoutRequestID=integration_req_ids[0] - ) - verify_transaction(**callback_response) - # test creation of integration request - integration_request = frappe.get_doc("Integration Request", integration_req_ids[0]) - - # test integration request creation and successful update of the status on receiving callback response - self.assertTrue(integration_request) - self.assertEqual(integration_request.status, "Completed") - - pos_invoice.reload() - integration_request.reload() - self.assertEqual(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R") - self.assertEqual(integration_request.status, "Completed") - - frappe.db.set_value("Customer", "_Test Customer", "default_currency", "") - integration_request.delete() - pr.reload() - pr.cancel() - pr.delete() - pos_invoice.delete() - - def test_processing_of_multiple_callback_payload(self): - mpesa_account = frappe.db.get_value( - "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" - ) - frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") - frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") - frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") - - pos_invoice = create_pos_invoice(do_not_submit=1) - pos_invoice.append( - "payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 1000} - ) - pos_invoice.contact_mobile = "093456543894" - pos_invoice.currency = "KES" - pos_invoice.save() - - pr = pos_invoice.create_payment_request() - # test payment request creation - self.assertEqual(pr.payment_gateway, "Mpesa-Payment") - - # submitting payment request creates integration requests with random id - integration_req_ids = frappe.get_all( - "Integration Request", - filters={ - "reference_doctype": pr.doctype, - "reference_docname": pr.name, - }, - pluck="name", - ) - - # create random receipt nos and send it as response to callback handler - mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids] - - integration_requests = [] - for i in range(len(integration_req_ids)): - callback_response = get_payment_callback_payload( - Amount=500, - CheckoutRequestID=integration_req_ids[i], - MpesaReceiptNumber=mpesa_receipt_numbers[i], - ) - # handle response manually - verify_transaction(**callback_response) - # test completion of integration request - integration_request = frappe.get_doc("Integration Request", integration_req_ids[i]) - self.assertEqual(integration_request.status, "Completed") - integration_requests.append(integration_request) - - # check receipt number once all the integration requests are completed - pos_invoice.reload() - self.assertEqual(pos_invoice.mpesa_receipt_number, ", ".join(mpesa_receipt_numbers)) - - frappe.db.set_value("Customer", "_Test Customer", "default_currency", "") - [d.delete() for d in integration_requests] - pr.reload() - pr.cancel() - pr.delete() - pos_invoice.delete() - - def test_processing_of_only_one_succes_callback_payload(self): - mpesa_account = frappe.db.get_value( - "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" - ) - frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") - frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") - frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") - - pos_invoice = create_pos_invoice(do_not_submit=1) - pos_invoice.append( - "payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 1000} - ) - pos_invoice.contact_mobile = "093456543894" - pos_invoice.currency = "KES" - pos_invoice.save() - - pr = pos_invoice.create_payment_request() - # test payment request creation - self.assertEqual(pr.payment_gateway, "Mpesa-Payment") - - # submitting payment request creates integration requests with random id - integration_req_ids = frappe.get_all( - "Integration Request", - filters={ - "reference_doctype": pr.doctype, - "reference_docname": pr.name, - }, - pluck="name", - ) - - # create random receipt nos and send it as response to callback handler - mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids] - - callback_response = get_payment_callback_payload( - Amount=500, - CheckoutRequestID=integration_req_ids[0], - MpesaReceiptNumber=mpesa_receipt_numbers[0], - ) - # handle response manually - verify_transaction(**callback_response) - # test completion of integration request - integration_request = frappe.get_doc("Integration Request", integration_req_ids[0]) - self.assertEqual(integration_request.status, "Completed") - - # now one request is completed - # second integration request fails - # now retrying payment request should make only one integration request again - pr = pos_invoice.create_payment_request() - new_integration_req_ids = frappe.get_all( - "Integration Request", - filters={ - "reference_doctype": pr.doctype, - "reference_docname": pr.name, - "name": ["not in", integration_req_ids], - }, - pluck="name", - ) - - self.assertEqual(len(new_integration_req_ids), 1) - - frappe.db.set_value("Customer", "_Test Customer", "default_currency", "") - frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'") - pr.reload() - pr.cancel() - pr.delete() - pos_invoice.delete() - - -def create_mpesa_settings(payment_gateway_name="Express"): - if frappe.db.exists("Mpesa Settings", payment_gateway_name): - return frappe.get_doc("Mpesa Settings", payment_gateway_name) - - doc = frappe.get_doc( - dict( # nosec - doctype="Mpesa Settings", - sandbox=1, - payment_gateway_name=payment_gateway_name, - consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn", - consumer_secret="VI1oS3oBGPJfh3JyvLHw", - online_passkey="LVI1oS3oBGPJfh3JyvLHwZOd", - till_number="174379", - ) - ) - - doc.insert(ignore_permissions=True) - return doc - - -def get_test_account_balance_response(): - """Response received after calling the account balance API.""" - return { - "ResultType": 0, - "ResultCode": 0, - "ResultDesc": "The service request has been accepted successfully.", - "OriginatorConversationID": "10816-694520-2", - "ConversationID": "AG_20200927_00007cdb1f9fb6494315", - "TransactionID": "LGR0000000", - "ResultParameters": { - "ResultParameter": [ - {"Key": "ReceiptNo", "Value": "LGR919G2AV"}, - {"Key": "Conversation ID", "Value": "AG_20170727_00004492b1b6d0078fbe"}, - {"Key": "FinalisedTime", "Value": 20170727101415}, - {"Key": "Amount", "Value": 10}, - {"Key": "TransactionStatus", "Value": "Completed"}, - {"Key": "ReasonType", "Value": "Salary Payment via API"}, - {"Key": "TransactionReason"}, - {"Key": "DebitPartyCharges", "Value": "Fee For B2C Payment|KES|33.00"}, - {"Key": "DebitAccountType", "Value": "Utility Account"}, - {"Key": "InitiatedTime", "Value": 20170727101415}, - {"Key": "Originator Conversation ID", "Value": "19455-773836-1"}, - {"Key": "CreditPartyName", "Value": "254708374149 - John Doe"}, - {"Key": "DebitPartyName", "Value": "600134 - Safaricom157"}, - ] - }, - "ReferenceData": {"ReferenceItem": {"Key": "Occasion", "Value": "aaaa"}}, - } - - -def get_payment_request_response_payload(Amount=500): - """Response received after successfully calling the stk push process request API.""" - - CheckoutRequestID = frappe.utils.random_string(10) - - return { - "MerchantRequestID": "8071-27184008-1", - "CheckoutRequestID": CheckoutRequestID, - "ResultCode": 0, - "ResultDesc": "The service request is processed successfully.", - "CallbackMetadata": { - "Item": [ - {"Name": "Amount", "Value": Amount}, - {"Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R"}, - {"Name": "TransactionDate", "Value": 20201006113336}, - {"Name": "PhoneNumber", "Value": 254723575670}, - ] - }, - } - - -def get_payment_callback_payload( - Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R" -): - """Response received from the server as callback after calling the stkpush process request API.""" - return { - "Body": { - "stkCallback": { - "MerchantRequestID": "19465-780693-1", - "CheckoutRequestID": CheckoutRequestID, - "ResultCode": 0, - "ResultDesc": "The service request is processed successfully.", - "CallbackMetadata": { - "Item": [ - {"Name": "Amount", "Value": Amount}, - {"Name": "MpesaReceiptNumber", "Value": MpesaReceiptNumber}, - {"Name": "Balance"}, - {"Name": "TransactionDate", "Value": 20170727154800}, - {"Name": "PhoneNumber", "Value": 254721566839}, - ] - }, - } - } - } - - -def get_account_balance_callback_payload(): - """Response received from the server as callback after calling the account balance API.""" - return { - "Result": { - "ResultType": 0, - "ResultCode": 0, - "ResultDesc": "The service request is processed successfully.", - "OriginatorConversationID": "16470-170099139-1", - "ConversationID": "AG_20200927_00007cdb1f9fb6494315", - "TransactionID": "OIR0000000", - "ResultParameters": { - "ResultParameter": [ - {"Key": "AccountBalance", "Value": "Working Account|KES|481000.00|481000.00|0.00|0.00"}, - {"Key": "BOCompletedTime", "Value": 20200927234123}, - ] - }, - "ReferenceData": { - "ReferenceItem": { - "Key": "QueueTimeoutURL", - "Value": "https://internalsandbox.safaricom.co.ke/mpesa/abresults/v1/submit", - } - }, - } - } diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py index 86e1b31eba..67168536e7 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py @@ -43,40 +43,6 @@ class TestPlaidSettings(unittest.TestCase): add_account_subtype("loan") self.assertEqual(frappe.get_doc("Bank Account Subtype", "loan").name, "loan") - def test_default_bank_account(self): - if not frappe.db.exists("Bank", "Citi"): - frappe.get_doc({"doctype": "Bank", "bank_name": "Citi"}).insert() - - bank_accounts = { - "account": { - "subtype": "checking", - "mask": "0000", - "type": "depository", - "id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK", - "name": "Plaid Checking", - }, - "account_id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK", - "link_session_id": "db673d75-61aa-442a-864f-9b3f174f3725", - "accounts": [ - { - "type": "depository", - "subtype": "checking", - "mask": "0000", - "id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK", - "name": "Plaid Checking", - } - ], - "institution": {"institution_id": "ins_6", "name": "Citi"}, - } - - bank = json.dumps(frappe.get_doc("Bank", "Citi").as_dict(), default=json_handler) - company = frappe.db.get_single_value("Global Defaults", "default_company") - frappe.db.set_value("Company", company, "default_bank_account", None) - - self.assertRaises( - frappe.ValidationError, add_bank_accounts, response=bank_accounts, bank=bank, company=company - ) - def test_new_transaction(self): if not frappe.db.exists("Bank", "Citi"): frappe.get_doc({"doctype": "Bank", "bank_name": "Citi"}).insert() diff --git a/erpnext/erpnext_integrations/stripe_integration.py b/erpnext/erpnext_integrations/stripe_integration.py deleted file mode 100644 index 634e5c2e89..0000000000 --- a/erpnext/erpnext_integrations/stripe_integration.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from frappe.integrations.utils import create_request_log - -from erpnext.utilities import payment_app_import_guard - - -def create_stripe_subscription(gateway_controller, data): - with payment_app_import_guard(): - import stripe - - stripe_settings = frappe.get_doc("Stripe Settings", gateway_controller) - stripe_settings.data = frappe._dict(data) - - stripe.api_key = stripe_settings.get_password(fieldname="secret_key", raise_exception=False) - stripe.default_http_client = stripe.http_client.RequestsClient() - - try: - stripe_settings.integration_request = create_request_log(stripe_settings.data, "Host", "Stripe") - stripe_settings.payment_plans = frappe.get_doc( - "Payment Request", stripe_settings.data.reference_docname - ).subscription_plans - return create_subscription_on_stripe(stripe_settings) - - except Exception: - stripe_settings.log_error("Unable to create Stripe subscription") - return { - "redirect_to": frappe.redirect_to_message( - _("Server Error"), - _( - "It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account." - ), - ), - "status": 401, - } - - -def create_subscription_on_stripe(stripe_settings): - with payment_app_import_guard(): - import stripe - - items = [] - for payment_plan in stripe_settings.payment_plans: - plan = frappe.db.get_value("Subscription Plan", payment_plan.plan, "product_price_id") - items.append({"price": plan, "quantity": payment_plan.qty}) - - try: - customer = stripe.Customer.create( - source=stripe_settings.data.stripe_token_id, - description=stripe_settings.data.payer_name, - email=stripe_settings.data.payer_email, - ) - - subscription = stripe.Subscription.create(customer=customer, items=items) - - if subscription.status == "active": - stripe_settings.integration_request.db_set("status", "Completed", update_modified=False) - stripe_settings.flags.status_changed_to = "Completed" - - else: - stripe_settings.integration_request.db_set("status", "Failed", update_modified=False) - frappe.log_error(f"Stripe Subscription ID {subscription.id}: Payment failed") - except Exception: - stripe_settings.integration_request.db_set("status", "Failed", update_modified=False) - stripe_settings.log_error("Unable to create Stripe subscription") - - return stripe_settings.finalize_request() diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index 981486eb30..8984f1bee7 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -6,8 +6,6 @@ from urllib.parse import urlparse import frappe from frappe import _ -from erpnext import get_default_company - def validate_webhooks_request(doctype, hmac_key, secret_key="secret"): def innerfn(fn): @@ -47,35 +45,6 @@ def get_webhook_address(connector_name, method, exclude_uri=False, force_https=F return server_url -def create_mode_of_payment(gateway, payment_type="General"): - payment_gateway_account = frappe.db.get_value( - "Payment Gateway Account", {"payment_gateway": gateway}, ["payment_account"] - ) - - mode_of_payment = frappe.db.exists("Mode of Payment", gateway) - if not mode_of_payment and payment_gateway_account: - mode_of_payment = frappe.get_doc( - { - "doctype": "Mode of Payment", - "mode_of_payment": gateway, - "enabled": 1, - "type": payment_type, - "accounts": [ - { - "doctype": "Mode of Payment Account", - "company": get_default_company(), - "default_account": payment_gateway_account, - } - ], - } - ) - mode_of_payment.insert(ignore_permissions=True) - - return mode_of_payment - elif mode_of_payment: - return frappe.get_doc("Mode of Payment", mode_of_payment) - - def get_tracking_url(carrier, tracking_number): # Return the formatted Tracking URL. tracking_url = "" diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2155699a4c..5483a10b57 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -52,11 +52,7 @@ leaderboards = "erpnext.startup.leaderboard.get_leaderboards" filters_config = "erpnext.startup.filters.get_filters_config" additional_print_settings = "erpnext.controllers.print_settings.get_print_settings" -on_session_creation = [ - "erpnext.portal.utils.create_customer_or_supplier", - "erpnext.e_commerce.shopping_cart.utils.set_cart_count", -] -on_logout = "erpnext.e_commerce.shopping_cart.utils.clear_cart_count" +on_session_creation = "erpnext.portal.utils.create_customer_or_supplier" treeviews = [ "Account", @@ -90,15 +86,11 @@ jinja = { } # website -update_website_context = [ - "erpnext.e_commerce.shopping_cart.utils.update_website_context", -] -my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context" webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context" calendars = ["Task", "Work Order", "Sales Order", "Holiday List", "ToDo"] -website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner"] +website_generators = ["BOM", "Sales Partner"] website_context = { "favicon": "/assets/erpnext/images/erpnext-favicon.svg", @@ -349,9 +341,6 @@ doc_events = { "Event": { "after_insert": "erpnext.crm.utils.link_events_with_prospect", }, - "Sales Taxes and Charges Template": { - "on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings" - }, "Sales Invoice": { "on_submit": [ "erpnext.regional.create_transaction_log", @@ -519,6 +508,7 @@ accounting_dimension_doctypes = [ "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", + "Sales Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 0d0fd5e270..4a0041662b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -228,7 +228,7 @@ }, { "default": "0", - "description": "If enabled, the system won't create material requests for the available items.", + "description": "If enabled, the system will create material requests even if the stock exists in the 'Raw Materials Warehouse'.", "fieldname": "ignore_existing_ordered_qty", "fieldtype": "Check", "label": "Ignore Available Stock" @@ -422,7 +422,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-07-28 13:37:43.926686", + "modified": "2023-09-29 11:41:03.246059", "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 e88b791401..ddd9375211 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1509,6 +1509,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d def get_materials_from_other_locations(item, warehouses, new_mr_items, company): from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations + stock_uom, purchase_uom = frappe.db.get_value( + "Item", item.get("item_code"), ["stock_uom", "purchase_uom"] + ) + locations = get_available_item_locations( item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True ) @@ -1519,6 +1523,10 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): if required_qty <= 0: return + conversion_factor = 1.0 + if purchase_uom != stock_uom and purchase_uom == item["uom"]: + conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"]) + new_dict = copy.deepcopy(item) quantity = required_qty if d.get("qty") > required_qty else d.get("qty") @@ -1531,25 +1539,14 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): } ) - required_qty -= quantity + required_qty -= quantity / conversion_factor new_mr_items.append(new_dict) # raise purchase request for remaining qty - if required_qty: - stock_uom, purchase_uom = frappe.db.get_value( - "Item", item["item_code"], ["stock_uom", "purchase_uom"] - ) - if purchase_uom != stock_uom and purchase_uom == item["uom"]: - conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"]) - if not (conversion_factor or frappe.flags.show_qty_in_stock_uom): - frappe.throw( - _("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format( - purchase_uom, stock_uom, item["item_code"] - ) - ) - - required_qty = required_qty / conversion_factor + precision = frappe.get_precision("Material Request Plan Item", "quantity") + if flt(required_qty, precision) > 0: + required_qty = required_qty if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): required_qty = ceil(required_qty) @@ -1620,18 +1617,25 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Material Request Plan Item") + non_completed_production_plans = get_non_completed_production_plans() + query = ( frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0))) + .select(Sum(child.required_bom_qty)) .where( (table.docstatus == 1) & (child.item_code == item_code) & (child.warehouse == warehouse) & (table.status.notin(["Completed", "Closed"])) ) - ).run() + ) + + if non_completed_production_plans: + query = query.where(table.name.isin(non_completed_production_plans)) + + query = query.run() if not query: return 0.0 @@ -1639,7 +1643,9 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): reserved_qty_for_production_plan = flt(query[0][0]) reserved_qty_for_production = flt( - get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True) + get_reserved_qty_for_production( + item_code, warehouse, non_completed_production_plans, check_production_plan=True + ) ) if reserved_qty_for_production > reserved_qty_for_production_plan: @@ -1648,6 +1654,25 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): return reserved_qty_for_production_plan - reserved_qty_for_production +def get_non_completed_production_plans(): + table = frappe.qb.DocType("Production Plan") + child = frappe.qb.DocType("Production Plan Item") + + query = ( + frappe.qb.from_(table) + .inner_join(child) + .on(table.name == child.parent) + .select(table.name) + .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])) + + def get_raw_materials_of_sub_assembly_items( item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1 ): diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2348d2b688..6ab9232788 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -7,6 +7,7 @@ from frappe.utils import add_to_date, flt, getdate, now_datetime, nowdate from erpnext.controllers.item_variant import create_variant from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, + get_non_completed_production_plans, get_sales_orders, get_warehouse_list, ) @@ -1103,6 +1104,49 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(after_qty, before_qty) + def test_resered_qty_for_production_plan_for_less_rm_qty(self): + from erpnext.stock.utils import get_or_make_bin + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln = create_production_plan(item_code="Test Production Item 1", planned_qty=10) + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty - before_qty, 10) + + pln.make_work_order() + + plans = [] + for row in frappe.get_all("Work Order", filters={"production_plan": pln.name}, fields=["name"]): + wo_doc = frappe.get_doc("Work Order", row.name) + wo_doc.source_warehouse = "_Test Warehouse - _TC" + wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC" + wo_doc.fg_warehouse = "_Test Warehouse - _TC" + for d in wo_doc.required_items: + d.source_warehouse = "_Test Warehouse - _TC" + d.required_qty -= 5 + make_stock_entry( + item_code=d.item_code, + qty=d.required_qty, + rate=100, + target="_Test Warehouse - _TC", + ) + + wo_doc.submit() + plans.append(pln.name) + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty, before_qty) + + completed_plans = get_non_completed_production_plans() + for plan in plans: + self.assertFalse(plan in completed_plans) + def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self): from erpnext.stock.utils import get_or_make_bin @@ -1230,6 +1274,64 @@ class TestProductionPlan(FrappeTestCase): if row.item_code == "SubAssembly2 For SUB Test": self.assertEqual(row.quantity, 10) + 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 + + bom_tree = { + "Test FG Item INK PEN": { + "Test RM Item INK": {}, + } + } + + parent_bom = create_nested_bom(bom_tree, prefix="") + if not frappe.db.exists("UOM Conversion Detail", {"parent": "Test RM Item INK", "uom": "Kg"}): + doc = frappe.get_doc("Item", "Test RM Item INK") + doc.purchase_uom = "Kg" + doc.append("uoms", {"uom": "Kg", "conversion_factor": 0.5}) + doc.save() + + wh1 = create_warehouse("PNE Warehouse", company="_Test Company") + wh2 = create_warehouse("MBE Warehouse", company="_Test Company") + mrp_warhouse = create_warehouse("MRPBE Warehouse", company="_Test Company") + + make_stock_entry( + item_code="Test RM Item INK", + qty=2, + rate=100, + target=wh1, + ) + + make_stock_entry( + item_code="Test RM Item INK", + qty=2, + rate=100, + target=wh2, + ) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=10, + do_not_submit=1, + warehouse="_Test Warehouse - _TC", + ) + + plan.for_warehouse = mrp_warhouse + + items = get_items_for_material_requests( + plan.as_dict(), warehouses=[{"warehouse": wh1}, {"warehouse": wh2}] + ) + + for row in items: + row = frappe._dict(row) + if row.material_request_type == "Material Transfer": + self.assertTrue(row.from_warehouse in [wh1, wh2]) + self.assertEqual(row.quantity, 2) + + if row.material_request_type == "Purchase": + self.assertTrue(row.warehouse == mrp_warhouse) + self.assertEqual(row.quantity, 12) + def create_production_plan(**args): """ diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index d8fc220386..f9fddcbb5e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -358,10 +358,10 @@ class WorkOrder(Document): else: self.update_work_order_qty_in_so() + self.update_ordered_qty() self.update_reserved_qty_for_production() self.update_completed_qty_in_material_request() self.update_planned_qty() - self.update_ordered_qty() self.create_job_card() def on_cancel(self): @@ -1513,7 +1513,10 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): def get_reserved_qty_for_production( - item_code: str, warehouse: str, check_production_plan: bool = False + item_code: str, + warehouse: str, + non_completed_production_plans: list = None, + check_production_plan: bool = False, ) -> float: """Get total reserved quantity for any item in specified warehouse""" wo = frappe.qb.DocType("Work Order") @@ -1535,16 +1538,22 @@ def get_reserved_qty_for_production( & (wo_item.parent == wo.name) & (wo.docstatus == 1) & (wo_item.source_warehouse == warehouse) - & (wo.status.notin(["Stopped", "Completed", "Closed"])) - & ( - (wo_item.required_qty > wo_item.transferred_qty) - | (wo_item.required_qty > wo_item.consumed_qty) - ) ) ) if check_production_plan: query = query.where(wo.production_plan.isnotnull()) + else: + query = query.where( + (wo.status.notin(["Stopped", "Completed", "Closed"])) + & ( + (wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty) + ) + ) + + if non_completed_production_plans: + query = query.where(wo.production_plan.isin(non_completed_production_plans)) return query.run()[0][0] or 0.0 diff --git a/erpnext/modules.txt b/erpnext/modules.txt index dcb421298d..c53cdf467d 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -17,5 +17,4 @@ Quality Management Communication Telephony Bulk Transaction -E-commerce -Subcontracting \ No newline at end of file +Subcontracting diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e9c056e3a9..d59fe0ec4c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -223,9 +223,6 @@ execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Catego erpnext.patches.v13_0.set_operation_time_based_on_operating_cost erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 erpnext.patches.v13_0.fix_invoice_statuses -erpnext.patches.v13_0.create_website_items #30-09-2021 -erpnext.patches.v13_0.populate_e_commerce_settings -erpnext.patches.v13_0.make_homepage_products_website_items erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item erpnext.patches.v13_0.update_dates_in_tax_withholding_category erpnext.patches.v14_0.update_opportunity_currency_fields @@ -242,7 +239,6 @@ erpnext.patches.v12_0.update_production_plan_status erpnext.patches.v13_0.healthcare_deprecation_warning erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v13_0.update_category_in_ltds_certificate -erpnext.patches.v13_0.fetch_thumbnail_in_website_items erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v14_0.migrate_crm_settings erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty @@ -257,6 +253,7 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair erpnext.patches.v15_0.delete_taxjar_doctypes +erpnext.patches.v15_0.delete_ecommerce_doctypes erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets erpnext.patches.v14_0.update_reference_due_date_in_journal_entry erpnext.patches.v15_0.saudi_depreciation_warning @@ -277,8 +274,6 @@ erpnext.patches.v14_0.delete_datev_doctypes erpnext.patches.v14_0.rearrange_company_fields erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v14_0.migrate_cost_center_allocations -erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template -erpnext.patches.v13_0.shopping_cart_to_ecommerce erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v14_0.delete_amazon_mws_doctype @@ -288,7 +283,6 @@ erpnext.patches.v14_0.update_batch_valuation_flag erpnext.patches.v14_0.delete_non_profit_doctypes erpnext.patches.v13_0.set_return_against_in_pos_invoice_references erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 -erpnext.patches.v13_0.copy_custom_field_filters_to_website_item erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype erpnext.patches.v13_0.requeue_recoverable_reposts erpnext.patches.v14_0.discount_accounting_separation @@ -344,5 +338,7 @@ erpnext.patches.v15_0.delete_woocommerce_settings_doctype erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults erpnext.patches.v14_0.update_invoicing_period_in_subscription execute:frappe.delete_doc("Page", "welcome-to-erpnext") +erpnext.patches.v15_0.delete_payment_gateway_doctypes +erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item # below migration patch should always run last -erpnext.patches.v14_0.migrate_gl_to_payment_ledger +erpnext.patches.v14_0.migrate_gl_to_payment_ledger \ No newline at end of file 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 efbb96c100..e53bdf8f19 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 @@ -3,23 +3,24 @@ import frappe def execute(): frappe.reload_doc("stock", "doctype", "quality_inspection_parameter") + params = set() - # get all distinct parameters from QI readigs table - reading_params = frappe.db.get_all( - "Quality Inspection Reading", fields=["distinct specification"] - ) - reading_params = [d.specification for d in reading_params] + # get all parameters from QI readings table + for (p,) in frappe.db.get_all( + "Quality Inspection Reading", fields=["specification"], as_list=True + ): + params.add(p.strip()) - # get all distinct parameters from QI Template as some may be unused in QI - template_params = frappe.db.get_all( - "Item Quality Inspection Parameter", fields=["distinct specification"] - ) - template_params = [d.specification for d in template_params] + # get all parameters from QI Template as some may be unused in QI + for (p,) in frappe.db.get_all( + "Item Quality Inspection Parameter", fields=["specification"], as_list=True + ): + params.add(p.strip()) - params = list(set(reading_params + template_params)) + # because db primary keys are case insensitive, so duplicates will cause an exception + params = set({x.casefold(): x for x in params}.values()) for parameter in params: - if not frappe.db.exists("Quality Inspection Parameter", parameter): - frappe.get_doc( - {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} - ).insert(ignore_permissions=True) + frappe.get_doc( + {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} + ).insert(ignore_permissions=True) 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 deleted file mode 100644 index 1bac0fdbf0..0000000000 --- a/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py +++ /dev/null @@ -1,60 +0,0 @@ -import json -from typing import List, Union - -import frappe - -from erpnext.e_commerce.doctype.website_item.website_item import make_website_item - - -def execute(): - """ - Convert all Item links to Website Item link values in - exisitng 'Item Card Group' Web Page Block data. - """ - frappe.reload_doc("e_commerce", "web_template", "item_card_group") - - blocks = frappe.db.get_all( - "Web Page Block", - filters={"web_template": "Item Card Group"}, - fields=["parent", "web_template_values", "name"], - ) - - fields = generate_fields_to_edit() - - for block in blocks: - web_template_value = json.loads(block.get("web_template_values")) - - for field in fields: - item = web_template_value.get(field) - if not item: - continue - - if frappe.db.exists("Website Item", {"item_code": item}): - website_item = frappe.db.get_value("Website Item", {"item_code": item}) - else: - website_item = make_new_website_item(item) - - if website_item: - web_template_value[field] = website_item - - frappe.db.set_value( - "Web Page Block", block.name, "web_template_values", json.dumps(web_template_value) - ) - - -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. - - return fields - - -def make_new_website_item(item: str) -> Union[str, None]: - try: - doc = frappe.get_doc("Item", item) - web_item = make_website_item(doc) # returns [website_item.name, item_name] - return web_item[0] - except Exception: - doc.log_error("Website Item creation failed") - return None 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 deleted file mode 100644 index 4ad572fdb0..0000000000 --- a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py +++ /dev/null @@ -1,94 +0,0 @@ -import frappe -from frappe.custom.doctype.custom_field.custom_field import create_custom_field - - -def execute(): - "Add Field Filters, that are not standard fields in Website Item, as Custom Fields." - - def move_table_multiselect_data(docfield): - "Copy child table data (Table Multiselect) from Item to Website Item for a docfield." - table_multiselect_data = get_table_multiselect_data(docfield) - field = docfield.fieldname - - for row in table_multiselect_data: - # add copied multiselect data rows in Website Item - web_item = frappe.db.get_value("Website Item", {"item_code": row.parent}) - web_item_doc = frappe.get_doc("Website Item", web_item) - - child_doc = frappe.new_doc(docfield.options, parent_doc=web_item_doc, parentfield=field) - - for field in ["name", "creation", "modified", "idx"]: - row[field] = None - - child_doc.update(row) - - child_doc.parenttype = "Website Item" - child_doc.parent = web_item - - child_doc.insert() - - def get_table_multiselect_data(docfield): - child_table = frappe.qb.DocType(docfield.options) - item = frappe.qb.DocType("Item") - - table_multiselect_data = ( # query table data for field - frappe.qb.from_(child_table) - .join(item) - .on(item.item_code == child_table.parent) - .select(child_table.star) - .where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1)) - ).run(as_dict=True) - - return table_multiselect_data - - settings = frappe.get_doc("E Commerce Settings") - - if not (settings.enable_field_filters or settings.filter_fields): - return - - item_meta = frappe.get_meta("Item") - valid_item_fields = [ - df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] - ] - - web_item_meta = frappe.get_meta("Website Item") - valid_web_item_fields = [ - df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] - ] - - for row in settings.filter_fields: - # skip if illegal field - if row.fieldname not in valid_item_fields: - continue - - # if Item field is not in Website Item, add it as a custom field - if row.fieldname not in valid_web_item_fields: - df = item_meta.get_field(row.fieldname) - create_custom_field( - "Website Item", - dict( - owner="Administrator", - fieldname=df.fieldname, - label=df.label, - fieldtype=df.fieldtype, - options=df.options, - description=df.description, - read_only=df.read_only, - no_copy=df.no_copy, - insert_after="on_backorder", - ), - ) - - # map field values - if df.fieldtype == "Table MultiSelect": - move_table_multiselect_data(df) - else: - frappe.db.sql( # nosemgrep - """ - UPDATE `tabWebsite Item` wi, `tabItem` i - SET wi.{0} = i.{0} - WHERE wi.item_code = i.item_code - """.format( - row.fieldname - ) - ) diff --git a/erpnext/patches/v13_0/create_website_items.py b/erpnext/patches/v13_0/create_website_items.py deleted file mode 100644 index b010f0ecc6..0000000000 --- a/erpnext/patches/v13_0/create_website_items.py +++ /dev/null @@ -1,85 +0,0 @@ -import frappe - -from erpnext.e_commerce.doctype.website_item.website_item import make_website_item - - -def execute(): - frappe.reload_doc("e_commerce", "doctype", "website_item") - frappe.reload_doc("e_commerce", "doctype", "website_item_tabbed_section") - frappe.reload_doc("e_commerce", "doctype", "website_offer") - frappe.reload_doc("e_commerce", "doctype", "recommended_items") - frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings") - frappe.reload_doc("stock", "doctype", "item") - - item_fields = [ - "item_code", - "item_name", - "item_group", - "stock_uom", - "brand", - "has_variants", - "variant_of", - "description", - "weightage", - ] - web_fields_to_map = [ - "route", - "slideshow", - "website_image_alt", - "website_warehouse", - "web_long_description", - "website_content", - "website_image", - "thumbnail", - ] - - # get all valid columns (fields) from Item master DB schema - item_table_fields = frappe.db.sql("desc `tabItem`", as_dict=1) # nosemgrep - item_table_fields = [d.get("Field") for d in item_table_fields] - - # prepare fields to query from Item, check if the web field exists in Item master - web_query_fields = [] - for web_field in web_fields_to_map: - if web_field in item_table_fields: - web_query_fields.append(web_field) - item_fields.append(web_field) - - # check if the filter fields exist in Item master - or_filters = {} - for field in ["show_in_website", "show_variant_in_website"]: - if field in item_table_fields: - or_filters[field] = 1 - - if not web_query_fields or not or_filters: - # web fields to map are not present in Item master schema - # most likely a fresh installation that doesnt need this patch - return - - items = frappe.db.get_all("Item", fields=item_fields, or_filters=or_filters) - total_count = len(items) - - for count, item in enumerate(items, start=1): - if frappe.db.exists("Website Item", {"item_code": item.item_code}): - continue - - # make new website item from item (publish item) - website_item = make_website_item(item, save=False) - website_item.ranking = item.get("weightage") - - for field in web_fields_to_map: - website_item.update({field: item.get(field)}) - - website_item.save() - - # move Website Item Group & Website Specification table to Website Item - for doctype in ("Website Item Group", "Item Website Specification"): - frappe.db.set_value( - doctype, - {"parenttype": "Item", "parent": item.item_code}, # filters - {"parenttype": "Website Item", "parent": website_item.name}, # value dict - ) - - if count % 20 == 0: # commit after every 20 items - frappe.db.commit() - - frappe.utils.update_progress_bar("Creating Website Items", count, total_count) diff --git a/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py b/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py deleted file mode 100644 index 9197d86058..0000000000 --- a/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py +++ /dev/null @@ -1,11 +0,0 @@ -import frappe - - -def execute(): - if frappe.db.has_column("Item", "thumbnail"): - website_item = frappe.qb.DocType("Website Item").as_("wi") - item = frappe.qb.DocType("Item") - - frappe.qb.update(website_item).inner_join(item).on(website_item.item_code == item.item_code).set( - website_item.thumbnail, item.thumbnail - ).where(website_item.website_image.notnull() & website_item.thumbnail.isnull()).run() diff --git a/erpnext/patches/v13_0/make_homepage_products_website_items.py b/erpnext/patches/v13_0/make_homepage_products_website_items.py deleted file mode 100644 index 50bfd358ea..0000000000 --- a/erpnext/patches/v13_0/make_homepage_products_website_items.py +++ /dev/null @@ -1,15 +0,0 @@ -import frappe - - -def execute(): - homepage = frappe.get_doc("Homepage") - - for row in homepage.products: - web_item = frappe.db.get_value("Website Item", {"item_code": row.item_code}, "name") - if not web_item: - continue - - row.item_code = web_item - - homepage.flags.ignore_mandatory = True - homepage.save() diff --git a/erpnext/patches/v13_0/populate_e_commerce_settings.py b/erpnext/patches/v13_0/populate_e_commerce_settings.py deleted file mode 100644 index ecf512b011..0000000000 --- a/erpnext/patches/v13_0/populate_e_commerce_settings.py +++ /dev/null @@ -1,68 +0,0 @@ -import frappe -from frappe.utils import cint - - -def execute(): - frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings") - frappe.reload_doc("portal", "doctype", "website_filter_field") - frappe.reload_doc("portal", "doctype", "website_attribute") - - products_settings_fields = [ - "hide_variants", - "products_per_page", - "enable_attribute_filters", - "enable_field_filters", - ] - - shopping_cart_settings_fields = [ - "enabled", - "show_attachments", - "show_price", - "show_stock_availability", - "enable_variants", - "show_contact_us_button", - "show_quantity_in_website", - "show_apply_coupon_code_in_website", - "allow_items_not_in_stock", - "company", - "price_list", - "default_customer_group", - "quotation_series", - "enable_checkout", - "payment_success_url", - "payment_gateway_account", - "save_quotations_as_draft", - ] - - settings = frappe.get_doc("E Commerce Settings") - - def map_into_e_commerce_settings(doctype, fields): - singles = frappe.qb.DocType("Singles") - query = ( - frappe.qb.from_(singles) - .select(singles["field"], singles.value) - .where((singles.doctype == doctype) & (singles["field"].isin(fields))) - ) - data = query.run(as_dict=True) - - # {'enable_attribute_filters': '1', ...} - mapper = {row.field: row.value for row in data} - - for key, value in mapper.items(): - value = cint(value) if (value and value.isdigit()) else value - settings.update({key: value}) - - settings.save() - - # shift data to E Commerce Settings - map_into_e_commerce_settings("Products Settings", products_settings_fields) - map_into_e_commerce_settings("Shopping Cart Settings", shopping_cart_settings_fields) - - # move filters and attributes tables to E Commerce Settings from Products Settings - for doctype in ("Website Filter Field", "Website Attribute"): - frappe.db.set_value( - doctype, - {"parent": "Products Settings"}, - {"parenttype": "E Commerce Settings", "parent": "E Commerce Settings"}, - update_modified=False, - ) diff --git a/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py b/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py deleted file mode 100644 index 35710a9bb4..0000000000 --- a/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py +++ /dev/null @@ -1,29 +0,0 @@ -import click -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) - - if frappe.db.get_single_value("E Commerce Settings", "enabled"): - notify_users() - - -def notify_users(): - - click.secho( - "Shopping cart and Product settings are merged into E-commerce settings.\n" - "Checkout the documentation to learn more:" - "https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce", - fg="yellow", - ) - - note = frappe.new_doc("Note") - note.title = "New E-Commerce Module" - note.public = 1 - note.notify_on_login = 1 - note.content = """You are seeing this message because Shopping Cart is enabled on your site.
Shopping Cart Settings and Products settings are now merged into "E Commerce Settings".
You can learn about new and improved E-Commerce features in the official documentation.
- ${__(review.review_title)} -
- -- ${__(review.comment)} -
-
- - {{ _(offer.offer_title) }}: - {{ _(offer.offer_subtitle) if offer.offer_subtitle else '' }} - - {{ _("More") }} - -
-- - {{ _(doc.item_group) }} - - - {{ _("Item Code") }}: - - {{ doc.item_code }} -
- {% if has_variants %} - - {% include "templates/generators/item/item_configure.html" %} - {% else %} - - {% include "templates/generators/item/item_add_to_cart.html" %} - {% endif %} - -| {{ d.label }} | -{{ d.description }} | -
{{ _("Cart is Empty") }}
- {% endif %} -| {{ _("Net Total (") + total_items + _(" Items)") }} | -{{ doc.get_formatted("net_total") }} | -
| - {{ d.description }} - | -- {{ d.get_formatted("base_tax_amount") }} - | -
| {{ _("Grand Total") }} | -{{ doc.get_formatted("grand_total") }} | -
{{ d.description }}
+{{ d.description }}
{% set supplier_part_no = frappe.db.get_value("Item Supplier", {'parent': d.item_code, 'supplier': doc.supplier}, "supplier_part_no") %}{% if supplier_part_no %} {{_("Supplier Part No") + ": "+ supplier_part_no}} {% endif %}
-| {{ _('Item') }} | -{{ _('Quantity') }} | - {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %} -{{ _('Subtotal') }} | - {% endif %} -- |
|---|
- ' + text + '
%(description)s
') + - '%(formatted_tax_amount)s
\ -- {{ _("Loading Payment System") }} -
- -{% endblock %} diff --git a/erpnext/templates/pages/integrations/gocardless_checkout.py b/erpnext/templates/pages/integrations/gocardless_checkout.py deleted file mode 100644 index 655be52c55..0000000000 --- a/erpnext/templates/pages/integrations/gocardless_checkout.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import json - -import frappe -from frappe import _ -from frappe.utils import flt, get_url - -from erpnext.erpnext_integrations.doctype.gocardless_settings.gocardless_settings import ( - get_gateway_controller, - gocardless_initialization, -) - -no_cache = 1 - -expected_keys = ( - "amount", - "title", - "description", - "reference_doctype", - "reference_docname", - "payer_name", - "payer_email", - "order_id", - "currency", -) - - -def get_context(context): - context.no_cache = 1 - - # all these keys exist in form_dict - if not (set(expected_keys) - set(frappe.form_dict.keys())): - for key in expected_keys: - context[key] = frappe.form_dict[key] - - context["amount"] = flt(context["amount"]) - - gateway_controller = get_gateway_controller(context.reference_docname) - context["header_img"] = frappe.db.get_value( - "GoCardless Settings", gateway_controller, "header_img" - ) - - else: - frappe.redirect_to_message( - _("Some information is missing"), - _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), - ) - frappe.local.flags.redirect_location = frappe.local.response.location - raise frappe.Redirect - - -@frappe.whitelist(allow_guest=True) -def check_mandate(data, reference_doctype, reference_docname): - data = json.loads(data) - - client = gocardless_initialization(reference_docname) - - payer = frappe.get_doc("Customer", data["payer_name"]) - - if payer.customer_type == "Individual" and payer.customer_primary_contact is not None: - primary_contact = frappe.get_doc("Contact", payer.customer_primary_contact) - prefilled_customer = { - "company_name": payer.name, - "given_name": primary_contact.first_name, - } - if primary_contact.last_name is not None: - prefilled_customer.update({"family_name": primary_contact.last_name}) - - if primary_contact.email_id is not None: - prefilled_customer.update({"email": primary_contact.email_id}) - else: - prefilled_customer.update({"email": frappe.session.user}) - - else: - prefilled_customer = {"company_name": payer.name, "email": frappe.session.user} - - success_url = get_url( - "./integrations/gocardless_confirmation?reference_doctype=" - + reference_doctype - + "&reference_docname=" - + reference_docname - ) - - try: - redirect_flow = client.redirect_flows.create( - params={ - "description": _("Pay {0} {1}").format(data["amount"], data["currency"]), - "session_token": frappe.session.user, - "success_redirect_url": success_url, - "prefilled_customer": prefilled_customer, - } - ) - - return {"redirect_to": redirect_flow.redirect_url} - - except Exception as e: - frappe.log_error("GoCardless Payment Error") - return {"redirect_to": "/integrations/payment-failed"} diff --git a/erpnext/templates/pages/integrations/gocardless_confirmation.html b/erpnext/templates/pages/integrations/gocardless_confirmation.html deleted file mode 100644 index d961c6344a..0000000000 --- a/erpnext/templates/pages/integrations/gocardless_confirmation.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "templates/web.html" %} - -{% block title %} Payment {% endblock %} - -{%- block header -%}{% endblock %} - -{% block script %} - -{% endblock %} - -{%- block page_content -%} -- {{ _("Payment Confirmation") }} -
- -{% endblock %} diff --git a/erpnext/templates/pages/integrations/gocardless_confirmation.py b/erpnext/templates/pages/integrations/gocardless_confirmation.py deleted file mode 100644 index 559aa4806d..0000000000 --- a/erpnext/templates/pages/integrations/gocardless_confirmation.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from frappe import _ - -from erpnext.erpnext_integrations.doctype.gocardless_settings.gocardless_settings import ( - get_gateway_controller, - gocardless_initialization, -) - -no_cache = 1 - -expected_keys = ("redirect_flow_id", "reference_doctype", "reference_docname") - - -def get_context(context): - context.no_cache = 1 - - # all these keys exist in form_dict - if not (set(expected_keys) - set(frappe.form_dict.keys())): - for key in expected_keys: - context[key] = frappe.form_dict[key] - - else: - frappe.redirect_to_message( - _("Some information is missing"), - _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), - ) - frappe.local.flags.redirect_location = frappe.local.response.location - raise frappe.Redirect - - -@frappe.whitelist(allow_guest=True) -def confirm_payment(redirect_flow_id, reference_doctype, reference_docname): - - client = gocardless_initialization(reference_docname) - - try: - redirect_flow = client.redirect_flows.complete( - redirect_flow_id, params={"session_token": frappe.session.user} - ) - - confirmation_url = redirect_flow.confirmation_url - gocardless_success_page = frappe.get_hooks("gocardless_success_page") - if gocardless_success_page: - confirmation_url = frappe.get_attr(gocardless_success_page[-1])( - reference_doctype, reference_docname - ) - - data = { - "mandate": redirect_flow.links.mandate, - "customer": redirect_flow.links.customer, - "redirect_to": confirmation_url, - "redirect_message": "Mandate successfully created", - "reference_doctype": reference_doctype, - "reference_docname": reference_docname, - } - - try: - create_mandate(data) - except Exception as e: - frappe.log_error("GoCardless Mandate Registration Error") - - gateway_controller = get_gateway_controller(reference_docname) - frappe.get_doc("GoCardless Settings", gateway_controller).create_payment_request(data) - - return {"redirect_to": confirmation_url} - - except Exception as e: - frappe.log_error("GoCardless Payment Error") - return {"redirect_to": "/integrations/payment-failed"} - - -def create_mandate(data): - data = frappe._dict(data) - frappe.logger().debug(data) - - mandate = data.get("mandate") - - if frappe.db.exists("GoCardless Mandate", mandate): - return - - else: - reference_doc = frappe.db.get_value( - data.get("reference_doctype"), - data.get("reference_docname"), - ["reference_doctype", "reference_name"], - as_dict=1, - ) - erpnext_customer = frappe.db.get_value( - reference_doc.reference_doctype, reference_doc.reference_name, ["customer_name"], as_dict=1 - ) - - try: - frappe.get_doc( - { - "doctype": "GoCardless Mandate", - "mandate": mandate, - "customer": erpnext_customer.customer_name, - "gocardless_customer": data.get("customer"), - } - ).insert(ignore_permissions=True) - - except Exception: - frappe.log_error("Gocardless: Unable to create mandate") diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index bc34ad5ac5..97bf48727c 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -1,5 +1,5 @@ {% extends "templates/web.html" %} -{% from "erpnext/templates/includes/order/order_macros.html" import item_name_and_description %} +{% from "erpnext/templates/includes/macros.html" import product_image %} {% block breadcrumbs %} {% include "templates/includes/breadcrumbs.html" %} @@ -34,18 +34,6 @@Available Points: {{ - available_loyalty_points }}
-
-