From 3ead28906cb849d50b53e2402704121acdfb5b9d Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 22 Aug 2023 14:41:07 +0000 Subject: [PATCH 001/135] feat: item(row) wise tax amount rounding --- .../doctype/accounts_settings/accounts_settings.json | 10 +++++----- .../accounts/doctype/payment_entry/payment_entry.py | 5 +++++ erpnext/controllers/taxes_and_totals.py | 5 +++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 6857ba343e..570be095ab 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", @@ -58,7 +59,6 @@ "closing_settings_tab", "period_closing_settings_section", "acc_frozen_upto", - "ignore_account_closing_balance", "column_break_25", "frozen_accounts_modifier", "tab_break_dpet", @@ -410,10 +410,10 @@ }, { "default": "0", - "description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ", - "fieldname": "ignore_account_closing_balance", + "description": "Tax Amount will be rounded on a row(items) level", + "fieldname": "round_row_wise_tax", "fieldtype": "Check", - "label": "Ignore Account Closing Balance" + "label": "Round Tax Amount Row-wise" } ], "icon": "icon-cog", @@ -421,7 +421,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-07-27 15:05:34.000264", + "modified": "2023-08-22 20:11:00.042840", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ac31e8a1db..39ee497fda 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1387,6 +1387,9 @@ class PaymentEntry(AccountsController): def calculate_taxes(self): self.total_taxes_and_charges = 0.0 self.base_total_taxes_and_charges = 0.0 + frappe.flags.round_row_wise_tax = ( + frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax") + ) actual_tax_dict = dict( [ @@ -1398,6 +1401,8 @@ class PaymentEntry(AccountsController): for i, tax in enumerate(self.get("taxes")): current_tax_amount = self.get_current_tax_amount(tax) + if frappe.flags.round_row_wise_tax: + current_tax_amount = flt(current_tax_amount, tax.precision("tax_amount")) if tax.charge_type == "Actual": actual_tax_dict[tax.idx] -= current_tax_amount diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 62d4c53868..1cf1788f43 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") @@ -368,6 +371,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": From c20258d2a355388b799b6e9eca710d9254cd0388 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 23 Aug 2023 03:59:08 +0000 Subject: [PATCH 002/135] fix: tax calc changes in js --- .../accounts/doctype/payment_entry/payment_entry.py | 5 ----- erpnext/public/js/controllers/taxes_and_totals.js | 10 +++++++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 39ee497fda..ac31e8a1db 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1387,9 +1387,6 @@ class PaymentEntry(AccountsController): def calculate_taxes(self): self.total_taxes_and_charges = 0.0 self.base_total_taxes_and_charges = 0.0 - frappe.flags.round_row_wise_tax = ( - frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax") - ) actual_tax_dict = dict( [ @@ -1401,8 +1398,6 @@ class PaymentEntry(AccountsController): for i, tax in enumerate(self.get("taxes")): current_tax_amount = self.get_current_tax_amount(tax) - if frappe.flags.round_row_wise_tax: - current_tax_amount = flt(current_tax_amount, tax.precision("tax_amount")) if tax.charge_type == "Actual": actual_tax_dict[tax.idx] -= current_tax_amount diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index eeb09cb8b0..8062ce05cd 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -185,7 +185,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { frappe.flags.round_off_applicable_accounts = []; if (me.frm.doc.company) { - return frappe.call({ + frappe.call({ "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", "args": { "company": me.frm.doc.company, @@ -198,6 +198,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } }); } + + frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax") + .then((round_row_wise_tax) => { + frappe.flags.round_row_wise_tax = round_row_wise_tax; + }) } determine_exclusive_rate() { @@ -338,6 +343,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { $.each(me.frm.doc["taxes"] || [], function(i, tax) { // tax_amount represents the amount of tax for the current step var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map); + if (frappe.flags.round_row_wise_tax) { + current_tax_amount = flt(current_tax_amount, precision("tax_amount", tax)); + } // Adjust divisional loss to the last item if (tax.charge_type == "Actual") { From dfb5b88abbd3d00f625f18afeb3e0e57d1fcade6 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 23 Aug 2023 04:01:00 +0000 Subject: [PATCH 003/135] chore: linters --- erpnext/controllers/taxes_and_totals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 1cf1788f43..243b5eb96e 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -25,8 +25,8 @@ 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") + 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") From 0ebcc2cf2c7c3a7c764bdf378822e542ade73253 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 23 Aug 2023 04:51:09 +0000 Subject: [PATCH 004/135] fix: round item_wise_tax_detail in taxes --- erpnext/controllers/taxes_and_totals.py | 9 +++++++-- erpnext/public/js/controllers/taxes_and_totals.js | 11 +++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 243b5eb96e..39d2cf632a 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -483,8 +483,13 @@ 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")) + 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)] diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8062ce05cd..81dcc06471 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -480,8 +480,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } let item_wise_tax_amount = current_tax_amount * this.frm.doc.conversion_rate; - if (tax_detail && tax_detail[key]) - item_wise_tax_amount += tax_detail[key][1]; + if (frappe.flags.round_row_wise_tax) { + item_wise_tax_amount = flt(item_wise_tax_amount, precision("tax_amount", tax)); + if (tax_detail && tax_detail[key]) { + item_wise_tax_amount += flt(tax_detail[key][1], precision("tax_amount", tax)); + } + } else { + if (tax_detail && tax_detail[key]) + item_wise_tax_amount += tax_detail[key][1]; + } tax_detail[key] = [tax_rate, flt(item_wise_tax_amount, precision("base_tax_amount", tax))]; } From 9e1b2c9f5799ca5b84bf63f34b64a72d15b99097 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Thu, 24 Aug 2023 05:02:14 +0000 Subject: [PATCH 005/135] fix: item wise split up rounding --- erpnext/controllers/taxes_and_totals.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 39d2cf632a..c1dc316c54 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -487,11 +487,15 @@ class calculate_taxes_and_totals(object): 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: From 89ddf3272e0ce20cb177688d738703e0f0386a1c Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Thu, 24 Aug 2023 05:56:56 +0000 Subject: [PATCH 006/135] fix(regional): item wise tax calc issue --- erpnext/accounts/utils.py | 6 +-- .../regional/united_arab_emirates/utils.py | 38 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9d6d0f91fb..1aefeaacf7 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -908,9 +908,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering - limit=None, # passed by reconciliation tool - voucher_no=None, # filter passed by reconciliation tool + vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering + limit=None, # passed by reconciliation tool + voucher_no=None, # filter passed by reconciliation tool ): ple = qb.DocType("Payment Ledger Entry") diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index a910af6a1d..efeaeed324 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -7,32 +7,32 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax def update_itemised_tax_data(doc): + # maybe this should be a standard function rather than a regional one if not doc.taxes: return + if not doc.items: + return + + meta = frappe.get_meta(doc.items[0].doctype) + if not meta.has_field("tax_rate"): + return + itemised_tax = get_itemised_tax(doc.taxes) for row in doc.items: - tax_rate = 0.0 - item_tax_rate = 0.0 + tax_rate, tax_amount = 0.0, 0.0 + # dont even bother checking in item tax template as it contains both input and output accounts - double the tax rate + item_code = row.item_code or row.item_name + if itemised_tax.get(item_code): + for tax in itemised_tax.get(row.item_code).values(): + _tax_rate = flt(tax.get("tax_rate", 0), row.precision("tax_rate")) + tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) + tax_rate += _tax_rate - if row.item_tax_rate: - item_tax_rate = frappe.parse_json(row.item_tax_rate) - - # First check if tax rate is present - # If not then look up in item_wise_tax_detail - if item_tax_rate: - for account, rate in item_tax_rate.items(): - tax_rate += rate - elif row.item_code and itemised_tax.get(row.item_code): - tax_rate = sum([tax.get("tax_rate", 0) for d, tax in itemised_tax.get(row.item_code).items()]) - - meta = frappe.get_meta(row.doctype) - - if meta.has_field("tax_rate"): - row.tax_rate = flt(tax_rate, row.precision("tax_rate")) - row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) - row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + row.tax_rate = flt(tax_rate, row.precision("tax_rate")) + row.tax_amount = flt(tax_amount, row.precision("tax_amount")) + row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) def get_account_currency(account): From 159be1d40fbc07b619c27f782d872bf2d1f35a3a Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Sun, 27 Aug 2023 18:43:42 +0000 Subject: [PATCH 007/135] fix: revert `ignore_account_closing_balance` field --- .../doctype/accounts_settings/accounts_settings.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 570be095ab..061bab320e 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -59,6 +59,7 @@ "closing_settings_tab", "period_closing_settings_section", "acc_frozen_upto", + "ignore_account_closing_balance", "column_break_25", "frozen_accounts_modifier", "tab_break_dpet", @@ -408,6 +409,13 @@ "fieldtype": "Check", "label": "Enable Fuzzy Matching" }, + { + "default": "0", + "description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ", + "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", @@ -421,7 +429,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-22 20:11:00.042840", + "modified": "2023-08-28 00:12:02.740633", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From eded7871f347f0f1e5149087d5e7a3ccfcf9dbf1 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 21 Sep 2023 07:32:08 +0530 Subject: [PATCH 008/135] refactor!: remove `GoCardless Settings` --- .../doctype/gocardless_settings/__init__.py | 89 ------- .../gocardless_settings.js | 8 - .../gocardless_settings.json | 211 ----------------- .../gocardless_settings.py | 220 ------------------ .../test_gocardless_settings.py | 8 - pyproject.toml | 2 - 6 files changed, 538 deletions(-) delete mode 100644 erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py delete mode 100644 erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js delete mode 100644 erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json delete mode 100644 erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py delete mode 100644 erpnext/erpnext_integrations/doctype/gocardless_settings/test_gocardless_settings.py diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py deleted file mode 100644 index 65be5993ff..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies and contributors -# For license information, please see license.txt - - -import hashlib -import hmac -import json - -import frappe - - -@frappe.whitelist(allow_guest=True) -def webhooks(): - r = frappe.request - if not r: - return - - if not authenticate_signature(r): - raise frappe.AuthenticationError - - gocardless_events = json.loads(r.get_data()) or [] - for event in gocardless_events["events"]: - set_status(event) - - return 200 - - -def set_status(event): - resource_type = event.get("resource_type", {}) - - if resource_type == "mandates": - set_mandate_status(event) - - -def set_mandate_status(event): - mandates = [] - if isinstance(event["links"], (list,)): - for link in event["links"]: - mandates.append(link["mandate"]) - else: - mandates.append(event["links"]["mandate"]) - - if ( - event["action"] == "pending_customer_approval" - or event["action"] == "pending_submission" - or event["action"] == "submitted" - or event["action"] == "active" - ): - disabled = 0 - else: - disabled = 1 - - for mandate in mandates: - frappe.db.set_value("GoCardless Mandate", mandate, "disabled", disabled) - - -def authenticate_signature(r): - """Returns True if the received signature matches the generated signature""" - received_signature = frappe.get_request_header("Webhook-Signature") - - if not received_signature: - return False - - for key in get_webhook_keys(): - computed_signature = hmac.new(key.encode("utf-8"), r.get_data(), hashlib.sha256).hexdigest() - if hmac.compare_digest(str(received_signature), computed_signature): - return True - - return False - - -def get_webhook_keys(): - def _get_webhook_keys(): - webhook_keys = [ - d.webhooks_secret - for d in frappe.get_all( - "GoCardless Settings", - fields=["webhooks_secret"], - ) - if d.webhooks_secret - ] - - return webhook_keys - - return frappe.cache().get_value("gocardless_webhooks_secret", _get_webhook_keys) - - -def clear_cache(): - frappe.cache().delete_value("gocardless_webhooks_secret") diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js deleted file mode 100644 index 241129719b..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('GoCardless Settings', { - refresh: function(frm) { - erpnext.utils.check_payments_app(); - } -}); diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json deleted file mode 100644 index cca36536ac..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json +++ /dev/null @@ -1,211 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:gateway_name", - "beta": 0, - "creation": "2018-02-06 16:11:10.028249", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gateway_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Payment Gateway Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_2", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "access_token", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Access Token", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "webhooks_secret", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Webhooks Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "use_sandbox", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Use Sandbox", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2022-02-12 14:18:47.209114", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "GoCardless Settings", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py deleted file mode 100644 index 4a29a6a21d..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py +++ /dev/null @@ -1,220 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies and contributors -# For license information, please see license.txt - - -from urllib.parse import urlencode - -import frappe -import gocardless_pro -from frappe import _ -from frappe.integrations.utils import create_request_log -from frappe.model.document import Document -from frappe.utils import call_hook_method, cint, flt, get_url - -from erpnext.utilities import payment_app_import_guard - - -class GoCardlessSettings(Document): - supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"] - - def validate(self): - self.initialize_client() - - def initialize_client(self): - self.environment = self.get_environment() - try: - self.client = gocardless_pro.Client( - access_token=self.access_token, environment=self.environment - ) - return self.client - except Exception as e: - frappe.throw(e) - - def on_update(self): - with payment_app_import_guard(): - from payments.utils import create_payment_gateway - - create_payment_gateway( - "GoCardless-" + self.gateway_name, settings="GoCardLess Settings", controller=self.gateway_name - ) - call_hook_method("payment_gateway_enabled", gateway="GoCardless-" + self.gateway_name) - - def on_payment_request_submission(self, data): - if data.reference_doctype != "Fees": - customer_data = frappe.db.get_value( - data.reference_doctype, data.reference_name, ["company", "customer_name"], as_dict=1 - ) - - data = { - "amount": flt(data.grand_total, data.precision("grand_total")), - "title": customer_data.company.encode("utf-8"), - "description": data.subject.encode("utf-8"), - "reference_doctype": data.doctype, - "reference_docname": data.name, - "payer_email": data.email_to or frappe.session.user, - "payer_name": customer_data.customer_name, - "order_id": data.name, - "currency": data.currency, - } - - valid_mandate = self.check_mandate_validity(data) - if valid_mandate is not None: - data.update(valid_mandate) - - self.create_payment_request(data) - return False - else: - return True - - def check_mandate_validity(self, data): - - if frappe.db.exists("GoCardless Mandate", dict(customer=data.get("payer_name"), disabled=0)): - registered_mandate = frappe.db.get_value( - "GoCardless Mandate", dict(customer=data.get("payer_name"), disabled=0), "mandate" - ) - self.initialize_client() - mandate = self.client.mandates.get(registered_mandate) - - if ( - mandate.status == "pending_customer_approval" - or mandate.status == "pending_submission" - or mandate.status == "submitted" - or mandate.status == "active" - ): - return {"mandate": registered_mandate} - else: - return None - else: - return None - - def get_environment(self): - if self.use_sandbox: - return "sandbox" - else: - return "live" - - def validate_transaction_currency(self, currency): - if currency not in self.supported_currencies: - frappe.throw( - _( - "Please select another payment method. Go Cardless does not support transactions in currency '{0}'" - ).format(currency) - ) - - def get_payment_url(self, **kwargs): - return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs))) - - def create_payment_request(self, data): - self.data = frappe._dict(data) - - try: - self.integration_request = create_request_log(self.data, "Host", "GoCardless") - return self.create_charge_on_gocardless() - - except Exception: - frappe.log_error("Gocardless payment reqeust failed") - return { - "redirect_to": frappe.redirect_to_message( - _("Server Error"), - _( - "There seems to be an issue with the server's GoCardless configuration. Don't worry, in case of failure, the amount will get refunded to your account." - ), - ), - "status": 401, - } - - def create_charge_on_gocardless(self): - redirect_to = self.data.get("redirect_to") or None - redirect_message = self.data.get("redirect_message") or None - - reference_doc = frappe.get_doc( - self.data.get("reference_doctype"), self.data.get("reference_docname") - ) - self.initialize_client() - - try: - payment = self.client.payments.create( - params={ - "amount": cint(reference_doc.grand_total * 100), - "currency": reference_doc.currency, - "links": {"mandate": self.data.get("mandate")}, - "metadata": { - "reference_doctype": reference_doc.doctype, - "reference_document": reference_doc.name, - }, - }, - headers={ - "Idempotency-Key": self.data.get("reference_docname"), - }, - ) - - if ( - payment.status == "pending_submission" - or payment.status == "pending_customer_approval" - or payment.status == "submitted" - ): - self.integration_request.db_set("status", "Authorized", update_modified=False) - self.flags.status_changed_to = "Completed" - self.integration_request.db_set("output", payment.status, update_modified=False) - - elif payment.status == "confirmed" or payment.status == "paid_out": - self.integration_request.db_set("status", "Completed", update_modified=False) - self.flags.status_changed_to = "Completed" - self.integration_request.db_set("output", payment.status, update_modified=False) - - elif ( - payment.status == "cancelled" - or payment.status == "customer_approval_denied" - or payment.status == "charged_back" - ): - self.integration_request.db_set("status", "Cancelled", update_modified=False) - frappe.log_error("Gocardless payment cancelled") - self.integration_request.db_set("error", payment.status, update_modified=False) - else: - self.integration_request.db_set("status", "Failed", update_modified=False) - frappe.log_error("Gocardless payment failed") - self.integration_request.db_set("error", payment.status, update_modified=False) - - except Exception as e: - frappe.log_error("GoCardless Payment Error") - - if self.flags.status_changed_to == "Completed": - status = "Completed" - if "reference_doctype" in self.data and "reference_docname" in self.data: - custom_redirect_to = None - try: - custom_redirect_to = frappe.get_doc( - self.data.get("reference_doctype"), self.data.get("reference_docname") - ).run_method("on_payment_authorized", self.flags.status_changed_to) - except Exception: - frappe.log_error("Gocardless redirect failed") - - if custom_redirect_to: - redirect_to = custom_redirect_to - - redirect_url = redirect_to - else: - status = "Error" - redirect_url = "payment-failed" - - if redirect_message: - redirect_url += "&" + urlencode({"redirect_message": redirect_message}) - - redirect_url = get_url(redirect_url) - - return {"redirect_to": redirect_url, "status": status} - - -def get_gateway_controller(doc): - payment_request = frappe.get_doc("Payment Request", doc) - gateway_controller = frappe.db.get_value( - "Payment Gateway", payment_request.payment_gateway, "gateway_controller" - ) - return gateway_controller - - -def gocardless_initialization(doc): - gateway_controller = get_gateway_controller(doc) - settings = frappe.get_doc("GoCardless Settings", gateway_controller) - client = settings.initialize_client() - return client diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/test_gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/test_gocardless_settings.py deleted file mode 100644 index 379afe51dd..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/test_gocardless_settings.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies and Contributors -# See license.txt - -import unittest - - -class TestGoCardlessSettings(unittest.TestCase): - pass diff --git a/pyproject.toml b/pyproject.toml index 7841c92054..604aa44585 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,11 +16,9 @@ dependencies = [ "holidays~=0.28", # integration dependencies - "gocardless-pro~=1.22.0", "googlemaps", "plaid-python~=7.2.1", "python-youtube~=0.8.0", - "tweepy~=4.14.0", # Not used directly - required by PyQRCode for PNG generation "pypng~=0.20220715.0", From eb419e8e591e9101954523a9fb2d122d0e778ae8 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 21 Sep 2023 07:32:39 +0530 Subject: [PATCH 009/135] refactor!: remove `Mpesa Settings` --- .../doctype/mpesa_settings/__init__.py | 0 .../mpesa_settings/account_balance.html | 28 -- .../doctype/mpesa_settings/mpesa_connector.py | 149 -------- .../mpesa_settings/mpesa_custom_fields.py | 56 --- .../doctype/mpesa_settings/mpesa_settings.js | 39 -- .../mpesa_settings/mpesa_settings.json | 152 -------- .../doctype/mpesa_settings/mpesa_settings.py | 354 ----------------- .../mpesa_settings/test_mpesa_settings.py | 361 ------------------ erpnext/erpnext_integrations/utils.py | 31 -- 9 files changed, 1170 deletions(-) delete mode 100644 erpnext/erpnext_integrations/doctype/mpesa_settings/__init__.py delete mode 100644 erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html delete mode 100644 erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py delete mode 100644 erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py delete mode 100644 erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js delete mode 100644 erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json delete mode 100644 erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py delete mode 100644 erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/__init__.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html b/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html deleted file mode 100644 index b74a7187f0..0000000000 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html +++ /dev/null @@ -1,28 +0,0 @@ - -{% if not jQuery.isEmptyObject(data) %} -
{{ __("Balance Details") }}
- - - - - - - - - - - - {% for(const [key, value] of Object.entries(data)) { %} - - - - - - - - {% } %} - -
{{ __("Account Type") }}{{ __("Current Balance") }}{{ __("Available Balance") }}{{ __("Reserved Balance") }}{{ __("Uncleared Balance") }}
{%= key %} {%= value["current_balance"] %} {%= value["available_balance"] %} {%= value["reserved_balance"] %} {%= value["uncleared_balance"] %}
-{% else %} -

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/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 = "" From 543a76863f2fc32fd10b64ce02637d11dc357a0d Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 22 Sep 2023 18:49:13 +0530 Subject: [PATCH 010/135] refactor!: remove `GoCardless Mandate` --- .../doctype/gocardless_mandate/__init__.py | 0 .../gocardless_mandate/gocardless_mandate.js | 5 - .../gocardless_mandate.json | 184 ------------------ .../gocardless_mandate/gocardless_mandate.py | 9 - .../test_gocardless_mandate.py | 8 - 5 files changed, 206 deletions(-) delete mode 100644 erpnext/erpnext_integrations/doctype/gocardless_mandate/__init__.py delete mode 100644 erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.js delete mode 100644 erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.json delete mode 100644 erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.py delete mode 100644 erpnext/erpnext_integrations/doctype/gocardless_mandate/test_gocardless_mandate.py diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/__init__.py b/erpnext/erpnext_integrations/doctype/gocardless_mandate/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.js b/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.js deleted file mode 100644 index 37f9f7b9df..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('GoCardless Mandate', { -}); diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.json b/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.json deleted file mode 100644 index edf652c8f3..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.json +++ /dev/null @@ -1,184 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:mandate", - "beta": 0, - "creation": "2018-02-08 11:33:15.721919", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Customer", - "length": 0, - "no_copy": 0, - "options": "Customer", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mandate", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mandate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gocardless_customer", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "GoCardless Customer", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-02-11 12:28:03.183095", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "GoCardless Mandate", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.py b/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.py deleted file mode 100644 index bceb3caebd..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class GoCardlessMandate(Document): - pass diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/test_gocardless_mandate.py b/erpnext/erpnext_integrations/doctype/gocardless_mandate/test_gocardless_mandate.py deleted file mode 100644 index 0c1952a16a..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_mandate/test_gocardless_mandate.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies and Contributors -# See license.txt - -import unittest - - -class TestGoCardlessMandate(unittest.TestCase): - pass From 9554f6ea3c35488cd33e27aa5efcaf6ea4b7b9b8 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 22 Sep 2023 18:52:26 +0530 Subject: [PATCH 011/135] refactor!: remove `GoCardless Templates` --- .../integrations/gocardless_checkout.js | 24 ---- .../integrations/gocardless_confirmation.js | 24 ---- .../templates/pages/integrations/__init__.py | 0 .../integrations/gocardless_checkout.html | 16 --- .../pages/integrations/gocardless_checkout.py | 100 ----------------- .../integrations/gocardless_confirmation.html | 16 --- .../integrations/gocardless_confirmation.py | 106 ------------------ 7 files changed, 286 deletions(-) delete mode 100644 erpnext/templates/includes/integrations/gocardless_checkout.js delete mode 100644 erpnext/templates/includes/integrations/gocardless_confirmation.js delete mode 100644 erpnext/templates/pages/integrations/__init__.py delete mode 100644 erpnext/templates/pages/integrations/gocardless_checkout.html delete mode 100644 erpnext/templates/pages/integrations/gocardless_checkout.py delete mode 100644 erpnext/templates/pages/integrations/gocardless_confirmation.html delete mode 100644 erpnext/templates/pages/integrations/gocardless_confirmation.py diff --git a/erpnext/templates/includes/integrations/gocardless_checkout.js b/erpnext/templates/includes/integrations/gocardless_checkout.js deleted file mode 100644 index b18d55090c..0000000000 --- a/erpnext/templates/includes/integrations/gocardless_checkout.js +++ /dev/null @@ -1,24 +0,0 @@ -$(document).ready(function() { - var data = {{ frappe.form_dict | json }}; - var doctype = "{{ reference_doctype }}" - var docname = "{{ reference_docname }}" - - frappe.call({ - method: "erpnext.templates.pages.integrations.gocardless_checkout.check_mandate", - freeze: true, - headers: { - "X-Requested-With": "XMLHttpRequest" - }, - args: { - "data": JSON.stringify(data), - "reference_doctype": doctype, - "reference_docname": docname - }, - callback: function(r) { - if (r.message) { - window.location.href = r.message.redirect_to - } - } - }) - -}) diff --git a/erpnext/templates/includes/integrations/gocardless_confirmation.js b/erpnext/templates/includes/integrations/gocardless_confirmation.js deleted file mode 100644 index fee1d2b632..0000000000 --- a/erpnext/templates/includes/integrations/gocardless_confirmation.js +++ /dev/null @@ -1,24 +0,0 @@ -$(document).ready(function() { - var redirect_flow_id = "{{ redirect_flow_id }}"; - var doctype = "{{ reference_doctype }}"; - var docname = "{{ reference_docname }}"; - - frappe.call({ - method: "erpnext.templates.pages.integrations.gocardless_confirmation.confirm_payment", - freeze: true, - headers: { - "X-Requested-With": "XMLHttpRequest" - }, - args: { - "redirect_flow_id": redirect_flow_id, - "reference_doctype": doctype, - "reference_docname": docname - }, - callback: function(r) { - if (r.message) { - window.location.href = r.message.redirect_to; - } - } - }); - -}); diff --git a/erpnext/templates/pages/integrations/__init__.py b/erpnext/templates/pages/integrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/templates/pages/integrations/gocardless_checkout.html b/erpnext/templates/pages/integrations/gocardless_checkout.html deleted file mode 100644 index 6072db49ea..0000000000 --- a/erpnext/templates/pages/integrations/gocardless_checkout.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "templates/web.html" %} - -{% block title %} Payment {% endblock %} - -{%- block header -%}{% endblock %} - -{% block script %} - -{% endblock %} - -{%- block page_content -%} -

- {{ _("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") From 38aebf65e2c9a93e3fc0d999668040f471c55b51 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 25 Sep 2023 11:08:24 +0530 Subject: [PATCH 012/135] refactor!: remove `stripe_integration.py` --- .../payment_request/payment_request.py | 4 +- .../stripe_integration.py | 70 ------------------- 2 files changed, 3 insertions(+), 71 deletions(-) delete mode 100644 erpnext/erpnext_integrations/stripe_integration.py diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 11d6d5f433..028efc4d6d 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -20,7 +20,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 @@ -393,6 +392,9 @@ class PaymentRequest(Document): 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) 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() From b1770b3f8679a2edf8375d0185547a7fcf2bb2bd Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 26 Sep 2023 15:10:20 +0530 Subject: [PATCH 013/135] refactor: remove test `test_default_bank_account` --- .../plaid_settings/test_plaid_settings.py | 34 ------------------- 1 file changed, 34 deletions(-) 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() From 296b233659f2596b6b7de479ca3a00df99885eb4 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 27 Sep 2023 15:27:29 +0530 Subject: [PATCH 014/135] chore: patch to delete Payment Gateways --- erpnext/patches.txt | 1 + erpnext/patches/v15_0/delete_payment_gateway_doctypes.py | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 erpnext/patches/v15_0/delete_payment_gateway_doctypes.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e9c056e3a9..8f2d076b53 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -344,5 +344,6 @@ 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 # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v15_0/delete_payment_gateway_doctypes.py b/erpnext/patches/v15_0/delete_payment_gateway_doctypes.py new file mode 100644 index 0000000000..959b065780 --- /dev/null +++ b/erpnext/patches/v15_0/delete_payment_gateway_doctypes.py @@ -0,0 +1,6 @@ +import frappe + + +def execute(): + for dt in ("GoCardless Settings", "GoCardless Mandate", "Mpesa Settings"): + frappe.delete_doc("DocType", dt, ignore_missing=True) From d3f94a03fc011b8a4ef380497a1e2bc4d54cea57 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 1 Oct 2023 14:04:34 +0200 Subject: [PATCH 015/135] fix(stock): add delivery user and manager role --- erpnext/setup/doctype/driver/driver.json | 18 +++++++++- erpnext/setup/doctype/vehicle/vehicle.json | 18 +++++++++- .../doctype/delivery_note/delivery_note.json | 30 ++++++++++++++++ .../delivery_settings/delivery_settings.json | 4 +-- .../doctype/delivery_trip/delivery_trip.json | 34 +++++++++++++++++-- 5 files changed, 98 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/doctype/driver/driver.json b/erpnext/setup/doctype/driver/driver.json index 8d426cc29a..2e994b5ff9 100644 --- a/erpnext/setup/doctype/driver/driver.json +++ b/erpnext/setup/doctype/driver/driver.json @@ -157,6 +157,22 @@ "role": "HR Manager", "share": 1, "write": 1 + }, + { + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery User" + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery Manager", + "share": 1, + "write": 1 } ], "quick_entry": 1, @@ -166,4 +182,4 @@ "sort_order": "DESC", "title_field": "full_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/vehicle/vehicle.json b/erpnext/setup/doctype/vehicle/vehicle.json index ed803a763a..b19d45924f 100644 --- a/erpnext/setup/doctype/vehicle/vehicle.json +++ b/erpnext/setup/doctype/vehicle/vehicle.json @@ -860,6 +860,22 @@ "share": 1, "submit": 0, "write": 1 + }, + { + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery User" + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery Manager", + "share": 1, + "write": 1 } ], "quick_entry": 1, @@ -872,4 +888,4 @@ "title_field": "", "track_changes": 1, "track_seen": 0 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index e0d49192eb..b85f296d0b 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1460,6 +1460,36 @@ "read": 1, "role": "Stock Manager", "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery Manager", + "share": 1, + "submit": 1, + "write": 1 } ], "search_fields": "status,customer,customer_name, territory,base_grand_total", diff --git a/erpnext/stock/doctype/delivery_settings/delivery_settings.json b/erpnext/stock/doctype/delivery_settings/delivery_settings.json index 963403b8f2..ad0ac45851 100644 --- a/erpnext/stock/doctype/delivery_settings/delivery_settings.json +++ b/erpnext/stock/doctype/delivery_settings/delivery_settings.json @@ -239,7 +239,7 @@ "print": 1, "read": 1, "report": 0, - "role": "System Manager", + "role": "Delivery Manager", "set_user_permissions": 0, "share": 1, "submit": 0, @@ -255,4 +255,4 @@ "track_changes": 1, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json index 9d8fe46e8c..ec72af8404 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json @@ -188,7 +188,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2023-06-27 11:22:27.927637", + "modified": "2023-10-01 07:06:06.314503", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Trip", @@ -224,10 +224,40 @@ "share": 1, "submit": 1, "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery Manager", + "share": 1, + "submit": 1, + "write": 1 } ], "sort_field": "modified", "sort_order": "DESC", "states": [], "title_field": "driver_name" -} \ No newline at end of file +} From 38ca164662532a97469db4b2d0c1519a570120eb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 9 Oct 2023 23:45:58 +0200 Subject: [PATCH 016/135] fix: german tranlations of "Is Return" --- erpnext/translations/de.csv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 79777f2338..79b9574239 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -4586,7 +4586,7 @@ ACC-PINV-.YYYY.-,ACC-PINV-.JJJJ.-, Tax Withholding Category,Steuereinbehalt Kategorie, Edit Posting Date and Time,Buchungsdatum und -uhrzeit bearbeiten, Is Paid,Ist bezahlt, -Is Return (Debit Note),ist Rücklieferung (Lastschrift), +Is Return (Debit Note),Ist Rechnungskorrektur (Retoure), Apply Tax Withholding Amount,Steuereinbehaltungsbetrag anwenden, Accounting Dimensions ,Buchhaltung Dimensionen, Supplier Invoice Details,Lieferant Rechnungsdetails, @@ -4710,7 +4710,7 @@ Item Wise Tax Detail ,Item Wise Tax Detail, ACC-SINV-.YYYY.-,ACC-SINV-.JJJJ.-, Include Payment (POS),(POS) Zahlung einschließen, Offline POS Name,Offline-Verkaufsstellen-Name, -Is Return (Credit Note),ist Rücklieferung (Gutschrift), +Is Return (Credit Note),Ist Rechnungskorrektur (Retoure), Update Billed Amount in Sales Order,Aktualisierung des Rechnungsbetrags im Auftrag, Customer PO Details,Auftragsdetails, Customer's Purchase Order,Bestellung des Kunden, @@ -6998,7 +6998,7 @@ Customs Tariff Number,Zolltarifnummer, Tariff Number,Tarifnummer, Delivery To,Lieferung an, MAT-DN-.YYYY.-,MAT-DN-.YYYY.-, -Is Return,Ist Rückgabe, +Is Return,Ist Retoure, Issue Credit Note,Gutschrift ausgeben, Return Against Delivery Note,Zurück zum Lieferschein, Customer's Purchase Order No,Bestellnummer des Kunden, From bda82bf1e9622dda9f7fa42b27b31b3879de342b Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 10 Oct 2023 10:30:09 +0000 Subject: [PATCH 017/135] fix(gp): wrong `allocated_amount` on multi sales person invoice --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 ++ 1 file changed, 2 insertions(+) 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) From 0d7a0f393deed1f3bec5d5925bd5f9bb4eab99c5 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 11 Oct 2023 10:51:40 +0530 Subject: [PATCH 018/135] fix(ux): allow MR to Stop until fully received --- .../doctype/material_request/material_request.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index bf3301f6d8..9673a70501 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -102,6 +102,12 @@ frappe.ui.form.on('Material Request', { if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') { let precision = frappe.defaults.get_default("float_precision"); + + if (flt(frm.doc.per_received, precision) < 100) { + frm.add_custom_button(__('Stop'), + () => frm.events.update_status(frm, 'Stopped')); + } + if (flt(frm.doc.per_ordered, precision) < 100) { let add_create_pick_list_button = () => { frm.add_custom_button(__('Pick List'), @@ -148,11 +154,6 @@ frappe.ui.form.on('Material Request', { } frm.page.set_inner_btn_group_as_primary(__('Create')); - - // stop - frm.add_custom_button(__('Stop'), - () => frm.events.update_status(frm, 'Stopped')); - } } From 4a6108e91251e4d284024b42ec48030493f87288 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Wed, 11 Oct 2023 11:17:47 +0530 Subject: [PATCH 019/135] fix: use mariadb instead of mysql Drop mysql-client in favour of mariadb-client Signed-off-by: Akhil Narang --- .github/helper/install.sh | 16 +++++++++------- .github/workflows/patch.yml | 2 +- .github/workflows/server-tests-mariadb.yml | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) 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/patch.yml b/.github/workflows/patch.yml index 07b8de7a90..21dd3d4879 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 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 From cea0d65fbdc2b38689521cdb3aeadf8ce88d50a4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 11 Oct 2023 12:22:06 +0530 Subject: [PATCH 020/135] chore: disable beta release --- .github/workflows/initiate_release.yml | 20 -------------------- 1 file changed, 20 deletions(-) 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 }} From c1782c50158e50e1e57e7eeda276ffd46203f86c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Oct 2023 17:12:47 +0530 Subject: [PATCH 021/135] refactor: for non-repost fields, don't validate --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 5 +++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 85ed1260d3..2433268627 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -539,8 +539,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: 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 From f3238f910509813a24fbc7ebfb726f42f6addd6f Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 11 Oct 2023 14:08:11 +0530 Subject: [PATCH 022/135] fix: production plan reserved qty incorrect calculation (#37400) --- .../production_plan/production_plan.py | 23 ++++++------------- .../production_plan/test_production_plan.py | 6 ++--- .../doctype/work_order/work_order.py | 19 ++++++++------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index deef020220..ddd9375211 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -8,7 +8,6 @@ import json import frappe from frappe import _, msgprint from frappe.model.document import Document -from frappe.query_builder import Case from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( add_days, @@ -1618,21 +1617,13 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Material Request Plan Item") - completed_production_plans = get_completed_production_plans() + non_completed_production_plans = get_non_completed_production_plans() - case = Case() query = ( frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select( - Sum( - child.quantity - * IfNull( - case.when(child.material_request_type == "Purchase", child.conversion_factor).else_(1.0), 1.0 - ) - ) - ) + .select(Sum(child.required_bom_qty)) .where( (table.docstatus == 1) & (child.item_code == item_code) @@ -1641,8 +1632,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): ) ) - if completed_production_plans: - query = query.where(table.name.notin(completed_production_plans)) + if non_completed_production_plans: + query = query.where(table.name.isin(non_completed_production_plans)) query = query.run() @@ -1653,7 +1644,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): reserved_qty_for_production = flt( get_reserved_qty_for_production( - item_code, warehouse, completed_production_plans, check_production_plan=True + item_code, warehouse, non_completed_production_plans, check_production_plan=True ) ) @@ -1663,7 +1654,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): return reserved_qty_for_production_plan - reserved_qty_for_production -def get_completed_production_plans(): +def get_non_completed_production_plans(): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Production Plan Item") @@ -1675,7 +1666,7 @@ def get_completed_production_plans(): .where( (table.docstatus == 1) & (table.status.notin(["Completed", "Closed"])) - & (child.ordered_qty >= child.planned_qty) + & (child.planned_qty > child.ordered_qty) ) ).run(as_dict=True) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 4ff9d29e0b..6ab9232788 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -6,8 +6,8 @@ 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_completed_production_plans, get_items_for_material_requests, + get_non_completed_production_plans, get_sales_orders, get_warehouse_list, ) @@ -1143,9 +1143,9 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(after_qty, before_qty) - completed_plans = get_completed_production_plans() + completed_plans = get_non_completed_production_plans() for plan in plans: - self.assertTrue(plan in completed_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 diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 3dc33ac578..f9fddcbb5e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1515,7 +1515,7 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): def get_reserved_qty_for_production( item_code: str, warehouse: str, - completed_production_plans: list = None, + non_completed_production_plans: list = None, check_production_plan: bool = False, ) -> float: """Get total reserved quantity for any item in specified warehouse""" @@ -1538,19 +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 completed_production_plans: - query = query.where(wo.production_plan.notin(completed_production_plans)) + if non_completed_production_plans: + query = query.where(wo.production_plan.isin(non_completed_production_plans)) return query.run()[0][0] or 0.0 From 0cdd6435a556309d62240fc669ce431efee040c3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 11 Oct 2023 14:42:23 +0530 Subject: [PATCH 023/135] refactor: add validation for Advances in SI/PI --- erpnext/controllers/accounts_controller.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6812940ee2..e170044f8e 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() From 5ebf7c8c29eb9c318529cd55943b76b2467f135c Mon Sep 17 00:00:00 2001 From: Rishik Sahu Date: Thu, 12 Oct 2023 14:03:49 +0530 Subject: [PATCH 024/135] fixed-#37231-changed-doc-to-d/closes-the-isse --- erpnext/public/js/controllers/accounts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index a2e4bdacac..354552137b 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -116,7 +116,7 @@ erpnext.accounts.taxes = { account_head: function(frm, cdt, cdn) { let d = locals[cdt][cdn]; - if (doc.docstatus == 1) { + if (d.docstatus == 1) { // Should not trigger any changes on change post submit return; } From 2c56ee97c7c14b5250b0fae82c73476baa50a822 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 12 Oct 2023 15:57:50 +0530 Subject: [PATCH 025/135] refactor: back calculate total amt for TDS --- .../tax_withholding_details/tax_withholding_details.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From 18e3a8907a78c323d1aacce6d46732f97ee8fdd2 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 12 Oct 2023 19:41:11 +0530 Subject: [PATCH 026/135] fix: don't set finance books if gross_purchase_amount is not set (#37480) --- erpnext/assets/doctype/asset/asset.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 5395f15e7a..f0e4c82048 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -337,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', []); @@ -490,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", []); From 17ca8756a72765e10e17d2a2b81f29129263ab26 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 12 Oct 2023 20:43:15 +0530 Subject: [PATCH 027/135] refactor(patch): ignore links on closing balance patch --- .../doctype/account_closing_balance/account_closing_balance.py | 1 + 1 file changed, 1 insertion(+) 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() From ad00df0af6556a806e3b9b40ecaac719f0ce20a4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 13 Oct 2023 15:39:54 +0530 Subject: [PATCH 028/135] fix: keyerror on gl and pl comparision report --- .../general_and_payment_ledger_comparison.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 = [] From b2cee396ac9edea5ba920382bfc27f3736600775 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 14 Oct 2023 20:42:26 +0530 Subject: [PATCH 029/135] fix: consider received qty while creating SO -> MR --- .../doctype/sales_order/sales_order.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index aae0fee467..b91002eb86 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -606,29 +606,37 @@ def close_or_unclose_sales_orders(names, status): def get_requested_item_qty(sales_order): - return frappe._dict( - frappe.db.sql( - """ - select sales_order_item, sum(qty) - from `tabMaterial Request Item` - where docstatus = 1 - and sales_order = %s - group by sales_order_item - """, - sales_order, - ) - ) + result = {} + for d in frappe.db.get_all( + "Material Request Item", + filters={"docstatus": 1, "sales_order": sales_order}, + fields=["sales_order_item", "sum(qty) as qty", "sum(received_qty) as received_qty"], + group_by="sales_order_item", + ): + result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty}) + + return result @frappe.whitelist() def make_material_request(source_name, target_doc=None): requested_item_qty = get_requested_item_qty(source_name) + def get_remaining_qty(so_item): + return flt( + flt(so_item.qty) + - flt(requested_item_qty.get(so_item.name, {}).get("qty")) + - max( + flt(so_item.get("delivered_qty")) + - flt(requested_item_qty.get(so_item.name, {}).get("received_qty")), + 0, + ) + ) + def update_item(source, target, source_parent): # qty is for packed items, because packed items don't have stock_qty field - qty = source.get("qty") target.project = source_parent.project - target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty")) + target.qty = get_remaining_qty(source) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) args = target.as_dict().copy() @@ -661,8 +669,8 @@ def make_material_request(source_name, target_doc=None): "Sales Order Item": { "doctype": "Material Request Item", "field_map": {"name": "sales_order_item", "parent": "sales_order"}, - "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) - and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0), + "condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code) + and get_remaining_qty(item) > 0, "postprocess": update_item, }, }, From c322e5f38140b1fab8f940db542e25c2b122ab54 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 09:28:42 +0530 Subject: [PATCH 030/135] test: use fixtures for sales and purchase invoice --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 8aa1f4c103..442dc99ef4 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 @@ -45,7 +45,7 @@ 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 @@ -53,6 +53,9 @@ class TestSalesInvoice(unittest.TestCase): create_internal_parties() setup_accounts() + def tearDown(self): + frappe.db.rollback() + def make(self): w = frappe.copy_doc(test_records[0]) w.is_pos = 0 From fc50b174eb5f37a40b522f7e073d11260e0c12c8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 10:56:39 +0530 Subject: [PATCH 031/135] refactor(test): unset accounts frozen date --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 442dc99ef4..bc44ef2637 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -52,10 +52,14 @@ class TestSalesInvoice(FrappeTestCase): create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) create_internal_parties() setup_accounts() + self.remove_accounts_frozen_date() def tearDown(self): frappe.db.rollback() + def remove_accounts_frozen_date(self): + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + def make(self): w = frappe.copy_doc(test_records[0]) w.is_pos = 0 From 58065f31b1e2e550661a47b4442f6861406ebec5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 13:12:21 +0530 Subject: [PATCH 032/135] refactor(test): use @change_settings in sales invoice --- .../sales_invoice/test_sales_invoice.py | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index bc44ef2637..ef31eaa97c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -52,14 +52,10 @@ class TestSalesInvoice(FrappeTestCase): create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) create_internal_parties() setup_accounts() - self.remove_accounts_frozen_date() def tearDown(self): frappe.db.rollback() - def remove_accounts_frozen_date(self): - frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) - def make(self): w = frappe.copy_doc(test_records[0]) w.is_pos = 0 @@ -3080,8 +3076,8 @@ class TestSalesInvoice(FrappeTestCase): 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() @@ -3090,8 +3086,6 @@ class TestSalesInvoice(FrappeTestCase): 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 @@ -3120,6 +3114,13 @@ class TestSalesInvoice(FrappeTestCase): 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", @@ -3127,11 +3128,6 @@ class TestSalesInvoice(FrappeTestCase): 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 @@ -3197,11 +3193,6 @@ class TestSalesInvoice(FrappeTestCase): 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): From 8ebe5733ac61b6291b22901dbbf070093200706f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 14:57:31 +0530 Subject: [PATCH 033/135] refactor(test): fix broken test cases in Sales Invoice --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ef31eaa97c..842d77a29a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -52,6 +52,7 @@ class TestSalesInvoice(FrappeTestCase): 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() @@ -3193,8 +3194,6 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) - 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 From de9baef84a77f5bb2aa94f200147d0689462b9c3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 11 Oct 2023 20:18:59 +0530 Subject: [PATCH 034/135] refactor(test): use @change_settings to fix failing test cases --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 842d77a29a..1f54736049 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -183,6 +183,7 @@ class TestSalesInvoice(FrappeTestCase): 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 @@ -1304,6 +1305,7 @@ class TestSalesInvoice(FrappeTestCase): 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, From 3bdf4f628c40d4e8ac19a41c738a8ba382d90d99 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 05:56:52 +0530 Subject: [PATCH 035/135] refactor(test): use test fixture in subscription --- erpnext/accounts/doctype/subscription/test_subscription.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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( From a2e064d214ffb9f012f2144bee14cd467e935241 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 06:35:22 +0530 Subject: [PATCH 036/135] refactor(test): use test fixture in purchase invoice --- .../doctype/purchase_invoice/test_purchase_invoice.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index aa3d1b3c15..cd055e3013 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 From 0207d6e7c996cd6c1b04f2ba171fdf3d6ccfa130 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 07:11:11 +0530 Subject: [PATCH 037/135] refactor(test): make use of @change_settings in PI test cases --- .../doctype/purchase_invoice/test_purchase_invoice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index cd055e3013..e365d60f20 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -425,6 +425,7 @@ class TestPurchaseInvoice(FrappeTestCase, 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, @@ -479,6 +480,7 @@ class TestPurchaseInvoice(FrappeTestCase, 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, @@ -1223,6 +1225,7 @@ class TestPurchaseInvoice(FrappeTestCase, 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" @@ -1423,6 +1426,7 @@ class TestPurchaseInvoice(FrappeTestCase, 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 From fbabf4ac2e96c473884c94e59b715d14dee3f960 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 08:07:29 +0530 Subject: [PATCH 038/135] refactor(test): make sure TDS Payable is available for testing --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 1f54736049..c1adffde31 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2781,6 +2781,13 @@ class TestSalesInvoice(FrappeTestCase): 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 From 46add06a29f8a0a5990dfd3aeda39f01413071bb Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 15 Oct 2023 15:46:29 +0530 Subject: [PATCH 039/135] fix: GL Entries not getting created for PR Return --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 6afa86e34e..de0db1aa8f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -339,7 +339,7 @@ class PurchaseReceipt(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) for d in self.get("items"): - if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): + if d.item_code in stock_items and flt(d.qty) and (flt(d.valuation_rate) or self.is_return): if warehouse_account.get(d.warehouse): stock_value_diff = frappe.db.get_value( "Stock Ledger Entry", From 253d4782c63963df78216ce51d9f9f9a80791531 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 15 Oct 2023 23:34:58 +0530 Subject: [PATCH 040/135] test: add test case for PR return with zero rate --- .../purchase_receipt/test_purchase_receipt.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index a8ef5e8e48..466e8e7b12 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2086,6 +2086,64 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr.reload() self.assertEqual(return_pr.status, "Completed") + def test_purchase_return_with_zero_rate(self): + company = "_Test Company with perpetual inventory" + + # Step - 1: Create Item + item, warehouse = ( + make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name, + "Stores - TCP1", + ) + + # Step - 2: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + se = make_stock_entry( + purpose="Material Receipt", + item_code=item, + qty=100, + basic_rate=100, + to_warehouse=warehouse, + company=company, + ) + + # Step - 3: Create Purchase Receipt + pr = make_purchase_receipt( + item_code=item, + qty=5, + rate=0, + warehouse=warehouse, + company=company, + ) + + # Step - 4: Create Purchase Return + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + pr_return = make_return_doc("Purchase Receipt", pr.name) + pr_return.save() + pr_return.submit() + + sl_entries = get_sl_entries(pr_return.doctype, pr_return.name) + gl_entries = get_gl_entries(pr_return.doctype, pr_return.name) + + # Test - 1: SLE Stock Value Difference should be equal to Qty * Average Rate + average_rate = ( + (se.items[0].qty * se.items[0].basic_rate) + (pr.items[0].qty * pr.items[0].rate) + ) / (se.items[0].qty + pr.items[0].qty) + expected_stock_value_difference = pr_return.items[0].qty * average_rate + self.assertEqual( + flt(sl_entries[0].stock_value_difference, 2), flt(expected_stock_value_difference, 2) + ) + + # Test - 2: GL Entries should be created for Stock Value Difference + self.assertEqual(len(gl_entries), 2) + + # Test - 3: SLE Stock Value Difference should be equal to Debit or Credit of GL Entries. + for entry in gl_entries: + self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) + + self.assertIsNotNone(get_gl_entries(pr_return.doctype, pr_return.name)) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 240b161e8161c77ce7e59b50b61d8e7ec9c6cdb7 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 16 Oct 2023 01:08:42 +0530 Subject: [PATCH 041/135] fix: purchase return test case --- erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 466e8e7b12..cdf50532fc 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2142,8 +2142,6 @@ class TestPurchaseReceipt(FrappeTestCase): for entry in gl_entries: self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) - self.assertIsNotNone(get_gl_entries(pr_return.doctype, pr_return.name)) - def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 2790ae07443381ec4b2b23d645bc52e98fdf9ee4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 16 Oct 2023 15:43:14 +0530 Subject: [PATCH 042/135] fix: Update frappe.link_search usage refer https://github.com/frappe/frappe/pull/22745 --- erpnext/public/js/utils/item_selector.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/utils/item_selector.js b/erpnext/public/js/utils/item_selector.js index 9fc264086a..e74d291acd 100644 --- a/erpnext/public/js/utils/item_selector.js +++ b/erpnext/public/js/utils/item_selector.js @@ -97,14 +97,14 @@ erpnext.ItemSelector = class ItemSelector { } var me = this; - frappe.link_search("Item", args, function(r) { - $.each(r.values, function(i, d) { + frappe.link_search("Item", args, function(results) { + $.each(results, function(i, d) { if(!d.image) { d.abbr = frappe.get_abbr(d.item_name); d.color = frappe.get_palette(d.item_name); } }); - me.dialog.results.html(frappe.render_template('item_selector', {'data':r.values})); + me.dialog.results.html(frappe.render_template('item_selector', {'data': results})); }); } }; From 08315522bbc198fce1168a5e8522684cad750276 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 3 Oct 2023 09:53:41 +0530 Subject: [PATCH 043/135] refactor: checkbox to toggle exchange rate inheritence in PO->PI --- .../doctype/purchase_invoice/purchase_invoice.json | 10 +++++++++- .../doctype/buying_settings/buying_settings.json | 10 +++++++++- erpnext/controllers/accounts_controller.py | 11 +++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index e4898826ec..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", @@ -1588,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-10-01 21:01:47.282533", + "modified": "2023-10-16 16:24:51.886231", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", 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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6812940ee2..2beee283cb 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -572,6 +572,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 From 5b4528e6146aaeb8f86f9fde3f272635d005eeec Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 16 Oct 2023 16:26:03 +0530 Subject: [PATCH 044/135] perf: index `dn_detail` in `Delivery Note Item` --- .../doctype/delivery_note_item/delivery_note_item.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 612d674e01..6148950462 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -725,7 +725,8 @@ "label": "Against Delivery Note Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "stock_qty_sec_break", @@ -892,7 +893,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-07-26 12:53:49.357171", + "modified": "2023-10-16 16:18:18.013379", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", @@ -902,4 +903,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file From d2096cfdb752bff03f8d3a00262d86c9eeb76c37 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 16 Oct 2023 16:53:43 +0530 Subject: [PATCH 045/135] fix: keep customer/supplier website role by default --- erpnext/setup/install.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 85eaf5fa92..b106cfcc1a 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -33,6 +33,7 @@ def after_install(): add_app_name() setup_log_settings() hide_workspaces() + update_roles() frappe.db.commit() @@ -232,6 +233,12 @@ def hide_workspaces(): frappe.db.set_value("Workspace", ws, "public", 0) +def update_roles(): + website_user_roles = ("Customer", "Supplier") + for role in website_user_roles: + frappe.db.set_value("Role", role, "desk_access", 0) + + def create_default_role_profiles(): for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items(): role_profile = frappe.new_doc("Role Profile") From 27a1e3bf834cb87f322d5943f8ef5405b0c30801 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 19:15:18 +0530 Subject: [PATCH 046/135] feat: validate negative stock for inventory dimension (backport #37373) (#37383) * feat: validate negative stock for inventory dimension (#37373) * feat: validate negative stock for inventory dimension * test: test case for validate negative stock for inv dimension (cherry picked from commit 1480acabb0faeae61c7c055bb7d1e81877b87cfb) # Conflicts: # erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py # erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py # erpnext/stock/stock_ledger.py * chore: fix conflicts * chore: fix conflicts * chore: fix conflicts * chore: fix linter issue * chore: fix linter issue * chore: fix linter issue * chore: fix linter issue * chore: fix linter issue --------- Co-authored-by: rohitwaghchaure --- .../inventory_dimension.js | 2 +- .../inventory_dimension.json | 14 +++- .../inventory_dimension.py | 5 +- .../test_inventory_dimension.py | 67 ++++++++++++++++++ .../stock_ledger_entry/stock_ledger_entry.py | 69 ++++++++++++++++++- .../stock_reconciliation.py | 43 +++++++++++- erpnext/stock/stock_ledger.py | 20 +++++- erpnext/stock/utils.py | 9 ++- 8 files changed, 218 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index 0310682a2c..35d1c02719 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', { if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger && frm.doc.__onload.has_stock_ledger.length) { let allow_to_edit_fields = ['disabled', 'fetch_from_parent', - 'type_of_transaction', 'condition', 'mandatory_depends_on']; + 'type_of_transaction', 'condition', 'mandatory_depends_on', 'validate_negative_stock']; frm.fields.forEach((field) => { if (!in_list(allow_to_edit_fields, field.df.fieldname)) { diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index eb6102a436..0e4055251f 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -17,6 +17,8 @@ "target_fieldname", "applicable_for_documents_tab", "apply_to_all_doctypes", + "column_break_niy2u", + "validate_negative_stock", "column_break_13", "document_type", "type_of_transaction", @@ -173,11 +175,21 @@ "fieldname": "reqd", "fieldtype": "Check", "label": "Mandatory" + }, + { + "fieldname": "column_break_niy2u", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "validate_negative_stock", + "fieldtype": "Check", + "label": "Validate Negative Stock" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-01-31 13:44:38.507698", + "modified": "2023-10-05 12:52:18.705431", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 8bff4d5147..257d18fc33 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -60,6 +60,7 @@ class InventoryDimension(Document): "fetch_from_parent", "type_of_transaction", "condition", + "validate_negative_stock", ] for field in frappe.get_meta("Inventory Dimension").fields: @@ -160,6 +161,7 @@ class InventoryDimension(Document): insert_after="inventory_dimension", options=self.reference_document, label=label, + search_index=1, reqd=self.reqd, mandatory_depends_on=self.mandatory_depends_on, ), @@ -255,7 +257,7 @@ def field_exists(doctype, fieldname) -> str or None: def get_inventory_documents( doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None ): - and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No"]]] + and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No", "Item Price"]]] or_filters = [ ["DocField", "options", "in", ["Batch", "Serial No"]], ["DocField", "parent", "in", ["Putaway Rule"]], @@ -340,6 +342,7 @@ def get_inventory_dimensions(): fields=[ "distinct target_fieldname as fieldname", "reference_document as doctype", + "validate_negative_stock", ], filters={"disabled": 0}, ) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 2d273c66fa..33394e5a11 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -414,6 +414,53 @@ class TestInventoryDimension(FrappeTestCase): else: self.assertEqual(d.store, "Inter Transfer Store 2") + def test_validate_negative_stock_for_inventory_dimension(self): + frappe.local.inventory_dimensions = {} + item_code = "Test Negative Inventory Dimension Item" + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) + create_item(item_code) + + inv_dimension = create_inventory_dimension( + apply_to_all_doctypes=1, + dimension_name="Inv Site", + reference_document="Inv Site", + document_type="Inv Site", + validate_negative_stock=1, + ) + + warehouse = create_warehouse("Negative Stock Warehouse") + doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True) + + doc.items[0].to_inv_site = "Site 1" + doc.submit() + + site_name = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] + )[0].inv_site + + self.assertEqual(site_name, "Site 1") + + doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) + + doc.items[0].inv_site = "Site 1" + self.assertRaises(frappe.ValidationError, doc.submit) + + inv_dimension.reload() + inv_dimension.db_set("validate_negative_stock", 0) + frappe.local.inventory_dimensions = {} + + doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) + + doc.items[0].inv_site = "Site 1" + doc.submit() + self.assertEqual(doc.docstatus, 1) + + site_name = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] + )[0].inv_site + + self.assertEqual(site_name, "Site 1") + def get_voucher_sl_entries(voucher_no, fields): return frappe.get_all( @@ -504,6 +551,26 @@ def prepare_test_data(): } ).insert(ignore_permissions=True) + if not frappe.db.exists("DocType", "Inv Site"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Inv Site", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:site_name", + "fields": [{"label": "Site Name", "fieldname": "site_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for site in ["Site 1", "Site 2"]: + if not frappe.db.exists("Inv Site", site): + frappe.get_doc({"doctype": "Inv Site", "site_name": site}).insert(ignore_permissions=True) + def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 3ca4bad4e4..c1b205132c 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -5,14 +5,16 @@ from datetime import date import frappe -from frappe import _ +from frappe import _, bold from frappe.core.doctype.role.role import get_users from frappe.model.document import Document -from frappe.utils import add_days, cint, formatdate, get_datetime, getdate +from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.serial_batch_bundle import SerialBatchBundle +from erpnext.stock.stock_ledger import get_previous_sle class StockFreezeError(frappe.ValidationError): @@ -48,6 +50,69 @@ class StockLedgerEntry(Document): self.validate_and_set_fiscal_year() self.block_transactions_against_group_warehouse() self.validate_with_last_transaction_posting_time() + self.validate_inventory_dimension_negative_stock() + + def validate_inventory_dimension_negative_stock(self): + extra_cond = "" + kwargs = {} + + dimensions = self._get_inventory_dimensions() + if not dimensions: + return + + for dimension, values in dimensions.items(): + kwargs[dimension] = values.get("value") + extra_cond += f" and {dimension} = %({dimension})s" + + kwargs.update( + { + "item_code": self.item_code, + "warehouse": self.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "company": self.company, + } + ) + + sle = get_previous_sle(kwargs, extra_cond=extra_cond) + if sle: + flt_precision = cint(frappe.db.get_default("float_precision")) or 2 + diff = sle.qty_after_transaction + flt(self.actual_qty) + diff = flt(diff, flt_precision) + if diff < 0 and abs(diff) > 0.0001: + self.throw_validation_error(diff, dimensions) + + def throw_validation_error(self, diff, dimensions): + dimension_msg = _(", with the inventory {0}: {1}").format( + "dimensions" if len(dimensions) > 1 else "dimension", + ", ".join(f"{bold(d.doctype)} ({d.value})" for k, d in dimensions.items()), + ) + + msg = _( + "{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction." + ).format( + abs(diff), + frappe.get_desk_link("Item", self.item_code), + frappe.get_desk_link("Warehouse", self.warehouse), + dimension_msg, + self.posting_date, + self.posting_time, + frappe.get_desk_link(self.voucher_type, self.voucher_no), + ) + + frappe.throw(msg, title=_("Inventory Dimension Negative Stock")) + + def _get_inventory_dimensions(self): + inv_dimensions = get_inventory_dimensions() + inv_dimension_dict = {} + for dimension in inv_dimensions: + if not dimension.get("validate_negative_stock") or not self.get(dimension.fieldname): + continue + + dimension["value"] = self.get(dimension.fieldname) + inv_dimension_dict.setdefault(dimension.fieldname, dimension) + + return inv_dimension_dict def on_submit(self): self.check_stock_frozen_date() diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index e36d5769bd..98b4ffdfcf 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -12,6 +12,7 @@ import erpnext from erpnext.accounts.utils import get_company_default from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( get_available_serial_nos, ) @@ -50,6 +51,7 @@ class StockReconciliation(StockController): self.clean_serial_nos() self.set_total_qty_and_amount() self.validate_putaway_capacity() + self.validate_inventory_dimension() if self._action == "submit": self.validate_reserved_stock() @@ -57,6 +59,17 @@ class StockReconciliation(StockController): def on_update(self): self.set_serial_and_batch_bundle(ignore_validate=True) + def validate_inventory_dimension(self): + dimensions = get_inventory_dimensions() + for dimension in dimensions: + for row in self.items: + if not row.batch_no and row.current_qty and row.get(dimension.get("fieldname")): + frappe.throw( + _( + "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries." + ).format(row.idx, bold(dimension.get("doctype"))) + ) + def on_submit(self): self.update_stock_ledger() self.make_gl_entries() @@ -202,8 +215,19 @@ class StockReconciliation(StockController): self.calculate_difference_amount(item, bundle_data) return True + inventory_dimensions_dict = {} + if not item.batch_no and not item.serial_no: + for dimension in get_inventory_dimensions(): + if item.get(dimension.get("fieldname")): + inventory_dimensions_dict[dimension.get("fieldname")] = item.get(dimension.get("fieldname")) + item_dict = get_stock_balance_for( - item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no + item.item_code, + item.warehouse, + self.posting_date, + self.posting_time, + batch_no=item.batch_no, + inventory_dimensions_dict=inventory_dimensions_dict, ) if (item.qty is None or item.qty == item_dict.get("qty")) and ( @@ -507,7 +531,13 @@ class StockReconciliation(StockController): if not row.batch_no: data.qty_after_transaction = flt(row.qty, row.precision("qty")) - if self.docstatus == 2: + dimensions = get_inventory_dimensions() + has_dimensions = False + for dimension in dimensions: + if row.get(dimension.get("fieldname")): + has_dimensions = True + + if self.docstatus == 2 and (not row.batch_no or not row.serial_and_batch_bundle): if row.current_qty: data.actual_qty = -1 * row.current_qty data.qty_after_transaction = flt(row.current_qty) @@ -523,6 +553,13 @@ class StockReconciliation(StockController): data.valuation_rate = flt(row.valuation_rate) data.stock_value_difference = -1 * flt(row.amount_difference) + elif ( + self.docstatus == 1 and has_dimensions and (not row.batch_no or not row.serial_and_batch_bundle) + ): + data.actual_qty = row.qty + data.qty_after_transaction = 0.0 + data.incoming_rate = flt(row.valuation_rate) + self.update_inventory_dimensions(row, data) return data @@ -911,6 +948,7 @@ def get_stock_balance_for( posting_time, batch_no: Optional[str] = None, with_valuation_rate: bool = True, + inventory_dimensions_dict=None, ): frappe.has_permission("Stock Reconciliation", "write", throw=True) @@ -939,6 +977,7 @@ def get_stock_balance_for( posting_time, with_valuation_rate=with_valuation_rate, with_serial_no=has_serial_no, + inventory_dimensions_dict=inventory_dimensions_dict, ) if has_serial_no: diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index d3807b0f97..48119b8d1f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -24,6 +24,7 @@ from frappe.utils import ( import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock, ) @@ -711,10 +712,17 @@ class update_entries_after(object): ): sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle) + dimensions = get_inventory_dimensions() + has_dimensions = False + if dimensions: + for dimension in dimensions: + if sle.get(dimension.get("fieldname")): + has_dimensions = True + if sle.serial_and_batch_bundle: self.calculate_valuation_for_serial_batch_bundle(sle) else: - if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: + if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions: # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction @@ -1297,7 +1305,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc return sle[0] if sle else frappe._dict() -def get_previous_sle(args, for_update=False): +def get_previous_sle(args, for_update=False, extra_cond=None): """ get the last sle on or before the current time-bucket, to get actual qty before transaction, this function @@ -1312,7 +1320,9 @@ def get_previous_sle(args, for_update=False): } """ args["name"] = args.get("sle", None) or "" - sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update) + sle = get_stock_ledger_entries( + args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond + ) return sle and sle[0] or {} @@ -1324,6 +1334,7 @@ def get_stock_ledger_entries( for_update=False, debug=False, check_serial_no=True, + extra_cond=None, ): """get stock ledger entries filtered by specific posting datetime conditions""" conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format( @@ -1361,6 +1372,9 @@ def get_stock_ledger_entries( if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" + if extra_cond: + conditions += f"{extra_cond}" + return frappe.db.sql( """ select *, timestamp(posting_date, posting_time) as "timestamp" diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 02444064c1..bd0d4697c9 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -95,6 +95,7 @@ def get_stock_balance( posting_time=None, with_valuation_rate=False, with_serial_no=False, + inventory_dimensions_dict=None, ): """Returns stock balance quantity at given warehouse on given posting date or current date. @@ -114,7 +115,13 @@ def get_stock_balance( "posting_time": posting_time, } - last_entry = get_previous_sle(args) + extra_cond = "" + if inventory_dimensions_dict: + for field, value in inventory_dimensions_dict.items(): + args[field] = value + extra_cond += f" and {field} = %({field})s" + + last_entry = get_previous_sle(args, extra_cond=extra_cond) if with_valuation_rate: if with_serial_no: From 8a72f4f58aee90e7155105b7275113555e788401 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Mon, 16 Oct 2023 18:12:10 -0300 Subject: [PATCH 047/135] fix: billed_qty to show a sum of all invoiced qty from the purchase order item. --- .../report/purchase_order_analysis/purchase_order_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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"), From fd6aee15e6bf3b7ea18487ab1b24b0a77526ac85 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 17 Oct 2023 12:48:07 +0530 Subject: [PATCH 048/135] fix(test): project test case --- erpnext/projects/doctype/task_depends_on/task_depends_on.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/task_depends_on/task_depends_on.json b/erpnext/projects/doctype/task_depends_on/task_depends_on.json index 5102986f00..3300b7eb90 100644 --- a/erpnext/projects/doctype/task_depends_on/task_depends_on.json +++ b/erpnext/projects/doctype/task_depends_on/task_depends_on.json @@ -24,6 +24,7 @@ }, { "fetch_from": "task.subject", + "fetch_if_empty": 1, "fieldname": "subject", "fieldtype": "Text", "in_list_view": 1, @@ -31,7 +32,6 @@ "read_only": 1 }, { - "fetch_from": "task.project", "fieldname": "project", "fieldtype": "Text", "label": "Project", @@ -40,7 +40,7 @@ ], "istable": 1, "links": [], - "modified": "2023-10-09 11:34:14.335853", + "modified": "2023-10-17 12:45:21.536165", "modified_by": "Administrator", "module": "Projects", "name": "Task Depends On", From 0b85a525fb36b2d72baa9cf039205a0550c28f25 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Oct 2023 13:28:56 +0530 Subject: [PATCH 049/135] fix: GL Entries for receiving non CWIP assets using Purchase Receipt --- .../purchase_receipt/purchase_receipt.py | 560 ++++++++---------- 1 file changed, 260 insertions(+), 300 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 6afa86e34e..de04381122 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -144,8 +144,8 @@ class PurchaseReceipt(BuyingController): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): # check cwip accounts before making auto assets # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account - arbnb_account = self.get_company_default("asset_received_but_not_billed") - cwip_account = get_asset_account( + self.get_company_default("asset_received_but_not_billed") + get_asset_account( "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company ) break @@ -313,7 +313,7 @@ class PurchaseReceipt(BuyingController): self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) self.make_tax_gl_entries(gl_entries) - self.get_asset_gl_entry(gl_entries) + # self.get_asset_gl_entry(gl_entries) return process_gl_map(gl_entries) @@ -322,11 +322,20 @@ class PurchaseReceipt(BuyingController): get_purchase_document_details, ) - stock_rbnb = None + is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) + stock_asset_rbnb = None + stock_asset_account_name = None + remarks = self.get("remarks") or _("Accounting Entry for {0}").format( + "Asset" if is_asset_pr else "Stock" + ) + if erpnext.is_perpetual_inventory_enabled(self.company): - stock_rbnb = self.get_company_default("stock_received_but_not_billed") + stock_asset_rbnb = ( + self.get_company_default("asset_received_but_not_billed") + if is_asset_pr + else self.get_company_default("stock_received_but_not_billed") + ) landed_cost_entries = get_item_account_wise_additional_cost(self.name) - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") warehouse_with_no_account = [] stock_items = self.get_stock_items() @@ -336,10 +345,243 @@ class PurchaseReceipt(BuyingController): ) ) + supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") + supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( + "account_currency" + ) exchange_rate_map, net_rate_map = get_purchase_document_details(self) + def make_item_asset_inward_entries(item): + if d.is_fixed_asset: + stock_asset_account_name = ( + get_asset_category_account( + asset_category=item.asset_category, + fieldname="capital_work_in_progress_account", + company=self.company, + ) + if is_cwip_accounting_enabled(d.asset_category) + else get_asset_category_account( + asset_category=item.asset_category, fieldname="fixed_asset_account", company=self.company + ) + ) + + stock_value_diff = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) + elif flt(item.valuation_rate) and flt(item.qty): + # If PR is sub-contracted and fg item rate is zero + # in that case if account for source and target warehouse are same, + # then GL entries should not be posted + if ( + flt(stock_value_diff) == flt(d.rm_supp_cost) + and warehouse_account.get(self.supplier_warehouse) + and stock_asset_account_name == supplier_warehouse_account + ): + return + + stock_asset_account_name = warehouse_account[item.warehouse]["account"] + + account_currency = get_account_currency(stock_asset_account_name) + self.add_gl_entry( + gl_entries=gl_entries, + account=stock_asset_account_name, + cost_center=d.cost_center, + debit=stock_value_diff, + credit=0.0, + remarks=remarks, + against_account=stock_asset_rbnb, + account_currency=account_currency, + item=item, + ) + + def make_stock_received_but_not_billed_entry(item, outgoing_amount): + # GL Entry for from warehouse or Stock Received but not billed + # Intentionally passed negative debit amount to avoid incorrect GL Entry validation + credit_amount = ( + flt(item.base_net_amount, item.precision("base_net_amount")) + if credit_currency == self.company_currency + else flt(item.net_amount, item.precision("net_amount")) + ) + + if self.is_internal_transfer() and item.valuation_rate: + outgoing_amount = abs( + frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": self.name, + "voucher_detail_no": item.name, + "warehouse": item.from_warehouse, + "is_cancelled": 0, + }, + "stock_value_difference", + ) + ) + credit_amount = outgoing_amount + + if credit_amount: + account = ( + warehouse_account[item.from_warehouse]["account"] if item.from_warehouse else stock_asset_rbnb + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=item.cost_center, + debit=-1 * flt(outgoing_amount, item.precision("base_net_amount")), + credit=0.0, + remarks=remarks, + against_account=stock_asset_account_name, + debit_in_account_currency=-1 * credit_amount, + account_currency=credit_currency, + item=item, + ) + + # check if the exchange rate has changed + if d.get("purchase_invoice"): + if ( + exchange_rate_map[item.purchase_invoice] + and self.conversion_rate != exchange_rate_map[item.purchase_invoice] + and item.net_rate == net_rate_map[item.purchase_invoice_item] + ): + + discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * ( + exchange_rate_map[item.purchase_invoice] - self.conversion_rate + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=item.cost_center, + debit=0.0, + credit=discrepancy_caused_by_exchange_rate_difference, + remarks=remarks, + against_account=self.supplier, + debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=credit_currency, + item=item, + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=self.get_company_default("exchange_gain_loss_account"), + cost_center=d.cost_center, + debit=discrepancy_caused_by_exchange_rate_difference, + credit=0.0, + remarks=remarks, + against_account=self.supplier, + debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=credit_currency, + item=item, + ) + + def make_landed_cost_gl_entries(item): + # Amount added through landed-cos-voucher + if item.landed_cost_voucher_amount and landed_cost_entries: + if (item.item_code, item.name) in landed_cost_entries: + for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): + account_currency = get_account_currency(account) + credit_amount = ( + flt(amount["base_amount"]) + if (amount["base_amount"] or account_currency != self.company_currency) + else flt(amount["amount"]) + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=item.cost_center, + debit=0.0, + credit=credit_amount, + remarks=remarks, + against_account=stock_asset_account_name, + credit_in_account_currency=flt(amount["amount"]), + account_currency=account_currency, + project=item.project, + item=item, + ) + + def make_rate_difference_entry(item): + if item.rate_difference_with_purchase_invoice and stock_asset_rbnb: + account_currency = get_account_currency(stock_asset_rbnb) + self.add_gl_entry( + gl_entries=gl_entries, + account=stock_asset_rbnb, + cost_center=item.cost_center, + debit=0.0, + credit=flt(item.rate_difference_with_purchase_invoice), + remarks=_("Adjustment based on Purchase Invoice rate"), + against_account=stock_asset_account_name, + account_currency=account_currency, + project=item.project, + item=item, + ) + + def make_sub_contracting_gl_entries(item): + # sub-contracting warehouse + if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): + self.add_gl_entry( + gl_entries=gl_entries, + account=supplier_warehouse_account, + cost_center=item.cost_center, + debit=0.0, + credit=flt(item.rm_supp_cost), + remarks=remarks, + against_account=stock_asset_account_name, + account_currency=supplier_warehouse_account_currency, + item=item, + ) + + def make_divisional_loss_gl_entry(item): + if item.is_fixed_asset: + return + + # divisional loss adjustment + expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + valuation_amount_as_per_doc = ( + flt(outgoing_amount, d.precision("base_net_amount")) + + flt(item.landed_cost_voucher_amount) + + flt(item.rm_supp_cost) + + flt(item.item_tax_amount) + + flt(item.rate_difference_with_purchase_invoice) + ) + + divisional_loss = flt( + valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount") + ) + + if divisional_loss: + if self.is_return or flt(item.item_tax_amount): + loss_account = expenses_included_in_valuation + else: + loss_account = ( + self.get_company_default("default_expense_account", ignore_validation=True) + or stock_asset_rbnb + ) + + cost_center = item.cost_center or frappe.get_cached_value( + "Company", self.company, "cost_center" + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=loss_account, + cost_center=cost_center, + debit=divisional_loss, + credit=0.0, + remarks=remarks, + against_account=stock_asset_account_name, + account_currency=credit_currency, + project=item.project, + item=item, + ) + for d in self.get("items"): - if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): + if d.item_code in stock_items or d.is_fixed_asset: + credit_currency = ( + get_account_currency(warehouse_account[d.from_warehouse]["account"]) + if d.from_warehouse + else get_account_currency(stock_asset_rbnb) + ) + if warehouse_account.get(d.warehouse): stock_value_diff = frappe.db.get_value( "Stock Ledger Entry", @@ -353,213 +595,13 @@ class PurchaseReceipt(BuyingController): "stock_value_difference", ) - warehouse_account_name = warehouse_account[d.warehouse]["account"] - warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"] - supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") - supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( - "account_currency" - ) - remarks = self.get("remarks") or _("Accounting Entry for Stock") - - # If PR is sub-contracted and fg item rate is zero - # in that case if account for source and target warehouse are same, - # then GL entries should not be posted - if ( - flt(stock_value_diff) == flt(d.rm_supp_cost) - and warehouse_account.get(self.supplier_warehouse) - and warehouse_account_name == supplier_warehouse_account - ): - continue - - self.add_gl_entry( - gl_entries=gl_entries, - account=warehouse_account_name, - cost_center=d.cost_center, - debit=stock_value_diff, - credit=0.0, - remarks=remarks, - against_account=stock_rbnb, - account_currency=warehouse_account_currency, - item=d, - ) - - # GL Entry for from warehouse or Stock Received but not billed - # Intentionally passed negative debit amount to avoid incorrect GL Entry validation - credit_currency = ( - get_account_currency(warehouse_account[d.from_warehouse]["account"]) - if d.from_warehouse - else get_account_currency(stock_rbnb) - ) - - credit_amount = ( - flt(d.base_net_amount, d.precision("base_net_amount")) - if credit_currency == self.company_currency - else flt(d.net_amount, d.precision("net_amount")) - ) - - outgoing_amount = d.base_net_amount - if self.is_internal_transfer() and d.valuation_rate: - outgoing_amount = abs( - frappe.db.get_value( - "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": self.name, - "voucher_detail_no": d.name, - "warehouse": d.from_warehouse, - "is_cancelled": 0, - }, - "stock_value_difference", - ) - ) - credit_amount = outgoing_amount - - if credit_amount: - account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb - - self.add_gl_entry( - gl_entries=gl_entries, - account=account, - cost_center=d.cost_center, - debit=-1 * flt(outgoing_amount, d.precision("base_net_amount")), - credit=0.0, - remarks=remarks, - against_account=warehouse_account_name, - debit_in_account_currency=-1 * credit_amount, - account_currency=credit_currency, - item=d, - ) - - # check if the exchange rate has changed - if d.get("purchase_invoice"): - if ( - exchange_rate_map[d.purchase_invoice] - and self.conversion_rate != exchange_rate_map[d.purchase_invoice] - and d.net_rate == net_rate_map[d.purchase_invoice_item] - ): - - discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * ( - exchange_rate_map[d.purchase_invoice] - self.conversion_rate - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=account, - cost_center=d.cost_center, - debit=0.0, - credit=discrepancy_caused_by_exchange_rate_difference, - remarks=remarks, - against_account=self.supplier, - debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, - account_currency=credit_currency, - item=d, - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=self.get_company_default("exchange_gain_loss_account"), - cost_center=d.cost_center, - debit=discrepancy_caused_by_exchange_rate_difference, - credit=0.0, - remarks=remarks, - against_account=self.supplier, - debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, - account_currency=credit_currency, - item=d, - ) - - # Amount added through landed-cos-voucher - if d.landed_cost_voucher_amount and landed_cost_entries: - if (d.item_code, d.name) in landed_cost_entries: - for account, amount in landed_cost_entries[(d.item_code, d.name)].items(): - account_currency = get_account_currency(account) - credit_amount = ( - flt(amount["base_amount"]) - if (amount["base_amount"] or account_currency != self.company_currency) - else flt(amount["amount"]) - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=account, - cost_center=d.cost_center, - debit=0.0, - credit=credit_amount, - remarks=remarks, - against_account=warehouse_account_name, - credit_in_account_currency=flt(amount["amount"]), - account_currency=account_currency, - project=d.project, - item=d, - ) - - if d.rate_difference_with_purchase_invoice and stock_rbnb: - account_currency = get_account_currency(stock_rbnb) - self.add_gl_entry( - gl_entries=gl_entries, - account=stock_rbnb, - cost_center=d.cost_center, - debit=0.0, - credit=flt(d.rate_difference_with_purchase_invoice), - remarks=_("Adjustment based on Purchase Invoice rate"), - against_account=warehouse_account_name, - account_currency=account_currency, - project=d.project, - item=d, - ) - - # sub-contracting warehouse - if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): - self.add_gl_entry( - gl_entries=gl_entries, - account=supplier_warehouse_account, - cost_center=d.cost_center, - debit=0.0, - credit=flt(d.rm_supp_cost), - remarks=remarks, - against_account=warehouse_account_name, - account_currency=supplier_warehouse_account_currency, - item=d, - ) - - # divisional loss adjustment - valuation_amount_as_per_doc = ( - flt(outgoing_amount, d.precision("base_net_amount")) - + flt(d.landed_cost_voucher_amount) - + flt(d.rm_supp_cost) - + flt(d.item_tax_amount) - + flt(d.rate_difference_with_purchase_invoice) - ) - - divisional_loss = flt( - valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount") - ) - - if divisional_loss: - if self.is_return or flt(d.item_tax_amount): - loss_account = expenses_included_in_valuation - else: - loss_account = ( - self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb - ) - - cost_center = d.cost_center or frappe.get_cached_value( - "Company", self.company, "cost_center" - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=loss_account, - cost_center=cost_center, - debit=divisional_loss, - credit=0.0, - remarks=remarks, - against_account=warehouse_account_name, - account_currency=credit_currency, - project=d.project, - item=d, - ) - + outgoing_amount = d.base_net_amount + d.item_tax_amount + make_item_asset_inward_entries(d) + make_stock_received_but_not_billed_entry(d, outgoing_amount) + make_landed_cost_gl_entries(d) + make_rate_difference_entry(d) + make_sub_contracting_gl_entries(d) + make_divisional_loss_gl_entry(d) elif ( d.warehouse not in warehouse_with_no_account or d.rejected_warehouse not in warehouse_with_no_account @@ -576,6 +618,9 @@ class PurchaseReceipt(BuyingController): d, gl_entries, self.posting_date, d.get("provisional_expense_account") ) + if d.is_fixed_asset: + self.update_assets(d, d.valuation_rate) + if warehouse_with_no_account: frappe.msgprint( _("No accounting entries for the following warehouses") @@ -709,94 +754,9 @@ class PurchaseReceipt(BuyingController): self.add_lcv_gl_entries(item, gl_entries) # update assets gross amount by its valuation rate # valuation rate is total of net rate, raw mat supp cost, tax amount, lcv amount per item - self.update_assets(item, item.valuation_rate) + return gl_entries - def add_asset_gl_entries(self, item, gl_entries): - arbnb_account = self.get_company_default("asset_received_but_not_billed") - # This returns category's cwip account if not then fallback to company's default cwip account - cwip_account = get_asset_account( - "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company - ) - - 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) - remarks = self.get("remarks") or _("Accounting Entry for Asset") - - cwip_account_currency = get_account_currency(cwip_account) - # debit cwip account - debit_in_account_currency = ( - base_asset_amount if cwip_account_currency == self.company_currency else asset_amount - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=cwip_account, - cost_center=item.cost_center, - debit=base_asset_amount, - credit=0.0, - remarks=remarks, - against_account=arbnb_account, - debit_in_account_currency=debit_in_account_currency, - item=item, - ) - - asset_rbnb_currency = get_account_currency(arbnb_account) - # credit arbnb account - credit_in_account_currency = ( - base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=arbnb_account, - cost_center=item.cost_center, - debit=0.0, - credit=base_asset_amount, - remarks=remarks, - against_account=cwip_account, - credit_in_account_currency=credit_in_account_currency, - item=item, - ) - - def add_lcv_gl_entries(self, item, gl_entries): - expenses_included_in_asset_valuation = self.get_company_default( - "expenses_included_in_asset_valuation" - ) - if not is_cwip_accounting_enabled(item.asset_category): - asset_account = get_asset_category_account( - asset_category=item.asset_category, fieldname="fixed_asset_account", company=self.company - ) - else: - # This returns company's default cwip account - asset_account = get_asset_account("capital_work_in_progress_account", company=self.company) - - remarks = self.get("remarks") or _("Accounting Entry for Stock") - - self.add_gl_entry( - gl_entries=gl_entries, - account=expenses_included_in_asset_valuation, - cost_center=item.cost_center, - debit=0.0, - credit=flt(item.landed_cost_voucher_amount), - remarks=remarks, - against_account=asset_account, - project=item.project, - item=item, - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=asset_account, - cost_center=item.cost_center, - debit=flt(item.landed_cost_voucher_amount), - credit=0.0, - remarks=remarks, - against_account=expenses_included_in_asset_valuation, - project=item.project, - item=item, - ) - def update_assets(self, item, valuation_rate): assets = frappe.db.get_all( "Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code} From f900a78995e20958828afcbd5ab8bb515fd2e075 Mon Sep 17 00:00:00 2001 From: Sabu Siyad Date: Tue, 17 Oct 2023 17:05:44 +0530 Subject: [PATCH 050/135] refactor!: drop ecommerce in favor of webshop (#33265) * refactor!: remove ecommerce item group field check Signed-off-by: Sabu Siyad * refactor!: remove `e_commerce` directory Signed-off-by: Sabu Siyad * refactor!: remove `get_context` from `item_group` https://frappeframework.com/docs/v14/user/en/guides/portal-development/context Signed-off-by: Sabu Siyad * refactor!: remove related `./templates` Signed-off-by: Sabu Siyad * refactor!(navbar): remove wishlist (ecommerce) Signed-off-by: Sabu Siyad * refactor!(js): remove js from scripts Signed-off-by: Sabu Siyad * refactor!: remove `www/all-products` Signed-off-by: Sabu Siyad * refactor!: remove pages and js Signed-off-by: Sabu Siyad * refactor!: remove js/customer_reviews Signed-off-by: Sabu Siyad * refactor!(portal utils): remove shopping cart debtor account Signed-off-by: Sabu Siyad * refactor!: remove e_commerce events from hooks Signed-off-by: Sabu Siyad * refactor!(web): remove e_commerce js from bundle Signed-off-by: Sabu Siyad * refactor!(setup): remove shopping cart setup Signed-off-by: Sabu Siyad * refactor!: remove pages Signed-off-by: Sabu Siyad * refactor(item): remove website item button Signed-off-by: Sabu Siyad * refactor!(payment request): remove `on_payment_authorized` Signed-off-by: Sabu Siyad * refactor!: @staticmethod `get_gateway_details` to avoid monkey patching, in custom apps https://discuss.erpnext.com/t/how-to-override-method-in-frappe/28786/36 Signed-off-by: Sabu Siyad * refactor!(pages): remove product page Signed-off-by: Sabu Siyad * refactor!(homepage): do not setup website items Signed-off-by: Sabu Siyad * refactor(workspace): remove link to ecommerce settings Signed-off-by: Sabu Siyad * refactor!(www): remove shop-by-category Signed-off-by: Sabu Siyad * refactor!(homepage): remove featured product Signed-off-by: Sabu Siyad * refactor: remove products in homepage Signed-off-by: Sabu Siyad * refactor(homepage): remove explore button Signed-off-by: Sabu Siyad * refactor: remove products fields from homepage Signed-off-by: Sabu Siyad * Revert "refactor!: @staticmethod `get_gateway_details`" This reverts commit 561bcd96680a930bb92627869502d9346b10611b. Signed-off-by: Sabu Siyad * refactor!: remove payment gateway e_commerce import Signed-off-by: Sabu Siyad * chore: pre-commit Signed-off-by: Sabu Siyad * refactor!: pass `party` into `get_price` Signed-off-by: Sabu Siyad * refactor: move `get_item_codes_by_attributes` to `utilities/product` Signed-off-by: Sabu Siyad * refactor!(quotation): input customer group Signed-off-by: Sabu Siyad * chore: pre-commit * refactor: remove custom `navbar_items.html` * refactor!(item): remove `published_in_website` * refactor: move `validate_duplicate_website_item` before rename * test: remove `test_shopping_cart_without_website_item` * chore: add doctype drop patch * refactor: removed website item related code * refactor: removed shopping_cart code * refactor: removed e-commerce related patches * refactor: removed website related fields from item group * fix: patch create_asset_depreciation_schedules_from_assets, KeyError: '0K BU64 AUY' --------- Signed-off-by: Sabu Siyad Co-authored-by: Rohit Waghchaure --- .../payment_request/payment_request.py | 46 +- .../subscription_plan/subscription_plan.py | 3 +- erpnext/accounts/doctype/tax_rule/tax_rule.py | 18 +- erpnext/controllers/item_variant.py | 29 +- erpnext/e_commerce/__init__.py | 0 erpnext/e_commerce/api.py | 81 - erpnext/e_commerce/doctype/__init__.py | 0 .../doctype/e_commerce_settings/__init__.py | 0 .../e_commerce_settings.js | 58 - .../e_commerce_settings.json | 395 ----- .../e_commerce_settings.py | 185 --- .../test_e_commerce_settings.py | 53 - .../doctype/item_review/__init__.py | 0 .../doctype/item_review/item_review.js | 8 - .../doctype/item_review/item_review.json | 134 -- .../doctype/item_review/item_review.py | 153 -- .../doctype/item_review/test_item_review.py | 84 - .../doctype/recommended_items/__init__.py | 0 .../recommended_items/recommended_items.json | 88 -- .../recommended_items/recommended_items.py | 9 - .../doctype/website_item/__init__.py | 0 .../website_item/templates/website_item.html | 7 - .../templates/website_item_row.html | 4 - .../doctype/website_item/test_website_item.py | 564 ------- .../doctype/website_item/website_item.js | 37 - .../doctype/website_item/website_item.json | 414 ----- .../doctype/website_item/website_item.py | 469 ------ .../doctype/website_item/website_item_list.js | 20 - .../website_item_tabbed_section/__init__.py | 0 .../website_item_tabbed_section.json | 37 - .../website_item_tabbed_section.py | 10 - .../doctype/website_offer/__init__.py | 0 .../doctype/website_offer/website_offer.json | 43 - .../doctype/website_offer/website_offer.py | 15 - .../e_commerce/doctype/wishlist/__init__.py | 0 .../doctype/wishlist/test_wishlist.py | 117 -- .../e_commerce/doctype/wishlist/wishlist.js | 8 - .../e_commerce/doctype/wishlist/wishlist.json | 65 - .../e_commerce/doctype/wishlist/wishlist.py | 70 - .../doctype/wishlist_item/__init__.py | 0 .../doctype/wishlist_item/wishlist_item.json | 147 -- .../doctype/wishlist_item/wishlist_item.py | 10 - erpnext/e_commerce/legacy_search.py | 134 -- .../e_commerce/product_data_engine/filters.py | 158 -- .../e_commerce/product_data_engine/query.py | 321 ---- .../test_item_group_product_data_engine.py | 170 -- .../test_product_data_engine.py | 348 ----- erpnext/e_commerce/product_ui/grid.js | 201 --- erpnext/e_commerce/product_ui/list.js | 205 --- erpnext/e_commerce/product_ui/search.js | 244 --- erpnext/e_commerce/product_ui/views.js | 548 ------- erpnext/e_commerce/redisearch_utils.py | 255 --- erpnext/e_commerce/shopping_cart/__init__.py | 0 erpnext/e_commerce/shopping_cart/cart.py | 721 --------- .../e_commerce/shopping_cart/product_info.py | 99 -- .../shopping_cart/test_shopping_cart.py | 398 ----- erpnext/e_commerce/shopping_cart/utils.py | 54 - .../e_commerce/variant_selector/__init__.py | 0 .../variant_selector/item_variants_cache.py | 130 -- .../variant_selector/test_variant_selector.py | 125 -- erpnext/e_commerce/variant_selector/utils.py | 251 --- erpnext/e_commerce/web_template/__init__.py | 0 .../web_template/hero_slider/__init__.py | 0 .../web_template/hero_slider/hero_slider.html | 86 - .../web_template/hero_slider/hero_slider.json | 288 ---- .../web_template/item_card_group/__init__.py | 0 .../item_card_group/item_card_group.html | 37 - .../item_card_group/item_card_group.json | 270 ---- .../web_template/product_card/__init__.py | 0 .../product_card/product_card.html | 0 .../product_card/product_card.json | 31 - .../product_category_cards/__init__.py | 0 .../product_category_cards.html | 47 - .../product_category_cards.json | 85 - erpnext/hooks.py | 15 +- erpnext/modules.txt | 3 +- erpnext/patches.txt | 10 +- ...ebsite_item_in_item_card_group_template.py | 60 - ...py_custom_field_filters_to_website_item.py | 94 -- erpnext/patches/v13_0/create_website_items.py | 85 - .../v13_0/fetch_thumbnail_in_website_items.py | 11 - .../make_homepage_products_website_items.py | 15 - .../v13_0/populate_e_commerce_settings.py | 68 - .../v13_0/shopping_cart_to_ecommerce.py | 29 - ...sset_depreciation_schedules_from_assets.py | 3 + .../v15_0/delete_ecommerce_doctypes.py | 30 + erpnext/portal/doctype/homepage/homepage.js | 9 - erpnext/portal/doctype/homepage/homepage.json | 27 +- erpnext/portal/doctype/homepage/homepage.py | 23 - .../homepage_featured_product/__init__.py | 0 .../homepage_featured_product.json | 118 -- .../homepage_featured_product.py | 9 - erpnext/portal/utils.py | 27 +- erpnext/public/js/customer_reviews.js | 138 -- erpnext/public/js/erpnext-web.bundle.js | 7 - erpnext/public/js/shopping_cart.js | 243 --- erpnext/public/js/wishlist.js | 204 --- erpnext/public/scss/erpnext-web.bundle.scss | 1 - erpnext/public/scss/shopping_cart.scss | 1381 ----------------- .../selling/doctype/quotation/quotation.py | 32 +- .../doctype/quotation/test_quotation.py | 9 - .../setup/doctype/item_group/item_group.js | 14 - .../setup/doctype/item_group/item_group.json | 104 +- .../setup/doctype/item_group/item_group.py | 146 +- .../setup_wizard/operations/company_setup.py | 14 - .../operations/install_fixtures.py | 15 - .../erpnext_settings/erpnext_settings.json | 998 ++++++------ erpnext/stock/doctype/item/item.js | 30 - erpnext/stock/doctype/item/item.json | 9 - erpnext/stock/doctype/item/item.py | 77 - erpnext/stock/doctype/item/item_dashboard.py | 1 - .../stock/doctype/price_list/price_list.py | 16 - erpnext/templates/generators/item/item.html | 80 - .../generators/item/item_add_to_cart.html | 180 --- .../generators/item/item_configure.html | 20 - .../generators/item/item_configure.js | 343 ---- .../generators/item/item_details.html | 63 - .../templates/generators/item/item_image.html | 108 -- .../generators/item/item_inquiry.html | 11 - .../templates/generators/item/item_inquiry.js | 77 - .../generators/item/item_reviews.html | 88 -- .../generators/item/item_specifications.html | 20 - erpnext/templates/generators/item_group.html | 72 - .../templates/includes/cart/address_card.html | 17 - .../includes/cart/address_picker_card.html | 12 - .../templates/includes/cart/cart_address.html | 189 --- .../includes/cart/cart_address_picker.html | 3 - .../includes/cart/cart_dropdown.html | 27 - .../templates/includes/cart/cart_items.html | 113 -- .../includes/cart/cart_items_dropdown.html | 12 - .../includes/cart/cart_items_total.html | 10 - .../templates/includes/cart/cart_macros.html | 22 - .../includes/cart/cart_payment_summary.html | 84 - .../includes/navbar/navbar_items.html | 22 - .../includes/order/order_macros.html | 52 - erpnext/templates/includes/product_page.js | 217 --- erpnext/templates/pages/cart.html | 132 -- erpnext/templates/pages/cart.js | 303 ---- erpnext/templates/pages/cart.py | 11 - erpnext/templates/pages/customer_reviews.html | 67 - erpnext/templates/pages/customer_reviews.py | 25 - erpnext/templates/pages/home.html | 23 - erpnext/templates/pages/home.py | 7 - erpnext/templates/pages/order.js | 42 - erpnext/templates/pages/order.py | 7 - erpnext/templates/pages/product_search.html | 32 - erpnext/templates/pages/product_search.py | 152 -- erpnext/templates/pages/wishlist.html | 28 - erpnext/templates/pages/wishlist.py | 81 - erpnext/utilities/product.py | 156 +- erpnext/www/all-products/__init__.py | 0 erpnext/www/all-products/index.html | 51 - erpnext/www/all-products/index.js | 27 - erpnext/www/all-products/index.py | 22 - erpnext/www/all-products/not_found.html | 1 - erpnext/www/shop-by-category/__init__.py | 0 .../category_card_section.html | 30 - erpnext/www/shop-by-category/index.html | 48 - erpnext/www/shop-by-category/index.js | 12 - erpnext/www/shop-by-category/index.py | 91 -- 160 files changed, 630 insertions(+), 15222 deletions(-) delete mode 100644 erpnext/e_commerce/__init__.py delete mode 100644 erpnext/e_commerce/api.py delete mode 100644 erpnext/e_commerce/doctype/__init__.py delete mode 100644 erpnext/e_commerce/doctype/e_commerce_settings/__init__.py delete mode 100644 erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js delete mode 100644 erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json delete mode 100644 erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py delete mode 100644 erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py delete mode 100644 erpnext/e_commerce/doctype/item_review/__init__.py delete mode 100644 erpnext/e_commerce/doctype/item_review/item_review.js delete mode 100644 erpnext/e_commerce/doctype/item_review/item_review.json delete mode 100644 erpnext/e_commerce/doctype/item_review/item_review.py delete mode 100644 erpnext/e_commerce/doctype/item_review/test_item_review.py delete mode 100644 erpnext/e_commerce/doctype/recommended_items/__init__.py delete mode 100644 erpnext/e_commerce/doctype/recommended_items/recommended_items.json delete mode 100644 erpnext/e_commerce/doctype/recommended_items/recommended_items.py delete mode 100644 erpnext/e_commerce/doctype/website_item/__init__.py delete mode 100644 erpnext/e_commerce/doctype/website_item/templates/website_item.html delete mode 100644 erpnext/e_commerce/doctype/website_item/templates/website_item_row.html delete mode 100644 erpnext/e_commerce/doctype/website_item/test_website_item.py delete mode 100644 erpnext/e_commerce/doctype/website_item/website_item.js delete mode 100644 erpnext/e_commerce/doctype/website_item/website_item.json delete mode 100644 erpnext/e_commerce/doctype/website_item/website_item.py delete mode 100644 erpnext/e_commerce/doctype/website_item/website_item_list.js delete mode 100644 erpnext/e_commerce/doctype/website_item_tabbed_section/__init__.py delete mode 100644 erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.json delete mode 100644 erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py delete mode 100644 erpnext/e_commerce/doctype/website_offer/__init__.py delete mode 100644 erpnext/e_commerce/doctype/website_offer/website_offer.json delete mode 100644 erpnext/e_commerce/doctype/website_offer/website_offer.py delete mode 100644 erpnext/e_commerce/doctype/wishlist/__init__.py delete mode 100644 erpnext/e_commerce/doctype/wishlist/test_wishlist.py delete mode 100644 erpnext/e_commerce/doctype/wishlist/wishlist.js delete mode 100644 erpnext/e_commerce/doctype/wishlist/wishlist.json delete mode 100644 erpnext/e_commerce/doctype/wishlist/wishlist.py delete mode 100644 erpnext/e_commerce/doctype/wishlist_item/__init__.py delete mode 100644 erpnext/e_commerce/doctype/wishlist_item/wishlist_item.json delete mode 100644 erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py delete mode 100644 erpnext/e_commerce/legacy_search.py delete mode 100644 erpnext/e_commerce/product_data_engine/filters.py delete mode 100644 erpnext/e_commerce/product_data_engine/query.py delete mode 100644 erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py delete mode 100644 erpnext/e_commerce/product_data_engine/test_product_data_engine.py delete mode 100644 erpnext/e_commerce/product_ui/grid.js delete mode 100644 erpnext/e_commerce/product_ui/list.js delete mode 100644 erpnext/e_commerce/product_ui/search.js delete mode 100644 erpnext/e_commerce/product_ui/views.js delete mode 100644 erpnext/e_commerce/redisearch_utils.py delete mode 100644 erpnext/e_commerce/shopping_cart/__init__.py delete mode 100644 erpnext/e_commerce/shopping_cart/cart.py delete mode 100644 erpnext/e_commerce/shopping_cart/product_info.py delete mode 100644 erpnext/e_commerce/shopping_cart/test_shopping_cart.py delete mode 100644 erpnext/e_commerce/shopping_cart/utils.py delete mode 100644 erpnext/e_commerce/variant_selector/__init__.py delete mode 100644 erpnext/e_commerce/variant_selector/item_variants_cache.py delete mode 100644 erpnext/e_commerce/variant_selector/test_variant_selector.py delete mode 100644 erpnext/e_commerce/variant_selector/utils.py delete mode 100644 erpnext/e_commerce/web_template/__init__.py delete mode 100644 erpnext/e_commerce/web_template/hero_slider/__init__.py delete mode 100644 erpnext/e_commerce/web_template/hero_slider/hero_slider.html delete mode 100644 erpnext/e_commerce/web_template/hero_slider/hero_slider.json delete mode 100644 erpnext/e_commerce/web_template/item_card_group/__init__.py delete mode 100644 erpnext/e_commerce/web_template/item_card_group/item_card_group.html delete mode 100644 erpnext/e_commerce/web_template/item_card_group/item_card_group.json delete mode 100644 erpnext/e_commerce/web_template/product_card/__init__.py delete mode 100644 erpnext/e_commerce/web_template/product_card/product_card.html delete mode 100644 erpnext/e_commerce/web_template/product_card/product_card.json delete mode 100644 erpnext/e_commerce/web_template/product_category_cards/__init__.py delete mode 100644 erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html delete mode 100644 erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json delete mode 100644 erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py delete mode 100644 erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py delete mode 100644 erpnext/patches/v13_0/create_website_items.py delete mode 100644 erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py delete mode 100644 erpnext/patches/v13_0/make_homepage_products_website_items.py delete mode 100644 erpnext/patches/v13_0/populate_e_commerce_settings.py delete mode 100644 erpnext/patches/v13_0/shopping_cart_to_ecommerce.py create mode 100644 erpnext/patches/v15_0/delete_ecommerce_doctypes.py delete mode 100644 erpnext/portal/doctype/homepage_featured_product/__init__.py delete mode 100644 erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json delete mode 100644 erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.py delete mode 100644 erpnext/public/js/customer_reviews.js delete mode 100644 erpnext/public/js/shopping_cart.js delete mode 100644 erpnext/public/js/wishlist.js delete mode 100644 erpnext/public/scss/shopping_cart.scss delete mode 100644 erpnext/templates/generators/item/item.html delete mode 100644 erpnext/templates/generators/item/item_add_to_cart.html delete mode 100644 erpnext/templates/generators/item/item_configure.html delete mode 100644 erpnext/templates/generators/item/item_configure.js delete mode 100644 erpnext/templates/generators/item/item_details.html delete mode 100644 erpnext/templates/generators/item/item_image.html delete mode 100644 erpnext/templates/generators/item/item_inquiry.html delete mode 100644 erpnext/templates/generators/item/item_inquiry.js delete mode 100644 erpnext/templates/generators/item/item_reviews.html delete mode 100644 erpnext/templates/generators/item/item_specifications.html delete mode 100644 erpnext/templates/generators/item_group.html delete mode 100644 erpnext/templates/includes/cart/address_card.html delete mode 100644 erpnext/templates/includes/cart/address_picker_card.html delete mode 100644 erpnext/templates/includes/cart/cart_address.html delete mode 100644 erpnext/templates/includes/cart/cart_address_picker.html delete mode 100644 erpnext/templates/includes/cart/cart_dropdown.html delete mode 100644 erpnext/templates/includes/cart/cart_items.html delete mode 100644 erpnext/templates/includes/cart/cart_items_dropdown.html delete mode 100644 erpnext/templates/includes/cart/cart_items_total.html delete mode 100644 erpnext/templates/includes/cart/cart_macros.html delete mode 100644 erpnext/templates/includes/cart/cart_payment_summary.html delete mode 100644 erpnext/templates/includes/navbar/navbar_items.html delete mode 100644 erpnext/templates/includes/order/order_macros.html delete mode 100644 erpnext/templates/includes/product_page.js delete mode 100644 erpnext/templates/pages/cart.html delete mode 100644 erpnext/templates/pages/cart.js delete mode 100644 erpnext/templates/pages/cart.py delete mode 100644 erpnext/templates/pages/customer_reviews.html delete mode 100644 erpnext/templates/pages/customer_reviews.py delete mode 100644 erpnext/templates/pages/order.js delete mode 100644 erpnext/templates/pages/product_search.html delete mode 100644 erpnext/templates/pages/product_search.py delete mode 100644 erpnext/templates/pages/wishlist.html delete mode 100644 erpnext/templates/pages/wishlist.py delete mode 100644 erpnext/www/all-products/__init__.py delete mode 100644 erpnext/www/all-products/index.html delete mode 100644 erpnext/www/all-products/index.js delete mode 100644 erpnext/www/all-products/index.py delete mode 100644 erpnext/www/all-products/not_found.html delete mode 100644 erpnext/www/shop-by-category/__init__.py delete mode 100644 erpnext/www/shop-by-category/category_card_section.html delete mode 100644 erpnext/www/shop-by-category/index.html delete mode 100644 erpnext/www/shop-by-category/index.js delete mode 100644 erpnext/www/shop-by-category/index.py diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index ce15bcff81..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 ( @@ -363,33 +359,6 @@ 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(): @@ -546,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/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/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/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( - `
${__("Follow these steps to create a landing page for your store")}: - - docs/store-landing-page - -
` - ); - } - - frappe.model.with_doctype("Website Item", () => { - const web_item_meta = frappe.get_meta('Website Item'); - - const valid_fields = web_item_meta.fields.filter(df => - ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden - ).map(df => - ({ label: df.label, value: df.fieldname }) - ); - - frm.get_field("filter_fields").grid.update_docfield_property( - 'fieldname', 'options', valid_fields - ); - }); - }, - enabled: function(frm) { - if (frm.doc.enabled === 1) { - frm.set_value('enable_variants', 1); - } - else { - frm.set_value('company', ''); - frm.set_value('price_list', ''); - frm.set_value('default_customer_group', ''); - frm.set_value('quotation_series', ''); - } - }, - - enable_checkout: function(frm) { - if (frm.doc.enable_checkout) { - erpnext.utils.check_payments_app(); - } - } -}); diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json deleted file mode 100644 index e6f08f708a..0000000000 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ /dev/null @@ -1,395 +0,0 @@ -{ - "actions": [], - "creation": "2021-02-10 17:13:39.139103", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "products_per_page", - "filter_categories_section", - "enable_field_filters", - "filter_fields", - "enable_attribute_filters", - "filter_attributes", - "display_settings_section", - "hide_variants", - "enable_variants", - "show_price", - "column_break_9", - "show_stock_availability", - "show_quantity_in_website", - "allow_items_not_in_stock", - "column_break_13", - "show_apply_coupon_code_in_website", - "show_contact_us_button", - "show_attachments", - "section_break_18", - "company", - "price_list", - "enabled", - "store_page_docs", - "column_break_21", - "default_customer_group", - "quotation_series", - "checkout_settings_section", - "enable_checkout", - "show_price_in_quotation", - "column_break_27", - "save_quotations_as_draft", - "payment_gateway_account", - "payment_success_url", - "add_ons_section", - "enable_wishlist", - "column_break_22", - "enable_reviews", - "column_break_23", - "enable_recommendations", - "item_search_settings_section", - "redisearch_warning", - "search_index_fields", - "is_redisearch_enabled", - "is_redisearch_loaded", - "shop_by_category_section", - "slideshow", - "guest_display_settings_section", - "hide_price_for_guest", - "redirect_on_action" - ], - "fields": [ - { - "default": "6", - "fieldname": "products_per_page", - "fieldtype": "Int", - "label": "Products per Page" - }, - { - "collapsible": 1, - "fieldname": "filter_categories_section", - "fieldtype": "Section Break", - "label": "Filters and Categories" - }, - { - "default": "0", - "fieldname": "hide_variants", - "fieldtype": "Check", - "label": "Hide Variants" - }, - { - "default": "0", - "description": "The field filters will also work as categories in the Shop by Category page.", - "fieldname": "enable_field_filters", - "fieldtype": "Check", - "label": "Enable Field Filters (Categories)" - }, - { - "default": "0", - "fieldname": "enable_attribute_filters", - "fieldtype": "Check", - "label": "Enable Attribute Filters" - }, - { - "depends_on": "enable_field_filters", - "fieldname": "filter_fields", - "fieldtype": "Table", - "label": "Website Item Fields", - "options": "Website Filter Field" - }, - { - "depends_on": "enable_attribute_filters", - "fieldname": "filter_attributes", - "fieldtype": "Table", - "label": "Attributes", - "options": "Website Attribute" - }, - { - "default": "0", - "fieldname": "enabled", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Enable Shopping Cart" - }, - { - "depends_on": "doc.enabled", - "fieldname": "store_page_docs", - "fieldtype": "HTML" - }, - { - "fieldname": "display_settings_section", - "fieldtype": "Section Break", - "label": "Display Settings" - }, - { - "default": "0", - "fieldname": "show_attachments", - "fieldtype": "Check", - "label": "Show Public Attachments" - }, - { - "default": "0", - "fieldname": "show_price", - "fieldtype": "Check", - "label": "Show Price" - }, - { - "default": "0", - "fieldname": "show_stock_availability", - "fieldtype": "Check", - "label": "Show Stock Availability" - }, - { - "default": "0", - "fieldname": "enable_variants", - "fieldtype": "Check", - "label": "Enable Variant Selection" - }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "show_contact_us_button", - "fieldtype": "Check", - "label": "Show Contact Us Button" - }, - { - "default": "0", - "depends_on": "show_stock_availability", - "fieldname": "show_quantity_in_website", - "fieldtype": "Check", - "label": "Show Stock Quantity" - }, - { - "default": "0", - "fieldname": "show_apply_coupon_code_in_website", - "fieldtype": "Check", - "label": "Show Apply Coupon Code" - }, - { - "default": "0", - "fieldname": "allow_items_not_in_stock", - "fieldtype": "Check", - "label": "Allow items not in stock to be added to cart" - }, - { - "fieldname": "section_break_18", - "fieldtype": "Section Break", - "label": "Shopping Cart" - }, - { - "depends_on": "enabled", - "fieldname": "company", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Company", - "mandatory_depends_on": "eval: doc.enabled === 1", - "options": "Company", - "remember_last_selected_value": 1 - }, - { - "depends_on": "enabled", - "description": "Prices will not be shown if Price List is not set", - "fieldname": "price_list", - "fieldtype": "Link", - "label": "Price List", - "mandatory_depends_on": "eval: doc.enabled === 1", - "options": "Price List" - }, - { - "fieldname": "column_break_21", - "fieldtype": "Column Break" - }, - { - "depends_on": "enabled", - "fieldname": "default_customer_group", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Customer Group", - "mandatory_depends_on": "eval: doc.enabled === 1", - "options": "Customer Group" - }, - { - "depends_on": "enabled", - "fieldname": "quotation_series", - "fieldtype": "Select", - "label": "Quotation Series", - "mandatory_depends_on": "eval: doc.enabled === 1" - }, - { - "collapsible": 1, - "collapsible_depends_on": "eval:doc.enable_checkout", - "depends_on": "enabled", - "fieldname": "checkout_settings_section", - "fieldtype": "Section Break", - "label": "Checkout Settings" - }, - { - "default": "0", - "fieldname": "enable_checkout", - "fieldtype": "Check", - "label": "Enable Checkout" - }, - { - "default": "Orders", - "depends_on": "enable_checkout", - "description": "After payment completion redirect user to selected page.", - "fieldname": "payment_success_url", - "fieldtype": "Select", - "label": "Payment Success Url", - "mandatory_depends_on": "enable_checkout", - "options": "\nOrders\nInvoices\nMy Account" - }, - { - "fieldname": "column_break_27", - "fieldtype": "Column Break" - }, - { - "default": "0", - "depends_on": "eval: doc.enable_checkout == 0", - "fieldname": "save_quotations_as_draft", - "fieldtype": "Check", - "label": "Save Quotations as Draft" - }, - { - "depends_on": "enable_checkout", - "fieldname": "payment_gateway_account", - "fieldtype": "Link", - "label": "Payment Gateway Account", - "mandatory_depends_on": "enable_checkout", - "options": "Payment Gateway Account" - }, - { - "collapsible": 1, - "depends_on": "enable_field_filters", - "fieldname": "shop_by_category_section", - "fieldtype": "Section Break", - "label": "Shop by Category" - }, - { - "fieldname": "slideshow", - "fieldtype": "Link", - "label": "Slideshow", - "options": "Website Slideshow" - }, - { - "collapsible": 1, - "fieldname": "add_ons_section", - "fieldtype": "Section Break", - "label": "Add-ons" - }, - { - "default": "0", - "fieldname": "enable_wishlist", - "fieldtype": "Check", - "label": "Enable Wishlist" - }, - { - "default": "0", - "fieldname": "enable_reviews", - "fieldtype": "Check", - "label": "Enable Reviews and Ratings" - }, - { - "fieldname": "search_index_fields", - "fieldtype": "Small Text", - "label": "Search Index Fields", - "mandatory_depends_on": "is_redisearch_enabled", - "read_only_depends_on": "eval:!doc.is_redisearch_loaded" - }, - { - "collapsible": 1, - "fieldname": "item_search_settings_section", - "fieldtype": "Section Break", - "label": "Item Search Settings" - }, - { - "default": "0", - "fieldname": "is_redisearch_loaded", - "fieldtype": "Check", - "hidden": 1, - "label": "Is Redisearch Loaded" - }, - { - "depends_on": "eval:!doc.is_redisearch_loaded", - "fieldname": "redisearch_warning", - "fieldtype": "HTML", - "label": "Redisearch Warning", - "options": "

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 %} -

{{ title }}

-{% endblock %} - - \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_item/templates/website_item_row.html b/erpnext/e_commerce/doctype/website_item/templates/website_item_row.html deleted file mode 100644 index d7014b453a..0000000000 --- a/erpnext/e_commerce/doctype/website_item/templates/website_item_row.html +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py deleted file mode 100644 index 2ba84c0500..0000000000 --- a/erpnext/e_commerce/doctype/website_item/test_website_item.py +++ /dev/null @@ -1,564 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -import frappe - -from erpnext.controllers.item_variant import create_variant -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( - get_shopping_cart_settings, -) -from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import ( - setup_e_commerce_settings, -) -from erpnext.e_commerce.doctype.website_item.website_item import make_website_item -from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website -from erpnext.stock.doctype.item.item import DataValidationError -from erpnext.stock.doctype.item.test_item import make_item - -WEBITEM_DESK_TESTS = ("test_website_item_desk_item_sync", "test_publish_variant_and_template") -WEBITEM_PRICE_TESTS = ( - "test_website_item_price_for_logged_in_user", - "test_website_item_price_for_guest_user", -) - - -class TestWebsiteItem(unittest.TestCase): - @classmethod - def setUpClass(cls): - setup_e_commerce_settings( - { - "company": "_Test Company", - "enabled": 1, - "default_customer_group": "_Test Customer Group", - "price_list": "_Test Price List India", - } - ) - - @classmethod - def tearDownClass(cls): - frappe.db.rollback() - - def setUp(self): - if self._testMethodName in WEBITEM_DESK_TESTS: - make_item( - "Test Web Item", - { - "has_variant": 1, - "variant_based_on": "Item Attribute", - "attributes": [{"attribute": "Test Size"}], - }, - ) - elif self._testMethodName in WEBITEM_PRICE_TESTS: - create_user_and_customer_if_not_exists( - "test_contact_customer@example.com", "_Test Contact For _Test Customer" - ) - create_regular_web_item() - make_web_item_price(item_code="Test Mobile Phone") - - # Note: When testing web item pricing rule logged-in user pricing rule must differ from guest pricing rule or test will falsely pass. - # This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor, - # when testing for logged-in user the test will get the previous pricing rule because "selling" is still true. - # - # I've attempted to mitigate this by setting applicable_for=Customer, and customer=Guest however, this only results in PermissionError failing the test. - make_web_pricing_rule( - title="Test Pricing Rule for Test Mobile Phone", item_code="Test Mobile Phone", selling=1 - ) - make_web_pricing_rule( - title="Test Pricing Rule for Test Mobile Phone (Customer)", - item_code="Test Mobile Phone", - selling=1, - discount_percentage="25", - applicable_for="Customer", - customer="_Test Customer", - ) - - def test_index_creation(self): - "Check if index is getting created in db." - from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update - - on_doctype_update() - - indices = frappe.db.sql("show index from `tabWebsite Item`", as_dict=1) - expected_columns = {"route", "item_group", "brand"} - for index in indices: - expected_columns.discard(index.get("Column_name")) - - if expected_columns: - self.fail(f"Expected db index on these columns: {', '.join(expected_columns)}") - - def test_website_item_desk_item_sync(self): - "Check creation/updation/deletion of Website Item and its impact on Item master." - web_item = None - item = make_item("Test Web Item") # will return item if exists - try: - web_item = make_website_item(item, save=False) - web_item.save() - except Exception: - self.fail(f"Error while creating website item for {item}") - - # check if website item was created - self.assertTrue(bool(web_item)) - self.assertTrue(bool(web_item.route)) - - item.reload() - self.assertEqual(web_item.published, 1) - self.assertEqual(item.published_in_website, 1) # check if item was back updated - self.assertEqual(web_item.item_group, item.item_group) - - # check if changing item data changes it in website item - item.item_name = "Test Web Item 1" - item.stock_uom = "Unit" - item.save() - web_item.reload() - self.assertEqual(web_item.item_name, item.item_name) - self.assertEqual(web_item.stock_uom, item.stock_uom) - - # check if disabling item unpublished website item - item.disabled = 1 - item.save() - web_item.reload() - self.assertEqual(web_item.published, 0) - - # check if website item deletion, unpublishes desk item - web_item.delete() - item.reload() - self.assertEqual(item.published_in_website, 0) - - item.delete() - - def test_publish_variant_and_template(self): - "Check if template is published on publishing variant." - # template "Test Web Item" created on setUp - variant = create_variant("Test Web Item", {"Test Size": "Large"}) - variant.save() - - # check if template is not published - self.assertIsNone(frappe.db.exists("Website Item", {"item_code": variant.variant_of})) - - variant_web_item = make_website_item(variant, save=False) - variant_web_item.save() - - # check if template is published - try: - template_web_item = frappe.get_doc("Website Item", {"item_code": variant.variant_of}) - except frappe.DoesNotExistError: - self.fail(f"Template of {variant.item_code}, {variant.variant_of} not published") - - # teardown - variant_web_item.delete() - template_web_item.delete() - variant.delete() - - def test_impact_on_merging_items(self): - "Check if merging items is blocked if old and new items both have website items" - first_item = make_item("Test First Item") - second_item = make_item("Test Second Item") - - first_web_item = make_website_item(first_item, save=False) - first_web_item.save() - second_web_item = make_website_item(second_item, save=False) - second_web_item.save() - - with self.assertRaises(DataValidationError): - frappe.rename_doc("Item", "Test First Item", "Test Second Item", merge=True) - - # tear down - second_web_item.delete() - first_web_item.delete() - second_item.delete() - first_item.delete() - - # Website Item Portal Tests Begin - - def test_website_item_breadcrumbs(self): - """ - Check if breadcrumbs include homepage, product listing navigation page, - parent item group(s) and item group - """ - from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups - - item_code = "Test Breadcrumb Item" - item = make_item( - item_code, - { - "item_group": "_Test Item Group B - 1", - }, - ) - - if not frappe.db.exists("Website Item", {"item_code": item_code}): - web_item = make_website_item(item, save=False) - web_item.save() - else: - web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code}) - - frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1) - frappe.db.set_value("Item Group", "_Test Item Group B", "show_in_website", 1) - - breadcrumbs = get_parent_item_groups(item.item_group) - - settings = frappe.get_cached_doc("E Commerce Settings") - if settings.enable_field_filters: - base_breadcrumb = "Shop by Category" - else: - base_breadcrumb = "All Products" - - self.assertEqual(breadcrumbs[0]["name"], "Home") - self.assertEqual(breadcrumbs[1]["name"], base_breadcrumb) - self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group - self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1") - - # tear down - web_item.delete() - item.delete() - - def test_website_item_price_for_logged_in_user(self): - "Check if price details are fetched correctly while logged in." - item_code = "Test Mobile Phone" - - # show price in e commerce settings - setup_e_commerce_settings({"show_price": 1}) - - # price and pricing rule added via setUp - - # login as customer with pricing rule - frappe.set_user("test_contact_customer@example.com") - - # check if price and slashed price is fetched correctly - frappe.local.shopping_cart_settings = None - data = get_product_info_for_website(item_code, skip_quotation_creation=True) - self.assertTrue(bool(data.product_info["price"])) - - price_object = data.product_info["price"] - self.assertEqual(price_object.get("discount_percent"), 25.0) - self.assertEqual(price_object.get("price_list_rate"), 750) - self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00") - self.assertEqual(price_object.get("formatted_price"), "₹ 750.00") - self.assertEqual(price_object.get("formatted_discount_percent"), "25.0%") - - # switch to admin and disable show price - frappe.set_user("Administrator") - setup_e_commerce_settings({"show_price": 0}) - - # price should not be fetched for logged in user. - frappe.set_user("test_contact_customer@example.com") - frappe.local.shopping_cart_settings = None - data = get_product_info_for_website(item_code, skip_quotation_creation=True) - self.assertFalse(bool(data.product_info["price"])) - - # tear down - frappe.set_user("Administrator") - - def test_website_item_price_for_guest_user(self): - "Check if price details are fetched correctly for guest user." - item_code = "Test Mobile Phone" - - # show price for guest user in e commerce settings - setup_e_commerce_settings({"show_price": 1, "hide_price_for_guest": 0}) - - # price and pricing rule added via setUp - - # switch to guest user - frappe.set_user("Guest") - - # price should be fetched - frappe.local.shopping_cart_settings = None - data = get_product_info_for_website(item_code, skip_quotation_creation=True) - self.assertTrue(bool(data.product_info["price"])) - - price_object = data.product_info["price"] - self.assertEqual(price_object.get("discount_percent"), 10) - self.assertEqual(price_object.get("price_list_rate"), 900) - - # hide price for guest user - frappe.set_user("Administrator") - setup_e_commerce_settings({"hide_price_for_guest": 1}) - frappe.set_user("Guest") - - # price should not be fetched - frappe.local.shopping_cart_settings = None - data = get_product_info_for_website(item_code, skip_quotation_creation=True) - self.assertFalse(bool(data.product_info["price"])) - - # tear down - frappe.set_user("Administrator") - - def test_website_item_stock_when_out_of_stock(self): - """ - Check if stock details are fetched correctly for empty inventory when: - 1) Showing stock availability enabled: - - Warehouse unset - - Warehouse set - 2) Showing stock availability disabled - """ - item_code = "Test Mobile Phone" - create_regular_web_item() - setup_e_commerce_settings({"show_stock_availability": 1}) - - frappe.local.shopping_cart_settings = None - data = get_product_info_for_website(item_code, skip_quotation_creation=True) - - # check if stock details are fetched and item not in stock without warehouse set - self.assertFalse(bool(data.product_info["in_stock"])) - self.assertFalse(bool(data.product_info["stock_qty"])) - - # set warehouse - frappe.db.set_value( - "Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC" - ) - - # check if stock details are fetched and item not in stock with warehouse set - data = get_product_info_for_website(item_code, skip_quotation_creation=True) - self.assertFalse(bool(data.product_info["in_stock"])) - self.assertEqual(data.product_info["stock_qty"], 0) - - # disable show stock availability - setup_e_commerce_settings({"show_stock_availability": 0}) - frappe.local.shopping_cart_settings = None - data = get_product_info_for_website(item_code, skip_quotation_creation=True) - - # check if stock detail attributes are not fetched if stock availability is hidden - self.assertIsNone(data.product_info.get("in_stock")) - self.assertIsNone(data.product_info.get("stock_qty")) - self.assertIsNone(data.product_info.get("show_stock_qty")) - - # tear down - frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete() - - def test_website_item_stock_when_in_stock(self): - """ - Check if stock details are fetched correctly for available inventory when: - 1) Showing stock availability enabled: - - Warehouse set - - Warehouse unset - 2) Showing stock availability disabled - """ - from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - - item_code = "Test Mobile Phone" - create_regular_web_item() - setup_e_commerce_settings({"show_stock_availability": 1}) - frappe.local.shopping_cart_settings = None - - # set warehouse - frappe.db.set_value( - "Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC" - ) - - # stock up item - stock_entry = make_stock_entry( - item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100 - ) - - # check if stock details are fetched and item is in stock with warehouse set - data = get_product_info_for_website(item_code, skip_quotation_creation=True) - self.assertTrue(bool(data.product_info["in_stock"])) - self.assertEqual(data.product_info["stock_qty"], 2) - - # unset warehouse - frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "") - - # check if stock details are fetched and item not in stock without warehouse set - # (even though it has stock in some warehouse) - data = get_product_info_for_website(item_code, skip_quotation_creation=True) - self.assertFalse(bool(data.product_info["in_stock"])) - self.assertFalse(data.product_info["stock_qty"]) - - # disable show stock availability - setup_e_commerce_settings({"show_stock_availability": 0}) - frappe.local.shopping_cart_settings = None - data = get_product_info_for_website(item_code, skip_quotation_creation=True) - - # check if stock detail attributes are not fetched if stock availability is hidden - self.assertIsNone(data.product_info.get("in_stock")) - self.assertIsNone(data.product_info.get("stock_qty")) - self.assertIsNone(data.product_info.get("show_stock_qty")) - - # tear down - stock_entry.cancel() - frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete() - - def test_recommended_item(self): - "Check if added recommended items are fetched correctly." - item_code = "Test Mobile Phone" - web_item = create_regular_web_item(item_code) - - setup_e_commerce_settings({"enable_recommendations": 1, "show_price": 1}) - - # create recommended web item and price for it - recommended_web_item = create_regular_web_item("Test Mobile Phone 1") - make_web_item_price(item_code="Test Mobile Phone 1") - - # add recommended item to first web item - web_item.append("recommended_items", {"website_item": recommended_web_item.name}) - web_item.save() - - frappe.local.shopping_cart_settings = None - e_commerce_settings = get_shopping_cart_settings() - recommended_items = web_item.get_recommended_items(e_commerce_settings) - - # test results if show price is enabled - self.assertEqual(len(recommended_items), 1) - recomm_item = recommended_items[0] - self.assertEqual(recomm_item.get("website_item_name"), "Test Mobile Phone 1") - self.assertTrue(bool(recomm_item.get("price_info"))) # price fetched - - price_info = recomm_item.get("price_info") - self.assertEqual(price_info.get("price_list_rate"), 1000) - self.assertEqual(price_info.get("formatted_price"), "₹ 1,000.00") - - # test results if show price is disabled - setup_e_commerce_settings({"show_price": 0}) - - frappe.local.shopping_cart_settings = None - e_commerce_settings = get_shopping_cart_settings() - recommended_items = web_item.get_recommended_items(e_commerce_settings) - - self.assertEqual(len(recommended_items), 1) - self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched - - # tear down - web_item.delete() - recommended_web_item.delete() - frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete() - - def test_recommended_item_for_guest_user(self): - "Check if added recommended items are fetched correctly for guest user." - item_code = "Test Mobile Phone" - web_item = create_regular_web_item(item_code) - - # price visible to guests - setup_e_commerce_settings( - {"enable_recommendations": 1, "show_price": 1, "hide_price_for_guest": 0} - ) - - # create recommended web item and price for it - recommended_web_item = create_regular_web_item("Test Mobile Phone 1") - make_web_item_price(item_code="Test Mobile Phone 1") - - # add recommended item to first web item - web_item.append("recommended_items", {"website_item": recommended_web_item.name}) - web_item.save() - - frappe.set_user("Guest") - - frappe.local.shopping_cart_settings = None - e_commerce_settings = get_shopping_cart_settings() - recommended_items = web_item.get_recommended_items(e_commerce_settings) - - # test results if show price is enabled - self.assertEqual(len(recommended_items), 1) - self.assertTrue(bool(recommended_items[0].get("price_info"))) # price fetched - - # price hidden from guests - frappe.set_user("Administrator") - setup_e_commerce_settings({"hide_price_for_guest": 1}) - frappe.set_user("Guest") - - frappe.local.shopping_cart_settings = None - e_commerce_settings = get_shopping_cart_settings() - recommended_items = web_item.get_recommended_items(e_commerce_settings) - - # test results if show price is enabled - self.assertEqual(len(recommended_items), 1) - self.assertFalse(bool(recommended_items[0].get("price_info"))) # price fetched - - # tear down - frappe.set_user("Administrator") - web_item.delete() - recommended_web_item.delete() - frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete() - - -def create_regular_web_item(item_code=None, item_args=None, web_args=None): - "Create Regular Item and Website Item." - item_code = item_code or "Test Mobile Phone" - item = make_item(item_code, properties=item_args) - - if not frappe.db.exists("Website Item", {"item_code": item_code}): - web_item = make_website_item(item, save=False) - if web_args: - web_item.update(web_args) - web_item.save() - else: - web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code}) - - return web_item - - -def make_web_item_price(**kwargs): - item_code = kwargs.get("item_code") - if not item_code: - return - - if not frappe.db.exists("Item Price", {"item_code": item_code}): - item_price = frappe.get_doc( - { - "doctype": "Item Price", - "item_code": item_code, - "price_list": kwargs.get("price_list") or "_Test Price List India", - "price_list_rate": kwargs.get("price_list_rate") or 1000, - } - ) - item_price.insert() - else: - item_price = frappe.get_cached_doc("Item Price", {"item_code": item_code}) - - return item_price - - -def make_web_pricing_rule(**kwargs): - title = kwargs.get("title") - if not title: - return - - if not frappe.db.exists("Pricing Rule", title): - pricing_rule = frappe.get_doc( - { - "doctype": "Pricing Rule", - "title": title, - "apply_on": kwargs.get("apply_on") or "Item Code", - "items": [{"item_code": kwargs.get("item_code")}], - "selling": kwargs.get("selling") or 0, - "buying": kwargs.get("buying") or 0, - "rate_or_discount": kwargs.get("rate_or_discount") or "Discount Percentage", - "discount_percentage": kwargs.get("discount_percentage") or 10, - "company": kwargs.get("company") or "_Test Company", - "currency": kwargs.get("currency") or "INR", - "for_price_list": kwargs.get("price_list") or "_Test Price List India", - "applicable_for": kwargs.get("applicable_for") or "", - "customer": kwargs.get("customer") or "", - } - ) - pricing_rule.insert() - else: - pricing_rule = frappe.get_doc("Pricing Rule", {"title": title}) - - return pricing_rule - - -def create_user_and_customer_if_not_exists(email, first_name=None): - if frappe.db.exists("User", email): - return - - frappe.get_doc( - { - "doctype": "User", - "user_type": "Website User", - "email": email, - "send_welcome_email": 0, - "first_name": first_name or email.split("@")[0], - } - ).insert(ignore_permissions=True) - - contact = frappe.get_last_doc("Contact", filters={"email_id": email}) - link = contact.append("links", {}) - link.link_doctype = "Customer" - link.link_name = "_Test Customer" - link.link_title = "_Test Customer" - contact.save() - - -test_dependencies = ["Price List", "Item Price", "Customer", "Contact", "Item"] diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js deleted file mode 100644 index b6595cce8a..0000000000 --- a/erpnext/e_commerce/doctype/website_item/website_item.js +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Website Item', { - onload: (frm) => { - // should never check Private - frm.fields_dict["website_image"].df.is_private = 0; - }, - - refresh: (frm) => { - frm.add_custom_button(__("Prices"), function() { - frappe.set_route("List", "Item Price", {"item_code": frm.doc.item_code}); - }, __("View")); - - frm.add_custom_button(__("Stock"), function() { - frappe.route_options = { - "item_code": frm.doc.item_code - }; - frappe.set_route("query-report", "Stock Balance"); - }, __("View")); - - frm.add_custom_button(__("E Commerce Settings"), function() { - frappe.set_route("Form", "E Commerce Settings"); - }, __("View")); - }, - - copy_from_item_group: (frm) => { - return frm.call({ - doc: frm.doc, - method: "copy_specification_from_item_group" - }); - }, - - set_meta_tags: (frm) => { - frappe.utils.set_meta_tag(frm.doc.route); - } -}); diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json deleted file mode 100644 index 6f551a0b42..0000000000 --- a/erpnext/e_commerce/doctype/website_item/website_item.json +++ /dev/null @@ -1,414 +0,0 @@ -{ - "actions": [], - "allow_guest_to_view": 1, - "allow_import": 1, - "autoname": "naming_series", - "creation": "2021-02-09 21:06:14.441698", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "naming_series", - "web_item_name", - "route", - "has_variants", - "variant_of", - "published", - "column_break_3", - "item_code", - "item_name", - "item_group", - "stock_uom", - "column_break_11", - "description", - "brand", - "display_section", - "website_image", - "website_image_alt", - "column_break_13", - "slideshow", - "thumbnail", - "stock_information_section", - "website_warehouse", - "column_break_24", - "on_backorder", - "section_break_17", - "short_description", - "web_long_description", - "column_break_27", - "website_specifications", - "copy_from_item_group", - "display_additional_information_section", - "show_tabbed_section", - "tabs", - "recommended_items_section", - "recommended_items", - "offers_section", - "offers", - "section_break_6", - "ranking", - "set_meta_tags", - "column_break_22", - "website_item_groups", - "advanced_display_section", - "website_content" - ], - "fields": [ - { - "description": "Website display name", - "fetch_from": "item_code.item_name", - "fetch_if_empty": 1, - "fieldname": "web_item_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Website Item Name", - "reqd": 1 - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fieldname": "item_code", - "fieldtype": "Link", - "label": "Item Code", - "options": "Item", - "read_only_depends_on": "eval:!doc.__islocal", - "reqd": 1 - }, - { - "fetch_from": "item_code.item_name", - "fieldname": "item_name", - "fieldtype": "Data", - "label": "Item Name", - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "label": "Search and SEO" - }, - { - "fieldname": "route", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Route", - "no_copy": 1 - }, - { - "description": "Items with higher ranking will be shown higher", - "fieldname": "ranking", - "fieldtype": "Int", - "label": "Ranking" - }, - { - "description": "Show a slideshow at the top of the page", - "fieldname": "slideshow", - "fieldtype": "Link", - "label": "Slideshow", - "options": "Website Slideshow" - }, - { - "description": "Item Image (if not slideshow)", - "fieldname": "website_image", - "fieldtype": "Attach Image", - "hidden": 1, - "in_preview": 1, - "label": "Website Image", - "print_hide": 1 - }, - { - "description": "Image Alternative Text", - "fieldname": "website_image_alt", - "fieldtype": "Data", - "label": "Image Description" - }, - { - "fieldname": "thumbnail", - "fieldtype": "Data", - "label": "Thumbnail", - "read_only": 1 - }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, - { - "description": "Show Stock availability based on this warehouse. If the parent warehouse is selected, then the system will display the consolidated available quantity of all child warehouses.", - "fieldname": "website_warehouse", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Website Warehouse", - "options": "Warehouse" - }, - { - "description": "List this Item in multiple groups on the website.", - "fieldname": "website_item_groups", - "fieldtype": "Table", - "label": "Website Item Groups", - "options": "Website Item Group" - }, - { - "fieldname": "set_meta_tags", - "fieldtype": "Button", - "label": "Set Meta Tags" - }, - { - "fieldname": "section_break_17", - "fieldtype": "Section Break", - "label": "Display Information" - }, - { - "fieldname": "copy_from_item_group", - "fieldtype": "Button", - "label": "Copy From Item Group" - }, - { - "fieldname": "website_specifications", - "fieldtype": "Table", - "label": "Website Specifications", - "options": "Item Website Specification" - }, - { - "fieldname": "web_long_description", - "fieldtype": "Text Editor", - "label": "Website Description" - }, - { - "description": "You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.", - "fieldname": "website_content", - "fieldtype": "HTML Editor", - "label": "Website Content" - }, - { - "fetch_from": "item_code.item_group", - "fieldname": "item_group", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Group", - "options": "Item Group", - "read_only": 1, - "search_index": 1 - }, - { - "default": "1", - "fieldname": "published", - "fieldtype": "Check", - "label": "Published" - }, - { - "default": "0", - "depends_on": "has_variants", - "fetch_from": "item_code.has_variants", - "fieldname": "has_variants", - "fieldtype": "Check", - "in_standard_filter": 1, - "label": "Has Variants", - "no_copy": 1, - "read_only": 1 - }, - { - "depends_on": "variant_of", - "fetch_from": "item_code.variant_of", - "fieldname": "variant_of", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "in_standard_filter": 1, - "label": "Variant Of", - "options": "Item", - "read_only": 1, - "search_index": 1, - "set_only_once": 1 - }, - { - "fetch_from": "item_code.stock_uom", - "fieldname": "stock_uom", - "fieldtype": "Link", - "label": "Stock UOM", - "options": "UOM", - "read_only": 1 - }, - { - "depends_on": "brand", - "fetch_from": "item_code.brand", - "fieldname": "brand", - "fieldtype": "Link", - "label": "Brand", - "options": "Brand", - "search_index": 1 - }, - { - "collapsible": 1, - "fieldname": "advanced_display_section", - "fieldtype": "Section Break", - "label": "Advanced Display Content" - }, - { - "fieldname": "display_section", - "fieldtype": "Section Break", - "label": "Display Images" - }, - { - "fieldname": "column_break_27", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_22", - "fieldtype": "Column Break" - }, - { - "fetch_from": "item_code.description", - "fieldname": "description", - "fieldtype": "Text Editor", - "label": "Item Description", - "read_only": 1 - }, - { - "default": "WEB-ITM-.####", - "fieldname": "naming_series", - "fieldtype": "Select", - "hidden": 1, - "label": "Naming Series", - "no_copy": 1, - "options": "WEB-ITM-.####", - "print_hide": 1 - }, - { - "fieldname": "display_additional_information_section", - "fieldtype": "Section Break", - "label": "Display Additional Information" - }, - { - "depends_on": "show_tabbed_section", - "fieldname": "tabs", - "fieldtype": "Table", - "label": "Tabs", - "options": "Website Item Tabbed Section" - }, - { - "default": "0", - "fieldname": "show_tabbed_section", - "fieldtype": "Check", - "label": "Add Section with Tabs" - }, - { - "collapsible": 1, - "fieldname": "offers_section", - "fieldtype": "Section Break", - "label": "Offers" - }, - { - "fieldname": "offers", - "fieldtype": "Table", - "label": "Offers to Display", - "options": "Website Offer" - }, - { - "fieldname": "column_break_11", - "fieldtype": "Column Break" - }, - { - "description": "Short Description for List View", - "fieldname": "short_description", - "fieldtype": "Small Text", - "label": "Short Website Description" - }, - { - "collapsible": 1, - "fieldname": "recommended_items_section", - "fieldtype": "Section Break", - "label": "Recommended Items" - }, - { - "fieldname": "recommended_items", - "fieldtype": "Table", - "label": "Recommended/Similar Items", - "options": "Recommended Items" - }, - { - "fieldname": "stock_information_section", - "fieldtype": "Section Break", - "label": "Stock Information" - }, - { - "fieldname": "column_break_24", - "fieldtype": "Column Break" - }, - { - "default": "0", - "description": "Indicate that Item is available on backorder and not usually pre-stocked", - "fieldname": "on_backorder", - "fieldtype": "Check", - "label": "On Backorder" - } - ], - "has_web_view": 1, - "image_field": "website_image", - "index_web_pages_for_search": 1, - "links": [], - "make_attachments_public": 1, - "modified": "2023-09-12 14:19:22.822689", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Website Item", - "naming_rule": "Expression (old style)", - "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, - "read": 1, - "report": 1, - "role": "Stock User", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock Manager", - "share": 1, - "write": 1 - } - ], - "search_fields": "web_item_name, item_code, item_group", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "title_field": "web_item_name", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py deleted file mode 100644 index 81b8ecab48..0000000000 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ /dev/null @@ -1,469 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import json -from typing import TYPE_CHECKING, List, Union - -if TYPE_CHECKING: - from erpnext.stock.doctype.item.item import Item - -import frappe -from frappe import _ -from frappe.utils import cint, cstr, flt, random_string -from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow -from frappe.website.website_generator import WebsiteGenerator - -from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews -from erpnext.e_commerce.redisearch_utils import ( - delete_item_from_index, - insert_item_to_index, - update_index_for_item, -) -from erpnext.e_commerce.shopping_cart.cart import _set_price_list -from erpnext.setup.doctype.item_group.item_group import ( - get_parent_item_groups, - invalidate_cache_for, -) -from erpnext.utilities.product import get_price - - -class WebsiteItem(WebsiteGenerator): - website = frappe._dict( - page_title_field="web_item_name", - condition_field="published", - template="templates/generators/item/item.html", - no_cache=1, - ) - - def autoname(self): - # use naming series to accomodate items with same name (different item code) - from frappe.model.naming import get_default_naming_series, make_autoname - - naming_series = get_default_naming_series("Website Item") - if not self.name and naming_series: - self.name = make_autoname(naming_series, doc=self) - - def onload(self): - super(WebsiteItem, self).onload() - - def validate(self): - super(WebsiteItem, self).validate() - - if not self.item_code: - frappe.throw(_("Item Code is required"), title=_("Mandatory")) - - self.validate_duplicate_website_item() - self.validate_website_image() - self.make_thumbnail() - self.publish_unpublish_desk_item(publish=True) - - if not self.get("__islocal"): - wig = frappe.qb.DocType("Website Item Group") - query = ( - frappe.qb.from_(wig) - .select(wig.item_group) - .where( - (wig.parentfield == "website_item_groups") - & (wig.parenttype == "Website Item") - & (wig.parent == self.name) - ) - ) - result = query.run(as_list=True) - - self.old_website_item_groups = [x[0] for x in result] - - def on_update(self): - invalidate_cache_for_web_item(self) - self.update_template_item() - - def on_trash(self): - super(WebsiteItem, self).on_trash() - delete_item_from_index(self) - self.publish_unpublish_desk_item(publish=False) - - def validate_duplicate_website_item(self): - existing_web_item = frappe.db.exists("Website Item", {"item_code": self.item_code}) - if existing_web_item and existing_web_item != self.name: - message = _("Website Item already exists against Item {0}").format(frappe.bold(self.item_code)) - frappe.throw(message, title=_("Already Published")) - - def publish_unpublish_desk_item(self, publish=True): - if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish: - return # if already published don't publish again - frappe.db.set_value("Item", self.item_code, "published_in_website", publish) - - def make_route(self): - """Called from set_route in WebsiteGenerator.""" - if not self.route: - return ( - cstr(frappe.db.get_value("Item Group", self.item_group, "route")) - + "/" - + self.scrub((self.item_name if self.item_name else self.item_code) + "-" + random_string(5)) - ) - - def update_template_item(self): - """Publish Template Item if Variant is published.""" - if self.variant_of: - if self.published: - # show template - template_item = frappe.get_doc("Item", self.variant_of) - - if not template_item.published_in_website: - template_item.flags.ignore_permissions = True - make_website_item(template_item) - - def validate_website_image(self): - if frappe.flags.in_import: - return - - """Validate if the website image is a public file""" - if not self.website_image: - return - - # find if website image url exists as public - file_doc = frappe.get_all( - "File", - filters={"file_url": self.website_image}, - fields=["name", "is_private"], - order_by="is_private asc", - limit_page_length=1, - ) - - if file_doc: - file_doc = file_doc[0] - - if not file_doc: - frappe.msgprint( - _("Website Image {0} attached to Item {1} cannot be found").format( - self.website_image, self.name - ) - ) - - self.website_image = None - - elif file_doc.is_private: - frappe.msgprint(_("Website Image should be a public file or website URL")) - - self.website_image = None - - def make_thumbnail(self): - """Make a thumbnail of `website_image`""" - if frappe.flags.in_import or frappe.flags.in_migrate: - return - - import requests.exceptions - - db_website_image = frappe.db.get_value(self.doctype, self.name, "website_image") - if not self.is_new() and self.website_image != db_website_image: - self.thumbnail = None - - if self.website_image and not self.thumbnail: - file_doc = None - - try: - file_doc = frappe.get_doc( - "File", - { - "file_url": self.website_image, - "attached_to_doctype": "Website Item", - "attached_to_name": self.name, - }, - ) - except frappe.DoesNotExistError: - pass - # cleanup - frappe.local.message_log.pop() - - except requests.exceptions.HTTPError: - frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image)) - self.website_image = None - - except requests.exceptions.SSLError: - frappe.msgprint( - _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image) - ) - self.website_image = None - - # for CSV import - if self.website_image and not file_doc: - try: - file_doc = frappe.get_doc( - { - "doctype": "File", - "file_url": self.website_image, - "attached_to_doctype": "Website Item", - "attached_to_name": self.name, - } - ).save() - - except IOError: - self.website_image = None - - if file_doc: - if not file_doc.thumbnail_url: - file_doc.make_thumbnail() - - self.thumbnail = file_doc.thumbnail_url - - def get_context(self, context): - context.show_search = True - context.search_link = "/search" - context.body_class = "product-page" - - context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs - self.attributes = frappe.get_all( - "Item Variant Attribute", - fields=["attribute", "attribute_value"], - filters={"parent": self.item_code}, - ) - - if self.slideshow: - context.update(get_slideshow(self)) - - self.set_metatags(context) - self.set_shopping_cart_data(context) - - settings = context.shopping_cart.cart_settings - - self.get_product_details_section(context) - - if settings.get("enable_reviews"): - reviews_data = get_item_reviews(self.name) - context.update(reviews_data) - context.reviews = context.reviews[:4] - - context.wished = False - if frappe.db.exists( - "Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user} - ): - context.wished = True - - context.user_is_customer = check_if_user_is_customer() - - context.recommended_items = None - if settings and settings.enable_recommendations: - context.recommended_items = self.get_recommended_items(settings) - - return context - - def set_selected_attributes(self, variants, context, attribute_values_available): - for variant in variants: - variant.attributes = frappe.get_all( - "Item Variant Attribute", - filters={"parent": variant.name}, - fields=["attribute", "attribute_value as value"], - ) - - # make an attribute-value map for easier access in templates - variant.attribute_map = frappe._dict( - {attr.attribute: attr.value for attr in variant.attributes} - ) - - for attr in variant.attributes: - values = attribute_values_available.setdefault(attr.attribute, []) - if attr.value not in values: - values.append(attr.value) - - if variant.name == context.variant.name: - context.selected_attributes[attr.attribute] = attr.value - - def set_attribute_values(self, attributes, context, attribute_values_available): - for attr in attributes: - values = context.attribute_values.setdefault(attr.attribute, []) - - if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): - for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): - values.append(val) - else: - # get list of values defined (for sequence) - for attr_value in frappe.db.get_all( - "Item Attribute Value", - fields=["attribute_value"], - filters={"parent": attr.attribute}, - order_by="idx asc", - ): - - if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): - values.append(attr_value.attribute_value) - - def set_metatags(self, context): - context.metatags = frappe._dict({}) - - safe_description = frappe.utils.to_markdown(self.description) - - context.metatags.url = frappe.utils.get_url() + "/" + context.route - - if context.website_image: - if context.website_image.startswith("http"): - url = context.website_image - else: - url = frappe.utils.get_url() + context.website_image - context.metatags.image = url - - context.metatags.description = safe_description[:300] - - context.metatags.title = self.web_item_name or self.item_name or self.item_code - - context.metatags["og:type"] = "product" - context.metatags["og:site_name"] = "ERPNext" - - def set_shopping_cart_data(self, context): - from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website - - context.shopping_cart = get_product_info_for_website( - self.item_code, skip_quotation_creation=True - ) - - @frappe.whitelist() - def copy_specification_from_item_group(self): - self.set("website_specifications", []) - if self.item_group: - for label, desc in frappe.db.get_values( - "Item Website Specification", {"parent": self.item_group}, ["label", "description"] - ): - row = self.append("website_specifications") - row.label = label - row.description = desc - - def get_product_details_section(self, context): - """Get section with tabs or website specifications.""" - context.show_tabs = self.show_tabbed_section - if self.show_tabbed_section and (self.tabs or self.website_specifications): - context.tabs = self.get_tabs() - else: - context.website_specifications = self.website_specifications - - def get_tabs(self): - tab_values = {} - tab_values["tab_1_title"] = "Product Details" - tab_values["tab_1_content"] = frappe.render_template( - "templates/generators/item/item_specifications.html", - {"website_specifications": self.website_specifications, "show_tabs": self.show_tabbed_section}, - ) - - for row in self.tabs: - tab_values[f"tab_{row.idx + 1}_title"] = _(row.label) - tab_values[f"tab_{row.idx + 1}_content"] = row.content - - return tab_values - - def get_recommended_items(self, settings): - ri = frappe.qb.DocType("Recommended Items") - wi = frappe.qb.DocType("Website Item") - - query = ( - frappe.qb.from_(ri) - .join(wi) - .on(ri.item_code == wi.item_code) - .select(ri.item_code, ri.route, ri.website_item_name, ri.website_item_thumbnail) - .where((ri.parent == self.name) & (wi.published == 1)) - .orderby(ri.idx) - ) - items = query.run(as_dict=True) - - if settings.show_price: - is_guest = frappe.session.user == "Guest" - # Show Price if logged in. - # If not logged in and price is hidden for guest, skip price fetch. - if is_guest and settings.hide_price_for_guest: - return items - - selling_price_list = _set_price_list(settings, None) - for item in items: - item.price_info = get_price( - item.item_code, selling_price_list, settings.default_customer_group, settings.company - ) - - return items - - -def invalidate_cache_for_web_item(doc): - """Invalidate Website Item Group cache and rebuild ItemVariantsCacheManager.""" - from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website - - invalidate_cache_for(doc, doc.item_group) - - website_item_groups = list( - set( - (doc.get("old_website_item_groups") or []) - + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group] - ) - ) - - for item_group in website_item_groups: - invalidate_cache_for(doc, item_group) - - # Update Search Cache - update_index_for_item(doc) - - invalidate_item_variants_cache_for_website(doc) - - -def on_doctype_update(): - # since route is a Text column, it needs a length for indexing - frappe.db.add_index("Website Item", ["route(500)"]) - - -def check_if_user_is_customer(user=None): - from frappe.contacts.doctype.contact.contact import get_contact_name - - if not user: - 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 - - return True if customer else False - - -@frappe.whitelist() -def make_website_item(doc: "Item", save: bool = True) -> Union["WebsiteItem", List[str]]: - "Make Website Item from Item. Used via Form UI or patch." - - if not doc: - return - - if isinstance(doc, str): - doc = json.loads(doc) - - if frappe.db.exists("Website Item", {"item_code": doc.get("item_code")}): - message = _("Website Item already exists against {0}").format(frappe.bold(doc.get("item_code"))) - frappe.throw(message, title=_("Already Published")) - - website_item = frappe.new_doc("Website Item") - website_item.web_item_name = doc.get("item_name") - - fields_to_map = [ - "item_code", - "item_name", - "item_group", - "stock_uom", - "brand", - "has_variants", - "variant_of", - "description", - ] - for field in fields_to_map: - website_item.update({field: doc.get(field)}) - - # Needed for publishing/mapping via Form UI only - if not frappe.flags.in_migrate and (doc.get("image") and not website_item.website_image): - website_item.website_image = doc.get("image") - - if not save: - return website_item - - website_item.save() - - # Add to search cache - insert_item_to_index(website_item) - - return [website_item.name, website_item.web_item_name] diff --git a/erpnext/e_commerce/doctype/website_item/website_item_list.js b/erpnext/e_commerce/doctype/website_item/website_item_list.js deleted file mode 100644 index b9dd9214a3..0000000000 --- a/erpnext/e_commerce/doctype/website_item/website_item_list.js +++ /dev/null @@ -1,20 +0,0 @@ -frappe.listview_settings['Website Item'] = { - add_fields: ["item_name", "web_item_name", "published", "website_image", "has_variants", "variant_of"], - filters: [["published", "=", "1"]], - - get_indicator: function(doc) { - if (doc.has_variants && doc.published) { - return [__("Template"), "orange", "has_variants,=,Yes|published,=,1"]; - } else if (doc.has_variants && !doc.published) { - return [__("Template"), "grey", "has_variants,=,Yes|published,=,0"]; - } else if (doc.variant_of && doc.published) { - return [__("Variant"), "blue", "published,=,1|variant_of,=," + doc.variant_of]; - } else if (doc.variant_of && !doc.published) { - return [__("Variant"), "grey", "published,=,0|variant_of,=," + doc.variant_of]; - } else if (doc.published) { - return [__("Published"), "green", "published,=,1"]; - } else { - return [__("Not Published"), "grey", "published,=,0"]; - } - } -}; \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_item_tabbed_section/__init__.py b/erpnext/e_commerce/doctype/website_item_tabbed_section/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.json b/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.json deleted file mode 100644 index 6601dd81f2..0000000000 --- a/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "actions": [], - "creation": "2021-03-18 20:32:15.321402", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "label", - "content" - ], - "fields": [ - { - "fieldname": "label", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Label" - }, - { - "fieldname": "content", - "fieldtype": "HTML Editor", - "in_list_view": 1, - "label": "Content" - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-03-18 20:35:26.991192", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Website Item Tabbed Section", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py b/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py deleted file mode 100644 index 91148b8b04..0000000000 --- a/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py +++ /dev/null @@ -1,10 +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.model.document import Document - - -class WebsiteItemTabbedSection(Document): - pass diff --git a/erpnext/e_commerce/doctype/website_offer/__init__.py b/erpnext/e_commerce/doctype/website_offer/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/doctype/website_offer/website_offer.json b/erpnext/e_commerce/doctype/website_offer/website_offer.json deleted file mode 100644 index 627d548146..0000000000 --- a/erpnext/e_commerce/doctype/website_offer/website_offer.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "actions": [], - "creation": "2021-04-21 13:37:14.162162", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "offer_title", - "offer_subtitle", - "offer_details" - ], - "fields": [ - { - "fieldname": "offer_title", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Offer Title" - }, - { - "fieldname": "offer_subtitle", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Offer Subtitle" - }, - { - "fieldname": "offer_details", - "fieldtype": "Text Editor", - "label": "Offer Details" - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-04-21 13:56:04.660331", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Website Offer", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_offer/website_offer.py b/erpnext/e_commerce/doctype/website_offer/website_offer.py deleted file mode 100644 index 8c92f75a1e..0000000000 --- a/erpnext/e_commerce/doctype/website_offer/website_offer.py +++ /dev/null @@ -1,15 +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.model.document import Document - - -class WebsiteOffer(Document): - pass - - -@frappe.whitelist(allow_guest=True) -def get_offer_details(offer_id): - return frappe.db.get_value("Website Offer", {"name": offer_id}, ["offer_details"]) diff --git a/erpnext/e_commerce/doctype/wishlist/__init__.py b/erpnext/e_commerce/doctype/wishlist/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/doctype/wishlist/test_wishlist.py b/erpnext/e_commerce/doctype/wishlist/test_wishlist.py deleted file mode 100644 index 9d27126fdb..0000000000 --- a/erpnext/e_commerce/doctype/wishlist/test_wishlist.py +++ /dev/null @@ -1,117 +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.website_item.website_item import make_website_item -from erpnext.e_commerce.doctype.wishlist.wishlist import add_to_wishlist, remove_from_wishlist -from erpnext.stock.doctype.item.test_item import make_item - - -class TestWishlist(unittest.TestCase): - def setUp(self): - item = make_item("Test Phone Series X") - if not frappe.db.exists("Website Item", {"item_code": "Test Phone Series X"}): - make_website_item(item, save=True) - - item = make_item("Test Phone Series Y") - if not frappe.db.exists("Website Item", {"item_code": "Test Phone Series Y"}): - make_website_item(item, save=True) - - def tearDown(self): - frappe.get_cached_doc("Website Item", {"item_code": "Test Phone Series X"}).delete() - frappe.get_cached_doc("Website Item", {"item_code": "Test Phone Series Y"}).delete() - frappe.get_cached_doc("Item", "Test Phone Series X").delete() - frappe.get_cached_doc("Item", "Test Phone Series Y").delete() - - def test_add_remove_items_in_wishlist(self): - "Check if items are added and removed from user's wishlist." - # add first item - add_to_wishlist("Test Phone Series X") - - # check if wishlist was created and item was added - self.assertTrue(frappe.db.exists("Wishlist", {"user": frappe.session.user})) - self.assertTrue( - frappe.db.exists( - "Wishlist Item", {"item_code": "Test Phone Series X", "parent": frappe.session.user} - ) - ) - - # add second item to wishlist - add_to_wishlist("Test Phone Series Y") - wishlist_length = frappe.db.get_value( - "Wishlist Item", {"parent": frappe.session.user}, "count(*)" - ) - self.assertEqual(wishlist_length, 2) - - remove_from_wishlist("Test Phone Series X") - remove_from_wishlist("Test Phone Series Y") - - wishlist_length = frappe.db.get_value( - "Wishlist Item", {"parent": frappe.session.user}, "count(*)" - ) - self.assertIsNone(frappe.db.exists("Wishlist Item", {"parent": frappe.session.user})) - self.assertEqual(wishlist_length, 0) - - # tear down - frappe.get_doc("Wishlist", {"user": frappe.session.user}).delete() - - def test_add_remove_in_wishlist_multiple_users(self): - "Check if items are added and removed from the correct user's wishlist." - test_user = create_user("test_reviewer@example.com", "Customer") - test_user_1 = create_user("test_reviewer_1@example.com", "Customer") - - # add to wishlist for first user - frappe.set_user(test_user.name) - add_to_wishlist("Test Phone Series X") - - # add to wishlist for second user - frappe.set_user(test_user_1.name) - add_to_wishlist("Test Phone Series X") - - # check wishlist and its content for users - self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user.name})) - self.assertTrue( - frappe.db.exists( - "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name} - ) - ) - - self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user_1.name})) - self.assertTrue( - frappe.db.exists( - "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user_1.name} - ) - ) - - # remove item for second user - remove_from_wishlist("Test Phone Series X") - - # make sure item was removed for second user and not first - self.assertFalse( - frappe.db.exists( - "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user_1.name} - ) - ) - self.assertTrue( - frappe.db.exists( - "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name} - ) - ) - - # remove item for first user - frappe.set_user(test_user.name) - remove_from_wishlist("Test Phone Series X") - self.assertFalse( - frappe.db.exists( - "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name} - ) - ) - - # tear down - frappe.set_user("Administrator") - frappe.get_doc("Wishlist", {"user": test_user.name}).delete() - frappe.get_doc("Wishlist", {"user": test_user_1.name}).delete() diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.js b/erpnext/e_commerce/doctype/wishlist/wishlist.js deleted file mode 100644 index d96e552ecd..0000000000 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.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('Wishlist', { - // refresh: function(frm) { - - // } -}); diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.json b/erpnext/e_commerce/doctype/wishlist/wishlist.json deleted file mode 100644 index 922924e53b..0000000000 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "actions": [], - "autoname": "field:user", - "creation": "2021-03-10 18:52:28.769126", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "user", - "section_break_2", - "items" - ], - "fields": [ - { - "fieldname": "user", - "fieldtype": "Link", - "in_list_view": 1, - "label": "User", - "options": "User", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "section_break_2", - "fieldtype": "Section Break" - }, - { - "fieldname": "items", - "fieldtype": "Table", - "label": "Items", - "options": "Wishlist Item" - } - ], - "in_create": 1, - "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-07-08 13:11:21.693956", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Wishlist", - "owner": "Administrator", - "permissions": [ - { - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1 - }, - { - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Website Manager", - "share": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py deleted file mode 100644 index eb74027d77..0000000000 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.py +++ /dev/null @@ -1,70 +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.model.document import Document - - -class Wishlist(Document): - pass - - -@frappe.whitelist() -def add_to_wishlist(item_code): - """Insert Item into wishlist.""" - - if frappe.db.exists("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}): - return - - web_item_data = frappe.db.get_value( - "Website Item", - {"item_code": item_code}, - [ - "website_image", - "website_warehouse", - "name", - "web_item_name", - "item_name", - "item_group", - "route", - ], - as_dict=1, - ) - - wished_item_dict = { - "item_code": item_code, - "item_name": web_item_data.get("item_name"), - "item_group": web_item_data.get("item_group"), - "website_item": web_item_data.get("name"), - "web_item_name": web_item_data.get("web_item_name"), - "image": web_item_data.get("website_image"), - "warehouse": web_item_data.get("website_warehouse"), - "route": web_item_data.get("route"), - } - - if not frappe.db.exists("Wishlist", frappe.session.user): - # initialise wishlist - wishlist = frappe.get_doc({"doctype": "Wishlist"}) - wishlist.user = frappe.session.user - wishlist.append("items", wished_item_dict) - wishlist.save(ignore_permissions=True) - else: - wishlist = frappe.get_doc("Wishlist", frappe.session.user) - item = wishlist.append("items", wished_item_dict) - item.db_insert() - - if hasattr(frappe.local, "cookie_manager"): - frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items))) - - -@frappe.whitelist() -def remove_from_wishlist(item_code): - if frappe.db.exists("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}): - frappe.db.delete("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}) - frappe.db.commit() # nosemgrep - - wishlist_items = frappe.db.get_values("Wishlist Item", filters={"parent": frappe.session.user}) - - if hasattr(frappe.local, "cookie_manager"): - frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist_items))) diff --git a/erpnext/e_commerce/doctype/wishlist_item/__init__.py b/erpnext/e_commerce/doctype/wishlist_item/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.json b/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.json deleted file mode 100644 index c0414a7f8e..0000000000 --- a/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "actions": [], - "creation": "2021-03-10 19:03:00.662714", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "item_code", - "website_item", - "web_item_name", - "column_break_3", - "item_name", - "item_group", - "item_details_section", - "description", - "column_break_7", - "route", - "image", - "image_view", - "section_break_8", - "warehouse_section", - "warehouse" - ], - "fields": [ - { - "fetch_from": "website_item.item_code", - "fetch_if_empty": 1, - "fieldname": "item_code", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Code", - "options": "Item", - "reqd": 1 - }, - { - "fieldname": "website_item", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Website Item", - "options": "Website Item", - "read_only": 1 - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fetch_from": "item_code.item_name", - "fetch_if_empty": 1, - "fieldname": "item_name", - "fieldtype": "Data", - "label": "Item Name", - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "item_details_section", - "fieldtype": "Section Break", - "label": "Item Details", - "read_only": 1 - }, - { - "fetch_from": "item_code.description", - "fetch_if_empty": 1, - "fieldname": "description", - "fieldtype": "Text Editor", - "label": "Description", - "read_only": 1 - }, - { - "fieldname": "column_break_7", - "fieldtype": "Column Break" - }, - { - "fetch_from": "item_code.image", - "fetch_if_empty": 1, - "fieldname": "image", - "fieldtype": "Attach", - "hidden": 1, - "label": "Image" - }, - { - "fetch_from": "item_code.image", - "fetch_if_empty": 1, - "fieldname": "image_view", - "fieldtype": "Image", - "hidden": 1, - "label": "Image View", - "options": "image", - "print_hide": 1 - }, - { - "fieldname": "warehouse_section", - "fieldtype": "Section Break", - "label": "Warehouse" - }, - { - "fieldname": "warehouse", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Warehouse", - "options": "Warehouse", - "read_only": 1 - }, - { - "fieldname": "section_break_8", - "fieldtype": "Section Break" - }, - { - "fetch_from": "item_code.item_group", - "fetch_if_empty": 1, - "fieldname": "item_group", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group", - "read_only": 1 - }, - { - "fetch_from": "website_item.route", - "fetch_if_empty": 1, - "fieldname": "route", - "fieldtype": "Small Text", - "label": "Route", - "read_only": 1 - }, - { - "fetch_from": "website_item.web_item_name", - "fetch_if_empty": 1, - "fieldname": "web_item_name", - "fieldtype": "Data", - "label": "Website Item Name", - "read_only": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-08-09 10:30:41.964802", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Wishlist Item", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py b/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py deleted file mode 100644 index 75ebccbc1b..0000000000 --- a/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py +++ /dev/null @@ -1,10 +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.model.document import Document - - -class WishlistItem(Document): - pass diff --git a/erpnext/e_commerce/legacy_search.py b/erpnext/e_commerce/legacy_search.py deleted file mode 100644 index ef8e86d442..0000000000 --- a/erpnext/e_commerce/legacy_search.py +++ /dev/null @@ -1,134 +0,0 @@ -import frappe -from frappe.search.full_text_search import FullTextSearch -from frappe.utils import strip_html_tags -from whoosh.analysis import StemmingAnalyzer -from whoosh.fields import ID, KEYWORD, TEXT, Schema -from whoosh.qparser import FieldsPlugin, MultifieldParser, WildcardPlugin -from whoosh.query import Prefix - -# TODO: Make obsolete -INDEX_NAME = "products" - - -class ProductSearch(FullTextSearch): - """Wrapper for WebsiteSearch""" - - def get_schema(self): - return Schema( - title=TEXT(stored=True, field_boost=1.5), - name=ID(stored=True), - path=ID(stored=True), - content=TEXT(stored=True, analyzer=StemmingAnalyzer()), - keywords=KEYWORD(stored=True, scorable=True, commas=True), - ) - - def get_id(self): - return "name" - - def get_items_to_index(self): - """Get all routes to be indexed, this includes the static pages - in www/ and routes from published documents - - Returns: - self (object): FullTextSearch Instance - """ - items = get_all_published_items() - documents = [self.get_document_to_index(item) for item in items] - return documents - - def get_document_to_index(self, item): - try: - item = frappe.get_doc("Item", item) - title = item.item_name - keywords = [item.item_group] - - if item.brand: - keywords.append(item.brand) - - if item.website_image_alt: - keywords.append(item.website_image_alt) - - if item.has_variants and item.variant_based_on == "Item Attribute": - keywords = keywords + [attr.attribute for attr in item.attributes] - - if item.web_long_description: - content = strip_html_tags(item.web_long_description) - elif item.description: - content = strip_html_tags(item.description) - - return frappe._dict( - title=title, - name=item.name, - path=item.route, - content=content, - keywords=", ".join(keywords), - ) - except Exception: - pass - - def search(self, text, scope=None, limit=20): - """Search from the current index - - Args: - text (str): String to search for - scope (str, optional): Scope to limit the search. Defaults to None. - limit (int, optional): Limit number of search results. Defaults to 20. - - Returns: - [List(_dict)]: Search results - """ - ix = self.get_index() - - results = None - out = [] - - with ix.searcher() as searcher: - parser = MultifieldParser(["title", "content", "keywords"], ix.schema) - parser.remove_plugin_class(FieldsPlugin) - parser.remove_plugin_class(WildcardPlugin) - query = parser.parse(text) - - filter_scoped = None - if scope: - filter_scoped = Prefix(self.id, scope) - results = searcher.search(query, limit=limit, filter=filter_scoped) - - for r in results: - out.append(self.parse_result(r)) - - return out - - def parse_result(self, result): - title_highlights = result.highlights("title") - content_highlights = result.highlights("content") - keyword_highlights = result.highlights("keywords") - - return frappe._dict( - title=result["title"], - path=result["path"], - keywords=result["keywords"], - title_highlights=title_highlights, - content_highlights=content_highlights, - keyword_highlights=keyword_highlights, - ) - - -def get_all_published_items(): - return frappe.get_all( - "Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code" - ) - - -def update_index_for_path(path): - search = ProductSearch(INDEX_NAME) - return search.update_index_by_name(path) - - -def remove_document_from_index(path): - search = ProductSearch(INDEX_NAME) - return search.remove_document_from_index(path) - - -def build_index_for_all_routes(): - search = ProductSearch(INDEX_NAME) - return search.build() diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py deleted file mode 100644 index e5e5e97f86..0000000000 --- a/erpnext/e_commerce/product_data_engine/filters.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt -import frappe -from frappe.utils import floor - - -class ProductFiltersBuilder: - def __init__(self, item_group=None): - if not item_group: - self.doc = frappe.get_doc("E Commerce Settings") - else: - self.doc = frappe.get_doc("Item Group", item_group) - - self.item_group = item_group - - def get_field_filters(self): - from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website - - if not self.item_group and not self.doc.enable_field_filters: - return - - fields, filter_data = [], [] - filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings - - # filter valid field filters i.e. those that exist in Website Item - web_item_meta = frappe.get_meta("Website Item", cached=True) - fields = [ - web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field) - ] - - for df in fields: - item_filters, item_or_filters = {"published": 1}, [] - link_doctype_values = self.get_filtered_link_doctype_records(df) - - if df.fieldtype == "Link": - if self.item_group: - include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants") - if include_child: - include_groups = get_child_groups_for_website(self.item_group, include_self=True) - include_groups = [x.name for x in include_groups] - item_or_filters.extend( - [ - ["item_group", "in", include_groups], - ["Website Item Group", "item_group", "=", self.item_group], # consider website item groups - ] - ) - else: - item_or_filters.extend( - [ - ["item_group", "=", self.item_group], - ["Website Item Group", "item_group", "=", self.item_group], # consider website item groups - ] - ) - - # exclude variants if mentioned in settings - if frappe.db.get_single_value("E Commerce Settings", "hide_variants"): - item_filters["variant_of"] = ["is", "not set"] - - # Get link field values attached to published items - item_values = frappe.get_all( - "Website Item", - fields=[df.fieldname], - filters=item_filters, - or_filters=item_or_filters, - distinct="True", - pluck=df.fieldname, - ) - - values = list(set(item_values) & link_doctype_values) # intersection of both - else: - # table multiselect - values = list(link_doctype_values) - - # Remove None - if None in values: - values.remove(None) - - if values: - filter_data.append([df, values]) - - return filter_data - - def get_filtered_link_doctype_records(self, field): - """ - Get valid link doctype records depending on filters. - Apply enable/disable/show_in_website filter. - Returns: - set: A set containing valid record names - """ - link_doctype = field.get_link_doctype() - meta = frappe.get_meta(link_doctype, cached=True) if link_doctype else None - if meta: - filters = self.get_link_doctype_filters(meta) - link_doctype_values = set(d.name for d in frappe.get_all(link_doctype, filters)) - - return link_doctype_values if meta else set() - - def get_link_doctype_filters(self, meta): - "Filters for Link Doctype eg. 'show_in_website'." - filters = {} - if not meta: - return filters - - if meta.has_field("enabled"): - filters["enabled"] = 1 - if meta.has_field("disabled"): - filters["disabled"] = 0 - if meta.has_field("show_in_website"): - filters["show_in_website"] = 1 - - return filters - - def get_attribute_filters(self): - if not self.item_group and not self.doc.enable_attribute_filters: - return - - attributes = [row.attribute for row in self.doc.filter_attributes] - - if not attributes: - return [] - - result = frappe.get_all( - "Item Variant Attribute", - filters={"attribute": ["in", attributes], "attribute_value": ["is", "set"]}, - fields=["attribute", "attribute_value"], - distinct=True, - ) - - attribute_value_map = {} - for d in result: - attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value) - - out = [] - for name, values in attribute_value_map.items(): - out.append(frappe._dict(name=name, item_attribute_values=values)) - return out - - def get_discount_filters(self, discounts): - discount_filters = [] - - # [25.89, 60.5] min max - min_discount, max_discount = discounts[0], discounts[1] - # [25, 60] rounded min max - min_range_absolute, max_range_absolute = floor(min_discount), floor(max_discount) - - min_range = int(min_discount - (min_range_absolute % 10)) # 20 - max_range = int(max_discount - (max_range_absolute % 10)) # 60 - - min_range = ( - (min_range + 10) if min_range != min_range_absolute else min_range - ) # 30 (upper limit of 25.89 in range of 10) - max_range = (max_range + 10) if max_range != max_range_absolute else max_range # 60 - - for discount in range(min_range, (max_range + 1), 10): - label = f"{discount}% and below" - discount_filters.append([discount, label]) - - return discount_filters diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py deleted file mode 100644 index 975f87608a..0000000000 --- a/erpnext/e_commerce/product_data_engine/query.py +++ /dev/null @@ -1,321 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from frappe.utils import flt - -from erpnext.e_commerce.doctype.item_review.item_review import get_customer -from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website -from erpnext.utilities.product import get_non_stock_item_status - - -class ProductQuery: - """Query engine for product listing - - Attributes: - fields (list): Fields to fetch in query - conditions (string): Conditions for query building - or_conditions (string): Search conditions - page_length (Int): Length of page for the query - settings (Document): E Commerce Settings DocType - """ - - def __init__(self): - self.settings = frappe.get_doc("E Commerce Settings") - self.page_length = self.settings.products_per_page or 20 - - self.or_filters = [] - self.filters = [["published", "=", 1]] - self.fields = [ - "web_item_name", - "name", - "item_name", - "item_code", - "website_image", - "variant_of", - "has_variants", - "item_group", - "web_long_description", - "short_description", - "route", - "website_warehouse", - "ranking", - "on_backorder", - ] - - def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): - """ - Args: - attributes (dict, optional): Item Attribute filters - fields (dict, optional): Field level filters - search_term (str, optional): Search term to lookup - start (int, optional): Page start - - Returns: - dict: Dict containing items, item count & discount range - """ - # track if discounts included in field filters - self.filter_with_discount = bool(fields.get("discount")) - result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0 - - if fields: - self.build_fields_filters(fields) - if item_group: - self.build_item_group_filters(item_group) - if search_term: - self.build_search_filters(search_term) - if self.settings.hide_variants: - self.filters.append(["variant_of", "is", "not set"]) - - # query results - if attributes: - result, count = self.query_items_with_attributes(attributes, start) - else: - result, count = self.query_items(start=start) - - # sort combined results by ranking - result = sorted(result, key=lambda x: x.get("ranking"), reverse=True) - - if self.settings.enabled: - cart_items = self.get_cart_items() - - result, discount_list = self.add_display_details(result, discount_list, cart_items) - - discounts = [] - if discount_list: - discounts = [min(discount_list), max(discount_list)] - - result = self.filter_results_by_discount(fields, result) - - return {"items": result, "items_count": count, "discounts": discounts} - - def query_items(self, start=0): - """Build a query to fetch Website Items based on field filters.""" - # MySQL does not support offset without limit, - # frappe does not accept two parameters for limit - # https://dev.mysql.com/doc/refman/8.0/en/select.html#id4651989 - count_items = frappe.db.get_all( - "Website Item", - filters=self.filters, - or_filters=self.or_filters, - limit_page_length=184467440737095516, - limit_start=start, # get all items from this offset for total count ahead - order_by="ranking desc", - ) - count = len(count_items) - - # If discounts included, return all rows. - # Slice after filtering rows with discount (See `filter_results_by_discount`). - # Slicing before hand will miss discounted items on the 3rd or 4th page. - # Discounts are fetched on computing Pricing Rules so we cannot query them directly. - page_length = 184467440737095516 if self.filter_with_discount else self.page_length - - items = frappe.db.get_all( - "Website Item", - fields=self.fields, - filters=self.filters, - or_filters=self.or_filters, - limit_page_length=page_length, - limit_start=start, - order_by="ranking desc", - ) - - return items, count - - def query_items_with_attributes(self, attributes, start=0): - """Build a query to fetch Website Items based on field & attribute filters.""" - item_codes = [] - - for attribute, values in attributes.items(): - if not isinstance(values, list): - values = [values] - - # get items that have selected attribute & value - item_code_list = frappe.db.get_all( - "Item", - fields=["item_code"], - filters=[ - ["published_in_website", "=", 1], - ["Item Variant Attribute", "attribute", "=", attribute], - ["Item Variant Attribute", "attribute_value", "in", values], - ], - ) - item_codes.append({x.item_code for x in item_code_list}) - - if item_codes: - item_codes = list(set.intersection(*item_codes)) - self.filters.append(["item_code", "in", item_codes]) - - items, count = self.query_items(start=start) - - return items, count - - def build_fields_filters(self, filters): - """Build filters for field values - - Args: - filters (dict): Filters - """ - for field, values in filters.items(): - if not values or field == "discount": - continue - - # handle multiselect fields in filter addition - meta = frappe.get_meta("Website Item", cached=True) - df = meta.get_field(field) - if df.fieldtype == "Table MultiSelect": - child_doctype = df.options - child_meta = frappe.get_meta(child_doctype, cached=True) - fields = child_meta.get("fields") - if fields: - self.filters.append([child_doctype, fields[0].fieldname, "IN", values]) - elif isinstance(values, list): - # If value is a list use `IN` query - self.filters.append([field, "in", values]) - else: - # `=` will be faster than `IN` for most cases - self.filters.append([field, "=", values]) - - def build_item_group_filters(self, item_group): - "Add filters for Item group page and include Website Item Groups." - from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website - - item_group_filters = [] - - item_group_filters.append(["Website Item", "item_group", "=", item_group]) - # Consider Website Item Groups - item_group_filters.append(["Website Item Group", "item_group", "=", item_group]) - - if frappe.db.get_value("Item Group", item_group, "include_descendants"): - # include child item group's items as well - # eg. Group Node A, will show items of child 1 and child 2 as well - # on it's web page - include_groups = get_child_groups_for_website(item_group, include_self=True) - include_groups = [x.name for x in include_groups] - item_group_filters.append(["Website Item", "item_group", "in", include_groups]) - - self.or_filters.extend(item_group_filters) - - def build_search_filters(self, search_term): - """Query search term in specified fields - - Args: - search_term (str): Search candidate - """ - # Default fields to search from - default_fields = {"item_code", "item_name", "web_long_description", "item_group"} - - # Get meta search fields - meta = frappe.get_meta("Website Item") - meta_fields = set(meta.get_search_fields()) - - # Join the meta fields and default fields set - search_fields = default_fields.union(meta_fields) - if frappe.db.count("Website Item", cache=True) > 50000: - search_fields.discard("web_long_description") - - # Build or filters for query - search = "%{}%".format(search_term) - for field in search_fields: - self.or_filters.append([field, "like", search]) - - def add_display_details(self, result, discount_list, cart_items): - """Add price and availability details in result.""" - for item in result: - product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get( - "product_info" - ) - - if product_info and product_info["price"]: - # update/mutate item and discount_list objects - self.get_price_discount_info(item, product_info["price"], discount_list) - - if self.settings.show_stock_availability: - self.get_stock_availability(item) - - item.in_cart = item.item_code in cart_items - - item.wished = False - if frappe.db.exists( - "Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user} - ): - item.wished = True - - return result, discount_list - - def get_price_discount_info(self, item, price_object, discount_list): - """Modify item object and add price details.""" - fields = ["formatted_mrp", "formatted_price", "price_list_rate"] - for field in fields: - item[field] = price_object.get(field) - - if price_object.get("discount_percent"): - item.discount_percent = flt(price_object.discount_percent) - discount_list.append(price_object.discount_percent) - - if item.formatted_mrp: - item.discount = price_object.get("formatted_discount_percent") or price_object.get( - "formatted_discount_rate" - ) - - def get_stock_availability(self, item): - from erpnext.templates.pages.wishlist import ( - get_stock_availability as get_stock_availability_from_template, - ) - - """Modify item object and add stock details.""" - item.in_stock = False - warehouse = item.get("website_warehouse") - is_stock_item = frappe.get_cached_value("Item", item.item_code, "is_stock_item") - - if item.get("on_backorder"): - return - - if not is_stock_item: - if warehouse: - # product bundle case - item.in_stock = get_non_stock_item_status(item.item_code, "website_warehouse") - else: - item.in_stock = True - elif warehouse: - item.in_stock = get_stock_availability_from_template(item.item_code, warehouse) - - def get_cart_items(self): - customer = get_customer(silent=True) - if customer: - quotation = frappe.get_all( - "Quotation", - fields=["name"], - filters={ - "party_name": customer, - "contact_email": frappe.session.user, - "order_type": "Shopping Cart", - "docstatus": 0, - }, - order_by="modified desc", - limit_page_length=1, - ) - if quotation: - items = frappe.get_all( - "Quotation Item", fields=["item_code"], filters={"parent": quotation[0].get("name")} - ) - items = [row.item_code for row in items] - return items - - return [] - - def filter_results_by_discount(self, fields, result): - if fields and fields.get("discount"): - discount_percent = frappe.utils.flt(fields["discount"][0]) - result = [ - row - for row in result - if row.get("discount_percent") and row.discount_percent <= discount_percent - ] - - if self.filter_with_discount: - # no limit was added to results while querying - # slice results manually - result[: self.page_length] - - return result diff --git a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py deleted file mode 100644 index 45bc20ece6..0000000000 --- a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import unittest - -import frappe - -from erpnext.e_commerce.api import get_product_filter_data -from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item - -test_dependencies = ["Item", "Item Group"] - - -class TestItemGroupProductDataEngine(unittest.TestCase): - "Test Products & Sub-Category Querying for Product Listing on Item Group Page." - - def setUp(self): - item_codes = [ - ("Test Mobile A", "_Test Item Group B"), - ("Test Mobile B", "_Test Item Group B"), - ("Test Mobile C", "_Test Item Group B - 1"), - ("Test Mobile D", "_Test Item Group B - 1"), - ("Test Mobile E", "_Test Item Group B - 2"), - ] - for item in item_codes: - item_code = item[0] - item_args = {"item_group": item[1]} - if not frappe.db.exists("Website Item", {"item_code": item_code}): - create_regular_web_item(item_code, item_args=item_args) - - frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1) - frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1) - - def tearDown(self): - frappe.db.rollback() - - def test_product_listing_in_item_group(self): - "Test if only products belonging to the Item Group are fetched." - result = get_product_filter_data( - query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B", - } - ) - - items = result.get("items") - item_codes = [item.get("item_code") for item in items] - - self.assertEqual(len(items), 2) - self.assertIn("Test Mobile A", item_codes) - self.assertNotIn("Test Mobile C", item_codes) - - def test_products_in_multiple_item_groups(self): - """Test if product is visible on multiple item group pages barring its own.""" - website_item = frappe.get_doc("Website Item", {"item_code": "Test Mobile E"}) - - # show item belonging to '_Test Item Group B - 2' in '_Test Item Group B - 1' as well - website_item.append("website_item_groups", {"item_group": "_Test Item Group B - 1"}) - website_item.save() - - result = get_product_filter_data( - query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B - 1", - } - ) - - items = result.get("items") - item_codes = [item.get("item_code") for item in items] - - self.assertEqual(len(items), 3) - self.assertIn("Test Mobile E", item_codes) # visible in other item groups - self.assertIn("Test Mobile C", item_codes) - self.assertIn("Test Mobile D", item_codes) - - result = get_product_filter_data( - query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B - 2", - } - ) - - items = result.get("items") - - self.assertEqual(len(items), 1) - self.assertEqual(items[0].get("item_code"), "Test Mobile E") # visible in own item group - - def test_item_group_with_sub_groups(self): - "Test Valid Sub Item Groups in Item Group Page." - frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0) - - result = get_product_filter_data( - query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B", - } - ) - - self.assertTrue(bool(result.get("sub_categories"))) - - child_groups = [d.name for d in result.get("sub_categories")] - # check if child group is fetched if shown in website - self.assertIn("_Test Item Group B - 1", child_groups) - - frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1) - result = get_product_filter_data( - query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B", - } - ) - child_groups = [d.name for d in result.get("sub_categories")] - - # check if child group is fetched if shown in website - self.assertIn("_Test Item Group B - 1", child_groups) - self.assertIn("_Test Item Group B - 2", child_groups) - - def test_item_group_page_with_descendants_included(self): - """ - Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3). - > _Test Item Group B [Level 1] - > _Test Item Group B - 1 [Level 2] - > _Test Item Group B - 1 - 1 [Level 3] - """ - frappe.get_doc( - { # create Level 3 nested child group - "doctype": "Item Group", - "is_group": 1, - "item_group_name": "_Test Item Group B - 1 - 1", - "parent_item_group": "_Test Item Group B - 1", - } - ).insert() - - create_regular_web_item( # create an item belonging to level 3 item group - "Test Mobile F", item_args={"item_group": "_Test Item Group B - 1 - 1"} - ) - - frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1) - - # enable 'include descendants' in Level 1 - frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1) - - result = get_product_filter_data( - query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B", - } - ) - - items = result.get("items") - item_codes = [item.get("item_code") for item in items] - - # check if all sub groups' items are pulled - self.assertEqual(len(items), 6) - self.assertIn("Test Mobile A", item_codes) - self.assertIn("Test Mobile C", item_codes) - self.assertIn("Test Mobile E", item_codes) - self.assertIn("Test Mobile F", item_codes) diff --git a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py deleted file mode 100644 index c3b6ed5da2..0000000000 --- a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py +++ /dev/null @@ -1,348 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import unittest - -import frappe - -from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import ( - setup_e_commerce_settings, -) -from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item -from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder -from erpnext.e_commerce.product_data_engine.query import ProductQuery - -test_dependencies = ["Item", "Item Group"] - - -class TestProductDataEngine(unittest.TestCase): - "Test Products Querying and Filters for Product Listing." - - @classmethod - def setUpClass(cls): - item_codes = [ - ("Test 11I Laptop", "Products"), # rank 1 - ("Test 12I Laptop", "Products"), # rank 2 - ("Test 13I Laptop", "Products"), # rank 3 - ("Test 14I Laptop", "Raw Material"), # rank 4 - ("Test 15I Laptop", "Raw Material"), # rank 5 - ("Test 16I Laptop", "Raw Material"), # rank 6 - ("Test 17I Laptop", "Products"), # rank 7 - ] - for index, item in enumerate(item_codes, start=1): - item_code = item[0] - item_args = {"item_group": item[1]} - web_args = {"ranking": index} - if not frappe.db.exists("Website Item", {"item_code": item_code}): - create_regular_web_item(item_code, item_args=item_args, web_args=web_args) - - setup_e_commerce_settings( - { - "products_per_page": 4, - "enable_field_filters": 1, - "filter_fields": [{"fieldname": "item_group"}], - "enable_attribute_filters": 1, - "filter_attributes": [{"attribute": "Test Size"}], - "company": "_Test Company", - "enabled": 1, - "default_customer_group": "_Test Customer Group", - "price_list": "_Test Price List India", - } - ) - frappe.local.shopping_cart_settings = None - - @classmethod - def tearDownClass(cls): - frappe.db.rollback() - - def test_product_list_ordering_and_paging(self): - "Test if website items appear by ranking on different pages." - engine = ProductQuery() - result = engine.query(attributes={}, fields={}, search_term=None, start=0, item_group=None) - items = result.get("items") - - self.assertIsNotNone(items) - self.assertEqual(len(items), 4) - self.assertGreater(result.get("items_count"), 4) - - # check if items appear as per ranking set in setUpClass - self.assertEqual(items[0].get("item_code"), "Test 17I Laptop") - self.assertEqual(items[1].get("item_code"), "Test 16I Laptop") - self.assertEqual(items[2].get("item_code"), "Test 15I Laptop") - self.assertEqual(items[3].get("item_code"), "Test 14I Laptop") - - # check next page - result = engine.query(attributes={}, fields={}, search_term=None, start=4, item_group=None) - items = result.get("items") - - # check if items appear as per ranking set in setUpClass on next page - self.assertEqual(items[0].get("item_code"), "Test 13I Laptop") - self.assertEqual(items[1].get("item_code"), "Test 12I Laptop") - self.assertEqual(items[2].get("item_code"), "Test 11I Laptop") - - def test_change_product_ranking(self): - "Test if item on second page appear on first if ranking is changed." - item_code = "Test 12I Laptop" - old_ranking = frappe.db.get_value("Website Item", {"item_code": item_code}, "ranking") - - # low rank, appears on second page - self.assertEqual(old_ranking, 2) - - # set ranking as highest rank - frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", 10) - - engine = ProductQuery() - result = engine.query(attributes={}, fields={}, search_term=None, start=0, item_group=None) - items = result.get("items") - - # check if item is the first item on the first page - self.assertEqual(items[0].get("item_code"), item_code) - self.assertEqual(items[1].get("item_code"), "Test 17I Laptop") - - # tear down - frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", old_ranking) - - def test_product_list_field_filter_builder(self): - "Test if field filters are fetched correctly." - frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 0) - - filter_engine = ProductFiltersBuilder() - field_filters = filter_engine.get_field_filters() - - # Web Items belonging to 'Products' and 'Raw Material' are available - # but only 'Products' has 'show_in_website' enabled - item_group_filters = field_filters[0] - docfield = item_group_filters[0] - valid_item_groups = item_group_filters[1] - - self.assertEqual(docfield.options, "Item Group") - self.assertIn("Products", valid_item_groups) - self.assertNotIn("Raw Material", valid_item_groups) - - frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 1) - field_filters = filter_engine.get_field_filters() - - #'Products' and 'Raw Materials' both have 'show_in_website' enabled - item_group_filters = field_filters[0] - docfield = item_group_filters[0] - valid_item_groups = item_group_filters[1] - - self.assertEqual(docfield.options, "Item Group") - self.assertIn("Products", valid_item_groups) - self.assertIn("Raw Material", valid_item_groups) - - def test_product_list_with_field_filter(self): - "Test if field filters are applied correctly." - field_filters = {"item_group": "Raw Material"} - - engine = ProductQuery() - result = engine.query( - attributes={}, fields=field_filters, search_term=None, start=0, item_group=None - ) - items = result.get("items") - - # check if only 'Raw Material' are fetched in the right order - self.assertEqual(len(items), 3) - self.assertEqual(items[0].get("item_code"), "Test 16I Laptop") - self.assertEqual(items[1].get("item_code"), "Test 15I Laptop") - - # def test_product_list_with_field_filter_table_multiselect(self): - # TODO - # pass - - def test_product_list_attribute_filter_builder(self): - "Test if attribute filters are fetched correctly." - create_variant_web_item() - - filter_engine = ProductFiltersBuilder() - attribute_filter = filter_engine.get_attribute_filters()[0] - attribute_values = attribute_filter.item_attribute_values - - self.assertEqual(attribute_filter.name, "Test Size") - self.assertGreater(len(attribute_values), 0) - self.assertIn("Large", attribute_values) - - def test_product_list_with_attribute_filter(self): - "Test if attribute filters are applied correctly." - create_variant_web_item() - - attribute_filters = {"Test Size": ["Large"]} - engine = ProductQuery() - result = engine.query( - attributes=attribute_filters, fields={}, search_term=None, start=0, item_group=None - ) - items = result.get("items") - - # check if only items with Test Size 'Large' are fetched - self.assertEqual(len(items), 1) - self.assertEqual(items[0].get("item_code"), "Test Web Item-L") - - def test_product_list_discount_filter_builder(self): - "Test if discount filters are fetched correctly." - from erpnext.e_commerce.doctype.website_item.test_website_item import ( - make_web_item_price, - make_web_pricing_rule, - ) - - item_code = "Test 12I Laptop" - make_web_item_price(item_code=item_code) - make_web_pricing_rule(title=f"Test Pricing Rule for {item_code}", item_code=item_code, selling=1) - - setup_e_commerce_settings({"show_price": 1}) - frappe.local.shopping_cart_settings = None - - engine = ProductQuery() - result = engine.query(attributes={}, fields={}, search_term=None, start=4, item_group=None) - self.assertTrue(bool(result.get("discounts"))) - - filter_engine = ProductFiltersBuilder() - discount_filters = filter_engine.get_discount_filters(result["discounts"]) - - self.assertEqual(len(discount_filters[0]), 2) - self.assertEqual(discount_filters[0][0], 10) - self.assertEqual(discount_filters[0][1], "10% and below") - - def test_product_list_with_discount_filters(self): - "Test if discount filters are applied correctly." - from erpnext.e_commerce.doctype.website_item.test_website_item import ( - make_web_item_price, - make_web_pricing_rule, - ) - - field_filters = {"discount": [10]} - - make_web_item_price(item_code="Test 12I Laptop") - make_web_pricing_rule( - title="Test Pricing Rule for Test 12I Laptop", # 10% discount - item_code="Test 12I Laptop", - selling=1, - ) - make_web_item_price(item_code="Test 13I Laptop") - make_web_pricing_rule( - title="Test Pricing Rule for Test 13I Laptop", # 15% discount - item_code="Test 13I Laptop", - discount_percentage=15, - selling=1, - ) - - setup_e_commerce_settings({"show_price": 1}) - frappe.local.shopping_cart_settings = None - - engine = ProductQuery() - result = engine.query( - attributes={}, fields=field_filters, search_term=None, start=0, item_group=None - ) - items = result.get("items") - - # check if only product with 10% and below discount are fetched - self.assertEqual(len(items), 1) - self.assertEqual(items[0].get("item_code"), "Test 12I Laptop") - - def test_product_list_with_api(self): - "Test products listing using API." - from erpnext.e_commerce.api import get_product_filter_data - - create_variant_web_item() - - result = get_product_filter_data( - query_args={ - "field_filters": {"item_group": "Products"}, - "attribute_filters": {"Test Size": ["Large"]}, - "start": 0, - } - ) - - items = result.get("items") - - self.assertEqual(len(items), 1) - self.assertEqual(items[0].get("item_code"), "Test Web Item-L") - - def test_product_list_with_variants(self): - "Test if variants are hideen on hiding variants in settings." - create_variant_web_item() - - setup_e_commerce_settings({"enable_attribute_filters": 0, "hide_variants": 1}) - frappe.local.shopping_cart_settings = None - - attribute_filters = {"Test Size": ["Large"]} - engine = ProductQuery() - result = engine.query( - attributes=attribute_filters, fields={}, search_term=None, start=0, item_group=None - ) - items = result.get("items") - - # check if any variants are fetched even though published variant exists - self.assertEqual(len(items), 0) - - # tear down - setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0}) - - def test_custom_field_as_filter(self): - "Test if custom field functions as filter correctly." - from frappe.custom.doctype.custom_field.custom_field import create_custom_field - - create_custom_field( - "Website Item", - dict( - owner="Administrator", - fieldname="supplier", - label="Supplier", - fieldtype="Link", - options="Supplier", - insert_after="on_backorder", - ), - ) - - frappe.db.set_value( - "Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier" - ) - frappe.db.set_value( - "Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1" - ) - - settings = frappe.get_doc("E Commerce Settings") - settings.append("filter_fields", {"fieldname": "supplier"}) - settings.save() - - filter_engine = ProductFiltersBuilder() - field_filters = filter_engine.get_field_filters() - custom_filter = field_filters[1] - filter_values = custom_filter[1] - - self.assertEqual(custom_filter[0].options, "Supplier") - self.assertEqual(len(filter_values), 2) - self.assertIn("_Test Supplier", filter_values) - - # test if custom filter works in query - field_filters = {"supplier": "_Test Supplier 1"} - engine = ProductQuery() - result = engine.query( - attributes={}, fields=field_filters, search_term=None, start=0, item_group=None - ) - items = result.get("items") - - # check if only 'Raw Material' are fetched in the right order - self.assertEqual(len(items), 1) - self.assertEqual(items[0].get("item_code"), "Test 12I Laptop") - - -def create_variant_web_item(): - "Create Variant and Template Website Items." - from erpnext.controllers.item_variant import create_variant - from erpnext.e_commerce.doctype.website_item.website_item import make_website_item - from erpnext.stock.doctype.item.test_item import make_item - - make_item( - "Test Web Item", - { - "has_variant": 1, - "variant_based_on": "Item Attribute", - "attributes": [{"attribute": "Test Size"}], - }, - ) - if not frappe.db.exists("Item", "Test Web Item-L"): - variant = create_variant("Test Web Item", {"Test Size": "Large"}) - variant.save() - - if not frappe.db.exists("Website Item", {"variant_of": "Test Web Item"}): - make_website_item(variant, save=True) diff --git a/erpnext/e_commerce/product_ui/grid.js b/erpnext/e_commerce/product_ui/grid.js deleted file mode 100644 index 20a6c30b52..0000000000 --- a/erpnext/e_commerce/product_ui/grid.js +++ /dev/null @@ -1,201 +0,0 @@ -erpnext.ProductGrid = class { - /* Options: - - items: Items - - settings: E Commerce Settings - - products_section: Products Wrapper - - preference: If preference is not grid view, render but hide - */ - constructor(options) { - Object.assign(this, options); - - if (this.preference !== "Grid View") { - this.products_section.addClass("hidden"); - } - - this.products_section.empty(); - this.make(); - } - - make() { - let me = this; - let html = ``; - - this.items.forEach(item => { - let title = item.web_item_name || item.item_name || item.item_code || ""; - title = title.length > 90 ? title.substr(0, 90) + "..." : title; - - html += `
`; - html += me.get_image_html(item, title); - html += me.get_card_body_html(item, title, me.settings); - html += `
`; - }); - - let $product_wrapper = this.products_section; - $product_wrapper.append(html); - } - - get_image_html(item, title) { - let image = item.website_image; - - if (image) { - return ` -
- - ${ title } - -
- `; - } else { - return ` - - `; - } - } - - get_card_body_html(item, title, settings) { - let body_html = ` -
-
- `; - body_html += this.get_title(item, title); - - // get floating elements - if (!item.has_variants) { - if (settings.enable_wishlist) { - body_html += this.get_wishlist_icon(item); - } - if (settings.enabled) { - body_html += this.get_cart_indicator(item); - } - - } - - body_html += `
`; - body_html += `
${ item.item_group || '' }
`; - - if (item.formatted_price) { - body_html += this.get_price_html(item); - } - - body_html += this.get_stock_availability(item, settings); - body_html += this.get_primary_button(item, settings); - body_html += `
`; // close div on line 49 - - return body_html; - } - - get_title(item, title) { - let title_html = ` - -
- ${ title || '' } -
-
- `; - return title_html; - } - - get_wishlist_icon(item) { - let icon_class = item.wished ? "wished" : "not-wished"; - return ` - - `; - } - - get_cart_indicator(item) { - return ` -
- 1 -
- `; - } - - get_price_html(item) { - let price_html = ` -
- ${ item.formatted_price || '' } - `; - - if (item.formatted_mrp) { - price_html += ` - - ${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" } - - - ${ item.discount } OFF - - `; - } - price_html += `
`; - return price_html; - } - - get_stock_availability(item, settings) { - if (settings.show_stock_availability && !item.has_variants) { - if (item.on_backorder) { - return ` - - ${ __("Available on backorder") } - - `; - } else if (!item.in_stock) { - return ` - - ${ __("Out of stock") } - - `; - } - } - - return ``; - } - - get_primary_button(item, settings) { - if (item.has_variants) { - return ` - -
- ${ __('Explore') } -
-
- `; - } else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) { - return ` -
- - - - - - ${ settings.enable_checkout ? __('Add to Cart') : __('Add to Quote') } -
- - -
- ${ settings.enable_checkout ? __('Go to Cart') : __('Go to Quote') } -
-
- `; - } else { - return ``; - } - } -}; \ No newline at end of file diff --git a/erpnext/e_commerce/product_ui/list.js b/erpnext/e_commerce/product_ui/list.js deleted file mode 100644 index c8fd7672c8..0000000000 --- a/erpnext/e_commerce/product_ui/list.js +++ /dev/null @@ -1,205 +0,0 @@ -erpnext.ProductList = class { - /* Options: - - items: Items - - settings: E Commerce Settings - - products_section: Products Wrapper - - preference: If preference is not list view, render but hide - */ - constructor(options) { - Object.assign(this, options); - - if (this.preference !== "List View") { - this.products_section.addClass("hidden"); - } - - this.products_section.empty(); - this.make(); - } - - make() { - let me = this; - let html = `

`; - - this.items.forEach(item => { - let title = item.web_item_name || item.item_name || item.item_code || ""; - title = title.length > 200 ? title.substr(0, 200) + "..." : title; - - html += `
`; - html += me.get_image_html(item, title, me.settings); - html += me.get_row_body_html(item, title, me.settings); - html += `
`; - }); - - let $product_wrapper = this.products_section; - $product_wrapper.append(html); - } - - get_image_html(item, title, settings) { - let image = item.website_image; - let wishlist_enabled = !item.has_variants && settings.enable_wishlist; - let image_html = ``; - - if (image) { - image_html += ` -
- - ${ title } - - ${ wishlist_enabled ? this.get_wishlist_icon(item): '' } -
- `; - } else { - image_html += ` -
- -
- ${ frappe.get_abbr(title) } -
-
- ${ wishlist_enabled ? this.get_wishlist_icon(item): '' } -
- `; - } - - return image_html; - } - - get_row_body_html(item, title, settings) { - let body_html = `
`; - body_html += this.get_title_html(item, title, settings); - body_html += this.get_item_details(item, settings); - body_html += `
`; - return body_html; - } - - get_title_html(item, title, settings) { - let title_html = `
`; - title_html += ` - - `; - - if (settings.enabled) { - title_html += `
`; - title_html += this.get_primary_button(item, settings); - title_html += `
`; - } - title_html += `
`; - - return title_html; - } - - get_item_details(item, settings) { - let details = ` -

- ${ item.item_group } | Item Code : ${ item.item_code } -

-
- ${ item.short_description || '' } -
-
- ${ item.formatted_price || '' } - `; - - if (item.formatted_mrp) { - details += ` - - ${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" } - - - ${ item.discount } OFF - - `; - } - - details += this.get_stock_availability(item, settings); - details += `
`; - - return details; - } - - get_stock_availability(item, settings) { - if (settings.show_stock_availability && !item.has_variants) { - if (item.on_backorder) { - return ` -
- - ${ __("Available on backorder") } - - `; - } else if (!item.in_stock) { - return ` -
- ${ __("Out of stock") } - `; - } - } - return ``; - } - - get_wishlist_icon(item) { - let icon_class = item.wished ? "wished" : "not-wished"; - - return ` - - `; - } - - get_primary_button(item, settings) { - if (item.has_variants) { - return ` - -
- ${ __('Explore') } -
-
- `; - } else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) { - return ` -
- - - - - - ${ settings.enable_checkout ? __('Add to Cart') : __('Add to Quote') } -
- -
- 1 -
- - -
- ${ settings.enable_checkout ? __('Go to Cart') : __('Go to Quote') } -
-
- `; - } else { - return ``; - } - } - -}; diff --git a/erpnext/e_commerce/product_ui/search.js b/erpnext/e_commerce/product_ui/search.js deleted file mode 100644 index 1688cc1fb6..0000000000 --- a/erpnext/e_commerce/product_ui/search.js +++ /dev/null @@ -1,244 +0,0 @@ -erpnext.ProductSearch = class { - constructor(opts) { - /* Options: search_box_id (for custom search box) */ - $.extend(this, opts); - this.MAX_RECENT_SEARCHES = 4; - this.search_box_id = this.search_box_id || "#search-box"; - this.searchBox = $(this.search_box_id); - - this.setupSearchDropDown(); - this.bindSearchAction(); - } - - setupSearchDropDown() { - this.search_area = $("#dropdownMenuSearch"); - this.setupSearchResultContainer(); - this.populateRecentSearches(); - } - - bindSearchAction() { - let me = this; - - // Show Search dropdown - this.searchBox.on("focus", () => { - this.search_dropdown.removeClass("hidden"); - }); - - // If click occurs outside search input/results, hide results. - // Click can happen anywhere on the page - $("body").on("click", (e) => { - let searchEvent = $(e.target).closest(this.search_box_id).length; - let resultsEvent = $(e.target).closest('#search-results-container').length; - let isResultHidden = this.search_dropdown.hasClass("hidden"); - - if (!searchEvent && !resultsEvent && !isResultHidden) { - this.search_dropdown.addClass("hidden"); - } - }); - - // Process search input - this.searchBox.on("input", (e) => { - let query = e.target.value; - - if (query.length == 0) { - me.populateResults(null); - me.populateCategoriesList(null); - } - - if (query.length < 3 || !query.length) return; - - frappe.call({ - method: "erpnext.templates.pages.product_search.search", - args: { - query: query - }, - callback: (data) => { - let product_results = null, category_results = null; - - // Populate product results - product_results = data.message ? data.message.product_results : null; - me.populateResults(product_results); - - // Populate categories - if (me.category_container) { - category_results = data.message ? data.message.category_results : null; - me.populateCategoriesList(category_results); - } - - // Populate recent search chips only on successful queries - if (!$.isEmptyObject(product_results) || !$.isEmptyObject(category_results)) { - me.setRecentSearches(query); - } - } - }); - - this.search_dropdown.removeClass("hidden"); - }); - } - - setupSearchResultContainer() { - this.search_dropdown = this.search_area.append(` - - `).find("#search-results-container"); - - this.setupCategoryContainer(); - this.setupProductsContainer(); - this.setupRecentsContainer(); - } - - setupProductsContainer() { - this.products_container = this.search_dropdown.append(` -
-
-
-
- `).find("#product-scroll"); - } - - setupCategoryContainer() { - this.category_container = this.search_dropdown.append(` -
-
-
-
- `).find(".category-chips"); - } - - setupRecentsContainer() { - let $recents_section = this.search_dropdown.append(` -
-
- ${ __("Recent") } -
-
- `).find(".recent-searches"); - - this.recents_container = $recents_section.append(` -
-
- `).find("#recents"); - } - - getRecentSearches() { - return JSON.parse(localStorage.getItem("recent_searches") || "[]"); - } - - attachEventListenersToChips() { - let me = this; - const chips = $(".recent-search"); - window.chips = chips; - - for (let chip of chips) { - chip.addEventListener("click", () => { - me.searchBox[0].value = chip.innerText.trim(); - - // Start search with `recent query` - me.searchBox.trigger("input"); - me.searchBox.focus(); - }); - } - } - - setRecentSearches(query) { - let recents = this.getRecentSearches(); - if (recents.length >= this.MAX_RECENT_SEARCHES) { - // Remove the `first` query - recents.splice(0, 1); - } - - if (recents.indexOf(query) >= 0) { - return; - } - - recents.push(query); - localStorage.setItem("recent_searches", JSON.stringify(recents)); - - this.populateRecentSearches(); - } - - populateRecentSearches() { - let recents = this.getRecentSearches(); - - if (!recents.length) { - this.recents_container.html(`No searches yet.`); - return; - } - - let html = ""; - recents.forEach((key) => { - html += ` - - `; - }); - - this.recents_container.html(html); - this.attachEventListenersToChips(); - } - - populateResults(product_results) { - if (!product_results || product_results.length === 0) { - let empty_html = ``; - this.products_container.html(empty_html); - return; - } - - let html = ""; - - product_results.forEach((res) => { - let thumbnail = res.thumbnail || '/assets/erpnext/images/ui-states/cart-empty-state.png'; - html += ` - - `; - }); - - this.products_container.html(html); - } - - populateCategoriesList(category_results) { - if (!category_results || category_results.length === 0) { - let empty_html = ` -
-
-
-
- `; - this.category_container.html(empty_html); - return; - } - - let html = ` -
- ${ __("Categories") } -
- `; - - category_results.forEach((category) => { - html += ` - - ${ category.name } - - `; - }); - - this.category_container.html(html); - } -}; diff --git a/erpnext/e_commerce/product_ui/views.js b/erpnext/e_commerce/product_ui/views.js deleted file mode 100644 index fb63b21a08..0000000000 --- a/erpnext/e_commerce/product_ui/views.js +++ /dev/null @@ -1,548 +0,0 @@ -erpnext.ProductView = class { - /* Options: - - View Type - - Products Section Wrapper, - - Item Group: If its an Item Group page - */ - constructor(options) { - Object.assign(this, options); - this.preference = this.view_type; - this.make(); - } - - make(from_filters=false) { - this.products_section.empty(); - this.prepare_toolbar(); - this.get_item_filter_data(from_filters); - } - - prepare_toolbar() { - this.products_section.append(` -
-
- `); - this.prepare_search(); - this.prepare_view_toggler(); - - new erpnext.ProductSearch(); - } - - prepare_view_toggler() { - - if (!$("#list").length || !$("#image-view").length) { - this.render_view_toggler(); - this.bind_view_toggler_actions(); - this.set_view_state(); - } - } - - get_item_filter_data(from_filters=false) { - // Get and render all Product related views - let me = this; - this.from_filters = from_filters; - let args = this.get_query_filters(); - - this.disable_view_toggler(true); - - frappe.call({ - method: "erpnext.e_commerce.api.get_product_filter_data", - args: { - query_args: args - }, - callback: function(result) { - if (!result || result.exc || !result.message || result.message.exc) { - me.render_no_products_section(true); - } else { - // Sub Category results are independent of Items - if (me.item_group && result.message["sub_categories"].length) { - me.render_item_sub_categories(result.message["sub_categories"]); - } - - if (!result.message["items"].length) { - // if result has no items or result is empty - me.render_no_products_section(); - } else { - // Add discount filters - me.re_render_discount_filters(result.message["filters"].discount_filters); - - // Render views - me.render_list_view(result.message["items"], result.message["settings"]); - me.render_grid_view(result.message["items"], result.message["settings"]); - - me.products = result.message["items"]; - me.product_count = result.message["items_count"]; - } - - // Bind filter actions - if (!from_filters) { - // If `get_product_filter_data` was triggered after checking a filter, - // don't touch filters unnecessarily, only data must change - // filter persistence is handle on filter change event - me.bind_filters(); - me.restore_filters_state(); - } - - // Bottom paging - me.add_paging_section(result.message["settings"]); - } - - me.disable_view_toggler(false); - } - }); - } - - disable_view_toggler(disable=false) { - $('#list').prop('disabled', disable); - $('#image-view').prop('disabled', disable); - } - - render_grid_view(items, settings) { - // loop over data and add grid html to it - let me = this; - this.prepare_product_area_wrapper("grid"); - - new erpnext.ProductGrid({ - items: items, - products_section: $("#products-grid-area"), - settings: settings, - preference: me.preference - }); - } - - render_list_view(items, settings) { - let me = this; - this.prepare_product_area_wrapper("list"); - - new erpnext.ProductList({ - items: items, - products_section: $("#products-list-area"), - settings: settings, - preference: me.preference - }); - } - - prepare_product_area_wrapper(view) { - let left_margin = view == "list" ? "ml-2" : ""; - let top_margin = view == "list" ? "mt-6" : "mt-minus-1"; - return this.products_section.append(` -
-
- `); - } - - get_query_filters() { - const filters = frappe.utils.get_query_params(); - let {field_filters, attribute_filters} = filters; - - field_filters = field_filters ? JSON.parse(field_filters) : {}; - attribute_filters = attribute_filters ? JSON.parse(attribute_filters) : {}; - - return { - field_filters: field_filters, - attribute_filters: attribute_filters, - item_group: this.item_group, - start: filters.start || null, - from_filters: this.from_filters || false - }; - } - - add_paging_section(settings) { - $(".product-paging-area").remove(); - - if (this.products) { - let paging_html = ` -
-
-
-
- `; - let query_params = frappe.utils.get_query_params(); - let start = query_params.start ? cint(JSON.parse(query_params.start)) : 0; - let page_length = settings.products_per_page || 0; - - let prev_disable = start > 0 ? "" : "disabled"; - let next_disable = (this.product_count > page_length) ? "" : "disabled"; - - paging_html += ` - `; - - paging_html += ` - - `; - - paging_html += `
`; - - $(".page_content").append(paging_html); - this.bind_paging_action(); - } - } - - prepare_search() { - $(".toolbar").append(` -
- -
- `); - } - - render_view_toggler() { - $(".toolbar").append(`
`); - - ["btn-list-view", "btn-grid-view"].forEach(view => { - let icon = view === "btn-list-view" ? "list" : "image-view"; - $(".toggle-container").append(` -
- -
- `); - }); - } - - bind_view_toggler_actions() { - $("#list").click(function() { - let $btn = $(this); - $btn.removeClass('btn-primary'); - $btn.addClass('btn-primary'); - $(".btn-grid-view").removeClass('btn-primary'); - - $("#products-grid-area").addClass("hidden"); - $("#products-list-area").removeClass("hidden"); - localStorage.setItem("product_view", "List View"); - }); - - $("#image-view").click(function() { - let $btn = $(this); - $btn.removeClass('btn-primary'); - $btn.addClass('btn-primary'); - $(".btn-list-view").removeClass('btn-primary'); - - $("#products-list-area").addClass("hidden"); - $("#products-grid-area").removeClass("hidden"); - localStorage.setItem("product_view", "Grid View"); - }); - } - - set_view_state() { - if (this.preference === "List View") { - $("#list").addClass('btn-primary'); - $("#image-view").removeClass('btn-primary'); - } else { - $("#image-view").addClass('btn-primary'); - $("#list").removeClass('btn-primary'); - } - } - - bind_paging_action() { - let me = this; - $('.btn-prev, .btn-next').click((e) => { - const $btn = $(e.target); - me.from_filters = false; - - $btn.prop('disabled', true); - const start = $btn.data('start'); - - let query_params = frappe.utils.get_query_params(); - query_params.start = start; - let path = window.location.pathname + '?' + frappe.utils.get_url_from_dict(query_params); - window.location.href = path; - }); - } - - re_render_discount_filters(filter_data) { - this.get_discount_filter_html(filter_data); - if (this.from_filters) { - // Bind filter action if triggered via filters - // if not from filter action, page load will bind actions - this.bind_discount_filter_action(); - } - // discount filters are rendered with Items (later) - // unlike the other filters - this.restore_discount_filter(); - } - - get_discount_filter_html(filter_data) { - $("#discount-filters").remove(); - if (filter_data) { - $("#product-filters").append(` -
-
${ __("Discounts") }
-
- `); - - let html = `
`; - filter_data.forEach(filter => { - html += ` -
- -
- `; - }); - html += `
`; - - $("#discount-filters").append(html); - } - } - - restore_discount_filter() { - const filters = frappe.utils.get_query_params(); - let field_filters = filters.field_filters; - if (!field_filters) return; - - field_filters = JSON.parse(field_filters); - - if (field_filters && field_filters["discount"]) { - const values = field_filters["discount"]; - const selector = values.map(value => { - return `input[data-filter-name="discount"][data-filter-value="${value}"]`; - }).join(','); - $(selector).prop('checked', true); - this.field_filters = field_filters; - } - } - - bind_discount_filter_action() { - let me = this; - $('.discount-filter').on('change', (e) => { - const $checkbox = $(e.target); - const is_checked = $checkbox.is(':checked'); - - const { - filterValue: filter_value - } = $checkbox.data(); - - delete this.field_filters["discount"]; - - if (is_checked) { - this.field_filters["discount"] = []; - this.field_filters["discount"].push(filter_value); - } - - if (this.field_filters["discount"].length === 0) { - delete this.field_filters["discount"]; - } - - me.change_route_with_filters(); - }); - } - - bind_filters() { - let me = this; - this.field_filters = {}; - this.attribute_filters = {}; - - $('.product-filter').on('change', (e) => { - me.from_filters = true; - - const $checkbox = $(e.target); - const is_checked = $checkbox.is(':checked'); - - if ($checkbox.is('.attribute-filter')) { - const { - attributeName: attribute_name, - attributeValue: attribute_value - } = $checkbox.data(); - - if (is_checked) { - this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || []; - this.attribute_filters[attribute_name].push(attribute_value); - } else { - this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || []; - this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name].filter(v => v !== attribute_value); - } - - if (this.attribute_filters[attribute_name].length === 0) { - delete this.attribute_filters[attribute_name]; - } - } else if ($checkbox.is('.field-filter') || $checkbox.is('.discount-filter')) { - const { - filterName: filter_name, - filterValue: filter_value - } = $checkbox.data(); - - if ($checkbox.is('.discount-filter')) { - // clear previous discount filter to accomodate new - delete this.field_filters["discount"]; - } - if (is_checked) { - this.field_filters[filter_name] = this.field_filters[filter_name] || []; - if (!in_list(this.field_filters[filter_name], filter_value)) { - this.field_filters[filter_name].push(filter_value); - } - } else { - this.field_filters[filter_name] = this.field_filters[filter_name] || []; - this.field_filters[filter_name] = this.field_filters[filter_name].filter(v => v !== filter_value); - } - - if (this.field_filters[filter_name].length === 0) { - delete this.field_filters[filter_name]; - } - } - - me.change_route_with_filters(); - }); - - // bind filter lookup input box - $('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => { - const $input = $(e.target); - const keyword = ($input.val() || '').toLowerCase(); - const $filter_options = $input.next('.filter-options'); - - $filter_options.find('.filter-lookup-wrapper').show(); - $filter_options.find('.filter-lookup-wrapper').each((i, el) => { - const $el = $(el); - const value = $el.data('value').toLowerCase(); - if (!value.includes(keyword)) { - $el.hide(); - } - }); - }, 300)); - } - - change_route_with_filters() { - let route_params = frappe.utils.get_query_params(); - - let start = this.if_key_exists(route_params.start) || 0; - if (this.from_filters) { - start = 0; // show items from first page if new filters are triggered - } - - const query_string = this.get_query_string({ - start: start, - field_filters: JSON.stringify(this.if_key_exists(this.field_filters)), - attribute_filters: JSON.stringify(this.if_key_exists(this.attribute_filters)), - }); - window.history.pushState('filters', '', `${location.pathname}?` + query_string); - - $('.page_content input').prop('disabled', true); - - this.make(true); - $('.page_content input').prop('disabled', false); - } - - restore_filters_state() { - const filters = frappe.utils.get_query_params(); - let {field_filters, attribute_filters} = filters; - - if (field_filters) { - field_filters = JSON.parse(field_filters); - for (let fieldname in field_filters) { - const values = field_filters[fieldname]; - const selector = values.map(value => { - return `input[data-filter-name="${fieldname}"][data-filter-value="${value}"]`; - }).join(','); - $(selector).prop('checked', true); - } - this.field_filters = field_filters; - } - if (attribute_filters) { - attribute_filters = JSON.parse(attribute_filters); - for (let attribute in attribute_filters) { - const values = attribute_filters[attribute]; - const selector = values.map(value => { - return `input[data-attribute-name="${attribute}"][data-attribute-value="${value}"]`; - }).join(','); - $(selector).prop('checked', true); - } - this.attribute_filters = attribute_filters; - } - } - - render_no_products_section(error=false) { - let error_section = ` -
- Something went wrong. Please refresh or contact us. -
- `; - let no_results_section = ` -
-
- Empty Cart -
-
${ __('No products found') }

-
- `; - - this.products_section.append(error ? error_section : no_results_section); - } - - render_item_sub_categories(categories) { - if (categories && categories.length) { - let sub_group_html = ` -
`; - - $("#product-listing").prepend(sub_group_html); - } - } - - get_query_string(object) { - const url = new URLSearchParams(); - for (let key in object) { - const value = object[key]; - if (value) { - url.append(key, value); - } - } - return url.toString(); - } - - if_key_exists(obj) { - let exists = false; - for (let key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key) && obj[key]) { - exists = true; - break; - } - } - return exists ? obj : undefined; - } -}; \ No newline at end of file diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py deleted file mode 100644 index 87ca9bd83d..0000000000 --- a/erpnext/e_commerce/redisearch_utils.py +++ /dev/null @@ -1,255 +0,0 @@ -# Copyright (c) 2022, 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.redis_wrapper import RedisWrapper -from redis import ResponseError -from redis.commands.search.field import TagField, TextField -from redis.commands.search.indexDefinition import IndexDefinition -from redis.commands.search.suggestion import Suggestion - -WEBSITE_ITEM_INDEX = "website_items_index" -WEBSITE_ITEM_KEY_PREFIX = "website_item:" -WEBSITE_ITEM_NAME_AUTOCOMPLETE = "website_items_name_dict" -WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = "website_items_category_dict" - - -def get_indexable_web_fields(): - "Return valid fields from Website Item that can be searched for." - web_item_meta = frappe.get_meta("Website Item", cached=True) - valid_fields = filter( - lambda df: df.fieldtype in ("Link", "Table MultiSelect", "Data", "Small Text", "Text Editor"), - web_item_meta.fields, - ) - - return [df.fieldname for df in valid_fields] - - -def is_redisearch_enabled(): - "Return True only if redisearch is loaded and enabled." - is_redisearch_enabled = frappe.db.get_single_value("E Commerce Settings", "is_redisearch_enabled") - return is_search_module_loaded() and is_redisearch_enabled - - -def is_search_module_loaded(): - try: - cache = frappe.cache() - for module in cache.module_list(): - if module.get(b"name") == b"search": - return True - except Exception: - return False # handling older redis versions - - -def if_redisearch_enabled(function): - "Decorator to check if Redisearch is enabled." - - def wrapper(*args, **kwargs): - if is_redisearch_enabled(): - func = function(*args, **kwargs) - return func - return - - return wrapper - - -def make_key(key): - return frappe.cache().make_key(key) - - -@if_redisearch_enabled -def create_website_items_index(): - "Creates Index Definition." - - redis = frappe.cache() - index = redis.ft(WEBSITE_ITEM_INDEX) - - try: - index.dropindex() # drop if already exists - except ResponseError: - # will most likely raise a ResponseError if index does not exist - # ignore and create index - pass - except Exception: - raise_redisearch_error() - - idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)]) - - # Index fields mentioned in e-commerce settings - idx_fields = frappe.db.get_single_value("E Commerce Settings", "search_index_fields") - idx_fields = idx_fields.split(",") if idx_fields else [] - - if "web_item_name" in idx_fields: - idx_fields.remove("web_item_name") - - idx_fields = [to_search_field(f) for f in idx_fields] - - # TODO: sortable? - index.create_index( - [TextField("web_item_name", sortable=True)] + idx_fields, - definition=idx_def, - ) - - reindex_all_web_items() - define_autocomplete_dictionary() - - -def to_search_field(field): - if field == "tags": - return TagField("tags", separator=",") - - return TextField(field) - - -@if_redisearch_enabled -def insert_item_to_index(website_item_doc): - # Insert item to index - key = get_cache_key(website_item_doc.name) - cache = frappe.cache() - web_item = create_web_item_map(website_item_doc) - - for field, value in web_item.items(): - super(RedisWrapper, cache).hset(make_key(key), field, value) - - insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name) - - -@if_redisearch_enabled -def insert_to_name_ac(web_name, doc_name): - ac = frappe.cache().ft() - ac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(web_name, payload=doc_name)) - - -def create_web_item_map(website_item_doc): - fields_to_index = get_fields_indexed() - web_item = {} - - for field in fields_to_index: - web_item[field] = website_item_doc.get(field) or "" - - return web_item - - -@if_redisearch_enabled -def update_index_for_item(website_item_doc): - # Reinsert to Cache - insert_item_to_index(website_item_doc) - define_autocomplete_dictionary() - - -@if_redisearch_enabled -def delete_item_from_index(website_item_doc): - cache = frappe.cache() - key = get_cache_key(website_item_doc.name) - - try: - cache.delete(key) - except Exception: - raise_redisearch_error() - - delete_from_ac_dict(website_item_doc) - return True - - -@if_redisearch_enabled -def delete_from_ac_dict(website_item_doc): - """Removes this items's name from autocomplete dictionary""" - ac = frappe.cache().ft() - ac.sugdel(website_item_doc.web_item_name) - - -@if_redisearch_enabled -def define_autocomplete_dictionary(): - """ - Defines/Redefines an autocomplete search dictionary for Website Item Name. - Also creats autocomplete dictionary for Published Item Groups. - """ - - cache = frappe.cache() - - # Delete both autocomplete dicts - try: - cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE)) - cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE)) - except Exception: - raise_redisearch_error() - - create_items_autocomplete_dict() - create_item_groups_autocomplete_dict() - - -@if_redisearch_enabled -def create_items_autocomplete_dict(): - "Add items as suggestions in Autocompleter." - - ac = frappe.cache().ft() - items = frappe.get_all( - "Website Item", fields=["web_item_name", "item_group"], filters={"published": 1} - ) - for item in items: - ac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(item.web_item_name)) - - -@if_redisearch_enabled -def create_item_groups_autocomplete_dict(): - "Add item groups with weightage as suggestions in Autocompleter." - - published_item_groups = frappe.get_all( - "Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1} - ) - if not published_item_groups: - return - - ac = frappe.cache().ft() - - for item_group in published_item_groups: - payload = json.dumps({"name": item_group.name, "route": item_group.route}) - ac.sugadd( - WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, - Suggestion( - string=item_group.name, - score=frappe.utils.flt(item_group.weightage) or 1.0, - payload=payload, # additional info that can be retrieved later - ), - ) - - -@if_redisearch_enabled -def reindex_all_web_items(): - items = frappe.get_all("Website Item", fields=get_fields_indexed(), filters={"published": True}) - - cache = frappe.cache() - for item in items: - web_item = create_web_item_map(item) - key = make_key(get_cache_key(item.name)) - - for field, value in web_item.items(): - super(RedisWrapper, cache).hset(key, field, value) - - -def get_cache_key(name): - name = frappe.scrub(name) - return f"{WEBSITE_ITEM_KEY_PREFIX}{name}" - - -def get_fields_indexed(): - fields_to_index = frappe.db.get_single_value("E Commerce Settings", "search_index_fields") - fields_to_index = fields_to_index.split(",") if fields_to_index else [] - - mandatory_fields = ["name", "web_item_name", "route", "thumbnail", "ranking"] - fields_to_index = fields_to_index + mandatory_fields - - return fields_to_index - - -def raise_redisearch_error(): - "Create an Error Log and raise error." - log = frappe.log_error("Redisearch Error") - log_link = frappe.utils.get_link_to_form("Error Log", log.name) - - frappe.throw( - msg=_("Something went wrong. Check {0}").format(log_link), title=_("Redisearch Error") - ) diff --git a/erpnext/e_commerce/shopping_cart/__init__.py b/erpnext/e_commerce/shopping_cart/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py deleted file mode 100644 index 7c7e169c52..0000000000 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ /dev/null @@ -1,721 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -import frappe.defaults -from frappe import _, throw -from frappe.contacts.doctype.address.address import get_address_display -from frappe.contacts.doctype.contact.contact import get_contact_name -from frappe.utils import cint, cstr, flt, get_fullname -from frappe.utils.nestedset import get_root_of - -from erpnext.accounts.utils import get_account_name -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( - get_shopping_cart_settings, -) -from erpnext.utilities.product import get_web_item_qty_in_stock - - -class WebsitePriceListMissingError(frappe.ValidationError): - pass - - -def set_cart_count(quotation=None): - if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")): - if not quotation: - quotation = _get_cart_quotation() - cart_count = cstr(cint(quotation.get("total_qty"))) - - if hasattr(frappe.local, "cookie_manager"): - frappe.local.cookie_manager.set_cookie("cart_count", cart_count) - - -@frappe.whitelist() -def get_cart_quotation(doc=None): - party = get_party() - - if not doc: - quotation = _get_cart_quotation(party) - doc = quotation - set_cart_count(quotation) - - addresses = get_address_docs(party=party) - - if not doc.customer_address and addresses: - update_cart_address("billing", addresses[0].name) - - return { - "doc": decorate_quotation_doc(doc), - "shipping_addresses": get_shipping_addresses(party), - "billing_addresses": get_billing_addresses(party), - "shipping_rules": get_applicable_shipping_rules(party), - "cart_settings": frappe.get_cached_doc("E Commerce Settings"), - } - - -@frappe.whitelist() -def get_shipping_addresses(party=None): - if not party: - party = get_party() - addresses = get_address_docs(party=party) - return [ - {"name": address.name, "title": address.address_title, "display": address.display} - for address in addresses - if address.address_type == "Shipping" - ] - - -@frappe.whitelist() -def get_billing_addresses(party=None): - if not party: - party = get_party() - addresses = get_address_docs(party=party) - return [ - {"name": address.name, "title": address.address_title, "display": address.display} - for address in addresses - if address.address_type == "Billing" - ] - - -@frappe.whitelist() -def place_order(): - quotation = _get_cart_quotation() - cart_settings = frappe.db.get_value( - "E Commerce Settings", None, ["company", "allow_items_not_in_stock"], as_dict=1 - ) - quotation.company = cart_settings.company - - quotation.flags.ignore_permissions = True - quotation.submit() - - if quotation.quotation_to == "Lead" and quotation.party_name: - # company used to create customer accounts - frappe.defaults.set_user_default("company", quotation.company) - - if not (quotation.shipping_address_name or quotation.customer_address): - frappe.throw(_("Set Shipping Address or Billing Address")) - - from erpnext.selling.doctype.quotation.quotation import _make_sales_order - - sales_order = frappe.get_doc(_make_sales_order(quotation.name, ignore_permissions=True)) - sales_order.payment_schedule = [] - - if not cint(cart_settings.allow_items_not_in_stock): - for item in sales_order.get("items"): - item.warehouse = frappe.db.get_value( - "Website Item", {"item_code": item.item_code}, "website_warehouse" - ) - is_stock_item = frappe.db.get_value("Item", item.item_code, "is_stock_item") - - if is_stock_item: - item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse") - if not cint(item_stock.in_stock): - throw(_("{0} Not in Stock").format(item.item_code)) - if item.qty > item_stock.stock_qty: - throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty, item.item_code)) - - sales_order.flags.ignore_permissions = True - sales_order.insert() - sales_order.submit() - - if hasattr(frappe.local, "cookie_manager"): - frappe.local.cookie_manager.delete_cookie("cart_count") - - return sales_order.name - - -@frappe.whitelist() -def request_for_quotation(): - quotation = _get_cart_quotation() - quotation.flags.ignore_permissions = True - - if get_shopping_cart_settings().save_quotations_as_draft: - quotation.save() - else: - quotation.submit() - return quotation.name - - -@frappe.whitelist() -def update_cart(item_code, qty, additional_notes=None, with_items=False): - quotation = _get_cart_quotation() - - empty_card = False - qty = flt(qty) - if qty == 0: - quotation_items = quotation.get("items", {"item_code": ["!=", item_code]}) - if quotation_items: - quotation.set("items", quotation_items) - else: - empty_card = True - - else: - warehouse = frappe.get_cached_value( - "Website Item", {"item_code": item_code}, "website_warehouse" - ) - - quotation_items = quotation.get("items", {"item_code": item_code}) - if not quotation_items: - quotation.append( - "items", - { - "doctype": "Quotation Item", - "item_code": item_code, - "qty": qty, - "additional_notes": additional_notes, - "warehouse": warehouse, - }, - ) - else: - quotation_items[0].qty = qty - quotation_items[0].additional_notes = additional_notes - quotation_items[0].warehouse = warehouse - - apply_cart_settings(quotation=quotation) - - quotation.flags.ignore_permissions = True - quotation.payment_schedule = [] - if not empty_card: - quotation.save() - else: - quotation.delete() - quotation = None - - set_cart_count(quotation) - - if cint(with_items): - context = get_cart_quotation(quotation) - return { - "items": frappe.render_template("templates/includes/cart/cart_items.html", context), - "total": frappe.render_template("templates/includes/cart/cart_items_total.html", context), - "taxes_and_totals": frappe.render_template( - "templates/includes/cart/cart_payment_summary.html", context - ), - } - else: - return {"name": quotation.name} - - -@frappe.whitelist() -def get_shopping_cart_menu(context=None): - if not context: - context = get_cart_quotation() - - return frappe.render_template("templates/includes/cart/cart_dropdown.html", context) - - -@frappe.whitelist() -def add_new_address(doc): - doc = frappe.parse_json(doc) - doc.update({"doctype": "Address"}) - address = frappe.get_doc(doc) - address.save(ignore_permissions=True) - - return address - - -@frappe.whitelist(allow_guest=True) -def create_lead_for_item_inquiry(lead, subject, message): - lead = frappe.parse_json(lead) - lead_doc = frappe.new_doc("Lead") - for fieldname in ("lead_name", "company_name", "email_id", "phone"): - lead_doc.set(fieldname, lead.get(fieldname)) - - lead_doc.set("lead_owner", "") - - if not frappe.db.exists("Lead Source", "Product Inquiry"): - frappe.get_doc({"doctype": "Lead Source", "source_name": "Product Inquiry"}).insert( - ignore_permissions=True - ) - - lead_doc.set("source", "Product Inquiry") - - try: - lead_doc.save(ignore_permissions=True) - except frappe.exceptions.DuplicateEntryError: - frappe.clear_messages() - lead_doc = frappe.get_doc("Lead", {"email_id": lead["email_id"]}) - - lead_doc.add_comment( - "Comment", - text=""" -
-
{subject}
-

{message}

-
- """.format( - subject=subject, message=message - ), - ) - - return lead_doc - - -@frappe.whitelist() -def get_terms_and_conditions(terms_name): - return frappe.db.get_value("Terms and Conditions", terms_name, "terms") - - -@frappe.whitelist() -def update_cart_address(address_type, address_name): - quotation = _get_cart_quotation() - address_doc = frappe.get_doc("Address", address_name).as_dict() - address_display = get_address_display(address_doc) - - if address_type.lower() == "billing": - quotation.customer_address = address_name - quotation.address_display = address_display - quotation.shipping_address_name = quotation.shipping_address_name or address_name - address_doc = next((doc for doc in get_billing_addresses() if doc["name"] == address_name), None) - elif address_type.lower() == "shipping": - quotation.shipping_address_name = address_name - quotation.shipping_address = address_display - quotation.customer_address = quotation.customer_address or address_name - address_doc = next( - (doc for doc in get_shipping_addresses() if doc["name"] == address_name), None - ) - apply_cart_settings(quotation=quotation) - - quotation.flags.ignore_permissions = True - quotation.save() - - context = get_cart_quotation(quotation) - context["address"] = address_doc - - return { - "taxes": frappe.render_template("templates/includes/order/order_taxes.html", context), - "address": frappe.render_template("templates/includes/cart/address_card.html", context), - } - - -def guess_territory(): - territory = None - geoip_country = frappe.session.get("session_country") - if geoip_country: - territory = frappe.db.get_value("Territory", geoip_country) - - return ( - territory - or frappe.db.get_value("E Commerce Settings", None, "territory") - or get_root_of("Territory") - ) - - -def decorate_quotation_doc(doc): - for d in doc.get("items", []): - item_code = d.item_code - fields = ["web_item_name", "thumbnail", "website_image", "description", "route"] - - # Variant Item - if not frappe.db.exists("Website Item", {"item_code": item_code}): - variant_data = frappe.db.get_values( - "Item", - filters={"item_code": item_code}, - fieldname=["variant_of", "item_name", "image"], - as_dict=True, - )[0] - item_code = variant_data.variant_of - fields = fields[1:] - d.web_item_name = variant_data.item_name - - if variant_data.image: # get image from variant or template web item - d.thumbnail = variant_data.image - fields = fields[2:] - - d.update(frappe.db.get_value("Website Item", {"item_code": item_code}, fields, as_dict=True)) - website_warehouse = frappe.get_cached_value( - "Website Item", {"item_code": item_code}, "website_warehouse" - ) - d.warehouse = website_warehouse - - return doc - - -def _get_cart_quotation(party=None): - """Return the open Quotation of type "Shopping Cart" or make a new one""" - if not party: - party = get_party() - - quotation = frappe.get_all( - "Quotation", - fields=["name"], - filters={ - "party_name": party.name, - "contact_email": frappe.session.user, - "order_type": "Shopping Cart", - "docstatus": 0, - }, - order_by="modified desc", - limit_page_length=1, - ) - - if quotation: - qdoc = frappe.get_doc("Quotation", quotation[0].name) - else: - company = frappe.db.get_value("E Commerce Settings", None, ["company"]) - qdoc = frappe.get_doc( - { - "doctype": "Quotation", - "naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-", - "quotation_to": party.doctype, - "company": company, - "order_type": "Shopping Cart", - "status": "Draft", - "docstatus": 0, - "__islocal": 1, - "party_name": party.name, - } - ) - - qdoc.contact_person = frappe.db.get_value("Contact", {"email_id": frappe.session.user}) - qdoc.contact_email = frappe.session.user - - qdoc.flags.ignore_permissions = True - qdoc.run_method("set_missing_values") - apply_cart_settings(party, qdoc) - - return qdoc - - -def update_party(fullname, company_name=None, mobile_no=None, phone=None): - party = get_party() - - party.customer_name = company_name or fullname - party.customer_type = "Company" if company_name else "Individual" - - contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user}) - contact = frappe.get_doc("Contact", contact_name) - contact.first_name = fullname - contact.last_name = None - contact.customer_name = party.customer_name - contact.mobile_no = mobile_no - contact.phone = phone - contact.flags.ignore_permissions = True - contact.save() - - party_doc = frappe.get_doc(party.as_dict()) - party_doc.flags.ignore_permissions = True - party_doc.save() - - qdoc = _get_cart_quotation(party) - if not qdoc.get("__islocal"): - qdoc.customer_name = company_name or fullname - qdoc.run_method("set_missing_lead_customer_details") - qdoc.flags.ignore_permissions = True - qdoc.save() - - -def apply_cart_settings(party=None, quotation=None): - if not party: - party = get_party() - if not quotation: - quotation = _get_cart_quotation(party) - - cart_settings = frappe.get_doc("E Commerce Settings") - - set_price_list_and_rate(quotation, cart_settings) - - quotation.run_method("calculate_taxes_and_totals") - - set_taxes(quotation, cart_settings) - - _apply_shipping_rule(party, quotation, cart_settings) - - -def set_price_list_and_rate(quotation, cart_settings): - """set price list based on billing territory""" - - _set_price_list(cart_settings, quotation) - - # reset values - quotation.price_list_currency = ( - quotation.currency - ) = quotation.plc_conversion_rate = quotation.conversion_rate = None - for item in quotation.get("items"): - item.price_list_rate = item.discount_percentage = item.rate = item.amount = None - - # refetch values - quotation.run_method("set_price_list_and_item_details") - - if hasattr(frappe.local, "cookie_manager"): - # set it in cookies for using in product page - frappe.local.cookie_manager.set_cookie("selling_price_list", quotation.selling_price_list) - - -def _set_price_list(cart_settings, quotation=None): - """Set price list based on customer or shopping cart default""" - from erpnext.accounts.party import get_default_price_list - - party_name = quotation.get("party_name") if quotation else get_party().get("name") - selling_price_list = None - - # check if default customer price list exists - if party_name and frappe.db.exists("Customer", party_name): - selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name)) - - # check default price list in shopping cart - if not selling_price_list: - selling_price_list = cart_settings.price_list - - if quotation: - quotation.selling_price_list = selling_price_list - - return selling_price_list - - -def set_taxes(quotation, cart_settings): - """set taxes based on billing territory""" - from erpnext.accounts.party import set_taxes - - customer_group = frappe.db.get_value("Customer", quotation.party_name, "customer_group") - - quotation.taxes_and_charges = set_taxes( - quotation.party_name, - "Customer", - quotation.transaction_date, - quotation.company, - customer_group=customer_group, - supplier_group=None, - tax_category=quotation.tax_category, - billing_address=quotation.customer_address, - shipping_address=quotation.shipping_address_name, - use_for_shopping_cart=1, - ) - # - # # clear table - quotation.set("taxes", []) - # - # # append taxes - quotation.append_taxes_from_master() - - -def get_party(user=None): - if not user: - user = frappe.session.user - - contact_name = get_contact_name(user) - party = None - - if contact_name: - contact = frappe.get_doc("Contact", contact_name) - if contact.links: - party_doctype = contact.links[0].link_doctype - party = contact.links[0].link_name - - cart_settings = frappe.get_doc("E Commerce Settings") - - debtors_account = "" - - if cart_settings.enable_checkout: - debtors_account = get_debtors_account(cart_settings) - - if party: - return frappe.get_doc(party_doctype, party) - - else: - if not cart_settings.enabled: - frappe.local.flags.redirect_location = "/contact" - raise frappe.Redirect - customer = frappe.new_doc("Customer") - fullname = get_fullname(user) - customer.update( - { - "customer_name": fullname, - "customer_type": "Individual", - "customer_group": get_shopping_cart_settings().default_customer_group, - "territory": get_root_of("Territory"), - } - ) - - customer.append("portal_users", {"user": user}) - - if debtors_account: - customer.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]}) - - customer.flags.ignore_mandatory = True - customer.insert(ignore_permissions=True) - - contact = frappe.new_doc("Contact") - contact.update({"first_name": fullname, "email_ids": [{"email_id": user, "is_primary": 1}]}) - contact.append("links", dict(link_doctype="Customer", link_name=customer.name)) - contact.flags.ignore_mandatory = True - contact.insert(ignore_permissions=True) - - return customer - - -def get_debtors_account(cart_settings): - if not cart_settings.payment_gateway_account: - frappe.throw(_("Payment Gateway Account not set"), _("Mandatory")) - - payment_gateway_account_currency = frappe.get_doc( - "Payment Gateway Account", cart_settings.payment_gateway_account - ).currency - - account_name = _("Debtors ({0})").format(payment_gateway_account_currency) - - debtors_account_name = get_account_name( - "Receivable", - "Asset", - is_group=0, - account_currency=payment_gateway_account_currency, - company=cart_settings.company, - ) - - if not debtors_account_name: - debtors_account = frappe.get_doc( - { - "doctype": "Account", - "account_type": "Receivable", - "root_type": "Asset", - "is_group": 0, - "parent_account": get_account_name( - root_type="Asset", is_group=1, company=cart_settings.company - ), - "account_name": account_name, - "currency": payment_gateway_account_currency, - } - ).insert(ignore_permissions=True) - - return debtors_account.name - - else: - return debtors_account_name - - -def get_address_docs( - doctype=None, txt=None, filters=None, limit_start=0, limit_page_length=20, party=None -): - if not party: - party = get_party() - - if not party: - return [] - - address_names = frappe.db.get_all( - "Dynamic Link", - fields=("parent"), - filters=dict(parenttype="Address", link_doctype=party.doctype, link_name=party.name), - ) - - out = [] - - for a in address_names: - address = frappe.get_doc("Address", a.parent) - address.display = get_address_display(address.as_dict()) - out.append(address) - - return out - - -@frappe.whitelist() -def apply_shipping_rule(shipping_rule): - quotation = _get_cart_quotation() - - quotation.shipping_rule = shipping_rule - - apply_cart_settings(quotation=quotation) - - quotation.flags.ignore_permissions = True - quotation.save() - - return get_cart_quotation(quotation) - - -def _apply_shipping_rule(party=None, quotation=None, cart_settings=None): - if not quotation.shipping_rule: - shipping_rules = get_shipping_rules(quotation, cart_settings) - - if not shipping_rules: - return - - elif quotation.shipping_rule not in shipping_rules: - quotation.shipping_rule = shipping_rules[0] - - if quotation.shipping_rule: - quotation.run_method("apply_shipping_rule") - quotation.run_method("calculate_taxes_and_totals") - - -def get_applicable_shipping_rules(party=None, quotation=None): - shipping_rules = get_shipping_rules(quotation) - - if shipping_rules: - # we need this in sorted order as per the position of the rule in the settings page - return [[rule, rule] for rule in shipping_rules] - - -def get_shipping_rules(quotation=None, cart_settings=None): - if not quotation: - quotation = _get_cart_quotation() - - shipping_rules = [] - if quotation.shipping_address_name: - country = frappe.db.get_value("Address", quotation.shipping_address_name, "country") - if country: - sr_country = frappe.qb.DocType("Shipping Rule Country") - sr = frappe.qb.DocType("Shipping Rule") - query = ( - frappe.qb.from_(sr_country) - .join(sr) - .on(sr.name == sr_country.parent) - .select(sr.name) - .distinct() - .where((sr_country.country == country) & (sr.disabled != 1)) - ) - result = query.run(as_list=True) - shipping_rules = [x[0] for x in result] - - return shipping_rules - - -def get_address_territory(address_name): - """Tries to match city, state and country of address to existing territory""" - territory = None - - if address_name: - address_fields = frappe.db.get_value("Address", address_name, ["city", "state", "country"]) - for value in address_fields: - territory = frappe.db.get_value("Territory", value) - if territory: - break - - return territory - - -def show_terms(doc): - return doc.tc_name - - -@frappe.whitelist(allow_guest=True) -def apply_coupon_code(applied_code, applied_referral_sales_partner): - quotation = True - - if not applied_code: - frappe.throw(_("Please enter a coupon code")) - - coupon_list = frappe.get_all("Coupon Code", filters={"coupon_code": applied_code}) - if not coupon_list: - frappe.throw(_("Please enter a valid coupon code")) - - coupon_name = coupon_list[0].name - - from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code - - validate_coupon_code(coupon_name) - quotation = _get_cart_quotation() - quotation.coupon_code = coupon_name - quotation.flags.ignore_permissions = True - quotation.save() - - if applied_referral_sales_partner: - sales_partner_list = frappe.get_all( - "Sales Partner", filters={"referral_code": applied_referral_sales_partner} - ) - if sales_partner_list: - sales_partner_name = sales_partner_list[0].name - quotation.referral_sales_partner = sales_partner_name - quotation.flags.ignore_permissions = True - quotation.save() - - return quotation diff --git a/erpnext/e_commerce/shopping_cart/product_info.py b/erpnext/e_commerce/shopping_cart/product_info.py deleted file mode 100644 index 0248ca73d7..0000000000 --- a/erpnext/e_commerce/shopping_cart/product_info.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( - get_shopping_cart_settings, - show_quantity_in_website, -) -from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, _set_price_list -from erpnext.utilities.product import ( - get_non_stock_item_status, - get_price, - get_web_item_qty_in_stock, -) - - -@frappe.whitelist(allow_guest=True) -def get_product_info_for_website(item_code, skip_quotation_creation=False): - """get product price / stock info for website""" - - cart_settings = get_shopping_cart_settings() - if not cart_settings.enabled: - # return settings even if cart is disabled - return frappe._dict({"product_info": {}, "cart_settings": cart_settings}) - - cart_quotation = frappe._dict() - if not skip_quotation_creation: - cart_quotation = _get_cart_quotation() - - selling_price_list = ( - cart_quotation.get("selling_price_list") - if cart_quotation - else _set_price_list(cart_settings, None) - ) - - price = {} - if cart_settings.show_price: - is_guest = frappe.session.user == "Guest" - # Show Price if logged in. - # If not logged in, check if price is hidden for guest. - if not is_guest or not cart_settings.hide_price_for_guest: - price = get_price( - item_code, selling_price_list, cart_settings.default_customer_group, cart_settings.company - ) - - stock_status = None - - if cart_settings.show_stock_availability: - on_backorder = frappe.get_cached_value("Website Item", {"item_code": item_code}, "on_backorder") - if on_backorder: - stock_status = frappe._dict({"on_backorder": True}) - else: - stock_status = get_web_item_qty_in_stock(item_code, "website_warehouse") - - product_info = { - "price": price, - "qty": 0, - "uom": frappe.db.get_value("Item", item_code, "stock_uom"), - "sales_uom": frappe.db.get_value("Item", item_code, "sales_uom"), - } - - if stock_status: - if stock_status.on_backorder: - product_info["on_backorder"] = True - else: - product_info["stock_qty"] = stock_status.stock_qty - product_info["in_stock"] = ( - stock_status.in_stock - if stock_status.is_stock_item - else get_non_stock_item_status(item_code, "website_warehouse") - ) - product_info["show_stock_qty"] = show_quantity_in_website() - - if product_info["price"]: - if frappe.session.user != "Guest": - item = cart_quotation.get({"item_code": item_code}) if cart_quotation else None - if item: - product_info["qty"] = item[0].qty - - return frappe._dict({"product_info": product_info, "cart_settings": cart_settings}) - - -def set_product_info_for_website(item): - """set product price uom for website""" - product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get( - "product_info" - ) - - if product_info: - item.update(product_info) - item["stock_uom"] = product_info.get("uom") - item["sales_uom"] = product_info.get("sales_uom") - if product_info.get("price"): - item["price_stock_uom"] = product_info.get("price").get("formatted_price") - item["price_sales_uom"] = product_info.get("price").get("formatted_price_sales_uom") - else: - item["price_stock_uom"] = "" - item["price_sales_uom"] = "" diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py deleted file mode 100644 index 8210f9743d..0000000000 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ /dev/null @@ -1,398 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import unittest - -import frappe -from frappe.tests.utils import change_settings -from frappe.utils import add_months, cint, nowdate - -from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule -from erpnext.e_commerce.doctype.website_item.website_item import make_website_item -from erpnext.e_commerce.shopping_cart.cart import ( - _get_cart_quotation, - get_cart_quotation, - get_party, - request_for_quotation, - update_cart, -) - - -class TestShoppingCart(unittest.TestCase): - """ - Note: - Shopping Cart == Quotation - """ - - def setUp(self): - frappe.set_user("Administrator") - self.enable_shopping_cart() - if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}): - make_website_item(frappe.get_cached_doc("Item", "_Test Item")) - - if not frappe.db.exists("Website Item", {"item_code": "_Test Item 2"}): - make_website_item(frappe.get_cached_doc("Item", "_Test Item 2")) - - def tearDown(self): - frappe.db.rollback() - frappe.set_user("Administrator") - self.disable_shopping_cart() - - @classmethod - def tearDownClass(cls): - frappe.db.sql("delete from `tabTax Rule`") - - def test_get_cart_new_user(self): - self.login_as_customer( - "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" - ) - create_address_and_contact( - address_title="_Test Address for Customer 2", - first_name="_Test Contact for Customer 2", - email="test_contact_two_customer@example.com", - customer="_Test Customer 2", - ) - # test if lead is created and quotation with new lead is fetched - customer = frappe.get_doc("Customer", "_Test Customer 2") - quotation = _get_cart_quotation(party=customer) - self.assertEqual(quotation.quotation_to, "Customer") - self.assertEqual( - quotation.contact_person, - frappe.db.get_value("Contact", dict(email_id="test_contact_two_customer@example.com")), - ) - self.assertEqual(quotation.contact_email, frappe.session.user) - - return quotation - - def test_get_cart_customer(self, customer="_Test Customer 2"): - def validate_quotation(customer_name): - # test if quotation with customer is fetched - party = frappe.get_doc("Customer", customer_name) - quotation = _get_cart_quotation(party=party) - self.assertEqual(quotation.quotation_to, "Customer") - self.assertEqual(quotation.party_name, customer_name) - self.assertEqual(quotation.contact_email, frappe.session.user) - return quotation - - quotation = validate_quotation(customer) - return quotation - - def test_add_to_cart(self): - self.login_as_customer( - "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" - ) - create_address_and_contact( - address_title="_Test Address for Customer 2", - first_name="_Test Contact for Customer 2", - email="test_contact_two_customer@example.com", - customer="_Test Customer 2", - ) - # clear existing quotations - self.clear_existing_quotations() - - # add first item - update_cart("_Test Item", 1) - - quotation = self.test_get_cart_customer("_Test Customer 2") - - self.assertEqual(quotation.get("items")[0].item_code, "_Test Item") - self.assertEqual(quotation.get("items")[0].qty, 1) - self.assertEqual(quotation.get("items")[0].amount, 10) - - # add second item - update_cart("_Test Item 2", 1) - quotation = self.test_get_cart_customer("_Test Customer 2") - self.assertEqual(quotation.get("items")[1].item_code, "_Test Item 2") - self.assertEqual(quotation.get("items")[1].qty, 1) - self.assertEqual(quotation.get("items")[1].amount, 20) - - self.assertEqual(len(quotation.get("items")), 2) - - def test_update_cart(self): - # first, add to cart - self.test_add_to_cart() - - # update first item - update_cart("_Test Item", 5) - quotation = self.test_get_cart_customer("_Test Customer 2") - self.assertEqual(quotation.get("items")[0].item_code, "_Test Item") - self.assertEqual(quotation.get("items")[0].qty, 5) - self.assertEqual(quotation.get("items")[0].amount, 50) - self.assertEqual(quotation.net_total, 70) - self.assertEqual(len(quotation.get("items")), 2) - - def test_remove_from_cart(self): - # first, add to cart - self.test_add_to_cart() - - # remove first item - update_cart("_Test Item", 0) - quotation = self.test_get_cart_customer("_Test Customer 2") - - self.assertEqual(quotation.get("items")[0].item_code, "_Test Item 2") - self.assertEqual(quotation.get("items")[0].qty, 1) - self.assertEqual(quotation.get("items")[0].amount, 20) - self.assertEqual(quotation.net_total, 20) - self.assertEqual(len(quotation.get("items")), 1) - - @unittest.skip("Flaky in CI") - def test_tax_rule(self): - self.create_tax_rule() - - self.login_as_customer( - "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" - ) - create_address_and_contact( - address_title="_Test Address for Customer 2", - first_name="_Test Contact for Customer 2", - email="test_contact_two_customer@example.com", - customer="_Test Customer 2", - ) - - quotation = self.create_quotation() - - from erpnext.accounts.party import set_taxes - - tax_rule_master = set_taxes( - quotation.party_name, - "Customer", - None, - quotation.company, - customer_group=None, - supplier_group=None, - tax_category=quotation.tax_category, - billing_address=quotation.customer_address, - shipping_address=quotation.shipping_address_name, - use_for_shopping_cart=1, - ) - - self.assertEqual(quotation.taxes_and_charges, tax_rule_master) - self.assertEqual(quotation.total_taxes_and_charges, 1000.0) - - self.remove_test_quotation(quotation) - - @change_settings( - "E Commerce Settings", - { - "company": "_Test Company", - "enabled": 1, - "default_customer_group": "_Test Customer Group", - "price_list": "_Test Price List India", - "show_price": 1, - }, - ) - def test_add_item_variant_without_web_item_to_cart(self): - "Test adding Variants having no Website Items in cart via Template Web Item." - from erpnext.controllers.item_variant import create_variant - from erpnext.e_commerce.doctype.website_item.website_item import make_website_item - from erpnext.stock.doctype.item.test_item import make_item - - template_item = make_item( - "Test-Tshirt-Temp", - { - "has_variant": 1, - "variant_based_on": "Item Attribute", - "attributes": [{"attribute": "Test Size"}, {"attribute": "Test Colour"}], - }, - ) - variant = create_variant("Test-Tshirt-Temp", {"Test Size": "Small", "Test Colour": "Red"}) - variant.save() - make_website_item(template_item) # publish template not variant - - update_cart("Test-Tshirt-Temp-S-R", 1) - - cart = get_cart_quotation() # test if cart page gets data without errors - doc = cart.get("doc") - - self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R") - - # test if items are rendered without error - frappe.render_template("templates/includes/cart/cart_items.html", cart) - - @change_settings("E Commerce Settings", {"save_quotations_as_draft": 1}) - def test_cart_without_checkout_and_draft_quotation(self): - "Test impact of 'save_quotations_as_draft' checkbox." - frappe.local.shopping_cart_settings = None - - # add item to cart - update_cart("_Test Item", 1) - quote_name = request_for_quotation() # Request for Quote - quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus")) - - self.assertEqual(quote_doctstatus, 0) - - frappe.db.set_single_value("E Commerce Settings", "save_quotations_as_draft", 0) - frappe.local.shopping_cart_settings = None - update_cart("_Test Item", 1) - quote_name = request_for_quotation() # Request for Quote - quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus")) - - self.assertEqual(quote_doctstatus, 1) - - def create_tax_rule(self): - tax_rule = frappe.get_test_records("Tax Rule")[0] - try: - frappe.get_doc(tax_rule).insert(ignore_if_duplicate=True) - except (frappe.DuplicateEntryError, ConflictingTaxRule): - pass - - def create_quotation(self): - quotation = frappe.new_doc("Quotation") - - values = { - "doctype": "Quotation", - "quotation_to": "Customer", - "order_type": "Shopping Cart", - "party_name": get_party(frappe.session.user).name, - "docstatus": 0, - "contact_email": frappe.session.user, - "selling_price_list": "_Test Price List Rest of the World", - "currency": "USD", - "taxes_and_charges": "_Test Tax 1 - _TC", - "conversion_rate": 1, - "transaction_date": nowdate(), - "valid_till": add_months(nowdate(), 1), - "items": [{"item_code": "_Test Item", "qty": 1}], - "taxes": frappe.get_doc("Sales Taxes and Charges Template", "_Test Tax 1 - _TC").taxes, - "company": "_Test Company", - } - - quotation.update(values) - - quotation.insert(ignore_permissions=True) - - return quotation - - def remove_test_quotation(self, quotation): - frappe.set_user("Administrator") - quotation.delete() - - # helper functions - def enable_shopping_cart(self): - settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings") - - settings.update( - { - "enabled": 1, - "company": "_Test Company", - "default_customer_group": "_Test Customer Group", - "quotation_series": "_T-Quotation-", - "price_list": "_Test Price List India", - } - ) - - # insert item price - if not frappe.db.get_value( - "Item Price", {"price_list": "_Test Price List India", "item_code": "_Test Item"} - ): - frappe.get_doc( - { - "doctype": "Item Price", - "price_list": "_Test Price List India", - "item_code": "_Test Item", - "price_list_rate": 10, - } - ).insert() - frappe.get_doc( - { - "doctype": "Item Price", - "price_list": "_Test Price List India", - "item_code": "_Test Item 2", - "price_list_rate": 20, - } - ).insert() - - settings.save() - frappe.local.shopping_cart_settings = None - - def disable_shopping_cart(self): - settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings") - settings.enabled = 0 - settings.save() - frappe.local.shopping_cart_settings = None - - def login_as_new_user(self): - self.create_user_if_not_exists("test_cart_user@example.com") - frappe.set_user("test_cart_user@example.com") - - def login_as_customer( - self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer" - ): - self.create_user_if_not_exists(email, name) - frappe.set_user(email) - - def clear_existing_quotations(self): - quotations = frappe.get_all( - "Quotation", - filters={"party_name": get_party().name, "order_type": "Shopping Cart", "docstatus": 0}, - order_by="modified desc", - pluck="name", - ) - - for quotation in quotations: - frappe.delete_doc("Quotation", quotation, ignore_permissions=True, force=True) - - def create_user_if_not_exists(self, email, first_name=None): - if frappe.db.exists("User", email): - return - - user = frappe.get_doc( - { - "doctype": "User", - "user_type": "Website User", - "email": email, - "send_welcome_email": 0, - "first_name": first_name or email.split("@")[0], - } - ).insert(ignore_permissions=True) - - user.add_roles("Customer") - - -def create_address_and_contact(**kwargs): - if not frappe.db.get_value("Address", {"address_title": kwargs.get("address_title")}): - frappe.get_doc( - { - "doctype": "Address", - "address_title": kwargs.get("address_title"), - "address_type": kwargs.get("address_type") or "Office", - "address_line1": kwargs.get("address_line1") or "Station Road", - "city": kwargs.get("city") or "_Test City", - "state": kwargs.get("state") or "Test State", - "country": kwargs.get("country") or "India", - "links": [ - {"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"} - ], - } - ).insert() - - if not frappe.db.get_value("Contact", {"first_name": kwargs.get("first_name")}): - contact = frappe.get_doc( - { - "doctype": "Contact", - "first_name": kwargs.get("first_name"), - "links": [ - {"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"} - ], - } - ) - contact.add_email(kwargs.get("email") or "test_contact_customer@example.com", is_primary=True) - contact.add_phone(kwargs.get("phone") or "+91 0000000000", is_primary_phone=True) - contact.insert() - - -test_dependencies = [ - "Sales Taxes and Charges Template", - "Price List", - "Item Price", - "Shipping Rule", - "Currency Exchange", - "Customer Group", - "Lead", - "Customer", - "Contact", - "Address", - "Item", - "Tax Rule", -] diff --git a/erpnext/e_commerce/shopping_cart/utils.py b/erpnext/e_commerce/shopping_cart/utils.py deleted file mode 100644 index 3d48c28dd1..0000000000 --- a/erpnext/e_commerce/shopping_cart/utils.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt -import frappe - -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled - - -def show_cart_count(): - if ( - is_cart_enabled() - and frappe.db.get_value("User", frappe.session.user, "user_type") == "Website User" - ): - return True - - return False - - -def set_cart_count(login_manager): - # since this is run only on hooks login event - # make sure user is already a customer - # before trying to set cart count - user_is_customer = is_customer() - if not user_is_customer: - return - - if show_cart_count(): - from erpnext.e_commerce.shopping_cart.cart import set_cart_count - - # set_cart_count will try to fetch existing cart quotation - # or create one if non existent (and create a customer too) - # cart count is calculated from this quotation's items - set_cart_count() - - -def clear_cart_count(login_manager): - if show_cart_count(): - frappe.local.cookie_manager.delete_cookie("cart_count") - - -def update_website_context(context): - cart_enabled = is_cart_enabled() - context["shopping_cart_enabled"] = cart_enabled - - -def is_customer(): - if frappe.session.user and frappe.session.user != "Guest": - contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user}) - if contact_name: - contact = frappe.get_doc("Contact", contact_name) - for link in contact.links: - if link.link_doctype == "Customer": - return True - - return False diff --git a/erpnext/e_commerce/variant_selector/__init__.py b/erpnext/e_commerce/variant_selector/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py deleted file mode 100644 index f8439d5d43..0000000000 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ /dev/null @@ -1,130 +0,0 @@ -import frappe - - -class ItemVariantsCacheManager: - def __init__(self, item_code): - self.item_code = item_code - - def get_item_variants_data(self): - val = frappe.cache().hget("item_variants_data", self.item_code) - - if not val: - self.build_cache() - - return frappe.cache().hget("item_variants_data", self.item_code) - - def get_attribute_value_item_map(self): - val = frappe.cache().hget("attribute_value_item_map", self.item_code) - - if not val: - self.build_cache() - - return frappe.cache().hget("attribute_value_item_map", self.item_code) - - def get_item_attribute_value_map(self): - val = frappe.cache().hget("item_attribute_value_map", self.item_code) - - if not val: - self.build_cache() - - return frappe.cache().hget("item_attribute_value_map", self.item_code) - - def get_optional_attributes(self): - val = frappe.cache().hget("optional_attributes", self.item_code) - - if not val: - self.build_cache() - - return frappe.cache().hget("optional_attributes", self.item_code) - - def get_ordered_attribute_values(self): - val = frappe.cache().get_value("ordered_attribute_values_map") - if val: - return val - - all_attribute_values = frappe.get_all( - "Item Attribute Value", ["attribute_value", "idx", "parent"], order_by="idx asc" - ) - - ordered_attribute_values_map = frappe._dict({}) - for d in all_attribute_values: - ordered_attribute_values_map.setdefault(d.parent, []).append(d.attribute_value) - - frappe.cache().set_value("ordered_attribute_values_map", ordered_attribute_values_map) - return ordered_attribute_values_map - - def build_cache(self): - parent_item_code = self.item_code - - attributes = [ - a.attribute - for a in frappe.get_all( - "Item Variant Attribute", {"parent": parent_item_code}, ["attribute"], order_by="idx asc" - ) - ] - - # Get Variants and tehir Attributes that are not disabled - iva = frappe.qb.DocType("Item Variant Attribute") - item = frappe.qb.DocType("Item") - query = ( - frappe.qb.from_(iva) - .join(item) - .on(item.name == iva.parent) - .select(iva.parent, iva.attribute, iva.attribute_value) - .where((iva.variant_of == parent_item_code) & (item.disabled == 0)) - .orderby(iva.name) - ) - item_variants_data = query.run() - - attribute_value_item_map = frappe._dict() - item_attribute_value_map = frappe._dict() - - for row in item_variants_data: - item_code, attribute, attribute_value = row - # (attr, value) => [item1, item2] - attribute_value_item_map.setdefault((attribute, attribute_value), []).append(item_code) - # item => {attr1: value1, attr2: value2} - item_attribute_value_map.setdefault(item_code, {})[attribute] = attribute_value - - optional_attributes = set() - for item_code, attr_dict in item_attribute_value_map.items(): - for attribute in attributes: - if attribute not in attr_dict: - optional_attributes.add(attribute) - - frappe.cache().hset("attribute_value_item_map", parent_item_code, attribute_value_item_map) - frappe.cache().hset("item_attribute_value_map", parent_item_code, item_attribute_value_map) - frappe.cache().hset("item_variants_data", parent_item_code, item_variants_data) - frappe.cache().hset("optional_attributes", parent_item_code, optional_attributes) - - def clear_cache(self): - keys = [ - "attribute_value_item_map", - "item_attribute_value_map", - "item_variants_data", - "optional_attributes", - ] - - for key in keys: - frappe.cache().hdel(key, self.item_code) - - def rebuild_cache(self): - self.clear_cache() - enqueue_build_cache(self.item_code) - - -def build_cache(item_code): - frappe.cache().hset("item_cache_build_in_progress", item_code, 1) - i = ItemVariantsCacheManager(item_code) - i.build_cache() - frappe.cache().hset("item_cache_build_in_progress", item_code, 0) - - -def enqueue_build_cache(item_code): - if frappe.cache().hget("item_cache_build_in_progress", item_code): - return - frappe.enqueue( - "erpnext.e_commerce.variant_selector.item_variants_cache.build_cache", - item_code=item_code, - queue="long", - ) diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py deleted file mode 100644 index 8eb497c1b5..0000000000 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ /dev/null @@ -1,125 +0,0 @@ -import frappe -from frappe.tests.utils import FrappeTestCase - -from erpnext.controllers.item_variant import create_variant -from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import ( - setup_e_commerce_settings, -) -from erpnext.e_commerce.doctype.website_item.website_item import make_website_item -from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values -from erpnext.stock.doctype.item.test_item import make_item - -test_dependencies = ["Item"] - - -class TestVariantSelector(FrappeTestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - template_item = make_item( - "Test-Tshirt-Temp", - { - "has_variant": 1, - "variant_based_on": "Item Attribute", - "attributes": [{"attribute": "Test Size"}, {"attribute": "Test Colour"}], - }, - ) - - # create L-R, L-G, M-R, M-G and S-R - for size in ( - "Large", - "Medium", - ): - for colour in ( - "Red", - "Green", - ): - variant = create_variant("Test-Tshirt-Temp", {"Test Size": size, "Test Colour": colour}) - variant.save() - - variant = create_variant("Test-Tshirt-Temp", {"Test Size": "Small", "Test Colour": "Red"}) - variant.save() - - make_website_item(template_item) # publish template not variants - - def test_item_attributes(self): - """ - Test if the right attributes are fetched in the popup. - (Attributes must only come from active items) - - Attribute selection must not be linked to Website Items. - """ - from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values - - attr_data = get_attributes_and_values("Test-Tshirt-Temp") - - self.assertEqual(attr_data[0]["attribute"], "Test Size") - self.assertEqual(attr_data[1]["attribute"], "Test Colour") - self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large'] - self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green'] - - # disable small red tshirt, now there are no small tshirts. - # but there are some red tshirts - small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R") - small_variant.disabled = 1 - small_variant.save() # trigger cache rebuild - - attr_data = get_attributes_and_values("Test-Tshirt-Temp") - - # Only L and M attribute values must be fetched since S is disabled - self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large'] - - # teardown - small_variant.disabled = 0 - small_variant.save() - - def test_next_item_variant_values(self): - """ - Test if on selecting an attribute value, the next possible values - are filtered accordingly. - Values that dont apply should not be fetched. - E.g. - There is a ** Small-Red ** Tshirt. No other colour in this size. - On selecting ** Small **, only ** Red ** should be selectable next. - """ - next_values = get_next_attribute_and_values( - "Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"} - ) - next_colours = next_values["valid_options_for_attributes"]["Test Colour"] - filtered_items = next_values["filtered_items"] - - self.assertEqual(len(next_colours), 1) - self.assertEqual(next_colours.pop(), "Red") - self.assertEqual(len(filtered_items), 1) - self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R") - - def test_exact_match_with_price(self): - """ - Test price fetching and matching of variant without Website Item - """ - from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price - - frappe.set_user("Administrator") - setup_e_commerce_settings( - { - "company": "_Test Company", - "enabled": 1, - "default_customer_group": "_Test Customer Group", - "price_list": "_Test Price List India", - "show_price": 1, - } - ) - - make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100) - - frappe.local.shopping_cart_settings = None # clear cached settings values - next_values = get_next_attribute_and_values( - "Test-Tshirt-Temp", selected_attributes={"Test Size": "Small", "Test Colour": "Red"} - ) - print(">>>>", next_values) - price_info = next_values["product_info"]["price"] - - self.assertEqual(next_values["exact_match"][0], "Test-Tshirt-Temp-S-R") - self.assertEqual(next_values["exact_match"][0], "Test-Tshirt-Temp-S-R") - self.assertEqual(price_info["price_list_rate"], 100.0) - self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00") diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py deleted file mode 100644 index 88356f5e90..0000000000 --- a/erpnext/e_commerce/variant_selector/utils.py +++ /dev/null @@ -1,251 +0,0 @@ -import frappe -from frappe.utils import cint, flt - -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( - get_shopping_cart_settings, -) -from erpnext.e_commerce.shopping_cart.cart import _set_price_list -from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager -from erpnext.utilities.product import get_price - - -def get_item_codes_by_attributes(attribute_filters, template_item_code=None): - items = [] - - for attribute, values in attribute_filters.items(): - attribute_values = values - - if not isinstance(attribute_values, list): - attribute_values = [attribute_values] - - if not attribute_values: - continue - - wheres = [] - query_values = [] - for attribute_value in attribute_values: - wheres.append("( attribute = %s and attribute_value = %s )") - query_values += [attribute, attribute_value] - - attribute_query = " or ".join(wheres) - - if template_item_code: - variant_of_query = "AND t2.variant_of = %s" - query_values.append(template_item_code) - else: - variant_of_query = "" - - query = """ - SELECT - t1.parent - FROM - `tabItem Variant Attribute` t1 - WHERE - 1 = 1 - AND ( - {attribute_query} - ) - AND EXISTS ( - SELECT - 1 - FROM - `tabItem` t2 - WHERE - t2.name = t1.parent - {variant_of_query} - ) - GROUP BY - t1.parent - ORDER BY - NULL - """.format( - attribute_query=attribute_query, variant_of_query=variant_of_query - ) - - item_codes = set([r[0] for r in frappe.db.sql(query, query_values)]) # nosemgrep - items.append(item_codes) - - res = list(set.intersection(*items)) - - return res - - -@frappe.whitelist(allow_guest=True) -def get_attributes_and_values(item_code): - """Build a list of attributes and their possible values. - This will ignore the values upon selection of which there cannot exist one item. - """ - item_cache = ItemVariantsCacheManager(item_code) - item_variants_data = item_cache.get_item_variants_data() - - attributes = get_item_attributes(item_code) - attribute_list = [a.attribute for a in attributes] - - valid_options = {} - for item_code, attribute, attribute_value in item_variants_data: - if attribute in attribute_list: - valid_options.setdefault(attribute, set()).add(attribute_value) - - item_attribute_values = frappe.db.get_all( - "Item Attribute Value", ["parent", "attribute_value", "idx"], order_by="parent asc, idx asc" - ) - ordered_attribute_value_map = frappe._dict() - for iv in item_attribute_values: - ordered_attribute_value_map.setdefault(iv.parent, []).append(iv.attribute_value) - - # build attribute values in idx order - for attr in attributes: - valid_attribute_values = valid_options.get(attr.attribute, []) - ordered_values = ordered_attribute_value_map.get(attr.attribute, []) - attr["values"] = [v for v in ordered_values if v in valid_attribute_values] - - return attributes - - -@frappe.whitelist(allow_guest=True) -def get_next_attribute_and_values(item_code, selected_attributes): - from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses - - """Find the count of Items that match the selected attributes. - Also, find the attribute values that are not applicable for further searching. - If less than equal to 10 items are found, return item_codes of those items. - If one item is matched exactly, return item_code of that item. - """ - selected_attributes = frappe.parse_json(selected_attributes) - - item_cache = ItemVariantsCacheManager(item_code) - item_variants_data = item_cache.get_item_variants_data() - - attributes = get_item_attributes(item_code) - attribute_list = [a.attribute for a in attributes] - filtered_items = get_items_with_selected_attributes(item_code, selected_attributes) - - next_attribute = None - - for attribute in attribute_list: - if attribute not in selected_attributes: - next_attribute = attribute - break - - valid_options_for_attributes = frappe._dict() - - for a in attribute_list: - valid_options_for_attributes[a] = set() - - selected_attribute = selected_attributes.get(a, None) - if selected_attribute: - # already selected attribute values are valid options - valid_options_for_attributes[a].add(selected_attribute) - - for row in item_variants_data: - item_code, attribute, attribute_value = row - if ( - item_code in filtered_items - and attribute not in selected_attributes - and attribute in attribute_list - ): - valid_options_for_attributes[attribute].add(attribute_value) - - optional_attributes = item_cache.get_optional_attributes() - exact_match = [] - # search for exact match if all selected attributes are required attributes - if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)): - item_attribute_value_map = item_cache.get_item_attribute_value_map() - for item_code, attr_dict in item_attribute_value_map.items(): - if item_code in filtered_items and set(attr_dict.keys()) == set(selected_attributes.keys()): - exact_match.append(item_code) - - filtered_items_count = len(filtered_items) - - # get product info if exact match - # from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website - if exact_match: - cart_settings = get_shopping_cart_settings() - product_info = get_item_variant_price_dict(exact_match[0], cart_settings) - - if product_info: - product_info["is_stock_item"] = frappe.get_cached_value("Item", exact_match[0], "is_stock_item") - product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock) - else: - product_info = None - - product_id = "" - warehouse = "" - if exact_match or filtered_items: - if exact_match and len(exact_match) == 1: - product_id = exact_match[0] - elif filtered_items_count == 1: - product_id = list(filtered_items)[0] - - if product_id: - warehouse = frappe.get_cached_value( - "Website Item", {"item_code": product_id}, "website_warehouse" - ) - - available_qty = 0.0 - if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1: - warehouses = get_child_warehouses(warehouse) - else: - warehouses = [warehouse] if warehouse else [] - - for warehouse in warehouses: - available_qty += flt( - frappe.db.get_value("Bin", {"item_code": product_id, "warehouse": warehouse}, "actual_qty") - ) - - return { - "next_attribute": next_attribute, - "valid_options_for_attributes": valid_options_for_attributes, - "filtered_items_count": filtered_items_count, - "filtered_items": filtered_items if filtered_items_count < 10 else [], - "exact_match": exact_match, - "product_info": product_info, - "available_qty": available_qty, - } - - -def get_items_with_selected_attributes(item_code, selected_attributes): - item_cache = ItemVariantsCacheManager(item_code) - attribute_value_item_map = item_cache.get_attribute_value_item_map() - - items = [] - for attribute, value in selected_attributes.items(): - filtered_items = attribute_value_item_map.get((attribute, value), []) - items.append(set(filtered_items)) - - return set.intersection(*items) - - -# utilities - - -def get_item_attributes(item_code): - attributes = frappe.db.get_all( - "Item Variant Attribute", - fields=["attribute"], - filters={"parenttype": "Item", "parent": item_code}, - order_by="idx asc", - ) - - optional_attributes = ItemVariantsCacheManager(item_code).get_optional_attributes() - - for a in attributes: - if a.attribute in optional_attributes: - a.optional = True - - return attributes - - -def get_item_variant_price_dict(item_code, cart_settings): - if cart_settings.enabled and cart_settings.show_price: - is_guest = frappe.session.user == "Guest" - # Show Price if logged in. - # If not logged in, check if price is hidden for guest. - if not is_guest or not cart_settings.hide_price_for_guest: - price_list = _set_price_list(cart_settings, None) - price = get_price( - item_code, price_list, cart_settings.default_customer_group, cart_settings.company - ) - return {"price": price} - - return None diff --git a/erpnext/e_commerce/web_template/__init__.py b/erpnext/e_commerce/web_template/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/web_template/hero_slider/__init__.py b/erpnext/e_commerce/web_template/hero_slider/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html deleted file mode 100644 index fe4fee375b..0000000000 --- a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html +++ /dev/null @@ -1,86 +0,0 @@ -{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%} -{%- set align_class = resolve_class({ - 'text-right': align == 'Right', - 'text-center': align == 'Centre', - 'text-left': align == 'Left', -}) -%} - -{%- set heading_class = resolve_class({ - 'text-white': theme == 'Dark', - '': theme == 'Light', -}) -%} - -{%- endmacro -%} - -{%- set hero_slider_id = 'id-' + frappe.utils.generate_hash('HeroSlider', 12) -%} - - - - diff --git a/erpnext/e_commerce/web_template/hero_slider/hero_slider.json b/erpnext/e_commerce/web_template/hero_slider/hero_slider.json deleted file mode 100644 index 39b2b3eaeb..0000000000 --- a/erpnext/e_commerce/web_template/hero_slider/hero_slider.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "__unsaved": 1, - "creation": "2020-11-17 15:21:51.207221", - "docstatus": 0, - "doctype": "Web Template", - "fields": [ - { - "fieldname": "slider_name", - "fieldtype": "Data", - "label": "Slider Name", - "reqd": 1 - }, - { - "default": "1", - "fieldname": "show_indicators", - "fieldtype": "Check", - "label": "Show Indicators", - "reqd": 0 - }, - { - "default": "1", - "fieldname": "show_controls", - "fieldtype": "Check", - "label": "Show Controls", - "reqd": 0 - }, - { - "fieldname": "slide_1", - "fieldtype": "Section Break", - "label": "Slide 1", - "reqd": 0 - }, - { - "fieldname": "slide_1_image", - "fieldtype": "Attach Image", - "label": "Image", - "reqd": 0 - }, - { - "fieldname": "slide_1_title", - "fieldtype": "Data", - "label": "Title", - "reqd": 0 - }, - { - "fieldname": "slide_1_subtitle", - "fieldtype": "Small Text", - "label": "Subtitle", - "reqd": 0 - }, - { - "fieldname": "slide_1_primary_action_label", - "fieldtype": "Data", - "label": "Primary Action Label", - "reqd": 0 - }, - { - "fieldname": "slide_1_primary_action", - "fieldtype": "Data", - "label": "Primary Action", - "reqd": 0 - }, - { - "fieldname": "slide_1_content_align", - "fieldtype": "Select", - "label": "Content Align", - "options": "Left\nCentre\nRight", - "reqd": 0 - }, - { - "fieldname": "slide_1_theme", - "fieldtype": "Select", - "label": "Slide Theme", - "options": "Dark\nLight", - "reqd": 0 - }, - { - "fieldname": "slide_2", - "fieldtype": "Section Break", - "label": "Slide 2", - "reqd": 0 - }, - { - "fieldname": "slide_2_image", - "fieldtype": "Attach Image", - "label": "Image ", - "reqd": 0 - }, - { - "fieldname": "slide_2_title", - "fieldtype": "Data", - "label": "Title ", - "reqd": 0 - }, - { - "fieldname": "slide_2_subtitle", - "fieldtype": "Small Text", - "label": "Subtitle ", - "reqd": 0 - }, - { - "fieldname": "slide_2_primary_action_label", - "fieldtype": "Data", - "label": "Primary Action Label ", - "reqd": 0 - }, - { - "fieldname": "slide_2_primary_action", - "fieldtype": "Data", - "label": "Primary Action ", - "reqd": 0 - }, - { - "default": "Left", - "fieldname": "slide_2_content_align", - "fieldtype": "Select", - "label": "Content Align", - "options": "Left\nCentre\nRight", - "reqd": 0 - }, - { - "fieldname": "slide_2_theme", - "fieldtype": "Select", - "label": "Slide Theme", - "options": "Dark\nLight", - "reqd": 0 - }, - { - "fieldname": "slide_3", - "fieldtype": "Section Break", - "label": "Slide 3", - "reqd": 0 - }, - { - "fieldname": "slide_3_image", - "fieldtype": "Attach Image", - "label": "Image", - "reqd": 0 - }, - { - "fieldname": "slide_3_title", - "fieldtype": "Data", - "label": "Title", - "reqd": 0 - }, - { - "fieldname": "slide_3_subtitle", - "fieldtype": "Small Text", - "label": "Subtitle", - "reqd": 0 - }, - { - "fieldname": "slide_3_primary_action_label", - "fieldtype": "Data", - "label": "Primary Action Label", - "reqd": 0 - }, - { - "fieldname": "slide_3_primary_action", - "fieldtype": "Data", - "label": "Primary Action", - "reqd": 0 - }, - { - "fieldname": "slide_3_content_align", - "fieldtype": "Select", - "label": "Content Align", - "options": "Left\nCentre\nRight", - "reqd": 0 - }, - { - "fieldname": "slide_3_theme", - "fieldtype": "Select", - "label": "Slide Theme", - "options": "Dark\nLight", - "reqd": 0 - }, - { - "fieldname": "slide_4", - "fieldtype": "Section Break", - "label": "Slide 4", - "reqd": 0 - }, - { - "fieldname": "slide_4_image", - "fieldtype": "Attach Image", - "label": "Image", - "reqd": 0 - }, - { - "fieldname": "slide_4_title", - "fieldtype": "Data", - "label": "Title", - "reqd": 0 - }, - { - "fieldname": "slide_4_subtitle", - "fieldtype": "Small Text", - "label": "Subtitle", - "reqd": 0 - }, - { - "fieldname": "slide_4_primary_action_label", - "fieldtype": "Data", - "label": "Primary Action Label", - "reqd": 0 - }, - { - "fieldname": "slide_4_primary_action", - "fieldtype": "Data", - "label": "Primary Action", - "reqd": 0 - }, - { - "fieldname": "slide_4_content_align", - "fieldtype": "Select", - "label": "Content Align", - "options": "Left\nCentre\nRight", - "reqd": 0 - }, - { - "fieldname": "slide_4_theme", - "fieldtype": "Select", - "label": "Slide Theme", - "options": "Dark\nLight", - "reqd": 0 - }, - { - "fieldname": "slide_5", - "fieldtype": "Section Break", - "label": "Slide 5", - "reqd": 0 - }, - { - "fieldname": "slide_5_image", - "fieldtype": "Attach Image", - "label": "Image", - "reqd": 0 - }, - { - "fieldname": "slide_5_title", - "fieldtype": "Data", - "label": "Title", - "reqd": 0 - }, - { - "fieldname": "slide_5_subtitle", - "fieldtype": "Small Text", - "label": "Subtitle", - "reqd": 0 - }, - { - "fieldname": "slide_5_primary_action_label", - "fieldtype": "Data", - "label": "Primary Action Label", - "reqd": 0 - }, - { - "fieldname": "slide_5_primary_action", - "fieldtype": "Data", - "label": "Primary Action", - "reqd": 0 - }, - { - "fieldname": "slide_5_content_align", - "fieldtype": "Select", - "label": "Content Align", - "options": "Left\nCentre\nRight", - "reqd": 0 - }, - { - "fieldname": "slide_5_theme", - "fieldtype": "Select", - "label": "Slide Theme", - "options": "Dark\nLight", - "reqd": 0 - } - ], - "idx": 2, - "modified": "2023-05-12 15:03:57.604060", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Hero Slider", - "owner": "Administrator", - "standard": 1, - "template": "", - "type": "Section" -} \ No newline at end of file diff --git a/erpnext/e_commerce/web_template/item_card_group/__init__.py b/erpnext/e_commerce/web_template/item_card_group/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/web_template/item_card_group/item_card_group.html b/erpnext/e_commerce/web_template/item_card_group/item_card_group.html deleted file mode 100644 index 07952f056a..0000000000 --- a/erpnext/e_commerce/web_template/item_card_group/item_card_group.html +++ /dev/null @@ -1,37 +0,0 @@ -{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %} - -
-
-
- {%- if title -%} -

{{ title }}

- {%- endif -%} - {%- if subtitle -%} -

{{ subtitle }}

- {%- endif -%} -
-
- {%- if primary_action -%} - - {{ primary_action_label }} - - {%- endif -%} -
-
- -
- {%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%} - {%- set item = values['card_' + index + '_item'] -%} - {%- if item -%} - {%- set web_item = frappe.get_doc("Website Item", item) -%} - {{ item_card( - web_item, is_featured=values['card_' + index + '_featured'], - is_full_width=True, align="Center" - ) }} - {%- endif -%} - {%- endfor -%} -
-
- - diff --git a/erpnext/e_commerce/web_template/item_card_group/item_card_group.json b/erpnext/e_commerce/web_template/item_card_group/item_card_group.json deleted file mode 100644 index ad9e2a7b24..0000000000 --- a/erpnext/e_commerce/web_template/item_card_group/item_card_group.json +++ /dev/null @@ -1,270 +0,0 @@ -{ - "__unsaved": 1, - "creation": "2020-11-17 15:35:05.285322", - "docstatus": 0, - "doctype": "Web Template", - "fields": [ - { - "fieldname": "title", - "fieldtype": "Data", - "label": "Title", - "reqd": 1 - }, - { - "fieldname": "subtitle", - "fieldtype": "Data", - "label": "Subtitle", - "reqd": 0 - }, - { - "fieldname": "primary_action_label", - "fieldtype": "Data", - "label": "Primary Action Label", - "reqd": 0 - }, - { - "fieldname": "primary_action", - "fieldtype": "Data", - "label": "Primary Action", - "reqd": 0 - }, - { - "fieldname": "card_1", - "fieldtype": "Section Break", - "label": "Card 1", - "reqd": 0 - }, - { - "fieldname": "card_1_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_1_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_2", - "fieldtype": "Section Break", - "label": "Card 2", - "reqd": 0 - }, - { - "fieldname": "card_2_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_2_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_3", - "fieldtype": "Section Break", - "label": "Card 3", - "options": "", - "reqd": 0 - }, - { - "fieldname": "card_3_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_3_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_4", - "fieldtype": "Section Break", - "label": "Card 4", - "reqd": 0 - }, - { - "fieldname": "card_4_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_4_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_5", - "fieldtype": "Section Break", - "label": "Card 5", - "reqd": 0 - }, - { - "fieldname": "card_5_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_5_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_6", - "fieldtype": "Section Break", - "label": "Card 6", - "reqd": 0 - }, - { - "fieldname": "card_6_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_6_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_7", - "fieldtype": "Section Break", - "label": "Card 7", - "reqd": 0 - }, - { - "fieldname": "card_7_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_7_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_8", - "fieldtype": "Section Break", - "label": "Card 8", - "reqd": 0 - }, - { - "fieldname": "card_8_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_8_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_9", - "fieldtype": "Section Break", - "label": "Card 9", - "reqd": 0 - }, - { - "fieldname": "card_9_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_9_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_10", - "fieldtype": "Section Break", - "label": "Card 10", - "reqd": 0 - }, - { - "fieldname": "card_10_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_10_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_11", - "fieldtype": "Section Break", - "label": "Card 11", - "reqd": 0 - }, - { - "fieldname": "card_11_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_11_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - }, - { - "fieldname": "card_12", - "fieldtype": "Section Break", - "label": "Card 12", - "reqd": 0 - }, - { - "fieldname": "card_12_item", - "fieldtype": "Link", - "label": "Website Item", - "options": "Website Item", - "reqd": 0 - }, - { - "fieldname": "card_12_featured", - "fieldtype": "Check", - "label": "Featured", - "reqd": 0 - } - ], - "idx": 0, - "modified": "2021-12-21 14:44:59.821335", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Item Card Group", - "owner": "Administrator", - "standard": 1, - "template": "", - "type": "Section" -} \ No newline at end of file diff --git a/erpnext/e_commerce/web_template/product_card/__init__.py b/erpnext/e_commerce/web_template/product_card/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/web_template/product_card/product_card.html b/erpnext/e_commerce/web_template/product_card/product_card.html deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/web_template/product_card/product_card.json b/erpnext/e_commerce/web_template/product_card/product_card.json deleted file mode 100644 index 2eb73741ef..0000000000 --- a/erpnext/e_commerce/web_template/product_card/product_card.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "__unsaved": 1, - "creation": "2020-11-17 15:28:47.809342", - "docstatus": 0, - "doctype": "Web Template", - "fields": [ - { - "fieldname": "item", - "fieldtype": "Link", - "label": "Item", - "options": "Item", - "reqd": 0 - }, - { - "fieldname": "featured", - "fieldtype": "Check", - "label": "Featured", - "options": "", - "reqd": 0 - } - ], - "idx": 0, - "modified": "2021-02-24 16:05:17.926610", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Product Card", - "owner": "Administrator", - "standard": 1, - "template": "", - "type": "Component" -} \ No newline at end of file diff --git a/erpnext/e_commerce/web_template/product_category_cards/__init__.py b/erpnext/e_commerce/web_template/product_category_cards/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html deleted file mode 100644 index 6d75a8b1d5..0000000000 --- a/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html +++ /dev/null @@ -1,47 +0,0 @@ -{%- macro card(title, image, url, text_primary=False) -%} -{%- set align_class = resolve_class({ - 'text-right': text_primary, - 'text-centre': align == 'Center', - 'text-left': align == 'Left', -}) -%} -
- {% if image %} - {{ title }} - {% else %} -
- - {{ frappe.utils.get_abbr(title or '') }} - -
- {% endif %} - -
- {{ title or '' }} -
- -
-{%- endmacro -%} - -
- {%- if title -%} -

{{ title }}

- {%- endif -%} - {%- if subtitle -%} -

{{ subtitle }}

- {%- endif -%} - -
-
- {%- for index in ['1', '2', '3', '4', '5', '6', '7', '8'] -%} - {%- set category = values['category_' + index] -%} - {%- if category -%} - {%- set category = frappe.get_doc("Item Group", category) -%} - {{ card(category.name, category.image, category.route) }} - {%- endif -%} - {%- endfor -%} -
-
-
- - diff --git a/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json deleted file mode 100644 index 0202165d08..0000000000 --- a/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "__unsaved": 1, - "creation": "2020-11-17 15:25:50.855934", - "docstatus": 0, - "doctype": "Web Template", - "fields": [ - { - "fieldname": "title", - "fieldtype": "Data", - "label": "Title", - "reqd": 1 - }, - { - "fieldname": "subtitle", - "fieldtype": "Data", - "label": "Subtitle", - "reqd": 0 - }, - { - "fieldname": "category_1", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group", - "reqd": 0 - }, - { - "fieldname": "category_2", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group", - "reqd": 0 - }, - { - "fieldname": "category_3", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group", - "reqd": 0 - }, - { - "fieldname": "category_4", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group", - "reqd": 0 - }, - { - "fieldname": "category_5", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group", - "reqd": 0 - }, - { - "fieldname": "category_6", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group", - "reqd": 0 - }, - { - "fieldname": "category_7", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group", - "reqd": 0 - }, - { - "fieldname": "category_8", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group", - "reqd": 0 - } - ], - "idx": 0, - "modified": "2021-02-24 16:03:33.835635", - "modified_by": "Administrator", - "module": "E-commerce", - "name": "Product Category Cards", - "owner": "Administrator", - "standard": 1, - "template": "", - "type": "Section" -} \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2155699a4c..7446f2cc36 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", 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 8f2d076b53..aebad557dd 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 @@ -346,4 +340,4 @@ 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 # 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_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.

  1. https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce


""" - note.insert(ignore_mandatory=True) diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index a53adf1a83..9a2a39fb78 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -11,6 +11,9 @@ def execute(): asset_depreciation_schedules_map = get_asset_depreciation_schedules_map() for asset in assets: + if not asset_depreciation_schedules_map.get(asset.name): + continue + depreciation_schedules = asset_depreciation_schedules_map[asset.name] for fb_row in asset_finance_books_map[asset.name]: diff --git a/erpnext/patches/v15_0/delete_ecommerce_doctypes.py b/erpnext/patches/v15_0/delete_ecommerce_doctypes.py new file mode 100644 index 0000000000..af0398782e --- /dev/null +++ b/erpnext/patches/v15_0/delete_ecommerce_doctypes.py @@ -0,0 +1,30 @@ +import click +import frappe + + +def execute(): + if "webshop" in frappe.get_installed_apps(): + return + + if not frappe.db.table_exists("Website Item"): + return + + doctypes = [ + "E Commerce Settings", + "Website Item", + "Recommended Items", + "Item Review", + "Wishlist Item", + "Wishlist", + "Website Offer", + "Website Item Tabbed Section", + ] + + for doctype in doctypes: + frappe.delete_doc("DocType", doctype, ignore_missing=True) + + click.secho( + "ECommerce is renamed and moved to a separate app" + "Please install the app for ECommerce features: https://github.com/frappe/webshop", + fg="yellow", + ) diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js index 59f808a315..6797904424 100644 --- a/erpnext/portal/doctype/homepage/homepage.js +++ b/erpnext/portal/doctype/homepage/homepage.js @@ -19,12 +19,3 @@ frappe.ui.form.on('Homepage', { }); }, }); - -frappe.ui.form.on('Homepage Featured Product', { - view: function(frm, cdt, cdn) { - var child= locals[cdt][cdn]; - if (child.item_code && child.route) { - window.open('/' + child.route, '_blank'); - } - } -}); diff --git a/erpnext/portal/doctype/homepage/homepage.json b/erpnext/portal/doctype/homepage/homepage.json index 73f816d4d4..2b891f7268 100644 --- a/erpnext/portal/doctype/homepage/homepage.json +++ b/erpnext/portal/doctype/homepage/homepage.json @@ -15,10 +15,7 @@ "description", "hero_image", "slideshow", - "hero_section", - "products_section", - "products_url", - "products" + "hero_section" ], "fields": [ { @@ -86,30 +83,11 @@ "fieldtype": "Link", "label": "Homepage Section", "options": "Homepage Section" - }, - { - "fieldname": "products_section", - "fieldtype": "Section Break", - "label": "Products" - }, - { - "default": "/all-products", - "fieldname": "products_url", - "fieldtype": "Data", - "label": "URL for \"All Products\"" - }, - { - "description": "Products to be shown on website homepage", - "fieldname": "products", - "fieldtype": "Table", - "label": "Products", - "options": "Homepage Featured Product", - "width": "40px" } ], "issingle": 1, "links": [], - "modified": "2021-02-18 13:29:29.531639", + "modified": "2022-12-19 21:10:29.127277", "modified_by": "Administrator", "module": "Portal", "name": "Homepage", @@ -138,6 +116,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "company", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/portal/doctype/homepage/homepage.py b/erpnext/portal/doctype/homepage/homepage.py index 0d2e360788..c0a0c07d7d 100644 --- a/erpnext/portal/doctype/homepage/homepage.py +++ b/erpnext/portal/doctype/homepage/homepage.py @@ -12,26 +12,3 @@ class Homepage(Document): if not self.description: self.description = frappe._("This is an example website auto-generated from ERPNext") delete_page_cache("home") - - def setup_items(self): - for d in frappe.get_all( - "Website Item", - fields=["name", "item_name", "description", "website_image", "route"], - filters={"published": 1}, - limit=3, - ): - - doc = frappe.get_doc("Website Item", d.name) - if not doc.route: - # set missing route - doc.save() - self.append( - "products", - dict( - item_code=d.name, - item_name=d.item_name, - description=d.description, - image=d.website_image, - route=d.route, - ), - ) diff --git a/erpnext/portal/doctype/homepage_featured_product/__init__.py b/erpnext/portal/doctype/homepage_featured_product/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json deleted file mode 100644 index 63789e35b5..0000000000 --- a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "actions": [], - "autoname": "hash", - "creation": "2016-04-22 05:57:06.261401", - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "item_code", - "col_break1", - "item_name", - "view", - "section_break_5", - "description", - "column_break_7", - "image", - "thumbnail", - "route" - ], - "fields": [ - { - "bold": 1, - "fieldname": "item_code", - "fieldtype": "Link", - "in_filter": 1, - "in_list_view": 1, - "label": "Item", - "oldfieldname": "item_code", - "oldfieldtype": "Link", - "options": "Website Item", - "print_width": "150px", - "reqd": 1, - "search_index": 1, - "width": "150px" - }, - { - "fieldname": "col_break1", - "fieldtype": "Column Break" - }, - { - "fetch_from": "item_code.item_name", - "fetch_if_empty": 1, - "fieldname": "item_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Item Name", - "oldfieldname": "item_name", - "oldfieldtype": "Data", - "print_hide": 1, - "print_width": "150", - "read_only": 1, - "reqd": 1, - "width": "150" - }, - { - "fieldname": "view", - "fieldtype": "Button", - "in_list_view": 1, - "label": "View" - }, - { - "collapsible": 1, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "label": "Details" - }, - { - "fetch_from": "item_code.web_long_description", - "fieldname": "description", - "fieldtype": "Text Editor", - "in_filter": 1, - "in_list_view": 1, - "label": "Description", - "oldfieldname": "description", - "oldfieldtype": "Small Text", - "print_width": "300px", - "width": "300px" - }, - { - "fieldname": "column_break_7", - "fieldtype": "Column Break" - }, - { - "fetch_from": "item_code.website_image", - "fetch_if_empty": 1, - "fieldname": "image", - "fieldtype": "Attach Image", - "label": "Image" - }, - { - "fetch_from": "item_code.thumbnail", - "fieldname": "thumbnail", - "fieldtype": "Attach Image", - "hidden": 1, - "label": "Thumbnail" - }, - { - "fetch_from": "item_code.route", - "fieldname": "route", - "fieldtype": "Small Text", - "label": "route", - "read_only": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-02-18 13:05:50.669311", - "modified_by": "Administrator", - "module": "Portal", - "name": "Homepage Featured Product", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC" -} \ No newline at end of file diff --git a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.py b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.py deleted file mode 100644 index c21461d631..0000000000 --- a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class HomepageFeaturedProduct(Document): - pass diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py index c8b03e678b..903d4a6196 100644 --- a/erpnext/portal/utils.py +++ b/erpnext/portal/utils.py @@ -1,10 +1,4 @@ import frappe -from frappe.utils.nestedset import get_root_of - -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( - get_shopping_cart_settings, -) -from erpnext.e_commerce.shopping_cart.cart import get_debtors_account def set_default_role(doc, method): @@ -56,26 +50,7 @@ def create_customer_or_supplier(): party = frappe.new_doc(doctype) fullname = frappe.utils.get_fullname(user) - if doctype == "Customer": - cart_settings = get_shopping_cart_settings() - - if cart_settings.enable_checkout: - debtors_account = get_debtors_account(cart_settings) - else: - debtors_account = "" - - party.update( - { - "customer_name": fullname, - "customer_type": "Individual", - "customer_group": cart_settings.default_customer_group, - "territory": get_root_of("Territory"), - } - ) - - if debtors_account: - party.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]}) - else: + if not doctype == "Customer": party.update( { "supplier_name": fullname, diff --git a/erpnext/public/js/customer_reviews.js b/erpnext/public/js/customer_reviews.js deleted file mode 100644 index e13ded6b48..0000000000 --- a/erpnext/public/js/customer_reviews.js +++ /dev/null @@ -1,138 +0,0 @@ -$(() => { - class CustomerReviews { - constructor() { - this.bind_button_actions(); - this.start = 0; - this.page_length = 10; - } - - bind_button_actions() { - this.write_review(); - this.view_more(); - } - - write_review() { - //TODO: make dialog popup on stray page - $('.page_content').on('click', '.btn-write-review', (e) => { - // Bind action on write a review button - const $btn = $(e.currentTarget); - - let d = new frappe.ui.Dialog({ - title: __("Write a Review"), - fields: [ - {fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1}, - {fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1}, - {fieldtype: "Section Break"}, - {fieldname: "comment", fieldtype: "Small Text", label: "Your Review"} - ], - primary_action: function() { - let data = d.get_values(); - frappe.call({ - method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review", - args: { - web_item: $btn.attr('data-web-item'), - title: data.title, - rating: data.rating, - comment: data.comment - }, - freeze: true, - freeze_message: __("Submitting Review ..."), - callback: (r) => { - if (!r.exc) { - frappe.msgprint({ - message: __("Thank you for submitting your review"), - title: __("Review Submitted"), - indicator: "green" - }); - d.hide(); - location.reload(); - } - } - }); - }, - primary_action_label: __('Submit') - }); - d.show(); - }); - } - - view_more() { - $('.page_content').on('click', '.btn-view-more', (e) => { - // Bind action on view more button - const $btn = $(e.currentTarget); - $btn.prop('disabled', true); - - this.start += this.page_length; - let me = this; - - frappe.call({ - method: "erpnext.e_commerce.doctype.item_review.item_review.get_item_reviews", - args: { - web_item: $btn.attr('data-web-item'), - start: me.start, - end: me.page_length - }, - callback: (result) => { - if (result.message) { - let res = result.message; - me.get_user_review_html(res.reviews); - - $btn.prop('disabled', false); - if (res.total_reviews <= (me.start + me.page_length)) { - $btn.hide(); - } - - } - } - }); - }); - - } - - get_user_review_html(reviews) { - let me = this; - let $content = $('.user-reviews'); - - reviews.forEach((review) => { - $content.append(` -
-
-

- ${__(review.review_title)} -

-
- ${me.get_review_stars(review.rating)} -
-
- -
-

- ${__(review.comment)} -

-
-
- ${__(review.customer)} - - ${__(review.published_on)} -
-
- `); - }); - } - - get_review_stars(rating) { - let stars = ``; - for (let i = 1; i < 6; i++) { - let fill_class = i <= rating ? 'star-click' : ''; - stars += ` - - - - `; - } - return stars; - } - } - - new CustomerReviews(); -}); \ No newline at end of file diff --git a/erpnext/public/js/erpnext-web.bundle.js b/erpnext/public/js/erpnext-web.bundle.js index cbe899dc06..45c6a648ec 100644 --- a/erpnext/public/js/erpnext-web.bundle.js +++ b/erpnext/public/js/erpnext-web.bundle.js @@ -1,8 +1 @@ import "./website_utils"; -import "./wishlist"; -import "./shopping_cart"; -import "./customer_reviews"; -import "../../e_commerce/product_ui/list"; -import "../../e_commerce/product_ui/views"; -import "../../e_commerce/product_ui/grid"; -import "../../e_commerce/product_ui/search"; \ No newline at end of file diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js deleted file mode 100644 index d14740c106..0000000000 --- a/erpnext/public/js/shopping_cart.js +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt - -// shopping cart -frappe.provide("erpnext.e_commerce.shopping_cart"); -var shopping_cart = erpnext.e_commerce.shopping_cart; - -var getParams = function (url) { - var params = []; - var parser = document.createElement('a'); - parser.href = url; - var query = parser.search.substring(1); - var vars = query.split('&'); - for (var i = 0; i < vars.length; i++) { - var pair = vars[i].split('='); - params[pair[0]] = decodeURIComponent(pair[1]); - } - return params; -}; - -frappe.ready(function() { - var full_name = frappe.session && frappe.session.user_fullname; - // update user - if(full_name) { - $('.navbar li[data-label="User"] a') - .html(' ' + full_name); - } - // set coupon code and sales partner code - - var url_args = getParams(window.location.href); - - var referral_coupon_code = url_args['cc']; - var referral_sales_partner = url_args['sp']; - - var d = new Date(); - // expires within 30 minutes - d.setTime(d.getTime() + (0.02 * 24 * 60 * 60 * 1000)); - var expires = "expires="+d.toUTCString(); - if (referral_coupon_code) { - document.cookie = "referral_coupon_code=" + referral_coupon_code + ";" + expires + ";path=/"; - } - if (referral_sales_partner) { - document.cookie = "referral_sales_partner=" + referral_sales_partner + ";" + expires + ";path=/"; - } - referral_coupon_code=frappe.get_cookie("referral_coupon_code"); - referral_sales_partner=frappe.get_cookie("referral_sales_partner"); - - if (referral_coupon_code && $(".tot_quotation_discount").val()==undefined ) { - $(".txtcoupon").val(referral_coupon_code); - } - if (referral_sales_partner) { - $(".txtreferral_sales_partner").val(referral_sales_partner); - } - - // update login - shopping_cart.show_shoppingcart_dropdown(); - shopping_cart.set_cart_count(); - shopping_cart.show_cart_navbar(); -}); - -$.extend(shopping_cart, { - show_shoppingcart_dropdown: function() { - $(".shopping-cart").on('shown.bs.dropdown', function() { - if (!$('.shopping-cart-menu .cart-container').length) { - return frappe.call({ - method: 'erpnext.e_commerce.shopping_cart.cart.get_shopping_cart_menu', - callback: function(r) { - if (r.message) { - $('.shopping-cart-menu').html(r.message); - } - } - }); - } - }); - }, - - update_cart: function(opts) { - if (frappe.session.user==="Guest") { - if (localStorage) { - localStorage.setItem("last_visited", window.location.pathname); - } - frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => { - window.location.href = res.message || "/login"; - }); - } else { - shopping_cart.freeze(); - return frappe.call({ - type: "POST", - method: "erpnext.e_commerce.shopping_cart.cart.update_cart", - args: { - item_code: opts.item_code, - qty: opts.qty, - additional_notes: opts.additional_notes !== undefined ? opts.additional_notes : undefined, - with_items: opts.with_items || 0 - }, - btn: opts.btn, - callback: function(r) { - shopping_cart.unfreeze(); - shopping_cart.set_cart_count(true); - if(opts.callback) - opts.callback(r); - } - }); - } - }, - - set_cart_count: function(animate=false) { - $(".intermediate-empty-cart").remove(); - - var cart_count = frappe.get_cookie("cart_count"); - if(frappe.session.user==="Guest") { - cart_count = 0; - } - - if(cart_count) { - $(".shopping-cart").toggleClass('hidden', false); - } - - var $cart = $('.cart-icon'); - var $badge = $cart.find("#cart-count"); - - if(parseInt(cart_count) === 0 || cart_count === undefined) { - $cart.css("display", "none"); - $(".cart-tax-items").hide(); - $(".btn-place-order").hide(); - $(".cart-payment-addresses").hide(); - - let intermediate_empty_cart_msg = ` -
- ${ __("Cart is Empty") } -
- `; - $(".cart-table").after(intermediate_empty_cart_msg); - } - else { - $cart.css("display", "inline"); - $("#cart-count").text(cart_count); - } - - if(cart_count) { - $badge.html(cart_count); - - if (animate) { - $cart.addClass("cart-animate"); - setTimeout(() => { - $cart.removeClass("cart-animate"); - }, 500); - } - } else { - $badge.remove(); - } - }, - - shopping_cart_update: function({item_code, qty, cart_dropdown, additional_notes}) { - shopping_cart.update_cart({ - item_code, - qty, - additional_notes, - with_items: 1, - btn: this, - callback: function(r) { - if(!r.exc) { - $(".cart-items").html(r.message.items); - $(".cart-tax-items").html(r.message.total); - $(".payment-summary").html(r.message.taxes_and_totals); - shopping_cart.set_cart_count(); - - if (cart_dropdown != true) { - $(".cart-icon").hide(); - } - } - }, - }); - }, - - show_cart_navbar: function () { - frappe.call({ - method: "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.is_cart_enabled", - callback: function(r) { - $(".shopping-cart").toggleClass('hidden', r.message ? false : true); - } - }); - }, - - toggle_button_class(button, remove, add) { - button.removeClass(remove); - button.addClass(add); - }, - - bind_add_to_cart_action() { - $('.page_content').on('click', '.btn-add-to-cart-list', (e) => { - const $btn = $(e.currentTarget); - $btn.prop('disabled', true); - - if (frappe.session.user==="Guest") { - if (localStorage) { - localStorage.setItem("last_visited", window.location.pathname); - } - frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => { - window.location.href = res.message || "/login"; - }); - return; - } - - $btn.addClass('hidden'); - $btn.closest('.cart-action-container').addClass('d-flex'); - $btn.parent().find('.go-to-cart').removeClass('hidden'); - $btn.parent().find('.go-to-cart-grid').removeClass('hidden'); - $btn.parent().find('.cart-indicator').removeClass('hidden'); - - const item_code = $btn.data('item-code'); - erpnext.e_commerce.shopping_cart.update_cart({ - item_code, - qty: 1 - }); - - }); - }, - - freeze() { - if (window.location.pathname !== "/cart") return; - - if (!$('#freeze').length) { - let freeze = $('') - .appendTo("body"); - - setTimeout(function() { - freeze.addClass("show"); - }, 1); - } else { - $("#freeze").addClass("show"); - } - }, - - unfreeze() { - if ($('#freeze').length) { - let freeze = $('#freeze').removeClass("show"); - setTimeout(function() { - freeze.remove(); - }, 1); - } - } -}); diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js deleted file mode 100644 index f6599e9f6d..0000000000 --- a/erpnext/public/js/wishlist.js +++ /dev/null @@ -1,204 +0,0 @@ -frappe.provide("erpnext.e_commerce.wishlist"); -var wishlist = erpnext.e_commerce.wishlist; - -frappe.provide("erpnext.e_commerce.shopping_cart"); -var shopping_cart = erpnext.e_commerce.shopping_cart; - -$.extend(wishlist, { - set_wishlist_count: function(animate=false) { - // set badge count for wishlist icon - var wish_count = frappe.get_cookie("wish_count"); - if (frappe.session.user==="Guest") { - wish_count = 0; - } - - if (wish_count) { - $(".wishlist").toggleClass('hidden', false); - } - - var $wishlist = $('.wishlist-icon'); - var $badge = $wishlist.find("#wish-count"); - - if (parseInt(wish_count) === 0 || wish_count === undefined) { - $wishlist.css("display", "none"); - } else { - $wishlist.css("display", "inline"); - } - if (wish_count) { - $badge.html(wish_count); - if (animate) { - $wishlist.addClass('cart-animate'); - setTimeout(() => { - $wishlist.removeClass('cart-animate'); - }, 500); - } - } else { - $badge.remove(); - } - }, - - bind_move_to_cart_action: function() { - // move item to cart from wishlist - $('.page_content').on("click", ".btn-add-to-cart", (e) => { - const $move_to_cart_btn = $(e.currentTarget); - let item_code = $move_to_cart_btn.data("item-code"); - - shopping_cart.shopping_cart_update({ - item_code, - qty: 1, - cart_dropdown: true - }); - - let success_action = function() { - const $card_wrapper = $move_to_cart_btn.closest(".wishlist-card"); - $card_wrapper.addClass("wish-removed"); - }; - let args = { item_code: item_code }; - this.add_remove_from_wishlist("remove", args, success_action, null, true); - }); - }, - - bind_remove_action: function() { - // remove item from wishlist - let me = this; - - $('.page_content').on("click", ".remove-wish", (e) => { - const $remove_wish_btn = $(e.currentTarget); - let item_code = $remove_wish_btn.data("item-code"); - - let success_action = function() { - const $card_wrapper = $remove_wish_btn.closest(".wishlist-card"); - $card_wrapper.addClass("wish-removed"); - if (frappe.get_cookie("wish_count") == 0) { - $(".page_content").empty(); - me.render_empty_state(); - } - }; - let args = { item_code: item_code }; - this.add_remove_from_wishlist("remove", args, success_action); - }); - }, - - bind_wishlist_action() { - // 'wish'('like') or 'unwish' item in product listing - $('.page_content').on('click', '.like-action, .like-action-list', (e) => { - const $btn = $(e.currentTarget); - this.wishlist_action($btn); - }); - }, - - wishlist_action(btn) { - const $wish_icon = btn.find('.wish-icon'); - let me = this; - - if (frappe.session.user==="Guest") { - if (localStorage) { - localStorage.setItem("last_visited", window.location.pathname); - } - this.redirect_guest(); - return; - } - - let success_action = function() { - erpnext.e_commerce.wishlist.set_wishlist_count(true); - }; - - if ($wish_icon.hasClass('wished')) { - // un-wish item - btn.removeClass("like-animate"); - btn.addClass("like-action-wished"); - this.toggle_button_class($wish_icon, 'wished', 'not-wished'); - - let args = { item_code: btn.data('item-code') }; - let failure_action = function() { - me.toggle_button_class($wish_icon, 'not-wished', 'wished'); - }; - this.add_remove_from_wishlist("remove", args, success_action, failure_action); - } else { - // wish item - btn.addClass("like-animate"); - btn.addClass("like-action-wished"); - this.toggle_button_class($wish_icon, 'not-wished', 'wished'); - - let args = {item_code: btn.data('item-code')}; - let failure_action = function() { - me.toggle_button_class($wish_icon, 'wished', 'not-wished'); - }; - this.add_remove_from_wishlist("add", args, success_action, failure_action); - } - }, - - toggle_button_class(button, remove, add) { - button.removeClass(remove); - button.addClass(add); - }, - - add_remove_from_wishlist(action, args, success_action, failure_action, async=false) { - /* AJAX call to add or remove Item from Wishlist - action: "add" or "remove" - args: args for method (item_code, price, formatted_price), - success_action: method to execute on successs, - failure_action: method to execute on failure, - async: make call asynchronously (true/false). */ - if (frappe.session.user==="Guest") { - if (localStorage) { - localStorage.setItem("last_visited", window.location.pathname); - } - this.redirect_guest(); - } else { - let method = "erpnext.e_commerce.doctype.wishlist.wishlist.add_to_wishlist"; - if (action === "remove") { - method = "erpnext.e_commerce.doctype.wishlist.wishlist.remove_from_wishlist"; - } - - frappe.call({ - async: async, - type: "POST", - method: method, - args: args, - callback: function (r) { - if (r.exc) { - if (failure_action && (typeof failure_action === 'function')) { - failure_action(); - } - frappe.msgprint({ - message: __("Sorry, something went wrong. Please refresh."), - indicator: "red", title: __("Note") - }); - } else if (success_action && (typeof success_action === 'function')) { - success_action(); - } - } - }); - } - }, - - redirect_guest() { - frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => { - window.location.href = res.message || "/login"; - }); - }, - - render_empty_state() { - $(".page_content").append(` -
-
- Empty Cart -
-
${ __('Wishlist is empty !') }

-
- `); - } - -}); - -frappe.ready(function() { - if (window.location.pathname !== "/wishlist") { - $(".wishlist").toggleClass('hidden', true); - wishlist.set_wishlist_count(); - } else { - wishlist.bind_move_to_cart_action(); - wishlist.bind_remove_action(); - } - -}); \ No newline at end of file diff --git a/erpnext/public/scss/erpnext-web.bundle.scss b/erpnext/public/scss/erpnext-web.bundle.scss index 6ef1892a3d..18d7c6cf4e 100644 --- a/erpnext/public/scss/erpnext-web.bundle.scss +++ b/erpnext/public/scss/erpnext-web.bundle.scss @@ -1,2 +1 @@ -@import "./shopping_cart"; @import "./website"; diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss deleted file mode 100644 index 6ae464d2c2..0000000000 --- a/erpnext/public/scss/shopping_cart.scss +++ /dev/null @@ -1,1381 +0,0 @@ -@import "frappe/public/scss/common/mixins"; - -:root { - --green-info: #38A160; - --product-bg-color: white; - --body-bg-color: var(--gray-50); -} - -body.product-page { - background: var(--body-bg-color); -} - -.item-breadcrumbs { - .breadcrumb-container { - a { - color: var(--gray-900); - } - } -} - -.carousel-control { - height: 42px; - width: 42px; - display: flex; - align-items: center; - justify-content: center; - background: white; - box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.08), 0px 1px 2px 1px rgba(0, 0, 0, 0.06); - border-radius: 100px; -} - -.carousel-control-prev, -.carousel-control-next { - opacity: 1; - width: 8%; - - @media (max-width: 1200px) { - width: 10%; - } - @media (max-width: 768px) { - width: 15%; - } -} - -.carousel-body { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; -} - -.carousel-content { - max-width: 400px; - margin-left: 5rem; - margin-right: 5rem; -} - -.card { - border: none; -} - -.product-category-section { - .card:hover { - box-shadow: 0px 16px 45px 6px rgba(0, 0, 0, 0.08), 0px 8px 10px -10px rgba(0, 0, 0, 0.04); - } - - .card-grid { - display: grid; - grid-gap: 15px; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - } -} - -.no-image-item { - height: 340px; - width: 340px; - background: var(--gray-100); - border-radius: var(--border-radius); - font-size: 2rem; - color: var(--gray-500); - display: flex; - align-items: center; - justify-content: center; -} - -.item-card-group-section { - .card { - height: 100%; - align-items: center; - justify-content: center; - - &:hover { - box-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04); - transition: box-shadow 400ms; - } - } - - .card:hover, .card:focus-within { - .btn-add-to-cart-list { - visibility: visible; - } - .like-action { - visibility: visible; - } - .btn-explore-variants { - visibility: visible; - } - } - - - .card-img-container { - height: 210px; - width: 100%; - } - - .card-img { - max-height: 210px; - object-fit: contain; - margin-top: 1.25rem; - } - - .no-image { - @include flex(flex, center, center, null); - height: 220px; - background: var(--gray-100); - width: 100%; - border-radius: var(--border-radius) var(--border-radius) 0 0; - font-size: 2rem; - color: var(--gray-500); - } - - .no-image-list { - @include flex(flex, center, center, null); - height: 150px; - background: var(--gray-100); - border-radius: var(--border-radius); - font-size: 2rem; - color: var(--gray-500); - margin-top: 15px; - margin-bottom: 15px; - } - - .card-body-flex { - display: flex; - flex-direction: column; - } - - .product-title { - font-size: 14px; - color: var(--gray-800); - font-weight: 500; - } - - .product-description { - font-size: 12px; - color: var(--text-color); - margin: 20px 0; - display: -webkit-box; - -webkit-line-clamp: 6; - -webkit-box-orient: vertical; - - p { - margin-bottom: 0.5rem; - } - } - - .product-category { - font-size: 13px; - color: var(--text-muted); - margin: var(--margin-sm) 0; - } - - .product-price { - font-size: 18px; - font-weight: 600; - color: var(--text-color); - margin: var(--margin-sm) 0; - margin-bottom: auto !important; - - .striked-price { - font-weight: 500; - font-size: 15px; - color: var(--gray-500); - } - } - - .product-info-green { - color: var(--green-info); - font-weight: 600; - } - - .item-card { - padding: var(--padding-sm); - min-width: 300px; - } - - .wishlist-card { - padding: var(--padding-sm); - min-width: 260px; - .card-body-flex { - display: flex; - flex-direction: column; - } - } -} - -#products-list-area, #products-grid-area { - padding: 0 5px; -} - -.list-row { - background-color: white; - padding-bottom: 1rem; - padding-top: 1.5rem !important; - border-radius: 8px; - border-bottom: 1px solid var(--gray-50); - - &:hover, &:focus-within { - box-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04); - transition: box-shadow 400ms; - - .btn-add-to-cart-list { - visibility: visible; - } - .like-action-list { - visibility: visible; - } - .btn-explore-variants { - visibility: visible; - } - } - - .product-code { - padding-top: 0 !important; - } - - .btn-explore-variants { - min-width: 135px; - max-height: 30px; - float: right; - padding: 0.25rem 1rem; - } -} - -[data-doctype="Item Group"], -#page-index { - .page-header { - font-size: 20px; - font-weight: 700; - color: var(--text-color); - } - - .filters-section { - .title-section { - border-bottom: 1px solid var(--table-border-color); - } - - .filter-title { - font-weight: 500; - } - - .clear-filters { - font-size: 13px; - } - - .filter-lookup-input { - background-color: white; - border: 1px solid var(--gray-300); - - &:focus { - border: 1px solid var(--primary); - } - } - - .filter-label { - font-size: 11px; - font-weight: 600; - color: var(--gray-700); - text-transform: uppercase; - } - - .filter-block { - border-bottom: 1px solid var(--table-border-color); - } - - .checkbox { - .label-area { - font-size: 13px; - color: var(--gray-800); - } - } - } -} - -.product-filter { - width: 14px !important; - height: 14px !important; -} - -.discount-filter { - &:before { - width: 14px !important; - height: 14px !important; - } -} - -.list-image { - border: none !important; - overflow: hidden; - max-height: 200px; - background-color: white; -} - -.product-container { - @include card($padding: var(--padding-md)); - background-color: var(--product-bg-color) !important; - min-height: fit-content; - - .product-details { - max-width: 50%; - - .btn-add-to-cart { - font-size: 14px; - } - } - - &.item-main { - .product-image { - width: 100%; - } - } - - .expand { - max-width: 100% !important; // expand in absence of slideshow - } - - @media (max-width: 789px) { - .product-details { - max-width: 90% !important; - - .btn-add-to-cart { - font-size: 14px; - } - } - } - - .btn-add-to-wishlist { - svg use { - --icon-stroke: #F47A7A; - } - } - - .btn-view-in-wishlist { - svg use { - fill: #F47A7A; - --icon-stroke: none; - } - } - - .product-title { - font-size: 16px; - font-weight: 600; - color: var(--text-color); - padding: 0 !important; - } - - .product-description { - font-size: 13px; - color: var(--gray-800); - } - - .product-image { - border-color: var(--table-border-color) !important; - padding: 15px; - - @media (max-width: var(--md-width)) { - height: 300px; - width: 300px; - } - - @media (min-width: var(--lg-width)) { - height: 350px; - width: 350px; - } - - img { - object-fit: contain; - } - } - - .item-slideshow { - - @media (max-width: var(--md-width)) { - max-height: 320px; - } - - @media (min-width: var(--lg-width)) { - max-height: 430px; - } - - overflow: auto; - } - - .item-slideshow-image { - height: 4rem; - width: 6rem; - object-fit: contain; - padding: 0.5rem; - border: 1px solid var(--table-border-color); - border-radius: 4px; - cursor: pointer; - - &:hover, &.active { - border-color: var(--primary); - } - } - - .item-cart { - .product-price { - font-size: 22px; - color: var(--text-color); - font-weight: 600; - - .formatted-price { - color: var(--text-muted); - font-size: 14px; - } - } - - .no-stock { - font-size: var(--text-base); - } - - .offers-heading { - font-size: 16px !important; - color: var(--text-color); - .tag-icon { - --icon-stroke: var(--gray-500); - } - } - - .w-30-40 { - width: 30%; - - @media (max-width: 992px) { - width: 40%; - } - } - } - - .tab-content { - font-size: 14px; - } -} - -// Item Recommendations -.recommended-item-section { - padding-right: 0; - - .recommendation-header { - font-size: 16px; - font-weight: 500 - } - - .recommendation-container { - padding: .5rem; - min-height: 0px; - - .r-item-image { - min-height: 100px; - width: 40%; - - .r-product-image { - padding: 2px 15px; - } - - .no-image-r-item { - display: flex; justify-content: center; - background-color: var(--gray-200); - align-items: center; - color: var(--gray-400); - margin-top: .15rem; - border-radius: 6px; - height: 100%; - font-size: 24px; - } - } - - .r-item-info { - font-size: 14px; - padding-right: 0; - padding-left: 10px; - width: 60%; - - a { - color: var(--gray-800); - font-weight: 400; - } - - .item-price { - font-size: 15px; - font-weight: 600; - color: var(--text-color); - } - - .striked-item-price { - font-weight: 500; - color: var(--gray-500); - } - } - } -} - -.product-code { - padding: .5rem 0; - color: var(--text-muted); - font-size: 14px; - .product-item-group { - padding-right: .25rem; - border-right: solid 1px var(--text-muted); - } - - .product-item-code { - padding-left: .5rem; - } -} - -.item-configurator-dialog { - .modal-body { - padding-bottom: var(--padding-xl); - - .status-area { - .alert { - padding: var(--padding-xs) var(--padding-sm); - font-size: var(--text-sm); - } - } - - .form-layout { - max-height: 50vh; - overflow-y: auto; - } - - .section-body { - .form-column { - .form-group { - .control-label { - font-size: var(--text-md); - color: var(--gray-700); - } - - .help-box { - margin-top: 2px; - font-size: var(--text-sm); - } - } - } - } - } -} - -.item-group-slideshow { - - .carousel-inner.rounded-carousel { - border-radius: var(--card-border-radius); - } -} - -.sub-category-container { - padding-bottom: .5rem; - margin-bottom: 1.25rem; - border-bottom: 1px solid var(--table-border-color); - - .heading { - color: var(--gray-500); - } -} - -.scroll-categories { - .category-pill { - display: inline-block; - width: fit-content; - padding: 6px 12px; - margin-bottom: 8px; - background-color: #ecf5fe; - font-size: 14px; - border-radius: 18px; - color: var(--blue-500); - } -} - - -.shopping-badge { - position: relative; - top: -10px; - left: -12px; - background: var(--red-600); - align-items: center; - height: 16px; - font-size: 10px; - border-radius: 50%; -} - - -.cart-animate { - animation: wiggle 0.5s linear; -} -@keyframes wiggle { - 8%, - 41% { - transform: translateX(-10px); - } - 25%, - 58% { - transform: translate(10px); - } - 75% { - transform: translate(-5px); - } - 92% { - transform: translate(5px); - } - 0%, - 100% { - transform: translate(0); - } -} - -.total-discount { - font-size: 14px; - color: var(--primary-color) !important; -} - -#page-cart { - .shopping-cart-header { - font-weight: bold; - } - - .cart-container { - color: var(--text-color); - - .frappe-card { - display: flex; - flex-direction: column; - justify-content: space-between; - height: fit-content; - } - - .cart-items-header { - font-weight: 600; - } - - .cart-table { - tr { - margin-bottom: 1rem; - } - - th, tr, td { - border-color: var(--border-color); - border-width: 1px; - } - - th { - font-weight: normal; - font-size: 13px; - color: var(--text-muted); - padding: var(--padding-sm) 0; - } - - td { - padding: var(--padding-sm) 0; - color: var(--text-color); - } - - .cart-item-image { - width: 20%; - min-width: 100px; - img { - max-height: 112px; - } - } - - .cart-items { - .item-title { - width: 80%; - font-size: 14px; - font-weight: 500; - color: var(--text-color); - } - - .item-subtitle { - color: var(--text-muted); - font-size: 13px; - } - - .item-subtotal { - font-size: 14px; - font-weight: 500; - } - - .sm-item-subtotal { - font-size: 14px; - font-weight: 500; - display: none; - - @media (max-width: 992px) { - display: unset !important; - } - } - - .item-rate { - font-size: 13px; - color: var(--text-muted); - } - - .free-tag { - padding: 4px 8px; - border-radius: 4px; - background-color: var(--dark-green-50); - } - - textarea { - width: 80%; - height: 60px; - font-size: 14px; - } - - } - - .cart-tax-items { - .item-grand-total { - font-size: 16px; - font-weight: 700; - color: var(--text-color); - } - } - - .column-sm-view { - @media (max-width: 992px) { - display: none !important; - } - } - - .item-column { - width: 50%; - @media (max-width: 992px) { - width: 70%; - } - } - - .remove-cart-item { - border-radius: 6px; - border: 1px solid var(--gray-100); - width: 28px; - height: 28px; - font-weight: 300; - color: var(--gray-700); - background-color: var(--gray-100); - float: right; - cursor: pointer; - margin-top: .25rem; - justify-content: center; - } - - .remove-cart-item-logo { - margin-top: 2px; - margin-left: 2.2px; - fill: var(--gray-700) !important; - } - } - - .cart-payment-addresses { - hr { - border-color: var(--border-color); - } - } - - .payment-summary { - h6 { - padding-bottom: 1rem; - border-bottom: solid 1px var(--gray-200); - } - - table { - font-size: 14px; - td { - padding: 0; - padding-top: 0.35rem !important; - border: none !important; - } - - &.grand-total { - border-top: solid 1px var(--gray-200); - } - } - - .bill-label { - color: var(--gray-600); - } - - .bill-content { - font-weight: 500; - &.net-total { - font-size: 16px; - font-weight: 600; - } - } - - .btn-coupon-code { - font-size: 14px; - border: dashed 1px var(--gray-400); - box-shadow: none; - } - } - - .number-spinner { - width: 75%; - min-width: 105px; - .cart-btn { - border: none; - background: var(--gray-100); - box-shadow: none; - width: 24px; - height: 28px; - align-items: center; - justify-content: center; - display: flex; - font-size: 20px; - font-weight: 300; - color: var(--gray-700); - } - - .cart-qty { - height: 28px; - font-size: 13px; - &:disabled { - background: var(--gray-100); - opacity: 0.65; - } - } - } - - .place-order-container { - .btn-place-order { - float: right; - } - } - } - - .t-and-c-container { - padding: 1.5rem; - } - - .t-and-c-terms { - font-size: 14px; - } -} - -.no-image-cart-item { - max-height: 112px; - display: flex; justify-content: center; - background-color: var(--gray-200); - align-items: center; - color: var(--gray-400); - margin-top: .15rem; - border-radius: 6px; - height: 100%; - font-size: 24px; -} - -.cart-empty.frappe-card { - min-height: 76vh; - @include flex(flex, center, center, column); - - .cart-empty-message { - font-size: 18px; - color: var(--text-color); - font-weight: bold; - } -} - -.address-card { - .card-title { - font-size: 14px; - font-weight: 500; - } - - .card-body { - max-width: 80%; - } - - .card-text { - font-size: 13px; - color: var(--gray-700); - } - - .card-link { - font-size: 13px; - - svg use { - stroke: var(--primary-color); - } - } - - .btn-change-address { - border: 1px solid var(--primary-color); - color: var(--primary-color); - box-shadow: none; - } -} - -.address-header { - margin-top: .15rem;padding: 0; -} - -.btn-new-address { - float: right; - font-size: 15px !important; - color: var(--primary-color) !important; -} - -.btn-new-address:hover, .btn-change-address:hover { - color: var(--primary-color) !important; -} - -.modal .address-card { - .card-body { - padding: var(--padding-sm); - border-radius: var(--border-radius); - border: 1px solid var(--dark-border-color); - } -} - -.cart-indicator { - position: absolute; - text-align: center; - width: 22px; - height: 22px; - left: calc(100% - 40px); - top: 22px; - - border-radius: 66px; - box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1); - background: white; - color: var(--primary-color); - font-size: 14px; - - &.list-indicator { - position: unset; - margin-left: auto; - } -} - - -.like-action { - visibility: hidden; - text-align: center; - position: absolute; - cursor: pointer; - width: 28px; - height: 28px; - left: 20px; - top: 20px; - - /* White */ - background: white; - box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1); - border-radius: 66px; - - &.like-action-wished { - visibility: visible !important; - } - - @media (max-width: 992px) { - visibility: visible !important; - } -} - -.like-action-list { - visibility: hidden; - text-align: center; - position: absolute; - cursor: pointer; - width: 28px; - height: 28px; - left: 20px; - top: 0; - - /* White */ - background: white; - box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1); - border-radius: 66px; - - &.like-action-wished { - visibility: visible !important; - } - - @media (max-width: 992px) { - visibility: visible !important; - } -} - -.like-action-item-fp { - visibility: visible !important; - position: unset; - float: right; -} - -.like-animate { - animation: expand cubic-bezier(0.04, 0.4, 0.5, 0.95) 1.6s forwards 1; -} - -@keyframes expand { - 30% { - transform: scale(1.3); - } - 50% { - transform: scale(0.8); - } - 70% { - transform: scale(1.1); - } - 100% { - transform: scale(1); - } - } - -.not-wished { - cursor: pointer; - --icon-stroke: #F47A7A !important; - - &:hover { - fill: #F47A7A; - } -} - -.wished { - --icon-stroke: none; - fill: #F47A7A !important; -} - -.list-row-checkbox { - &:before { - display: none; - } - - &:checked:before { - display: block; - z-index: 1; - } -} - -#pay-for-order { - padding: .5rem 1rem; // Pay button in SO -} - -.btn-explore-variants { - visibility: hidden; - box-shadow: none; - margin: var(--margin-sm) 0; - width: 90px; - max-height: 50px; // to avoid resizing on window resize - flex: none; - transition: 0.3s ease; - - color: white; - background-color: var(--orange-500); - border: 1px solid var(--orange-500); - font-size: 13px; - - &:hover { - color: white; - } -} - -.btn-add-to-cart-list{ - visibility: hidden; - box-shadow: none; - margin: var(--margin-sm) 0; - // margin-top: auto !important; - max-height: 50px; // to avoid resizing on window resize - flex: none; - transition: 0.3s ease; - - font-size: 13px; - - &:hover { - color: white; - } - - @media (max-width: 992px) { - visibility: visible !important; - } -} - -.go-to-cart-grid { - max-height: 30px; - margin-top: 1rem !important; -} - -.go-to-cart { - max-height: 30px; - float: right; -} - -.remove-wish { - background-color: white; - position: absolute; - cursor: pointer; - top:10px; - right: 20px; - width: 32px; - height: 32px; - - border-radius: 50%; - border: 1px solid var(--gray-100); - box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1); -} - -.wish-removed { - display: none; -} - -.item-website-specification { - font-size: .875rem; - .product-title { - font-size: 18px; - } - - .table { - width: 70%; - } - - td { - border: none !important; - } - - .spec-label { - color: var(--gray-600); - } - - .spec-content { - color: var(--gray-800); - } -} - -.reviews-full-page { - padding: 1rem 2rem; -} - -.ratings-reviews-section { - border-top: 1px solid #E2E6E9; - padding: .5rem 1rem; -} - -.reviews-header { - font-size: 20px; - font-weight: 600; - color: var(--gray-800); - display: flex; - align-items: center; - padding: 0; -} - -.btn-write-review { - float: right; - padding: .5rem 1rem; - font-size: 14px; - font-weight: 400; - border: none !important; - box-shadow: none; - - color: var(--gray-900); - background-color: var(--gray-100); - - &:hover { - box-shadow: var(--btn-shadow); - } -} - -.btn-view-more { - font-size: 14px; -} - -.rating-summary-section { - display: flex; -} - -.rating-summary-title { - margin-top: 0.15rem; - font-size: 18px; -} - -.rating-summary-numbers { - display: flex; - flex-direction: column; - align-items: center; - - border-right: solid 1px var(--gray-100); -} - -.user-review-title { - margin-top: 0.15rem; - font-size: 15px; - font-weight: 600; -} - -.rating { - --star-fill: var(--gray-300); - .star-hover { - --star-fill: var(--yellow-100); - } - .star-click { - --star-fill: var(--yellow-300); - } -} - -.ratings-pill { - background-color: var(--gray-100); - padding: .5rem 1rem; - border-radius: 66px; -} - -.review { - max-width: 80%; - line-height: 1.6; - padding-bottom: 0.5rem; - border-bottom: 1px solid #E2E6E9; -} - -.review-signature { - display: flex; - font-size: 13px; - color: var(--gray-500); - font-weight: 400; - - .reviewer { - padding-right: 8px; - color: var(--gray-600); - } -} - -.rating-progress-bar-section { - padding-bottom: 2rem; - - .rating-bar-title { - margin-left: -15px; - } - - .rating-progress-bar { - margin-bottom: 4px; - height: 7px; - margin-top: 6px; - - .progress-bar-cosmetic { - background-color: var(--gray-600); - border-radius: var(--border-radius); - } - } -} - -.offer-container { - font-size: 14px; -} - -#search-results-container { - border: 1px solid var(--gray-200); - padding: .25rem 1rem; - - .category-chip { - background-color: var(--gray-100); - border: none !important; - box-shadow: none; - } - - .recent-search { - padding: .5rem .5rem; - border-radius: var(--border-radius); - - &:hover { - background-color: var(--gray-100); - } - } -} - -#search-box { - background-color: white; - height: 100%; - padding-left: 2.5rem; - border: 1px solid var(--gray-200); -} - -.search-icon { - position: absolute; - left: 0; - top: 0; - width: 2.5rem; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - padding-bottom: 1px; -} - -#toggle-view { - float: right; - - .btn-primary { - background-color: var(--gray-600); - box-shadow: 0 0 0 0.2rem var(--gray-400); - } -} - -.placeholder-div { - height:80%; - width: -webkit-fill-available; - padding: 50px; - text-align: center; - background-color: #F9FAFA; - border-top-left-radius: calc(0.75rem - 1px); - border-top-right-radius: calc(0.75rem - 1px); -} -.placeholder { - font-size: 72px; -} - -[data-path="cart"] { - .modal-backdrop { - background-color: var(--gray-50); // lighter backdrop only on cart freeze - } -} - -.item-thumb { - height: 50px; - max-width: 80px; - min-width: 80px; - object-fit: cover; -} - -.brand-line { - color: gray; -} - -.btn-next, .btn-prev { - font-size: 14px; -} - -.alert-error { - color: #e27a84; - background-color: #fff6f7; - border-color: #f5c6cb; -} - -.font-md { - font-size: 14px !important; -} - -.in-green { - color: var(--green-info) !important; - font-weight: 500; -} - -.has-stock { - font-weight: 400 !important; -} - -.out-of-stock { - font-weight: 400; - font-size: 14px; - line-height: 20px; - color: #F47A7A; -} - -.mt-minus-2 { - margin-top: -2rem; -} - -.mt-minus-1 { - margin-top: -1rem; -} \ No newline at end of file diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 8ff681b048..95d2d2c577 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -26,7 +26,6 @@ class Quotation(SellingController): self.set_status() self.validate_uom_is_integer("stock_uom", "qty") self.validate_valid_till() - self.validate_shopping_cart_items() self.set_customer_name() if self.items: self.with_items = 1 @@ -42,26 +41,6 @@ class Quotation(SellingController): if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date): frappe.throw(_("Valid till date cannot be before transaction date")) - def validate_shopping_cart_items(self): - if self.order_type != "Shopping Cart": - return - - for item in self.items: - has_web_item = frappe.db.exists("Website Item", {"item_code": item.item_code}) - - # If variant is unpublished but template is published: valid - template = frappe.get_cached_value("Item", item.item_code, "variant_of") - if template and not has_web_item: - has_web_item = frappe.db.exists("Website Item", {"item_code": template}) - - if not has_web_item: - frappe.throw( - _("Row #{0}: Item {1} must have a Website Item for Shopping Cart Quotations").format( - item.idx, frappe.bold(item.item_code) - ), - title=_("Unpublished Item"), - ) - def set_has_alternative_item(self): """Mark 'Has Alternative Item' for rows.""" if not any(row.is_alternative for row in self.get("items")): @@ -263,8 +242,8 @@ def make_sales_order(source_name: str, target_doc=None): return _make_sales_order(source_name, target_doc) -def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): - customer = _make_customer(source_name, ignore_permissions) +def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_permissions=False): + customer = _make_customer(source_name, ignore_permissions, customer_group) ordered_items = frappe._dict( frappe.db.get_all( "Sales Order Item", @@ -428,7 +407,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): return doclist -def _make_customer(source_name, ignore_permissions=False): +def _make_customer(source_name, ignore_permissions=False, customer_group=None): quotation = frappe.db.get_value( "Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1 ) @@ -445,10 +424,7 @@ def _make_customer(source_name, ignore_permissions=False): customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions) customer = frappe.get_doc(customer_doclist) customer.flags.ignore_permissions = ignore_permissions - if quotation.get("party_name") == "Shopping Cart": - customer.customer_group = frappe.db.get_value( - "E Commerce Settings", None, "default_customer_group" - ) + customer.customer_group = customer_group try: customer.insert() diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 5623a12cdd..590cd3d0cf 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -161,15 +161,6 @@ class TestQuotation(FrappeTestCase): make_sales_order(quotation.name) - def test_shopping_cart_without_website_item(self): - if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}): - frappe.get_last_doc("Website Item", {"item_code": "_Test Item Home Desktop 100"}).delete() - - quotation = frappe.copy_doc(test_records[0]) - quotation.order_type = "Shopping Cart" - quotation.valid_till = getdate() - self.assertRaises(frappe.ValidationError, quotation.validate) - def test_create_quotation_with_margin(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order from erpnext.selling.doctype.sales_order.sales_order import ( diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js index 4b04ac1d5e..d6eb11f73d 100644 --- a/erpnext/setup/doctype/item_group/item_group.js +++ b/erpnext/setup/doctype/item_group/item_group.js @@ -71,20 +71,6 @@ frappe.ui.form.on("Item Group", { frappe.set_route("List", "Item", {"item_group": frm.doc.name}); }); } - - frappe.model.with_doctype('Website Item', () => { - const web_item_meta = frappe.get_meta('Website Item'); - - const valid_fields = web_item_meta.fields.filter(df => - ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden - ).map(df => - ({ label: df.label, value: df.fieldname }) - ); - - frm.get_field("filter_fields").grid.update_docfield_property( - 'fieldname', 'options', valid_fields - ); - }); }, set_root_readonly: function(frm) { diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index e0f5090474..dfa5a8ed0a 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -19,22 +19,9 @@ "item_group_defaults", "sec_break_taxes", "taxes", - "sb9", - "route", - "website_title", - "description", - "show_in_website", - "include_descendants", - "column_break_16", - "weightage", - "slideshow", - "website_specifications", - "website_filters_section", - "filter_fields", - "filter_attributes", "lft", - "rgt", - "old_parent" + "old_parent", + "rgt" ], "fields": [ { @@ -106,54 +93,6 @@ "label": "Taxes", "options": "Item Tax" }, - { - "fieldname": "sb9", - "fieldtype": "Section Break", - "label": "Website Settings" - }, - { - "default": "0", - "description": "Make Item Group visible in website", - "fieldname": "show_in_website", - "fieldtype": "Check", - "label": "Show in Website" - }, - { - "depends_on": "show_in_website", - "fieldname": "route", - "fieldtype": "Data", - "label": "Route", - "no_copy": 1, - "unique": 1 - }, - { - "depends_on": "show_in_website", - "fieldname": "weightage", - "fieldtype": "Int", - "label": "Weightage" - }, - { - "depends_on": "show_in_website", - "description": "Show this slideshow at the top of the page", - "fieldname": "slideshow", - "fieldtype": "Link", - "label": "Slideshow", - "options": "Website Slideshow" - }, - { - "depends_on": "show_in_website", - "description": "HTML / Banner that will show on the top of product list.", - "fieldname": "description", - "fieldtype": "Text Editor", - "label": "Description" - }, - { - "depends_on": "show_in_website", - "fieldname": "website_specifications", - "fieldtype": "Table", - "label": "Website Specifications", - "options": "Item Website Specification" - }, { "fieldname": "lft", "fieldtype": "Int", @@ -188,43 +127,6 @@ "options": "Item Group", "print_hide": 1, "report_hide": 1 - }, - { - "collapsible": 1, - "depends_on": "show_in_website", - "fieldname": "website_filters_section", - "fieldtype": "Section Break", - "label": "Website Filters" - }, - { - "fieldname": "filter_fields", - "fieldtype": "Table", - "label": "Item Fields", - "options": "Website Filter Field" - }, - { - "fieldname": "filter_attributes", - "fieldtype": "Table", - "label": "Attributes", - "options": "Website Attribute" - }, - { - "depends_on": "show_in_website", - "fieldname": "website_title", - "fieldtype": "Data", - "label": "Title" - }, - { - "fieldname": "column_break_16", - "fieldtype": "Column Break" - }, - { - "default": "0", - "depends_on": "show_in_website", - "description": "Include Website Items belonging to child Item Groups", - "fieldname": "include_descendants", - "fieldtype": "Check", - "label": "Include Descendants" } ], "icon": "fa fa-sitemap", @@ -233,7 +135,7 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2023-08-28 22:27:48.382985", + "modified": "2023-10-12 13:44:13.611287", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index cc67c696b4..fe7a241dc4 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -2,39 +2,19 @@ # License: GNU General Public License v3. See license.txt import copy -from urllib.parse import quote import frappe from frappe import _ -from frappe.utils import cint from frappe.utils.nestedset import NestedSet -from frappe.website.utils import clear_cache -from frappe.website.website_generator import WebsiteGenerator - -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ECommerceSettings -from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder -class ItemGroup(NestedSet, WebsiteGenerator): - nsm_parent_field = "parent_item_group" - website = frappe._dict( - condition_field="show_in_website", - template="templates/generators/item_group.html", - no_cache=1, - no_breadcrumbs=1, - ) - +class ItemGroup(NestedSet): def validate(self): - super(ItemGroup, self).validate() - if not self.parent_item_group and not frappe.flags.in_test: if frappe.db.exists("Item Group", _("All Item Groups")): self.parent_item_group = _("All Item Groups") - - self.make_route() self.validate_item_group_defaults() self.check_item_tax() - ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True) def check_item_tax(self): """Check whether Tax Rate is not entered twice for same Tax Type""" @@ -53,66 +33,13 @@ class ItemGroup(NestedSet, WebsiteGenerator): def on_update(self): NestedSet.on_update(self) - invalidate_cache_for(self) self.validate_one_root() self.delete_child_item_groups_key() - def make_route(self): - """Make website route""" - if not self.route: - self.route = "" - if self.parent_item_group: - parent_item_group = frappe.get_doc("Item Group", self.parent_item_group) - - # make parent route only if not root - if parent_item_group.parent_item_group and parent_item_group.route: - self.route = parent_item_group.route + "/" - - self.route += self.scrub(self.item_group_name) - - return self.route - def on_trash(self): NestedSet.on_trash(self, allow_root_deletion=True) - WebsiteGenerator.on_trash(self) self.delete_child_item_groups_key() - def get_context(self, context): - context.show_search = True - context.body_class = "product-page" - context.page_length = ( - cint(frappe.db.get_single_value("E Commerce Settings", "products_per_page")) or 6 - ) - context.search_link = "/product_search" - - filter_engine = ProductFiltersBuilder(self.name) - - context.field_filters = filter_engine.get_field_filters() - context.attribute_filters = filter_engine.get_attribute_filters() - - context.update({"parents": get_parent_item_groups(self.parent_item_group), "title": self.name}) - - if self.slideshow: - values = {"show_indicators": 1, "show_controls": 0, "rounded": 1, "slider_name": self.slideshow} - slideshow = frappe.get_doc("Website Slideshow", self.slideshow) - slides = slideshow.get({"doctype": "Website Slideshow Item"}) - for index, slide in enumerate(slides): - values[f"slide_{index + 1}_image"] = slide.image - values[f"slide_{index + 1}_title"] = slide.heading - values[f"slide_{index + 1}_subtitle"] = slide.description - values[f"slide_{index + 1}_theme"] = slide.get("theme") or "Light" - values[f"slide_{index + 1}_content_align"] = slide.get("content_align") or "Centre" - values[f"slide_{index + 1}_primary_action"] = slide.url - - context.slideshow = values - - context.no_breadcrumbs = False - context.title = self.website_title or self.name - context.name = self.name - context.item_group_name = self.item_group_name - - return context - def delete_child_item_groups_key(self): frappe.cache().hdel("child_item_groups", self.name) @@ -122,20 +49,6 @@ class ItemGroup(NestedSet, WebsiteGenerator): validate_item_default_company_links(self.item_group_defaults) -def get_child_groups_for_website(item_group_name, immediate=False, include_self=False): - """Returns child item groups *excluding* passed group.""" - item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - filters = {"lft": [">", item_group.lft], "rgt": ["<", item_group.rgt], "show_in_website": 1} - - if immediate: - filters["parent_item_group"] = item_group_name - - if include_self: - filters.update({"lft": [">=", item_group.lft], "rgt": ["<=", item_group.rgt]}) - - return frappe.get_all("Item Group", filters=filters, fields=["name", "route"], order_by="name") - - def get_child_item_groups(item_group_name): item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) @@ -149,63 +62,6 @@ def get_child_item_groups(item_group_name): return child_item_groups or {} -def get_item_for_list_in_html(context): - # add missing absolute link in files - # user may forget it during upload - if (context.get("website_image") or "").startswith("files/"): - context["website_image"] = "/" + quote(context["website_image"]) - - products_template = "templates/includes/products_as_list.html" - - return frappe.get_template(products_template).render(context) - - -def get_parent_item_groups(item_group_name, from_item=False): - settings = frappe.get_cached_doc("E Commerce Settings") - - if settings.enable_field_filters: - base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"} - else: - base_nav_page = {"name": _("All Products"), "route": "/all-products"} - - if from_item and frappe.request.environ.get("HTTP_REFERER"): - # base page after 'Home' will vary on Item page - last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0] - if last_page and last_page in ("shop-by-category", "all-products"): - base_nav_page_title = " ".join(last_page.split("-")).title() - base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} - - base_parents = [ - {"name": _("Home"), "route": "/"}, - base_nav_page, - ] - - if not item_group_name: - return base_parents - - item_group = frappe.db.get_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - parent_groups = frappe.db.sql( - """select name, route from `tabItem Group` - where lft <= %s and rgt >= %s - and show_in_website=1 - order by lft asc""", - (item_group.lft, item_group.rgt), - as_dict=True, - ) - - return base_parents + parent_groups - - -def invalidate_cache_for(doc, item_group=None): - if not item_group: - item_group = doc.name - - for d in get_parent_item_groups(item_group): - item_group_name = frappe.db.get_value("Item Group", d.get("name")) - if item_group_name: - clear_cache(frappe.db.get_value("Item Group", item_group_name, "route")) - - def get_item_group_defaults(item, company): item = frappe.get_cached_doc("Item", item) item_group = frappe.get_cached_doc("Item Group", item.item_group) diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py index ace5cca0b0..d4aac5ee46 100644 --- a/erpnext/setup/setup_wizard/operations/company_setup.py +++ b/erpnext/setup/setup_wizard/operations/company_setup.py @@ -33,20 +33,6 @@ def create_fiscal_year_and_company(args): ).insert() -def enable_shopping_cart(args): # nosemgrep - # Needs price_lists - frappe.get_doc( - { - "doctype": "E Commerce Settings", - "enabled": 1, - "company": args.get("company_name"), - "price_list": frappe.db.get_value("Price List", {"selling": 1}), - "default_customer_group": _("Individual"), - "quotation_series": "QTN-", - } - ).insert() - - def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index ae6881b99e..2205924e50 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -454,7 +454,6 @@ def install_defaults(args=None): # nosemgrep set_global_defaults(args) update_stock_settings() - update_shopping_cart_settings(args) args.update({"set_default": 1}) create_bank_account(args) @@ -529,20 +528,6 @@ def create_bank_account(args): pass -def update_shopping_cart_settings(args): # nosemgrep - shopping_cart = frappe.get_doc("E Commerce Settings") - shopping_cart.update( - { - "enabled": 1, - "company": args.company_name, - "price_list": frappe.db.get_value("Price List", {"selling": 1}), - "default_customer_group": _("Individual"), - "quotation_series": "QTN-", - } - ) - shopping_cart.update_single(shopping_cart.get_valid_dict()) - - def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json index 5806fd1f78..2f9cec40b0 100644 --- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json +++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json @@ -1,500 +1,500 @@ { - "charts": [], - "content": "[{\"id\":\"NO5yYHJopc\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"col\":12}},{\"id\":\"CDxIM-WuZ9\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"id\":\"-Uh7DKJNJX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":3}},{\"id\":\"K9ST9xcDXh\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":3}},{\"id\":\"27IdVHVQMb\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":3}},{\"id\":\"Rwp5zff88b\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":3}},{\"id\":\"hkfnQ2sevf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Global Defaults\",\"col\":3}},{\"id\":\"jjxI_PDawD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"id\":\"R3CoYYFXye\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yynbm1J_VO\",\"type\":\"header\",\"data\":{\"text\":\"Settings\",\"col\":12}},{\"id\":\"KDCv2MvSg3\",\"type\":\"card\",\"data\":{\"card_name\":\"Module Settings\",\"col\":4}},{\"id\":\"Q0_bqT7cxQ\",\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"id\":\"UnqK5haBnh\",\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"id\":\"kp7u1H5hCd\",\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"id\":\"Ufc3jycgy9\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"89bSNzv3Yh\",\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]", - "creation": "2022-01-27 13:14:47.349433", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "setting", - "idx": 0, - "is_hidden": 0, - "label": "ERPNext Settings", - "links": [ - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Import Data", - "link_count": 0, - "link_to": "Data Import", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Export Data", - "link_count": 0, - "link_to": "Data Export", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Bulk Update", - "link_count": 0, - "link_to": "Bulk Update", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Download Backups", - "link_count": 0, - "link_to": "backups", - "link_type": "Page", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Deleted Documents", - "link_count": 0, - "link_to": "Deleted Document", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Email / Notifications", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Email Account", - "link_count": 0, - "link_to": "Email Account", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Email Domain", - "link_count": 0, - "link_to": "Email Domain", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Notification", - "link_count": 0, - "link_to": "Notification", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Email Template", - "link_count": 0, - "link_to": "Email Template", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Auto Email Report", - "link_count": 0, - "link_to": "Auto Email Report", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Newsletter", - "link_count": 0, - "link_to": "Newsletter", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Notification Settings", - "link_count": 0, - "link_to": "Notification Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Website", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Website Settings", - "link_count": 0, - "link_to": "Website Settings", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Website Theme", - "link_count": 0, - "link_to": "Website Theme", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Website Script", - "link_count": 0, - "link_to": "Website Script", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "About Us Settings", - "link_count": 0, - "link_to": "About Us Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Contact Us Settings", - "link_count": 0, - "link_to": "Contact Us Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Printing", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Print Format Builder", - "link_count": 0, - "link_to": "print-format-builder", - "link_type": "Page", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Print Settings", - "link_count": 0, - "link_to": "Print Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Print Format", - "link_count": 0, - "link_to": "Print Format", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Print Style", - "link_count": 0, - "link_to": "Print Style", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Workflow", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Workflow", - "link_count": 0, - "link_to": "Workflow", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Workflow State", - "link_count": 0, - "link_to": "Workflow State", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Workflow Action", - "link_count": 0, - "link_to": "Workflow Action", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Core", - "link_count": 3, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "System Settings", - "link_count": 0, - "link_to": "System Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Domain Settings", - "link_count": 0, - "link_to": "Domain Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Global Defaults", - "link_count": 0, - "link_to": "Global Defaults", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Module Settings", - "link_count": 8, - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Accounts Settings", - "link_count": 0, - "link_to": "Accounts Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Stock Settings", - "link_count": 0, - "link_to": "Stock Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Selling Settings", - "link_count": 0, - "link_to": "Selling Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Buying Settings", - "link_count": 0, - "link_to": "Buying Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Manufacturing Settings", - "link_count": 0, - "link_to": "Manufacturing Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "CRM Settings", - "link_count": 0, - "link_to": "CRM Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Projects Settings", - "link_count": 0, - "link_to": "Projects Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Support Settings", - "link_count": 0, - "link_to": "Support Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2023-05-24 14:47:25.356531", - "modified_by": "Administrator", - "module": "Setup", - "name": "ERPNext Settings", - "number_cards": [], - "owner": "Administrator", - "parent_page": "", - "public": 1, - "quick_lists": [], - "restrict_to_domain": "", - "roles": [], - "sequence_id": 19.0, - "shortcuts": [ - { - "color": "Grey", - "doc_view": "List", - "label": "Print Settings", - "link_to": "Print Settings", - "type": "DocType" - }, - { - "color": "Grey", - "doc_view": "List", - "label": "System Settings", - "link_to": "System Settings", - "type": "DocType" - }, - { - "icon": "accounting", - "label": "Accounts Settings", - "link_to": "Accounts Settings", - "type": "DocType" - }, - { - "color": "Grey", - "doc_view": "List", - "label": "Global Defaults", - "link_to": "Global Defaults", - "type": "DocType" - }, - { - "icon": "stock", - "label": "Stock Settings", - "link_to": "Stock Settings", - "type": "DocType" - }, - { - "icon": "sell", - "label": "Selling Settings", - "link_to": "Selling Settings", - "type": "DocType" - }, - { - "icon": "buying", - "label": "Buying Settings", - "link_to": "Buying Settings", - "type": "DocType" - } - ], - "title": "ERPNext Settings" -} \ No newline at end of file + "charts": [], + "content": "[{\"id\":\"NO5yYHJopc\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"col\":12}},{\"id\":\"CDxIM-WuZ9\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"id\":\"-Uh7DKJNJX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":3}},{\"id\":\"K9ST9xcDXh\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":3}},{\"id\":\"27IdVHVQMb\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":3}},{\"id\":\"Rwp5zff88b\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":3}},{\"id\":\"hkfnQ2sevf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Global Defaults\",\"col\":3}},{\"id\":\"jjxI_PDawD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"id\":\"R3CoYYFXye\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yynbm1J_VO\",\"type\":\"header\",\"data\":{\"text\":\"Settings\",\"col\":12}},{\"id\":\"KDCv2MvSg3\",\"type\":\"card\",\"data\":{\"card_name\":\"Module Settings\",\"col\":4}},{\"id\":\"Q0_bqT7cxQ\",\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"id\":\"UnqK5haBnh\",\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"id\":\"kp7u1H5hCd\",\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"id\":\"Ufc3jycgy9\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"89bSNzv3Yh\",\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]", + "creation": "2022-01-27 13:14:47.349433", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "setting", + "idx": 0, + "is_hidden": 0, + "label": "ERPNext Settings", + "links": [ + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Import Data", + "link_count": 0, + "link_to": "Data Import", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Export Data", + "link_count": 0, + "link_to": "Data Export", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Bulk Update", + "link_count": 0, + "link_to": "Bulk Update", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Download Backups", + "link_count": 0, + "link_to": "backups", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Deleted Documents", + "link_count": 0, + "link_to": "Deleted Document", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Email / Notifications", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Account", + "link_count": 0, + "link_to": "Email Account", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Domain", + "link_count": 0, + "link_to": "Email Domain", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Notification", + "link_count": 0, + "link_to": "Notification", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Template", + "link_count": 0, + "link_to": "Email Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Auto Email Report", + "link_count": 0, + "link_to": "Auto Email Report", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Newsletter", + "link_count": 0, + "link_to": "Newsletter", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Notification Settings", + "link_count": 0, + "link_to": "Notification Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Website", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Website Settings", + "link_count": 0, + "link_to": "Website Settings", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Website Theme", + "link_count": 0, + "link_to": "Website Theme", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Website Script", + "link_count": 0, + "link_to": "Website Script", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "About Us Settings", + "link_count": 0, + "link_to": "About Us Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Contact Us Settings", + "link_count": 0, + "link_to": "Contact Us Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Printing", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Format Builder", + "link_count": 0, + "link_to": "print-format-builder", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Settings", + "link_count": 0, + "link_to": "Print Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Format", + "link_count": 0, + "link_to": "Print Format", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Style", + "link_count": 0, + "link_to": "Print Style", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Workflow", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workflow", + "link_count": 0, + "link_to": "Workflow", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workflow State", + "link_count": 0, + "link_to": "Workflow State", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workflow Action", + "link_count": 0, + "link_to": "Workflow Action", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Core", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "System Settings", + "link_count": 0, + "link_to": "System Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Domain Settings", + "link_count": 0, + "link_to": "Domain Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Global Defaults", + "link_count": 0, + "link_to": "Global Defaults", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Settings", + "link_count": 8, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Accounts Settings", + "link_count": 0, + "link_to": "Accounts Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Stock Settings", + "link_count": 0, + "link_to": "Stock Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Selling Settings", + "link_count": 0, + "link_to": "Selling Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Buying Settings", + "link_count": 0, + "link_to": "Buying Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Manufacturing Settings", + "link_count": 0, + "link_to": "Manufacturing Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "CRM Settings", + "link_count": 0, + "link_to": "CRM Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Projects Settings", + "link_count": 0, + "link_to": "Projects Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Support Settings", + "link_count": 0, + "link_to": "Support Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2023-05-24 14:47:25.356531", + "modified_by": "Administrator", + "module": "Setup", + "name": "ERPNext Settings", + "number_cards": [], + "owner": "Administrator", + "parent_page": "", + "public": 1, + "quick_lists": [], + "restrict_to_domain": "", + "roles": [], + "sequence_id": 19.0, + "shortcuts": [ + { + "color": "Grey", + "doc_view": "List", + "label": "Print Settings", + "link_to": "Print Settings", + "type": "DocType" + }, + { + "color": "Grey", + "doc_view": "List", + "label": "System Settings", + "link_to": "System Settings", + "type": "DocType" + }, + { + "icon": "accounting", + "label": "Accounts Settings", + "link_to": "Accounts Settings", + "type": "DocType" + }, + { + "color": "Grey", + "doc_view": "List", + "label": "Global Defaults", + "link_to": "Global Defaults", + "type": "DocType" + }, + { + "icon": "stock", + "label": "Stock Settings", + "link_to": "Stock Settings", + "type": "DocType" + }, + { + "icon": "sell", + "label": "Selling Settings", + "link_to": "Selling Settings", + "type": "DocType" + }, + { + "icon": "buying", + "label": "Buying Settings", + "link_to": "Buying Settings", + "type": "DocType" + } + ], + "title": "ERPNext Settings" + } \ No newline at end of file diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 4ae9bf5b2a..6e810e5987 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -125,36 +125,6 @@ frappe.ui.form.on("Item", { erpnext.toggle_naming_series(); } - if (!frm.doc.published_in_website) { - frm.add_custom_button(__("Publish in Website"), function() { - frappe.call({ - method: "erpnext.e_commerce.doctype.website_item.website_item.make_website_item", - args: {doc: frm.doc}, - freeze: true, - freeze_message: __("Publishing Item ..."), - callback: function(result) { - frappe.msgprint({ - message: __("Website Item {0} has been created.", - [repl('%(item)s', { - item_encoded: encodeURIComponent(result.message[0]), - item: result.message[1] - })] - ), - title: __("Published"), - indicator: "green" - }); - } - }); - }, __('Actions')); - } else { - frm.add_custom_button(__("Website Item"), function() { - frappe.db.get_value("Website Item", {item_code: frm.doc.name}, "name", (d) => { - if (!d.name) frappe.throw(__("Website Item not found")); - frappe.set_route("Form", "Website Item", d.name); - }); - }, __("View")); - } - erpnext.item.edit_prices_button(frm); erpnext.item.toggle_attributes(frm); diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 1bcddfa77e..54491bbee3 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -117,7 +117,6 @@ "customer_code", "default_item_manufacturer", "default_manufacturer_part_no", - "published_in_website", "total_projected_qty" ], "fields": [ @@ -815,14 +814,6 @@ "label": "Default Manufacturer Part No", "read_only": 1 }, - { - "default": "0", - "depends_on": "published_in_website", - "fieldname": "published_in_website", - "fieldtype": "Check", - "label": "Published in Website", - "read_only": 1 - }, { "default": "1", "fieldname": "grant_commission", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index aff958738a..9e281990b5 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -32,7 +32,6 @@ from erpnext.controllers.item_variant import ( make_variant_item_code, validate_item_variant_attributes, ) -from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for from erpnext.stock.doctype.item_default.item_default import ItemDefault @@ -122,10 +121,8 @@ class Item(Document): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") def on_update(self): - invalidate_cache_for_item(self) self.update_variants() self.update_item_price() - self.update_website_item() def validate_description(self): """Clean HTML description if set""" @@ -248,29 +245,6 @@ class Item(Document): if self.stock_uom not in uoms_list: self.append("uoms", {"uom": self.stock_uom, "conversion_factor": 1}) - def update_website_item(self): - """Update Website Item if change in Item impacts it.""" - web_item = frappe.db.exists("Website Item", {"item_code": self.item_code}) - - if web_item: - changed = {} - editable_fields = ["item_name", "item_group", "stock_uom", "brand", "description", "disabled"] - doc_before_save = self.get_doc_before_save() - - for field in editable_fields: - if doc_before_save.get(field) != self.get(field): - if field == "disabled": - changed["published"] = not self.get(field) - else: - changed[field] = self.get(field) - - if not changed: - return - - web_item_doc = frappe.get_doc("Website Item", web_item) - web_item_doc.update(changed) - web_item_doc.save() - def validate_item_tax_net_rate_range(self): for tax in self.get("taxes"): if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate): @@ -454,7 +428,6 @@ class Item(Document): if merge: self.validate_properties_before_merge(new_name) self.validate_duplicate_product_bundles_before_merge(old_name, new_name) - self.validate_duplicate_website_item_before_merge(old_name, new_name) self.delete_old_bins(old_name) def after_rename(self, old_name, new_name, merge): @@ -466,9 +439,6 @@ class Item(Document): title=_("Note"), ) - if self.published_in_website: - invalidate_cache_for_item(self) - frappe.db.set_value("Item", new_name, "item_code", new_name) if merge: @@ -554,27 +524,6 @@ class Item(Document): ) frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) - def validate_duplicate_website_item_before_merge(self, old_name, new_name): - """ - Block merge if both old and new items have website items against them. - This is to avoid duplicate website items after merging. - """ - web_items = frappe.get_all( - "Website Item", - filters={"item_code": ["in", [old_name, new_name]]}, - fields=["item_code", "name"], - ) - - if len(web_items) <= 1: - return - - old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0] - web_item_link = get_link_to_form("Website Item", old_web_item) - old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) - - msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}" - frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError) - def set_last_purchase_rate(self, new_name): last_purchase_rate = get_last_purchase_details(new_name).get("base_net_rate", 0) frappe.db.set_value("Item", new_name, "last_purchase_rate", last_purchase_rate) @@ -1151,32 +1100,6 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): return out -def invalidate_cache_for_item(doc): - """Invalidate Item Group cache and rebuild ItemVariantsCacheManager.""" - invalidate_cache_for(doc, doc.item_group) - - if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group: - invalidate_cache_for(doc, doc.old_item_group) - - invalidate_item_variants_cache_for_website(doc) - - -def invalidate_item_variants_cache_for_website(doc): - """Rebuild ItemVariantsCacheManager via Item or Website Item.""" - from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager - - item_code = None - is_web_item = doc.get("published_in_website") or doc.get("published") - if doc.has_variants and is_web_item: - item_code = doc.item_code - elif doc.variant_of and frappe.db.get_value("Item", doc.variant_of, "published_in_website"): - item_code = doc.variant_of - - if item_code: - item_cache = ItemVariantsCacheManager(item_code) - item_cache.rebuild_cache() - - def check_stock_uom_with_bin(item, stock_uom): if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): return diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py index 34bb4d1225..88ae34f228 100644 --- a/erpnext/stock/doctype/item/item_dashboard.py +++ b/erpnext/stock/doctype/item/item_dashboard.py @@ -32,6 +32,5 @@ def get_data(): {"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]}, {"label": _("Traceability"), "items": ["Serial No", "Batch"]}, {"label": _("Stock Movement"), "items": ["Stock Entry", "Stock Reconciliation"]}, - {"label": _("E-commerce"), "items": ["Website Item"]}, ], } diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index e77d53a367..21c0f18cc3 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -13,9 +13,6 @@ class PriceList(Document): if not cint(self.buying) and not cint(self.selling): throw(_("Price List must be applicable for Buying or Selling")) - if not self.is_new(): - self.check_impact_on_shopping_cart() - def on_update(self): self.set_default_if_missing() self.update_item_price() @@ -37,19 +34,6 @@ class PriceList(Document): (self.currency, cint(self.buying), cint(self.selling), self.name), ) - def check_impact_on_shopping_cart(self): - "Check if Price List currency change impacts E Commerce Cart." - from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( - validate_cart_settings, - ) - - doc_before_save = self.get_doc_before_save() - currency_changed = self.currency != doc_before_save.currency - affects_cart = self.name == frappe.db.get_single_value("E Commerce Settings", "price_list") - - if currency_changed and affects_cart: - validate_cart_settings() - def on_trash(self): self.delete_price_list_details_key() diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html deleted file mode 100644 index 358c1c52e5..0000000000 --- a/erpnext/templates/generators/item/item.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends "templates/web.html" %} -{% from "erpnext/templates/includes/macros.html" import recommended_item_row %} - -{% block title %} {{ title }} {% endblock %} - -{% block breadcrumbs %} -
- {% include "templates/includes/breadcrumbs.html" %} -
-{% endblock %} - -{% block page_content %} -
- {% from "erpnext/templates/includes/macros.html" import product_image %} -
-
- -
- {% include "templates/generators/item/item_image.html" %} - {% include "templates/generators/item/item_details.html" %} -
-
-
-
- - -
- {% set show_recommended_items = recommended_items and shopping_cart.cart_settings.enable_recommendations %} - {% set info_col = 'col-9' if show_recommended_items else 'col-12' %} - - {% set padding_top = 'pt-0' if (show_tabs and tabs) else '' %} - -
-
-
- - {% if show_tabs and tabs %} -
- - {{ web_block("Section with Tabs", values=tabs, add_container=0, - add_top_padding=0, add_bottom_padding=0) - }} -
- {% elif website_specifications %} - {% include "templates/generators/item/item_specifications.html"%} - {% endif %} - - - {{ doc.website_content or '' }} - - - {% if shopping_cart.cart_settings.enable_reviews and not doc.has_variants %} - {% include "templates/generators/item/item_reviews.html"%} - {% endif %} -
-
-
- - - {% if show_recommended_items %} - - {% endif %} - -
-{% endblock %} - -{% block base_scripts %} - - -{{ include_script("frappe-web.bundle.js") }} -{{ include_script("controls.bundle.js") }} -{{ include_script("dialog.bundle.js") }} -{% endblock %} diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html deleted file mode 100644 index 9bd3f7514c..0000000000 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ /dev/null @@ -1,180 +0,0 @@ -{% if shopping_cart and shopping_cart.cart_settings.enabled %} - -{% set cart_settings = shopping_cart.cart_settings %} -{% set product_info = shopping_cart.product_info %} - -
-
- - {% if cart_settings.show_price and product_info.price %} - {% set price_info = product_info.price %} - -
- - - {{ price_info.formatted_price_sales_uom }} - {{ price_info.currency }} - - - - {% if price_info.formatted_mrp %} - - MRP {{ price_info.formatted_mrp }} - - - -{{ price_info.get("formatted_discount_percent") or price_info.get("formatted_discount_rate")}} - - {% endif %} - - - - ({{ price_info.formatted_price }} / {{ product_info.uom }}) - -
- {% else %} - {{ _("UOM") }} : {{ product_info.uom }} - {% endif %} - - {% if cart_settings.show_stock_availability %} -
- {% if product_info.get("on_backorder") %} - - {{ _('Available on backorder') }} - - {% elif product_info.in_stock == 0 %} - - {{ _('Out of stock') }} - - {% elif product_info.in_stock == 1 %} - - {{ _('In stock') }} - {% if product_info.show_stock_qty and product_info.stock_qty %} - ({{ product_info.stock_qty }}) - {% endif %} - - {% endif %} -
- {% endif %} - - - {% if doc.offers %} -
-
- - - - Available Offers -
-
- {% for offer in doc.offers %} -
-
- - - - - - -
-

- {{ _(offer.offer_title) }}: - {{ _(offer.offer_subtitle) if offer.offer_subtitle else '' }} - - {{ _("More") }} - -

-
- {% endfor %} -
- {% endif %} - - -
-
- - {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %} - - - {% endif %} - - - {% if cart_settings.show_contact_us_button %} - {% include "templates/generators/item/item_inquiry.html" %} - {% endif %} -
-
-
-
- - - -{% endif %} diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html deleted file mode 100644 index e97a275fbd..0000000000 --- a/erpnext/templates/generators/item/item_configure.html +++ /dev/null @@ -1,20 +0,0 @@ -{% if shopping_cart and shopping_cart.cart_settings.enabled %} -{% set cart_settings = shopping_cart.cart_settings %} - -
- {% if cart_settings.enable_variants | int %} - - {% endif %} - {% if cart_settings.show_contact_us_button %} - {% include "templates/generators/item/item_inquiry.html" %} - {% endif %} -
- -{% endif %} diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js deleted file mode 100644 index 9beba3fd01..0000000000 --- a/erpnext/templates/generators/item/item_configure.js +++ /dev/null @@ -1,343 +0,0 @@ -class ItemConfigure { - constructor(item_code, item_name) { - this.item_code = item_code; - this.item_name = item_name; - - this.get_attributes_and_values() - .then(attribute_data => { - this.attribute_data = attribute_data; - this.show_configure_dialog(); - }); - } - - show_configure_dialog() { - const fields = this.attribute_data.map(a => { - return { - fieldtype: 'Select', - label: a.attribute, - fieldname: a.attribute, - options: a.values.map(v => { - return { - label: v, - value: v - }; - }), - change: (e) => { - this.on_attribute_selection(e); - } - }; - }); - - this.dialog = new frappe.ui.Dialog({ - title: __('Select Variant for {0}', [this.item_name]), - fields, - on_hide: () => { - set_continue_configuration(); - } - }); - - this.attribute_data.forEach(a => { - const field = this.dialog.get_field(a.attribute); - const $a = $(`${__("Clear")}`); - $a.on('click', (e) => { - e.preventDefault(); - this.dialog.set_value(a.attribute, ''); - }); - field.$wrapper.find('.help-box').append($a); - }); - - this.append_status_area(); - this.dialog.show(); - - this.dialog.set_values(JSON.parse(localStorage.getItem(this.get_cache_key()))); - - $('.btn-configure').prop('disabled', false); - } - - on_attribute_selection(e) { - if (e) { - const changed_fieldname = $(e.target).data('fieldname'); - this.show_range_input_if_applicable(changed_fieldname); - } else { - this.show_range_input_for_all_fields(); - } - - const values = this.dialog.get_values(); - if (Object.keys(values).length === 0) { - this.clear_status(); - localStorage.removeItem(this.get_cache_key()); - return; - } - - // save state - localStorage.setItem(this.get_cache_key(), JSON.stringify(values)); - - // show - this.set_loading_status(); - - this.get_next_attribute_and_values(values) - .then(data => { - const { - valid_options_for_attributes, - } = data; - - this.set_item_found_status(data); - - for (let attribute in valid_options_for_attributes) { - const valid_options = valid_options_for_attributes[attribute]; - const options = this.dialog.get_field(attribute).df.options; - const new_options = options.map(o => { - o.disabled = !valid_options.includes(o.value); - return o; - }); - - this.dialog.set_df_property(attribute, 'options', new_options); - this.dialog.get_field(attribute).set_options(); - } - }); - } - - show_range_input_for_all_fields() { - this.dialog.fields.forEach(f => { - this.show_range_input_if_applicable(f.fieldname); - }); - } - - show_range_input_if_applicable(fieldname) { - const changed_field = this.dialog.get_field(fieldname); - const changed_value = changed_field.get_value(); - if (changed_value && changed_value.includes(' to ')) { - // possible range input - let numbers = changed_value.split(' to '); - numbers = numbers.map(number => parseFloat(number)); - - if (!numbers.some(n => isNaN(n))) { - numbers.sort((a, b) => a - b); - if (changed_field.$input_wrapper.find('.range-selector').length) { - return; - } - const parent = $('
') - .insertBefore(changed_field.$input_wrapper.find('.help-box')); - const control = frappe.ui.form.make_control({ - df: { - fieldtype: 'Int', - label: __('Enter value betweeen {0} and {1}', [numbers[0], numbers[1]]), - change: () => { - const value = control.get_value(); - if (value < numbers[0] || value > numbers[1]) { - control.$wrapper.addClass('was-validated'); - control.set_description( - __('Value must be between {0} and {1}', [numbers[0], numbers[1]])); - control.$input[0].setCustomValidity('error'); - } else { - control.$wrapper.removeClass('was-validated'); - control.set_description(''); - control.$input[0].setCustomValidity(''); - this.update_range_values(fieldname, value); - } - } - }, - render_input: true, - parent - }); - control.$wrapper.addClass('mt-3'); - } - } - } - - update_range_values(attribute, range_value) { - this.range_values = this.range_values || {}; - this.range_values[attribute] = range_value; - } - - show_remaining_optional_attributes() { - // show all attributes if remaining - // unselected attributes are all optional - const unselected_attributes = this.dialog.fields.filter(df => { - const value_selected = this.dialog.get_value(df.fieldname); - return !value_selected; - }); - const is_optional_attribute = df => { - const optional_attributes = this.attribute_data - .filter(a => a.optional).map(a => a.attribute); - return optional_attributes.includes(df.fieldname); - }; - if (unselected_attributes.every(is_optional_attribute)) { - unselected_attributes.forEach(df => { - this.dialog.fields_dict[df.fieldname].$wrapper.show(); - }); - } - } - - set_loading_status() { - this.dialog.$status_area.html(` - - `); - } - - set_item_found_status(data) { - const html = this.get_html_for_item_found(data); - this.dialog.$status_area.html(html); - } - - clear_status() { - this.dialog.$status_area.empty(); - } - - get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info, available_qty, settings }) { - const one_item = exact_match.length === 1 - ? exact_match[0] - : filtered_items_count === 1 - ? filtered_items[0] - : ''; - - let item_add_to_cart = one_item ? ` - - ` : ''; - - const items_found = filtered_items_count === 1 ? - __('{0} item found.', [filtered_items_count]) : - __('{0} items found.', [filtered_items_count]); - - /* eslint-disable indent */ - const item_found_status = exact_match.length === 1 - ? `` - : ``; - /* eslint-disable indent */ - - if (!product_info?.allow_items_not_in_stock && available_qty === 0 - && product_info && product_info?.is_stock_item) { - item_add_to_cart = ''; - } - - return ` - ${item_found_status} - ${item_add_to_cart} - `; - } - - btn_add_to_cart(e) { - if (frappe.session.user !== 'Guest') { - localStorage.removeItem(this.get_cache_key()); - } - const item_code = $(e.currentTarget).data('item-code'); - const additional_notes = Object.keys(this.range_values || {}).map(attribute => { - return `${attribute}: ${this.range_values[attribute]}`; - }).join('\n'); - erpnext.e_commerce.shopping_cart.update_cart({ - item_code, - additional_notes, - qty: 1 - }); - this.dialog.hide(); - } - - btn_clear_values() { - this.dialog.fields_list.forEach(f => { - if (f.df?.options) { - f.df.options = f.df.options.map(option => { - option.disabled = false; - return option; - }); - } - }); - this.dialog.clear(); - this.dialog.$status_area.empty(); - this.on_attribute_selection(); - } - - append_status_area() { - this.dialog.$status_area = $('
'); - this.dialog.$wrapper.find('.modal-body').append(this.dialog.$status_area); - this.dialog.$wrapper.on('click', '[data-action]', (e) => { - e.preventDefault(); - const $target = $(e.currentTarget); - const action = $target.data('action'); - const method = this[action]; - method.call(this, e); - }); - this.dialog.$wrapper.addClass('item-configurator-dialog'); - } - - get_next_attribute_and_values(selected_attributes) { - return this.call('erpnext.e_commerce.variant_selector.utils.get_next_attribute_and_values', { - item_code: this.item_code, - selected_attributes - }); - } - - get_attributes_and_values() { - return this.call('erpnext.e_commerce.variant_selector.utils.get_attributes_and_values', { - item_code: this.item_code - }); - } - - get_cache_key() { - return `configure:${this.item_code}`; - } - - call(method, args) { - // promisified frappe.call - return new Promise((resolve, reject) => { - frappe.call(method, args) - .then(r => resolve(r.message)) - .fail(reject); - }); - } -} - -function set_continue_configuration() { - const $btn_configure = $('.btn-configure'); - const { itemCode } = $btn_configure.data(); - - if (localStorage.getItem(`configure:${itemCode}`)) { - $btn_configure.text(__('Continue Selection')); - } else { - $btn_configure.text(__('Select Variant')); - } -} - -frappe.ready(() => { - const $btn_configure = $('.btn-configure'); - if (!$btn_configure.length) return; - const { itemCode, itemName } = $btn_configure.data(); - - set_continue_configuration(); - - $btn_configure.on('click', () => { - $btn_configure.prop('disabled', true); - new ItemConfigure(itemCode, itemName); - }); -}); diff --git a/erpnext/templates/generators/item/item_details.html b/erpnext/templates/generators/item/item_details.html deleted file mode 100644 index 028936bf5f..0000000000 --- a/erpnext/templates/generators/item/item_details.html +++ /dev/null @@ -1,63 +0,0 @@ -{% set width_class = "expand" if not slides else "" %} -{% set cart_settings = shopping_cart.cart_settings %} -{% set product_info = shopping_cart.product_info %} -{% set price_info = product_info.get('price') or {} %} - -
-
- -
- {{ doc.web_item_name }} -
- - - {% if cart_settings.enable_wishlist %} - - {% endif %} -
- -

- - {{ _(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 %} - -
- {% if frappe.utils.strip_html(doc.web_long_description or '') %} - {{ doc.web_long_description | safe }} - {% elif frappe.utils.strip_html(doc.description or '') %} - {{ doc.description | safe }} - {% else %} - {{ "" }} - {% endif %} -
-
- -{% block base_scripts %} - - -{% endblock %} - - \ No newline at end of file diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html deleted file mode 100644 index e1bb3b9865..0000000000 --- a/erpnext/templates/generators/item/item_image.html +++ /dev/null @@ -1,108 +0,0 @@ -{% set column_size = 5 if slides else 4 %} -
- {% if slides %} -
- {% for item in slides %} - {{ item.heading }} - {% endfor %} -
- {{ product_image(slides[0].image, 'product-image') }} - - - {% else %} - {{ product_image(doc.website_image, alt=doc.website_image_alt or doc.item_name) }} - {% endif %} - - - - -
- - diff --git a/erpnext/templates/generators/item/item_inquiry.html b/erpnext/templates/generators/item/item_inquiry.html deleted file mode 100644 index af636f1582..0000000000 --- a/erpnext/templates/generators/item/item_inquiry.html +++ /dev/null @@ -1,11 +0,0 @@ -{% if shopping_cart and shopping_cart.cart_settings.enabled %} -{% set cart_settings = shopping_cart.cart_settings %} - {% if cart_settings.show_contact_us_button | int %} - - {% endif %} - -{% endif %} diff --git a/erpnext/templates/generators/item/item_inquiry.js b/erpnext/templates/generators/item/item_inquiry.js deleted file mode 100644 index 0aee996672..0000000000 --- a/erpnext/templates/generators/item/item_inquiry.js +++ /dev/null @@ -1,77 +0,0 @@ -frappe.ready(() => { - const d = new frappe.ui.Dialog({ - title: __('Contact Us'), - fields: [ - { - fieldtype: 'Data', - label: __('Full Name'), - fieldname: 'lead_name', - reqd: 1 - }, - { - fieldtype: 'Data', - label: __('Organization Name'), - fieldname: 'company_name', - }, - { - fieldtype: 'Data', - label: __('Email'), - fieldname: 'email_id', - options: 'Email', - reqd: 1 - }, - { - fieldtype: 'Data', - label: __('Phone Number'), - fieldname: 'phone', - options: 'Phone', - reqd: 1 - }, - { - fieldtype: 'Data', - label: __('Subject'), - fieldname: 'subject', - reqd: 1 - }, - { - fieldtype: 'Text', - label: __('Message'), - fieldname: 'message', - reqd: 1 - } - ], - primary_action: send_inquiry, - primary_action_label: __('Send') - }); - - function send_inquiry() { - const values = d.get_values(); - const doc = Object.assign({}, values); - delete doc.subject; - delete doc.message; - - d.hide(); - - frappe.call('erpnext.e_commerce.shopping_cart.cart.create_lead_for_item_inquiry', { - lead: doc, - subject: values.subject, - message: values.message - }).then(r => { - if (r.message) { - d.clear(); - } - }); - } - - $('.btn-inquiry').click((e) => { - const $btn = $(e.target); - const item_code = $btn.data('item-code'); - d.set_value('subject', 'Inquiry about ' + item_code); - if (!['Administrator', 'Guest'].includes(frappe.session.user)) { - d.set_value('email_id', frappe.session.user); - d.set_value('lead_name', frappe.get_cookie('full_name')); - } - - d.show(); - }); -}); diff --git a/erpnext/templates/generators/item/item_reviews.html b/erpnext/templates/generators/item/item_reviews.html deleted file mode 100644 index c62c6f7749..0000000000 --- a/erpnext/templates/generators/item/item_reviews.html +++ /dev/null @@ -1,88 +0,0 @@ -{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %} - -
- -
-
- {{ _("Customer Reviews") }} -
- -
- - {% if frappe.session.user != "Guest" and user_is_customer %} - - {% endif %} -
-
- - - {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }} - - - -
- {% if reviews %} - {{ user_review(reviews) }} - - {% if total_reviews > 4 %} - - {% endif %} - - {% else %} -
- {{ _("No Reviews") }} -
- {% endif %} -
-
- - diff --git a/erpnext/templates/generators/item/item_specifications.html b/erpnext/templates/generators/item/item_specifications.html deleted file mode 100644 index 0814d81c8a..0000000000 --- a/erpnext/templates/generators/item/item_specifications.html +++ /dev/null @@ -1,20 +0,0 @@ - -{% if website_specifications %} -
-
- {% if not show_tabs %} -
- Product Details -
- {% endif %} - - {% for d in website_specifications -%} - - - - - {%- endfor %} -
{{ d.label }}{{ d.description }}
-
-
-{% endif %} diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html deleted file mode 100644 index 956c3c51e6..0000000000 --- a/erpnext/templates/generators/item_group.html +++ /dev/null @@ -1,72 +0,0 @@ -{% from "erpnext/templates/includes/macros.html" import field_filter_section, attribute_filter_section, discount_range_filters %} -{% extends "templates/web.html" %} - -{% block header %} -
{{ _(item_group_name) }}
-{% endblock header %} - -{% block script %} - -{% endblock %} - -{% block breadcrumbs %} -
- {% include "templates/includes/breadcrumbs.html" %} -
-{% endblock %} - -{% block page_content %} -
-
- {% if slideshow %} - {{ web_block( - "Hero Slider", - values=slideshow, - add_container=0, - add_top_padding=0, - add_bottom_padding=0, - ) }} - {% endif %} - - {% if description %} -
{{ description or ""}}
- {% endif %} -
-
-
- -
- -
-
-
-
{{ _('Filters') }}
- {{ _('Clear All') }} -
- - {{ field_filter_section(field_filters) }} - - - {{ attribute_filter_section(attribute_filters) }} - -
- -
-
-
- - -{% endblock %} diff --git a/erpnext/templates/includes/cart/address_card.html b/erpnext/templates/includes/cart/address_card.html deleted file mode 100644 index 830ed649f5..0000000000 --- a/erpnext/templates/includes/cart/address_card.html +++ /dev/null @@ -1,17 +0,0 @@ -
-
- {{ _('Change') }} -
-
-
{{ address.title }}
-
- {{ address.display }} -
- - - - - {{ _('Edit') }} - -
-
diff --git a/erpnext/templates/includes/cart/address_picker_card.html b/erpnext/templates/includes/cart/address_picker_card.html deleted file mode 100644 index 646210e65f..0000000000 --- a/erpnext/templates/includes/cart/address_picker_card.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
- -
-
-
{{ address.title }}
-

- {{ address.display }} -

- {{ _('Edit') }} -
-
diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html deleted file mode 100644 index a8188ec825..0000000000 --- a/erpnext/templates/includes/cart/cart_address.html +++ /dev/null @@ -1,189 +0,0 @@ -{% from "erpnext/templates/includes/cart/cart_macros.html" import show_address %} - -{% if addresses | length == 1%} - {% set select_address = True %} -{% endif %} - -
-
-
{{ _("Shipping Address") }}
- -
- -
- {% for address in shipping_addresses %} - {% if doc.shipping_address_name == address.name %} -
-
- {% include "templates/includes/cart/address_card.html" %} -
-
- {% endif %} - {% endfor %} -
- - -
- -
- -{% if billing_addresses %} -
-
-
{{ _("Billing Address") }}
- -
- -
- {% for address in billing_addresses %} - {% if doc.customer_address == address.name %} -
-
- {% include "templates/includes/cart/address_card.html" %} -
-
- {% endif %} - {% endfor %} -
-{% endif %} - - diff --git a/erpnext/templates/includes/cart/cart_address_picker.html b/erpnext/templates/includes/cart/cart_address_picker.html deleted file mode 100644 index 66a50ecc9f..0000000000 --- a/erpnext/templates/includes/cart/cart_address_picker.html +++ /dev/null @@ -1,3 +0,0 @@ -
-
{{ _("Shipping Address") }}
-
diff --git a/erpnext/templates/includes/cart/cart_dropdown.html b/erpnext/templates/includes/cart/cart_dropdown.html deleted file mode 100644 index 38ad183916..0000000000 --- a/erpnext/templates/includes/cart/cart_dropdown.html +++ /dev/null @@ -1,27 +0,0 @@ -
- - -
-
- {{ _("Item") }} -
-
- {{ _("Price") }} -
-
- - {% if doc.items %} -
-
- {% include "templates/includes/cart/cart_items_dropdown.html" %} -
-
- {% else %} -

{{ _("Cart is Empty") }}

- {% endif %} -
diff --git a/erpnext/templates/includes/cart/cart_items.html b/erpnext/templates/includes/cart/cart_items.html deleted file mode 100644 index 428b36e9b3..0000000000 --- a/erpnext/templates/includes/cart/cart_items.html +++ /dev/null @@ -1,113 +0,0 @@ -{% from "erpnext/templates/includes/macros.html" import product_image %} - -{% macro item_subtotal(item) %} -
- {{ item.get_formatted('amount') }} -
- - {% if item.is_free_item %} -
- - {{ _('FREE') }} - -
- {% else %} - - {{ _('Rate:') }} {{ item.get_formatted('rate') }} - - {% endif %} -{% endmacro %} - -{% for d in doc.items %} - - -
-
- {% if d.thumbnail %} - {{ product_image(d.thumbnail, alt="d.web_item_name", no_border=True) }} - {% else %} -
- {{ frappe.utils.get_abbr(d.web_item_name) or "NA" }} -
- {% endif %} -
- -
-
- {{ d.get("web_item_name") or d.item_name }} -
-
- {{ d.item_code }} -
- {%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %} - {% if variant_of %} - - {{ _('Variant of') }} - - {{ variant_of }} - - - {% endif %} - -
- -
-
-
- - - - -
- {% set disabled = 'disabled' if d.is_free_item else '' %} -
- - - - - - - - - -
- -
- {% if not d.is_free_item %} -
- - - -
- {% endif %} -
-
- - - - {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %} -
- {{ item_subtotal(d) }} -
- {% endif %} - - - - {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %} - - {{ item_subtotal(d) }} - - {% endif %} - -{% endfor %} diff --git a/erpnext/templates/includes/cart/cart_items_dropdown.html b/erpnext/templates/includes/cart/cart_items_dropdown.html deleted file mode 100644 index 5d107fc0d0..0000000000 --- a/erpnext/templates/includes/cart/cart_items_dropdown.html +++ /dev/null @@ -1,12 +0,0 @@ -{% from "erpnext/templates/includes/order/order_macros.html" import item_name_and_description_cart %} - -{% for d in doc.items %} -
-
- {{ item_name_and_description_cart(d) }} -
-
- {{ d.get_formatted("amount") }} -
-
-{% endfor %} diff --git a/erpnext/templates/includes/cart/cart_items_total.html b/erpnext/templates/includes/cart/cart_items_total.html deleted file mode 100644 index c94fde462b..0000000000 --- a/erpnext/templates/includes/cart/cart_items_total.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - {{ _("Total") }} - - - {{ doc.get_formatted("total") }} - - \ No newline at end of file diff --git a/erpnext/templates/includes/cart/cart_macros.html b/erpnext/templates/includes/cart/cart_macros.html deleted file mode 100644 index fd95dba424..0000000000 --- a/erpnext/templates/includes/cart/cart_macros.html +++ /dev/null @@ -1,22 +0,0 @@ -{% macro show_address(address, doc, fieldname, select_address=False) %} -{% set selected=address.name==doc.get(fieldname) %} - -
-
-
-
- {{ address.name }}
-
-
-
-
-
-
{{ address.display }}
-
-
-{% endmacro %} diff --git a/erpnext/templates/includes/cart/cart_payment_summary.html b/erpnext/templates/includes/cart/cart_payment_summary.html deleted file mode 100644 index b5655a237b..0000000000 --- a/erpnext/templates/includes/cart/cart_payment_summary.html +++ /dev/null @@ -1,84 +0,0 @@ - -{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %} -
- {{ _("Payment Summary") }} -
-{% endif %} - -
-
- {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %} - - - {% set total_items = frappe.utils.cstr(frappe.utils.flt(doc.total_qty, 0)) %} - - - - - - {% for d in doc.taxes %} - {% if d.base_tax_amount %} - - - - - {% endif %} - {% endfor %} -
{{ _("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") }}
- {% endif %} - - {% if cart_settings.enable_checkout %} - - {% else %} - - {% endif %} -
-
- - - \ No newline at end of file diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html deleted file mode 100644 index d7adae562e..0000000000 --- a/erpnext/templates/includes/navbar/navbar_items.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends 'frappe/templates/includes/navbar/navbar_items.html' %} - -{% block navbar_right_extension %} - - {% if frappe.db.get_single_value("E Commerce Settings", "enable_wishlist") %} - - {% endif %} -{% endblock %} diff --git a/erpnext/templates/includes/order/order_macros.html b/erpnext/templates/includes/order/order_macros.html deleted file mode 100644 index d95b28961c..0000000000 --- a/erpnext/templates/includes/order/order_macros.html +++ /dev/null @@ -1,52 +0,0 @@ -{% from "erpnext/templates/includes/macros.html" import product_image %} - -{% macro item_name_and_description(d) %} -
-
-
- {% if d.thumbnail or d.image %} - {{ product_image(d.thumbnail or d.image, no_border=True) }} - {% else %} -
- {{ frappe.utils.get_abbr(d.item_name) or "NA" }} -
- {% endif %} -
-
-
- {{ d.item_code }} -
- {{ html2text(d.description) | truncate(140) }} -
- - {{ _("Qty ") }}({{ d.get_formatted("qty") }}) - -
-
-{% endmacro %} - -{% macro item_name_and_description_cart(d) %} -
-
-
- {{ product_image_square(d.thumbnail or d.image) }} -
-
-
- {{ d.item_name|truncate(25) }} -
- - - - - - - -
-
-
-{% endmacro %} diff --git a/erpnext/templates/includes/product_page.js b/erpnext/templates/includes/product_page.js deleted file mode 100644 index a3979d037b..0000000000 --- a/erpnext/templates/includes/product_page.js +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt - -frappe.ready(function() { - window.item_code = $('[itemscope] [itemprop="productID"]').text().trim(); - var qty = 0; - - frappe.call({ - type: "POST", - method: "erpnext.e_commerce.shopping_cart.product_info.get_product_info_for_website", - args: { - item_code: get_item_code() - }, - callback: function(r) { - if(r.message) { - if(r.message.cart_settings.enabled) { - let hide_add_to_cart = !r.message.product_info.price - || (!r.message.product_info.in_stock && !r.message.cart_settings.allow_items_not_in_stock); - $(".item-cart, .item-price, .item-stock").toggleClass('hide', hide_add_to_cart); - } - if(r.message.cart_settings.show_price) { - $(".item-price").toggleClass("hide", false); - } - if(r.message.cart_settings.show_stock_availability) { - $(".item-stock").toggleClass("hide", false); - } - if(r.message.product_info.price) { - $(".item-price") - .html(r.message.product_info.price.formatted_price_sales_uom + "
\ - (" + r.message.product_info.price.formatted_price + " / " + r.message.product_info.uom + ")
"); - - if(r.message.product_info.in_stock===0) { - $(".item-stock").html("
{{ _("Not in stock") }}
"); - } - else if(r.message.product_info.in_stock===1 && r.message.cart_settings.show_stock_availability) { - var qty_display = "{{ _("In stock") }}"; - if (r.message.product_info.show_stock_qty) { - qty_display += " ("+r.message.product_info.stock_qty+")"; - } - $(".item-stock").html("
\ - "+qty_display+"
"); - } - - if(r.message.product_info.qty) { - qty = r.message.product_info.qty; - toggle_update_cart(r.message.product_info.qty); - } else { - toggle_update_cart(0); - } - } - } - } - }) - - $("#item-add-to-cart button").on("click", function() { - frappe.provide('erpnext.shopping_cart'); - - erpnext.shopping_cart.update_cart({ - item_code: get_item_code(), - qty: $("#item-spinner .cart-qty").val(), - callback: function(r) { - if(!r.exc) { - toggle_update_cart(1); - qty = 1; - } - }, - btn: this, - }); - }); - - $("#item-spinner").on('click', '.number-spinner button', function () { - var btn = $(this), - input = btn.closest('.number-spinner').find('input'), - oldValue = input.val().trim(), - newVal = 0; - - if (btn.attr('data-dir') == 'up') { - newVal = Number.parseInt(oldValue) + 1; - } else if (btn.attr('data-dir') == 'dwn') { - if (Number.parseInt(oldValue) > 1) { - newVal = Number.parseInt(oldValue) - 1; - } - else { - newVal = Number.parseInt(oldValue); - } - } - input.val(newVal); - }); - - $("[itemscope] .item-view-attribute .form-control").on("change", function() { - try { - var item_code = encodeURIComponent(get_item_code()); - - } catch(e) { - // unable to find variant - // then chose the closest available one - - var attribute = $(this).attr("data-attribute"); - var attribute_value = $(this).val(); - var item_code = find_closest_match(attribute, attribute_value); - - if (!item_code) { - frappe.msgprint(__("Cannot find a matching Item. Please select some other value for {0}.", [attribute])) - throw e; - } - } - - if (window.location.search == ("?variant=" + item_code) || window.location.search.includes(item_code)) { - return; - } - - window.location.href = window.location.pathname + "?variant=" + item_code; - }); - - // change the item image src when alternate images are hovered - $(document.body).on('mouseover', '.item-alternative-image', (e) => { - const $alternative_image = $(e.currentTarget); - const src = $alternative_image.find('img').prop('src'); - $('.item-image img').prop('src', src); - }); -}); - -var toggle_update_cart = function(qty) { - $("#item-add-to-cart").toggle(qty ? false : true); - $("#item-update-cart") - .toggle(qty ? true : false) - .find("input").val(qty); - $("#item-spinner").toggle(qty ? false : true); -} - -function get_item_code() { - var variant_info = window.variant_info; - if(variant_info) { - var attributes = get_selected_attributes(); - var no_of_attributes = Object.keys(attributes).length; - - for(var i in variant_info) { - var variant = variant_info[i]; - - if (variant.attributes.length < no_of_attributes) { - // the case when variant has less attributes than template - continue; - } - - var match = true; - for(var j in variant.attributes) { - if(attributes[variant.attributes[j].attribute] - != variant.attributes[j].attribute_value - ) { - match = false; - break; - } - } - if(match) { - return variant.name; - } - } - throw "Unable to match variant"; - } else { - return window.item_code; - } -} - -function find_closest_match(selected_attribute, selected_attribute_value) { - // find the closest match keeping the selected attribute in focus and get the item code - - var attributes = get_selected_attributes(); - - var previous_match_score = 0; - var previous_no_of_attributes = 0; - var matched; - - var variant_info = window.variant_info; - for(var i in variant_info) { - var variant = variant_info[i]; - var match_score = 0; - var has_selected_attribute = false; - - for(var j in variant.attributes) { - if(attributes[variant.attributes[j].attribute]===variant.attributes[j].attribute_value) { - match_score = match_score + 1; - - if (variant.attributes[j].attribute==selected_attribute && variant.attributes[j].attribute_value==selected_attribute_value) { - has_selected_attribute = true; - } - } - } - - if (has_selected_attribute - && ((match_score > previous_match_score) || (match_score==previous_match_score && previous_no_of_attributes < variant.attributes.length))) { - previous_match_score = match_score; - matched = variant; - previous_no_of_attributes = variant.attributes.length; - - - } - } - - if (matched) { - for (var j in matched.attributes) { - var attr = matched.attributes[j]; - $('[itemscope]') - .find(repl('.item-view-attribute .form-control[data-attribute="%(attribute)s"]', attr)) - .val(attr.attribute_value); - } - - return matched.name; - } -} - -function get_selected_attributes() { - var attributes = {}; - $('[itemscope]').find(".item-view-attribute .form-control").each(function() { - attributes[$(this).attr('data-attribute')] = $(this).val(); - }); - return attributes; -} diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html deleted file mode 100644 index 2b7d9e3523..0000000000 --- a/erpnext/templates/pages/cart.html +++ /dev/null @@ -1,132 +0,0 @@ -{% extends "templates/web.html" %} - -{% block title %} {{ _("Shopping Cart") }} {% endblock %} - -{% block header %}

{{ _("Shopping Cart") }}

{% endblock %} - -{% block header_actions %} -{% endblock %} - -{% block page_content %} - -{% from "templates/includes/macros.html" import item_name_and_description %} - -{% if doc.items %} -
-
- -
-
- -
- {{ _('Items') }} -
- - - - - - {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %} - - {% endif %} - - - - - {% include "templates/includes/cart/cart_items.html" %} - - - {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %} - - {% include "templates/includes/cart/cart_items_total.html" %} - - {% endif %} -
{{ _('Item') }}{{ _('Quantity') }}{{ _('Subtotal') }}
- -
-
- {% if cart_settings.enable_checkout %} - - {{ _('Past Orders') }} - - {% else %} - - {{ _('Past Quotes') }} - - {% endif %} -
-
- {% if doc.items %} - - {% endif %} -
-
-
- - - {% if doc.items %} - {% if doc.terms %} -
-
{{ _("Terms and Conditions") }}
-
- {{ doc.terms }} -
-
- {% endif %} -
- - -
-
- - {% set show_coupon_code = cart_settings.show_apply_coupon_code_in_website and cart_settings.enable_checkout %} - {% if show_coupon_code == 1%} -
-
- - - -
-
- {% endif %} - -
- {% include "templates/includes/cart/cart_payment_summary.html" %} -
- - {% include "templates/includes/cart/cart_address.html" %} -
-
- {% endif %} -
-
-{% else %} -
-
- Empty State -
-
{{ _('Your cart is Empty') }}

- {% if cart_settings.enable_checkout %} - - {{ _('See past orders') }} - - {% else %} - - {{ _('See past quotations') }} - - {% endif %} -
-{% endif %} - -{% endblock %} - -{% block base_scripts %} - -{{ include_script("frappe-web.bundle.js") }} -{{ include_script("controls.bundle.js") }} -{{ include_script("dialog.bundle.js") }} -{% endblock %} diff --git a/erpnext/templates/pages/cart.js b/erpnext/templates/pages/cart.js deleted file mode 100644 index fb2d159dcf..0000000000 --- a/erpnext/templates/pages/cart.js +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt - -// JS exclusive to /cart page -frappe.provide("erpnext.e_commerce.shopping_cart"); -var shopping_cart = erpnext.e_commerce.shopping_cart; - -$.extend(shopping_cart, { - show_error: function(title, text) { - $("#cart-container").html('

' + - title + '

' + text + '

'); - }, - - bind_events: function() { - shopping_cart.bind_address_picker_dialog(); - shopping_cart.bind_place_order(); - shopping_cart.bind_request_quotation(); - shopping_cart.bind_change_qty(); - shopping_cart.bind_remove_cart_item(); - shopping_cart.bind_change_notes(); - shopping_cart.bind_coupon_code(); - }, - - bind_address_picker_dialog: function() { - const d = this.get_update_address_dialog(); - this.parent.find('.btn-change-address').on('click', (e) => { - const type = $(e.currentTarget).parents('.address-container').attr('data-address-type'); - $(d.get_field('address_picker').wrapper).html( - this.get_address_template(type) - ); - d.show(); - }); - }, - - get_update_address_dialog() { - let d = new frappe.ui.Dialog({ - title: "Select Address", - fields: [{ - 'fieldtype': 'HTML', - 'fieldname': 'address_picker', - }], - primary_action_label: __('Set Address'), - primary_action: () => { - const $card = d.$wrapper.find('.address-card.active'); - const address_type = $card.closest('[data-address-type]').attr('data-address-type'); - const address_name = $card.closest('[data-address-name]').attr('data-address-name'); - frappe.call({ - type: "POST", - method: "erpnext.e_commerce.shopping_cart.cart.update_cart_address", - freeze: true, - args: { - address_type, - address_name - }, - callback: function(r) { - d.hide(); - if (!r.exc) { - $(".cart-tax-items").html(r.message.total); - shopping_cart.parent.find( - `.address-container[data-address-type="${address_type}"]` - ).html(r.message.address); - } - } - }); - } - }); - - return d; - }, - - get_address_template(type) { - return { - shipping: `
-
- {% for address in shipping_addresses %} -
- {% include "templates/includes/cart/address_picker_card.html" %} -
- {% endfor %} -
-
`, - billing: `
-
- {% for address in billing_addresses %} -
- {% include "templates/includes/cart/address_picker_card.html" %} -
- {% endfor %} -
-
`, - }[type]; - }, - - bind_place_order: function() { - $(".btn-place-order").on("click", function() { - shopping_cart.place_order(this); - }); - }, - - bind_request_quotation: function() { - $('.btn-request-for-quotation').on('click', function() { - shopping_cart.request_quotation(this); - }); - }, - - bind_change_qty: function() { - // bind update button - $(".cart-items").on("change", ".cart-qty", function() { - var item_code = $(this).attr("data-item-code"); - var newVal = $(this).val(); - shopping_cart.shopping_cart_update({item_code, qty: newVal}); - }); - - $(".cart-items").on('click', '.number-spinner button', function () { - var btn = $(this), - input = btn.closest('.number-spinner').find('input'), - oldValue = input.val().trim(), - newVal = 0; - - if (btn.attr('data-dir') == 'up') { - newVal = parseInt(oldValue) + 1; - } else { - if (oldValue > 1) { - newVal = parseInt(oldValue) - 1; - } - } - input.val(newVal); - - let notes = input.closest("td").siblings().find(".notes").text().trim(); - var item_code = input.attr("data-item-code"); - shopping_cart.shopping_cart_update({ - item_code, - qty: newVal, - additional_notes: notes - }); - }); - }, - - bind_change_notes: function() { - $('.cart-items').on('change', 'textarea', function() { - const $textarea = $(this); - const item_code = $textarea.attr('data-item-code'); - const qty = $textarea.closest('tr').find('.cart-qty').val(); - const notes = $textarea.val(); - shopping_cart.shopping_cart_update({ - item_code, - qty, - additional_notes: notes - }); - }); - }, - - bind_remove_cart_item: function() { - $(".cart-items").on("click", ".remove-cart-item", (e) => { - const $remove_cart_item_btn = $(e.currentTarget); - var item_code = $remove_cart_item_btn.data("item-code"); - - shopping_cart.shopping_cart_update({ - item_code: item_code, - qty: 0 - }); - }); - }, - - render_tax_row: function($cart_taxes, doc, shipping_rules) { - var shipping_selector; - if(shipping_rules) { - shipping_selector = ''; - } - - var $tax_row = $(repl('
\ -
\ -
\ -
' + - (shipping_selector || '

%(description)s

') + - '
\ -
\ -
\ -
\ - %(formatted_tax_amount)s

\ -
\ -
', doc)).appendTo($cart_taxes); - - if(shipping_selector) { - $tax_row.find('select option').each(function(i, opt) { - if($(opt).html() == doc.description) { - $(opt).attr("selected", "selected"); - } - }); - $tax_row.find('select').on("change", function() { - shopping_cart.apply_shipping_rule($(this).val(), this); - }); - } - }, - - apply_shipping_rule: function(rule, btn) { - return frappe.call({ - btn: btn, - type: "POST", - method: "erpnext.e_commerce.shopping_cart.cart.apply_shipping_rule", - args: { shipping_rule: rule }, - callback: function(r) { - if(!r.exc) { - shopping_cart.render(r.message); - } - } - }); - }, - - place_order: function(btn) { - shopping_cart.freeze(); - - return frappe.call({ - type: "POST", - method: "erpnext.e_commerce.shopping_cart.cart.place_order", - btn: btn, - callback: function(r) { - if(r.exc) { - shopping_cart.unfreeze(); - var msg = ""; - if(r._server_messages) { - msg = JSON.parse(r._server_messages || []).join("
"); - } - - $("#cart-error") - .empty() - .html(msg || frappe._("Something went wrong!")) - .toggle(true); - } else { - $(btn).hide(); - window.location.href = '/orders/' + encodeURIComponent(r.message); - } - } - }); - }, - - request_quotation: function(btn) { - shopping_cart.freeze(); - - return frappe.call({ - type: "POST", - method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation", - btn: btn, - callback: function(r) { - if(r.exc) { - shopping_cart.unfreeze(); - var msg = ""; - if(r._server_messages) { - msg = JSON.parse(r._server_messages || []).join("
"); - } - - $("#cart-error") - .empty() - .html(msg || frappe._("Something went wrong!")) - .toggle(true); - } else { - $(btn).hide(); - window.location.href = '/quotations/' + encodeURIComponent(r.message); - } - } - }); - }, - - bind_coupon_code: function() { - $(".bt-coupon").on("click", function() { - shopping_cart.apply_coupon_code(this); - }); - }, - - apply_coupon_code: function(btn) { - return frappe.call({ - type: "POST", - method: "erpnext.e_commerce.shopping_cart.cart.apply_coupon_code", - btn: btn, - args : { - applied_code : $('.txtcoupon').val(), - applied_referral_sales_partner: $('.txtreferral_sales_partner').val() - }, - callback: function(r) { - if (r && r.message){ - location.reload(); - } - } - }); - } -}); - -frappe.ready(function() { - if (window.location.pathname === "/cart") { - $(".cart-icon").hide(); - } - shopping_cart.parent = $(".cart-container"); - shopping_cart.bind_events(); -}); - -function show_terms() { - var html = $(".cart-terms").html(); - frappe.msgprint(html); -} diff --git a/erpnext/templates/pages/cart.py b/erpnext/templates/pages/cart.py deleted file mode 100644 index cadb46f265..0000000000 --- a/erpnext/templates/pages/cart.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -no_cache = 1 - -from erpnext.e_commerce.shopping_cart.cart import get_cart_quotation - - -def get_context(context): - context.body_class = "product-page" - context.update(get_cart_quotation()) diff --git a/erpnext/templates/pages/customer_reviews.html b/erpnext/templates/pages/customer_reviews.html deleted file mode 100644 index 121bec378c..0000000000 --- a/erpnext/templates/pages/customer_reviews.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends "templates/web.html" %} -{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %} - -{% block title %} {{ _("Customer Reviews") }} {% endblock %} - -{% block page_content %} -
- {% if enable_reviews %} - -
-
- {{ _("Customer Reviews") }} -
- -
- - {% if frappe.session.user != "Guest" and user_is_customer %} - - {% endif %} -
-
- - - {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }} - - - -
- {% if reviews %} - {{ user_review(reviews) }} - - {% if not reviews | len >= total_reviews %} - - {% endif %} - - {% else %} -
- {{ _("No Reviews") }} -
- {% endif %} -
- {% else %} - -
-

- {{ _("No Reviews") }} -

-
- {% endif %} -
- -{% endblock %} - -{% block base_scripts %} - - - - - - -{% endblock %} \ No newline at end of file diff --git a/erpnext/templates/pages/customer_reviews.py b/erpnext/templates/pages/customer_reviews.py deleted file mode 100644 index c1f0c93f1a..0000000000 --- a/erpnext/templates/pages/customer_reviews.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt -import frappe - -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( - get_shopping_cart_settings, -) -from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews -from erpnext.e_commerce.doctype.website_item.website_item import check_if_user_is_customer - - -def get_context(context): - context.body_class = "product-page" - context.no_cache = 1 - context.full_page = True - context.reviews = None - - if frappe.form_dict and frappe.form_dict.get("web_item"): - context.web_item = frappe.form_dict.get("web_item") - context.user_is_customer = check_if_user_is_customer() - context.enable_reviews = get_shopping_cart_settings().enable_reviews - - if context.enable_reviews: - reviews_data = get_item_reviews(context.web_item) - context.update(reviews_data) diff --git a/erpnext/templates/pages/home.html b/erpnext/templates/pages/home.html index 27d966ad42..08e0432dcf 100644 --- a/erpnext/templates/pages/home.html +++ b/erpnext/templates/pages/home.html @@ -17,9 +17,6 @@

{{ homepage.description }}

- {% elif homepage.hero_section_based_on == 'Slideshow' and slideshow %}
@@ -29,26 +26,6 @@ {{ render_homepage_section(homepage.hero_section_doc) }} {% endif %} - {% if homepage.products %} -
-

{{ _('Products') }}

- -
- {% for item in homepage.products %} -
-
- {{ item.item_name }} -
-
{{ item.item_name }}
- {{ _('More details') }} -
-
-
- {% endfor %} -
-
- {% endif %} - {% if blogs %}

{{ _('Publications') }}

diff --git a/erpnext/templates/pages/home.py b/erpnext/templates/pages/home.py index 47fb89dea3..751a5b0b50 100644 --- a/erpnext/templates/pages/home.py +++ b/erpnext/templates/pages/home.py @@ -10,11 +10,6 @@ no_cache = 1 def get_context(context): homepage = frappe.get_cached_doc("Homepage") - for item in homepage.products: - route = frappe.db.get_value("Website Item", {"item_code": item.item_code}, "route") - if route: - item.route = "/" + route - homepage.title = homepage.title or homepage.company context.title = homepage.title context.homepage = homepage @@ -52,5 +47,3 @@ def get_context(context): context.metatags = context.metatags or frappe._dict({}) context.metatags.image = homepage.hero_image or None context.metatags.description = homepage.description or None - - context.explore_link = "/all-products" diff --git a/erpnext/templates/pages/order.js b/erpnext/templates/pages/order.js deleted file mode 100644 index 0574cdedc0..0000000000 --- a/erpnext/templates/pages/order.js +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ready(function(){ - - var loyalty_points_input = document.getElementById("loyalty-point-to-redeem"); - var loyalty_points_status = document.getElementById("loyalty-points-status"); - if (loyalty_points_input) { - loyalty_points_input.onblur = apply_loyalty_points; - } - - function apply_loyalty_points() { - var loyalty_points = parseInt(loyalty_points_input.value); - if (loyalty_points) { - frappe.call({ - method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_redeemption_factor", - args: { - "customer": doc_info.customer - }, - callback: function(r) { - if (r) { - var message = "" - let loyalty_amount = flt(r.message*loyalty_points); - if (doc_info.grand_total && doc_info.grand_total < loyalty_amount) { - let redeemable_amount = parseInt(doc_info.grand_total/r.message); - message = "You can only redeem max " + redeemable_amount + " points in this order."; - frappe.msgprint(__(message)); - } else { - message = loyalty_points + " Loyalty Points of amount "+ loyalty_amount + " is applied." - frappe.msgprint(__(message)); - var remaining_amount = flt(doc_info.grand_total) - flt(loyalty_amount); - var payment_button = document.getElementById("pay-for-order"); - payment_button.innerHTML = __("Pay Remaining"); - payment_button.href = "/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn="+doc_info.doctype_name+"&dt="+doc_info.doctype+"&loyalty_points="+loyalty_points+"&submit_doc=1&order_type=Shopping Cart"; - } - loyalty_points_status.innerHTML = message; - } - } - }); - } - } -}) diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index 13772d3129..d0968bf88a 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -4,8 +4,6 @@ import frappe from frappe import _ -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import show_attachments - def get_context(context): context.no_cache = 1 @@ -14,17 +12,12 @@ def get_context(context): if hasattr(context.doc, "set_indicator"): context.doc.set_indicator() - if show_attachments(): - context.attachments = get_attachments(frappe.form_dict.doctype, frappe.form_dict.name) - context.parents = frappe.form_dict.parents context.title = frappe.form_dict.name context.payment_ref = frappe.db.get_value( "Payment Request", {"reference_name": frappe.form_dict.name}, "name" ) - context.enabled_checkout = frappe.get_doc("E Commerce Settings").enable_checkout - default_print_format = frappe.db.get_value( "Property Setter", dict(property="default_print_format", doc_type=frappe.form_dict.doctype), diff --git a/erpnext/templates/pages/product_search.html b/erpnext/templates/pages/product_search.html deleted file mode 100644 index 6a5425bbf8..0000000000 --- a/erpnext/templates/pages/product_search.html +++ /dev/null @@ -1,32 +0,0 @@ -{% extends "templates/web.html" %} - -{% block title %} {{ _("Product Search") }} {% endblock %} - -{% block header %}

{{ _("Product Search") }}

{% endblock %} - -{% block page_content %} - - - - -
-

{{ _("Search Results") }}

-
- -
-
- -
-
-{% endblock %} diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py deleted file mode 100644 index f40fd479f4..0000000000 --- a/erpnext/templates/pages/product_search.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import json - -import frappe -from frappe.utils import cint, cstr -from redis.commands.search.query import Query - -from erpnext.e_commerce.redisearch_utils import ( - WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, - WEBSITE_ITEM_INDEX, - WEBSITE_ITEM_NAME_AUTOCOMPLETE, - is_redisearch_enabled, -) -from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website -from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_html - -no_cache = 1 - - -def get_context(context): - context.show_search = True - - -@frappe.whitelist(allow_guest=True) -def get_product_list(search=None, start=0, limit=12): - data = get_product_data(search, start, limit) - - for item in data: - set_product_info_for_website(item) - - return [get_item_for_list_in_html(r) for r in data] - - -def get_product_data(search=None, start=0, limit=12): - # limit = 12 because we show 12 items in the grid view - # base query - query = """ - SELECT - web_item_name, item_name, item_code, brand, route, - website_image, thumbnail, item_group, - description, web_long_description as website_description, - website_warehouse, ranking - FROM `tabWebsite Item` - WHERE published = 1 - """ - - # search term condition - if search: - query += """ and (item_name like %(search)s - or web_item_name like %(search)s - or brand like %(search)s - or web_long_description like %(search)s)""" - search = "%" + cstr(search) + "%" - - # order by - query += """ ORDER BY ranking desc, modified desc limit %s offset %s""" % ( - cint(limit), - cint(start), - ) - - return frappe.db.sql(query, {"search": search}, as_dict=1) # nosemgrep - - -@frappe.whitelist(allow_guest=True) -def search(query): - product_results = product_search(query) - category_results = get_category_suggestions(query) - - return { - "product_results": product_results.get("results") or [], - "category_results": category_results.get("results") or [], - } - - -@frappe.whitelist(allow_guest=True) -def product_search(query, limit=10, fuzzy_search=True): - search_results = {"from_redisearch": True, "results": []} - - if not is_redisearch_enabled(): - # Redisearch module not enabled - search_results["from_redisearch"] = False - search_results["results"] = get_product_data(query, 0, limit) - return search_results - - if not query: - return search_results - - redis = frappe.cache() - query = clean_up_query(query) - - # TODO: Check perf/correctness with Suggestions & Query vs only Query - # TODO: Use Levenshtein Distance in Query (max=3) - redisearch = redis.ft(WEBSITE_ITEM_INDEX) - suggestions = redisearch.sugget( - WEBSITE_ITEM_NAME_AUTOCOMPLETE, - query, - num=limit, - fuzzy=fuzzy_search and len(query) > 3, - ) - - # Build a query - query_string = query - - for s in suggestions: - query_string += f"|('{clean_up_query(s.string)}')" - - q = Query(query_string) - results = redisearch.search(q) - - search_results["results"] = list(map(convert_to_dict, results.docs)) - search_results["results"] = sorted( - search_results["results"], key=lambda k: frappe.utils.cint(k["ranking"]), reverse=True - ) - - return search_results - - -def clean_up_query(query): - return "".join(c for c in query if c.isalnum() or c.isspace()) - - -def convert_to_dict(redis_search_doc): - return redis_search_doc.__dict__ - - -@frappe.whitelist(allow_guest=True) -def get_category_suggestions(query): - search_results = {"results": []} - - if not is_redisearch_enabled(): - # Redisearch module not enabled, query db - categories = frappe.db.get_all( - "Item Group", - filters={"name": ["like", "%{0}%".format(query)], "show_in_website": 1}, - fields=["name", "route"], - ) - search_results["results"] = categories - return search_results - - if not query: - return search_results - - ac = frappe.cache().ft() - suggestions = ac.sugget(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, query, num=10, with_payloads=True) - - results = [json.loads(s.payload) for s in suggestions] - - search_results["results"] = results - - return search_results diff --git a/erpnext/templates/pages/wishlist.html b/erpnext/templates/pages/wishlist.html deleted file mode 100644 index 7a81dedb49..0000000000 --- a/erpnext/templates/pages/wishlist.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "templates/web.html" %} - -{% block title %} {{ _("Wishlist") }} {% endblock %} - -{% block header %}

{{ _("Wishlist") }}

{% endblock %} - -{% block page_content %} -{% if items %} -
-
-
- {% from "erpnext/templates/includes/macros.html" import wishlist_card %} - {% for item in items %} - {{ wishlist_card(item, settings) }} - {% endfor %} -
-
-
-{% else %} -
-
- Empty Cart -
-
{{ _('Wishlist is empty!') }}

-
-{% endif %} - -{% endblock %} \ No newline at end of file diff --git a/erpnext/templates/pages/wishlist.py b/erpnext/templates/pages/wishlist.py deleted file mode 100644 index 17607e45f9..0000000000 --- a/erpnext/templates/pages/wishlist.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt -import frappe - -from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( - get_shopping_cart_settings, -) -from erpnext.e_commerce.shopping_cart.cart import _set_price_list -from erpnext.utilities.product import get_price - - -def get_context(context): - is_guest = frappe.session.user == "Guest" - - settings = get_shopping_cart_settings() - items = get_wishlist_items() if not is_guest else [] - selling_price_list = _set_price_list(settings) if not is_guest else None - - items = set_stock_price_details(items, settings, selling_price_list) - - context.body_class = "product-page" - context.items = items - context.settings = settings - context.no_cache = 1 - - -def get_stock_availability(item_code, warehouse): - from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses - - if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1: - warehouses = get_child_warehouses(warehouse) - else: - warehouses = [warehouse] if warehouse else [] - - stock_qty = 0.0 - for warehouse in warehouses: - stock_qty += frappe.utils.flt( - frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") - ) - - return bool(stock_qty) - - -def get_wishlist_items(): - if not frappe.db.exists("Wishlist", frappe.session.user): - return [] - - return frappe.db.get_all( - "Wishlist Item", - filters={"parent": frappe.session.user}, - fields=[ - "web_item_name", - "item_code", - "item_name", - "website_item", - "warehouse", - "image", - "item_group", - "route", - ], - ) - - -def set_stock_price_details(items, settings, selling_price_list): - for item in items: - if settings.show_stock_availability: - item.available = get_stock_availability(item.item_code, item.get("warehouse")) - - price_details = get_price( - item.item_code, selling_price_list, settings.default_customer_group, settings.company - ) - - if price_details: - item.formatted_price = price_details.get("formatted_price") - item.formatted_mrp = price_details.get("formatted_mrp") - if item.formatted_mrp: - item.discount = price_details.get("formatted_discount_percent") or price_details.get( - "formatted_discount_rate" - ) - - return items diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index e967f7061b..7897c15e94 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -2,93 +2,12 @@ # License: GNU General Public License v3. See license.txt import frappe -from frappe.utils import cint, flt, fmt_money, getdate, nowdate +from frappe.utils import cint, flt, fmt_money from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item -from erpnext.stock.doctype.batch.batch import get_batch_qty -from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses -def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): - in_stock, stock_qty = 0, "" - template_item_code, is_stock_item = frappe.db.get_value( - "Item", item_code, ["variant_of", "is_stock_item"] - ) - - if not warehouse: - warehouse = frappe.db.get_value("Website Item", {"item_code": item_code}, item_warehouse_field) - - if not warehouse and template_item_code and template_item_code != item_code: - warehouse = frappe.db.get_value( - "Website Item", {"item_code": template_item_code}, item_warehouse_field - ) - - if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1: - warehouses = get_child_warehouses(warehouse) - else: - warehouses = [warehouse] if warehouse else [] - - total_stock = 0.0 - if warehouses: - for warehouse in warehouses: - stock_qty = frappe.db.sql( - """ - select GREATEST(S.actual_qty - S.reserved_qty - S.reserved_qty_for_production - S.reserved_qty_for_sub_contract, 0) / IFNULL(C.conversion_factor, 1) - from tabBin S - inner join `tabItem` I on S.item_code = I.Item_code - left join `tabUOM Conversion Detail` C on I.sales_uom = C.uom and C.parent = I.Item_code - where S.item_code=%s and S.warehouse=%s""", - (item_code, warehouse), - ) - - if stock_qty: - total_stock += adjust_qty_for_expired_items(item_code, stock_qty, warehouse) - - in_stock = total_stock > 0 and 1 or 0 - - return frappe._dict( - {"in_stock": in_stock, "stock_qty": total_stock, "is_stock_item": is_stock_item} - ) - - -def adjust_qty_for_expired_items(item_code, stock_qty, warehouse): - batches = frappe.get_all("Batch", filters=[{"item": item_code}], fields=["expiry_date", "name"]) - expired_batches = get_expired_batches(batches) - stock_qty = [list(item) for item in stock_qty] - - for batch in expired_batches: - if warehouse: - stock_qty[0][0] = max(0, stock_qty[0][0] - get_batch_qty(batch, warehouse)) - else: - stock_qty[0][0] = max(0, stock_qty[0][0] - qty_from_all_warehouses(get_batch_qty(batch))) - - if not stock_qty[0][0]: - break - - return stock_qty[0][0] if stock_qty else 0 - - -def get_expired_batches(batches): - """ - :param batches: A list of dict in the form [{'expiry_date': datetime.date(20XX, 1, 1), 'name': 'batch_id'}, ...] - """ - return [b.name for b in batches if b.expiry_date and b.expiry_date <= getdate(nowdate())] - - -def qty_from_all_warehouses(batch_info): - """ - :param batch_info: A list of dict in the form [{u'warehouse': u'Stores - I', u'qty': 0.8}, ...] - """ - qty = 0 - for batch in batch_info: - qty = qty + batch.qty - - return qty - - -def get_price(item_code, price_list, customer_group, company, qty=1): - from erpnext.e_commerce.shopping_cart.cart import get_party - +def get_price(item_code, price_list, customer_group, company, qty=1, party=None): template_item_code = frappe.db.get_value("Item", item_code, "variant_of") if price_list: @@ -106,7 +25,6 @@ def get_price(item_code, price_list, customer_group, company, qty=1): ) if price: - party = get_party() pricing_rule_dict = frappe._dict( { "item_code": item_code, @@ -187,16 +105,62 @@ def get_price(item_code, price_list, customer_group, company, qty=1): return price_obj -def get_non_stock_item_status(item_code, item_warehouse_field): - # if item is a product bundle, check if its bundle items are in stock - if frappe.db.exists("Product Bundle", item_code): - items = frappe.get_doc("Product Bundle", item_code).get_all_children() - bundle_warehouse = frappe.db.get_value( - "Website Item", {"item_code": item_code}, item_warehouse_field +def get_item_codes_by_attributes(attribute_filters, template_item_code=None): + items = [] + + for attribute, values in attribute_filters.items(): + attribute_values = values + + if not isinstance(attribute_values, list): + attribute_values = [attribute_values] + + if not attribute_values: + continue + + wheres = [] + query_values = [] + for attribute_value in attribute_values: + wheres.append("( attribute = %s and attribute_value = %s )") + query_values += [attribute, attribute_value] + + attribute_query = " or ".join(wheres) + + if template_item_code: + variant_of_query = "AND t2.variant_of = %s" + query_values.append(template_item_code) + else: + variant_of_query = "" + + query = """ + SELECT + t1.parent + FROM + `tabItem Variant Attribute` t1 + WHERE + 1 = 1 + AND ( + {attribute_query} + ) + AND EXISTS ( + SELECT + 1 + FROM + `tabItem` t2 + WHERE + t2.name = t1.parent + {variant_of_query} + ) + GROUP BY + t1.parent + ORDER BY + NULL + """.format( + attribute_query=attribute_query, variant_of_query=variant_of_query ) - return all( - get_web_item_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock - for d in items - ) - else: - return 1 + + item_codes = set([r[0] for r in frappe.db.sql(query, query_values)]) + items.append(item_codes) + + res = list(set.intersection(*items)) + + return res diff --git a/erpnext/www/all-products/__init__.py b/erpnext/www/all-products/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html deleted file mode 100644 index 04fc74c08f..0000000000 --- a/erpnext/www/all-products/index.html +++ /dev/null @@ -1,51 +0,0 @@ -{% from "erpnext/templates/includes/macros.html" import attribute_filter_section, field_filter_section, discount_range_filters %} -{% extends "templates/web.html" %} - -{% block title %}{{ _('All Products') }}{% endblock %} -{% block header %} -
{{ _('All Products') }}
-{% endblock header %} - -{% block page_content %} -
- -
- -
- - -
-
-
-
{{ _('Filters') }}
- {{ _('Clear All') }} -
- - {% if field_filters %} - {{ field_filter_section(field_filters) }} - {% endif %} - - - {% if attribute_filters %} - {{ attribute_filter_section(attribute_filters) }} - {% endif %} -
- -
-
- - - -{% endblock %} diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js deleted file mode 100644 index 98a8441525..0000000000 --- a/erpnext/www/all-products/index.js +++ /dev/null @@ -1,27 +0,0 @@ -$(() => { - class ProductListing { - constructor() { - let me = this; - let is_item_group_page = $(".item-group-content").data("item-group"); - this.item_group = is_item_group_page || null; - - let view_type = localStorage.getItem("product_view") || "List View"; - - // Render Product Views, Filters & Search - new erpnext.ProductView({ - view_type: view_type, - products_section: $('#product-listing'), - item_group: me.item_group - }); - - this.bind_card_actions(); - } - - bind_card_actions() { - erpnext.e_commerce.shopping_cart.bind_add_to_cart_action(); - erpnext.e_commerce.wishlist.bind_wishlist_action(); - } - } - - new ProductListing(); -}); diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py deleted file mode 100644 index fbf0dce059..0000000000 --- a/erpnext/www/all-products/index.py +++ /dev/null @@ -1,22 +0,0 @@ -import frappe -from frappe.utils import cint - -from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder - -sitemap = 1 - - -def get_context(context): - # Add homepage as parent - context.body_class = "product-page" - context.parents = [{"name": frappe._("Home"), "route": "/"}] - - filter_engine = ProductFiltersBuilder() - context.field_filters = filter_engine.get_field_filters() - context.attribute_filters = filter_engine.get_attribute_filters() - - context.page_length = ( - cint(frappe.db.get_single_value("E Commerce Settings", "products_per_page")) or 20 - ) - - context.no_cache = 1 diff --git a/erpnext/www/all-products/not_found.html b/erpnext/www/all-products/not_found.html deleted file mode 100644 index 91989a9ef4..0000000000 --- a/erpnext/www/all-products/not_found.html +++ /dev/null @@ -1 +0,0 @@ -
{{ _('No products found') }}
diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/www/shop-by-category/category_card_section.html b/erpnext/www/shop-by-category/category_card_section.html deleted file mode 100644 index 56cb63a5b6..0000000000 --- a/erpnext/www/shop-by-category/category_card_section.html +++ /dev/null @@ -1,30 +0,0 @@ -{%- macro card(title, image, type, url=None, text_primary=False) -%} - -
- {% if image %} - {{ title }} - {% else %} -
- - {{ frappe.utils.get_abbr(title) }} - -
- {% endif %} -
- {{ title or '' }} -
- -
-{%- endmacro -%} - -
-
- {%- for row in data -%} - {%- set title = row.name -%} - {%- set image = row.get("image") -%} - {%- if title -%} - {{ card(title, image, type, row.get("route")) }} - {%- endif -%} - {%- endfor -%} -
-
\ No newline at end of file diff --git a/erpnext/www/shop-by-category/index.html b/erpnext/www/shop-by-category/index.html deleted file mode 100644 index 04d2d578cb..0000000000 --- a/erpnext/www/shop-by-category/index.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "templates/web.html" %} -{% block title %}{{ _('Shop by Category') }}{% endblock %} - -{% block head_include %} - -{% endblock %} - -{% block script %} - -{% endblock %} - -{% block page_content %} -
-
- {% if slideshow %} - - {{ web_block( - "Hero Slider", - values=slideshow, - add_container=0, - add_top_padding=0, - add_bottom_padding=0, - ) }} - {% endif %} -
-
- {% if tabs %} - - {{ web_block( - "Section with Tabs", - values=tabs, - add_container=0, - add_top_padding=0, - add_bottom_padding=0 - ) }} - {% endif %} -
-
-{% endblock %} \ No newline at end of file diff --git a/erpnext/www/shop-by-category/index.js b/erpnext/www/shop-by-category/index.js deleted file mode 100644 index 1b3116f5ba..0000000000 --- a/erpnext/www/shop-by-category/index.js +++ /dev/null @@ -1,12 +0,0 @@ -$(() => { - $('.category-card').on('click', (e) => { - let category_type = e.currentTarget.dataset.type; - let category_name = e.currentTarget.dataset.name; - - if (category_type != "item_group") { - let filters = {}; - filters[category_type] = [category_name]; - window.location.href = "/all-products?field_filters=" + JSON.stringify(filters); - } - }); -}); \ No newline at end of file diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py deleted file mode 100644 index 913c1836ac..0000000000 --- a/erpnext/www/shop-by-category/index.py +++ /dev/null @@ -1,91 +0,0 @@ -import frappe -from frappe import _ - -sitemap = 1 - - -def get_context(context): - context.body_class = "product-page" - - settings = frappe.get_cached_doc("E Commerce Settings") - context.categories_enabled = settings.enable_field_filters - - if context.categories_enabled: - categories = [row.fieldname for row in settings.filter_fields] - context.tabs = get_tabs(categories) - - if settings.slideshow: - context.slideshow = get_slideshow(settings.slideshow) - - context.no_cache = 1 - - -def get_slideshow(slideshow): - values = {"show_indicators": 1, "show_controls": 1, "rounded": 1, "slider_name": "Categories"} - slideshow = frappe.get_cached_doc("Website Slideshow", slideshow) - slides = slideshow.get({"doctype": "Website Slideshow Item"}) - for index, slide in enumerate(slides, start=1): - values[f"slide_{index}_image"] = slide.image - values[f"slide_{index}_title"] = slide.heading - values[f"slide_{index}_subtitle"] = slide.description - values[f"slide_{index}_theme"] = slide.get("theme") or "Light" - values[f"slide_{index}_content_align"] = slide.get("content_align") or "Centre" - values[f"slide_{index}_primary_action"] = slide.url - - return values - - -def get_tabs(categories): - tab_values = { - "title": _("Shop by Category"), - } - - categorical_data = get_category_records(categories) - for index, tab in enumerate(categorical_data, start=1): - tab_values[f"tab_{index + 1}_title"] = frappe.unscrub(tab) - # pre-render cards for each tab - tab_values[f"tab_{index + 1}_content"] = frappe.render_template( - "erpnext/www/shop-by-category/category_card_section.html", - {"data": categorical_data[tab], "type": tab}, - ) - return tab_values - - -def get_category_records(categories: list): - categorical_data = {} - website_item_meta = frappe.get_meta("Website Item", cached=True) - - for c in categories: - if c == "item_group": - categorical_data["item_group"] = frappe.db.get_all( - "Item Group", - filters={"parent_item_group": "All Item Groups", "show_in_website": 1}, - fields=["name", "parent_item_group", "is_group", "image", "route"], - ) - - continue - - field_type = website_item_meta.get_field(c).fieldtype - - if field_type == "Table MultiSelect": - child_doc = website_item_meta.get_field(c).options - for field in frappe.get_meta(child_doc, cached=True).fields: - if field.fieldtype == "Link" and field.reqd: - doctype = field.options - else: - doctype = website_item_meta.get_field(c).options - - fields = ["name"] - - try: - meta = frappe.get_meta(doctype, cached=True) - if meta.get_field("image"): - fields += ["image"] - - data = frappe.db.get_all(doctype, fields=fields) - categorical_data[c] = data - except BaseException: - frappe.throw(_("DocType {} not found").format(doctype)) - continue - - return categorical_data From 7f865492d0ba41c9e7d31d8dbecb2411f0574ed9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Oct 2023 17:14:37 +0530 Subject: [PATCH 051/135] chore: pass stock value diff --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b26a95ba8b..6440f8013f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -351,7 +351,7 @@ class PurchaseReceipt(BuyingController): ) exchange_rate_map, net_rate_map = get_purchase_document_details(self) - def make_item_asset_inward_entries(item): + def make_item_asset_inward_entries(item, stock_value_diff): if d.is_fixed_asset: stock_asset_account_name = ( get_asset_category_account( @@ -596,7 +596,7 @@ class PurchaseReceipt(BuyingController): ) outgoing_amount = d.base_net_amount + d.item_tax_amount - make_item_asset_inward_entries(d) + make_item_asset_inward_entries(d, stock_value_diff) make_stock_received_but_not_billed_entry(d, outgoing_amount) make_landed_cost_gl_entries(d) make_rate_difference_entry(d) From 601ab4567ea7062b78448235fc2fc62b15dce6a6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 17 Oct 2023 16:50:20 +0530 Subject: [PATCH 052/135] refactor: use account in key while grouping voucher in ar/ap report --- .../report/accounts_receivable/accounts_receivable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e3b671f397..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 From e31db1891237aa22c19d71929f69d9db5596ae3c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Oct 2023 18:19:47 +0530 Subject: [PATCH 053/135] chore: Add accounting dimensions to Sales Order Item table --- .../accounting_dimension.py | 27 +++++++++++++++++++ erpnext/hooks.py | 1 + erpnext/patches.txt | 1 + ...counting_dimensions_in_sales_order_item.py | 7 +++++ .../sales_order_item/sales_order_item.json | 11 ++++++-- 5 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py 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/hooks.py b/erpnext/hooks.py index 2155699a4c..33b82846b7 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -519,6 +519,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/patches.txt b/erpnext/patches.txt index 8f2d076b53..01fdcc2c6e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -345,5 +345,6 @@ 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 diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py b/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py new file mode 100644 index 0000000000..8f77c35b12 --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py @@ -0,0 +1,7 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + create_accounting_dimensions_for_doctype, +) + + +def execute(): + create_accounting_dimensions_for_doctype(doctype="Sales Order Item") diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index e6f7456620..f82047f511 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -68,6 +68,7 @@ "total_weight", "column_break_21", "weight_uom", + "accounting_dimensions_section", "warehouse_and_reference", "warehouse", "target_warehouse", @@ -889,12 +890,18 @@ "label": "Production Plan Qty", "no_copy": 1, "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-28 14:56:42.031636", + "modified": "2023-10-17 18:18:26.475259", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", @@ -905,4 +912,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file From 7b9cedebf62545669dc2530de9575d06d2c28702 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 17 Oct 2023 19:00:52 +0530 Subject: [PATCH 054/135] fix: Ignore addr permission in internal code --- erpnext/controllers/selling_controller.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 418a56f5fe..c01ac8115a 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -4,7 +4,6 @@ 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.controllers.accounts_controller import get_taxes_and_charges @@ -593,6 +592,12 @@ class SellingController(StockController): ) def set_customer_address(self): + try: + from frappe.contacts.doctype.address.address import render_address + except ImportError: + # Older frappe versions where this function is not available + from frappe.contacts.doctype.address.address import get_address_display as render_address + address_dict = { "customer_address": "address_display", "shipping_address_name": "shipping_address", @@ -602,7 +607,8 @@ 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))) + address = frappe.call(render_address, self.get(address_field), ignore_permissions=True) + self.set(address_display_field, address) def validate_for_duplicate_items(self): check_list, chk_dupl_itm = [], [] From 8c61fe2ca5cbee253784917ec90c5e01c6c0b8a7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Oct 2023 20:36:09 +0530 Subject: [PATCH 055/135] fix: rearrange functions --- erpnext/controllers/stock_controller.py | 5 - .../purchase_receipt/purchase_receipt.py | 114 +++++++----------- .../templates/pages/integrations/__init__.py | 0 3 files changed, 46 insertions(+), 73 deletions(-) create mode 100644 erpnext/templates/pages/integrations/__init__.py diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index ae54b801f1..4b2c922cb7 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -73,11 +73,6 @@ 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) - def validate_serialized_batch(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 6440f8013f..bd3aeac2e9 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -313,7 +313,6 @@ class PurchaseReceipt(BuyingController): self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) self.make_tax_gl_entries(gl_entries) - # self.get_asset_gl_entry(gl_entries) return process_gl_map(gl_entries) @@ -351,34 +350,7 @@ class PurchaseReceipt(BuyingController): ) exchange_rate_map, net_rate_map = get_purchase_document_details(self) - def make_item_asset_inward_entries(item, stock_value_diff): - if d.is_fixed_asset: - stock_asset_account_name = ( - get_asset_category_account( - asset_category=item.asset_category, - fieldname="capital_work_in_progress_account", - company=self.company, - ) - if is_cwip_accounting_enabled(d.asset_category) - else get_asset_category_account( - asset_category=item.asset_category, fieldname="fixed_asset_account", company=self.company - ) - ) - - stock_value_diff = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) - elif (flt(item.valuation_rate) or self.is_return) and flt(item.qty): - # If PR is sub-contracted and fg item rate is zero - # in that case if account for source and target warehouse are same, - # then GL entries should not be posted - if ( - flt(stock_value_diff) == flt(d.rm_supp_cost) - and warehouse_account.get(self.supplier_warehouse) - and stock_asset_account_name == supplier_warehouse_account - ): - return - - stock_asset_account_name = warehouse_account[item.warehouse]["account"] - + def make_item_asset_inward_entries(item, stock_value_diff, stock_asset_account_name): account_currency = get_account_currency(stock_asset_account_name) self.add_gl_entry( gl_entries=gl_entries, @@ -402,20 +374,7 @@ class PurchaseReceipt(BuyingController): ) if self.is_internal_transfer() and item.valuation_rate: - outgoing_amount = abs( - frappe.db.get_value( - "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": self.name, - "voucher_detail_no": item.name, - "warehouse": item.from_warehouse, - "is_cancelled": 0, - }, - "stock_value_difference", - ) - ) - credit_amount = outgoing_amount + credit_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse) or 0) if credit_amount: account = ( @@ -583,20 +542,37 @@ class PurchaseReceipt(BuyingController): ) if warehouse_account.get(d.warehouse): - stock_value_diff = frappe.db.get_value( - "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": self.name, - "voucher_detail_no": d.name, - "warehouse": d.warehouse, - "is_cancelled": 0, - }, - "stock_value_difference", - ) - + stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) outgoing_amount = d.base_net_amount + d.item_tax_amount - make_item_asset_inward_entries(d, stock_value_diff) + + if d.is_fixed_asset: + stock_asset_account_name = ( + get_asset_category_account( + asset_category=d.asset_category, + fieldname="capital_work_in_progress_account", + company=self.company, + ) + if is_cwip_accounting_enabled(d.asset_category) + else get_asset_category_account( + asset_category=d.asset_category, fieldname="fixed_asset_account", company=self.company + ) + ) + + stock_value_diff = flt(d.net_amount) + flt(d.item_tax_amount / self.conversion_rate) + elif (flt(d.valuation_rate) or self.is_return) and flt(d.qty): + stock_asset_account_name = warehouse_account[d.warehouse]["account"] + + # If PR is sub-contracted and fg item rate is zero + # in that case if account for source and target warehouse are same, + # then GL entries should not be posted + if ( + flt(stock_value_diff) == flt(d.rm_supp_cost) + and warehouse_account.get(self.supplier_warehouse) + and stock_asset_account_name == supplier_warehouse_account + ): + continue + + make_item_asset_inward_entries(d, stock_value_diff, stock_asset_account_name) make_stock_received_but_not_billed_entry(d, outgoing_amount) make_landed_cost_gl_entries(d) make_rate_difference_entry(d) @@ -745,18 +721,6 @@ class PurchaseReceipt(BuyingController): i += 1 - def get_asset_gl_entry(self, gl_entries): - for item in self.get("items"): - if item.is_fixed_asset: - if is_cwip_accounting_enabled(item.asset_category): - self.add_asset_gl_entries(item, gl_entries) - if flt(item.landed_cost_voucher_amount): - self.add_lcv_gl_entries(item, gl_entries) - # update assets gross amount by its valuation rate - # valuation rate is total of net rate, raw mat supp cost, tax amount, lcv amount per item - - return gl_entries - def update_assets(self, item, valuation_rate): assets = frappe.db.get_all( "Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code} @@ -790,6 +754,20 @@ class PurchaseReceipt(BuyingController): self.load_from_db() +def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse): + frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": voucher_no, + "voucher_detail_no": voucher_detail_no, + "warehouse": warehouse, + "is_cancelled": 0, + }, + "stock_value_difference", + ) + + def update_billed_amount_based_on_po(po_details, update_modified=True): po_billed_amt_details = get_billed_amount_against_po(po_details) diff --git a/erpnext/templates/pages/integrations/__init__.py b/erpnext/templates/pages/integrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 244cec64b2641c50bd6102e6dba65a481d24da0d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 17 Oct 2023 20:37:48 +0530 Subject: [PATCH 056/135] test: report output if party is missing --- .../test_accounts_receivable.py | 115 ++++++++++++++---- 1 file changed, 92 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 4307689158..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( @@ -643,3 +621,94 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): 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) From e15546b42f0bba2e859f447484fc420c31bc51d3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Oct 2023 21:28:59 +0530 Subject: [PATCH 057/135] chore: rearrange functions --- .../purchase_receipt/purchase_receipt.py | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index bd3aeac2e9..72808db873 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -344,10 +344,6 @@ class PurchaseReceipt(BuyingController): ) ) - supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") - supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( - "account_currency" - ) exchange_rate_map, net_rate_map = get_purchase_document_details(self) def make_item_asset_inward_entries(item, stock_value_diff, stock_asset_account_name): @@ -365,22 +361,26 @@ class PurchaseReceipt(BuyingController): ) def make_stock_received_but_not_billed_entry(item, outgoing_amount): + account = ( + warehouse_account[item.from_warehouse]["account"] if item.from_warehouse else stock_asset_rbnb + ) + account_currency = get_account_currency(account) + # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation credit_amount = ( flt(item.base_net_amount, item.precision("base_net_amount")) - if credit_currency == self.company_currency + if account_currency == self.company_currency else flt(item.net_amount, item.precision("net_amount")) ) if self.is_internal_transfer() and item.valuation_rate: - credit_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse) or 0) + outgoing_amount = abs( + get_stock_value_difference(self.name, item.name, item.from_warehouse) or 0 + ) + credit_amount = outgoing_amount if credit_amount: - account = ( - warehouse_account[item.from_warehouse]["account"] if item.from_warehouse else stock_asset_rbnb - ) - self.add_gl_entry( gl_entries=gl_entries, account=account, @@ -390,7 +390,7 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=stock_asset_account_name, debit_in_account_currency=-1 * credit_amount, - account_currency=credit_currency, + account_currency=account_currency, item=item, ) @@ -415,7 +415,7 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, - account_currency=credit_currency, + account_currency=account_currency, item=item, ) @@ -428,7 +428,7 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, - account_currency=credit_currency, + account_currency=account_currency, item=item, ) @@ -519,7 +519,7 @@ class PurchaseReceipt(BuyingController): cost_center = item.cost_center or frappe.get_cached_value( "Company", self.company, "cost_center" ) - + account_currency = get_account_currency(loss_account) self.add_gl_entry( gl_entries=gl_entries, account=loss_account, @@ -528,23 +528,32 @@ class PurchaseReceipt(BuyingController): credit=0.0, remarks=remarks, against_account=stock_asset_account_name, - account_currency=credit_currency, + account_currency=account_currency, project=item.project, item=item, ) for d in self.get("items"): if d.item_code in stock_items or d.is_fixed_asset: - credit_currency = ( - get_account_currency(warehouse_account[d.from_warehouse]["account"]) - if d.from_warehouse - else get_account_currency(stock_asset_rbnb) - ) - if warehouse_account.get(d.warehouse): stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) outgoing_amount = d.base_net_amount + d.item_tax_amount + supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") + supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( + "account_currency" + ) + + # If PR is sub-contracted and fg item rate is zero + # in that case if account for source and target warehouse are same, + # then GL entries should not be posted + if ( + flt(stock_value_diff) == flt(d.rm_supp_cost) + and warehouse_account.get(self.supplier_warehouse) + and stock_asset_account_name == supplier_warehouse_account + ): + continue + if d.is_fixed_asset: stock_asset_account_name = ( get_asset_category_account( @@ -562,16 +571,6 @@ class PurchaseReceipt(BuyingController): elif (flt(d.valuation_rate) or self.is_return) and flt(d.qty): stock_asset_account_name = warehouse_account[d.warehouse]["account"] - # If PR is sub-contracted and fg item rate is zero - # in that case if account for source and target warehouse are same, - # then GL entries should not be posted - if ( - flt(stock_value_diff) == flt(d.rm_supp_cost) - and warehouse_account.get(self.supplier_warehouse) - and stock_asset_account_name == supplier_warehouse_account - ): - continue - make_item_asset_inward_entries(d, stock_value_diff, stock_asset_account_name) make_stock_received_but_not_billed_entry(d, outgoing_amount) make_landed_cost_gl_entries(d) From f4d74990fe1cc2abda56359ce8d09644526c62a6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 17 Oct 2023 22:11:56 +0530 Subject: [PATCH 058/135] fix: E-commerce permissions --- erpnext/accounts/party.py | 32 ++++++++++++++++------- erpnext/controllers/selling_controller.py | 12 +++------ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b99bb83c5b..365aa7f2a3 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -6,11 +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.address.address import get_company_address, get_default_address from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values @@ -133,6 +129,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 +190,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 +204,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 +232,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 +241,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) ) @@ -995,3 +999,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/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index c01ac8115a..d34fbeb0da 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -6,6 +6,7 @@ import frappe from frappe import _, bold, throw 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 @@ -592,12 +593,6 @@ class SellingController(StockController): ) def set_customer_address(self): - try: - from frappe.contacts.doctype.address.address import render_address - except ImportError: - # Older frappe versions where this function is not available - from frappe.contacts.doctype.address.address import get_address_display as render_address - address_dict = { "customer_address": "address_display", "shipping_address_name": "shipping_address", @@ -607,8 +602,9 @@ class SellingController(StockController): for address_field, address_display_field in address_dict.items(): if self.get(address_field): - address = frappe.call(render_address, self.get(address_field), ignore_permissions=True) - self.set(address_display_field, address) + 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 = [], [] From 7f1d916f04e0129136137cd0dc52b142a5ba0f51 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 18 Oct 2023 08:59:28 +0530 Subject: [PATCH 059/135] chore: rearrange functions --- .../purchase_receipt/purchase_receipt.py | 91 +++++++++---------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 72808db873..c2c4d0f539 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -323,7 +323,6 @@ class PurchaseReceipt(BuyingController): is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) stock_asset_rbnb = None - stock_asset_account_name = None remarks = self.get("remarks") or _("Accounting Entry for {0}").format( "Asset" if is_asset_pr else "Stock" ) @@ -360,12 +359,13 @@ class PurchaseReceipt(BuyingController): item=item, ) - def make_stock_received_but_not_billed_entry(item, outgoing_amount): + def make_stock_received_but_not_billed_entry(item): account = ( warehouse_account[item.from_warehouse]["account"] if item.from_warehouse else stock_asset_rbnb ) account_currency = get_account_currency(account) + outgoing_amount = item.base_net_amount # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation credit_amount = ( @@ -375,9 +375,7 @@ class PurchaseReceipt(BuyingController): ) if self.is_internal_transfer() and item.valuation_rate: - outgoing_amount = abs( - get_stock_value_difference(self.name, item.name, item.from_warehouse) or 0 - ) + outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse)) credit_amount = outgoing_amount if credit_amount: @@ -496,7 +494,7 @@ class PurchaseReceipt(BuyingController): # divisional loss adjustment expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") valuation_amount_as_per_doc = ( - flt(outgoing_amount, d.precision("base_net_amount")) + flt(item.base_net_amount, d.precision("base_net_amount")) + flt(item.landed_cost_voucher_amount) + flt(item.rm_supp_cost) + flt(item.item_tax_amount) @@ -534,11 +532,37 @@ class PurchaseReceipt(BuyingController): ) for d in self.get("items"): - if d.item_code in stock_items or d.is_fixed_asset: - if warehouse_account.get(d.warehouse): - stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) - outgoing_amount = d.base_net_amount + d.item_tax_amount + if ( + d.item_code not in stock_items + and flt(d.qty) + and provisional_accounting_for_non_stock_items + and d.get("provisional_expense_account") + ): + self.add_provisional_gl_entry( + d, gl_entries, self.posting_date, d.get("provisional_expense_account") + ) + elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return): + if d.is_fixed_asset: + stock_asset_account_name = ( + get_asset_category_account( + asset_category=d.asset_category, + fieldname="capital_work_in_progress_account", + company=self.company, + ) + if is_cwip_accounting_enabled(d.asset_category) + else get_asset_category_account( + asset_category=d.asset_category, fieldname="fixed_asset_account", company=self.company + ) + ) + stock_value_diff = flt(d.net_amount) + flt(d.item_tax_amount / self.conversion_rate) + elif ( + (flt(d.valuation_rate) or self.is_return) + and flt(d.qty) + and warehouse_account.get(d.warehouse) + ): + stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) + stock_asset_account_name = warehouse_account[d.warehouse]["account"] supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( "account_currency" @@ -554,44 +578,17 @@ class PurchaseReceipt(BuyingController): ): continue - if d.is_fixed_asset: - stock_asset_account_name = ( - get_asset_category_account( - asset_category=d.asset_category, - fieldname="capital_work_in_progress_account", - company=self.company, - ) - if is_cwip_accounting_enabled(d.asset_category) - else get_asset_category_account( - asset_category=d.asset_category, fieldname="fixed_asset_account", company=self.company - ) - ) - - stock_value_diff = flt(d.net_amount) + flt(d.item_tax_amount / self.conversion_rate) - elif (flt(d.valuation_rate) or self.is_return) and flt(d.qty): - stock_asset_account_name = warehouse_account[d.warehouse]["account"] - - make_item_asset_inward_entries(d, stock_value_diff, stock_asset_account_name) - make_stock_received_but_not_billed_entry(d, outgoing_amount) - make_landed_cost_gl_entries(d) - make_rate_difference_entry(d) - make_sub_contracting_gl_entries(d) - make_divisional_loss_gl_entry(d) - elif ( - d.warehouse not in warehouse_with_no_account - or d.rejected_warehouse not in warehouse_with_no_account - ): - warehouse_with_no_account.append(d.warehouse) + make_item_asset_inward_entries(d, stock_value_diff, stock_asset_account_name) + make_stock_received_but_not_billed_entry(d) + make_landed_cost_gl_entries(d) + make_rate_difference_entry(d) + make_sub_contracting_gl_entries(d) + make_divisional_loss_gl_entry(d) elif ( - d.item_code not in stock_items - and not d.is_fixed_asset - and flt(d.qty) - and provisional_accounting_for_non_stock_items - and d.get("provisional_expense_account") + d.warehouse not in warehouse_with_no_account + or d.rejected_warehouse not in warehouse_with_no_account ): - self.add_provisional_gl_entry( - d, gl_entries, self.posting_date, d.get("provisional_expense_account") - ) + warehouse_with_no_account.append(d.warehouse) if d.is_fixed_asset: self.update_assets(d, d.valuation_rate) @@ -754,7 +751,7 @@ class PurchaseReceipt(BuyingController): def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse): - frappe.db.get_value( + return frappe.db.get_value( "Stock Ledger Entry", { "voucher_type": "Purchase Receipt", From 1a2f659de2a06bea513ced0a5b8ff007ebec6437 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 18 Oct 2023 11:42:19 +0530 Subject: [PATCH 060/135] fix: filter tax template based on company --- erpnext/accounts/doctype/subscription/subscription.js | 8 ++++++++ 1 file changed, 8 insertions(+) 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) { From 36a996d70499a8bc4cf9c28853c2b5606d73f81d Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 18 Oct 2023 12:01:22 +0530 Subject: [PATCH 061/135] chore: make `Reserve Stock` checkbox visible in SO --- erpnext/selling/doctype/sales_order/sales_order.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index a74084d21f..01d047cead 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1631,10 +1631,9 @@ { "default": "0", "depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)", - "description": "If checked, Stock Reservation Entries will be created on Submit", + "description": "If checked, Stock will be reserved on Submit", "fieldname": "reserve_stock", "fieldtype": "Check", - "hidden": 1, "label": "Reserve Stock", "no_copy": 1, "print_hide": 1, @@ -1645,7 +1644,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-07-24 08:59:11.599875", + "modified": "2023-10-18 12:41:54.813462", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", From 2851a41310a050afee753c8ac396eb808d0d123c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 18 Oct 2023 16:31:35 +0530 Subject: [PATCH 062/135] fix: Issues related to RFQ and Supplier Quotation on Portal (#37565) fix: RFQ and Supplier Quotation for Portal --- erpnext/accounts/party.py | 17 +++- erpnext/templates/includes/rfq.js | 2 + .../templates/includes/rfq/rfq_macros.html | 24 ++++-- erpnext/templates/pages/order.html | 86 ++++++------------- erpnext/templates/pages/rfq.html | 4 +- 5 files changed, 59 insertions(+), 74 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 365aa7f2a3..310e41208f 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -7,7 +7,6 @@ from typing import Optional import frappe from frappe import _, msgprint, scrub from frappe.contacts.doctype.address.address import get_company_address, get_default_address -from frappe.contacts.doctype.contact.contact import get_contact_details 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 @@ -294,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): diff --git a/erpnext/templates/includes/rfq.js b/erpnext/templates/includes/rfq.js index 37beb5a584..ed0f1b1ff3 100644 --- a/erpnext/templates/includes/rfq.js +++ b/erpnext/templates/includes/rfq.js @@ -73,6 +73,7 @@ rfq = class rfq { submit_rfq(){ $('.btn-sm').click(function(){ + debugger frappe.freeze(); frappe.call({ type: "POST", @@ -82,6 +83,7 @@ rfq = class rfq { }, btn: this, callback: function(r){ + debugger frappe.unfreeze(); if(r.message){ $('.btn-sm').hide() diff --git a/erpnext/templates/includes/rfq/rfq_macros.html b/erpnext/templates/includes/rfq/rfq_macros.html index 88724c30de..78ec6ff5f8 100644 --- a/erpnext/templates/includes/rfq/rfq_macros.html +++ b/erpnext/templates/includes/rfq/rfq_macros.html @@ -1,19 +1,25 @@ {% from "erpnext/templates/includes/macros.html" import product_image_square, product_image %} {% macro item_name_and_description(d, doc) %} -
-
- {{ product_image(d.image) }} -
-
- {{ d.item_code }} -

{{ d.description }}

+
+
+ {% if d.image %} + {{ product_image(d.image) }} + {% else %} +
+ {{ frappe.utils.get_abbr(d.item_name)}} +
+ {% endif %} +
+
+ {{ d.item_code }} +

{{ 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 %}

-
-
+
+
{% endmacro %} 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 @@
- {% if show_pay_button %} - - {% endif %}
{% endblock %} @@ -130,42 +118,6 @@
- {% if enabled_checkout and ((doc.doctype=="Sales Order" and doc.per_billed <= 0) - or (doc.doctype=="Sales Invoice" and doc.outstanding_amount> 0)) %} -
-
-
-
-
- {% if available_loyalty_points %} -
-
-
- Loyalty Points -
-
-
- -
-
Enter Loyalty Points
-
-
- -
-

Available Points: {{ - available_loyalty_points }}

-
-
- {% endif %} -
-
-
-
-
- {% endif %} - - {% if attachments %}
@@ -193,15 +145,27 @@ {% endif %} {% endblock %} -{% block script %} - - -{% endblock %} \ No newline at end of file +{% macro item_name_and_description(d) %} +
+
+
+ {% if d.thumbnail or d.image %} + {{ product_image(d.thumbnail or d.image, no_border=True) }} + {% else %} +
+ {{ frappe.utils.get_abbr(d.item_name) or "NA" }} +
+ {% endif %} +
+
+
+ {{ d.item_code }} +
+ {{ html2text(d.description) | truncate(140) }} +
+ + {{ _("Qty ") }}({{ d.get_formatted("qty") }}) + +
+
+{% endmacro %} \ No newline at end of file diff --git a/erpnext/templates/pages/rfq.html b/erpnext/templates/pages/rfq.html index 6516482c23..d371bf2161 100644 --- a/erpnext/templates/pages/rfq.html +++ b/erpnext/templates/pages/rfq.html @@ -1,7 +1,7 @@ {% extends "templates/web.html" %} {% block header %} -

{{ doc.name }}

+

{{ doc.name }}

{% endblock %} {% block script %} @@ -16,7 +16,7 @@ {% if doc.items %} + {{ _("Make Quotation") }} {% endif %} {% endblock %} From 10311ff114b7513b47df9be993fa568d6e586f1d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 18 Oct 2023 17:42:35 +0530 Subject: [PATCH 063/135] fix: payment entry count on supplier dashboard (#37571) --- erpnext/buying/doctype/supplier/supplier_dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"]}, From d09618bf05ef1c9ef6d1c4fd9605aef19190aa84 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 19 Oct 2023 12:35:36 +0530 Subject: [PATCH 064/135] chore: remove debugger (#37584) --- erpnext/templates/includes/rfq.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/templates/includes/rfq.js b/erpnext/templates/includes/rfq.js index ed0f1b1ff3..37beb5a584 100644 --- a/erpnext/templates/includes/rfq.js +++ b/erpnext/templates/includes/rfq.js @@ -73,7 +73,6 @@ rfq = class rfq { submit_rfq(){ $('.btn-sm').click(function(){ - debugger frappe.freeze(); frappe.call({ type: "POST", @@ -83,7 +82,6 @@ rfq = class rfq { }, btn: this, callback: function(r){ - debugger frappe.unfreeze(); if(r.message){ $('.btn-sm').hide() From 2b4fa98941817966b8a936e4076be84406ad035a Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 18 Oct 2023 12:38:42 +0530 Subject: [PATCH 065/135] refactor: rename field `Auto Reserve Stock for Sales Order` --- .../doctype/sales_order/sales_order.js | 12 +++----- .../stock_settings/stock_settings.json | 29 +++++++++---------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index ba8bc339f3..3ad18daf19 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -87,17 +87,13 @@ frappe.ui.form.on("Sales Order", { frm.events.get_items_from_internal_purchase_order(frm); } - if (frm.is_new()) { + if (frm.doc.docstatus === 0) { frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => { - if (value) { - frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order").then((value) => { - // If `Reserve Stock on Sales Order Submission` is enabled in Stock Settings, set Reserve Stock to 1 else 0. - frm.set_value("reserve_stock", value ? 1 : 0); - }) - } else { - // If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and read only. + if (!value) { + // If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and make the field read-only and hidden. frm.set_value("reserve_stock", 0); frm.set_df_property("reserve_stock", "read_only", 1); + frm.set_df_property("reserve_stock", "hidden", 1); } }) } diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 2052daafed..122829032d 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -38,8 +38,8 @@ "stock_reservation_tab", "enable_stock_reservation", "column_break_rx3e", - "auto_reserve_stock_for_sales_order", "allow_partial_reservation", + "auto_reserve_stock_for_sales_order_on_purchase", "serial_and_batch_reservation_section", "auto_reserve_serial_and_batch", "serial_and_batch_item_settings_tab", @@ -65,8 +65,7 @@ "stock_frozen_upto_days", "column_break_26", "role_allowed_to_create_edit_back_dated_transactions", - "stock_auth_role", - "section_break_plhx" + "stock_auth_role" ], "fields": [ { @@ -356,7 +355,7 @@ { "default": "1", "depends_on": "eval: doc.enable_stock_reservation", - "description": "If enabled, Partial Stock Reservation Entries can be created. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ", + "description": "Partial stock can be reserved. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ", "fieldname": "allow_partial_reservation", "fieldtype": "Check", "label": "Allow Partial Reservation" @@ -383,7 +382,7 @@ { "default": "1", "depends_on": "eval: doc.enable_stock_reservation", - "description": "If enabled, Serial and Batch Nos will be auto-reserved based on Pick Serial / Batch Based On", + "description": "Serial and Batch Nos will be auto-reserved based on Pick Serial / Batch Based On", "fieldname": "auto_reserve_serial_and_batch", "fieldtype": "Check", "label": "Auto Reserve Serial and Batch Nos" @@ -393,14 +392,6 @@ "fieldtype": "Section Break", "label": "Serial and Batch Reservation" }, - { - "default": "0", - "depends_on": "eval: doc.enable_stock_reservation", - "description": "If enabled, Stock Reservation Entries will be created on submission of Sales Order", - "fieldname": "auto_reserve_stock_for_sales_order", - "fieldtype": "Check", - "label": "Auto Reserve Stock for Sales Order" - }, { "fieldname": "conversion_factor_section", "fieldtype": "Section Break", @@ -421,6 +412,14 @@ "fieldname": "allow_to_edit_stock_uom_qty_for_purchase", "fieldtype": "Check", "label": "Allow to Edit Stock UOM Qty for Purchase Documents" + }, + { + "default": "0", + "depends_on": "eval: doc.enable_stock_reservation", + "description": "Stock will be reserved on submission of Purchase Receipt created against Material Receipt for Sales Order.", + "fieldname": "auto_reserve_stock_for_sales_order_on_purchase", + "fieldtype": "Check", + "label": "Auto Reserve Stock for Sales Order on Purchase" } ], "icon": "icon-cog", @@ -428,7 +427,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-01 14:22:36.136111", + "modified": "2023-10-18 12:35:30.068799", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", @@ -453,4 +452,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} +} \ No newline at end of file From 188175be84b5eaa0be73face69f4f2b58b80dd64 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 19 Oct 2023 14:43:29 +0530 Subject: [PATCH 066/135] feat: add fields to hold SO and SO Item ref in PR Item --- .../doctype/purchase_order/purchase_order.py | 2 ++ .../purchase_receipt_item.json | 28 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 465fe96b58..7c40aafbe0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -556,6 +556,8 @@ def make_purchase_receipt(source_name, target_doc=None): "bom": "bom", "material_request": "material_request", "material_request_item": "material_request_item", + "sales_order": "sales_order", + "sales_order_item": "sales_order_item", }, "postprocess": update_item, "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index d93d21c1f2..f5240a6094 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -125,7 +125,9 @@ "dimension_col_break", "cost_center", "section_break_80", - "page_break" + "page_break", + "sales_order", + "sales_order_item" ], "fields": [ { @@ -1062,12 +1064,32 @@ "fieldtype": "Link", "label": "WIP Composite Asset", "options": "Asset" + }, + { + "fieldname": "sales_order", + "fieldtype": "Link", + "label": "Sales Order", + "no_copy": 1, + "options": "Sales Order", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "sales_order_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Sales Order Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "search_index": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-10-03 21:11:50.547261", + "modified": "2023-10-19 10:50:58.071735", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", @@ -1078,4 +1100,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file From 64497c922892d19ca378b5da75cf6b528f5e52d9 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 19 Oct 2023 14:48:13 +0530 Subject: [PATCH 067/135] feat: reserve stock for SO on PR submission --- .../doctype/sales_order/sales_order.py | 2 +- erpnext/stock/doctype/pick_list/pick_list.py | 2 +- .../purchase_receipt/purchase_receipt.py | 26 +++++++ .../stock_reservation_entry.py | 67 ++++++++++--------- 4 files changed, 62 insertions(+), 35 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b91002eb86..d38216242e 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -541,7 +541,7 @@ class SalesOrder(SellingController): create_stock_reservation_entries_for_so_items as create_stock_reservation_entries, ) - create_stock_reservation_entries(so=self, items_details=items_details, notify=notify) + create_stock_reservation_entries(sales_order=self, items_details=items_details, notify=notify) @frappe.whitelist() def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None: diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 2fcd1025a0..8c9d03c1bd 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -242,7 +242,7 @@ class PickList(Document): for so, locations in so_details.items(): so_doc = frappe.get_doc("Sales Order", so) create_stock_reservation_entries_for_so_items( - so=so_doc, items_details=locations, against_pick_list=True, notify=notify + sales_order=so_doc, items_details=locations, against_pick_list=True, notify=notify ) @frappe.whitelist() diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index de0db1aa8f..fc88dd8d5f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -264,6 +264,7 @@ class PurchaseReceipt(BuyingController): self.make_gl_entries() self.repost_future_sle_and_gle() self.set_consumed_qty_in_subcontract_order() + self.reserve_stock_for_sales_order() def check_next_docstatus(self): submit_rv = frappe.db.sql( @@ -829,6 +830,31 @@ class PurchaseReceipt(BuyingController): self.load_from_db() + def reserve_stock_for_sales_order(self): + if self.is_return or not cint( + frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase") + ): + return + + self.reload() # reload to get the Serial and Batch Bundle Details + + so_items_details_map = {} + for item in self.items: + if item.sales_order and item.sales_order_item: + item_details = { + "name": item.sales_order_item, + "item_code": item.item_code, + "warehouse": item.warehouse, + "qty_to_reserve": item.stock_qty, + "serial_and_batch_bundle": item.get("serial_and_batch_bundle"), + } + so_items_details_map.setdefault(item.sales_order, []).append(item_details) + + if so_items_details_map: + for so, items_details in so_items_details_map.items(): + so_doc = frappe.get_doc("Sales Order", so) + so_doc.create_stock_reservation_entries(items_details) + def update_billed_amount_based_on_po(po_details, update_modified=True): po_billed_amt_details = get_billed_amount_against_po(po_details) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 936be3f73b..effad7df98 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -761,7 +761,7 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st def create_stock_reservation_entries_for_so_items( - so: object, + sales_order: object, items_details: list[dict] = None, against_pick_list: bool = False, notify=True, @@ -771,15 +771,17 @@ def create_stock_reservation_entries_for_so_items( from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty if not against_pick_list and ( - so.get("_action") == "submit" - and so.set_warehouse - and cint(frappe.get_cached_value("Warehouse", so.set_warehouse, "is_group")) + sales_order.get("_action") == "submit" + and sales_order.set_warehouse + and cint(frappe.get_cached_value("Warehouse", sales_order.set_warehouse, "is_group")) ): return frappe.msgprint( - _("Stock cannot be reserved in the group warehouse {0}.").format(frappe.bold(so.set_warehouse)) + _("Stock cannot be reserved in the group warehouse {0}.").format( + frappe.bold(sales_order.set_warehouse) + ) ) - validate_stock_reservation_settings(so) + validate_stock_reservation_settings(sales_order) allow_partial_reservation = frappe.db.get_single_value( "Stock Settings", "allow_partial_reservation" @@ -787,29 +789,28 @@ def create_stock_reservation_entries_for_so_items( items = [] if items_details: + item_field = "sales_order_item" if against_pick_list else "name" + for item in items_details: - so_item = frappe.get_doc( - "Sales Order Item", item.get("sales_order_item") if against_pick_list else item.get("name") - ) - so_item.reserve_stock = 1 + so_item = frappe.get_doc("Sales Order Item", item.get(item_field)) so_item.warehouse = item.get("warehouse") so_item.qty_to_reserve = ( item.get("picked_qty") - item.get("stock_reserved_qty", 0) if against_pick_list else (flt(item.get("qty_to_reserve")) * flt(so_item.conversion_factor, 1)) ) + so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle") if against_pick_list: so_item.pick_list = item.get("parent") so_item.pick_list_item = item.get("name") - so_item.pick_list_sbb = item.get("serial_and_batch_bundle") items.append(so_item) sre_count = 0 - reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", so.name) + reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", sales_order.name) - for item in items if items_details else so.get("items"): + for item in items if items_details else sales_order.get("items"): # Skip if `Reserved Stock` is not checked for the item. if not item.get("reserve_stock"): continue @@ -817,9 +818,9 @@ def create_stock_reservation_entries_for_so_items( # Stock should be reserved from the Pick List if has Picked Qty. if not against_pick_list and flt(item.picked_qty) > 0: frappe.throw( - _( - "Row #{0}: Item {1} has been picked, please create a Stock Reservation from the Pick List." - ).format(item.idx, frappe.bold(item.item_code)) + _("Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.").format( + item.idx, frappe.bold(item.item_code) + ) ) is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value( @@ -915,33 +916,33 @@ def create_stock_reservation_entries_for_so_items( sre.warehouse = item.warehouse sre.has_serial_no = has_serial_no sre.has_batch_no = has_batch_no - sre.voucher_type = so.doctype - sre.voucher_no = so.name + sre.voucher_type = sales_order.doctype + sre.voucher_no = sales_order.name sre.voucher_detail_no = item.name sre.available_qty = available_qty_to_reserve sre.voucher_qty = item.stock_qty sre.reserved_qty = qty_to_be_reserved - sre.company = so.company + sre.company = sales_order.company sre.stock_uom = item.stock_uom - sre.project = so.project + sre.project = sales_order.project if against_pick_list: sre.against_pick_list = item.pick_list sre.against_pick_list_item = item.pick_list_item - if item.pick_list_sbb: - sbb = frappe.get_doc("Serial and Batch Bundle", item.pick_list_sbb) - sre.reservation_based_on = "Serial and Batch" - for entry in sbb.entries: - sre.append( - "sb_entries", - { - "serial_no": entry.serial_no, - "batch_no": entry.batch_no, - "qty": 1 if has_serial_no else abs(entry.qty), - "warehouse": entry.warehouse, - }, - ) + if item.serial_and_batch_bundle: + sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) + sre.reservation_based_on = "Serial and Batch" + for entry in sbb.entries: + sre.append( + "sb_entries", + { + "serial_no": entry.serial_no, + "batch_no": entry.batch_no, + "qty": 1 if has_serial_no else abs(entry.qty), + "warehouse": entry.warehouse, + }, + ) sre.save() sre.submit() From 40cdde88202b62464284477bed1589c8d51f0a1c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 19 Oct 2023 15:52:53 +0530 Subject: [PATCH 068/135] ci: seutp v15 config https://github.com/frappe/frappe/issues/22817 --- .github/workflows/patch.yml | 1 + .mergify.yml | 46 ++++--------------------------------- 2 files changed, 5 insertions(+), 42 deletions(-) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 21dd3d4879..3514f0d2d9 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -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/.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: From 5ae9c2f62b741d64da4ce74b3b251339711e8256 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 19 Oct 2023 16:21:32 +0530 Subject: [PATCH 069/135] feat: add field `From Voucher Type` in SRE --- .../stock_reservation_entry.json | 21 ++++++++++++++----- .../stock_reservation_entry.py | 1 + 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json index 5c3018f734..f92bf49137 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json @@ -17,6 +17,7 @@ "voucher_no", "voucher_detail_no", "column_break_7dxj", + "from_voucher_type", "against_pick_list", "against_pick_list_item", "section_break_xt4m", @@ -272,10 +273,10 @@ }, { "fieldname": "against_pick_list", - "fieldtype": "Link", - "label": "Against Pick List", + "fieldtype": "Dynamic Link", + "label": "From Voucher No", "no_copy": 1, - "options": "Pick List", + "options": "from_voucher_type", "print_hide": 1, "read_only": 1, "report_hide": 1, @@ -284,7 +285,7 @@ { "fieldname": "against_pick_list_item", "fieldtype": "Data", - "label": "Against Pick List Item", + "label": "From Voucher Detail No", "no_copy": 1, "print_hide": 1, "read_only": 1, @@ -297,6 +298,16 @@ { "fieldname": "column_break_grdt", "fieldtype": "Column Break" + }, + { + "fieldname": "from_voucher_type", + "fieldtype": "Select", + "label": "From Voucher Type", + "no_copy": 1, + "options": "\nPick List\nPurchase Receipt", + "print_hide": 1, + "read_only": 1, + "report_hide": 1 } ], "hide_toolbar": 1, @@ -304,7 +315,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-08 17:15:13.317706", + "modified": "2023-10-19 16:09:08.418544", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reservation Entry", diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index effad7df98..80f4c1e505 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -927,6 +927,7 @@ def create_stock_reservation_entries_for_so_items( sre.project = sales_order.project if against_pick_list: + sre.from_voucher_type = "Pick List" sre.against_pick_list = item.pick_list sre.against_pick_list_item = item.pick_list_item From 78fe56741931ad2c253bf9acca2896c2411f4ac6 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 19 Oct 2023 16:38:43 +0530 Subject: [PATCH 070/135] refactor: rename field `against_pick_list_item` --- .../stock_reservation_entry.json | 22 +++++++++---------- .../stock_reservation_entry.py | 13 ++++++----- .../test_stock_reservation_entry.py | 3 ++- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json index f92bf49137..1a518fa38a 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json @@ -19,7 +19,7 @@ "column_break_7dxj", "from_voucher_type", "against_pick_list", - "against_pick_list_item", + "from_voucher_detail_no", "section_break_xt4m", "stock_uom", "column_break_grdt", @@ -282,15 +282,6 @@ "report_hide": 1, "search_index": 1 }, - { - "fieldname": "against_pick_list_item", - "fieldtype": "Data", - "label": "From Voucher Detail No", - "no_copy": 1, - "print_hide": 1, - "read_only": 1, - "report_hide": 1 - }, { "fieldname": "column_break_7dxj", "fieldtype": "Column Break" @@ -308,6 +299,15 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "fieldname": "from_voucher_detail_no", + "fieldtype": "Data", + "label": "From Voucher Detail No", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "report_hide": 1 } ], "hide_toolbar": 1, @@ -315,7 +315,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-19 16:09:08.418544", + "modified": "2023-10-19 16:26:46.598043", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reservation Entry", diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 80f4c1e505..66e246a86a 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -316,21 +316,24 @@ class StockReservationEntry(Document): ) -> None: """Updates total reserved qty in the Pick List.""" - if self.against_pick_list and self.against_pick_list_item: + if ( + self.from_voucher_type == "Pick List" and self.against_pick_list and self.from_voucher_detail_no + ): sre = frappe.qb.DocType("Stock Reservation Entry") reserved_qty = ( frappe.qb.from_(sre) .select(Sum(sre.reserved_qty)) .where( (sre.docstatus == 1) + & (sre.from_voucher_type == "Pick List") & (sre.against_pick_list == self.against_pick_list) - & (sre.against_pick_list_item == self.against_pick_list_item) + & (sre.from_voucher_detail_no == self.from_voucher_detail_no) ) ).run(as_list=True)[0][0] or 0 frappe.db.set_value( "Pick List Item", - self.against_pick_list_item, + self.from_voucher_detail_no, reserved_qty_field, reserved_qty, update_modified=update_modified, @@ -803,7 +806,7 @@ def create_stock_reservation_entries_for_so_items( if against_pick_list: so_item.pick_list = item.get("parent") - so_item.pick_list_item = item.get("name") + so_item.from_voucher_detail_no = item.get("name") items.append(so_item) @@ -929,7 +932,7 @@ def create_stock_reservation_entries_for_so_items( if against_pick_list: sre.from_voucher_type = "Pick List" sre.against_pick_list = item.pick_list - sre.against_pick_list_item = item.pick_list_item + sre.from_voucher_detail_no = item.from_voucher_detail_no if item.serial_and_batch_bundle: sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index 1168a4e1c6..27f43bf668 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -555,8 +555,9 @@ class TestStockReservationEntry(FrappeTestCase): (sre.voucher_type == "Sales Order") & (sre.voucher_no == location.sales_order) & (sre.voucher_detail_no == location.sales_order_item) + & (sre.from_voucher_type == "Pick List") & (sre.against_pick_list == pl.name) - & (sre.against_pick_list_item == location.name) + & (sre.from_voucher_detail_no == location.name) ) ).run(as_dict=True) reserved_sb_details: set[tuple] = { From 961d2d9926a1a9c0396c3292d431d3bad6865e62 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 19 Oct 2023 17:51:34 +0530 Subject: [PATCH 071/135] refactor: rename field `against_pick_list` --- erpnext/stock/doctype/pick_list/pick_list.js | 3 +- erpnext/stock/doctype/pick_list/pick_list.py | 6 +- .../doctype/pick_list/pick_list_dashboard.py | 2 +- .../stock_reservation_entry.js | 2 +- .../stock_reservation_entry.json | 30 ++++---- .../stock_reservation_entry.py | 70 +++++++++++-------- .../test_stock_reservation_entry.py | 2 +- .../report/reserved_stock/reserved_stock.js | 20 +++++- .../report/reserved_stock/reserved_stock.py | 23 +++--- 9 files changed, 96 insertions(+), 62 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index ae05b80727..7cd171ea92 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -265,7 +265,8 @@ frappe.ui.form.on('Pick List', { from_date: moment(frm.doc.creation).format('YYYY-MM-DD'), to_date: to_date, voucher_type: "Sales Order", - against_pick_list: frm.doc.name, + from_voucher_type: "Pick List", + from_voucher_no: frm.doc.name, } frappe.set_route("query-report", "Reserved Stock"); } diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 8c9d03c1bd..3503556f3e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -242,7 +242,7 @@ class PickList(Document): for so, locations in so_details.items(): so_doc = frappe.get_doc("Sales Order", so) create_stock_reservation_entries_for_so_items( - sales_order=so_doc, items_details=locations, against_pick_list=True, notify=notify + sales_order=so_doc, items_details=locations, from_voucher_type="Pick List", notify=notify ) @frappe.whitelist() @@ -253,7 +253,9 @@ class PickList(Document): cancel_stock_reservation_entries, ) - cancel_stock_reservation_entries(against_pick_list=self.name, notify=notify) + cancel_stock_reservation_entries( + from_voucher_type="Pick List", from_voucher_no=self.name, notify=notify + ) def validate_picked_qty(self, data): over_delivery_receipt_allowance = 100 + flt( diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py index 0830fa2143..29571a5400 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py +++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py @@ -2,7 +2,7 @@ def get_data(): return { "fieldname": "pick_list", "non_standard_fieldnames": { - "Stock Reservation Entry": "against_pick_list", + "Stock Reservation Entry": "from_voucher_no", }, "internal_links": { "Sales Order": ["locations", "sales_order"], diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js index c5df319e22..f60a0378b6 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js @@ -92,7 +92,7 @@ frappe.ui.form.on('Stock Reservation Entry', { 'qty', 'read_only', frm.doc.has_serial_no ); - frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.against_pick_list ? 0 : 1); + frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.from_voucher_type == "Pick List" ? 0 : 1); }, hide_rate_related_fields(frm) { diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json index 1a518fa38a..76cedd4b1e 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json @@ -18,7 +18,7 @@ "voucher_detail_no", "column_break_7dxj", "from_voucher_type", - "against_pick_list", + "from_voucher_no", "from_voucher_detail_no", "section_break_xt4m", "stock_uom", @@ -159,7 +159,7 @@ "oldfieldname": "actual_qty", "oldfieldtype": "Currency", "print_width": "150px", - "read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.against_pick_list) || (doc.delivered_qty > 0))", + "read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.from_voucher_type == \"Pick List\") || (doc.delivered_qty > 0))", "width": "150px" }, { @@ -269,18 +269,7 @@ "label": "Reservation Based On", "no_copy": 1, "options": "Qty\nSerial and Batch", - "read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.against_pick_list)" - }, - { - "fieldname": "against_pick_list", - "fieldtype": "Dynamic Link", - "label": "From Voucher No", - "no_copy": 1, - "options": "from_voucher_type", - "print_hide": 1, - "read_only": 1, - "report_hide": 1, - "search_index": 1 + "read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.from_voucher_type == \"Pick List\")" }, { "fieldname": "column_break_7dxj", @@ -308,6 +297,17 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "fieldname": "from_voucher_no", + "fieldtype": "Dynamic Link", + "label": "From Voucher No", + "no_copy": 1, + "options": "from_voucher_type", + "print_hide": 1, + "read_only": 1, + "report_hide": 1, + "search_index": 1 } ], "hide_toolbar": 1, @@ -315,7 +315,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-19 16:26:46.598043", + "modified": "2023-10-19 16:41:16.545416", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reservation Entry", diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 66e246a86a..9097e621e6 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -1,6 +1,8 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from typing import Literal + import frappe from frappe import _ from frappe.model.document import Document @@ -113,7 +115,7 @@ class StockReservationEntry(Document): """Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`.""" if ( - not self.against_pick_list + not self.from_voucher_type and (self.get("_action") == "submit") and (self.has_serial_no or self.has_batch_no) and cint(frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch")) @@ -317,7 +319,7 @@ class StockReservationEntry(Document): """Updates total reserved qty in the Pick List.""" if ( - self.from_voucher_type == "Pick List" and self.against_pick_list and self.from_voucher_detail_no + self.from_voucher_type == "Pick List" and self.from_voucher_no and self.from_voucher_detail_no ): sre = frappe.qb.DocType("Stock Reservation Entry") reserved_qty = ( @@ -326,7 +328,7 @@ class StockReservationEntry(Document): .where( (sre.docstatus == 1) & (sre.from_voucher_type == "Pick List") - & (sre.against_pick_list == self.against_pick_list) + & (sre.from_voucher_no == self.from_voucher_no) & (sre.from_voucher_detail_no == self.from_voucher_detail_no) ) ).run(as_list=True)[0][0] or 0 @@ -368,7 +370,7 @@ class StockReservationEntry(Document): ).format(self.status, self.doctype) frappe.throw(msg) - if self.against_pick_list: + if self.from_voucher_type == "Pick List": msg = _( "Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one." ) @@ -766,14 +768,14 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st def create_stock_reservation_entries_for_so_items( sales_order: object, items_details: list[dict] = None, - against_pick_list: bool = False, + from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, notify=True, ) -> None: """Creates Stock Reservation Entries for Sales Order Items.""" from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty - if not against_pick_list and ( + if not from_voucher_type and ( sales_order.get("_action") == "submit" and sales_order.set_warehouse and cint(frappe.get_cached_value("Warehouse", sales_order.set_warehouse, "is_group")) @@ -792,20 +794,20 @@ def create_stock_reservation_entries_for_so_items( items = [] if items_details: - item_field = "sales_order_item" if against_pick_list else "name" + item_field = "sales_order_item" if from_voucher_type == "Pick List" else "name" for item in items_details: so_item = frappe.get_doc("Sales Order Item", item.get(item_field)) so_item.warehouse = item.get("warehouse") so_item.qty_to_reserve = ( item.get("picked_qty") - item.get("stock_reserved_qty", 0) - if against_pick_list + if from_voucher_type == "Pick List" else (flt(item.get("qty_to_reserve")) * flt(so_item.conversion_factor, 1)) ) so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle") - if against_pick_list: - so_item.pick_list = item.get("parent") + if from_voucher_type == "Pick List": + so_item.from_voucher_no = item.get("parent") so_item.from_voucher_detail_no = item.get("name") items.append(so_item) @@ -819,7 +821,7 @@ def create_stock_reservation_entries_for_so_items( continue # Stock should be reserved from the Pick List if has Picked Qty. - if not against_pick_list and flt(item.picked_qty) > 0: + if not from_voucher_type == "Pick List" and flt(item.picked_qty) > 0: frappe.throw( _("Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.").format( item.idx, frappe.bold(item.item_code) @@ -929,9 +931,9 @@ def create_stock_reservation_entries_for_so_items( sre.stock_uom = item.stock_uom sre.project = sales_order.project - if against_pick_list: - sre.from_voucher_type = "Pick List" - sre.against_pick_list = item.pick_list + if from_voucher_type: + sre.from_voucher_type = from_voucher_type + sre.from_voucher_no = item.from_voucher_no sre.from_voucher_detail_no = item.from_voucher_detail_no if item.serial_and_batch_bundle: @@ -961,29 +963,37 @@ def cancel_stock_reservation_entries( voucher_type: str = None, voucher_no: str = None, voucher_detail_no: str = None, - against_pick_list: str = None, + from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, + from_voucher_no: str = None, + from_voucher_detail_no: str = None, sre_list: list[dict] = None, notify: bool = True, ) -> None: """Cancel Stock Reservation Entries.""" - if not sre_list and against_pick_list: - sre = frappe.qb.DocType("Stock Reservation Entry") - sre_list = ( - frappe.qb.from_(sre) - .select(sre.name) - .where( - (sre.docstatus == 1) - & (sre.against_pick_list == against_pick_list) - & (sre.status.notin(["Delivered", "Cancelled"])) + if not sre_list: + if voucher_type and voucher_no: + sre_list = get_stock_reservation_entries_for_voucher( + voucher_type, voucher_no, voucher_detail_no, fields=["name"] + ) + elif from_voucher_type and from_voucher_no: + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select(sre.name) + .where( + (sre.docstatus == 1) + & (sre.from_voucher_type == from_voucher_type) + & (sre.from_voucher_no == from_voucher_no) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .orderby(sre.creation) ) - .orderby(sre.creation) - ).run(as_dict=True) - elif not sre_list and (voucher_type and voucher_no): - sre_list = get_stock_reservation_entries_for_voucher( - voucher_type, voucher_no, voucher_detail_no, fields=["name"] - ) + if from_voucher_detail_no: + query = query.where(sre.from_voucher_detail_no == from_voucher_detail_no) + + sre_list = query.run(as_dict=True) if sre_list: for sre in sre_list: diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index 27f43bf668..9ea35ecacb 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -556,7 +556,7 @@ class TestStockReservationEntry(FrappeTestCase): & (sre.voucher_no == location.sales_order) & (sre.voucher_detail_no == location.sales_order_item) & (sre.from_voucher_type == "Pick List") - & (sre.against_pick_list == pl.name) + & (sre.from_voucher_no == pl.name) & (sre.from_voucher_detail_no == location.name) ) ).run(as_dict=True) diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.js b/erpnext/stock/report/reserved_stock/reserved_stock.js index 2199f52df0..68727411d5 100644 --- a/erpnext/stock/report/reserved_stock/reserved_stock.js +++ b/erpnext/stock/report/reserved_stock/reserved_stock.js @@ -91,16 +91,30 @@ frappe.query_reports["Reserved Stock"] = { }, }, { - fieldname: "against_pick_list", - label: __("Against Pick List"), + fieldname: "from_voucher_type", + label: __("From Voucher Type"), fieldtype: "Link", - options: "Pick List", + options: "DocType", + get_query: () => ({ + filters: { + name: ["in", ["Pick List", "Purchase Receipt"]], + } + }), + }, + { + fieldname: "from_voucher_no", + label: __("From Voucher No"), + fieldtype: "Dynamic Link", + options: "from_voucher_type", get_query: () => ({ filters: { docstatus: 1, company: frappe.query_report.get_filter_value("company"), }, }), + get_options: function () { + return frappe.query_report.get_filter_value("from_voucher_type"); + }, }, { fieldname: "reservation_based_on", diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.py b/erpnext/stock/report/reserved_stock/reserved_stock.py index d93ee1c88f..21ce203ad6 100644 --- a/erpnext/stock/report/reserved_stock/reserved_stock.py +++ b/erpnext/stock/report/reserved_stock/reserved_stock.py @@ -44,7 +44,8 @@ def get_data(filters): (sre.available_qty - sre.reserved_qty).as_("available_qty"), sre.voucher_type, sre.voucher_no, - sre.against_pick_list, + sre.from_voucher_type, + sre.from_voucher_no, sre.name.as_("stock_reservation_entry"), sre.status, sre.project, @@ -65,7 +66,8 @@ def get_data(filters): "warehouse", "voucher_type", "voucher_no", - "against_pick_list", + "from_voucher_type", + "from_voucher_no", "reservation_based_on", "status", "project", @@ -142,7 +144,6 @@ def get_columns(): "fieldname": "voucher_type", "label": _("Voucher Type"), "fieldtype": "Data", - "options": "Warehouse", "width": 110, }, { @@ -153,11 +154,17 @@ def get_columns(): "width": 120, }, { - "fieldname": "against_pick_list", - "label": _("Against Pick List"), - "fieldtype": "Link", - "options": "Pick List", - "width": 130, + "fieldname": "from_voucher_type", + "label": _("From Voucher Type"), + "fieldtype": "Data", + "width": 110, + }, + { + "fieldname": "from_voucher_no", + "label": _("From Voucher No"), + "fieldtype": "Dynamic Link", + "options": "from_voucher_type", + "width": 120, }, { "fieldname": "stock_reservation_entry", From 45395027d3b5c51ac3ccdbebb1f0d23d5ffd2ec1 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 19 Oct 2023 19:57:50 +0530 Subject: [PATCH 072/135] fix: incorrect serial and batch get reserved --- .../doctype/sales_order/sales_order.py | 15 +++++++++-- erpnext/stock/doctype/pick_list/pick_list.py | 27 ++++++++++++------- .../purchase_receipt/purchase_receipt.py | 10 +++++-- .../stock_reservation_entry.py | 19 +++++++------ 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index d38216242e..94f9d6e37c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -3,6 +3,7 @@ import json +from typing import Literal import frappe import frappe.utils @@ -534,14 +535,24 @@ class SalesOrder(SellingController): return False @frappe.whitelist() - def create_stock_reservation_entries(self, items_details=None, notify=True) -> None: + def create_stock_reservation_entries( + self, + items_details: list[dict] = None, + from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, + notify=True, + ) -> None: """Creates Stock Reservation Entries for Sales Order Items.""" from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( create_stock_reservation_entries_for_so_items as create_stock_reservation_entries, ) - create_stock_reservation_entries(sales_order=self, items_details=items_details, notify=notify) + create_stock_reservation_entries( + sales_order=self, + items_details=items_details, + from_voucher_type=from_voucher_type, + notify=notify, + ) @frappe.whitelist() def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None: diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 3503556f3e..ed20209577 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -229,20 +229,27 @@ class PickList(Document): def create_stock_reservation_entries(self, notify=True) -> None: """Creates Stock Reservation Entries for Sales Order Items against Pick List.""" - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - create_stock_reservation_entries_for_so_items, - ) - - so_details = {} + so_items_details_map = {} for location in self.locations: if location.warehouse and location.sales_order and location.sales_order_item: - so_details.setdefault(location.sales_order, []).append(location) + item_details = { + "name": location.sales_order_item, + "item_code": location.item_code, + "warehouse": location.warehouse, + "qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)), + "from_voucher_no": location.parent, + "from_voucher_detail_no": location.name, + "serial_and_batch_bundle": location.serial_and_batch_bundle, + } + so_items_details_map.setdefault(location.sales_order, []).append(item_details) - if so_details: - for so, locations in so_details.items(): + if so_items_details_map: + for so, items_details in so_items_details_map.items(): so_doc = frappe.get_doc("Sales Order", so) - create_stock_reservation_entries_for_so_items( - sales_order=so_doc, items_details=locations, from_voucher_type="Pick List", notify=notify + so_doc.create_stock_reservation_entries( + items_details=items_details, + from_voucher_type="Pick List", + notify=notify, ) @frappe.whitelist() diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index fc88dd8d5f..42d6b02dee 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -846,14 +846,20 @@ class PurchaseReceipt(BuyingController): "item_code": item.item_code, "warehouse": item.warehouse, "qty_to_reserve": item.stock_qty, - "serial_and_batch_bundle": item.get("serial_and_batch_bundle"), + "from_voucher_no": item.parent, + "from_voucher_detail_no": item.name, + "serial_and_batch_bundle": item.serial_and_batch_bundle, } so_items_details_map.setdefault(item.sales_order, []).append(item_details) if so_items_details_map: for so, items_details in so_items_details_map.items(): so_doc = frappe.get_doc("Sales Order", so) - so_doc.create_stock_reservation_entries(items_details) + so_doc.create_stock_reservation_entries( + items_details=items_details, + from_voucher_type="Purchase Receipt", + notify=True, + ) def update_billed_amount_based_on_po(po_details, update_modified=True): diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 9097e621e6..e589628c62 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -794,22 +794,21 @@ def create_stock_reservation_entries_for_so_items( items = [] if items_details: - item_field = "sales_order_item" if from_voucher_type == "Pick List" else "name" - for item in items_details: - so_item = frappe.get_doc("Sales Order Item", item.get(item_field)) + so_item = frappe.get_doc("Sales Order Item", item.get("name")) so_item.warehouse = item.get("warehouse") so_item.qty_to_reserve = ( - item.get("picked_qty") - item.get("stock_reserved_qty", 0) - if from_voucher_type == "Pick List" - else (flt(item.get("qty_to_reserve")) * flt(so_item.conversion_factor, 1)) + flt(item.get("qty_to_reserve")) + if from_voucher_type in ["Pick List", "Purchase Receipt"] + else ( + flt(item.get("qty_to_reserve")) + * (flt(item.get("conversion_factor")) or flt(so_item.conversion_factor) or 1) + ) ) + so_item.from_voucher_no = item.get("from_voucher_no") + so_item.from_voucher_detail_no = item.get("from_voucher_detail_no") so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle") - if from_voucher_type == "Pick List": - so_item.from_voucher_no = item.get("parent") - so_item.from_voucher_detail_no = item.get("name") - items.append(so_item) sre_count = 0 From 77cc91d06b9ea1e5758546a78bbcbb2ef97f549b Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 19 Oct 2023 22:35:55 +0530 Subject: [PATCH 073/135] fix: add regional support to extend purchase gl entries --- .../doctype/purchase_invoice/purchase_invoice.py | 2 +- erpnext/controllers/stock_controller.py | 12 ++++++++++-- .../doctype/purchase_receipt/purchase_receipt.py | 12 ++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 2433268627..e8fc445bc9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1066,7 +1066,7 @@ class PurchaseInvoice(BuyingController): "debit_in_account_currency": ( base_asset_amount if cwip_account_currency == self.company_currency else asset_amount ), - "cost_center": self.cost_center, + "cost_center": item.cost_center or self.cost_center, "project": item.project or self.project, }, item=item, diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index ae54b801f1..9eeffd8ea6 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -76,6 +76,7 @@ class StockController(AccountsController): elif self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self.docstatus == 1: gl_entries = [] gl_entries = self.get_asset_gl_entry(gl_entries) + update_regional_gl_entries(gl_entries, self) make_gl_entries(gl_entries, from_repost=from_repost) def validate_serialized_batch(self): @@ -855,8 +856,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 +869,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 +1219,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/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index de0db1aa8f..662ab6d423 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -314,6 +314,7 @@ class PurchaseReceipt(BuyingController): self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) self.make_tax_gl_entries(gl_entries) self.get_asset_gl_entry(gl_entries) + update_regional_gl_entries(gl_entries, self) return process_gl_map(gl_entries) @@ -827,8 +828,6 @@ class PurchaseReceipt(BuyingController): pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) - self.load_from_db() - def update_billed_amount_based_on_po(po_details, update_modified=True): po_billed_amt_details = get_billed_amount_against_po(po_details) @@ -941,9 +940,6 @@ def get_billed_amount_against_po(po_items): def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): - # Reload as billed amount was set in db directly - pr_doc.load_from_db() - # Update Billing % based on pending accepted qty total_amount, total_billed_amount = 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) @@ -969,7 +965,6 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) - pr_doc.load_from_db() if update_modified: pr_doc.set_status(update=True) @@ -1255,3 +1250,8 @@ def get_item_account_wise_additional_cost(purchase_document): def on_doctype_update(): frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"]) + + +@erpnext.allow_regional +def update_regional_gl_entries(gl_list, doc): + return From ff7108a3b139b2f019230be681147f1a1c90a681 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 20 Oct 2023 09:33:37 +0530 Subject: [PATCH 074/135] fix: update existing doc if possible --- .../purchase_receipt/purchase_receipt.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 662ab6d423..3734892f17 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -822,14 +822,14 @@ class PurchaseReceipt(BuyingController): po_details.append(d.purchase_order_item) if po_details: - updated_pr += update_billed_amount_based_on_po(po_details, update_modified) + updated_pr += update_billed_amount_based_on_po(po_details, update_modified, self) for pr in set(updated_pr): pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) -def update_billed_amount_based_on_po(po_details, update_modified=True): +def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=None): po_billed_amt_details = get_billed_amount_against_po(po_details) # Get all Purchase Receipt Item rows against the Purchase Order Items @@ -858,13 +858,19 @@ def update_billed_amount_based_on_po(po_details, update_modified=True): po_billed_amt_details[pr_item.purchase_order_item] = billed_against_po if pr_item.billed_amt != billed_amt_agianst_pr: - frappe.db.set_value( - "Purchase Receipt Item", - pr_item.name, - "billed_amt", - billed_amt_agianst_pr, - update_modified=update_modified, - ) + # update existing doc if possible + if pr_doc and pr_item.parent == pr_doc.name: + pr_item = next((item for item in pr_doc.items if item.name == pr_item.name), None) + pr_item.db_set("billed_amt", billed_amt_agianst_pr, update_modified=update_modified) + + else: + frappe.db.set_value( + "Purchase Receipt Item", + pr_item.name, + "billed_amt", + billed_amt_agianst_pr, + update_modified=update_modified, + ) updated_pr.append(pr_item.parent) From 4f363f5bf3da286999966f10d0cca22264f42199 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 20 Oct 2023 11:44:14 +0530 Subject: [PATCH 075/135] fix: partial reservation against SBB --- .../stock_reservation_entry.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index e589628c62..c7a9e16d0e 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -935,20 +935,28 @@ def create_stock_reservation_entries_for_so_items( sre.from_voucher_no = item.from_voucher_no sre.from_voucher_detail_no = item.from_voucher_detail_no - if item.serial_and_batch_bundle: + if item.get("serial_and_batch_bundle"): sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) sre.reservation_based_on = "Serial and Batch" - for entry in sbb.entries: + + index, picked_qty = 0, 0 + while index < len(sbb.entries) and picked_qty < qty_to_be_reserved: + entry = sbb.entries[index] + qty = 1 if has_serial_no else min(abs(entry.qty), qty_to_be_reserved - picked_qty) + sre.append( "sb_entries", { "serial_no": entry.serial_no, "batch_no": entry.batch_no, - "qty": 1 if has_serial_no else abs(entry.qty), + "qty": qty, "warehouse": entry.warehouse, }, ) + index += 1 + picked_qty += qty + sre.save() sre.submit() From ce7ac29d060833faefee24c7e1eaebacda983a20 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 20 Oct 2023 15:50:40 +0530 Subject: [PATCH 076/135] fix: Correctly extract last message (#37602) frappe.message_log now contains plain dictionary and not JSON strings, so no need to load them. --- .../pos_invoice_merge_log/pos_invoice_merge_log.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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) From 85488cd0dcac2efe02478ced44349a0cadb3b064 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Fri, 20 Oct 2023 12:22:55 +0200 Subject: [PATCH 077/135] feat(delivery): link to delivery notes list view from delivery trip --- erpnext/stock/doctype/delivery_trip/delivery_trip.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index de503dc73f..2d7a528ade 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -64,6 +64,11 @@ frappe.ui.form.on('Delivery Trip', { }) }, __("Get customers from")); } + frm.add_custom_button(__("Delivery Notes"), function () { + frappe.set_route("List", "Delivery Note", + {'name': ["in", frm.doc.delivery_stops.map((stop) => {return stop.delivery_note;})]} + ); + }, __("View")); }, calculate_arrival_time: function (frm) { From 79d51a0a0b685909371e9bda68d9702fb287c53e Mon Sep 17 00:00:00 2001 From: David Arnold Date: Fri, 20 Oct 2023 12:31:46 +0200 Subject: [PATCH 078/135] fix(delivery): rename dt fetch stop action --- erpnext/stock/doctype/delivery_trip/delivery_trip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index de503dc73f..4a72d77956 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -62,7 +62,7 @@ frappe.ui.form.on('Delivery Trip', { company: frm.doc.company, } }) - }, __("Get customers from")); + }, __("Get stops from")); } }, From 14b009b09355f53b1dfcd05d0f7ba918b0b25210 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 20 Oct 2023 16:22:29 +0530 Subject: [PATCH 079/135] fix: incorrect cost center in the purchase invoice (#37591) --- .../purchase_invoice/test_purchase_invoice.py | 24 +++++++++++++++++++ erpnext/stock/doctype/item/test_item.py | 12 +++++++++- erpnext/stock/get_item_details.py | 6 +++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e365d60f20..13593bcf9b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1949,6 +1949,30 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): 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/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 0c6dc77635..a942f58bd6 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -907,6 +907,8 @@ def create_item( opening_stock=0, is_fixed_asset=0, asset_category=None, + buying_cost_center=None, + selling_cost_center=None, company="_Test Company", ): if not frappe.db.exists("Item", item_code): @@ -924,7 +926,15 @@ def create_item( item.is_purchase_item = is_purchase_item item.is_customer_provided_item = is_customer_provided_item item.customer = customer or "" - item.append("item_defaults", {"default_warehouse": warehouse, "company": company}) + item.append( + "item_defaults", + { + "default_warehouse": warehouse, + "company": company, + "selling_cost_center": selling_cost_center, + "buying_cost_center": buying_cost_center, + }, + ) item.save() else: item = frappe.get_doc("Item", item_code) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index a6ab63bb59..595446228f 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -737,6 +737,12 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan data = frappe.get_attr(path)(args.get("item_code"), company) if data and (data.selling_cost_center or data.buying_cost_center): + if args.get("customer") and data.selling_cost_center: + return data.selling_cost_center + + elif args.get("supplier") and data.buying_cost_center: + return data.buying_cost_center + return data.selling_cost_center or data.buying_cost_center if not cost_center and args.get("cost_center"): From fff97b1cd20305adf6bbd4478adf86d6847cc025 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Fri, 20 Oct 2023 17:34:57 +0530 Subject: [PATCH 080/135] chore: new erpnext logo as per espresso --- erpnext/public/images/erpnext-favicon.svg | 2 +- erpnext/public/images/erpnext-logo.png | Bin 2360 -> 4260 bytes erpnext/public/images/erpnext-logo.svg | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/images/erpnext-favicon.svg b/erpnext/public/images/erpnext-favicon.svg index 6bc6b2c2db..768e6e5514 100644 --- a/erpnext/public/images/erpnext-favicon.svg +++ b/erpnext/public/images/erpnext-favicon.svg @@ -1,5 +1,5 @@ - + diff --git a/erpnext/public/images/erpnext-logo.png b/erpnext/public/images/erpnext-logo.png index 3090727d8ff5f95e49d25278b24b10bf13747f4a..b4c27493e215b73cb61a5323131d6d2294c419ef 100644 GIT binary patch literal 4260 zcmd^DTU1kL7TzZz1c?e*6+{`J7p(&VEgBS*go>zxAQd$#LehFc3gM2RAtXv$#)4o= zA&4mHPyrPo6a;~U9AK292q-s8fS`azBnE_VNl4Bd`!rAUG%v26wa)tY-v8ySe_y|S zF8KNGTw-i#3;;`Z@ABLSK)^`?7#ZTjbV8Q~9~MRKIvfK)`QY7)KyID|E<|GX?fe2t z+t$3o8G~RC9}j@?49db+BmmPbyFESjCm_lJw~V|4=bNATcW0}1`LJ$&;Q1q)x;Eg) zj^mlnZ2Q74?{#{eb|I}|(WR#ss5bCh)cHOCKE82LZkRZqWqdfh@A2~AtNN;HIqk1* zykX0@@yx-S*wn5a2h^kh(>yt+?aW^FgEWh!DO2tFH0#pR(x8Zl-kzSpSL(A<$~bOo z1P9RY`z!$)H{XlHyvr~4Y^R* zdB2q|l}iJUvG%(b`Xf%O<|4WUsU$eryzlCYy93vKzP)M$L>kz7Wtz^_=9N1ydB^>d z-FDS2e#%~DvpExT`Wt6=aL*I~z1VeS689j-;KhUWs}~!xcVf&3($i`py`*+c$Nk2= zr9l$S^yF|e&#p+(brRMxbj%f$ zXu{g+S)IgU0U0W$I{YfjIpT|!J0f8LDwk0cC<~5ZqK?fH$Du>Sq1)AiwFipq^!tH7 z+%-e`*%F~;ABzsdb+JIR1GN0svGM0oUTC=izrh-O2XQi#{3qESwF3<$(Pb0CkMIdb zD!+HYB4H#3)>dnDe!zGBY|A>Q(jbSxUSL{wIzBH?yxgRb=1O>;#0y0Eks zc9=s-tO63=jC|@H2gQ`71%_JoYqEC%Qr~5tCWJy^*)g(xx{O zA@cdsJJv&}H)O`~l4%g@FEg3w_TbIYaf!*?db{h!$Swa|O`T z(-Xb|U%w}{YPm=ih;Z8AXx(Y$S*aZ3AW--qsmS2X7NQ>++b(AqL7v2w5*b>iQdvTr z63f(79s+(yR^Ib z+#^8i&H1dXtlN+HG6C$th-jFWHFfVGJk^;%1zW4+uSPoDXR7~X!|Sc)dV|6Y(Ek2@ zHNI2)=rCVDa#76#BR|0cuk)JAEG(vg)701w>}$^8W-`mkqQo3*8n1f`2FF!1TN(fj zJBxo-B(18VGJ$V#>(;GaZI&B=%|j8Jux2ILw7BWiYIVFZm=>h#B4#LX>4lXyz@^}N z62A^#mkPkAPt5?(Z4iKS26#4GhNrMye@#K#3dP-Oc~Tu(SmFedfYHZ)DM4&0 zy_u_3R+DNK4;`iUavsJ#7xFPE#Itgqlx{vy6H|m%{)Pv|dm0$R7}92Enr_;Eer`B} zf`&K~-`lJFU(?fn$8|1OKmT&IiDH@admh2otM7ot3b6Ty^dRa;f?Z$ZZ2&=MkI(JFacVC!Fi^t4 zt88d!=6hybA%`6k6SLY}Cwb(!*f6=uEV3R)A>T|-JZp-VrQXgUmR5j#1+Y$bmnVfx z#qngl0%LHh(CG9%_w)y$q@<*h3+}5-;bhnR)Jp-C89_ovwn97+M+2Jhjh5^{MFf*@ zs+y>;0dRgf>ZU&{lkZ6<>32rC0JQ#edjPuL00x5?C1Qh~D0Ky<@_v`~M@82F`@z2%ywWc@_FH(aqN0lmoT)EKYx98q{? zVQx}hdbH+k_=dFCBv5=?^;)`A&)eFnT9dGd)mahTuD|7e(HJ^N1*ztagImWPtz6-L z_wkDjNdTJz3aD!{yb88ww4^NM7jjEjIwBAE2Io$yLN)!RlpIyO4P@KSPM?ZZk99MD z@Hq#OE5m!@FzL@9xkkfAw*S!T2=U{#Wryqmy>TkP+nM@1|c_h=(Z-u5$JVe5$m+N_$@ou)f$L0 zTP7Rgzj-A-EDv&oORvLaqf9a;&GtV%<_xkxyc5viIM_TcgjEjof40P8%FMeG70OF_ zf>0=w{4S$Y4+%{KaK1?)Vuij2XyIwTAGh{|7w`{1xqgo4UqoQ}*BukOa}RDGO8y$} z<6d=9XSukR>jEw3T;e{5sw?&;iTjy@pW=;!QZdNE!;|9i@cAgfWYsWcB73b1>JuV?8Otn>c?9R`a0 literal 2360 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn2Hfk$L90|U1(2s1Lwnj--e zWGoJHcVbv~PUa<$!;&U>cv7h@-A}a#}$5~Go$B+ufw>K9Sh9*k5UVLwOJ(!Va zX^Y;A2EGdl@_l>`i7y#^RP^{9(=r-dj@~ty==pE<-Z0VXbCzc-@3S*_R2nik^f53r zC@?ZGI09YN!~(R06KJv!1A~AH1A~GG1B1haQ3b;&94?q`*kP{E&iDFPT7oB2!_&XD z_5~5k&og@$F%HG3-qv`ucCQqJ+>Ok0zwHd6fknm8%iOovDvb93JGL=j-W!Kx!3@l;vG@68ZvEfg_;k**J@1(rmh2d - + From eec4057e8de4b6387303869d9ac3c5f953cbf5e6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 20 Oct 2023 19:42:14 +0530 Subject: [PATCH 081/135] fix: Purchase Invoice GL entires for assets --- .../purchase_receipt/purchase_receipt.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c2c4d0f539..8c2cbf7c5e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -543,16 +543,15 @@ class PurchaseReceipt(BuyingController): ) elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return): if d.is_fixed_asset: - stock_asset_account_name = ( - get_asset_category_account( - asset_category=d.asset_category, - fieldname="capital_work_in_progress_account", - company=self.company, - ) + account_type = ( + "capital_work_in_progress_account" if is_cwip_accounting_enabled(d.asset_category) - else get_asset_category_account( - asset_category=d.asset_category, fieldname="fixed_asset_account", company=self.company - ) + else "fixed_asset_account" + ) + stock_asset_account_name = get_asset_category_account( + asset_category=d.asset_category, + fieldname=account_type, + company=self.company, ) stock_value_diff = flt(d.net_amount) + flt(d.item_tax_amount / self.conversion_rate) From 94749084491d8d72a50622fafe9629d29e01e659 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 20 Oct 2023 19:49:41 +0530 Subject: [PATCH 082/135] fix: Purchase Invoice GL entires for assets --- .../purchase_invoice/purchase_invoice.py | 202 +++--------------- erpnext/accounts/general_ledger.py | 2 +- 2 files changed, 33 insertions(+), 171 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 2433268627..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)) @@ -587,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 @@ -598,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) @@ -702,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 @@ -817,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) @@ -970,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"), @@ -989,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/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( _( From 21c3d9c3712ffca28d763b560ec8dbc9e5512fb0 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Sat, 21 Oct 2023 11:19:45 +0530 Subject: [PATCH 083/135] refactor: use gzip library's compress() and decompress() methods directly (#37611) The util methods in framework were added for python2.7 compat, so can be removed Signed-off-by: Akhil Narang [skip ci] --- .../closing_stock_balance.py | 6 +++--- erpnext/stock/stock_ledger.py | 17 ++++------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py index 295d979b83..b0499bfe86 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -1,6 +1,6 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - +import gzip import json import frappe @@ -8,7 +8,7 @@ from frappe import _ from frappe.core.doctype.prepared_report.prepared_report import create_json_gz_file from frappe.desk.form.load import get_attachments from frappe.model.document import Document -from frappe.utils import get_link_to_form, gzip_decompress, parse_json +from frappe.utils import get_link_to_form, parse_json from frappe.utils.background_jobs import enqueue from erpnext.stock.report.stock_balance.stock_balance import execute @@ -109,7 +109,7 @@ class ClosingStockBalance(Document): attachment = attachments[0] attached_file = frappe.get_doc("File", attachment.name) - data = gzip_decompress(attached_file.get_content()) + data = gzip.decompress(attached_file.get_content()) if data := json.loads(data.decode("utf-8")): data = data diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 48119b8d1f..b950f18810 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import copy +import gzip import json from typing import Optional, Set, Tuple @@ -10,17 +11,7 @@ from frappe import _, scrub from frappe.model.meta import get_field_precision from frappe.query_builder import Case from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import ( - cint, - flt, - get_link_to_form, - getdate, - gzip_compress, - gzip_decompress, - now, - nowdate, - parse_json, -) +from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, parse_json import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty @@ -295,7 +286,7 @@ def get_reposting_data(file_path) -> dict: attached_file = frappe.get_doc("File", file_name) - data = gzip_decompress(attached_file.get_content()) + data = gzip.decompress(attached_file.get_content()) if data := json.loads(data.decode("utf-8")): data = data @@ -378,7 +369,7 @@ def get_reposting_file_name(dt, dn): def create_json_gz_file(data, doc, file_name=None) -> str: encoded_content = frappe.safe_encode(frappe.as_json(data)) - compressed_content = gzip_compress(encoded_content) + compressed_content = gzip.compress(encoded_content) if not file_name: json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz" From b0d440c34b9cb4d0e0d75153c279ccaa6206253d Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Sat, 21 Oct 2023 17:58:43 +0530 Subject: [PATCH 084/135] fix: set empty value for tax template in item details (#37496) * fix: empty tax template for items with invalid templates * fix: test for empty tax template * fix: test for item tax template calculation * fix: test for pos inv tax template calculation --- .../doctype/pos_invoice/test_pos_invoice.py | 115 ++++++++--------- .../sales_invoice/test_sales_invoice.py | 120 +++++++++--------- erpnext/stock/doctype/item/test_item.py | 10 +- erpnext/stock/get_item_details.py | 1 + 4 files changed, 122 insertions(+), 124 deletions(-) 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/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index c1adffde31..16477324e6 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -516,70 +516,72 @@ class TestSalesInvoice(FrappeTestCase): 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) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index a942f58bd6..09d3dd1dad 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -163,7 +163,7 @@ class TestItem(FrappeTestCase): { "item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 2", - "item_tax_template": None, + "item_tax_template": "", }, { "item_code": "_Test Item Inherit Group Item Tax Template 1", @@ -178,7 +178,7 @@ class TestItem(FrappeTestCase): { "item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 2", - "item_tax_template": None, + "item_tax_template": "", }, { "item_code": "_Test Item Inherit Group Item Tax Template 2", @@ -193,7 +193,7 @@ class TestItem(FrappeTestCase): { "item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 2", - "item_tax_template": None, + "item_tax_template": "", }, { "item_code": "_Test Item Override Group Item Tax Template", @@ -208,12 +208,12 @@ class TestItem(FrappeTestCase): { "item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 2", - "item_tax_template": None, + "item_tax_template": "", }, ] expected_item_tax_map = { - None: {}, + "": {}, "_Test Account Excise Duty @ 10 - _TC": {"_Test Account Excise Duty - _TC": 10}, "_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12}, "_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15}, diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 595446228f..8c6fd84bc4 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -606,6 +606,7 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): # all templates have validity and no template is valid if not taxes_with_validity and (not taxes_with_no_validity): + out["item_tax_template"] = "" return None # do not change if already a valid template From 9d392970f02b510799baa7123e1eb64fbb62dcf5 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Sat, 21 Oct 2023 17:59:12 +0530 Subject: [PATCH 085/135] fix(minor): filter bank accounts in bank statement import (#37525) fix: filter by company in bank account --- .../bank_statement_import/bank_statement_import.js | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 a70af7a90e..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; From 1cc1c9aa38ad3816c1456ec71dce478b672b011e Mon Sep 17 00:00:00 2001 From: William Moreno Date: Sat, 21 Oct 2023 06:32:02 -0600 Subject: [PATCH 086/135] fix: Typo in Nicaraguan chart of accounts (#37620) fix: Typo in Nicaraguan chart of accounts --- .../chart_of_accounts/verified/ni_catalogo_de_cuentas.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 +} From 35020a94234aee3412df99c1074d9c3a7fca16c1 Mon Sep 17 00:00:00 2001 From: Vishnu VS Date: Sat, 21 Oct 2023 18:02:32 +0530 Subject: [PATCH 087/135] fix: error while loading Financial Ratios report (#37613) --- erpnext/accounts/report/financial_ratios/financial_ratios.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 98cc7434d286b0e13a919a8aa7a481524d200650 Mon Sep 17 00:00:00 2001 From: Vishnu VS Date: Sat, 21 Oct 2023 18:04:54 +0530 Subject: [PATCH 088/135] feat(Supplier Scorecard): added method for invoiced quantity in supplier scorecard (#37580) feat(Supplier Scorecard): added method for invoiced quantity in supplier scorecard Co-authored-by: vishnu --- .../supplier_scorecard/supplier_scorecard.py | 5 +++++ .../supplier_scorecard_variable.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) 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) From 4aa841786fbfd4b438a9a5913b002bf1faaa4bb7 Mon Sep 17 00:00:00 2001 From: Niraj Gautam Date: Sat, 21 Oct 2023 18:13:53 +0530 Subject: [PATCH 089/135] fix: Update user profile picture, if employee profile pic is changed (#37483) * fix: Update user pic if employee pic is changed. * fix: Update condition --- erpnext/setup/doctype/employee/employee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 566392c327..78fb4dfc58 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -123,7 +123,7 @@ class Employee(NestedSet): user.gender = self.gender if self.image: - if not user.user_image: + if not user.user_image or self.has_value_changed("image"): user.user_image = self.image try: frappe.get_doc( From 5136fe196b4e3aab6bb18d2edf5effbfacd2b060 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 22 Oct 2023 20:03:02 +0530 Subject: [PATCH 090/135] fix: remove from or target warehouse for non internal transfer entries (#37612) --- erpnext/controllers/stock_controller.py | 22 +++++++++++++------ .../delivery_note/test_delivery_note.py | 15 +++++++++++++ .../purchase_receipt/test_purchase_receipt.py | 15 +++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 98d8248fff..61d7107437 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -689,13 +689,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 ( diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 48b8ab7504..d06819208e 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1230,6 +1230,21 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) + def non_internal_transfer_delivery_note(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + dn = create_delivery_note(do_not_submit=True) + warehouse = create_warehouse("Internal Transfer Warehouse", dn.company) + dn.items[0].db_set("target_warehouse", "warehouse") + + dn.reload() + + self.assertEqual(dn.items[0].target_warehouse, warehouse.name) + + dn.save() + dn.reload() + self.assertFalse(dn.items[0].target_warehouse) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index cdf50532fc..1af7b9aefc 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2142,6 +2142,21 @@ class TestPurchaseReceipt(FrappeTestCase): for entry in gl_entries: self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) + def non_internal_transfer_purchase_receipt(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + pr_doc = make_purchase_receipt(do_not_submit=True) + warehouse = create_warehouse("Internal Transfer Warehouse", pr_doc.company) + pr_doc.items[0].db_set("target_warehouse", "warehouse") + + pr_doc.reload() + + self.assertEqual(pr_doc.items[0].from_warehouse, warehouse.name) + + pr_doc.save() + pr_doc.reload() + self.assertFalse(pr_doc.items[0].from_warehouse) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From d1ec0a609329ae94f90fcccc2d6a9f7a473f013d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 23 Oct 2023 00:16:40 +0530 Subject: [PATCH 091/135] chore: Add missing commits back (#37618) * chore: Add missing commits back * test: cwip accounting unit tests * chore: Attribute error * chore: Purchase Invoice tests * chore: Missing asset account * chore: Missing asset account * chore: update tests * fix: Internal transfer GL Entries --- erpnext/assets/doctype/asset/test_asset.py | 10 +- erpnext/controllers/stock_controller.py | 3 + .../purchase_receipt/purchase_receipt.py | 158 +++++++++++------- 3 files changed, 101 insertions(+), 70 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 88ef69cddc..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( diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 61d7107437..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) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 9fe06a2d2e..9fdb01a662 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -13,7 +13,6 @@ from pypika import functions as fn import erpnext from erpnext.accounts.utils import get_account_currency from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled -from erpnext.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.buying_controller import BuyingController from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction @@ -313,6 +312,7 @@ class PurchaseReceipt(BuyingController): self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) self.make_tax_gl_entries(gl_entries) + update_regional_gl_entries(gl_entries, self) return process_gl_map(gl_entries) @@ -321,22 +321,6 @@ class PurchaseReceipt(BuyingController): get_purchase_document_details, ) - is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) - stock_asset_rbnb = None - remarks = self.get("remarks") or _("Accounting Entry for {0}").format( - "Asset" if is_asset_pr else "Stock" - ) - - if erpnext.is_perpetual_inventory_enabled(self.company): - stock_asset_rbnb = ( - self.get_company_default("asset_received_but_not_billed") - if is_asset_pr - else self.get_company_default("stock_received_but_not_billed") - ) - landed_cost_entries = get_item_account_wise_additional_cost(self.name) - - warehouse_with_no_account = [] - stock_items = self.get_stock_items() provisional_accounting_for_non_stock_items = cint( frappe.db.get_value( "Company", self.company, "enable_provisional_accounting_for_non_stock_items" @@ -345,8 +329,15 @@ class PurchaseReceipt(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) - def make_item_asset_inward_entries(item, stock_value_diff, stock_asset_account_name): + def validate_account(account_type): + frappe.throw(_("{0} account not found while submitting purchase receipt").format(account_type)) + + def make_item_asset_inward_gl_entry(item, stock_value_diff, stock_asset_account_name): account_currency = get_account_currency(stock_asset_account_name) + + if not stock_asset_account_name: + validate_account("Asset or warehouse account") + self.add_gl_entry( gl_entries=gl_entries, account=stock_asset_account_name, @@ -365,7 +356,6 @@ class PurchaseReceipt(BuyingController): ) account_currency = get_account_currency(account) - outgoing_amount = item.base_net_amount # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation credit_amount = ( @@ -374,11 +364,15 @@ class PurchaseReceipt(BuyingController): else flt(item.net_amount, item.precision("net_amount")) ) + outgoing_amount = item.base_net_amount if self.is_internal_transfer() and item.valuation_rate: outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse)) credit_amount = outgoing_amount if credit_amount: + if not account: + validate_account("Stock or Asset Received But Not Billed") + self.add_gl_entry( gl_entries=gl_entries, account=account, @@ -387,7 +381,7 @@ class PurchaseReceipt(BuyingController): credit=0.0, remarks=remarks, against_account=stock_asset_account_name, - debit_in_account_currency=-1 * credit_amount, + debit_in_account_currency=-1 * flt(outgoing_amount, item.precision("base_net_amount")), account_currency=account_currency, item=item, ) @@ -430,8 +424,10 @@ class PurchaseReceipt(BuyingController): item=item, ) + return outgoing_amount + def make_landed_cost_gl_entries(item): - # Amount added through landed-cos-voucher + # Amount added through landed-cost-voucher if item.landed_cost_voucher_amount and landed_cost_entries: if (item.item_code, item.name) in landed_cost_entries: for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): @@ -442,6 +438,9 @@ class PurchaseReceipt(BuyingController): else flt(amount["amount"]) ) + if not account: + validate_account("Landed Cost Account") + self.add_gl_entry( gl_entries=gl_entries, account=account, @@ -487,14 +486,14 @@ class PurchaseReceipt(BuyingController): item=item, ) - def make_divisional_loss_gl_entry(item): + def make_divisional_loss_gl_entry(item, outgoing_amount): if item.is_fixed_asset: return # divisional loss adjustment expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") valuation_amount_as_per_doc = ( - flt(item.base_net_amount, d.precision("base_net_amount")) + flt(outgoing_amount, d.precision("base_net_amount")) + flt(item.landed_cost_voucher_amount) + flt(item.rm_supp_cost) + flt(item.item_tax_amount) @@ -531,35 +530,53 @@ class PurchaseReceipt(BuyingController): item=item, ) + stock_items = self.get_stock_items() + warehouse_with_no_account = [] + for d in self.get("items"): if ( - d.item_code not in stock_items + provisional_accounting_for_non_stock_items + and d.item_code not in stock_items and flt(d.qty) - and provisional_accounting_for_non_stock_items and d.get("provisional_expense_account") + and not d.is_fixed_asset ): self.add_provisional_gl_entry( d, gl_entries, self.posting_date, d.get("provisional_expense_account") ) elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return): + is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) + remarks = self.get("remarks") or _("Accounting Entry for {0}").format( + "Asset" if is_asset_pr else "Stock" + ) + + if not (erpnext.is_perpetual_inventory_enabled(self.company) or is_asset_pr): + return + + stock_asset_rbnb = ( + self.get_company_default("asset_received_but_not_billed") + if is_asset_pr + else self.get_company_default("stock_received_but_not_billed") + ) + landed_cost_entries = get_item_account_wise_additional_cost(self.name) + if d.is_fixed_asset: account_type = ( "capital_work_in_progress_account" if is_cwip_accounting_enabled(d.asset_category) else "fixed_asset_account" ) - stock_asset_account_name = get_asset_category_account( - asset_category=d.asset_category, - fieldname=account_type, - company=self.company, + + stock_asset_account_name = get_asset_account( + account_type, asset_category=d.asset_category, company=self.company ) - stock_value_diff = flt(d.net_amount) + flt(d.item_tax_amount / self.conversion_rate) - elif ( - (flt(d.valuation_rate) or self.is_return) - and flt(d.qty) - and warehouse_account.get(d.warehouse) - ): + stock_value_diff = ( + flt(d.net_amount) + + flt(d.item_tax_amount / self.conversion_rate) + + flt(d.landed_cost_voucher_amount) + ) + elif warehouse_account.get(d.warehouse): stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) stock_asset_account_name = warehouse_account[d.warehouse]["account"] supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") @@ -577,12 +594,13 @@ class PurchaseReceipt(BuyingController): ): continue - make_item_asset_inward_entries(d, stock_value_diff, stock_asset_account_name) - make_stock_received_but_not_billed_entry(d) - make_landed_cost_gl_entries(d) - make_rate_difference_entry(d) - make_sub_contracting_gl_entries(d) - make_divisional_loss_gl_entry(d) + if (flt(d.valuation_rate) or self.is_return or d.is_fixed_asset) and flt(d.qty): + make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name) + outgoing_amount = make_stock_received_but_not_billed_entry(d) + make_landed_cost_gl_entries(d) + make_rate_difference_entry(d) + make_sub_contracting_gl_entries(d) + make_divisional_loss_gl_entry(d, outgoing_amount) elif ( d.warehouse not in warehouse_with_no_account or d.rejected_warehouse not in warehouse_with_no_account @@ -603,8 +621,8 @@ class PurchaseReceipt(BuyingController): self, item, gl_entries, posting_date, provisional_account, reverse=0 ): credit_currency = get_account_currency(provisional_account) - debit_currency = get_account_currency(item.expense_account) expense_account = item.expense_account + debit_currency = get_account_currency(item.expense_account) remarks = self.get("remarks") or _("Accounting Entry for Service") multiplication_factor = 1 @@ -645,11 +663,8 @@ class PurchaseReceipt(BuyingController): ) def make_tax_gl_entries(self, gl_entries): - - if erpnext.is_perpetual_inventory_enabled(self.company): - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") - negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")]) + is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) # Cost center-wise amount breakup for other charges included for valuation valuation_tax = {} for tax in self.get("taxes"): @@ -673,22 +688,24 @@ class PurchaseReceipt(BuyingController): # and charges added via Landed Cost Voucher, # post valuation related charges on "Stock Received But Not Billed" # introduced in 2014 for backward compatibility of expenses already booked in expenses_included_in_valuation account - - negative_expense_booked_in_pi = frappe.db.sql( - """select name from `tabPurchase Invoice Item` pi - where docstatus = 1 and purchase_receipt=%s - and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' - and voucher_no=pi.parent and account=%s)""", - (self.name, expenses_included_in_valuation), - ) - against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked - stock_rbnb = self.get_company_default("stock_received_but_not_billed") + stock_rbnb = ( + self.get("asset_received_but_not_billed") + if is_asset_pr + else self.get_company_default("stock_received_but_not_billed") + ) i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): + negative_expense_booked_in_pi = frappe.db.sql( + """select name from `tabPurchase Invoice Item` pi + where docstatus = 1 and purchase_receipt=%s + and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' + and voucher_no=pi.parent and account=%s)""", + (self.name, tax.account_head), + ) if negative_expense_booked_in_pi: account = stock_rbnb @@ -740,7 +757,7 @@ class PurchaseReceipt(BuyingController): po_details.append(d.purchase_order_item) if po_details: - updated_pr += update_billed_amount_based_on_po(po_details, update_modified) + updated_pr += update_billed_amount_based_on_po(po_details, update_modified, self) for pr in set(updated_pr): pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) @@ -763,7 +780,7 @@ def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse): ) -def update_billed_amount_based_on_po(po_details, update_modified=True): +def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=None): po_billed_amt_details = get_billed_amount_against_po(po_details) # Get all Purchase Receipt Item rows against the Purchase Order Items @@ -792,13 +809,19 @@ def update_billed_amount_based_on_po(po_details, update_modified=True): po_billed_amt_details[pr_item.purchase_order_item] = billed_against_po if pr_item.billed_amt != billed_amt_agianst_pr: - frappe.db.set_value( - "Purchase Receipt Item", - pr_item.name, - "billed_amt", - billed_amt_agianst_pr, - update_modified=update_modified, - ) + # update existing doc if possible + if pr_doc and pr_item.parent == pr_doc.name: + pr_item = next((item for item in pr_doc.items if item.name == pr_item.name), None) + pr_item.db_set("billed_amt", billed_amt_agianst_pr, update_modified=update_modified) + + else: + frappe.db.set_value( + "Purchase Receipt Item", + pr_item.name, + "billed_amt", + billed_amt_agianst_pr, + update_modified=update_modified, + ) updated_pr.append(pr_item.parent) @@ -1184,3 +1207,8 @@ def get_item_account_wise_additional_cost(purchase_document): def on_doctype_update(): frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"]) + + +@erpnext.allow_regional +def update_regional_gl_entries(gl_list, doc): + return From ec9434aae3d0ec0cfa6f6c58dcc9db21b677ac8a Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Mon, 23 Oct 2023 06:45:23 +0200 Subject: [PATCH 092/135] refactor: remove fr translation duplicate in frappe app (#37288) --- erpnext/translations/fr.csv | 412 +----------------------------------- 1 file changed, 4 insertions(+), 408 deletions(-) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 32fe9fffa0..d3875c1132 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -21,9 +21,6 @@ A Lead requires either a person's name or an organization's name,Un responsable A customer with the same name already exists,Un client avec un nom identique existe déjà, A question must have more than one options,Une question doit avoir plus d'une option, A qustion must have at least one correct options,Une qustion doit avoir au moins une des options correctes, -A4,A4, -API Endpoint,API Endpoint, -API Key,Clé API, Abbr can not be blank or space,Abré. ne peut être vide ou contenir un espace, Abbreviation already used for another company,Abréviation déjà utilisée pour une autre société, Abbreviation cannot have more than 5 characters,L'abbréviation ne peut pas avoir plus de 5 caractères, @@ -36,9 +33,7 @@ Academic Term: ,Période scolaire:, Academic Year,Année académique, Academic Year: ,Année scolaire:, Accepted + Rejected Qty must be equal to Received quantity for Item {0},La Qté Acceptée + Rejetée doit être égale à la quantité Reçue pour l'Article {0}, -Access Token,Jeton d'Accès, Accessable Value,Valeur accessible, -Account,Compte, Account Number,Numéro de compte, Account Number {0} already used in account {1},Numéro de compte {0} déjà utilisé dans le compte {1}, Account Pay Only,Compte Bénéficiaire Seulement, @@ -75,12 +70,10 @@ Accounting Entry for {0}: {1} can only be made in currency: {2},Écriture Compta Accounting Ledger,Livre des Comptes, Accounting journal entries.,Les écritures comptables., Accounts,Comptes, -Accounts Manager,Responsable des Comptes, Accounts Payable,Comptes Créditeurs, Accounts Payable Summary,Résumé des Comptes Créditeurs, Accounts Receivable,Comptes débiteurs, Accounts Receivable Summary,Résumé des Comptes Débiteurs, -Accounts User,Comptable, Accounts table cannot be blank.,Le tableau de comptes ne peut être vide., Accumulated Depreciation,Amortissement Cumulé, Accumulated Depreciation Amount,Montant d'Amortissement Cumulé, @@ -89,10 +82,8 @@ Accumulated Monthly,Cumul mensuel, Accumulated Values,Valeurs accumulées, Accumulated Values in Group Company,Valeurs accumulées dans la société mère, Achieved ({}),Atteint ({}), -Action,Action, Action Initialised,Action initialisée, Actions,Actions, -Active,actif, Activity Cost exists for Employee {0} against Activity Type - {1},Des Coûts d'Activité existent pour l'Employé {0} pour le Type d'Activité - {1}, Activity Cost per Employee,Coût de l'Activité par Employé, Activity Type,Type d'activité, @@ -104,7 +95,6 @@ Actual Qty {0} / Waiting Qty {1},Qté Réelle {0} / Quantité en Attente {1}, Actual Qty: Quantity available in the warehouse.,Quantité réelle : Quantité disponible dans l'entrepôt ., Actual qty in stock,Qté réelle en stock, Actual type tax cannot be included in Item rate in row {0},Le type de taxe réel ne peut pas être inclus dans le prix de l'Article à la ligne {0}, -Add,Ajouter, Add / Edit Prices,Ajouter / Modifier Prix, Add Comment,Ajouter un Commentaire, Add Customers,Ajouter des clients, @@ -127,17 +117,11 @@ Add more items or open full form,Ajouter plus d'articles ou ouvrir le formulaire Add notes,Ajouter des notes, Add the rest of your organization as your users. You can also add invite Customers to your portal by adding them from Contacts,Ajouter le reste de votre organisation en tant qu'utilisateurs. Vous pouvez aussi inviter des Clients sur votre portail en les ajoutant depuis les Contacts, Add/Remove Recipients,Ajouter/Supprimer des Destinataires, -Added,Ajouté, Added {0} users,{0} utilisateurs ajoutés, Additional Salary Component Exists.,La composante salariale supplémentaire existe., -Address,Adresse, -Address Line 2,Adresse Ligne 2, Address Name,Nom de l'Adresse, -Address Title,Titre de l'Adresse, -Address Type,Type d'Adresse, Administrative Expenses,Charges Administratives, Administrative Officer,Agent administratif, -Administrator,Administrateur, Admission,Admission, Admission and Enrollment,Admission et inscription, Admissions for {0},Admissions pour {0}, @@ -171,7 +155,6 @@ All Assessment Groups,Tous les Groupes d'Évaluation, All BOMs,Toutes les nomenclatures, All Contacts.,Tous les contacts., All Customer Groups,Tous les Groupes Client, -All Day,Toute la Journée, All Departments,Tous les départements, All Healthcare Service Units,Tous les services de soins de santé, All Item Groups,Tous les Groupes d'Articles, @@ -193,8 +176,6 @@ Already record exists for the item {0},L'enregistrement existe déjà pour l'art "Already set default in pos profile {0} for user {1}, kindly disabled default","Déjà défini par défaut dans le profil pdv {0} pour l'utilisateur {1}, veuillez désactiver la valeur par défaut", Alternate Item,Article alternatif, Alternative item must not be same as item code,L'article alternatif ne doit pas être le même que le code article, -Amended From,Modifié Depuis, -Amount,Montant, Amount After Depreciation,Montant après amortissement, Amount of Integrated Tax,Montant de la taxe intégrée, Amount of TDS Deducted,Quantité de TDS déduite, @@ -216,7 +197,6 @@ Another Period Closing Entry {0} has been made after {1},Une autre Entrée de Cl Another Sales Person {0} exists with the same Employee id,Un autre Commercial {0} existe avec le même ID d'Employé, Antibiotic,Antibiotique, Apparel & Accessories,Vêtements & Accessoires, -Applicable For,Applicable Pour, "Applicable if the company is SpA, SApA or SRL","Applicable si la société est SpA, SApA ou SRL", Applicable if the company is a limited liability company,Applicable si la société est une société à responsabilité limitée, Applicable if the company is an Individual or a Proprietorship,Applicable si la société est un particulier ou une entreprise, @@ -264,15 +244,12 @@ Asset scrapped via Journal Entry {0},Actif mis au rebut via Écriture de Journal Asset {0} does not belong to company {1},L'actif {0} ne fait pas partie à la société {1}, Asset {0} must be submitted,L'actif {0} doit être soumis, Assets,Actifs - Immo., -Assign To,Attribuer À, Associate,Associé, At least one mode of payment is required for POS invoice.,Au moins un mode de paiement est nécessaire pour une facture de PDV, Atleast one item should be entered with negative quantity in return document,Au moins un article doit être saisi avec quantité négative dans le document de retour, Atleast one of the Selling or Buying must be selected,Au moins Vente ou Achat doit être sélectionné, Atleast one warehouse is mandatory,Au moins un entrepôt est obligatoire, Attach Logo,Attacher le logo, -Attachment,Pièce jointe, -Attachments,Pièces jointes, Attendance can not be marked for future dates,La présence ne peut pas être marquée pour les dates à venir, Attendance date can not be less than employee's joining date,Date de présence ne peut pas être antérieure à la date d'embauche de l'employé, Attendance for employee {0} is already marked,La présence de l'employé {0} est déjà marquée, @@ -282,7 +259,6 @@ Attribute table is mandatory,Table d'Attribut est obligatoire, Attribute {0} selected multiple times in Attributes Table,Attribut {0} sélectionné à plusieurs reprises dans le Tableau des Attributs, Authorized Signatory,Signataire Autorisé, Auto Material Requests Generated,Demandes de Matériel Générées Automatiquement, -Auto Repeat,Répétition automatique, Auto repeat document updated,Document de répétition automatique mis à jour, Automotive,Automobile, Available,Disponible, @@ -332,8 +308,6 @@ Banking,Banque, Banking and Payments,Banque et paiements, Barcode {0} already used in Item {1},Le Code Barre {0} est déjà utilisé dans l'article {1}, Barcode {0} is not a valid {1} code,Le code-barres {0} n'est pas un code {1} valide, -Base URL,URL de base, -Based On,Basé Sur, Based On Payment Terms,Basé sur les conditions de paiement, Batch,Lot, Batch Entries,Entrées de lot, @@ -406,7 +380,6 @@ Can only make payment against unbilled {0},Le paiement n'est possible qu'avec le Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total',Peut se référer à ligne seulement si le type de charge est 'Montant de la ligne précedente' ou 'Total des lignes précedente', "Can't change valuation method, as there are transactions against some items which does not have it's own valuation method","Impossible de modifier la méthode de valorisation, car il existe des transactions sur certains articles ne possèdant pas leur propre méthode de valorisation", Can't create standard criteria. Please rename the criteria,Impossible de créer des critères standard. Veuillez renommer les critères, -Cancel,Annuler, Cancel Material Visit {0} before cancelling this Warranty Claim,Annuler la Visite Matérielle {0} avant d'annuler cette Réclamation de Garantie, Cancel Material Visits {0} before cancelling this Maintenance Visit,Annuler les Visites Matérielles {0} avant d'annuler cette Visite de Maintenance, Cancel Subscription,Annuler l'abonnement, @@ -459,8 +432,6 @@ Cash Flow from Operations,Flux de trésorerie provenant des opérations, Cash In Hand,Liquidités, Cash or Bank Account is mandatory for making payment entry,Espèces ou Compte Bancaire est obligatoire pour réaliser une écriture de paiement, Cashier Closing,Fermeture de la caisse, -Category,Catégorie, -Category Name,Nom de la Catégorie, Caution,Mise en garde, Central Tax,Taxe centrale, Certification,Certification, @@ -488,32 +459,22 @@ Child Task exists for this Task. You can not delete this Task.,Une tâche enfant Child nodes can be only created under 'Group' type nodes,Les noeuds enfants peuvent être créés uniquement dans les nœuds de type 'Groupe', Child warehouse exists for this warehouse. You can not delete this warehouse.,Un entrepôt enfant existe pour cet entrepôt. Vous ne pouvez pas supprimer cet entrepôt., Circular Reference Error,Erreur de référence circulaire, -City,Ville, -City/Town,Ville, Clay,Argile, -Clear filters,Effacer les filtres, Clear values,Des valeurs claires, Clearance Date,Date de Compensation, Clearance Date not mentioned,Date de Compensation non indiquée, Clearance Date updated,Date de Compensation mise à jour, -Client,Client, -Client ID,ID Client, -Client Secret,Secret Client, Clinical Procedure,Procédure clinique, Clinical Procedure Template,Modèle de procédure clinique, Close Balance Sheet and book Profit or Loss.,Clôturer Bilan et Compte de Résultats., Close Loan,Prêt proche, Close the POS,Clôturer le point de vente, -Closed,Fermé, Closed order cannot be cancelled. Unclose to cancel.,Les commandes fermées ne peuvent être annulées. Réouvrir pour annuler., Closing (Cr),Fermeture (Cr), Closing (Dr),Fermeture (Dr), Closing (Opening + Total),Fermeture (ouverture + total), Closing Account {0} must be of type Liability / Equity,Le Compte Clôturé {0} doit être de type Passif / Capitaux Propres, Closing Balance,Solde de clôture, -Code,Code, -Collapse All,Tout réduire, -Color,Couleur, Colour,Couleur, Combined invoice portion must equal 100%,La portion combinée de la facture doit être égale à 100%, Commercial,Commercial, @@ -525,7 +486,6 @@ Community Forum,Forum de la communauté, Company (not Customer or Supplier) master.,Données de base de la Société (ni les Clients ni les Fournisseurs), Company Abbreviation,Abréviation de la Société, Company Abbreviation cannot have more than 5 characters,L'abréviation de l'entreprise ne peut pas comporter plus de 5 caractères, -Company Name,Nom de la Société, Company Name cannot be Company,Nom de la Société ne peut pas être Company, Company currencies of both the companies should match for Inter Company Transactions.,Les devises des deux sociétés doivent correspondre pour les transactions inter-sociétés., Company is manadatory for company account,La société est le maître d'œuvre du compte d'entreprise, @@ -535,7 +495,6 @@ Compensatory leave request days not in valid holidays,Les jours de la demande de Complaint,Plainte, Completion Date,Date d'Achèvement, Computer,Ordinateur, -Condition,Conditions, Configure,Configurer, Configure {0},Configurer {0}, Confirmed orders from Customers.,Commandes confirmées des clients., @@ -552,11 +511,8 @@ Consumed,Consommé, Consumed Amount,Montant Consommé, Consumed Qty,Qté Consommée, Consumer Products,Produits de Consommation, -Contact,Contact, Contact Us,Contactez nous, -Content,Contenu, Content Masters,Masters de contenu, -Content Type,Type de Contenu, Continue Configuration,Continuer la configuration, Contract,Contrat, Contract End Date must be greater than Date of Joining,La Date de Fin de Contrat doit être supérieure à la Date d'Embauche, @@ -597,7 +553,6 @@ Course Enrollment {0} does not exists,L'inscription au cours {0} n'existe pas, Course Schedule,Horaire du cours, Course: ,Cours:, Cr,Cr, -Create,Créer, Create BOM,Créer une nomenclature, Create Delivery Trip,Créer un voyage de livraison, Create Employee,Créer un employé, @@ -673,8 +628,6 @@ Current BOM and New BOM can not be same,La nomenclature actuelle et la nouvelle Current Liabilities,Dettes Actuelles, Current Qty,Qté actuelle, Current invoice {0} is missing,La facture en cours {0} est manquante, -Custom HTML,HTML Personnalisé, -Custom?,Personnaliser ?, Customer,Client, Customer Addresses And Contacts,Adresses et Contacts des Clients, Customer Contact,Contact client, @@ -699,7 +652,6 @@ Daily Reminders,Rappels quotidiens, Data Import and Export,Importer et Exporter des Données, Data Import and Settings,Importation de données et paramètres, Database of potential customers.,Base de données de clients potentiels., -Date Format,Format de Date, Date Of Retirement must be greater than Date of Joining,La Date de Départ à la Retraite doit être supérieure à Date d'Embauche, Date of Birth,Date de naissance, Date of Birth cannot be greater than today.,Date de Naissance ne peut être après la Date du Jour., @@ -707,7 +659,6 @@ Date of Commencement should be greater than Date of Incorporation,La date de dé Date of Joining,Date d'Embauche, Date of Joining must be greater than Date of Birth,La Date d'Embauche doit être après à la Date de Naissance, Date of Transaction,Date de transaction, -Day,Jour, Debit,Débit, Debit ({0}),Débit ({0}), Debit Account,Compte de débit, @@ -723,14 +674,12 @@ Default Activity Cost exists for Activity Type - {0},Un Coût d’Activité par Default BOM ({0}) must be active for this item or its template,Nomenclature par défaut ({0}) doit être actif pour ce produit ou son modèle, Default BOM for {0} not found,Nomenclature par défaut {0} introuvable, Default BOM not found for Item {0} and Project {1},La nomenclature par défaut n'a pas été trouvée pour l'Article {0} et le Projet {1}, -Default Letter Head,En-Tête de Courrier par Défaut, Default Tax Template,Modèle de Taxes par Défaut, Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,L’Unité de Mesure par Défaut pour l’Article {0} ne peut pas être modifiée directement parce que vous avez déjà fait une (des) transaction (s) avec une autre unité de mesure. Vous devez créer un nouvel article pour utiliser une UdM par défaut différente., Default Unit of Measure for Variant '{0}' must be same as in Template '{1}',L’Unité de mesure par défaut pour la variante '{0}' doit être la même que dans le Modèle '{1}', Default settings for buying transactions.,Paramètres par défaut pour les transactions d'achat., Default settings for selling transactions.,Paramètres par défaut pour les transactions de vente., Default tax templates for sales and purchase are created.,Les modèles de taxe par défaut pour les ventes et les achats sont créés., -Defaults,Valeurs Par Défaut, Defense,Défense, Define Project type.,Définir le type de projet., Define budget for a financial year.,Définir le budget pour un exercice., @@ -750,10 +699,8 @@ Delivery Note {0} is not submitted,Bon de Livraison {0} n'est pas soumis, Delivery Note {0} must not be submitted,Bon de Livraison {0} ne doit pas être soumis, Delivery Notes {0} must be cancelled before cancelling this Sales Order,Bons de Livraison {0} doivent être annulés avant d’annuler cette Commande Client, Delivery Notes {0} updated,Notes de livraison {0} mises à jour, -Delivery Status,Statut de la Livraison, Delivery Trip,Service de Livraison, Delivery warehouse required for stock item {0},Entrepôt de Livraison requis pour article du stock {0}, -Department,Département, Department Stores,Grands magasins, Depreciation,Amortissement, Depreciation Amount,Montant d'Amortissement, @@ -768,7 +715,6 @@ Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date,Ligne d'amortissement {0}: la date d'amortissement suivante ne peut pas être antérieure à la date d'achat, Designer,Designer, Detailed Reason,Raison détaillée, -Details,Détails, Details of Outward Supplies and inward supplies liable to reverse charge,Détails des livraisons sortantes et des livraisons entrantes susceptibles d'inverser la charge, Details of the operations carried out.,Détails des opérations effectuées., Diagnosis,Diagnostique, @@ -805,16 +751,11 @@ Doc Date,Date du document, Doc Name,Nom du document, Doc Type,Type de document, Docs Search,Recherche de documents, -Document Name,Nom du Document, -Document Type,Type de Document, -Domain,Domaine, -Domains,Domaines, Done,Terminé, Donor,Donneur, Donor Type information.,Informations sur le type de donneur., Donor information.,Informations sur le donneur, Download JSON,Télécharger JSON, -Draft,Brouillon, Drop Ship,Expédition Directe, Drug,Médicament, Due / Reference Date cannot be after {0},Date d’échéance / de référence ne peut pas être après le {0}, @@ -835,7 +776,6 @@ ERPNext Demo,Démo ERPNext, ERPNext Settings,Paramètres ERPNext, Earliest,Au plus tôt, Earnest Money,Arrhes, -Edit,modifier, Edit Publishing Details,Modifier les détails de publication, "Edit in full page for more options like assets, serial nos, batches etc.","Modifier en pleine page pour plus d'options comme les actifs, les numéros de série, les lots, etc.", Education,Éducation, @@ -846,13 +786,9 @@ Electrical,Électrique, Electronic Equipments,Équipements électroniques, Electronics,Électronique, Eligible ITC,CTI éligible, -Email Account,Compte Email, -Email Address,Adresse électronique, "Email Address must be unique, already exists for {0}","Adresse Email doit être unique, existe déjà pour {0}", Email Digest: ,Compte Rendu par Email :, Email Reminders will be sent to all parties with email contacts,Les rappels par emails seront envoyés à toutes les parties avec des contacts ayant une adresse email, -Email Sent,Email Envoyé, -Email Template,Modèle d'email, Email not found in default contact,Email non trouvé dans le contact par défaut, Email sent to {0},Email envoyé à {0}, Employee,Employé, @@ -866,9 +802,7 @@ Employee cannot report to himself.,L'employé ne peut pas rendre de compte à lu Employee {0} has already applied for {1} between {2} and {3} : ,L'employé {0} a déjà postulé pour {1} entre {2} et {3}:, Employee {0} of grade {1} have no default leave policy,L'employé {0} avec l'échelon {1} n'a pas de politique de congé par défaut, Enable / disable currencies.,Activer / Désactiver les devises, -Enabled,Activé, "Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart","Activation de 'Utiliser pour Panier', comme le Panier est activé et qu'il devrait y avoir au moins une Règle de Taxes pour le Panier", -End Date,Date de Fin, End Date can not be less than Start Date,La date de fin ne peut être inférieure à la date de début, End Date cannot be before Start Date.,La date de fin ne peut pas être antérieure à la date de début., End Year,Année de Fin, @@ -889,7 +823,6 @@ Enter value betweeen {0} and {1},Entrez une valeur entre {0} et {1}, Entertainment & Leisure,Divertissement et Loisir, Entertainment Expenses,Charges de Représentation, Equity,Capitaux Propres, -Error Log,Journal des Erreurs, Error evaluating the criteria formula,Erreur lors de l'évaluation de la formule du critère, Error in formula or condition: {0},Erreur dans la formule ou dans la condition : {0}, Error: Not a valid id?,Erreur : Pas un identifiant valide ?, @@ -901,7 +834,6 @@ Exchange Rate must be same as {0} {1} ({2}),Taux de Change doit être le même q Excise Invoice,Facture d'Accise, Execution,Exécution, Executive Search,Recrutement de Cadres, -Expand All,Développer Tout, Expected Delivery Date,Date de livraison prévue, Expected Delivery Date should be after Sales Order Date,La Date de Livraison Prévue doit être après la Date indiquée sur la Commande Client, Expected End Date,Date de fin prévue, @@ -924,30 +856,22 @@ Explore,Explorer, Export E-Invoices,Exporter des factures électroniques, Extra Large,Extra large, Extra Small,Très Petit, -Fail,Échec, -Failed,Échoué, Failed to create website,Échec de la création du site Web, Failed to install presets,Échec de l'installation des préréglages, Failed to login,Échec de la connexion, Failed to setup company,Échec de la configuration de la société, Failed to setup defaults,Échec de la configuration par défaut, Failed to setup post company fixtures,Échec de la configuration des éléments liés la société, -Fax,Fax, Fee,Frais, Fee Created,Honoraires Créés, Fee Creation Failed,La création des honoraires a échoué, Fee Creation Pending,Création d'honoraires en attente, Fee Records Created - {0},Archive d'Honoraires Créée - {0}, -Feedback,Retour d’Expérience, Fees,Honoraires, -Female,Féminin, Fetch Data,Récupérer des données, Fetch Subscription Updates,Vérifier les mises à jour des abonnements, Fetch exploded BOM (including sub-assemblies),Récupérer la nomenclature éclatée (y compris les sous-ensembles), Fetching records......,Récupération des enregistrements ......, -Field Name,Nom du Champ, -Fieldname,Nom du Champ, -Fields,Champ, "Filter Fields Row #{0}: Fieldname {1} must be of type ""Link"" or ""Table MultiSelect""",Filtrer les champs Ligne # {0}: le nom de champ {1} doit être de type "Lien" ou "Table MultiSelect", Filter Total Zero Qty,Filtrer les totaux pour les qtés égales à zéro, Finance Book,Livre comptable, @@ -961,7 +885,6 @@ Finished Good Item Code,Code d'article fini, Finished Goods,Produits finis, Finished Item {0} must be entered for Manufacture type entry,Le Produit Fini {0} doit être saisi pour une écriture de type Production, Finished product quantity {0} and For Quantity {1} cannot be different,La quantité de produit fini {0} et Pour la quantité {1} ne peut pas être différente, -First Name,Prénom, "Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}","Le régime fiscal est obligatoire, veuillez définir le régime fiscal de l'entreprise {0}", Fiscal Year,Exercice fiscal, Fiscal Year End Date should be one year after Fiscal Year Start Date,La date de fin d'exercice doit être un an après la date de début d'exercice, @@ -994,9 +917,6 @@ For row {0}: Enter Planned Qty,Pour la ligne {0}: entrez la quantité planifiée Forum Activity,Activité du forum, Free item code is not selected,Le code d'article gratuit n'est pas sélectionné, Freight and Forwarding Charges,Frais de Fret et d'Expédition, -Frequency,Fréquence, -Friday,Vendredi, -From,À partir de, From Address 1,Ligne d'addresse 1 (Origine), From Address 2,Ligne d'addresse 2 (Origine), From Currency and To Currency cannot be same,La Devise de Base et la Devise de Cotation ne peuvent pas identiques, @@ -1021,18 +941,15 @@ From and To dates required,Les date Du et Au sont requises, From value must be less than to value in row {0},De la valeur doit être inférieure à la valeur de la ligne {0}, From {0} | {1} {2},Du {0} | {1} {2}, Fulfillment,Livraison, -Full Name,Nom Complet, Fully Depreciated,Complètement Déprécié, Furnitures and Fixtures,Meubles et Accessoires, "Further accounts can be made under Groups, but entries can be made against non-Groups","D'autres comptes individuels peuvent être créés dans les groupes, mais les écritures ne peuvent être faites que sur les comptes individuels", Further cost centers can be made under Groups but entries can be made against non-Groups,"D'autres centres de coûts peuvent être créés dans des Groupes, mais des écritures ne peuvent être faites que sur des centres de coûts individuels.", -Further nodes can be only created under 'Group' type nodes,D'autres nœuds peuvent être créés uniquement sous les nœuds de type 'Groupe', GSTIN,GSTIN, GSTR3B-Form,GSTR3B-Form, Gain/Loss on Asset Disposal,Gain/Perte sur Cessions des Immobilisations, Gantt Chart,Diagramme de Gantt, Gantt chart of all tasks.,Diagramme de Gantt de toutes les tâches., -Gender,Sexe, General,Général, General Ledger,Grand Livre, Generate Material Requests (MRP) and Work Orders.,Générer des demandes de matériel (MRP) et des ordres de travail., @@ -1050,7 +967,6 @@ Get Updates,Obtenir les mises à jour, Get customers from,Obtenir les clients de, Get from Patient Encounter,Obtenez de la rencontre du patient, Getting Started,Commencer, -GitHub Sync ID,GitHub Sync ID, Global settings for all manufacturing processes.,Paramètres globaux pour tous les processus de production., Go to the Desktop and start using ERPNext,Accédez au bureau et commencez à utiliser ERPNext, GoCardless SEPA Mandate,Mandat SEPA GoCardless, @@ -1090,8 +1006,6 @@ Guardian2 Name,Nom du Tuteur 2, HR Manager,Responsable RH, HSN,HSN, HSN/SAC,HSN / SAC, -Half Yearly,Semestriel, -Half-Yearly,Semestriel, Hardware,Matériel, Head of Marketing and Sales,Responsable du Marketing et des Ventes, Health Care,Soins de santé, @@ -1106,7 +1020,6 @@ Healthcare Service Unit Type,Type d'unité de service de soins de santé, Healthcare Services,Services de santé, Healthcare Settings,Paramètres de santé, Help Results for,Aide Résultats pour, -High,Haut, High Sensitivity,Haute sensibilité, Hold,Mettre en attente, Hold Invoice,Facture en attente, @@ -1114,15 +1027,12 @@ Holiday,Vacances, Holiday List,Liste de vacances, Hotel Rooms of type {0} are unavailable on {1},Les chambres d'hôtel de type {0} sont indisponibles le {1}, Hotels,Hôtels, -Hourly,Horaire, Hours,Heures, How Pricing Rule is applied?,Comment la Règle de Prix doit-elle être appliquée ?, Hub Category,Catégorie du Hub, -Hub Sync ID,Hub Sync ID, Human Resource,Ressource humaine, Human Resources,Ressources humaines, IGST Amount,IGST Montant, -IP Address,Adresse IP, ITC Available (whether in full op part),CIT Disponible (que ce soit en partie op), ITC Reversed,CTI inversé, Identifying Decision Makers,Identifier les décideurs, @@ -1133,11 +1043,7 @@ Identifying Decision Makers,Identifier les décideurs, "If unlimited expiry for the Loyalty Points, keep the Expiry Duration empty or 0.","Si vous souhaitez ne pas mettre de date d'expiration pour les points de fidélité, laissez la durée d'expiration vide ou mettez 0.", "If you have any questions, please get back to us.","Si vous avez des questions, veuillez revenir vers nous.", Ignore Existing Ordered Qty,Ignorer la quantité commandée existante, -Image,Image, -Image View,Voir l'Image, -Import Data,Importer des données, Import Day Book Data,Données du journal d'importation, -Import Log,Journal d'import, Import Master Data,Importer des données de base, Import in Bulk,Importer en Masse, Import of goods,Importation de marchandises, @@ -1151,7 +1057,6 @@ In Stock Qty,Qté En Stock, In Stock: ,En Stock :, In Value,En valeur, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Dans le cas d'un programme à plusieurs échelons, les clients seront automatiquement affectés au niveau approprié en fonction de leurs dépenses", -Inactive,Inactif, Incentives,Incitations, Include Default Book Entries,Inclure les entrées de livre par défaut, Include Exploded Items,Inclure les articles éclatés, @@ -1185,7 +1090,6 @@ Integrated Tax,Taxe intégrée, Inter-State Supplies,Fournitures inter-Etats, Internet Publishing,Publication Internet, Intra-State Supplies,Fournitures intra-étatiques, -Introduction,Introduction, Invalid Attribute,Attribut invalide, Invalid Blanket Order for the selected Customer and Item,Commande avec limites non valide pour le client et l'article sélectionnés, Invalid Company for Inter Company Transaction.,Société non valide pour une transaction inter-sociétés., @@ -1217,8 +1121,6 @@ Invoices,Factures, Invoices for Costumers.,Factures pour les clients., Inward supplies from ISD,Approvisionnement entrant de la DSI, Inward supplies liable to reverse charge (other than 1 & 2 above),Approvisionnements entrants susceptibles d’être dédouanés (autres que 1 et 2 ci-dessus), -Is Active,Est Active, -Is Default,Est Défaut, Is Existing Asset,Est Actif Existant, Is Frozen,Est gelé, Is Group,Est un Groupe, @@ -1288,7 +1190,6 @@ Join,Joindre, Journal Entries {0} are un-linked,Les Écritures de Journal {0} ne sont pas liées, Journal Entry,Écriture de Journal, Journal Entry {0} does not have account {1} or already matched against other voucher,L’Écriture de Journal {0} n'a pas le compte {1} ou est déjà réconciliée avec une autre pièce justificative, -Kanban Board,Tableau Kanban, Key Reports,Rapports clés, LMS Activity,Activité LMS, Lab Test,Test de laboratoire, @@ -1299,12 +1200,10 @@ Lab Test UOM,UdM de test de laboratoire, Lab Tests and Vital Signs,Tests de laboratoire et signes vitaux, Lab result datetime cannot be before testing datetime,La date et l'heure du résultat de laboratoire ne peuvent pas être avant la date et l'heure du test, Lab testing datetime cannot be before collection datetime,La date et l'heure du test de laboratoire ne peuvent pas être avant la date et l'heure de collecte, -Label,Étiquette, Laboratory,Laboratoire, Large,Grand, Last Communication,Dernière communication, Last Communication Date,Date de la Dernière Communication, -Last Name,Nom de Famille, Last Order Amount,Montant de la Dernière Commande, Last Order Date,Date de la dernière commande, Last Purchase Price,Dernier prix d'achat, @@ -1326,9 +1225,7 @@ Leaves must be allocated in multiples of 0.5,"Les Congés doivent être alloués Ledger,Livre, Legal,Juridique, Legal Expenses,Frais juridiques, -Letter Head,En-Tête, Letter Heads for print templates.,En-Têtes pour les modèles d'impression., -Level,Niveau, Liability,Passif, Limit Crossed,Limite Dépassée, Link to Material Request,Lien vers la demande de matériel, @@ -1343,7 +1240,6 @@ Local,Locale, Logs for maintaining sms delivery status,Journaux pour maintenir le statut de livraison des sms, Lost,Perdu, Lost Reasons,Raisons perdues, -Low,Bas, Low Sensitivity,Faible sensibilité, Lower Income,Revenu bas, Loyalty Amount,Montant de fidélité, @@ -1355,13 +1251,11 @@ Loyalty Program,Programme de fidélité, Main,Principal, Maintenance,Entretien, Maintenance Log,Journal de maintenance, -Maintenance Manager,Responsable de Maintenance, Maintenance Schedule,Échéancier d'Entretien, Maintenance Schedule is not generated for all the items. Please click on 'Generate Schedule',L'Échéancier d'Entretien n'est pas créé pour tous les articles. Veuillez clicker sur 'Créer un Échéancier', Maintenance Schedule {0} exists against {1},Un Calendrier de Maintenance {0} existe pour {1}, Maintenance Schedule {0} must be cancelled before cancelling this Sales Order,L'Échéancier d'Entretien {0} doit être annulé avant d'annuler cette Commande Client, Maintenance Status has to be Cancelled or Completed to Submit,Le statut de maintenance doit être annulé ou complété pour pouvoir être envoyé, -Maintenance User,Maintenance Utilisateur, Maintenance Visit,Visite d'Entretien, Maintenance Visit {0} must be cancelled before cancelling this Sales Order,La Visite d'Entretien {0} doit être annulée avant d'annuler cette Commande Client, Maintenance start date can not be before delivery date for Serial No {0},La date de début d'entretien ne peut pas être antérieure à la date de livraison pour le N° de Série {0}, @@ -1369,7 +1263,6 @@ Make,Faire, Make Payment,Faire un Paiement, Make project from a template.,Faire un projet à partir d'un modèle., Making Stock Entries,Faire des Écritures de Stock, -Male,Masculin, Manage Customer Group Tree.,Gérer l'Arborescence des Groupes de Clients., Manage Sales Partners.,Gérer les Partenaires Commerciaux., Manage Sales Person Tree.,Gérer l'Arborescence des Vendeurs., @@ -1379,7 +1272,6 @@ Management,Gestion, Manager,Directeur, Managing Projects,Gestion de Projets, Managing Subcontracting,Gestion de la Sous-traitance, -Mandatory,Obligatoire, Mandatory field - Academic Year,Champ Obligatoire - Année Académique, Mandatory field - Get Students From,Champ Obligatoire - Obtenir des étudiants de, Mandatory field - Program,Champ obligatoire - Programme, @@ -1388,8 +1280,6 @@ Manufacturer,Fabricant, Manufacturer Part Number,Numéro de Pièce du Fabricant, Manufacturing,Production, Manufacturing Quantity is mandatory,Quantité de production obligatoire, -Mapping,Mapping, -Mapping Type,Type de Mapping, Mark Absent,Marquer Absent, Mark Half Day,Marquer Demi-Journée, Mark Present,Marquer Présent, @@ -1424,7 +1314,6 @@ Medical Code,Code médical, Medical Code Standard,Standard du code médical, Medical Department,Département médical, Medical Record,Dossier médical, -Medium,Moyen, Member Activity,Activité des membres, Member ID,ID du membre, Member Name,Nom de membre, @@ -1439,12 +1328,8 @@ Merge,Fusionner, Merge Account,Fusionner le compte, Merge with Existing Account,Fusionner avec un compte existant, "Merging is only possible if following properties are same in both records. Is Group, Root Type, Company","La combinaison est possible seulement si les propriétés suivantes sont les mêmes dans les deux dossiers. Est Groupe, Type de Racine, Société", -Message Examples,Exemples de Messages, Message Sent,Message envoyé, -Method,Méthode, Middle Income,Revenu Intermédiaire, -Middle Name,Deuxième Nom, -Middle Name (Optional),Deuxième Prénom (Optionnel), Min Amt can not be greater than Max Amt,Min Amt ne peut pas être supérieur à Max Amt, Min Qty can not be greater than Max Qty,Qté Min ne peut pas être supérieure à Qté Max, Minimum Lead Age (Days),Âge Minimum du lead (Jours), @@ -1458,14 +1343,9 @@ Mode of Transport,Mode de transport, Mode of Transportation,Mode de transport, Model,Modèle, Moderate Sensitivity,Sensibilité modérée, -Monday,Lundi, -Monthly,Mensuel, Monthly Distribution,Répartition Mensuelle, -More,Plus, -More Information,Informations Complémentaires, More...,Plus..., Motion Picture & Video,Cinéma & Vidéo, -Move,mouvement, Move Item,Déplacer l'Article, Multi Currency,Multi-devise, Multiple Item prices.,Plusieurs Prix d'Articles., @@ -1497,7 +1377,6 @@ Net ITC Available(A) - (B),CTI net disponible (A) - (B), Net Profit,Bénéfice net, Net Total,Total net, New Account Name,Nouveau Nom de Compte, -New Address,Nouvelle adresse, New BOM,Nouvelle nomenclature, New Batch ID (Optional),Nouveau Numéro de Lot (Optionnel), New Batch Qty,Nouvelle Qté de Lot, @@ -1517,13 +1396,11 @@ New credit limit is less than current outstanding amount for the customer. Credi New task,Nouvelle tâche, New {0} pricing rules are created,De nouvelles règles de tarification {0} sont créées., Newspaper Publishers,Éditeurs de journaux, -Next,Suivant, Next Contact By cannot be same as the Lead Email Address,Prochain Contact Par ne peut être identique à l’Adresse Email du Lead, Next Contact Date cannot be in the past,La Date de Prochain Contact ne peut pas être dans le passé, Next Steps,Prochaines étapes, No Action,Pas d'action, No Customers yet!,Pas encore de clients!, -No Data,Aucune Donnée, No Delivery Note selected for Customer {},Aucun bon de livraison sélectionné pour le client {}, No Item with Barcode {0},Aucun Article avec le Code Barre {0}, No Item with Serial No {0},Aucun Article avec le N° de Série {0}, @@ -1565,15 +1442,12 @@ Non Profit,À But Non Lucratif, Non Profit (beta),Association (bêta), Non-GST outward supplies,Fournitures sortantes non liées à la TPS, Non-Group to Group,Non-Groupe à Groupe, -None,Aucun, None of the items have any change in quantity or value.,Aucun des Articles n’a de changement en quantité ou en valeur., Nos,N°, Not Available,Indisponible, Not Marked,Non marqué, Not Paid and Not Delivered,Non payé et non livré, -Not Permitted,Non Autorisé, Not Started,Non Commencé, -Not active,Non actif, Not allow to set alternative item for the item {0},Ne permet pas de définir un autre article pour l'article {0}, Not allowed to update stock transactions older than {0},Non autorisé à mettre à jour les transactions du stock antérieures à {0}, Not authorized to edit frozen Account {0},Vous n'êtes pas autorisé à modifier le compte gelé {0}, @@ -1592,7 +1466,6 @@ Notes,Remarques, Nothing is included in gross,Rien n'est inclus dans le brut, Nothing more to show.,Rien de plus à montrer., Notify Customers via Email,Avertir les clients par courrier électronique, -Number,Nombre, Number of Depreciations Booked cannot be greater than Total Number of Depreciations,Nombre d’Amortissements Comptabilisés ne peut pas être supérieur à Nombre Total d'Amortissements, Number of Interaction,Nombre d'Interactions, Number of Order,Nombre de Commandes, @@ -1634,7 +1507,6 @@ Opening Stock,Stock d'Ouverture, Opening Stock Balance,Solde d'Ouverture des Stocks, Opening Value,Valeur d'Ouverture, Opening {0} Invoice created,Ouverture {0} Facture créée, -Operation,Opération, Operation Time must be greater than 0 for Operation {0},Temps de l'Opération doit être supérieur à 0 pour l'Opération {0}, "Operation {0} longer than any available working hours in workstation {1}, break down the operation into multiple operations","Opération {0} plus longue que toute heure de travail disponible dans la station de travail {1}, veuillez séparer l'opération en plusieurs opérations", Operations,Opérations, @@ -1647,7 +1519,6 @@ Opportunity,Opportunité, Opportunity Amount,Montant de l'opportunité, "Optional. Sets company's default currency, if not specified.","Optionnel. Défini la devise par défaut de l'entreprise, si non spécifié.", Optional. This setting will be used to filter in various transactions.,Facultatif. Ce paramètre sera utilisé pour filtrer différentes transactions., -Options,Options, Order Count,Compte de Commandes, Order Entry,Saisie de Commande, Order Value,Valeur de la commande, @@ -1660,9 +1531,9 @@ Orders,Commandes, Orders released for production.,Commandes validées pour la production., Organization,Organisation, Organization Name,Nom de l'Organisation, -Other,Autre, Other Reports,Autres rapports, "Other outward supplies(Nil rated,Exempted)","Autres livraisons sortantes (cotations nulles, exemptées)", +Others,Autres, Out Qty,Qté Sortante, Out Value,Valeur Sortante, Out of Order,Hors service, @@ -1676,7 +1547,6 @@ Outward taxable supplies(zero rated),Fournitures taxables à la sortie (détaxé Overdue,En retard, Overlap in scoring between {0} and {1},Chevauchement dans la notation entre {0} et {1}, Overlapping conditions found between:,Conditions qui coincident touvées entre :, -Owner,Responsable, PAN,Numéro de compte permanent (PAN), POS,PDV, POS Profile,Profil PDV, @@ -1691,7 +1561,6 @@ Paid Amount,Montant payé, Paid Amount cannot be greater than total negative outstanding amount {0},Le Montant Payé ne peut pas être supérieur au montant impayé restant {0}, Paid amount + Write Off Amount can not be greater than Grand Total,Le Montant Payé + Montant Repris ne peut pas être supérieur au Total Général, Paid and Not Delivered,Payé et non livré, -Parameter,Paramètre, Parent Item {0} must not be a Stock Item,L'Article Parent {0} ne doit pas être un Élément de Stock, Parents Teacher Meeting Attendance,Participation à la réunion parents-professeurs, Partially Depreciated,Partiellement déprécié, @@ -1751,7 +1620,6 @@ Pending activities for today,Activités en Attente pour aujourd'hui, Pension Funds,Fonds de Pension, Percentage Allocation should be equal to 100%,Pourcentage d'Allocation doit être égale à 100 %, Perception Analysis,Analyse de perception, -Period,Période, Period Closing Entry,Écriture de Clôture de la Période, Period Closing Voucher,Bon de Clôture de la Période, Periodicity,Périodicité, @@ -1778,7 +1646,6 @@ Please create purchase receipt or purchase invoice for the item {0},Veuillez cr Please define grade for Threshold 0%,Veuillez définir une note pour le Seuil 0%, Please enable Applicable on Booking Actual Expenses,Veuillez activer l'option : Applicable sur la base de l'enregistrement des dépenses réelles, Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses,Veuillez activer les options : Applicable sur la base des bons de commande d'achat et Applicable sur la base des bons de commande d'achat, -Please enable pop-ups,Veuillez autoriser les pop-ups, Please enter 'Is Subcontracted' as Yes or No,Veuillez entrer Oui ou Non pour 'Est sous-traitée', Please enter API Consumer Key,"Veuillez entrer la clé ""API Consumer Key""", Please enter API Consumer Secret,"Veuillez entrer la clé ""API Consumer Secret""", @@ -1834,7 +1701,6 @@ Please select BOM for Item in Row {0},Veuillez sélectionnez une nomenclature po Please select BOM in BOM field for Item {0},Veuillez sélectionner une nomenclature dans le champ nomenclature pour l’Article {0}, Please select Category first,Veuillez d’abord sélectionner une Catégorie, Please select Charge Type first,Veuillez d’abord sélectionner le Type de Facturation, -Please select Company,Veuillez sélectionner une Société, Please select Company and Posting Date to getting entries,Veuillez sélectionner la société et la date de comptabilisation pour obtenir les écritures, Please select Company first,Veuillez d’abord sélectionner une Société, Please select Completion Date for Completed Asset Maintenance Log,Veuillez sélectionner la date d'achèvement pour le journal de maintenance des actifs terminé, @@ -1875,7 +1741,6 @@ Please select the Multiple Tier Program type for more than one collection rules. Please select the assessment group other than 'All Assessment Groups',Sélectionnez un groupe d'évaluation autre que «Tous les Groupes d'Évaluation», Please select the document type first,Veuillez d’abord sélectionner le type de document, Please select weekly off day,Veuillez sélectionnez les jours de congé hebdomadaires, -Please select {0},Veuillez sélectionner {0}, Please select {0} first,Veuillez d’abord sélectionner {0}, Please set 'Apply Additional Discount On',Veuillez définir ‘Appliquer Réduction Supplémentaire Sur ‘, Please set 'Asset Depreciation Cost Center' in Company {0},Veuillez définir 'Centre de Coûts des Amortissements d’Actifs’ de la Société {0}, @@ -1941,7 +1806,6 @@ Prescription Dosage,Dosage de la prescription, Prescription Duration,Durée de la prescription, Prescriptions,Les prescriptions, Prev,Précédent, -Preview,Aperçu, Previous Financial Year is not closed,L’Exercice Financier Précédent n’est pas fermé, Price,Prix, Price List,Liste de prix, @@ -1959,13 +1823,11 @@ Pricing Rule {0} is updated,La règle de tarification {0} est mise à jour, Pricing Rules are further filtered based on quantity.,Les Règles de Tarification sont d'avantage filtrés en fonction de la quantité., Primary Address Details,Détails de l'adresse principale, Primary Contact Details,Détails du contact principal, -Print Format,Format d'Impression, Print IRS 1099 Forms,Imprimer les formulaires IRS 1099, Print Report Card,Imprimer le rapport, Print Settings,Paramètres d'impression, Print and Stationery,Impression et Papeterie, Print settings updated in respective print format,Paramètres d'impression mis à jour avec le format d'impression indiqué, -Print taxes with zero amount,Impression de taxes avec un montant nul, Printing and Branding,Impression et Marque, Private Equity,Capital Investissement, Procedure,Procédure, @@ -2012,15 +1874,12 @@ Prospecting,Prospection, Provisional Profit / Loss (Credit),Gain / Perte (Crédit) Provisoire, Publications,Des publications, Publish Items on Website,Publier les Articles sur le Site Web, -Published,Publié, Publishing,Édition, Purchase,achat, Purchase Amount,Montant de l'Achat, Purchase Date,Date d'Achat, Purchase Invoice,Facture d’Achat, Purchase Invoice {0} is already submitted,La Facture d’Achat {0} est déjà soumise, -Purchase Manager,Responsable des Achats, -Purchase Master Manager,Responsable des Données d’Achats, Purchase Order,Commande d'Achat, Purchase Order Amount,Montant de la Commande d'Achat, Purchase Order Amount(Company Currency),Montant de la Commande d'Achat (devise de la société), @@ -2035,7 +1894,6 @@ Purchase Price List,Liste des Prix d'Achat, Purchase Receipt,Reçu d’Achat, Purchase Receipt {0} is not submitted,Le Reçu d’Achat {0} n'est pas soumis, Purchase Tax Template,Modèle de Taxes pour les Achats, -Purchase User,Utilisateur Acheteur, Purchase orders help you plan and follow up on your purchases,Les Bons de Commande vous aider à planifier et à assurer le suivi de vos achats, Purchasing,Achat, Purpose must be one of {0},L'Objet doit être parmi {0}, @@ -2065,7 +1923,6 @@ Quantity to Make,Quantité à faire, Quantity to Manufacture must be greater than 0.,La quantité à produire doit être supérieur à 0., Quantity to Produce,Quantité à produire, Quantity to Produce can not be less than Zero,La quantité à produire ne peut être inférieure à zéro, -Query Options,Options de Requête, Queued for replacing the BOM. It may take a few minutes.,En file d'attente pour remplacer la nomenclature. Cela peut prendre quelques minutes., Queued for updating latest price in all Bill of Materials. It may take a few minutes.,Mise à jour des prix les plus récents dans toutes les nomenclatures en file d'attente. Cela peut prendre quelques minutes., Quick Journal Entry,Écriture Rapide dans le Journal, @@ -2080,10 +1937,8 @@ Quotations received from Suppliers.,Devis reçus des Fournisseurs., Quotations: ,Devis :, Quotes to Leads or Customers.,Devis de Lead ou Clients., RFQs are not allowed for {0} due to a scorecard standing of {1},Les Appels d'Offres ne sont pas autorisés pour {0} en raison d'une note de {1} sur la fiche d'évaluation, -Range,Plage, Rate,Prix, Rate:,Prix:, -Rating,Évaluation, Raw Material,Matières Premières, Raw Materials,Matières premières, Raw Materials cannot be blank.,Matières Premières ne peuvent pas être vides., @@ -2099,35 +1954,25 @@ Receipt,Reçu, Receipt document must be submitted,Le reçu doit être soumis, Receivable,Créance, Receivable Account,Compte Débiteur, -Received,Reçu, Received On,Reçu le, Received Quantity,Quantité reçue, Received Stock Entries,Entrées de stock reçues, Receiver List is empty. Please create Receiver List,La Liste de Destinataires est vide. Veuillez créer une Liste de Destinataires, -Recipients,Destinataires, Reconcile,Réconcilier, "Record of all communications of type email, phone, chat, visit, etc.","Enregistrement de toutes les communications de type email, téléphone, chat, visite, etc.", Records,Dossiers, -Redirect URL,URL de Redirection, Ref,Ref, Ref Date,Date de Réf., -Reference,Référence, Reference #{0} dated {1},Référence #{0} datée du {1}, -Reference Date,Date de Référence, Reference Doctype must be one of {0},Doctype de la Référence doit être parmi {0}, -Reference Document,Document de Référence, -Reference Document Type,Type du document de référence, Reference No & Reference Date is required for {0},N° et Date de Référence sont nécessaires pour {0}, Reference No and Reference Date is mandatory for Bank transaction,Le N° de Référence et la Date de Référence sont nécessaires pour une Transaction Bancaire, Reference No is mandatory if you entered Reference Date,N° de Référence obligatoire si vous avez entré une date, Reference No.,Numéro de référence, Reference Number,Numéro de réference, -Reference Type,Type de référence, "Reference: {0}, Item Code: {1} and Customer: {2}","Référence: {0}, Code de l'article: {1} et Client: {2}", References,Références, -Refresh Token,Jeton de Rafraîchissement, Register,registre, -Rejected,Rejeté, Related,en relation, Relation with Guardian1,Relation avec Tuteur1, Relation with Guardian2,Relation avec Tuteur2, @@ -2139,17 +1984,12 @@ Remarks,Remarques, Reminder to update GSTIN Sent,Rappel pour mettre à jour GSTIN envoyé, Remove item if charges is not applicable to that item,Retirer l'article si les charges ne lui sont pas applicables, Removed items with no change in quantity or value.,Les articles avec aucune modification de quantité ou de valeur ont étés retirés., -Reopen,Ré-ouvrir, Reorder Level,Niveau de réapprovisionnement, Reorder Qty,Qté de Réapprovisionnement, Repeat Customer Revenue,Revenus de Clients Récurrents, Repeat Customers,Clients Récurrents, Replace BOM and update latest price in all BOMs,Remplacer la nomenclature et actualiser les prix les plus récents dans toutes les nomenclatures, -Replied,Répondu, -Report,Rapport, -Report Type,Type de Rapport, Report Type is mandatory,Le Type de Rapport est nécessaire, -Reports,Rapports, Reqd By Date,Requis par date, Reqd Qty,Qté obligatoire, Request for Quotation,Appel d'Offre, @@ -2208,7 +2048,6 @@ Root cannot be edited.,La racine ne peut pas être modifiée., Root cannot have a parent cost center,Racine ne peut pas avoir un centre de coûts parent, Round Off,Arrondi, Rounded Total,Total arrondi, -Route,Route, Row # {0}: ,Ligne # {0} :, Row # {0}: Batch No must be same as {1} {2},Ligne # {0} : Le N° de Lot doit être le même que {1} {2}, Row # {0}: Cannot return more than {1} for Item {2},Ligne # {0} : Vous ne pouvez pas retourner plus de {1} pour l’Article {2}, @@ -2297,8 +2136,6 @@ Sales Funnel,Entonnoir de vente, Sales Invoice,Facture de vente, Sales Invoice {0} has already been submitted,La Facture Vente {0} a déjà été transmise, Sales Invoice {0} must be cancelled before cancelling this Sales Order,Facture de Vente {0} doit être annulée avant l'annulation de cette Commande Client, -Sales Manager,Responsable des Ventes, -Sales Master Manager,Directeur des Ventes, Sales Order,Commande client, Sales Order Item,Article de la Commande Client, Sales Order required for Item {0},Commande Client requise pour l'Article {0}, @@ -2314,11 +2151,9 @@ Sales Return,Retour de Ventes, Sales Summary,Récapitulatif des ventes, Sales Tax Template,Modèle de la Taxe de Vente, Sales Team,Équipe des Ventes, -Sales User,Chargé de Ventes, Sales and Returns,Ventes et retours, Sales campaigns.,Campagnes de vente., Sales orders are not available for production,Aucune commande client n'est disponible pour la production, -Salutation,Salutations, Same Company is entered more than once,La même Société a été entrée plus d'une fois, Same item cannot be entered multiple times.,Le même article ne peut pas être entré plusieurs fois., Same supplier has been entered multiple times,Le même fournisseur a été saisi plusieurs fois, @@ -2326,7 +2161,6 @@ Sample Collection,Collecte d'Échantillons, Sample quantity {0} cannot be more than received quantity {1},La quantité d'échantillon {0} ne peut pas dépasser la quantité reçue {1}, Sanctioned,Sanctionné, Sand,Le sable, -Saturday,Samedi, Saving {0},Enregistrement {0}, Scan Barcode,Scan Code Barre, Schedule,Calendrier, @@ -2334,18 +2168,15 @@ Schedule Admission,Calendrier d'admission, Schedule Course,Cours Calendrier, Schedule Date,Date du Calendrier, Schedule Discharge,Décharge horaire, -Scheduled,Prévu, Scheduled Upto,Programmé jusqu'à, "Schedules for {0} overlaps, do you want to proceed after skiping overlaped slots ?","Les plannings pour {0} se chevauchent, voulez-vous continuer sans prendre en compte les créneaux qui se chevauchent ?", Score cannot be greater than Maximum Score,Score ne peut pas être supérieure à Score maximum, Scorecards,Fiches d'Évaluation, Scrapped,Mis au rebut, -Search,Rechercher, Search Results,Résultats de la recherche, Search Sub Assemblies,Rechercher les Sous-Ensembles, "Search by item code, serial number, batch no or barcode","Recherche par code article, numéro de série, numéro de lot ou code-barres", "Seasonality for setting budgets, targets etc.","Saisonnalité de l'établissement des budgets, des objectifs, etc.", -Secret Key,Clef Secrète, Secretary,secrétaire, Section Code,Code de section, Secured Loans,Prêts garantis, @@ -2355,7 +2186,6 @@ See All Articles,Voir tous les articles, See all open tickets,Voir tous les tickets ouverts, See past orders,Voir les commandes passées, See past quotations,Voir les citations passées, -Select,Sélectionner, Select Alternate Item,Sélectionnez un autre élément, Select Attribute Values,Sélectionner les valeurs d'attribut, Select BOM,Sélectionner une nomenclature, @@ -2369,7 +2199,6 @@ Select Company...,Sélectionner la Société ..., Select Customer,Sélectionnez un client, Select Days,Choisissez des jours, Select Default Supplier,Sélectionner le Fournisseur par Défaut, -Select DocType,Sélectionner le DocType, Select Fiscal Year...,Sélectionner Exercice ..., Select Item (optional),Sélectionnez l'Article (facultatif), Select Items based on Delivery Date,Sélectionnez les articles en fonction de la Date de Livraison, @@ -2399,11 +2228,9 @@ Selling Price List,Liste de prix de vente, Selling Rate,Prix de vente, "Selling must be checked, if Applicable For is selected as {0}","Vente doit être vérifiée, si ""Applicable pour"" est sélectionné comme {0}", Send Grant Review Email,Envoyer un email d'examen de la demande de subvention, -Send Now,Envoyer Maintenant, Send SMS,Envoyer un SMS, Send mass SMS to your contacts,Envoyer un SMS en masse à vos contacts, Sensitivity,Sensibilité, -Sent,Envoyé, Serial No and Batch,N° de Série et lot, Serial No is mandatory for Item {0},N° de Série est obligatoire pour l'Article {0}, Serial No {0} does not belong to Batch {1},Le numéro de série {0} n'appartient pas au lot {1}, @@ -2428,7 +2255,6 @@ Serialized Inventory,Inventaire Sérialisé, Series Updated,Série mise à jour, Series Updated Successfully,Mise à jour des Séries Réussie, Series is mandatory,Série est obligatoire, -Service,Service, Service Level Agreement,Contrat de niveau de service, Service Level Agreement.,Contrat de niveau de service., Service Level.,Niveau de service., @@ -2455,7 +2281,6 @@ Setting up Email Account,Configuration du Compte Email, Setting up Employees,Configuration des Employés, Setting up Taxes,Configuration des Impôts, Setting up company,Création d'entreprise, -Settings,Paramètres, "Settings for online shopping cart such as shipping rules, price list etc.","Paramètres du panier tels que les règles de livraison, liste de prix, etc.", Settings for website homepage,Paramètres de la page d'accueil du site, Settings for website product listing,Paramètres pour la liste de produits de sites Web, @@ -2481,7 +2306,6 @@ Shipping rule only applicable for Selling,Règle d'expédition applicable unique Shopify Supplier,Fournisseur Shopify, Shopping Cart,Panier, Shopping Cart Settings,Paramètres du panier, -Short Name,Nom Court, Shortage Qty,Qté de Pénurie, Show Completed,Montrer terminé, Show Cumulative Amount,Afficher le montant cumulatif, @@ -2500,7 +2324,6 @@ Silt,Limon, Single Variant,Variante unique, Single unit of an Item.,Seule unité d'un Article., "Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}","Attribution des congés de congé pour les employés suivants, car des dossiers de répartition des congés existent déjà contre eux. {0}", -Slideshow,Diaporama, Slots for {0} are not added to the schedule,Les créneaux pour {0} ne sont pas ajoutés à l'agenda, Small,Petit, Soap & Detergent,Savons & Détergents, @@ -2513,8 +2336,6 @@ Some emails are invalid,Certains emails sont invalides, Some information is missing,Certaines informations sont manquantes, Something went wrong!,Quelque chose a mal tourné !, "Sorry, Serial Nos cannot be merged","Désolé, les N° de Série ne peut pas être fusionnés", -Source,Source, -Source Name,Nom de la Source, Source Warehouse,Entrepôt source, Source and Target Location cannot be same,Les localisations source et cible ne peuvent pas être identiques, Source and target warehouse cannot be same for row {0},L'entrepôt source et destination ne peuvent être similaire dans la ligne {0}, @@ -2529,14 +2350,12 @@ Sports,Sportif, Standard Buying,Achat standard, Standard Selling,Vente standard, Standard contract terms for Sales or Purchase.,Termes contractuels standards pour Ventes ou Achats, -Start Date,Date de Début, Start Date of Agreement can't be greater than or equal to End Date.,La date de début de l'accord ne peut être supérieure ou égale à la date de fin., Start Year,Année de début, Start date should be less than end date for Item {0},La date de début doit être antérieure à la date de fin pour l'Article {0}, Start date should be less than end date for task {0},La date de début doit être inférieure à la date de fin de la tâche {0}, Start day is greater than end day in task '{0}',La date de début est supérieure à la date de fin dans la tâche '{0}', Start on,Démarrer, -State,Etat, State/UT Tax,Taxe Etat / UT, Statement of Account,Relevé de compte, Status must be one of {0},Le statut doit être l'un des {0}, @@ -2569,8 +2388,6 @@ Stock cannot be updated against Delivery Note {0},Stock ne peut pas être mis à Stock cannot be updated against Purchase Receipt {0},Stock ne peut pas être mis à jour pour le Reçu d'Achat {0}, Stock cannot exist for Item {0} since has variants,Stock ne peut pas exister pour l'Article {0} puisqu'il a des variantes, Stock transactions before {0} are frozen,Les transactions du stock avant {0} sont gelées, -Stop,Arrêter, -Stopped,Arrêté, "Stopped Work Order cannot be cancelled, Unstop it first to cancel","Un ordre de fabrication arrêté ne peut être annulé, Re-démarrez le pour pouvoir l'annuler", Stores,Magasins, Student,Étudiant, @@ -2601,8 +2418,6 @@ Sub Assemblies,Sous-Ensembles, Sub Type,Sous type, Sub-contracting,Sous-traitant, Subcontract,Sous-traiter, -Subject,Sujet, -Submit,Valider, Submit this Work Order for further processing.,Valider cet ordre de fabrication pour continuer son traitement., Subscription,Abonnement, Subscription Management,Gestion des abonnements, @@ -2615,10 +2430,8 @@ Successfully created payment entries,Ecritures de paiement créées avec succès Successfully deleted all transactions related to this company!,Suppression de toutes les transactions liées à cette société avec succès !, Sum of Scores of Assessment Criteria needs to be {0}.,Somme des Scores de Critères d'Évaluation doit être {0}., Sum of points for all goals should be 100. It is {0},Somme des points pour tous les objectifs devraient être 100. Il est {0}, -Summary,Résumé, Summary for this month and pending activities,Résumé du mois et des activités en suspens, Summary for this week and pending activities,Résumé de la semaine et des activités en suspens, -Sunday,Dimanche, Suplier,Fournisseur, Supplier,Fournisseur, Supplier Group,Groupe de fournisseurs, @@ -2648,20 +2461,15 @@ Susceptible,Sensible, Sync has been temporarily disabled because maximum retries have been exceeded,La synchronisation a été temporairement désactivée car les tentatives maximales ont été dépassées, Syntax error in condition: {0},Erreur de syntaxe dans la condition: {0}, Syntax error in formula or condition: {0},Erreur de syntaxe dans la formule ou condition : {0}, -System Manager,Responsable Système, TDS Rate %,Pourcentage de TDS, Tap items to add them here,Choisissez des articles pour les ajouter ici, -Target,Cible, Target ({}),Cible ({}), Target On,Cible sur, Target Warehouse,Entrepôt cible, Target warehouse is mandatory for row {0},L’Entrepôt cible est obligatoire pour la ligne {0}, -Task,Tâche, -Tasks,Tâches, Tasks have been created for managing the {0} disease (on row {1}),Des tâches ont été créées pour gérer la maladie {0} (sur la ligne {1}), Tax,Taxe, Tax Assets,Actifs d'Impôts, -Tax Category,Catégorie de taxe, Tax Category for overriding tax rates.,Catégorie de taxe pour les taux de taxe prépondérants., "Tax Category has been changed to ""Total"" because all the Items are non-stock items","La Catégorie de Taxe a été changée à ""Total"" car tous les articles sont des articles hors stock", Tax ID,Numéro d'identification fiscale, @@ -2769,11 +2577,9 @@ Timesheet {0} is already completed or cancelled,La Feuille de Temps {0} est déj Timesheets,Feuilles de temps, "Timesheets help keep track of time, cost and billing for activites done by your team","Les Feuilles de Temps aident au suivi du temps, coût et facturation des activités effectuées par votre équipe", Titles for print templates e.g. Proforma Invoice.,Titres pour les modèles d'impression e.g. Facture Proforma., -To,À, To Address 1,Ligne d'adresse 1 (Destination), To Address 2,Ligne d'adresse 2 (Destination), To Bill,À Facturer, -To Date,Jusqu'au, To Date cannot be before From Date,La date de fin ne peut être antérieure à la date de début, To Date cannot be less than From Date,La date de fin ne peut pas précéder la date de début, To Date must be greater than From Date,La date de fin doit être supérieure à la date de début, @@ -2809,6 +2615,7 @@ Total (Credit),Total (Crédit), Total (Without Tax),Total (hors taxes), Total Achieved,Total Obtenu, Total Actual,Total réel, +Total Allocated Leaves,Total des congés alloués, Total Amount,Montant total, Total Amount Credited,Montant total crédité, Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges,Total des Frais Applicables dans la Table des Articles de Reçus d’Achat doit être égal au Total des Taxes et Frais, @@ -2888,7 +2695,6 @@ Types of activities for Time Logs,Types d'activités pour Journaux de Temps, UOM,UdM, UOM Conversion factor is required in row {0},Facteur de conversion de l'UdM est obligatoire dans la ligne {0}, UOM coversion factor required for UOM: {0} in Item: {1},Facteur de coversion UdM requis pour l'UdM : {0} dans l'Article : {1}, -URL,URL, Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually,Impossible de trouver le taux de change pour {0} à {1} pour la date clé {2}. Veuillez créer une entrée de taux de change manuellement, Unable to find score starting at {0}. You need to have standing scores covering 0 to 100,Impossible de trouver un score démarrant à {0}. Vous devez avoir des scores couvrant 0 à 100, Unable to find variable: ,Impossible de trouver une variable:, @@ -2902,7 +2708,6 @@ Unknown,Inconnu, Unpaid,Impayé, Unsecured Loans,Prêts non garantis, Unsubscribe from this Email Digest,Se Désinscire de ce Compte Rendu par Email, -Unsubscribed,Désinscrit, Until,Jusqu'à, Unverified Webhook Data,Données de Webhook non vérifiées, Update Account Name / Number,Mettre à jour le nom / numéro du compte, @@ -2918,8 +2723,7 @@ Updating Variants...,Mise à jour des variantes ..., Upload your letter head and logo. (you can edit them later).,Charger votre en-tête et logo. (vous pouvez les modifier ultérieurement)., Upper Income,Revenu Élevé, Use Sandbox,Utiliser Sandbox, -User,Utilisateur, -User ID,Identifiant d'utilisateur, +Used Leaves,Congés utilisés, User ID not set for Employee {0},ID de l'Utilisateur non défini pour l'Employé {0}, User Remark,Remarque de l'Utilisateur, User has not applied rule on the invoice {0},L'utilisateur n'a pas appliqué la règle sur la facture {0}, @@ -2929,14 +2733,12 @@ User {0} does not exist,Utilisateur {0} n'existe pas, User {0} doesn't have any default POS Profile. Check Default at Row {1} for this User.,L'utilisateur {0} n'a aucun profil POS par défaut. Vérifiez par défaut à la ligne {1} pour cet utilisateur., User {0} is already assigned to Employee {1},Utilisateur {0} est déjà attribué à l'Employé {1}, User {0} is already assigned to Healthcare Practitioner {1},L'utilisateur {0} est déjà attribué à un professionnel de la santé {1}, -Users,Utilisateurs, Utility Expenses,Frais de Services d'Utilité Publique, Valid From Date must be lesser than Valid Upto Date.,La date de début de validité doit être inférieure à la date de mise en service valide., Valid Till,Valable Jusqu'au, Valid from and valid upto fields are mandatory for the cumulative,Les champs valides à partir de et valables jusqu'à sont obligatoires pour le cumulatif., Valid from date must be less than valid upto date,La date de début de validité doit être inférieure à la date de validité, Valid till date cannot be before transaction date,La date de validité ne peut pas être avant la date de transaction, -Validity,Validité, Validity period of this quotation has ended.,La période de validité de ce devis a pris fin., Valuation Rate,Taux de Valorisation, Valuation Rate is mandatory if Opening Stock entered,Le Taux de Valorisation est obligatoire si un Stock Initial est entré, @@ -2991,7 +2793,6 @@ Warehouse {0} does not exist,L'entrepôt {0} n'existe pas, Warehouses with child nodes cannot be converted to ledger,Les entrepôts avec nœuds enfants ne peuvent pas être convertis en livre, Warehouses with existing transaction can not be converted to group.,Les entrepôts avec des transactions existantes ne peuvent pas être convertis en groupe., Warehouses with existing transaction can not be converted to ledger.,Les entrepôts avec des transactions existantes ne peuvent pas être convertis en livre., -Warning,Avertissement, Warning: Another {0} # {1} exists against stock entry {2},Attention : Un autre {0} {1} # existe pour l'écriture de stock {2}, Warning: Invalid SSL certificate on attachment {0},Attention : certificat SSL non valide sur la pièce jointe {0}, Warning: Invalid attachment {0},Attention : Pièce jointe non valide {0}, @@ -3001,16 +2802,9 @@ Warning: System will not check overbilling since amount for Item {0} in {1} is z Warranty,garantie, Warranty Claim,Réclamation de Garantie, Warranty Claim against Serial No.,Réclamation de Garantie pour le N° de Série., -Website,Site Web, Website Image should be a public file or website URL,L'Image du Site Web doit être un fichier public ou l'URL d'un site web, Website Image {0} attached to Item {1} cannot be found,Image pour le Site Web {0} attachée à l'Article {1} ne peut pas être trouvée, -Website Manager,Responsable du Site Web, -Website Settings,Paramètres du Site web, -Wednesday,Mercredi, -Week,Semaine, -Weekly,Hebdomadaire, "Weight is mentioned,\nPlease mention ""Weight UOM"" too","Poids est mentionné,\nVeuillez aussi mentionner ""UdM de Poids""", -Welcome email sent,Email de bienvenue envoyé, Welcome to ERPNext,Bienvenue sur ERPNext, What do you need help with?,Avec quoi avez vous besoin d'aide ?, What does it do?,Qu'est-ce que ça fait ?, @@ -3073,9 +2867,7 @@ disabled user,utilisateur désactivé, "e.g. ""Build tools for builders""","e.g. ""Construire des outils pour les constructeurs""", "e.g. ""Primary School"" or ""University""","e.g. ""École Primaire"" ou ""Université""", "e.g. Bank, Cash, Credit Card","e.g. Cash, Banque, Carte de crédit", -hidden,Masqué, modified,modifié, -old_parent,grand_parent, on,sur, {0} '{1}' is disabled,{0} '{1}' est désactivé(e), {0} '{1}' not in Fiscal Year {2},{0} '{1}' n'est pas dans l’Exercice {2}, @@ -3108,7 +2900,6 @@ on,sur, {0} hours,{0} heures, {0} in row {1},{0} dans la ligne {1}, {0} is blocked so this transaction cannot proceed,{0} est bloqué donc cette transaction ne peut pas continuer, -{0} is mandatory,{0} est obligatoire, {0} is mandatory for Item {1},{0} est obligatoire pour l’Article {1}, {0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.,{0} est obligatoire. Peut-être qu’un enregistrement de Taux de Change n'est pas créé pour {1} et {2}., {0} is not a stock Item,{0} n'est pas un Article de stock, @@ -3168,37 +2959,8 @@ on,sur, {0}: {1} does not exists,{0} : {1} n’existe pas, {0}: {1} not found in Invoice Details table,{0} : {1} introuvable dans la table de Détails de la Facture, {} of {},{} de {}, -Assigned To,Assigné À, -Chat,Chat, Completed By,Effectué par, -Day of Week,Jour de la semaine, -"Dear System Manager,","Cher Administrateur Système ,", -Default Value,Valeur par Défaut, -Email Group,Groupe Email, -Email Settings,Paramètres d'Email, -Email not sent to {0} (unsubscribed / disabled),Email pas envoyé à {0} (désabonné / désactivé), -Error Message,Message d'erreur, -Fieldtype,Type de Champ, -Help Articles,Articles d'Aide, -ID,ID, -Import,Importer, -Language,Langue, -Likes,Aime, -Merge with existing,Fusionner avec existant, -Orientation,Orientation, -Parent,Parent, Payment Failed,Le Paiement a Échoué, -Personal,Personnel, -Post,Poster, -Postal Code,code postal, -Provider,Fournisseur, -Read Only,Lecture Seule, -Recipient,Destinataire, -Reviews,Avis, -Sender,Expéditeur, -There were errors while sending email. Please try again.,Il y a eu des erreurs lors de l'envoi d’emails. Veuillez essayer à nouveau., -Values Changed,Valeurs Modifiées, -or,ou, Ageing Range 4,Gamme de vieillissement 4, Allocated amount cannot be greater than unadjusted amount,Le montant alloué ne peut être supérieur au montant non ajusté, Allocated amount cannot be negative,Le montant alloué ne peut être négatif, @@ -3224,22 +2986,7 @@ Rules for applying different promotional schemes.,Règles d'application de diff Show {0},Montrer {0}, Target Details,Détails de la cible, {0} already has a Parent Procedure {1}.,{0} a déjà une procédure parent {1}., -API,API, -Annual,Annuel, -Change,Changement, -Contact Email,Email du Contact, -From Date,A partir du, -Group By,Par groupe, -Invalid URL,URL invalide, -Landscape,Paysage, -Naming Series,Masque de numérotation, -No data to export,Aucune donnée à exporter, -Portrait,Portrait, Print Heading,Imprimer Titre, -Scheduler Inactive,Planificateur inactif, -Scheduler is inactive. Cannot import data.,Le planificateur est inactif. Impossible d'importer des données., -Show Document,Afficher le document, -Show Traceback,Afficher le traçage, Video,Vidéo, % Of Grand Total,% Du grand total, Company is a mandatory filter.,La société est un filtre obligatoire., @@ -3257,20 +3004,11 @@ Accounting Dimension {0} is required for 'Balance Sheet' account {1}.,La Accounting Dimension {0} is required for 'Profit and Loss' account {1}.,La dimension de comptabilité {0} est requise pour le compte 'Bénéfices et pertes' {1}., Accounting Masters,Maîtres Comptables, Accounting Period overlaps with {0},La période comptable chevauche avec {0}, -Activity,Activité, -Add / Manage Email Accounts.,Ajouter / Gérer les Comptes de Messagerie., -Add Child,Ajouter une Sous-Catégorie, -Add Multiple,Ajout Multiple, -Add Participants,Ajouter des participants, Add to Featured Item,Ajouter à l'article en vedette, Add your review,Ajouter votre avis, Add/Edit Coupon Conditions,Ajouter / Modifier les conditions du coupon, Added to Featured Items,Ajouté aux articles en vedette, -Added {0} ({1}),Ajouté {0} ({1}), -Address Line 1,Adresse Ligne 1, -Addresses,Adresses, Admission End Date should be greater than Admission Start Date.,La date de fin d'admission doit être supérieure à la date de début d'admission., -All,Tout, All bank transactions have been created,Toutes les transactions bancaires ont été créées, All the depreciations has been booked,Toutes les amortissements ont été comptabilisés, Allow Resetting Service Level Agreement from Support Settings.,Autoriser la réinitialisation du contrat de niveau de service à partir des paramètres de support., @@ -3305,17 +3043,12 @@ Bank accounts added,Comptes bancaires ajoutés, Batch no is required for batched item {0},Le numéro de lot est requis pour l'article en lot {0}., Billing Date,Date de facturation, Billing Interval Count cannot be less than 1,Le nombre d'intervalles de facturation ne peut pas être inférieur à 1, -Blue,Bleu, -Book,Livre, Book Appointment,Prendre rendez-vous, -Brand,Marque, -Browse,Feuilleter, Call Connected,Appel connecté, Call Disconnected,Appel déconnecté, Call Missed,Appel manqué, Call Summary,Résumé d'appel, Call Summary Saved,Résumé de l'appel enregistré, -Cancelled,Annulé, Cannot Calculate Arrival Time as Driver Address is Missing.,Impossible de calculer l'heure d'arrivée car l'adresse du conducteur est manquante., Cannot Optimize Route as Driver Address is Missing.,Impossible d'optimiser l'itinéraire car l'adresse du pilote est manquante., Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.,Impossible de terminer la tâche {0} car sa tâche dépendante {1} n'est pas terminée / annulée., @@ -3324,26 +3057,19 @@ Cannot find a matching Item. Please select some other value for {0}.,Impossible "Capacity Planning Error, planned start time can not be same as end time","Erreur de planification de capacité, l'heure de début prévue ne peut pas être identique à l'heure de fin", Categories,Catégories, Changes in {0},Changements dans {0}, -Chart,Graphique, Choose a corresponding payment,Choisissez un paiement correspondant, Click on the link below to verify your email and confirm the appointment,Cliquez sur le lien ci-dessous pour vérifier votre email et confirmer le rendez-vous, -Close,Fermer, Communication,Communication, Compact Item Print,Impression de l'Article Compacté, -Company,Société, Company of asset {0} and purchase document {1} doesn't matches.,La société de l'actif {0} et le document d'achat {1} ne correspondent pas., Compare BOMs for changes in Raw Materials and Operations,Comparer les nomenclatures aux modifications apportées aux matières premières et aux opérations, Compare List function takes on list arguments,La fonction de comparaison de liste accepte les arguments de liste, -Complete,Terminé, -Completed,Terminé, Completed Quantity,Quantité terminée, Connect your Exotel Account to ERPNext and track call logs,Connectez votre compte Exotel à ERPNext et suivez les journaux d'appels, Connect your bank accounts to ERPNext,Connectez vos comptes bancaires à ERPNext, Contact Seller,Contacter le vendeur, -Continue,Continuer, Cost Center: {0} does not exist,Centre de coûts: {0} n'existe pas, Couldn't Set Service Level Agreement {0}.,Impossible de définir le contrat de service {0}., -Country,Pays, Country Code in File does not match with country code set up in the system,Le code de pays dans le fichier ne correspond pas au code de pays configuré dans le système, Create New Contact,Créer un nouveau contact, Create New Lead,Créer une nouvelle lead, @@ -3354,34 +3080,20 @@ Creating bank entries...,Création d'entrées bancaires ..., Credit limit is already defined for the Company {0},La limite de crédit est déjà définie pour la société {0}., Ctrl + Enter to submit,Ctrl + Entrée pour valider, Ctrl+Enter to submit,Ctrl + Entrée pour valider, -Currency,Devise, -Current Status,Statut Actuel, Customer PO,Commande d'Achat client, -Daily,Quotidien, -Date,Date, Date of Birth cannot be greater than Joining Date.,La date de naissance ne peut pas être supérieure à la date d'adhésion., -Dear,Cher/Chère, -Default,Par Défaut, Define coupon codes.,Définissez les codes promo., Delayed Days,Jours retardés, -Delete,Supprimer, Delivered Quantity,Quantité livrée, Delivery Notes,Bons de livraison, Depreciated Amount,Montant amorti, -Description,Description, -Designation,Désignation, Difference Value,Valeur de différence, Dimension Filter,Filtre de dimension, -Disabled,Desactivé, Disbursement and Repayment,Décaissement et remboursement, Distance cannot be greater than 4000 kms,La distance ne peut pas dépasser 4000 km, Do you want to submit the material request,Voulez-vous valider la demande de matériel, -Doctype,Doctype, Document {0} successfully uncleared,Document {0} non effacé avec succès, -Download Template,Télécharger le Modèle, Dr,Dr, -Due Date,Date d'Échéance, -Duplicate,Dupliquer, Duplicate Project with Tasks,Projet en double avec tâches, Duplicate project has been created,Un projet en double a été créé, E-Way Bill JSON can only be generated from a submitted document,E-Way Bill JSON ne peut être généré qu'à partir d'un document soumis, @@ -3391,7 +3103,6 @@ ERPNext could not find any matching payment entry,ERPNext n'a trouvé aucune ent Earliest Age,Âge le plus précoce, Edit Details,Modifier les détails, Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Un numéro d'identification de transporteur ou un numéro de véhicule est requis si le mode de transport est la route., -Email,Email, Email Campaigns,Campagnes de courrier électronique, Employee ID is linked with another instructor,L'ID de l'employé est lié à un autre instructeur, Employee Tax and Benefits,Impôt et avantages sociaux des employés, @@ -3403,21 +3114,13 @@ End Time,Heure de Fin, Energy Point Leaderboard,Point de classement énergétique, Enter API key in Google Settings.,Entrez la clé API dans les paramètres Google., Enter Supplier,Entrez le fournisseur, -Enter Value,Entrez une Valeur, -Entity Type,Type d'entité, -Error,Erreur, Error in Exotel incoming call,Erreur dans un appel entrant Exotel, Error: {0} is mandatory field,Erreur: {0} est un champ obligatoire, Exception occurred while reconciling {0},Une exception s'est produite lors de la réconciliation {0}, Expected and Discharge dates cannot be less than Admission Schedule date,Les dates prévues et de sortie ne peuvent pas être inférieures à la date du calendrier d'admission, -Expired,Expiré, -Export,Exporter, -Export not allowed. You need {0} role to export.,Pas autorisé à exporter. Vous devez avoir le rôle {0} pour exporter., Failed to add Domain,Impossible d'ajouter le domaine, Fetch Items from Warehouse,Récupérer des articles de l'entrepôt, Fetching...,Aller chercher..., -Field,Champ, -Filters,Filtres, Finding linked payments,Trouver des paiements liés, Fleet Management,Gestion de flotte, Following fields are mandatory to create address:,Les champs suivants sont obligatoires pour créer une adresse:, @@ -3433,28 +3136,17 @@ Future Payment Ref,Paiement futur Ref, Future Payments,Paiements futurs, GST HSN Code does not exist for one or more items,Le code HSN de la TPS n’existe pas pour un ou plusieurs articles, Generate E-Way Bill JSON,Générer E-Way Bill JSON, -Get Items,Obtenir les Articles, Get Outstanding Documents,Obtenez des documents en suspens, -Goal,Objectif, Greater Than Amount,Plus grand que le montant, -Green,Vert, -Group,Groupe, Group By Customer,Regrouper par client, Group By Supplier,Regrouper par fournisseur, -Group Node,Noeud de Groupe, Group Warehouses cannot be used in transactions. Please change the value of {0},Les entrepôts de groupe ne peuvent pas être utilisés dans les transactions. Veuillez modifier la valeur de {0}, -Help,Aidez-moi, -Help Article,Article d’Aide, "Helps you keep tracks of Contracts based on Supplier, Customer and Employee","Vous aide à garder une trace des contrats en fonction du fournisseur, client et employé", Helps you manage appointments with your leads,Vous aide à gérer les rendez-vous avec vos leads, -Home,Accueil, IBAN is not valid,IBAN n'est pas valide, -Import Data from CSV / Excel files.,Importer des données à partir de fichiers CSV / Excel, -In Progress,En cours, Incoming call from {0},Appel entrant du {0}, Incorrect Warehouse,Entrepôt incorrect, Invalid Barcode. There is no Item attached to this barcode.,Code à barres invalide. Il n'y a pas d'article attaché à ce code à barres., -Invalid credentials,les informations d'identification invalides, Issue Priority.,Priorité d'émission., Issue Type.,Type de probleme., "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.","Il semble qu'il y a un problème avec la configuration de Stripe sur le serveur. En cas d'erreur, le montant est remboursé sur votre compte.", @@ -3470,13 +3162,11 @@ Latest Age,Dernier âge, Leaves Taken,Feuilles prises, Less Than Amount,Moins que le montant, Liabilities,Passifs, -Loading...,Chargement en Cours ..., Loan Applications from customers and employees.,Demandes de prêt des clients et des employés., Loan Processes,Processus de prêt, Loan Type for interest and penalty rates,Type de prêt pour les taux d'intérêt et de pénalité, Loans,Les prêts, Loans provided to customers and employees.,Prêts accordés aux clients et aux employés., -Location,Lieu, Looks like someone sent you to an incomplete URL. Please ask them to look into it.,On dirait que quelqu'un vous a envoyé vers URL incomplète. Veuillez leur demander d’analyser l’erreur., Make Journal Entry,Faire une écriture de journal, Make Purchase Invoice,Faire la facture d'achat, @@ -3485,12 +3175,6 @@ Mark Work From Home,Mark Work From Home, Master,Maître, Max strength cannot be less than zero.,La force maximale ne peut pas être inférieure à zéro., Maximum attempts for this quiz reached!,Nombre maximal de tentatives pour ce quiz atteint!, -Message,Message, -Missing Values Required,Valeurs Manquantes Requises, -Mobile No,N° Mobile, -Mobile Number,Numéro de Mobile, -Month,Mois, -Name,Nom, Near you,Près de toi, Net Profit/Loss,Résultat net, New Expense,Nouvelle dépense, @@ -3509,10 +3193,8 @@ No outstanding invoices require exchange rate revaluation,Aucune facture en atte No reviews yet,Pas encore d'avis, No views yet,Pas encore de vue, Non stock items,Articles hors stock, -Not Allowed,Non Autorisé, Not allowed to create accounting dimension for {0},Non autorisé à créer une dimension comptable pour {0}, Not permitted. Please disable the Lab Test Template,Pas permis. Veuillez désactiver le modèle de test de laboratoire, -Note,Note, Notes: ,Remarques :, On Converting Opportunity,Sur l'opportunité de conversion, On Purchase Order Submission,Sur soumission de commande, @@ -3520,13 +3202,11 @@ On Sales Order Submission,Envoi de commande client, On Task Completion,En fin de tâche, On {0} Creation,Sur {0} Creation, Only .csv and .xlsx files are supported currently,Seuls les fichiers .csv et .xlsx sont actuellement pris en charge., -Open,Ouvert, Open Contact,Contact ouvert, Open Lead,Ouvrir le Lead, Opening and Closing,Ouverture et fermeture, Operating Cost as per Work Order / BOM,Coût d'exploitation selon l'ordre de fabrication / nomenclature, Order Amount,Montant de la commande, -Page {0} of {1},Page {0} sur {1}, Paid amount cannot be less than {0},Le montant payé ne peut pas être inférieur à {0}, Parent Company must be a group company,La société mère doit être une société du groupe, Passing Score value should be between 0 and 100,La note de passage doit être comprise entre 0 et 100, @@ -3535,11 +3215,9 @@ Pause,Pause, Pay,Payer, Payment Document Type,Type de document de paiement, Payment Name,Nom du paiement, -Pending,En Attente, Performance,Performance, Period based On,Période basée sur, Perpetual inventory required for the company {0} to view this report.,Inventaire permanent requis pour que la société {0} puisse consulter ce rapport., -Phone,Téléphone, Pick List,Liste de sélection, Plaid authentication error,Erreur d'authentification du plaid, Plaid public token error,Erreur de jeton public Plaid, @@ -3570,14 +3248,11 @@ Please set up the Campaign Schedule in the Campaign {0},Configurez le calendrier Please set valid GSTIN No. in Company Address for company {0},Veuillez définir un numéro GSTIN valide dans l'adresse de l'entreprise pour l'entreprise {0}, Please set {0},Veuillez définir {0},customer Please setup a default bank account for company {0},Veuillez configurer un compte bancaire par défaut pour la société {0}., -Please specify,Veuillez spécifier, Please specify a {0},Veuillez spécifier un {0},lead -Priority,Priorité, Priority has been changed to {0}.,La priorité a été changée en {0}., Priority {0} has been repeated.,La priorité {0} a été répétée., Processing XML Files,Traitement des fichiers XML, Profitability,Rentabilité, -Project,Projet, Provide the academic year and set the starting and ending date.,Indiquez l'année universitaire et définissez la date de début et de fin., Public token is missing for this bank,Un jeton public est manquant pour cette banque, Publish 1 Item,Publier 1 élément, @@ -3595,30 +3270,22 @@ Qty of Finished Goods Item,Quantité de produits finis, Quality Inspection required for Item {0} to submit,Inspection de qualité requise pour que l'élément {0} soit envoyé, Quantity to Manufacture,Quantité à fabriquer, Quantity to Manufacture can not be zero for the operation {0},La quantité à fabriquer ne peut pas être nulle pour l'opération {0}, -Quarterly,Trimestriel, -Queued,File d'Attente, -Quick Entry,Écriture Rapide, Quiz {0} does not exist,Le questionnaire {0} n'existe pas, Quotation Amount,Montant du devis, Rate or Discount is required for the price discount.,Le prix ou la remise est requis pour la remise., -Reason,Raison, Reconcile Entries,Réconcilier les entrées, Reconcile this account,Réconcilier ce compte, Reconciled,Réconcilié, Recruitment,Recrutement, -Red,Rouge, Release date must be in the future,La date de sortie doit être dans le futur, Relieving Date must be greater than or equal to Date of Joining,La date de libération doit être supérieure ou égale à la date d'adhésion, -Rename,Renommer, Rename Not Allowed,Renommer non autorisé, Report Item,Élément de rapport, Report this Item,Signaler cet article, Reserved Qty for Subcontract: Raw materials quantity to make subcontracted items.,Quantité réservée pour la sous-traitance: quantité de matières premières pour fabriquer des articles sous-traités., -Reset,Réinitialiser, Reset Service Level Agreement,Réinitialiser l'accord de niveau de service, Resetting Service Level Agreement.,Réinitialisation de l'accord de niveau de service., Return amount cannot be greater unclaimed amount,Le montant du retour ne peut pas être supérieur au montant non réclamé, -Review,La revue, Room,Chambre, Room Type,Type de chambre, Row # ,Ligne #, @@ -3643,47 +3310,36 @@ Row {0}:Sibling Date of Birth cannot be greater than today.,Ligne {0}: la date d Row({0}): {1} is already discounted in {2},Ligne ({0}): {1} est déjà réduit dans {2}., Rows Added in {0},Lignes ajoutées dans {0}, Rows Removed in {0},Lignes supprimées dans {0}, -Save,sauvegarder, Save Item,Enregistrer l'élément, Saved Items,Articles sauvegardés, Search Items ...,Rechercher des articles ..., Search for a payment,Rechercher un paiement, Search for anything ...,Rechercher n'importe quoi ..., -Search results for,Résultats de recherche pour, Select Difference Account,Sélectionnez compte différentiel, Select a Default Priority.,Sélectionnez une priorité par défaut., Select a company,Sélectionnez une entreprise, Select finance book for the item {0} at row {1},Sélectionnez le livre de financement pour l'élément {0} à la ligne {1}., Select only one Priority as Default.,Sélectionnez une seule priorité par défaut., Seller Information,Information du vendeur, -Send,Envoyer, Send a message,Envoyer un message, -Sending,Envoi, Sends Mails to lead or contact based on a Campaign schedule,Envoie des courriers à diriger ou à contacter en fonction d'un calendrier de campagne, Serial Number Created,Numéro de série créé, Serial Numbers Created,Numéros de série créés, Serial no(s) required for serialized item {0},N ° de série requis pour l'article sérialisé {0}, Series,Séries, -Server Error,Erreur du Serveur, Service Level Agreement has been changed to {0}.,L'accord de niveau de service a été remplacé par {0}., Service Level Agreement was reset.,L'accord de niveau de service a été réinitialisé., Service Level Agreement with Entity Type {0} and Entity {1} already exists.,L'accord de niveau de service avec le type d'entité {0} et l'entité {1} existe déjà., Set Meta Tags,Définir les balises méta, Set {0} in company {1},Définissez {0} dans l'entreprise {1}, -Setup,Configuration, Shift Management,Gestion des quarts, Show Future Payments,Afficher les paiements futurs, Show Linked Delivery Notes,Afficher les bons de livraison liés, Show Sales Person,Afficher le vendeur, Show Stock Ageing Data,Afficher les données sur le vieillissement des stocks, Show Warehouse-wise Stock,Afficher le stock entre les magasins, -Size,Taille, Something went wrong while evaluating the quiz.,Quelque chose s'est mal passé lors de l'évaluation du quiz., -Sr,Sr, -Start,Démarrer, Start Date cannot be before the current date,La date de début ne peut pas être antérieure à la date du jour, -Start Time,Heure de Début, -Status,Statut, Status must be Cancelled or Completed,Le statut doit être annulé ou complété, Stock Balance Report,Rapport de solde des stocks, Stock Entry has been already created against this Pick List,Une entrée de stock a déjà été créée dans cette liste de choix, @@ -3692,10 +3348,8 @@ Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and Stores - {0},Magasins - {0}, Student with email {0} does not exist,Étudiant avec le courrier électronique {0} n'existe pas, Submit Review,Poster un commentaire, -Submitted,Valider, Supplier Addresses And Contacts,Adresses et contacts des fournisseurs, Synchronize this account,Synchroniser ce compte, -Tag,Étiquette, Target Location is required while receiving Asset {0} from an employee,L'emplacement cible est requis lors de la réception de l'élément {0} d'un employé, Target Location is required while transferring Asset {0},L'emplacement cible est requis lors du transfert de l'élément {0}, Target Location or To Employee is required while receiving Asset {0},L'emplacement cible ou l'employé est requis lors de la réception de l'élément {0}, @@ -3703,7 +3357,6 @@ Task's {0} End Date cannot be after Project's End Date.,La date de fin {0} de la Task's {0} Start Date cannot be after Project's End Date.,La date de début {0} de la tâche ne peut pas être postérieure à la date de fin du projet., Tax Account not specified for Shopify Tax {0},Compte de taxe non spécifié pour Shopify Tax {0}, Tax Total,Total de la taxe, -Template,Modèle, The Campaign '{0}' already exists for the {1} '{2}',La campagne '{0}' existe déjà pour le {1} '{2}'., The difference between from time and To Time must be a multiple of Appointment,La différence entre from time et To Time doit être un multiple de Appointment, The field Asset Account cannot be blank,Le champ Compte d'actif ne peut pas être vide, @@ -3720,19 +3373,13 @@ This bank account is already synchronized,Ce compte bancaire est déjà synchron This bank transaction is already fully reconciled,Cette transaction bancaire est déjà totalement réconciliée, This page keeps track of items you want to buy from sellers.,Cette page répertorie les articles que vous souhaitez acheter auprès des vendeurs., This page keeps track of your items in which buyers have showed some interest.,Cette page conserve une trace de vos articles pour lesquels les acheteurs ont manifesté un certain intérêt., -Thursday,Jeudi, -Title,Titre, "To allow over billing, update ""Over Billing Allowance"" in Accounts Settings or the Item.","Pour autoriser la facturation excédentaire, mettez à jour "Provision de facturation excédentaire" dans les paramètres de compte ou le poste.", "To allow over receipt / delivery, update ""Over Receipt/Delivery Allowance"" in Stock Settings or the Item.","Pour autoriser le dépassement de réception / livraison, mettez à jour "Limite de dépassement de réception / livraison" dans les paramètres de stock ou le poste.", -Total,Total, Total Payment Request amount cannot be greater than {0} amount,Le montant total de la demande de paiement ne peut être supérieur à {0}., Total payments amount can't be greater than {},Le montant total des paiements ne peut être supérieur à {}, -Totals,Totaux, Transactions already retreived from the statement,Transactions déjà extraites de la déclaration, Transfer Material to Supplier,Transfert de matériel au fournisseur, Transport Receipt No and Date are mandatory for your chosen Mode of Transport,Le numéro de reçu de transport et la date sont obligatoires pour le mode de transport choisi, -Tuesday,Mardi, -Type,Type, Unable to find the time slot in the next {0} days for the operation {1}.,Impossible de trouver l'intervalle de temps dans les {0} jours suivants pour l'opération {1}., Unable to update remote activity,Impossible de mettre à jour l'activité à distance, Unknown Caller,Appelant inconnu, @@ -3740,13 +3387,10 @@ Unlink external integrations,Dissocier les intégrations externes, Unpublish Item,Annuler la publication, Unreconciled,Non réconcilié, Unsupported GST Category for E-Way Bill JSON generation,Catégorie GST non prise en charge pour la génération e-Way Bill JSON, -Update,Mettre à Jour, Update Taxes for Items,Mettre à jour les taxes pour les articles, "Upload a bank statement, link or reconcile a bank account","Télécharger un relevé bancaire, un lien ou un rapprochement d'un compte bancaire", Upload a statement,Télécharger une déclaration, Use a name that is different from previous project name,Utilisez un nom différent du nom du projet précédent, -User {0} is disabled,Utilisateur {0} est désactivé, -Users and Permissions,Utilisateurs et Autorisations, Valuation Rate required for Item {0} at row {1},Taux de valorisation requis pour le poste {0} à la ligne {1}, Values Out Of Sync,Valeurs désynchronisées, Vehicle Type is required if Mode of Transport is Road,Le type de véhicule est requis si le mode de transport est la route, @@ -3755,15 +3399,11 @@ Verify Email,Vérifier les courriels, View,Vue, View all issues from {0},Afficher tous les problèmes de {0}, View call log,Voir le journal des appels, -Warehouse,Entrepôt, Warehouse not found against the account {0},Entrepôt introuvable sur le compte {0}, -Welcome to {0},Bienvenue sur {0}, Why do think this Item should be removed?,Pourquoi pensez-vous que cet élément devrait être supprimé?, Work Order {0}: Job Card not found for the operation {1},Bon de travail {0}: carte de travail non trouvée pour l'opération {1}, Workday {0} has been repeated.,La journée de travail {0} a été répétée., XML Files Processed,Fichiers XML traités, -Year,Année, -Yearly,Annuel, You are not allowed to enroll for this course,Vous n'êtes pas autorisé à vous inscrire à ce cours, You are not enrolled in program {0},Vous n'êtes pas inscrit au programme {0}, You can Feature upto 8 items.,Vous pouvez présenter jusqu'à 8 éléments., @@ -3776,7 +3416,6 @@ Your Featured Items,Vos articles en vedette, Your Items,Vos articles, Your Profile,Votre profil, Your rating:,Votre note :, -and,et, e-Way Bill already exists for this document,e-Way Bill existe déjà pour ce document, woocommerce - {0},woocommerce - {0}, {0} Coupon used are {1}. Allowed quantity is exhausted,Le {0} coupon utilisé est {1}. La quantité autorisée est épuisée, @@ -3788,7 +3427,6 @@ woocommerce - {0},woocommerce - {0}, {0} is not a company bank account,{0} n'est pas un compte bancaire d'entreprise, {0} is not a group node. Please select a group node as parent cost center,{0} n'est pas un nœud de groupe. Veuillez sélectionner un nœud de groupe comme centre de coûts parent, {0} is not the default supplier for any items.,{0} n'est le fournisseur par défaut d'aucun élément., -{0} is required,{0} est nécessaire, {0}: {1} must be less than {2},{0}: {1} doit être inférieur à {2}, {} is required to generate E-Way Bill JSON,{} est requis pour générer e-Way Bill JSON, "Invalid lost reason {0}, please create a new lost reason","Motif perdu non valide {0}, veuillez créer un nouveau motif perdu", @@ -3797,20 +3435,6 @@ Total Expense,Dépense totale, Total Expense This Year,Dépenses totales cette année, Total Income,Revenu total, Total Income This Year,Revenu total cette année, -Barcode,code à barre, -Clear,Clair, -Comments,Commentaires, -DocType,DocType, -Download,Télécharger, -Left,Parti, -Link,Lien, -New,Nouveau, -Print,Impression, -Reference Name,Nom de référence, -Refresh,Actualiser, -Success,Succès, -Time,Temps, -Value,Valeur, Actual,Réel, Add to Cart,Ajouter au Panier, Days Since Last Order,Jours depuis la dernière commande, @@ -3824,10 +3448,6 @@ Sales Person,Vendeur, To date cannot be before From date,La date de fin ne peut être antérieure à la date de début, Write Off,Reprise, {0} Created,{0} Créé, -Email Id,Identifiant Email, -No,Non, -Reference Doctype,DocType de la Référence, -Yes,Oui, Actual ,Réel, Add to cart,Ajouter au Panier, Budget,Budget, @@ -3838,16 +3458,13 @@ Download as JSON,Télécharger en JSON, End date can not be less than start date,La date de Fin ne peut pas être antérieure à la Date de Début, For Default Supplier (Optional),Pour le fournisseur par défaut (facultatif), From date cannot be greater than To date,La Date Initiale ne peut pas être postérieure à la Date Finale, -Group by,Grouper Par, In stock,En stock, Item name,Libellé de l'article, Minimum Qty,Quantité minimum, More details,Plus de détails, Nature of Supplies,Nature des fournitures, -No Items found.,Aucun article trouvé., No students found,Aucun étudiant trouvé, Not in stock,En rupture, -Not permitted,Pas permis, Open Issues ,Tickets ouverts, Open Projects ,Projets ouverts, Open To Do ,ToDo ouvertes, @@ -3872,8 +3489,6 @@ Write off,Reprise, hours,heures, received from,reçu de, to,à, -Cards,Cartes, -Percentage,Pourcentage, Failed to setup defaults for country {0}. Please contact support@erpnext.com,Échec de la configuration des paramètres par défaut pour le pays {0}. Veuillez contacter support@erpnext.com, Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.,Ligne # {0}: l'article {1} n'est pas un article sérialisé / en lot. Il ne peut pas avoir de numéro de série / de lot contre lui., Please set {0},Veuillez définir {0}, @@ -3911,9 +3526,6 @@ Note: Item {0} added multiple times,Remarque: l'élément {0} a été ajouté pl YouTube,Youtube, Vimeo,Vimeo, Publish Date,Date de publication, -Duration,Durée, -Advanced Settings,Réglages avancés, -Path,Chemin, Components,Composants, Verified By,Vérifié Par, Invalid naming series (. missing) for {0},Masque de numérotation non valide (. Manquante) pour {0}, @@ -4230,7 +3842,6 @@ Is Finance Cost Adjustment,Est un ajustement des coûts financiers, Is Income Tax Liability,Est une dette d'impôt sur le revenu, Is Income Tax Expense,Est une dépense d'impôt sur le revenu, Cash Flow Mapping Accounts,Comptes du Mapping des Flux de Trésorerie, -account,Compte, Cash Flow Mapping Template,Modèle de Mapping des Flux de Trésorerie, Cash Flow Mapping Template Details,Détails du Modèle de Mapping des Flux de Trésorerie, POS-CLO-,POS-CLO-, @@ -4474,7 +4085,6 @@ Payment Schedule,Calendrier de paiement, Invoice Portion,Pourcentage de facturation, Payment Amount,Montant du paiement, Payment Term Name,Nom du terme de paiement, -Due Date Based On,Date d'échéance basée sur, Day(s) after invoice date,Jour (s) après la date de la facture, Day(s) after the end of the invoice month,Jour (s) après la fin du mois de facture, Month(s) after the end of the invoice month,Mois (s) après la fin du mois de la facture, @@ -4769,7 +4379,6 @@ Asset Account,Compte d'actif, (including),(compris), ACC-SH-.YYYY.-,ACC-SH-.YYYY.-, Folio no.,No. de Folio, -Address and Contacts,Adresse et Contacts, Contact List,Liste de contacts, Hidden list maintaining the list of contacts linked to Shareholder,Liste cachée maintenant la liste des contacts liés aux actionnaires, Specify conditions to calculate shipping amount,Spécifier les conditions pour calculer le montant de la livraison, @@ -5157,9 +4766,6 @@ Supplier Scorecard Scoring Criteria,Critères de Notation de la Fiche d'Évaluat Score,Score, Supplier Scorecard Scoring Standing,Classement de la Fiche d'Évaluation Fournisseur, Standing Name,Nom du Classement, -Purple,Violet, -Yellow,jaune, -Orange,orange, Min Grade,Note Minimale, Max Grade,Note Maximale, Warn Purchase Orders,Avertir lors de Bons de Commande, @@ -5194,7 +4800,6 @@ Unverified,Non vérifié, Customer Details,Détails du client, Phone Number,Numéro de téléphone, Skype ID,ID Skype, -Linked Documents,Documents liés, Appointment With,Rendez-vous avec, Calendar Event,Événement de calendrier, Appointment Booking Settings,Paramètres de réservation de rendez-vous, @@ -5212,7 +4817,6 @@ Success Settings,Paramètres de réussite, Success Redirect URL,URL de redirection réussie, "Leave blank for home.\nThis is relative to site URL, for example ""about"" will redirect to ""https://yoursitename.com/about""","Laissez vide pour la maison. Ceci est relatif à l'URL du site, par exemple "about" redirigera vers "https://yoursitename.com/about"", Appointment Booking Slots,Horaires de prise de rendez-vous, -Day Of Week,Jour de la Semaine, From Time ,Horaire de Début, Campaign Email Schedule,Calendrier des e-mails de campagne, Send After (days),Envoyer après (jours), @@ -5255,7 +4859,6 @@ Campaign Name,Nom de la Campagne, Follow Up,Suivre, Next Contact By,Contact Suivant Par, Next Contact Date,Date du Prochain Contact, -Ends On,Se termine le, Address & Contact,Adresse & Contact, Mobile No.,N° Mobile., Lead Type,Type de Lead, @@ -5302,8 +4905,6 @@ Social Media Post,Publication sur les réseaux sociaux, Post Status,Statut du message, Posted,Publié, Share On,Partager sur, -Twitter,Twitter, -LinkedIn,LinkedIn, Twitter Post Id,Identifiant de publication Twitter, LinkedIn Post Id,Identifiant de publication LinkedIn, Tweet,Tweet, @@ -5853,7 +5454,6 @@ Require Result Value,Nécessite la Valeur du Résultat, Normal Test Template,Modèle de Test Normal, Patient Demographics,Démographie du Patient, HLC-PAT-.YYYY.-,HLC-PAT-. AAAA.-, -Middle Name (optional),Prénom (facultatif), Inpatient Status,Statut d'hospitalisation, "If ""Link Customer to Patient"" is checked in Healthcare Settings and an existing Customer is not selected then, a Customer will be created for this Patient for recording transactions in Accounts module.","Si «Lier le client au patient» est coché dans les paramètres de soins de santé et qu'un client existant n'est pas sélectionné, un client sera créé pour ce patient pour enregistrer les transactions dans le module Comptes.", Personal and Social History,Antécédents Personnels et Sociaux, @@ -6180,7 +5780,6 @@ Maintenance Schedule Detail,Détails de l'Échéancier d'Entretien, Scheduled Date,Date Prévue, Actual Date,Date Réelle, Maintenance Schedule Item,Article de Calendrier d'Entretien, -Random,aléatoire, No of Visits,Nb de Visites, MAT-MVS-.YYYY.-,MAT-MVS-. AAAA.-, Maintenance Date,Date de l'Entretien, @@ -6430,7 +6029,6 @@ Member Since,Membre depuis, Payment ID,ID de paiement, Membership Settings,Paramètres d'adhésion, Enable RazorPay For Memberships,Activer RazorPay pour les adhésions, -RazorPay Settings,Paramètres de RazorPay, Billing Cycle,Cycle de facturation, Billing Frequency,Fréquence de facturation, "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.","Le nombre de cycles de facturation pour lesquels le client doit être facturé. Par exemple, si un client achète un abonnement d'un an qui doit être facturé sur une base mensuelle, cette valeur doit être de 12.", @@ -7009,7 +6607,7 @@ Leave blank to use the standard Delivery Note format,Laissez vide pour utiliser Send with Attachment,Envoyer avec pièce jointe, Delay between Delivery Stops,Délai entre les arrêts de livraison, Delivery Stop,Étape de Livraison, -Lock,Fermer à clé, +Lock,Verrouiller, Visited,Visité, Order Information,Informations sur la commande, Contact Information,Informations de contact, @@ -8191,7 +7789,6 @@ Topics updated,Sujets mis à jour, Academic Term and Program,Terme académique et programme, Please remove this item and try to submit again or update the posting time.,Veuillez supprimer cet élément et réessayer de le valider ou mettre à jour l'heure de publication., Failed to Authenticate the API key.,Échec de l'authentification de la clé API., -Invalid Credentials,Les informations d'identification invalides, URL can only be a string,L'URL ne peut être qu'une chaîne, "Here is your webhook secret, this will be shown to you only once.","Voici votre secret de webhook, il ne vous sera montré qu'une seule fois.", The payment for this membership is not paid. To generate invoice fill the payment details,"Le paiement de cette adhésion n'est pas payé. Pour générer une facture, remplissez les détails du paiement", @@ -8756,7 +8353,6 @@ Journal Energy Point,Historique des points d'énergies, Billing Address Details,Adresse de facturation (détails) Supplier Address Details,Adresse Fournisseur (détails) Retail,Commerce, -Users,Utilisateurs, Permission Manager,Gestion des permissions, Fetch Timesheet,Récuprer les temps saisis, Get Supplier Group Details,Appliquer les informations depuis le Groupe de fournisseur, From 55dbcee36a2acf4aa41c66147c263a85ef606f81 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Oct 2023 17:16:54 +0530 Subject: [PATCH 093/135] refactor: gain_loss posting date fields in the allocation table --- .../payment_reconciliation_allocation.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index ec718aa70d..2fddd85732 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -151,11 +151,16 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" + }, + { + "fieldname": "gain_loss_posting_date", + "fieldtype": "Date", + "label": "Difference Posting Date" } ], "istable": 1, "links": [], - "modified": "2023-09-03 07:52:33.684217", + "modified": "2023-10-23 10:44:56.066303", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", From 5323bb7beeb6526d16bcb19fc2f3acd3a95927e6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 22 Oct 2023 08:59:52 +0530 Subject: [PATCH 094/135] refactor: introduce fields in popup --- .../payment_reconciliation/payment_reconciliation.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index d9f00befa9..7599b5e852 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -229,6 +229,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.data = []; const dialog = new frappe.ui.Dialog({ title: __("Select Difference Account"), + size: 'extra-large', fields: [ { fieldname: "allocation", @@ -252,6 +253,13 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo in_list_view: 1, read_only: 1 }, { + fieldtype:'Date', + fieldname:"gain_loss_posting_date", + label: __("Posting Date"), + in_list_view: 1, + reqd: 1, + }, { + fieldtype:'Link', options: 'Account', in_list_view: 1, @@ -285,6 +293,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo args.forEach(d => { frappe.model.set_value("Payment Reconciliation Allocation", d.docname, "difference_account", d.difference_account); + }); this.reconcile_payment_entries(); @@ -300,6 +309,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo 'reference_name': d.reference_name, 'difference_amount': d.difference_amount, 'difference_account': d.difference_account, + 'gain_loss_posting_date': d.gain_loss_posting_date }); } }); From b099590b2c1dcd041b833af50e99eb3e7988c595 Mon Sep 17 00:00:00 2001 From: Richard Case <110036763+casesolved-co-uk@users.noreply.github.com> Date: Mon, 23 Oct 2023 07:10:07 +0100 Subject: [PATCH 095/135] fix: Quality Inspection Parameter migration - DuplicateEntryError due to case sensitivity (#37499) * fix: account for case-insensitive database primary key for parameter names * chore: linting --- .../convert_qi_parameter_to_link_field.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) 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) From 7e600a6494d7f07c6fd2b8f1cc71857801a2573c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 22 Oct 2023 20:26:45 +0530 Subject: [PATCH 096/135] refactor: pass gain loss posting date to controller --- .../payment_reconciliation/payment_reconciliation.js | 2 ++ .../payment_reconciliation/payment_reconciliation.py | 2 ++ .../payment_reconciliation_allocation.json | 1 + erpnext/accounts/utils.py | 4 +++- erpnext/controllers/accounts_controller.py | 6 ++++-- 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 7599b5e852..fc90c3dec0 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -293,6 +293,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo args.forEach(d => { frappe.model.set_value("Payment Reconciliation Allocation", d.docname, "difference_account", d.difference_account); + frappe.model.set_value("Payment Reconciliation Allocation", d.docname, + "gain_loss_posting_date", d.gain_loss_posting_date); }); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 3285a529d2..1626f25f3e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -328,6 +328,7 @@ class PaymentReconciliation(Document): res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"]) res.difference_account = default_exchange_gain_loss_account res.exchange_rate = inv.get("exchange_rate") + res.update({"gain_loss_posting_date": pay.get("posting_date")}) if pay.get("amount") == 0: entries.append(res) @@ -434,6 +435,7 @@ class PaymentReconciliation(Document): "allocated_amount": flt(row.get("allocated_amount")), "difference_amount": flt(row.get("difference_amount")), "difference_account": row.get("difference_account"), + "difference_posting_date": row.get("gain_loss_posting_date"), "cost_center": row.get("cost_center"), } ) diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 2fddd85732..5b8556e7c8 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -19,6 +19,7 @@ "is_advance", "section_break_5", "difference_amount", + "gain_loss_posting_date", "column_break_7", "difference_account", "exchange_rate", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 555ed4ffa2..f2691fb980 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -679,7 +679,9 @@ def update_reference_in_payment_entry( if not skip_ref_details_update_for_pe: payment_entry.set_missing_ref_details() payment_entry.set_amounts() - payment_entry.make_exchange_gain_loss_journal() + payment_entry.make_exchange_gain_loss_journal( + frappe._dict({"difference_posting_date": d.difference_posting_date}) + ) if not do_not_save: payment_entry.save(ignore_permissions=True) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index cc5d643c14..6efe631a29 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1178,7 +1178,9 @@ class AccountsController(TransactionBase): self.name, arg.get("referenced_row"), ): - posting_date = frappe.db.get_value(arg.voucher_type, arg.voucher_no, "posting_date") + posting_date = arg.get("difference_posting_date") or frappe.db.get_value( + arg.voucher_type, arg.voucher_no, "posting_date" + ) je = create_gain_loss_journal( self.company, posting_date, @@ -1261,7 +1263,7 @@ class AccountsController(TransactionBase): je = create_gain_loss_journal( self.company, - self.posting_date, + args.get("difference_posting_date") if args else self.posting_date, self.party_type, self.party, party_account, From 514d5434a3ae24e2c7839fbd76a115d6c0841513 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 23 Oct 2023 12:32:10 +0530 Subject: [PATCH 097/135] test: varying posting date for gain loss journal --- .../tests/test_accounts_controller.py | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 391258fde7..97d3c5c32d 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -7,7 +7,7 @@ import frappe from frappe import qb from frappe.query_builder.functions import Sum from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowdate +from frappe.utils import add_days, flt, getdate, nowdate from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -614,6 +614,73 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) + def test_15_gain_loss_on_different_posting_date(self): + # Invoice in Foreign Currency + si = self.create_sales_invoice( + posting_date=add_days(nowdate(), -2), qty=2, conversion_rate=80, rate=1 + ) + # Payment + pe = ( + self.create_payment_entry(posting_date=add_days(nowdate(), -1), amount=2, source_exc_rate=75) + .save() + .submit() + ) + + # There should be outstanding in both currencies + si.reload() + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) + + # Reconcile the remaining amount + pr = frappe.get_doc("Payment Reconciliation") + pr.company = self.company + pr.party_type = "Customer" + pr.party = self.customer + pr.receivable_payable_account = self.debit_usd + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].gain_loss_posting_date = add_days(nowdate(), 1) + pr.reconcile() + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + + self.assertEqual( + frappe.db.get_value("Journal Entry", exc_je_for_si[0].parent, "posting_date"), + getdate(add_days(nowdate(), 1)), + ) + + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be no outstanding + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Cancel Payment + pe.reload() + pe.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) From 2b64e1ca8b3e9162d4320552e1104385f010841f Mon Sep 17 00:00:00 2001 From: Imesha Sudasingha Date: Mon, 23 Oct 2023 15:28:52 +0530 Subject: [PATCH 098/135] chore: typo in description (#37636) chore: typo in description --- erpnext/stock/doctype/item/item.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 54491bbee3..c13d3ebe0f 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -379,7 +379,7 @@ "options": "fa fa-rss" }, { - "description": "Will also apply for variants unless overrridden", + "description": "Will also apply for variants unless overridden", "fieldname": "reorder_levels", "fieldtype": "Table", "label": "Reorder level based on Warehouse", @@ -961,4 +961,4 @@ "states": [], "title_field": "item_name", "track_changes": 1 -} \ No newline at end of file +} From a432290a828478265a8a463d05aea818c2b75914 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 23 Oct 2023 16:26:00 +0530 Subject: [PATCH 099/135] fix: ignore qty msg if From Voucher is set --- .../stock_reservation_entry.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index c7a9e16d0e..81e9dfa69b 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -833,13 +833,15 @@ def create_stock_reservation_entries_for_so_items( # Skip if Non-Stock Item. if not is_stock_item: - frappe.msgprint( - _("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format( - item.idx, frappe.bold(item.item_code) - ), - title=_("Stock Reservation"), - indicator="yellow", - ) + if not from_voucher_type: + frappe.msgprint( + _("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Stock Reservation"), + indicator="yellow", + ) + item.db_set("reserve_stock", 0) continue @@ -858,13 +860,15 @@ def create_stock_reservation_entries_for_so_items( # Stock is already reserved for the item, notify the user and skip the item. if unreserved_qty <= 0: - frappe.msgprint( - _("Row #{0}: Stock is already reserved for the Item {1}.").format( - item.idx, frappe.bold(item.item_code) - ), - title=_("Stock Reservation"), - indicator="yellow", - ) + if not from_voucher_type: + frappe.msgprint( + _("Row #{0}: Stock is already reserved for the Item {1}.").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Stock Reservation"), + indicator="yellow", + ) + continue available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse) @@ -872,7 +876,7 @@ def create_stock_reservation_entries_for_so_items( # No stock available to reserve, notify the user and skip the item. if available_qty_to_reserve <= 0: frappe.msgprint( - _("Row #{0}: No available stock to reserve for the Item {1} in Warehouse {2}.").format( + _("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format( item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse) ), title=_("Stock Reservation"), @@ -898,7 +902,9 @@ def create_stock_reservation_entries_for_so_items( # Partial Reservation if qty_to_be_reserved < unreserved_qty: - if not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")): + if not from_voucher_type and ( + not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")) + ): msg = _("Row #{0}: Only {1} available to reserve for the Item {2}").format( item.idx, frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom), From adf313a6d3308f957b4876574e455ca750b22106 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 23 Oct 2023 17:52:59 +0530 Subject: [PATCH 100/135] test: add test case for auto-reservation from PR --- .../test_stock_reservation_entry.py | 89 ++++++++++++++++++- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index 9ea35ecacb..f4c74a8aac 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -5,6 +5,7 @@ from random import randint import frappe from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import today from erpnext.selling.doctype.sales_order.sales_order import create_pick_list, make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order @@ -28,10 +29,6 @@ class TestStockReservationEntry(FrappeTestCase): items={self.sr_item.name: self.sr_item}, warehouse=self.warehouse, qty=100 ) - def tearDown(self) -> None: - cancel_all_stock_reservation_entries() - return super().tearDown() - @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_validate_stock_reservation_settings(self) -> None: from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( @@ -568,6 +565,90 @@ class TestStockReservationEntry(FrappeTestCase): # Test - 3: Reserved Serial/Batch Nos should be equal to Picked Serial/Batch Nos. self.assertSetEqual(picked_sb_details, reserved_sb_details) + @change_settings( + "Stock Settings", + { + "allow_negative_stock": 0, + "enable_stock_reservation": 1, + "auto_reserve_serial_and_batch": 1, + "pick_serial_and_batch_based_on": "FIFO", + "auto_reserve_stock_for_sales_order_on_purchase": 1, + }, + ) + def test_stock_reservation_from_purchase_receipt(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.selling.doctype.sales_order.sales_order import make_material_request + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + + items_details = create_items() + create_material_receipt(items_details, self.warehouse, qty=10) + + item_list = [] + for item_code, properties in items_details.items(): + item_list.append( + { + "item_code": item_code, + "warehouse": self.warehouse, + "qty": randint(11, 100), + "uom": properties.stock_uom, + "rate": randint(10, 400), + } + ) + + so = make_sales_order( + item_list=item_list, + warehouse=self.warehouse, + ) + + mr = make_material_request(so.name) + mr.schedule_date = today() + mr.save().submit() + + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + po.save().submit() + + pr = make_purchase_receipt(po.name) + pr.save().submit() + + for item in pr.items: + sre, status, reserved_qty = frappe.db.get_value( + "Stock Reservation Entry", + { + "from_voucher_type": "Purchase Receipt", + "from_voucher_no": pr.name, + "from_voucher_detail_no": item.name, + }, + ["name", "status", "reserved_qty"], + ) + + # Test - 1: SRE status should be `Reserved`. + self.assertEqual(status, "Reserved") + + # Test - 2: SRE Reserved Qty should be equal to PR Item Qty. + self.assertEqual(reserved_qty, item.qty) + + if item.serial_and_batch_bundle: + sb_details = frappe.db.get_all( + "Serial and Batch Entry", + filters={"parent": item.serial_and_batch_bundle}, + fields=["serial_no", "batch_no", "qty"], + as_list=True, + ) + reserved_sb_details = frappe.db.get_all( + "Serial and Batch Entry", + filters={"parent": sre}, + fields=["serial_no", "batch_no", "qty"], + as_list=True, + ) + + # Test - 3: Reserved Serial/Batch Nos should be equal to PR Item Serial/Batch Nos. + self.assertEqual(set(sb_details), set(reserved_sb_details)) + + def tearDown(self) -> None: + cancel_all_stock_reservation_entries() + return super().tearDown() + def create_items() -> dict: items_properties = [ From 24788ddcc085fb825d2b14145a82ced02842f512 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 23 Oct 2023 18:00:58 +0530 Subject: [PATCH 101/135] chore: add SRE link in PR Connections --- .../doctype/purchase_receipt/purchase_receipt_dashboard.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py index b3ae7b58b4..71489fbb49 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py @@ -10,6 +10,7 @@ def get_data(): "Landed Cost Voucher": "receipt_document", "Auto Repeat": "reference_document", "Purchase Receipt": "return_against", + "Stock Reservation Entry": "from_voucher_no", }, "internal_links": { "Material Request": ["items", "material_request"], @@ -18,7 +19,10 @@ def get_data(): "Quality Inspection": ["items", "quality_inspection"], }, "transactions": [ - {"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]}, + { + "label": _("Related"), + "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset", "Stock Reservation Entry"], + }, { "label": _("Reference"), "items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"], From 3bfb7b79f297ff1ab6c80c810431ecebd6bedecb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 23 Oct 2023 18:23:45 +0530 Subject: [PATCH 102/135] refactor: Remove expense included in valuation accounts (#37632) * refactor: Remove expense included in valuation accounts * test: Deprecate tests * test: Depricate tests * test: Depricate tests --- .../purchase_invoice/purchase_invoice.py | 39 +-------- .../sales_invoice/test_sales_invoice.py | 36 -------- erpnext/manufacturing/doctype/bom/bom.py | 8 +- ...se_account_in_landed_cost_voucher_taxes.py | 42 ---------- erpnext/setup/doctype/company/company.js | 3 - erpnext/setup/doctype/company/company.json | 20 +---- erpnext/setup/doctype/company/company.py | 3 - .../purchase_receipt/purchase_receipt.py | 14 +--- .../purchase_receipt/test_purchase_receipt.py | 82 ------------------- .../doctype/stock_entry/test_stock_entry.py | 12 +-- .../subcontracting_receipt.py | 6 +- 11 files changed, 17 insertions(+), 248 deletions(-) delete mode 100644 erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 2f08b65ac6..97ee5cc93b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -585,13 +585,12 @@ class PurchaseInvoice(BuyingController): def get_gl_entries(self, warehouse_account=None): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) + self.asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") + 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 self.negative_expense_to_be_booked = 0.0 gl_entries = [] @@ -913,40 +912,6 @@ class PurchaseInvoice(BuyingController): ) ) - # If asset is bought through this document and not linked to PR - if self.update_stock and item.landed_cost_voucher_amount: - expenses_included_in_asset_valuation = self.get_company_default( - "expenses_included_in_asset_valuation" - ) - # Amount added through landed-cost-voucher - gl_entries.append( - self.get_gl_dict( - { - "account": expenses_included_in_asset_valuation, - "against": expense_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": expense_account, - "against": expenses_included_in_asset_valuation, - "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 asset bought through this document assets = frappe.db.get_all( "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 16477324e6..231b3bf7fe 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2500,12 +2500,6 @@ class TestSalesInvoice(FrappeTestCase): "stock_received_but_not_billed", "Stock Received But Not Billed - _TC1", ) - frappe.db.set_value( - "Company", - "_Test Company 1", - "expenses_included_in_valuation", - "Expenses Included In Valuation - _TC1", - ) # begin test si = create_sales_invoice( @@ -2545,36 +2539,6 @@ class TestSalesInvoice(FrappeTestCase): frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock) - def test_sle_for_target_warehouse(self): - se = make_stock_entry( - item_code="138-CMS Shoe", - target="Finished Goods - _TC", - company="_Test Company", - qty=1, - basic_rate=500, - ) - - si = frappe.copy_doc(test_records[0]) - si.update_stock = 1 - si.set_warehouse = "Finished Goods - _TC" - si.set_target_warehouse = "Stores - _TC" - si.get("items")[0].warehouse = "Finished Goods - _TC" - si.get("items")[0].target_warehouse = "Stores - _TC" - si.insert() - si.submit() - - sles = frappe.get_all( - "Stock Ledger Entry", filters={"voucher_no": si.name}, fields=["name", "actual_qty"] - ) - - # check if both SLEs are created - self.assertEqual(len(sles), 2) - self.assertEqual(sum(d.actual_qty for d in sles), 0.0) - - # tear down - si.cancel() - se.cancel() - def test_internal_transfer_gl_entry(self): si = create_sales_invoice( company="_Test Company with perpetual inventory", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 023166849d..229f8853ff 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1196,12 +1196,12 @@ def get_children(parent=None, is_root=False, **filters): def add_additional_cost(stock_entry, work_order): # Add non stock items cost in the additional cost stock_entry.additional_costs = [] - expenses_included_in_valuation = frappe.get_cached_value( - "Company", work_order.company, "expenses_included_in_valuation" + default_expense_account = frappe.get_cached_value( + "Company", work_order.company, "default_expense_account" ) - add_non_stock_items_cost(stock_entry, work_order, expenses_included_in_valuation) - add_operations_cost(stock_entry, work_order, expenses_included_in_valuation) + add_non_stock_items_cost(stock_entry, work_order, default_expense_account) + add_operations_cost(stock_entry, work_order, default_expense_account) def add_non_stock_items_cost(stock_entry, work_order, expense_account): diff --git a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py b/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py deleted file mode 100644 index 9588e026d3..0000000000 --- a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py +++ /dev/null @@ -1,42 +0,0 @@ -import frappe - - -def execute(): - frappe.reload_doctype("Landed Cost Taxes and Charges") - - company_account_map = frappe._dict( - frappe.db.sql( - """ - SELECT name, expenses_included_in_valuation from `tabCompany` - """ - ) - ) - - for company, account in company_account_map.items(): - frappe.db.sql( - """ - UPDATE - `tabLanded Cost Taxes and Charges` t, `tabLanded Cost Voucher` l - SET - t.expense_account = %s - WHERE - l.docstatus = 1 - AND l.company = %s - AND t.parent = l.name - """, - (account, company), - ) - - frappe.db.sql( - """ - UPDATE - `tabLanded Cost Taxes and Charges` t, `tabStock Entry` s - SET - t.expense_account = %s - WHERE - s.docstatus = 1 - AND s.company = %s - AND t.parent = s.name - """, - (account, company), - ) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 4973dab505..23b93dc161 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -221,7 +221,6 @@ erpnext.company.setup_queries = function(frm) { ["cost_center", {}], ["round_off_cost_center", {}], ["depreciation_cost_center", {}], - ["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}], ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}], @@ -236,8 +235,6 @@ erpnext.company.setup_queries = function(frm) { $.each([ ["stock_adjustment_account", {"root_type": "Expense", "account_type": "Stock Adjustment"}], - ["expenses_included_in_valuation", - {"root_type": "Expense", "account_type": "Expenses Included in Valuation"}], ["stock_received_but_not_billed", {"root_type": "Liability", "account_type": "Stock Received But Not Billed"}], ["service_received_but_not_billed", diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 24d7da45b8..b9ff3dddd1 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -80,7 +80,6 @@ "accumulated_depreciation_account", "depreciation_expense_account", "series_for_depreciation_entry", - "expenses_included_in_asset_valuation", "column_break_40", "disposal_account", "depreciation_cost_center", @@ -103,11 +102,10 @@ "enable_provisional_accounting_for_non_stock_items", "default_inventory_account", "stock_adjustment_account", - "default_in_transit_warehouse", "column_break_32", "stock_received_but_not_billed", "default_provisional_account", - "expenses_included_in_valuation", + "default_in_transit_warehouse", "dashboard_tab" ], "fields": [ @@ -469,14 +467,6 @@ "no_copy": 1, "options": "Account" }, - { - "fieldname": "expenses_included_in_valuation", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Expenses Included In Valuation", - "no_copy": 1, - "options": "Account" - }, { "fieldname": "accumulated_depreciation_account", "fieldtype": "Link", @@ -496,12 +486,6 @@ "fieldtype": "Data", "label": "Series for Asset Depreciation Entry (Journal Entry)" }, - { - "fieldname": "expenses_included_in_asset_valuation", - "fieldtype": "Link", - "label": "Expenses Included In Asset Valuation", - "options": "Account" - }, { "fieldname": "column_break_40", "fieldtype": "Column Break" @@ -782,7 +766,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2023-09-10 21:53:13.860791", + "modified": "2023-10-23 10:19:24.322898", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index b05696ad96..3413702c5a 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -92,7 +92,6 @@ class Company(NestedSet): ["Default Income Account", "default_income_account"], ["Stock Received But Not Billed Account", "stock_received_but_not_billed"], ["Stock Adjustment Account", "stock_adjustment_account"], - ["Expense Included In Valuation Account", "expenses_included_in_valuation"], ] for account in accounts: @@ -384,7 +383,6 @@ class Company(NestedSet): "depreciation_expense_account": "Depreciation", "capital_work_in_progress_account": "Capital Work in Progress", "asset_received_but_not_billed": "Asset Received But Not Billed", - "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation", "default_expense_account": "Cost of Goods Sold", } @@ -394,7 +392,6 @@ class Company(NestedSet): "stock_received_but_not_billed": "Stock Received But Not Billed", "default_inventory_account": "Stock", "stock_adjustment_account": "Stock Adjustment", - "expenses_included_in_valuation": "Expenses Included In Valuation", } ) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 9fdb01a662..d89d8057a8 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -491,7 +491,6 @@ class PurchaseReceipt(BuyingController): return # divisional loss adjustment - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") valuation_amount_as_per_doc = ( flt(outgoing_amount, d.precision("base_net_amount")) + flt(item.landed_cost_voucher_amount) @@ -505,13 +504,10 @@ class PurchaseReceipt(BuyingController): ) if divisional_loss: - if self.is_return or flt(item.item_tax_amount): - loss_account = expenses_included_in_valuation - else: - loss_account = ( - self.get_company_default("default_expense_account", ignore_validation=True) - or stock_asset_rbnb - ) + loss_account = ( + self.get_company_default("default_expense_account", ignore_validation=True) + or stock_asset_rbnb + ) cost_center = item.cost_center or frappe.get_cached_value( "Company", self.company, "cost_center" @@ -684,10 +680,8 @@ class PurchaseReceipt(BuyingController): if negative_expense_to_be_booked and valuation_tax: # Backward compatibility: - # If expenses_included_in_valuation account has been credited in against PI # and charges added via Landed Cost Voucher, # post valuation related charges on "Stock Received But Not Billed" - # introduced in 2014 for backward compatibility of expenses already booked in expenses_included_in_valuation account against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 1af7b9aefc..e998b842d1 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -957,88 +957,6 @@ class TestPurchaseReceipt(FrappeTestCase): pr1.reload() pr1.cancel() - def test_stock_transfer_from_purchase_receipt(self): - pr1 = make_purchase_receipt( - warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory" - ) - - pr = make_purchase_receipt( - company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 - ) - - pr.supplier_warehouse = "" - pr.items[0].from_warehouse = "Work In Progress - TCP1" - - pr.submit() - - gl_entries = get_gl_entries("Purchase Receipt", pr.name) - sl_entries = get_sl_entries("Purchase Receipt", pr.name) - - self.assertFalse(gl_entries) - - expected_sle = {"Work In Progress - TCP1": -5, "Stores - TCP1": 5} - - for sle in sl_entries: - self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) - - pr.cancel() - pr1.cancel() - - def test_stock_transfer_from_purchase_receipt_with_valuation(self): - create_warehouse( - "_Test Warehouse for Valuation", - company="_Test Company with perpetual inventory", - properties={"account": "_Test Account Stock In Hand - TCP1"}, - ) - - pr1 = make_purchase_receipt( - warehouse="_Test Warehouse for Valuation - TCP1", - company="_Test Company with perpetual inventory", - ) - - pr = make_purchase_receipt( - company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 - ) - - pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1" - pr.supplier_warehouse = "" - - pr.append( - "taxes", - { - "charge_type": "On Net Total", - "account_head": "_Test Account Shipping Charges - TCP1", - "category": "Valuation and Total", - "cost_center": "Main - TCP1", - "description": "Test", - "rate": 9, - }, - ) - - pr.submit() - - gl_entries = get_gl_entries("Purchase Receipt", pr.name) - sl_entries = get_sl_entries("Purchase Receipt", pr.name) - - expected_gle = [ - ["Stock In Hand - TCP1", 272.5, 0.0], - ["_Test Account Stock In Hand - TCP1", 0.0, 250.0], - ["_Test Account Shipping Charges - TCP1", 0.0, 22.5], - ] - - expected_sle = {"_Test Warehouse for Valuation - TCP1": -5, "Stores - TCP1": 5} - - for sle in sl_entries: - self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) - - for i, gle in enumerate(gl_entries): - self.assertEqual(gle.account, expected_gle[i][0]) - self.assertEqual(gle.debit, expected_gle[i][1]) - self.assertEqual(gle.credit, expected_gle[i][2]) - - pr.cancel() - pr1.cancel() - def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: - Create PO diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index cc8a108bc9..3e0610ef6e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -449,9 +449,7 @@ class TestStockEntry(FrappeTestCase): repack.posting_date = nowdate() repack.posting_time = nowtime() - expenses_included_in_valuation = frappe.get_value( - "Company", company, "expenses_included_in_valuation" - ) + default_expense_account = frappe.get_value("Company", company, "default_expense_account") items = get_multiple_items() repack.items = [] @@ -462,12 +460,12 @@ class TestStockEntry(FrappeTestCase): "additional_costs", [ { - "expense_account": expenses_included_in_valuation, + "expense_account": default_expense_account, "description": "Actual Operating Cost", "amount": 1000, }, { - "expense_account": expenses_included_in_valuation, + "expense_account": default_expense_account, "description": "Additional Operating Cost", "amount": 200, }, @@ -506,9 +504,7 @@ class TestStockEntry(FrappeTestCase): self.check_gl_entries( "Stock Entry", repack.name, - sorted( - [[stock_in_hand_account, 1200, 0.0], ["Expenses Included In Valuation - TCP1", 0.0, 1200.0]] - ), + sorted([[stock_in_hand_account, 1200, 0.0], ["Cost of Goods Sold - TCP1", 0.0, 1200.0]]), ) def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 6aecaf98a5..7e06444e1e 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -410,7 +410,6 @@ class SubcontractingReceipt(SubcontractingController): def make_item_gl_entries(self, gl_entries, warehouse_account=None): stock_rbnb = self.get_company_default("stock_received_but_not_billed") - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") warehouse_with_no_account = [] @@ -482,10 +481,7 @@ class SubcontractingReceipt(SubcontractingController): divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount")) if divisional_loss: - if self.is_return: - loss_account = expenses_included_in_valuation - else: - loss_account = item.expense_account + loss_account = item.expense_account self.add_gl_entry( gl_entries=gl_entries, From 6942ab10125cfaf07c526df53a7c88bffcc5b9da Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 23 Oct 2023 19:12:55 +0530 Subject: [PATCH 103/135] chore: patch to update `From Voucher` details --- erpnext/patches.txt | 1 + .../v15_0/update_sre_from_voucher_details.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 erpnext/patches/v15_0/update_sre_from_voucher_details.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d59fe0ec4c..53bddb562c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -340,5 +340,6 @@ 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 +erpnext.patches.v15_0.update_sre_from_voucher_details # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger \ No newline at end of file diff --git a/erpnext/patches/v15_0/update_sre_from_voucher_details.py b/erpnext/patches/v15_0/update_sre_from_voucher_details.py new file mode 100644 index 0000000000..a9653ccbf4 --- /dev/null +++ b/erpnext/patches/v15_0/update_sre_from_voucher_details.py @@ -0,0 +1,15 @@ +import frappe +from frappe.query_builder.functions import IfNull + + +def execute(): + sre = frappe.qb.DocType("Stock Reservation Entry") + ( + frappe.qb.update(sre) + .set(sre.from_voucher_type, "Pick List") + .set(sre.from_voucher_no, sre.against_pick_list) + .set(sre.from_voucher_detail_no, sre.against_pick_list_item) + .where( + (IfNull(sre.against_pick_list, "") != "") & (IfNull(sre.against_pick_list_item, "") != "") + ) + ).run() From 89f484282a90516c117c31624fb2cc5eab6fb840 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 22 Oct 2023 10:58:39 +0530 Subject: [PATCH 104/135] refactor: exc rate on foreign currency JE from Bank Reconciliation --- .../bank_reconciliation_tool.py | 89 ++++++++++++++----- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 9a7a9a31d5..777e315298 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -18,6 +18,7 @@ from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_s get_entries, ) from erpnext.accounts.utils import get_account_currency, get_balance_on +from erpnext.setup.utils import get_exchange_rate class BankReconciliationTool(Document): @@ -144,29 +145,74 @@ def create_journal_entry_bts( ) company = frappe.get_value("Account", company_account, "company") + company_default_currency = frappe.get_cached_value("Company", company, "default_currency") + company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency") + second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency") + + is_multi_currency = ( + True + if company_default_currency != company_account_currency + or company_default_currency != second_account_currency + else False + ) accounts = [] - # Multi Currency? - accounts.append( - { - "account": second_account, - "credit_in_account_currency": bank_transaction.deposit, - "debit_in_account_currency": bank_transaction.withdrawal, - "party_type": party_type, - "party": party, - "cost_center": get_default_cost_center(company), - } - ) + second_account_dict = { + "account": second_account, + "account_currency": second_account_currency, + "credit_in_account_currency": bank_transaction.deposit, + "debit_in_account_currency": bank_transaction.withdrawal, + "party_type": party_type, + "party": party, + "cost_center": get_default_cost_center(company), + } - accounts.append( - { - "account": company_account, - "bank_account": bank_transaction.bank_account, - "credit_in_account_currency": bank_transaction.withdrawal, - "debit_in_account_currency": bank_transaction.deposit, - "cost_center": get_default_cost_center(company), - } - ) + company_account_dict = { + "account": company_account, + "account_currency": company_account_currency, + "bank_account": bank_transaction.bank_account, + "credit_in_account_currency": bank_transaction.withdrawal, + "debit_in_account_currency": bank_transaction.deposit, + "cost_center": get_default_cost_center(company), + } + + if is_multi_currency: + exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date) + withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal)) + deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit)) + + if second_account_currency != company_default_currency: + exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date) + second_account_dict.update( + { + "exchange_rate": exc_rate, + "credit": deposit_in_company_currency, + "debit": withdrawal_in_company_currency, + } + ) + else: + second_account_dict.update( + { + "exchange_rate": 1, + "credit": deposit_in_company_currency, + "debit": withdrawal_in_company_currency, + "credit_in_account_currency": deposit_in_company_currency, + "debit_in_account_currency": withdrawal_in_company_currency, + } + ) + + if company_account_currency != company_default_currency: + exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date) + company_account_dict.update( + { + "exchange_rate": exc_rate, + "credit": withdrawal_in_company_currency, + "debit": deposit_in_company_currency, + } + ) + + accounts.append(second_account_dict) + accounts.append(company_account_dict) journal_entry_dict = { "voucher_type": entry_type, @@ -176,6 +222,9 @@ def create_journal_entry_bts( "cheque_no": reference_number, "mode_of_payment": mode_of_payment, } + if is_multi_currency: + journal_entry_dict.update({"multi_currency": True}) + journal_entry = frappe.new_doc("Journal Entry") journal_entry.update(journal_entry_dict) journal_entry.set("accounts", accounts) From 23df4205f8abfca6764d84665cb9877703c1a470 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 19 Oct 2023 11:32:21 +0530 Subject: [PATCH 105/135] fix: overallocation on Payment with PO/SO --- erpnext/accounts/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index f2691fb980..1c7052f8ff 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -645,7 +645,7 @@ def update_reference_in_payment_entry( "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, "exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(), - "exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation + "exchange_gain_loss": d.exchange_gain_loss, "account": d.account, } @@ -658,22 +658,21 @@ def update_reference_in_payment_entry( existing_row.reference_doctype, existing_row.reference_name ).set_total_advance_paid() - original_row = existing_row.as_dict().copy() - existing_row.update(reference_details) + if d.allocated_amount <= existing_row.allocated_amount: + existing_row.allocated_amount -= d.allocated_amount - if d.allocated_amount < original_row.allocated_amount: new_row = payment_entry.append("references") new_row.docstatus = 1 for field in list(reference_details): - new_row.set(field, original_row[field]) + new_row.set(field, reference_details[field]) - new_row.allocated_amount = original_row.allocated_amount - d.allocated_amount else: new_row = payment_entry.append("references") new_row.docstatus = 1 new_row.update(reference_details) payment_entry.flags.ignore_validate_update_after_submit = True + payment_entry.clear_unallocated_reference_document_rows() payment_entry.setup_party_account_field() payment_entry.set_missing_values() if not skip_ref_details_update_for_pe: From 946228d783cb58cd2809f4b0bc0a854c49cc4060 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 19 Oct 2023 14:03:21 +0530 Subject: [PATCH 106/135] test: overalloction on reconciliation when PO is involved --- .../test_payment_reconciliation.py | 183 +++++++++++++++++- 1 file changed, 178 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 1d843abde1..48d1cf2cc2 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -14,6 +14,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.stock.doctype.item.test_item import create_item test_dependencies = ["Item"] @@ -151,6 +152,64 @@ class TestPaymentReconciliation(FrappeTestCase): payment.posting_date = posting_date return payment + def create_purchase_invoice( + self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in sales invoice + """ + pinv = make_purchase_invoice( + qty=qty, + rate=rate, + company=self.company, + customer=self.supplier, + item_code=self.item, + item_name=self.item, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + update_stock=0, + currency="INR", + is_pos=0, + is_return=0, + return_against=None, + income_account=self.income_account, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return pinv + + def create_purchase_order( + self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in sales invoice + """ + pord = create_purchase_order( + qty=qty, + rate=rate, + company=self.company, + customer=self.supplier, + item_code=self.item, + item_name=self.item, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + update_stock=0, + currency="INR", + is_pos=0, + is_return=0, + return_against=None, + income_account=self.income_account, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return pord + def clear_old_entries(self): doctype_list = [ "GL Entry", @@ -163,13 +222,11 @@ class TestPaymentReconciliation(FrappeTestCase): for doctype in doctype_list: qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() - def create_payment_reconciliation(self): + def create_payment_reconciliation(self, party_is_customer=True): pr = frappe.new_doc("Payment Reconciliation") pr.company = self.company - pr.party_type = ( - self.party_type if hasattr(self, "party_type") and self.party_type else "Customer" - ) - pr.party = self.customer + pr.party_type = "Customer" if party_is_customer else "Supplier" + pr.party = self.customer if party_is_customer else self.supplier pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company) pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() return pr @@ -931,6 +988,7 @@ class TestPaymentReconciliation(FrappeTestCase): if invoice.invoice_number == pi.name: invoices.append(invoice.as_dict()) break + for payment in pr.payments: if payment.reference_name == pi_return.name: payments.append(payment.as_dict()) @@ -941,6 +999,121 @@ class TestPaymentReconciliation(FrappeTestCase): # Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit. pr.reconcile() + def test_reconciliation_from_purchase_order_to_multiple_invoices(self): + """ + Reconciling advance payment from PO/SO to multiple invoices should not cause overallocation + """ + + self.supplier = "_Test Supplier" + + pi1 = self.create_purchase_invoice(qty=10, rate=100) + pi2 = self.create_purchase_invoice(qty=10, rate=100) + po = self.create_purchase_order(qty=20, rate=100) + pay = get_payment_entry(po.doctype, po.name) + # Overpay Puchase Order + pay.paid_amount = 3000 + pay.save().submit() + # assert total allocated and unallocated before reconciliation + self.assertEqual( + ( + pay.references[0].reference_doctype, + pay.references[0].reference_name, + pay.references[0].allocated_amount, + ), + (po.doctype, po.name, 2000), + ) + self.assertEqual(pay.total_allocated_amount, 2000) + self.assertEqual(pay.unallocated_amount, 1000) + self.assertEqual(pay.difference_amount, 0) + + pr = self.create_payment_reconciliation(party_is_customer=False) + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 2) + self.assertEqual(len(pr.payments), 2) + + for x in pr.payments: + self.assertEqual((x.reference_type, x.reference_name), (pay.doctype, pay.name)) + + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + # partial allocation on pi1 and full allocate on pi2 + pr.allocation[0].allocated_amount = 100 + pr.reconcile() + + # assert references and total allocated and unallocated amount + pay.reload() + self.assertEqual(len(pay.references), 3) + self.assertEqual( + ( + pay.references[0].reference_doctype, + pay.references[0].reference_name, + pay.references[0].allocated_amount, + ), + (po.doctype, po.name, 900), + ) + self.assertEqual( + ( + pay.references[1].reference_doctype, + pay.references[1].reference_name, + pay.references[1].allocated_amount, + ), + (pi1.doctype, pi1.name, 100), + ) + self.assertEqual( + ( + pay.references[2].reference_doctype, + pay.references[2].reference_name, + pay.references[2].allocated_amount, + ), + (pi2.doctype, pi2.name, 1000), + ) + self.assertEqual(pay.total_allocated_amount, 2000) + self.assertEqual(pay.unallocated_amount, 1000) + self.assertEqual(pay.difference_amount, 0) + + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 2) + + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + # assert references and total allocated and unallocated amount + pay.reload() + self.assertEqual(len(pay.references), 3) + # PO references should be removed now + self.assertEqual( + ( + pay.references[0].reference_doctype, + pay.references[0].reference_name, + pay.references[0].allocated_amount, + ), + (pi1.doctype, pi1.name, 100), + ) + self.assertEqual( + ( + pay.references[1].reference_doctype, + pay.references[1].reference_name, + pay.references[1].allocated_amount, + ), + (pi2.doctype, pi2.name, 1000), + ) + self.assertEqual( + ( + pay.references[2].reference_doctype, + pay.references[2].reference_name, + pay.references[2].allocated_amount, + ), + (pi1.doctype, pi1.name, 900), + ) + self.assertEqual(pay.total_allocated_amount, 2000) + self.assertEqual(pay.unallocated_amount, 1000) + self.assertEqual(pay.difference_amount, 0) + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): From 547993f80103fa192563a82447c39fe122918767 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 19 Oct 2023 14:57:05 +0530 Subject: [PATCH 107/135] refactor(test): make use of utility methods --- .../test_payment_reconciliation.py | 79 ++++++++++++------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 48d1cf2cc2..71bc498b49 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -86,26 +86,44 @@ class TestPaymentReconciliation(FrappeTestCase): self.customer5 = make_customer("_Test PR Customer 5", "EUR") def create_account(self): - account_name = "Debtors EUR" - if not frappe.db.get_value( - "Account", filters={"account_name": account_name, "company": self.company} - ): - acc = frappe.new_doc("Account") - acc.account_name = account_name - acc.parent_account = "Accounts Receivable - _PR" - acc.company = self.company - acc.account_currency = "EUR" - acc.account_type = "Receivable" - acc.insert() - else: - name = frappe.db.get_value( - "Account", - filters={"account_name": account_name, "company": self.company}, - fieldname="name", - pluck=True, - ) - acc = frappe.get_doc("Account", name) - self.debtors_eur = acc.name + accounts = [ + { + "attribute": "debtors_eur", + "account_name": "Debtors EUR", + "parent_account": "Accounts Receivable - _PR", + "account_currency": "EUR", + "account_type": "Receivable", + }, + { + "attribute": "creditors_usd", + "account_name": "Payable USD", + "parent_account": "Accounts Payable - _PR", + "account_currency": "USD", + "account_type": "Payable", + }, + ] + + for x in accounts: + x = frappe._dict(x) + if not frappe.db.get_value( + "Account", filters={"account_name": x.account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = x.account_name + acc.parent_account = x.parent_account + acc.company = self.company + acc.account_currency = x.account_currency + acc.account_type = x.account_type + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": x.account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + setattr(self, x.attribute, acc.name) def create_sales_invoice( self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False @@ -963,9 +981,13 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(pr.allocation[0].difference_amount, 0) def test_reconciliation_purchase_invoice_against_return(self): - pi = make_purchase_invoice( - supplier="_Test Supplier USD", currency="USD", conversion_rate=50 - ).submit() + self.supplier = "_Test Supplier USD" + pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True) + pi.supplier = self.supplier + pi.currency = "USD" + pi.conversion_rate = 50 + pi.credit_to = self.creditors_usd + pi.save().submit() pi_return = frappe.get_doc(pi.as_dict()) pi_return.name = None @@ -975,11 +997,12 @@ class TestPaymentReconciliation(FrappeTestCase): pi_return.items[0].qty = -pi_return.items[0].qty pi_return.submit() - self.company = "_Test Company" - self.party_type = "Supplier" - self.customer = "_Test Supplier USD" - - pr = self.create_payment_reconciliation() + pr = frappe.get_doc("Payment Reconciliation") + pr.company = self.company + pr.party_type = "Supplier" + pr.party = self.supplier + pr.receivable_payable_account = self.creditors_usd + pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() pr.get_unreconciled_entries() invoices = [] From 4dff2c7a0dad1de840a2b1f53d51e9fe1682fa7f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 24 Oct 2023 08:09:22 +0530 Subject: [PATCH 108/135] chore: fix flakiness `test_sales_order_partial_advance_payment` --- erpnext/selling/doctype/sales_order/test_sales_order.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 83689a2b0b..d8b5878aa3 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1784,10 +1784,10 @@ class TestSalesOrder(FrappeTestCase): si.submit() pe.load_from_db() - self.assertEqual(pe.references[0].reference_name, si.name) - self.assertEqual(pe.references[0].allocated_amount, 200) - self.assertEqual(pe.references[1].reference_name, so.name) - self.assertEqual(pe.references[1].allocated_amount, 300) + self.assertEqual(pe.references[0].reference_name, so.name) + self.assertEqual(pe.references[0].allocated_amount, 300) + self.assertEqual(pe.references[1].reference_name, si.name) + self.assertEqual(pe.references[1].allocated_amount, 200) def test_delivered_item_material_request(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." From 8e523961dc8dae52b90e8f0c98aeee37f3880d9a Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 24 Oct 2023 14:13:26 +0530 Subject: [PATCH 109/135] fix(patch): `update_sre_from_voucher_details` (#37649) --- .../v15_0/update_sre_from_voucher_details.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/erpnext/patches/v15_0/update_sre_from_voucher_details.py b/erpnext/patches/v15_0/update_sre_from_voucher_details.py index a9653ccbf4..06ba553e3a 100644 --- a/erpnext/patches/v15_0/update_sre_from_voucher_details.py +++ b/erpnext/patches/v15_0/update_sre_from_voucher_details.py @@ -3,13 +3,16 @@ from frappe.query_builder.functions import IfNull def execute(): - sre = frappe.qb.DocType("Stock Reservation Entry") - ( - frappe.qb.update(sre) - .set(sre.from_voucher_type, "Pick List") - .set(sre.from_voucher_no, sre.against_pick_list) - .set(sre.from_voucher_detail_no, sre.against_pick_list_item) - .where( - (IfNull(sre.against_pick_list, "") != "") & (IfNull(sre.against_pick_list_item, "") != "") - ) - ).run() + columns = frappe.db.get_table_columns("Stock Reservation Entry") + + if set(["against_pick_list", "against_pick_list_item"]).issubset(set(columns)): + sre = frappe.qb.DocType("Stock Reservation Entry") + ( + frappe.qb.update(sre) + .set(sre.from_voucher_type, "Pick List") + .set(sre.from_voucher_no, sre.against_pick_list) + .set(sre.from_voucher_detail_no, sre.against_pick_list_item) + .where( + (IfNull(sre.against_pick_list, "") != "") & (IfNull(sre.against_pick_list_item, "") != "") + ) + ).run() From 11d956fa18d4cfec89e2bc8c93dd43dad739f02e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 24 Oct 2023 14:28:19 +0530 Subject: [PATCH 110/135] fix: Purchase Receipt GL Entries (#37642) * fix: Purchase Receipt GL Entries * chore: cleanup * test: set cwip account --- erpnext/assets/doctype/asset/test_asset.py | 1 + erpnext/controllers/stock_controller.py | 7 ------- .../doctype/purchase_receipt/purchase_receipt.py | 14 +++++++------- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 99824b7f67..d69f5ef0b7 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1789,6 +1789,7 @@ def create_asset_category(): "fixed_asset_account": "_Test Fixed Asset - _TC", "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", "depreciation_expense_account": "_Test Depreciations - _TC", + "capital_work_in_progress_account": "CWIP Account - _TC", }, ) asset_category.append( diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index a40976b8dd..a7330ec63c 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -76,8 +76,6 @@ class StockController(AccountsController): gl_entries = self.get_gl_entries(warehouse_account) 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 @@ -1226,8 +1224,3 @@ 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/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 029d89c179..91344eaa5c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -542,17 +542,19 @@ class PurchaseReceipt(BuyingController): d, gl_entries, self.posting_date, d.get("provisional_expense_account") ) elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return): - is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) remarks = self.get("remarks") or _("Accounting Entry for {0}").format( - "Asset" if is_asset_pr else "Stock" + "Asset" if d.is_fixed_asset else "Stock" ) - if not (erpnext.is_perpetual_inventory_enabled(self.company) or is_asset_pr): - return + if not ( + (erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items) + or d.is_fixed_asset + ): + continue stock_asset_rbnb = ( self.get_company_default("asset_received_but_not_billed") - if is_asset_pr + if d.is_fixed_asset else self.get_company_default("stock_received_but_not_billed") ) landed_cost_entries = get_item_account_wise_additional_cost(self.name) @@ -758,8 +760,6 @@ class PurchaseReceipt(BuyingController): pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) - self.load_from_db() - def reserve_stock_for_sales_order(self): if self.is_return or not cint( frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase") From d92eb0c6037065d9a6f1bf9899ae0f15fba8ff0f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 24 Oct 2023 15:25:03 +0530 Subject: [PATCH 111/135] Update initiate_release.yml --- .github/workflows/initiate_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml index 70347738f2..e51c1943fd 100644 --- a/.github/workflows/initiate_release.yml +++ b/.github/workflows/initiate_release.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - version: ["13", "14"] + version: ["13", "14", "15"] steps: - uses: octokit/request-action@v2.x From 92cbe580e6fb6380a8d2c1e3420b09680826cb76 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 24 Oct 2023 15:28:55 +0530 Subject: [PATCH 112/135] fix: incorrect process loss validation for multiple finished items (#37576) --- .../stock/doctype/stock_entry/stock_entry.py | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a2cae7ff8d..35f6230cd0 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -438,31 +438,37 @@ class StockEntry(StockController): item_code.append(item.item_code) def validate_fg_completed_qty(self): - item_wise_qty = {} - if self.purpose == "Manufacture" and self.work_order: - for d in self.items: - if d.is_finished_item: - if self.process_loss_qty: - d.qty = self.fg_completed_qty - self.process_loss_qty + if self.purpose != "Manufacture": + return - item_wise_qty.setdefault(d.item_code, []).append(d.qty) + fg_qty = defaultdict(float) + for d in self.items: + if d.is_finished_item: + fg_qty[d.item_code] += flt(d.qty) + + if not fg_qty: + return precision = frappe.get_precision("Stock Entry Detail", "qty") - for item_code, qty_list in item_wise_qty.items(): - total = flt(sum(qty_list), precision) + fg_item = list(fg_qty.keys())[0] + fg_item_qty = flt(fg_qty[fg_item], precision) + fg_completed_qty = flt(self.fg_completed_qty, precision) - if (self.fg_completed_qty - total) > 0 and not self.process_loss_qty: - self.process_loss_qty = flt(self.fg_completed_qty - total, precision) - self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty) + for d in self.items: + if not fg_qty.get(d.item_code): + continue - if self.process_loss_qty: - total += flt(self.process_loss_qty, precision) + if (fg_completed_qty - fg_item_qty) > 0: + self.process_loss_qty = fg_completed_qty - fg_item_qty - if self.fg_completed_qty != total: + if not self.process_loss_qty: + continue + + if fg_completed_qty != (flt(fg_item_qty) + flt(self.process_loss_qty, precision)): frappe.throw( - _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format( - frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty) - ) + _( + "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table." + ).format(frappe.bold(self.process_loss_qty), frappe.bold(d.item_code)) ) def validate_difference_account(self): From 74a0d6408a2082a2a039cd55547e56206e7c70bd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 25 Oct 2023 09:45:05 +0530 Subject: [PATCH 113/135] refactor: handle bank transaction in foreign currency --- .../bank_reconciliation_tool.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 777e315298..7e2f763137 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -131,7 +131,7 @@ def create_journal_entry_bts( bank_transaction = frappe.db.get_values( "Bank Transaction", bank_transaction_name, - fieldname=["name", "deposit", "withdrawal", "bank_account"], + fieldname=["name", "deposit", "withdrawal", "bank_account", "currency"], as_dict=True, )[0] company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") @@ -149,10 +149,12 @@ def create_journal_entry_bts( company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency") second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency") + # determine if multi-currency Journal or not is_multi_currency = ( True if company_default_currency != company_account_currency or company_default_currency != second_account_currency + or company_default_currency != bank_transaction.currency else False ) @@ -176,11 +178,16 @@ def create_journal_entry_bts( "cost_center": get_default_cost_center(company), } + # convert transaction amount to company currency if is_multi_currency: - exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date) + exc_rate = get_exchange_rate(bank_transaction.currency, company_default_currency, posting_date) withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal)) deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit)) + else: + withdrawal_in_company_currency = bank_transaction.withdrawal + deposit_in_company_currency = bank_transaction.deposit + # if second account is of foreign currency, convert and set debit and credit fields. if second_account_currency != company_default_currency: exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date) second_account_dict.update( @@ -188,6 +195,8 @@ def create_journal_entry_bts( "exchange_rate": exc_rate, "credit": deposit_in_company_currency, "debit": withdrawal_in_company_currency, + "credit_in_account_currency": flt(deposit_in_company_currency / exc_rate) or 0, + "debit_in_account_currency": flt(withdrawal_in_company_currency / exc_rate) or 0, } ) else: @@ -201,6 +210,7 @@ def create_journal_entry_bts( } ) + # if company account is of foreign currency, convert and set debit and credit fields. if company_account_currency != company_default_currency: exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date) company_account_dict.update( @@ -210,6 +220,16 @@ def create_journal_entry_bts( "debit": deposit_in_company_currency, } ) + else: + company_account_dict.update( + { + "exchange_rate": 1, + "credit": withdrawal_in_company_currency, + "debit": deposit_in_company_currency, + "credit_in_account_currency": withdrawal_in_company_currency, + "debit_in_account_currency": deposit_in_company_currency, + } + ) accounts.append(second_account_dict) accounts.append(company_account_dict) From 7be578485e2cafd124c98cc8394c67430c2b31b8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 25 Oct 2023 12:42:35 +0530 Subject: [PATCH 114/135] fix: force delete removed report (#37668) --- erpnext/patches.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 53bddb562c..bc2497c513 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -316,7 +316,7 @@ erpnext.patches.v14_0.update_closing_balances #14-07-2023 execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts erpnext.patches.v14_0.update_subscription_details -execute:frappe.delete_doc_if_exists("Report", "Tax Detail") +execute:frappe.delete_doc("Report", "Tax Detail", force=True) erpnext.patches.v15_0.enable_all_leads erpnext.patches.v14_0.update_company_in_ldc erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes @@ -342,4 +342,4 @@ erpnext.patches.v15_0.delete_payment_gateway_doctypes erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item erpnext.patches.v15_0.update_sre_from_voucher_details # below migration patch should always run last -erpnext.patches.v14_0.migrate_gl_to_payment_ledger \ No newline at end of file +erpnext.patches.v14_0.migrate_gl_to_payment_ledger From 886102d462650243a7040177c585c734082e3734 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:46:39 +0530 Subject: [PATCH 115/135] chore: fixed test cases related to Internal Transfer (backport #37659) (#37662) * chore: fixed test cases related to Internal Transfer (#37659) (cherry picked from commit 72d32a49012329d33fd4ecea70988fbfbfce566f) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py # erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py * chore: fix conflicts * chore: fix conflicts * chore: fix test cases --------- Co-authored-by: rohitwaghchaure --- .../sales_invoice/test_sales_invoice.py | 45 +++++++ .../purchase_receipt/test_purchase_receipt.py | 113 ++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 231b3bf7fe..21cc253959 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2539,6 +2539,37 @@ class TestSalesInvoice(FrappeTestCase): frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock) + def test_sle_for_target_warehouse(self): + se = make_stock_entry( + item_code="138-CMS Shoe", + target="Finished Goods - _TC", + company="_Test Company", + qty=1, + basic_rate=500, + ) + + si = frappe.copy_doc(test_records[0]) + si.customer = "_Test Internal Customer 3" + si.update_stock = 1 + si.set_warehouse = "Finished Goods - _TC" + si.set_target_warehouse = "Stores - _TC" + si.get("items")[0].warehouse = "Finished Goods - _TC" + si.get("items")[0].target_warehouse = "Stores - _TC" + si.insert() + si.submit() + + sles = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": si.name}, fields=["name", "actual_qty"] + ) + + # check if both SLEs are created + self.assertEqual(len(sles), 2) + self.assertEqual(sum(d.actual_qty for d in sles), 0.0) + + # tear down + si.cancel() + se.cancel() + def test_internal_transfer_gl_entry(self): si = create_sales_invoice( company="_Test Company with perpetual inventory", @@ -3662,6 +3693,20 @@ def create_internal_parties(): allowed_to_interact_with="_Test Company with perpetual inventory", ) + create_internal_customer( + customer_name="_Test Internal Customer 3", + represents_company="_Test Company", + allowed_to_interact_with="_Test Company", + ) + + account = create_account( + account_name="Unrealized Profit", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) + + frappe.db.set_value("Company", "_Test Company", "unrealized_profit_loss_account", account) + create_internal_supplier( supplier_name="_Test Internal Supplier", represents_company="Wind Power LLC", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index e998b842d1..146cbff1aa 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -957,6 +957,119 @@ class TestPurchaseReceipt(FrappeTestCase): pr1.reload() pr1.cancel() + def test_stock_transfer_from_purchase_receipt(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + pr1 = make_purchase_receipt( + warehouse="Stores - TCP1", company="_Test Company with perpetual inventory" + ) + + dn1 = create_delivery_note( + item_code=pr1.items[0].item_code, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=5, + rate=500, + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", + ) + + pr = make_inter_company_purchase_receipt(dn1.name) + pr.items[0].from_warehouse = "Work In Progress - TCP1" + pr.items[0].warehouse = "Stores - TCP1" + pr.submit() + + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + sl_entries = get_sl_entries("Purchase Receipt", pr.name) + + self.assertFalse(gl_entries) + + expected_sle = {"Work In Progress - TCP1": -5, "Stores - TCP1": 5} + + for sle in sl_entries: + self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) + + pr.cancel() + + def test_stock_transfer_from_purchase_receipt_with_valuation(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + + create_warehouse( + "_Test Warehouse for Valuation", + company="_Test Company with perpetual inventory", + properties={"account": "_Test Account Stock In Hand - TCP1"}, + ) + + pr1 = make_purchase_receipt( + warehouse="Stores - TCP1", + company="_Test Company with perpetual inventory", + ) + + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + dn1 = create_delivery_note( + item_code=pr1.items[0].item_code, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=5, + rate=50, + warehouse="Stores - TCP1", + target_warehouse="_Test Warehouse for Valuation - TCP1", + ) + + pr = make_inter_company_purchase_receipt(dn1.name) + pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1" + pr.items[0].warehouse = "Stores - TCP1" + + pr.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Shipping Charges - TCP1", + "category": "Valuation and Total", + "cost_center": "Main - TCP1", + "description": "Test", + "rate": 9, + }, + ) + + pr.submit() + + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + sl_entries = get_sl_entries("Purchase Receipt", pr.name) + + expected_gle = [ + ["Stock In Hand - TCP1", 272.5, 0.0], + ["_Test Account Stock In Hand - TCP1", 0.0, 250.0], + ["_Test Account Shipping Charges - TCP1", 0.0, 22.5], + ] + + expected_sle = {"_Test Warehouse for Valuation - TCP1": -5, "Stores - TCP1": 5} + + for sle in sl_entries: + self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) + + for i, gle in enumerate(gl_entries): + self.assertEqual(gle.account, expected_gle[i][0]) + self.assertEqual(gle.debit, expected_gle[i][1]) + self.assertEqual(gle.credit, expected_gle[i][2]) + + pr.cancel() + def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: - Create PO From 5deba1b6f9b03ce5d078d624e339f6b0209a1555 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 25 Oct 2023 12:50:16 +0530 Subject: [PATCH 116/135] fix: copy all child fields to item variant --- erpnext/stock/doctype/item/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 9e281990b5..d8935fe203 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -255,7 +255,7 @@ class Item(Document): # add item taxes from template for d in template.get("taxes"): - self.append("taxes", {"item_tax_template": d.item_tax_template}) + self.append("taxes", d) # copy re-order table if empty if not self.get("reorder_levels"): From d436a407390c0e0d89c66445539bbb95784be7eb Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 25 Oct 2023 13:06:03 +0530 Subject: [PATCH 117/135] fix: only update if variant table empty --- erpnext/stock/get_item_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 8c6fd84bc4..d1999070f8 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -268,7 +268,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): if not item: item = frappe.get_doc("Item", args.get("item_code")) - if item.variant_of: + if item.variant_of and not item.taxes: item.update_template_tables() item_defaults = get_item_defaults(item.name, args.company) From 2bcff4c7f2264d2408a1f98bddc78041b167a632 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 25 Oct 2023 13:24:34 +0530 Subject: [PATCH 118/135] chore: fixed test case non_internal_transfer_delivery_note (#37671) --- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index d06819208e..1eecf6dc2a 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1230,16 +1230,16 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) - def non_internal_transfer_delivery_note(self): + def test_non_internal_transfer_delivery_note(self): from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse dn = create_delivery_note(do_not_submit=True) - warehouse = create_warehouse("Internal Transfer Warehouse", dn.company) - dn.items[0].db_set("target_warehouse", "warehouse") + warehouse = create_warehouse("Internal Transfer Warehouse", company=dn.company) + dn.items[0].db_set("target_warehouse", warehouse) dn.reload() - self.assertEqual(dn.items[0].target_warehouse, warehouse.name) + self.assertEqual(dn.items[0].target_warehouse, warehouse) dn.save() dn.reload() From 8ffa2bfe25d8fae317d77705aec82a92dc269874 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 25 Oct 2023 11:38:09 +0530 Subject: [PATCH 119/135] refactor: rename field `Over Order Allowance` to `Blanket Order Allowance` --- .../buying_settings/buying_settings.json | 18 +++++++++--------- .../doctype/blanket_order/blanket_order.py | 2 +- .../blanket_order/test_blanket_order.py | 6 +++--- .../selling_settings/selling_settings.json | 16 ++++++++-------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 71cb01b188..059999245d 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -16,7 +16,7 @@ "transaction_settings_section", "po_required", "pr_required", - "over_order_allowance", + "blanket_order_allowance", "column_break_12", "maintain_same_rate", "set_landed_cost_based_on_purchase_invoice_rate", @@ -159,19 +159,19 @@ "fieldtype": "Check", "label": "Set Landed Cost Based on Purchase Invoice Rate" }, - { - "default": "0", - "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", - "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" + }, + { + "default": "0", + "description": "Percentage you are allowed to order beyond the Blanket Order quantity.", + "fieldname": "blanket_order_allowance", + "fieldtype": "Float", + "label": "Blanket Order Allowance (%)" } ], "icon": "fa fa-cog", @@ -179,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-16 16:22:03.201078", + "modified": "2023-10-25 14:03:32.520418", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index 32f1c365ad..0135a4f971 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -107,7 +107,7 @@ def validate_against_blanket_order(order_doc): allowance = flt( frappe.db.get_single_value( "Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings", - "over_order_allowance", + "blanket_order_allowance", ) ) for bo_name, item_data in order_data.items(): diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index 58f3c95059..e9fc25b5bc 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -63,7 +63,7 @@ class TestBlanketOrder(FrappeTestCase): po1.currency = get_company_currency(po1.company) self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) - def test_over_order_allowance(self): + def test_blanket_order_allowance(self): # Sales Order bo = make_blanket_order(blanket_order_type="Selling", quantity=100) @@ -74,7 +74,7 @@ class TestBlanketOrder(FrappeTestCase): so.items[0].qty = 110 self.assertRaises(frappe.ValidationError, so.submit) - frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10) + frappe.db.set_single_value("Selling Settings", "blanket_order_allowance", 10) so.submit() # Purchase Order @@ -87,7 +87,7 @@ class TestBlanketOrder(FrappeTestCase): po.items[0].qty = 110 self.assertRaises(frappe.ValidationError, po.submit) - frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10) + frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10) po.submit() diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 6855012d5f..d6829ce24b 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -25,7 +25,7 @@ "so_required", "dn_required", "sales_update_frequency", - "over_order_allowance", + "blanket_order_allowance", "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", @@ -183,12 +183,6 @@ "fieldtype": "Check", "label": "Allow Sales Order Creation For Expired Quotation" }, - { - "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", - "fieldname": "over_order_allowance", - "fieldtype": "Float", - "label": "Over Order Allowance (%)" - }, { "default": "0", "fieldname": "dont_reserve_sales_order_qty_on_sales_return", @@ -200,6 +194,12 @@ "fieldname": "allow_negative_rates_for_items", "fieldtype": "Check", "label": "Allow Negative rates for Items" + }, + { + "description": "Percentage you are allowed to sell beyond the Blanket Order quantity.", + "fieldname": "blanket_order_allowance", + "fieldtype": "Float", + "label": "Blanket Order Allowance (%)" } ], "icon": "fa fa-cog", @@ -207,7 +207,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-14 20:33:05.693667", + "modified": "2023-10-25 14:03:03.966701", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From fcfcf6957e07ad5afc281e5f43154ca1170e14c4 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 25 Oct 2023 11:52:49 +0530 Subject: [PATCH 120/135] chore: patch to rename field `over_order_allowance` --- erpnext/patches.txt | 1 + .../v14_0/rename_over_order_allowance_field.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 erpnext/patches/v14_0/rename_over_order_allowance_field.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 53bddb562c..4c574bf33f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,5 +341,6 @@ 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 erpnext.patches.v15_0.update_sre_from_voucher_details +erpnext.patches.v14_0.rename_over_order_allowance_field # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger \ No newline at end of file diff --git a/erpnext/patches/v14_0/rename_over_order_allowance_field.py b/erpnext/patches/v14_0/rename_over_order_allowance_field.py new file mode 100644 index 0000000000..a81fe888c2 --- /dev/null +++ b/erpnext/patches/v14_0/rename_over_order_allowance_field.py @@ -0,0 +1,15 @@ +from frappe.model.utils.rename_field import rename_field + + +def execute(): + rename_field( + "Buying Settings", + "over_order_allowance", + "blanket_order_allowance", + ) + + rename_field( + "Selling Settings", + "over_order_allowance", + "blanket_order_allowance", + ) From 8e3b9ec87977b54cfdee4b65283723d20e91f274 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 25 Oct 2023 18:06:23 +0530 Subject: [PATCH 121/135] feat: allow return of components for SCO that don't have SCR created --- .../doctype/subcontracting_order/subcontracting_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index f2b395ac10..47d870447f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -107,7 +107,7 @@ frappe.ui.form.on('Subcontracting Order', { get_materials_from_supplier: function (frm) { let sco_rm_details = []; - if (frm.doc.status != "Closed" && frm.doc.supplied_items && frm.doc.per_received > 0) { + if (frm.doc.status != "Closed" && frm.doc.supplied_items) { frm.doc.supplied_items.forEach(d => { if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) { sco_rm_details.push(d.name); From 3290df5593009a4e237e7903feb078961f15ae12 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 25 Oct 2023 18:28:30 +0530 Subject: [PATCH 122/135] fix: consider returned qty while calculating unsupplied qty --- .../stock/doctype/stock_entry/stock_entry.py | 28 ++++++++++++++++--- .../subcontracting_order.js | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 35f6230cd0..c41349fcfb 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1020,14 +1020,34 @@ class StockEntry(StockController): & (se.docstatus == 1) & (se_detail.item_code == se_item.item_code) & ( - (se.purchase_order == self.purchase_order) + ((se.purchase_order == self.purchase_order) & (se_detail.po_detail == se_item.po_detail)) if self.subcontract_data.order_doctype == "Purchase Order" - else (se.subcontracting_order == self.subcontracting_order) + else ( + (se.subcontracting_order == self.subcontracting_order) + & (se_detail.sco_rm_detail == se_item.sco_rm_detail) + ) ) ) - ).run()[0][0] + ).run()[0][0] or 0 - if flt(total_supplied, precision) > flt(total_allowed, precision): + total_returned = 0 + if self.subcontract_data.order_doctype == "Subcontracting Order": + total_returned = ( + frappe.qb.from_(se) + .inner_join(se_detail) + .on(se.name == se_detail.parent) + .select(Sum(se_detail.transfer_qty)) + .where( + (se.purpose == "Material Transfer") + & (se.docstatus == 1) + & (se.is_return == 1) + & (se_detail.item_code == se_item.item_code) + & (se_detail.sco_rm_detail == se_item.sco_rm_detail) + & (se.subcontracting_order == self.subcontracting_order) + ) + ).run()[0][0] or 0 + + if flt(total_supplied - total_returned, precision) > flt(total_allowed, precision): frappe.throw( _("Row {0}# Item {1} cannot be transferred more than {2} against {3} {4}").format( se_item.idx, diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index 47d870447f..587a3b4ebf 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -193,7 +193,7 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll } has_unsupplied_items() { - return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); + return this.frm.doc['supplied_items'].some(item => item.required_qty > (item.supplied_qty - item.returned_qty)); } make_subcontracting_receipt() { From 46ea8685590e81266beb1cccbe72925f28bc42ba Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Oct 2023 22:58:24 +0530 Subject: [PATCH 123/135] fix(plaid): Do not sync pending transactions --- .../doctype/bank_transaction/bank_transaction.py | 1 - .../doctype/plaid_settings/plaid_settings.py | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 6a47562412..4649d23162 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -89,7 +89,6 @@ class BankTransaction(StatusUpdater): - 0 > a: Error: already over-allocated - clear means: set the latest transaction date as clearance date """ - gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account") remaining_amount = self.unallocated_amount for payment_entry in self.payment_entries: if payment_entry.allocated_amount == 0.0: diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 11d5f6a9c4..eb99345991 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.desk.doctype.tag.tag import add_tag from frappe.model.document import Document -from frappe.utils import add_months, formatdate, getdate, today +from frappe.utils import add_months, formatdate, getdate, sbool, today from plaid.errors import ItemError from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account @@ -237,8 +237,6 @@ def new_bank_transaction(transaction): deposit = abs(amount) withdrawal = 0.0 - status = "Pending" if transaction["pending"] == True else "Settled" - tags = [] if transaction["category"]: try: @@ -247,13 +245,14 @@ def new_bank_transaction(transaction): except KeyError: pass - if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])): + if not frappe.db.exists( + "Bank Transaction", dict(transaction_id=transaction["transaction_id"]) + ) and not sbool(transaction["pending"]): try: new_transaction = frappe.get_doc( { "doctype": "Bank Transaction", "date": getdate(transaction["date"]), - "status": status, "bank_account": bank_account, "deposit": deposit, "withdrawal": withdrawal, From 681782121cd0d723f725e4c1c4c30167eb1622ec Mon Sep 17 00:00:00 2001 From: David Arnold Date: Thu, 26 Oct 2023 13:46:50 +0200 Subject: [PATCH 124/135] fix: avoid name clash in delivery stop (#37306) * fix(stock): avoid name clash in delivery stop with Document.lock() * chore(stock): format delivery stop json according to doctype builder --- erpnext/patches.txt | 1 + .../v14_0/migrate_delivery_stop_lock_field.py | 7 + .../doctype/delivery_stop/delivery_stop.json | 956 ++++-------------- .../doctype/delivery_trip/delivery_trip.py | 2 +- .../delivery_trip/test_delivery_trip.py | 4 +- 5 files changed, 180 insertions(+), 790 deletions(-) create mode 100644 erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b05ec4e7bf..d7f33adeea 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -342,5 +342,6 @@ erpnext.patches.v15_0.delete_payment_gateway_doctypes erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item erpnext.patches.v15_0.update_sre_from_voucher_details erpnext.patches.v14_0.rename_over_order_allowance_field +erpnext.patches.v14_0.migrate_delivery_stop_lock_field # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py b/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py new file mode 100644 index 0000000000..c9ec1e113d --- /dev/null +++ b/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py @@ -0,0 +1,7 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + if frappe.db.has_column("Delivery Stop", "lock"): + rename_field("Delivery Stop", "lock", "locked") diff --git a/erpnext/stock/doctype/delivery_stop/delivery_stop.json b/erpnext/stock/doctype/delivery_stop/delivery_stop.json index 5610a8108a..42560e612e 100644 --- a/erpnext/stock/doctype/delivery_stop/delivery_stop.json +++ b/erpnext/stock/doctype/delivery_stop/delivery_stop.json @@ -1,815 +1,197 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-10-16 16:46:28.166950", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-10-16 16:46:28.166950", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer", + "address", + "locked", + "column_break_6", + "customer_address", + "visited", + "order_information_section", + "delivery_note", + "cb_order", + "grand_total", + "section_break_7", + "contact", + "email_sent_to", + "column_break_7", + "customer_contact", + "section_break_9", + "distance", + "estimated_arrival", + "lat", + "column_break_19", + "uom", + "lng", + "more_information_section", + "details" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Customer", - "length": 0, - "no_copy": 0, - "options": "Customer", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Address Name", - "length": 0, - "no_copy": 0, - "options": "Address", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Address Name", + "options": "Address", + "print_hide": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lock", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Lock", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "locked", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Locked" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_address", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer_address", + "fieldtype": "Small Text", + "label": "Customer Address", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.docstatus==1", - "fieldname": "visited", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Visited", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "default": "0", + "depends_on": "eval:doc.docstatus==1", + "fieldname": "visited", + "fieldtype": "Check", + "label": "Visited", + "no_copy": 1, + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "order_information_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Order Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "order_information_section", + "fieldtype": "Section Break", + "label": "Order Information" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "delivery_note", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Delivery Note", - "length": 0, - "no_copy": 1, - "options": "Delivery Note", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "delivery_note", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Delivery Note", + "no_copy": 1, + "options": "Delivery Note", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cb_order", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "cb_order", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "grand_total", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Grand Total", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "grand_total", + "fieldtype": "Currency", + "label": "Grand Total", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "label": "Contact Information" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact Name", - "length": 0, - "no_copy": 0, - "options": "Contact", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "contact", + "fieldtype": "Link", + "label": "Contact Name", + "options": "Contact", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "email_sent_to", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Email sent to", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "email_sent_to", + "fieldtype": "Data", + "label": "Email sent to", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_contact", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Contact", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer_contact", + "fieldtype": "Small Text", + "label": "Customer Contact", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Dispatch Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Dispatch Information" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "distance", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Distance", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "distance", + "fieldtype": "Float", + "label": "Distance", + "precision": "2", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "estimated_arrival", - "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Estimated Arrival", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "estimated_arrival", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Estimated Arrival" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lat", - "fieldtype": "Float", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Latitude", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "lat", + "fieldtype": "Float", + "hidden": 1, + "label": "Latitude" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_19", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "depends_on": "eval:doc.distance", - "fieldname": "uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.distance", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lng", - "fieldtype": "Float", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Longitude", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "lng", + "fieldtype": "Float", + "hidden": 1, + "label": "Longitude" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "more_information_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "More Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "details", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "details", + "fieldtype": "Text Editor", + "label": "Details" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-10-16 05:23:25.661542", - "modified_by": "Administrator", - "module": "Stock", - "name": "Delivery Stop", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-09-29 09:22:53.435161", + "modified_by": "Administrator", + "module": "Stock", + "name": "Delivery Stop", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index af2f4113e1..c531a8769c 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -170,7 +170,7 @@ class DeliveryTrip(Document): for stop in self.delivery_stops: leg.append(stop.customer_address) - if optimize and stop.lock: + if optimize and stop.locked: route_list.append(leg) leg = [stop.customer_address] diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py index ed699e37b8..9b8b46e6e0 100644 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py @@ -46,7 +46,7 @@ class TestDeliveryTrip(FrappeTestCase): self.assertEqual(len(route_list[0]), 4) def test_unoptimized_route_list_with_locks(self): - self.delivery_trip.delivery_stops[0].lock = 1 + self.delivery_trip.delivery_stops[0].locked = 1 self.delivery_trip.save() route_list = self.delivery_trip.form_route_list(optimize=False) @@ -65,7 +65,7 @@ class TestDeliveryTrip(FrappeTestCase): self.assertEqual(len(route_list[0]), 4) def test_optimized_route_list_with_locks(self): - self.delivery_trip.delivery_stops[0].lock = 1 + self.delivery_trip.delivery_stops[0].locked = 1 self.delivery_trip.save() route_list = self.delivery_trip.form_route_list(optimize=True) From 1612d7ba3f353e23ad6ae9ba12b995106cddcb9e Mon Sep 17 00:00:00 2001 From: David Arnold Date: Thu, 26 Oct 2023 14:03:22 +0200 Subject: [PATCH 125/135] fix(defaults): apply discount and provisonal defaults from item group and brand if available (#37466) --- erpnext/stock/get_item_details.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 8c6fd84bc4..a8eb777342 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -330,8 +330,12 @@ def get_basic_details(args, item, overwrite_warehouse=True): ), "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), - "discount_account": get_default_discount_account(args, item_defaults), - "provisional_expense_account": get_provisional_account(args, item_defaults), + "discount_account": get_default_discount_account( + args, item_defaults, item_group_defaults, brand_defaults + ), + "provisional_expense_account": get_provisional_account( + args, item_defaults, item_group_defaults, brand_defaults + ), "cost_center": get_default_cost_center( args, item_defaults, item_group_defaults, brand_defaults ), @@ -686,12 +690,22 @@ def get_default_expense_account(args, item, item_group, brand): ) -def get_provisional_account(args, item): - return item.get("default_provisional_account") or args.default_provisional_account +def get_provisional_account(args, item, item_group, brand): + return ( + item.get("default_provisional_account") + or item_group.get("default_provisional_account") + or brand.get("default_provisional_account") + or args.default_provisional_account + ) -def get_default_discount_account(args, item): - return item.get("default_discount_account") or args.discount_account +def get_default_discount_account(args, item, item_group, brand): + return ( + item.get("default_discount_account") + or item_group.get("default_discount_account") + or brand.get("default_discount_account") + or args.discount_account + ) def get_default_deferred_account(args, item, fieldname=None): From dc5d2c740611c687b64066246272428e8f7a4962 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 26 Oct 2023 20:36:52 +0530 Subject: [PATCH 126/135] fix: typerror on TDS payable monthly report --- .../report/tax_withholding_details/tax_withholding_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f2ec31c70e..eac5426b8b 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,7 @@ def get_result( tax_amount += entry.credit - entry.debit if net_total_map.get(name): - if voucher_type == "Journal Entry": + if voucher_type == "Journal Entry" and tax_amount and rate: # back calcalute total amount from rate and tax_amount total_amount = grand_total = base_total = tax_amount / (rate / 100) else: From 8d9b90f3f53083e631444a0d39d86bda0b08f479 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 27 Oct 2023 12:37:37 +0530 Subject: [PATCH 127/135] refactor: ignore cancelled GLE's while looking for currency --- erpnext/accounts/party.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 310e41208f..16e73ea52f 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -5,7 +5,7 @@ from typing import Optional import frappe -from frappe import _, msgprint, scrub +from frappe import _, msgprint, qb, scrub from frappe.contacts.doctype.address.address import get_company_address, get_default_address from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values @@ -480,11 +480,19 @@ def get_party_account_currency(party_type, party, company): def get_party_gle_currency(party_type, party, company): def generator(): - existing_gle_currency = frappe.db.sql( - """select account_currency from `tabGL Entry` - where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s - limit 1""", - {"company": company, "party_type": party_type, "party": party}, + gl = qb.DocType("GL Entry") + existing_gle_currency = ( + qb.from_(gl) + .select(gl.account_currency) + .where( + (gl.docstatus == 1) + & (gl.company == company) + & (gl.party_type == party_type) + & (gl.party == party) + & (gl.is_cancelled == 0) + ) + .limit(1) + .run() ) return existing_gle_currency[0][0] if existing_gle_currency else None From 48c66b68abe5573c477cbbdc49af22dbde1ea2db Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 27 Oct 2023 17:07:16 +0530 Subject: [PATCH 128/135] fix: typo in function name and msg --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 4 ++-- .../stock_reservation_entry/stock_reservation_entry.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 96e4a55630..3ce121f31f 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -658,7 +658,7 @@ class SerialandBatchBundle(Document): if not available_batches: return - available_batches = get_availabel_batches_qty(available_batches) + available_batches = get_available_batches_qty(available_batches) for batch_no in batches: if batch_no not in available_batches or available_batches[batch_no] < 0: self.throw_error_message( @@ -1074,7 +1074,7 @@ def get_auto_data(**kwargs): return get_auto_batch_nos(kwargs) -def get_availabel_batches_qty(available_batches): +def get_available_batches_qty(available_batches): available_batches_qty = defaultdict(float) for batch in available_batches: available_batches_qty[batch.batch_no] += batch.qty diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 81e9dfa69b..6b39965f9b 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -241,7 +241,7 @@ class StockReservationEntry(Document): if available_qty_to_reserve <= 0: msg = _( - "Row #{0}: Stock not availabe to reserve for Item {1} against Batch {2} in Warehouse {3}." + "Row #{0}: Stock not available to reserve for Item {1} against Batch {2} in Warehouse {3}." ).format( entry.idx, frappe.bold(self.item_code), From d99a56bc2787975016b23602fd42b0f595594ad1 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Fri, 27 Oct 2023 18:54:36 +0530 Subject: [PATCH 129/135] chore: allow wip_composite_asset in the MR PO PR PI flow (#37723) --- .../purchase_invoice/purchase_invoice.py | 1 + erpnext/assets/doctype/asset/asset.json | 16 +++++++++--- .../asset_capitalization.py | 6 +---- .../doctype/purchase_order/purchase_order.py | 2 ++ .../purchase_order_item.json | 16 ++++++++++-- .../material_request/material_request.py | 1 + .../material_request_item.json | 26 ++++++++++++++++--- .../purchase_receipt/purchase_receipt.py | 1 + 8 files changed, 54 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 97ee5cc93b..c398d14183 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1664,6 +1664,7 @@ def make_purchase_receipt(source_name, target_doc=None): "po_detail": "purchase_order_item", "material_request": "material_request", "material_request_item": "material_request_item", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index c7d08e2041..40f51ab570 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -221,11 +221,11 @@ "read_only": 1 }, { + "depends_on": "eval:!(doc.is_composite_asset && !doc.capitalized_in)", "fieldname": "gross_purchase_amount", "fieldtype": "Currency", "label": "Gross Purchase Amount", "options": "Company:company:default_currency", - "read_only": 1, "read_only_depends_on": "eval:!doc.is_existing_asset", "reqd": 1 }, @@ -399,6 +399,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset", "fieldname": "purchase_receipt", "fieldtype": "Link", "label": "Purchase Receipt", @@ -416,6 +417,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset", "fieldname": "purchase_invoice", "fieldtype": "Link", "label": "Purchase Invoice", @@ -479,10 +481,11 @@ "read_only": 1 }, { + "depends_on": "eval.doc.asset_quantity", "fieldname": "asset_quantity", "fieldtype": "Int", "label": "Asset Quantity", - "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" + "read_only": 1 }, { "fieldname": "depr_entry_posting_status", @@ -562,9 +565,14 @@ "link_doctype": "Journal Entry", "link_fieldname": "reference_name", "table_fieldname": "accounts" + }, + { + "group": "Asset Capitalization", + "link_doctype": "Asset Capitalization", + "link_fieldname": "target_asset" } ], - "modified": "2023-10-03 23:28:26.732269", + "modified": "2023-10-27 17:03:46.629617", "modified_by": "Administrator", "module": "Assets", "name": "Asset", @@ -608,4 +616,4 @@ "states": [], "title_field": "asset_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 0d6f6b4da1..728764be72 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -876,12 +876,8 @@ def get_items_tagged_to_wip_composite_asset(asset): "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 + return pr_items diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 7c40aafbe0..961697c0ac 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -558,6 +558,7 @@ def make_purchase_receipt(source_name, target_doc=None): "material_request_item": "material_request_item", "sales_order": "sales_order", "sales_order_item": "sales_order_item", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) @@ -634,6 +635,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "field_map": { "name": "po_detail", "parent": "purchase_order", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), 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 6b29984491..b1da97d634 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -86,6 +86,8 @@ "billed_amt", "accounting_details", "expense_account", + "column_break_fyqr", + "wip_composite_asset", "manufacture_details", "manufacturer", "manufacturer_part_no", @@ -896,13 +898,23 @@ "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply TDS" + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" + }, + { + "fieldname": "column_break_fyqr", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-09-13 16:22:40.825092", + "modified": "2023-10-27 15:50:42.655573", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", @@ -915,4 +927,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index a51028da19..ecdec800e5 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -401,6 +401,7 @@ def make_purchase_order(source_name, target_doc=None, args=None): ["uom", "uom"], ["sales_order", "sales_order"], ["sales_order_item", "sales_order_item"], + ["wip_composite_asset", "wip_composite_asset"], ], "postprocess": update_item, "condition": select_item, diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index c585d6c490..9912be145f 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -37,6 +37,10 @@ "rate", "col_break3", "amount", + "accounting_details_section", + "expense_account", + "column_break_glru", + "wip_composite_asset", "manufacture_details", "manufacturer", "manufacturer_part_no", @@ -50,11 +54,10 @@ "lead_time_date", "sales_order", "sales_order_item", + "col_break4", "production_plan", "material_request_plan_item", "job_card_item", - "col_break4", - "expense_account", "section_break_46", "page_break" ], @@ -454,13 +457,28 @@ "label": "Job Card Item", "no_copy": 1, "print_hide": 1 + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "column_break_glru", + "fieldtype": "Column Break" + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-07 20:23:31.250252", + "modified": "2023-10-27 15:53:41.444236", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", @@ -471,4 +489,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 91344eaa5c..2a4b6f34b5 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1069,6 +1069,7 @@ def make_purchase_invoice(source_name, target_doc=None): "is_fixed_asset": "is_fixed_asset", "asset_location": "asset_location", "asset_category": "asset_category", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "filter": lambda d: get_pending_qty(d)[0] <= 0 From fd78f868e1aed2bdb3baa47927f37b239e16a174 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 27 Oct 2023 23:33:04 +0530 Subject: [PATCH 130/135] fix: unsupported operand type(s) for serial and batch bundle in POS Invoice (#37721) --- .../doctype/pos_invoice/test_pos_invoice.py | 36 ++++++++++++++++--- .../serial_and_batch_bundle.py | 9 +++-- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 887f1eaeb1..982bdc198a 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -771,19 +771,28 @@ class TestPOSInvoice(unittest.TestCase): ) create_batch_item_with_batch("_BATCH ITEM Test For Reserve", "TestBatch-RS 02") - make_stock_entry( + se = make_stock_entry( target="_Test Warehouse - _TC", item_code="_BATCH ITEM Test For Reserve", - qty=20, + qty=30, basic_rate=100, - batch_no="TestBatch-RS 02", ) + se.reload() + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + # POS Invoice 1, for the batch without bundle pos_inv1 = create_pos_invoice( - item="_BATCH ITEM Test For Reserve", rate=300, qty=15, batch_no="TestBatch-RS 02" + item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1 ) + + pos_inv1.items[0].batch_no = batch_no pos_inv1.save() pos_inv1.submit() + pos_inv1.reload() + + self.assertFalse(pos_inv1.items[0].serial_and_batch_bundle) batches = get_auto_batch_nos( frappe._dict( @@ -792,7 +801,24 @@ class TestPOSInvoice(unittest.TestCase): ) for batch in batches: - if batch.batch_no == "TestBatch-RS 02" and batch.warehouse == "_Test Warehouse - _TC": + if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC": + self.assertEqual(batch.qty, 15) + + # POS Invoice 2, for the batch with bundle + pos_inv2 = create_pos_invoice( + item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no + ) + pos_inv2.reload() + self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle) + + batches = get_auto_batch_nos( + frappe._dict( + {"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"} + ) + ) + + for batch in batches: + if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC": self.assertEqual(batch.qty, 5) def test_pos_batch_item_qty_validation(self): diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 96e4a55630..8142ba5927 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1301,6 +1301,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict: "POS Invoice", fields=[ "`tabPOS Invoice Item`.batch_no", + "`tabPOS Invoice Item`.qty", "`tabPOS Invoice`.is_return", "`tabPOS Invoice Item`.warehouse", "`tabPOS Invoice Item`.name as child_docname", @@ -1321,9 +1322,6 @@ def get_reserved_batches_for_pos(kwargs) -> dict: if pos_invoice.serial_and_batch_bundle ] - if not ids: - return {} - if ids: for d in get_serial_batch_ledgers(kwargs.item_code, docstatus=1, name=ids): key = (d.batch_no, d.warehouse) @@ -1337,6 +1335,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict: else: pos_batches[key].qty += d.qty + # POS invoices having batch without bundle (to handle old POS invoices) for row in pos_invoices: if not row.batch_no: continue @@ -1346,11 +1345,11 @@ def get_reserved_batches_for_pos(kwargs) -> dict: key = (row.batch_no, row.warehouse) if key in pos_batches: - pos_batches[key] -= row.qty * -1 if row.is_return else row.qty + pos_batches[key]["qty"] -= row.qty * -1 if row.is_return else row.qty else: pos_batches[key] = frappe._dict( { - "qty": (row.qty * -1 if row.is_return else row.qty), + "qty": (row.qty * -1 if not row.is_return else row.qty), "warehouse": row.warehouse, } ) From f276fbba4f84979e12b8091492be7eddbf0caa56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Oliver=20S=C3=BCnderhauf?= <46800703+bosue@users.noreply.github.com> Date: Sat, 28 Oct 2023 02:10:28 +0200 Subject: [PATCH 131/135] refactor: remove extraneous disabled filters --- .../profitability_analysis/profitability_analysis.js | 7 ------- erpnext/assets/doctype/asset/asset.js | 1 - .../supplier_quotation_comparison.js | 5 ----- .../report/bom_operations_time/bom_operations_time.js | 2 +- erpnext/public/js/controllers/accounts.js | 1 - erpnext/stock/doctype/item_price/item_price.js | 1 - .../doctype/stock_reconciliation/stock_reconciliation.js | 7 ------- erpnext/support/doctype/issue/issue.js | 7 ------- 8 files changed, 1 insertion(+), 30 deletions(-) diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js index 4a3d9bb479..b6bbd979ed 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -32,13 +32,6 @@ frappe.query_reports["Profitability Analysis"] = { "label": __("Accounting Dimension"), "fieldtype": "Link", "options": "Accounting Dimension", - "get_query": () =>{ - return { - filters: { - "disabled": 0 - } - } - } }, { "fieldname": "fiscal_year", diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f0e4c82048..d378fbd26a 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -9,7 +9,6 @@ frappe.ui.form.on('Asset', { frm.set_query("item_code", function() { return { "filters": { - "disabled": 0, "is_fixed_asset": 1, "is_stock_item": 0 } diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index fd73b870c5..579c0a65ad 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -44,11 +44,6 @@ frappe.query_reports["Supplier Quotation Comparison"] = { } } } - else { - return { - filters: { "disabled": 0 } - } - } } }, { diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js index 34edb9d538..8729775dc2 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js @@ -12,7 +12,7 @@ frappe.query_reports["BOM Operations Time"] = { "options": "Item", "get_query": () =>{ return { - filters: { "disabled": 0, "is_stock_item": 1 } + filters: { "is_stock_item": 1 } } } }, diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 354552137b..7879173cd1 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -30,7 +30,6 @@ erpnext.accounts.taxes = { filters: { "account_type": account_type, "company": doc.company, - "disabled": 0 } } }); diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js index ce489ff52b..8a4b4eef0a 100644 --- a/erpnext/stock/doctype/item_price/item_price.js +++ b/erpnext/stock/doctype/item_price/item_price.js @@ -6,7 +6,6 @@ frappe.ui.form.on("Item Price", { frm.set_query("item_code", function() { return { filters: { - "disabled": 0, "has_variants": 0 } }; diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 5452692a24..b3998b7c7e 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -123,13 +123,6 @@ frappe.ui.form.on("Stock Reconciliation", { fieldname: "item_code", fieldtype: "Link", options: "Item", - "get_query": function() { - return { - "filters": { - "disabled": 0, - } - }; - } }, { label: __("Ignore Empty Stock"), diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index d4daacd4ea..f96823b290 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -1,13 +1,6 @@ frappe.ui.form.on("Issue", { onload: function(frm) { frm.email_field = "raised_by"; - frm.set_query("customer", function () { - return { - filters: { - "disabled": 0 - } - }; - }); frappe.db.get_value("Support Settings", {name: "Support Settings"}, ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { From 3a8736374cfd73d828cd98936900cef1ec78fdb7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 29 Oct 2023 10:18:47 +0530 Subject: [PATCH 132/135] fix: fetch asset received but not billed account only when needed --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c398d14183..e1f0f1932e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -585,7 +585,6 @@ class PurchaseInvoice(BuyingController): def get_gl_entries(self, warehouse_account=None): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) - self.asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") if self.auto_accounting_for_stock: self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") @@ -937,10 +936,11 @@ class PurchaseInvoice(BuyingController): ) stock_rbnb = ( - self.asset_received_but_not_billed + self.get_company_default("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( From 500435b856a028bdab7fdbe12647ec0f11287eab Mon Sep 17 00:00:00 2001 From: Didiman1998 <118364772+Didiman1998@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:48:41 +0100 Subject: [PATCH 133/135] fix: make changes that enable gantt view for job cards (#37661) * fix: make changes that enable gantt view for job cards * fix: add fields on listview and remove from json file * fix: undo modified date --------- Co-authored-by: Dietmar Fischer --- erpnext/manufacturing/doctype/job_card/job_card_calendar.js | 4 ++-- erpnext/manufacturing/doctype/job_card/job_card_list.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js index f4877fdca0..9e32085351 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js @@ -10,8 +10,8 @@ frappe.views.calendar["Job Card"] = { }, gantt: { field_map: { - "start": "started_time", - "end": "started_time", + "start": "expected_start_date", + "end": "expected_end_date", "id": "name", "title": "subject", "color": "color", diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js index 5d883bf9fa..99fca9570f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_list.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js @@ -1,6 +1,6 @@ frappe.listview_settings['Job Card'] = { has_indicator_for_draft: true, - + add_fields: ["expected_start_date", "expected_end_date"], get_indicator: function(doc) { const status_colors = { "Work In Progress": "orange", From afc64ed9eedb1aca2802f5d5e53cc5668359f575 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 30 Oct 2023 15:39:11 +0530 Subject: [PATCH 134/135] fix: ignore permissions while mapping DN Item --- erpnext/selling/doctype/sales_order/sales_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 94f9d6e37c..2f6578ea16 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -831,6 +831,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): "postprocess": update_dn_item, } }, + ignore_permissions=True, ) dn_item.qty = flt(sre.reserved_qty) * flt(dn_item.get("conversion_factor", 1)) From ca698452382eba85bd940dfb6344ff19d1eab4e4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 30 Oct 2023 16:15:05 +0530 Subject: [PATCH 135/135] chore: add index to posting_date in PLE --- .../doctype/payment_ledger_entry/payment_ledger_entry.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index 9cf2ac6c2a..4ae813571a 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -30,7 +30,8 @@ { "fieldname": "posting_date", "fieldtype": "Date", - "label": "Posting Date" + "label": "Posting Date", + "search_index": 1 }, { "fieldname": "account_type", @@ -153,7 +154,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-29 12:24:20.500632", + "modified": "2023-10-30 16:15:00.470283", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Ledger Entry",