diff --git a/erpnext/public/images/ui-states/cart-empty-state.png b/erpnext/public/images/ui-states/cart-empty-state.png new file mode 100644 index 0000000000..e1ead0e175 Binary files /dev/null and b/erpnext/public/images/ui-states/cart-empty-state.png differ diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 16a19ad270..7bce6abafa 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -5,6 +5,19 @@ body.product-page { background: var(--gray-50); } + +.item-breadcrumbs { + .breadcrumb-container { + ol.breadcrumb { + background-color: var(--gray-50) !important; + } + + a { + color: var(--gray-900); + } + } +} + .carousel-control { height: 42px; width: 42px; @@ -77,6 +90,18 @@ body.product-page { margin-top: 1.25rem; } + .no-image { + @include flex(flex, center, center, null); + height: 200px; + margin: 0 auto; + margin-top: var(--margin-xl); + background: var(--gray-100); + width: 80%; + border-radius: var(--border-radius); + font-size: 2rem; + color: var(--gray-500); + } + .product-title { font-size: 14px; color: var(--gray-800); @@ -98,7 +123,7 @@ body.product-page { .product-category { font-size: 13px; - color: var(--gray-600); + color: var(--text-muted); margin: var(--margin-sm) 0; } @@ -159,6 +184,15 @@ body.product-page { @include card($padding: var(--padding-md)); min-height: 70vh; + .product-details { + max-width: 40%; + margin-left: -30px; + + .btn-add-to-cart { + font-size: var(--text-base); + } + } + .product-title { font-size: 24px; font-weight: 600; @@ -166,7 +200,7 @@ body.product-page { } .product-code { - color: var(--gray-600); + color: var(--text-muted); font-size: 13px; } @@ -180,15 +214,15 @@ body.product-page { padding: 15px; @include media-breakpoint-between(xs, md) { - height: 320px; - width: 320px; + height: 300px; + width: 300px; } @include media-breakpoint-up(lg) { - height: 430px; - width: 420px; + height: 350px; + width: 350px; } - + img { object-fit: contain; } @@ -202,13 +236,13 @@ body.product-page { @include media-breakpoint-up(lg) { max-height: 430px; } - + overflow: scroll; } .item-slideshow-image { height: 4rem; - width: 4rem; + width: 6rem; object-fit: contain; padding: 0.5rem; border: 1px solid var(--table-border-color); @@ -227,25 +261,57 @@ body.product-page { font-weight: 600; .formatted-price { - color: var(--gray-600); - font-size: 14px; + color: var(--text-muted); + font-size: var(--text-base); } } .no-stock { - font-size: 14px; + font-size: var(--text-base); } } +} - .item-breadcrumbs { - .breadcrumb-container { - margin: 0px; - padding: 0px; +.item-configurator-dialog { + .modal-header { + padding: var(--padding-md) var(--padding-xl); + } - a { - color: $text-muted; + .modal-body { + padding: 0 var(--padding-xl); + 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); + } + } + } + } + + svg { + display: none; + } } } @@ -257,4 +323,168 @@ body.product-page { .carousel-inner.rounded-carousel { border-radius: $card-border-radius; } -} \ No newline at end of file +} + +.cart-icon { + .cart-badge { + position: relative; + top: -10px; + left: -12px; + background: var(--red-600); + width: 16px; + align-items: center; + height: 16px; + font-size: 10px; + border-radius: 50%; + } +} + + +#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; + } + + .cart-items-header { + font-weight: 600; + } + + .cart-table { + 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-items { + .item-title { + font-size: var(--text-base); + font-weight: 500; + color: var(--text-color); + } + + .item-subtitle { + color: var(--text-muted); + font-size: var(--text-md); + } + + .item-subtotal { + font-size: var(--text-base); + font-weight: 500; + } + + .item-rate { + font-size: var(--text-md); + color: var(--text-muted); + } + + textarea { + width: 40%; + } + } + + .cart-tax-items { + .item-grand-total { + font-size: 16px; + font-weight: 600; + color: var(--text-color); + } + } + } + + .cart-addresses { + hr { + border-color: var(--border-color); + } + } + + .number-spinner { + width: 75%; + .cart-btn { + border: none; + background: var(--gray-100); + box-shadow: none; + height: 28px; + align-items: center; + display: flex; + } + + .cart-qty { + height: 28px; + font-size: var(--text-md); + } + } + + .place-order-container { + .btn-place-order { + width: 62%; + } + } + } +} + +.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: var(--text-base); + font-weight: 500; + } + + .card-text { + font-size: var(--text-md); + color: var(--gray-700); + } + + .card-link { + font-size: var(--text-md); + + svg use { + stroke: var(--blue-500); + } + } + + .btn-change-address { + color: var(--blue-500); + box-shadow: none; + border: 1px solid var(--blue-500); + } +} + +.modal .address-card { + .card-body { + padding: var(--padding-sm); + border-radius: var(--border-radius); + border: 1px solid var(--dark-border-color); + } +} + diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index c2549fe7dd..fa9dcedc64 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -42,14 +42,30 @@ def get_cart_quotation(doc=None): return { "doc": decorate_quotation_doc(doc), - "shipping_addresses": [{"name": address.name, "title": address.address_title, "display": address.display} - for address in addresses if address.address_type == "Shipping"], - "billing_addresses": [{"name": address.name, "title": address.address_title, "display": address.display} - for address in addresses if address.address_type == "Billing"], + "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("Shopping Cart 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() @@ -203,27 +219,33 @@ def get_terms_and_conditions(terms_name): @frappe.whitelist() def update_cart_address(address_type, address_name): quotation = _get_cart_quotation() - address_display = get_address_display(frappe.get_doc("Address", address_name).as_dict()) + 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 diff --git a/erpnext/templates/includes/cart.js b/erpnext/templates/includes/cart.js index c6dfd35e29..0de02676af 100644 --- a/erpnext/templates/includes/cart.js +++ b/erpnext/templates/includes/cart.js @@ -14,7 +14,7 @@ $.extend(shopping_cart, { }, bind_events: function() { - shopping_cart.bind_address_select(); + shopping_cart.bind_address_picker_dialog(); shopping_cart.bind_place_order(); shopping_cart.bind_request_quotation(); shopping_cart.bind_change_qty(); @@ -23,28 +23,76 @@ $.extend(shopping_cart, { shopping_cart.bind_coupon_code(); }, - bind_address_select: function() { - $(".cart-addresses").on('click', '.address-card', function(e) { - const $card = $(e.currentTarget); - const address_type = $card.closest('[data-address-type]').attr('data-address-type'); - const address_name = $card.closest('[data-address-name]').attr('data-address-name'); - return frappe.call({ - type: "POST", - method: "erpnext.shopping_cart.cart.update_cart_address", - freeze: true, - args: { - address_type, - address_name - }, - callback: function(r) { - if(!r.exc) { - $(".cart-tax-items").html(r.message.taxes); - } - } - }); + 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() { + return 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.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.taxes); + shopping_cart.parent.find( + `.address-container[data-address-type="${address_type}"]` + ).html(r.message.address); + } + } + }); + } + }); + }, + + 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); @@ -221,6 +269,7 @@ $.extend(shopping_cart, { frappe.ready(function() { $(".cart-icon").hide(); + shopping_cart.parent = $(".cart-container"); shopping_cart.bind_events(); }); diff --git a/erpnext/templates/includes/cart/address_card.html b/erpnext/templates/includes/cart/address_card.html index 646210e65f..667144bd87 100644 --- a/erpnext/templates/includes/cart/address_card.html +++ b/erpnext/templates/includes/cart/address_card.html @@ -1,12 +1,17 @@
-
- +
+ {{ _('Change') }}
-
-
{{ address.title }}
-

+

+
{{ address.title }}
+
{{ address.display }} -

- {{ _('Edit') }} +
+ + + + + {{ _('Edit') }} +
diff --git a/erpnext/templates/includes/cart/address_picker_card.html b/erpnext/templates/includes/cart/address_picker_card.html new file mode 100644 index 0000000000..2334ea2955 --- /dev/null +++ b/erpnext/templates/includes/cart/address_picker_card.html @@ -0,0 +1,12 @@ +
+
+ +
+
+
{{ address.title }}
+

+ {{ address.display }} +

+ {{ _('Edit') }} +
+
\ No newline at end of file diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index aa25c885fe..3c8267c43d 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -14,31 +14,39 @@
{% endif %} -
-
{{ _("Shipping Address") }}
+
+
{{ _("Shipping Address") }}
+
+ {% for address in shipping_addresses %} + {% if doc.shipping_address_name == address.name %}
- {% for address in shipping_addresses %} -
- {% include "templates/includes/cart/address_card.html" %} -
- {% endfor %} +
+ {% include "templates/includes/cart/address_card.html" %} +
+ {% endif %} + {% endfor %}
-
-
{{ _("Billing Address") }}
-
- {% for address in billing_addresses %} -
- {% include "templates/includes/cart/address_card.html" %} -
- {% endfor %} -
+
+
-
- - +
+
{{ _("Billing Address") }}
+
+ {% for address in billing_addresses %} + {% if doc.customer_address == address.name %} +
+
+ {% include "templates/includes/cart/address_card.html" %} +
+
+ {% endif %} + {% endfor %}
- + diff --git a/erpnext/templates/includes/cart/cart_address_picker.html b/erpnext/templates/includes/cart/cart_address_picker.html new file mode 100644 index 0000000000..72cc5f5142 --- /dev/null +++ b/erpnext/templates/includes/cart/cart_address_picker.html @@ -0,0 +1,4 @@ +
+
{{ _("Shipping Address") }}
+
+ diff --git a/erpnext/templates/includes/cart/cart_items.html b/erpnext/templates/includes/cart/cart_items.html index ca5744bebb..75441c42bc 100644 --- a/erpnext/templates/includes/cart/cart_items.html +++ b/erpnext/templates/includes/cart/cart_items.html @@ -1,15 +1,15 @@ {% for d in doc.items %} -
+
{{ 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 %} @@ -20,20 +20,20 @@
- + - + - +
{% if cart_settings.enable_checkout %} - +
{{ d.get_formatted('amount') }}
- + {{ _('Rate:') }} {{ d.get_formatted('rate') }} diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 74c84ba27c..0adfbc9dfd 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -90,6 +90,10 @@
{% if image %} {{ title }} + {% else %} +
+ {{ frappe.utils.get_abbr(title) }} +
{% endif %} {{ item_card_body(title, description, url, rate, category, is_featured, align) }}
diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html index 4daf0e74e0..133d99e5eb 100644 --- a/erpnext/templates/includes/navbar/navbar_items.html +++ b/erpnext/templates/includes/navbar/navbar_items.html @@ -2,9 +2,11 @@ {% block navbar_right_extension %} {% endblock %} \ No newline at end of file diff --git a/erpnext/templates/includes/order/order_taxes.html b/erpnext/templates/includes/order/order_taxes.html index ebec838d03..d2c458e0a4 100644 --- a/erpnext/templates/includes/order/order_taxes.html +++ b/erpnext/templates/includes/order/order_taxes.html @@ -29,12 +29,12 @@ {{ _("Discount") }} - {% set tot_quotation_discount = [] %} - {%- for item in doc.items -%} - {% if tot_quotation_discount.append((((item.price_list_rate * item.qty) - * item.discount_percentage) / 100)) %}{% endif %} - {% endfor %} - {{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }} + {% set tot_quotation_discount = [] %} + {%- for item in doc.items -%} + {% if tot_quotation_discount.append((((item.price_list_rate * item.qty) + * item.discount_percentage) / 100)) %}{% endif %} + {% endfor %} + {{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }} {% endif %} @@ -47,51 +47,52 @@ {{ _("Total Amount") }} - - {% set total_amount = [] %} - {%- for item in doc.items -%} - {% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %} - {% endfor %} - {{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }} - + + {% set total_amount = [] %} + {%- for item in doc.items -%} + {% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %} + {% endfor %} + {{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }} + - - {{ _("Applied Coupon Code") }} - - - - {%- for row in frappe.get_all(doctype="Coupon Code", - fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%} - {{ row.coupon_code }} - {% endfor %} - - + + {{ _("Applied Coupon Code") }} + + + + {%- for row in frappe.get_all(doctype="Coupon Code", + fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%} + {{ row.coupon_code }} + {% endfor %} + + - - {{ _("Discount") }} - - - - {% set tot_SO_discount = [] %} - {%- for item in doc.items -%} - {% if tot_SO_discount.append((((item.price_list_rate * item.qty) - * item.discount_percentage) / 100)) %}{% endif %} - {% endfor %} - {{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }} - - + + {{ _("Discount") }} + + + + {% set tot_SO_discount = [] %} + {%- for item in doc.items -%} + {% if tot_SO_discount.append((((item.price_list_rate * item.qty) + * item.discount_percentage) / 100)) %}{% endif %} + {% endfor %} + {{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }} + + {% endif %} {% endif %} - + + {{ _("Grand Total") }} - + {{ doc.get_formatted("grand_total") }} diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html index 3033d1587d..2cabf5a9e0 100644 --- a/erpnext/templates/pages/cart.html +++ b/erpnext/templates/pages/cart.html @@ -2,7 +2,7 @@ {% block title %} {{ _("Shopping Cart") }} {% endblock %} -{% block header %}

{{ _("Shopping Cart") }}

{% endblock %} +{% block header %}

{{ _("Shopping Cart") }}

{% endblock %}