diff --git a/webshop/hooks.py b/webshop/hooks.py index ed854d07cd..4e1cb67cc0 100644 --- a/webshop/hooks.py +++ b/webshop/hooks.py @@ -12,7 +12,7 @@ web_include_css = "webshop-web.bundle.css" -web_include_js = "web.bundle.js" +web_include_js =["web.bundle.js", "assets/webshop/js/momo_checkout_override.js"] after_install = "webshop.setup.install.after_install" on_logout = "webshop.webshop.shopping_cart.utils.clear_cart_count" diff --git a/webshop/public/js/momo_checkout_override.js b/webshop/public/js/momo_checkout_override.js new file mode 100644 index 0000000000..a47a103306 --- /dev/null +++ b/webshop/public/js/momo_checkout_override.js @@ -0,0 +1,34 @@ +// Override place_order to handle MoMo payment redirect +frappe.ready(function() { + // Hook into the checkout form submit + // The webshop uses a Shopping Cart controller — we patch after DOM ready + $(document).on("click", ".btn-place-order, [data-action='place_order'], #btn-place-order", function(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + + var btn = $(this); + btn.prop("disabled", true).text(__("Processing...")); + + frappe.call({ + method: "webshop.webshop.shopping_cart.cart.place_order", + callback: function(r) { + if (r.message && r.message.payment_url) { + // MoMo flow: redirect to MoMo checkout page + window.location.href = r.message.payment_url; + } else if (r.message && r.message.sales_order) { + // Fallback: go to order page + window.location.href = "/orders?name=" + r.message.sales_order; + } else if (typeof r.message === "string") { + // Original webshop response (plain SO name) + window.location.href = "/orders?name=" + r.message; + } else { + btn.prop("disabled", false).text(__("Place Order")); + frappe.msgprint(__("Something went wrong. Please try again.")); + } + }, + error: function() { + btn.prop("disabled", false).text(__("Place Order")); + } + }); + }); +}); diff --git a/webshop/templates/pages/cart.js b/webshop/templates/pages/cart.js index dd03f3d1c7..5022feab06 100644 --- a/webshop/templates/pages/cart.js +++ b/webshop/templates/pages/cart.js @@ -6,244 +6,238 @@ frappe.provide("webshop.webshop.shopping_cart"); var shopping_cart = webshop.webshop.shopping_cart; $.extend(shopping_cart, { - show_error: function(title, text) { - $("#cart-container").html('

' + - title + '

' + text + '

'); - }, - - bind_events: function() { - 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(); - shopping_cart.bind_remove_coupon_code(); - }, - - 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: "webshop.webshop.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: "webshop.webshop.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: "webshop.webshop.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: "webshop.webshop.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(); - } - } - }); - }, - - bind_remove_coupon_code: function() { - $(".bt-remove-coupon-code").on("click", function() { - shopping_cart.remove_coupon_code(this); - }); - }, - remove_coupon_code: function(btn) { - return frappe.call({ - type: "POST", - method: "webshop.webshop.shopping_cart.cart.remove_coupon_code", - btn: btn, - callback: function(r) { - if (r && r.message){ - location.reload(); - } - } - }); - }, + show_error: function(title, text) { + $("#cart-container").html('

' + + title + '

' + text + '

'); + }, + + bind_events: function() { + 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(); + shopping_cart.bind_remove_coupon_code(); + }, + + 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: "webshop.webshop.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) { + frappe.call({ + method: "webshop.webshop.shopping_cart.cart.place_order", + btn: btn, + callback: function(r) { + if (r.message && r.message.payment_url) { + window.location.href = r.message.payment_url; + } else if (typeof r.message === "string") { + // fallback for non-MoMo + window.location.href = "/orders?name=" + r.message; + } + } + }); + }, + + request_quotation: function(btn) { + shopping_cart.freeze(); + + return frappe.call({ + type: "POST", + method: "webshop.webshop.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: "webshop.webshop.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(); + } + } + }); + }, + + bind_remove_coupon_code: function() { + $(".bt-remove-coupon-code").on("click", function() { + shopping_cart.remove_coupon_code(this); + }); + }, + + remove_coupon_code: function(btn) { + return frappe.call({ + type: "POST", + method: "webshop.webshop.shopping_cart.cart.remove_coupon_code", + btn: btn, + 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(); + 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); + var html = $(".cart-terms").html(); + frappe.msgprint(html); } diff --git a/webshop/webshop/shopping_cart/cart.py b/webshop/webshop/shopping_cart/cart.py index f4ed155e42..220c0d661e 100644 --- a/webshop/webshop/shopping_cart/cart.py +++ b/webshop/webshop/shopping_cart/cart.py @@ -1,20 +1,25 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +import json +import uuid +from urllib.parse import urlencode + 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 import cint, cstr, flt, get_fullname, get_url from frappe.utils.nestedset import get_root_of +from payments.utils import get_payment_gateway_controller from erpnext.accounts.utils import get_account_name +from erpnext.selling.doctype.quotation.quotation import _make_sales_order from webshop.webshop.doctype.webshop_settings.webshop_settings import ( get_shopping_cart_settings, ) from webshop.webshop.utils.product import get_web_item_qty_in_stock -from erpnext.selling.doctype.quotation.quotation import _make_sales_order class WebsitePriceListMissingError(frappe.ValidationError): @@ -22,633 +27,703 @@ class WebsitePriceListMissingError(frappe.ValidationError): def set_cart_count(quotation=None): - if cint(frappe.db.get_singles_value("Webshop Settings", "enabled")): - if not quotation: - quotation = _get_cart_quotation() - cart_count = cstr(cint(quotation.get("total_qty"))) + if cint(frappe.db.get_singles_value("Webshop 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) + 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() + party = get_party() - if not doc: - quotation = _get_cart_quotation(party) - doc = quotation - set_cart_count(quotation) + if not doc: + quotation = _get_cart_quotation(party) + doc = quotation + set_cart_count(quotation) - addresses = get_address_docs(party=party) + addresses = get_address_docs(party=party) - if not doc.customer_address and addresses: - update_cart_address("billing", addresses[0].name) + 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("Webshop Settings"), - } + 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("Webshop 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" - ] + 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" - ] + 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.get_cached_doc("Webshop Settings") - 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")) - - 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.logger("momo").error("MOMO_DEBUG: place_order called") + quotation = _get_cart_quotation() + cart_settings = frappe.get_cached_doc("Webshop Settings") + + if not (quotation.shipping_address_name or quotation.customer_address): + frappe.throw(_("Set Shipping Address or Billing Address")) + + # Snapshot the quotation data into a cache token + token = str(uuid.uuid4()) + + # Reject if this quotation already has a completed SO + existing_so = frappe.db.get_value( + "Sales Order", {"po_no": quotation.name, "docstatus": 1}, "name" + ) + if existing_so: + has_pe = frappe.db.exists( + "Payment Entry Reference", + { + "reference_doctype": "Sales Order", + "reference_name": existing_so, + }, + ) + if has_pe: + frappe.throw( + _( + "This cart has already been paid. Please refresh the page to start a new order." + ), + title=_("Already Paid"), + ) + + cart_data = { + "quotation_name": quotation.name, + "customer": quotation.party_name, + "company": cart_settings.company, + "currency": quotation.currency, + "grand_total": quotation.grand_total, + "items": [ + { + "item_code": d.item_code, + "qty": d.qty, + "rate": d.rate, + "warehouse": d.warehouse, + "uom": d.uom, + } + for d in quotation.get("items") + ], + "taxes": [ + { + "charge_type": t.charge_type, + "account_head": t.account_head, + "description": t.description, + "rate": t.rate, + } + for t in quotation.get("taxes", []) + ], + "shipping_address_name": quotation.shipping_address_name, + "customer_address": quotation.customer_address, + "contact_email": quotation.contact_email, + "customer_name": quotation.customer_name, + } + + # Store in Frappe cache for 30 minutes + frappe.cache().set_value( + f"momo_pending_{token}", json.dumps(cart_data), expires_in_sec=1800 + ) + + # Get gateway + momo_gateway = get_default_momo_gateway() + controller = get_payment_gateway_controller("MoMo-" + momo_gateway) + + params = urlencode( + { + "token": token, + "amount": quotation.grand_total, + "currency": quotation.currency, + "gateway_name": momo_gateway, + "title": f"Order from {quotation.customer_name}", + } + ) + payment_url = get_url(f"/momo_checkout?{params}") + + return {"payment_url": payment_url} @frappe.whitelist() def request_for_quotation(): - quotation = _get_cart_quotation() - quotation.flags.ignore_permissions = True + quotation = _get_cart_quotation() + quotation.flags.ignore_permissions = True - if get_shopping_cart_settings().save_quotations_as_draft: - quotation.save() - else: - quotation.submit() + if get_shopping_cart_settings().save_quotations_as_draft: + quotation.save() + else: + quotation.submit() - return quotation.name + 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].warehouse = warehouse - quotation_items[0].additional_notes = additional_notes - - 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} + 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].warehouse = warehouse + quotation_items[0].additional_notes = additional_notes + + 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() + if not context: + context = get_cart_quotation() - return frappe.render_template("templates/includes/cart/cart_dropdown.html", context) + 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) + doc = frappe.parse_json(doc) + doc.update({"doctype": "Address"}) + address = frappe.get_doc(doc) + address.save(ignore_permissions=True) - return address + 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 = 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", "") + 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) + 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") + 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"]}) + 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 - ), - ) + lead_doc.add_comment( + "Comment", + text=""" +
+
{subject}
+

{message}

+
+ """.format( + subject=subject, message=message + ), + ) - return lead_doc + return lead_doc @frappe.whitelist() def get_terms_and_conditions(terms_name): - return frappe.db.get_value("Terms and Conditions", terms_name, "terms") + 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 - ), - } + 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 get_root_of("Territory") - ) + territory = None + geoip_country = frappe.session.get("session_country") + if geoip_country: + territory = frappe.db.get_value("Territory", geoip_country) + + return territory or get_root_of("Territory") + + +def get_default_momo_gateway(): + """Return the first active MoMo Settings gateway name.""" + momo = frappe.db.get_value("MoMo Settings", {}, "gateway_name") + if not momo: + frappe.throw(_("No MoMo gateway configured in MoMo Settings")) + return momo + + +def create_momo_payment_request(sales_order_name, gateway_name): + """Create a draft Payment Request for a Sales Order (no auto‑push).""" + so = frappe.get_doc("Sales Order", sales_order_name) + gateway_full = ( + "MoMo-" + gateway_name if not gateway_name.startswith("MoMo-") else gateway_name + ) + + pga = frappe.db.get_value( + "Payment Gateway Account", + {"payment_gateway": gateway_full}, + ["name", "payment_account", "currency"], + as_dict=True, + ) + + pr = frappe.new_doc("Payment Request") + pr.payment_request_type = "Inward" + pr.party_type = "Customer" + pr.party = so.customer + pr.reference_doctype = "Sales Order" + pr.reference_name = so.name + pr.payment_gateway_account = pga.name if pga else "" + pr.payment_gateway = gateway_full + pr.payment_account = pga.payment_account if pga else "" + pr.currency = so.currency + pr.grand_total = so.grand_total + pr.base_grand_total = so.base_grand_total + pr.outstanding_amount = so.grand_total + pr.base_outstanding_amount = so.base_grand_total + pr.email_to = so.customer + pr.subject = f"Payment Request for {so.name}" + + # IMPORTANT: Do NOT submit (docstatus=0) so that the automatic push is not triggered. + pr.flags.ignore_permissions = True + pr.insert() + return pr 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"] + 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 + # 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:] + 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 - ) - ) + 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" - ) + website_warehouse = frappe.get_cached_value( + "Website Item", {"item_code": item_code}, "website_warehouse" + ) - d.warehouse = website_warehouse + d.warehouse = website_warehouse - return doc + 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_single_value("Webshop Settings", "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 + """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_single_value("Webshop Settings", "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 = get_party() - party.customer_name = company_name or fullname - party.customer_type = "Company" if company_name else "Individual" + 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() + 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() + 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() + 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) + if not party: + party = get_party() + if not quotation: + quotation = _get_cart_quotation(party) - cart_settings = frappe.get_cached_doc("Webshop Settings") + cart_settings = frappe.get_cached_doc("Webshop Settings") - set_price_list_and_rate(quotation, cart_settings) + set_price_list_and_rate(quotation, cart_settings) - quotation.run_method("calculate_taxes_and_totals") + quotation.run_method("calculate_taxes_and_totals") - set_taxes(quotation, cart_settings) + set_taxes(quotation, cart_settings) - _apply_shipping_rule(party, 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 based on billing territory""" - _set_price_list(cart_settings, quotation) + _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 + # 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") + # 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 - ) + 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 + """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 + 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 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 + # 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 + if quotation: + quotation.selling_price_list = selling_price_list - return 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() - quotation.append_taxes_from_item_tax_template() + """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() + quotation.append_taxes_from_item_tax_template() 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_cached_doc("Webshop Settings") - - debtors_account = "" - - if cart_settings.enable_checkout: - debtors_account = get_debtors_account(cart_settings) - - if party: - doc = frappe.get_doc(party_doctype, party) - if doc.doctype in ["Customer", "Supplier"]: - if not frappe.db.exists("Portal User", {"parent": doc.name, "user": user}): - doc.append("portal_users", {"user": user}) - doc.flags.ignore_permissions = True - doc.flags.ignore_mandatory = True - doc.save() - - return doc - - elif not frappe.db.exists("Portal User", {"user": user}): - 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 - else: - customer = frappe.db.get_value( - "Portal User", {"user": user}, ["parent"] - ) - - if frappe.db.exists("Customer", customer): - return frappe.get_doc("Customer", customer) + 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_cached_doc("Webshop Settings") + + debtors_account = "" + + if cart_settings.enable_checkout: + debtors_account = get_debtors_account(cart_settings) + + if party: + doc = frappe.get_doc(party_doctype, party) + if doc.doctype in ["Customer", "Supplier"]: + if not frappe.db.exists("Portal User", {"parent": doc.name, "user": user}): + doc.append("portal_users", {"user": user}) + doc.flags.ignore_permissions = True + doc.flags.ignore_mandatory = True + doc.save() + + return doc + + elif not frappe.db.exists("Portal User", {"user": user}): + 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 + else: + customer = frappe.db.get_value("Portal User", {"user": user}, ["parent"]) + + if frappe.db.exists("Customer", customer): + return frappe.get_doc("Customer", 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 + 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( @@ -659,162 +734,162 @@ def get_address_docs( limit_page_length=20, party=None, ): - if not party: - party = get_party() + if not party: + party = get_party() - if not party: - return [] + 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 - ), - ) + address_names = frappe.db.get_all( + "Dynamic Link", + fields=("parent"), + filters=dict( + parenttype="Address", link_doctype=party.doctype, link_name=party.name + ), + ) - out = [] + out = [] - for a in address_names: - address = frappe.get_doc("Address", a.parent) - address.display = get_address_display(address.as_dict()) - out.append(address) + 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 + return out @frappe.whitelist() def apply_shipping_rule(shipping_rule): - quotation = _get_cart_quotation() + quotation = _get_cart_quotation() - quotation.shipping_rule = shipping_rule + quotation.shipping_rule = shipping_rule - apply_cart_settings(quotation=quotation) + apply_cart_settings(quotation=quotation) - quotation.flags.ignore_permissions = True - quotation.save() + quotation.flags.ignore_permissions = True + quotation.save() - return get_cart_quotation(quotation) + 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 quotation.shipping_rule: + shipping_rules = get_shipping_rules(quotation, cart_settings) - if not shipping_rules: - return + if not shipping_rules: + return - elif quotation.shipping_rule not in shipping_rules: - quotation.shipping_rule = shipping_rules[0] + 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") + 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) + shipping_rules = get_shipping_rules(quotation) - if shipping_rules: - rule_label_map = frappe.db.get_values("Shipping Rule", shipping_rules, "label") - # 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] + 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) & (sr.shipping_rule_type == "Selling")) - ) - result = query.run(as_list=True) - shipping_rules = [x[0] for x in result] - - return shipping_rules + 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) + & (sr.shipping_rule_type == "Selling") + ) + ) + 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 + """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 + 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 + return territory def show_terms(doc): - return doc.tc_name + 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")) - 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_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 - coupon_name = coupon_list[0].name + from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code - from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(coupon_name) + quotation = _get_cart_quotation() + quotation.ignore_pricing_rule = 0 + quotation.coupon_code = coupon_name + quotation.flags.ignore_permissions = True + quotation.save() - validate_coupon_code(coupon_name) - quotation = _get_cart_quotation() - quotation.ignore_pricing_rule = 0 - 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() - 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 - return quotation - @frappe.whitelist(allow_guest=True) def remove_coupon_code(): - quotation = _get_cart_quotation() - quotation.coupon_code = "" - quotation.referral_sales_partner = "" - quotation.flags.ignore_permissions = True + quotation = _get_cart_quotation() + quotation.coupon_code = "" + quotation.referral_sales_partner = "" + quotation.flags.ignore_permissions = True - # reset discount amount if coupon code is removed (on desk it is done in client side) - # as we are enabling ignore_pricing_rule, so we also need to manually reset discount percentage - quotation.discount_amount = 0 - quotation.additional_discount_percentage = 0 - quotation.ignore_pricing_rule = 1 + # reset discount amount if coupon code is removed + quotation.discount_amount = 0 + quotation.additional_discount_percentage = 0 + quotation.ignore_pricing_rule = 1 - quotation.save() + quotation.save() - return quotation \ No newline at end of file + return quotation