chore: Shopping Cart styles and cleanup shallow
- Added remove button to cart item rows
- Freeze on change in Shopping Cart (UX)
- Fixed cart items and taxes/totals alignment issues
- Made Cart responsive
- Added free item indicator
- Fixed item group nested routing issue
- Sales Order is populated with right source warehouse
diff --git a/erpnext/e_commerce/product_list.js b/erpnext/e_commerce/product_list.js
index fa35c7e..3aa6e8c 100644
--- a/erpnext/e_commerce/product_list.js
+++ b/erpnext/e_commerce/product_list.js
@@ -39,7 +39,7 @@
if (image) {
return `
- <div class="col-2 border text-center rounded product-image" style="overflow: hidden; max-height: 200px;">
+ <div class="col-2 border text-center rounded list-image">
<a class="product-link product-list-link" href="/${ item.route || '#' }">
<img itemprop="image" class="website-image h-100 w-100" alt="${ title }"
src="${ image }">
diff --git a/erpnext/e_commerce/product_view.js b/erpnext/e_commerce/product_view.js
index a64d55f..4e1c23c 100644
--- a/erpnext/e_commerce/product_view.js
+++ b/erpnext/e_commerce/product_view.js
@@ -79,7 +79,7 @@
let me = this;
this.prepare_product_area_wrapper("grid");
- frappe.require('assets/js/e-commerce.min.js', function() {
+ frappe.require('/assets/js/e-commerce.min.js', function() {
new erpnext.ProductGrid({
items: items,
products_section: $("#products-grid-area"),
@@ -93,7 +93,7 @@
let me = this;
this.prepare_product_area_wrapper("list");
- frappe.require('assets/js/e-commerce.min.js', function() {
+ frappe.require('/assets/js/e-commerce.min.js', function() {
new erpnext.ProductList({
items: items,
products_section: $("#products-list-area"),
@@ -304,10 +304,10 @@
}
let route_params = frappe.utils.get_query_params();
- const query_string = get_query_string({
- start: if_key_exists(route_params.start) || 0,
- field_filters: JSON.stringify(if_key_exists(this.field_filters)),
- attribute_filters: JSON.stringify(if_key_exists(this.attribute_filters)),
+ const query_string = me.get_query_string({
+ start: me.if_key_exists(route_params.start) || 0,
+ field_filters: JSON.stringify(me.if_key_exists(this.field_filters)),
+ attribute_filters: JSON.stringify(me.if_key_exists(this.attribute_filters)),
});
window.history.pushState('filters', '', `${location.pathname}?` + query_string);
@@ -380,26 +380,26 @@
$("#product-listing").prepend(sub_group_html);
}
}
-};
-function get_query_string(object) {
- const url = new URLSearchParams();
- for (let key in object) {
- const value = object[key];
- if (value) {
- url.append(key, value);
+ 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();
}
- return url.toString();
-}
-function if_key_exists(obj) {
- let exists = false;
- for (let key in obj) {
- if (obj.hasOwnProperty(key) && obj[key]) {
- exists = true;
- break;
+ if_key_exists(obj) {
+ let exists = false;
+ for (let key in obj) {
+ if (obj.hasOwnProperty(key) && obj[key]) {
+ exists = true;
+ break;
+ }
}
+ return exists ? obj : undefined;
}
- return exists ? obj : undefined;
}
\ No newline at end of file
diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
index 34ed10b..d3b860d 100644
--- a/erpnext/e_commerce/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -89,8 +89,14 @@
if not cint(cart_settings.allow_items_not_in_stock):
for item in sales_order.get("items"):
- item.reserved_warehouse, is_stock_item = frappe.db.get_value("Item",
- item.item_code, ["website_warehouse", "is_stock_item"])
+ 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")
diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js
index 80f731e..6b42987 100644
--- a/erpnext/public/js/shopping_cart.js
+++ b/erpnext/public/js/shopping_cart.js
@@ -80,6 +80,7 @@
}
window.location.href = "/login";
} else {
+ shopping_cart.freeze();
return frappe.call({
type: "POST",
method: "erpnext.e_commerce.shopping_cart.cart.update_cart",
@@ -91,6 +92,7 @@
},
btn: opts.btn,
callback: function(r) {
+ shopping_cart.unfreeze();
shopping_cart.set_cart_count();
if(opts.callback)
opts.callback(r);
@@ -135,7 +137,6 @@
},
shopping_cart_update: function({item_code, qty, cart_dropdown, additional_notes}) {
- frappe.freeze();
shopping_cart.update_cart({
item_code,
qty,
@@ -143,7 +144,6 @@
with_items: 1,
btn: this,
callback: function(r) {
- frappe.unfreeze();
if(!r.exc) {
$(".cart-items").html(r.message.items);
$(".cart-tax-items").html(r.message.taxes);
@@ -196,6 +196,29 @@
});
});
- }
+ },
+ freeze() {
+ if (window.location.pathname !== "/cart") return
+
+ if (!$('#freeze').length) {
+ let freeze = $('<div id="freeze" class="modal-backdrop fade"></div>')
+ .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/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index df0d51c..8048f76 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -184,6 +184,12 @@
}
}
+.list-image {
+ overflow: hidden;
+ max-height: 200px;
+ background-color: white;
+}
+
.product-container {
@include card($padding: var(--padding-md));
min-height: 70vh;
@@ -413,7 +419,10 @@
}
}
-
+.total-discount {
+ font-size: var(--text-base);
+ color: var(--primary-color);
+}
#page-cart {
.shopping-cart-header {
@@ -434,6 +443,10 @@
}
.cart-table {
+ tr {
+ margin-bottom: 1rem;
+ }
+
th, tr, td {
border-color: var(--border-color);
border-width: 1px;
@@ -468,6 +481,16 @@
font-weight: 500;
}
+ .sm-item-subtotal {
+ font-size: var(--text-base);
+ font-weight: 500;
+ display: none;
+
+ @include media-breakpoint-between(xs, md) {
+ display: unset !important;
+ }
+ }
+
.item-rate {
font-size: var(--text-md);
color: var(--text-muted);
@@ -481,10 +504,44 @@
.cart-tax-items {
.item-grand-total {
font-size: 16px;
- font-weight: 600;
+ font-weight: 700;
color: var(--text-color);
}
}
+
+ .column-sm-view {
+ @include media-breakpoint-between(xs, md) {
+ display: none !important;
+ }
+ }
+
+ .item-column {
+ width: 50%;
+ @include media-breakpoint-between(xs, md) {
+ width: 70%;
+ }
+ }
+
+ .remove-cart-item {
+ border-radius: 50%;
+ border: 1px solid var(--gray-100);
+ width: 22px;
+ height: 22px;
+ background-color: var(--gray-200);
+ float: right;
+ }
+
+ .remove-cart-item-logo {
+ margin-bottom: 6px;
+ margin-left: 1px;
+ }
+
+ .totals {
+ padding-right: 4rem;
+ @include media-breakpoint-between(xs, md) {
+ padding-right: 1rem;
+ }
+ }
}
.cart-addresses {
@@ -495,12 +552,16 @@
.number-spinner {
width: 75%;
+ min-width: 105px;
.cart-btn {
border: none;
- background: var(--gray-100);
+ background: var(--primary-color);
+ color: white;
box-shadow: none;
+ width: 24px;
height: 28px;
align-items: center;
+ justify-content: center;
display: flex;
}
@@ -512,7 +573,7 @@
.place-order-container {
.btn-place-order {
- width: 62%;
+ float: right;
}
}
}
@@ -652,12 +713,12 @@
}
.not-added {
- color: var(--blue-500);
- background-color: white;
+ color: var(--primary-color);
+ background-color: transparent;
border: 1px solid var(--blue-500);
&:hover {
- background-color: var(--blue-500);
+ background-color: var(--primary-color);
color: white;
}
}
@@ -795,3 +856,12 @@
.placeholder {
font-size: 72px;
}
+
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ right: 0;
+ left: 0;
+ background-color: var(--gray-100);
+ height: 100%;
+}
diff --git a/erpnext/templates/includes/cart.js b/erpnext/templates/includes/cart.js
index f0781ab..28fe882 100644
--- a/erpnext/templates/includes/cart.js
+++ b/erpnext/templates/includes/cart.js
@@ -18,6 +18,7 @@
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();
},
@@ -153,6 +154,18 @@
});
},
+ 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) {
diff --git a/erpnext/templates/includes/cart/cart_items.html b/erpnext/templates/includes/cart/cart_items.html
index d534b8f..226c600 100644
--- a/erpnext/templates/includes/cart/cart_items.html
+++ b/erpnext/templates/includes/cart/cart_items.html
@@ -1,42 +1,86 @@
-{% for d in doc.items %}
-<tr data-name="{{ d.name }}">
- <td>
- <div class="item-title mb-1">
- {{ d.item_name }}
- </div>
- <div class="item-subtitle">
- {{ d.item_code }}
- </div>
- {%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %}
- {% if variant_of %}
- <span class="item-subtitle">
- {{ _('Variant of') }} <a href="{{frappe.db.get_value('Item', variant_of, 'route')}}">{{ variant_of }}</a>
- </span>
- {% endif %}
- <div class="mt-2 notes">
- <textarea data-item-code="{{d.item_code}}" class="form-control" rows="2" placeholder="{{ _('Add notes') }}">{{d.additional_notes or ''}}</textarea>
- </div>
- </td>
- <td class="text-right">
- <div class="input-group number-spinner">
- <span class="input-group-prepend d-none d-sm-inline-block">
- <button class="btn cart-btn" data-dir="dwn">–</button>
- </span>
- <input class="form-control text-center cart-qty" value="{{ d.get_formatted('qty') }}" data-item-code="{{ d.item_code }}">
- <span class="input-group-append d-none d-sm-inline-block">
- <button class="btn cart-btn" data-dir="up">+</button>
+{% macro item_subtotal(item) %}
+ <div>
+ {{ item.get_formatted('amount') }}
+ </div>
+
+ {% if item.is_free_item %}
+ <div class="text-success mt-4">
+ <span style="
+ padding: 4px 8px;
+ border-radius: 4px;
+ border: 1px dashed">
+ {{ _('FREE') }}
</span>
</div>
- </td>
- {% if cart_settings.enable_checkout %}
- <td class="text-right item-subtotal">
- <div>
- {{ d.get_formatted('amount') }}
- </div>
+ {% else %}
<span class="item-rate">
- {{ _('Rate:') }} {{ d.get_formatted('rate') }}
+ {{ _('Rate:') }} {{ item.get_formatted('rate') }}
</span>
- </td>
{% endif %}
-</tr>
+{% endmacro %}
+
+{% for d in doc.items %}
+ <tr data-name="{{ d.name }}">
+ <td>
+ <div class="item-title mb-1 mr-3">
+ {{ d.item_name }}
+ </div>
+ <div class="item-subtitle mr-2">
+ {{ d.item_code }}
+ </div>
+ {%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %}
+ {% if variant_of %}
+ <span class="item-subtitle mr-2">
+ {{ _('Variant of') }} <a href="{{frappe.db.get_value('Item', variant_of, 'route')}}">{{ variant_of }}</a>
+ </span>
+ {% endif %}
+ <div class="mt-2 notes">
+ <textarea data-item-code="{{d.item_code}}" class="form-control" rows="2" placeholder="{{ _('Add notes') }}">
+ {{d.additional_notes or ''}}
+ </textarea>
+ </div>
+ </td>
+
+ <!-- Qty column -->
+ <td class="text-right">
+ <div class="input-group number-spinner mt-1 mb-4">
+ <span class="input-group-prepend d-sm-inline-block">
+ <button class="btn cart-btn" data-dir="dwn">–</button>
+ </span>
+ <input class="form-control text-center cart-qty" value="{{ d.get_formatted('qty') }}" data-item-code="{{ d.item_code }}">
+ <span class="input-group-append d-sm-inline-block">
+ <button class="btn cart-btn" data-dir="up">+</button>
+ </span>
+ </div>
+
+ <!-- Shown on mobile view, else hidden -->
+ {% if cart_settings.enable_checkout %}
+ <div class="text-right sm-item-subtotal">
+ {{ item_subtotal(d) }}
+ </div>
+ {% endif %}
+ </td>
+
+ <!-- Subtotal column -->
+ {% if cart_settings.enable_checkout %}
+ <td class="text-right item-subtotal column-sm-view">
+ {{ item_subtotal(d) }}
+ </td>
+ {% endif %}
+
+ <!-- Show close button irrespective except on free items -->
+ <td class="text-right">
+ {% if not d.is_free_item %}
+ <div class="ml-1 remove-cart-item column-sm-view" data-item-code="{{ d.item_code }}">
+ <span>
+ <svg class="icon sm remove-cart-item-logo"
+ width="18" height="18" viewBox="0 0 18 18"
+ xmlns="http://www.w3.org/2000/svg" id="icon-close">
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M4.146 11.217a.5.5 0 1 0 .708.708l3.182-3.182 3.181 3.182a.5.5 0 1 0 .708-.708l-3.182-3.18 3.182-3.182a.5.5 0 1 0-.708-.708l-3.18 3.181-3.183-3.182a.5.5 0 0 0-.708.708l3.182 3.182-3.182 3.181z" stroke-width="0"></path>
+ </svg>
+ </span>
+ </div>
+ {% endif %}
+ </td>
+ </tr>
{% endfor %}
diff --git a/erpnext/templates/includes/order/order_taxes.html b/erpnext/templates/includes/order/order_taxes.html
index d2c458e..b821e62 100644
--- a/erpnext/templates/includes/order/order_taxes.html
+++ b/erpnext/templates/includes/order/order_taxes.html
@@ -1,9 +1,9 @@
{% if doc.taxes %}
<tr>
- <td class="text-right" colspan="2">
+ <td class="text-left" colspan="1">
{{ _("Net Total") }}
</td>
- <td class="text-right">
+ <td class="text-right totals" colspan="3">
{{ doc.get_formatted("net_total") }}
</td>
</tr>
@@ -12,10 +12,10 @@
{% for d in doc.taxes %}
{% if d.base_tax_amount %}
<tr>
- <td class="text-right" colspan="2">
+ <td class="text-left" colspan="1">
{{ d.description }}
</td>
- <td class="text-right">
+ <td class="text-right totals" colspan="3">
{{ d.get_formatted("base_tax_amount") }}
</td>
</tr>
@@ -23,76 +23,62 @@
{% endfor %}
{% if doc.doctype == 'Quotation' %}
-{% if doc.coupon_code %}
-<tr>
- <th class="text-right" colspan="2">
- {{ _("Discount") }}
- </th>
- <th class="text-right tot_quotation_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) }}
- </th>
-</tr>
-{% endif %}
+ {% if doc.coupon_code %}
+ <tr>
+ <td class="text-left total-discount" colspan="1">
+ {{ _("Savings") }}
+ </td>
+ <td class="text-right tot_quotation_discount total-discount totals" colspan="3">
+ {% 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) }}
+ </td>
+ </tr>
+ {% endif %}
{% endif %}
{% if doc.doctype == 'Sales Order' %}
-{% if doc.coupon_code %}
-<tr>
- <th class="text-right" colspan="2">
- {{ _("Total Amount") }}
- </th>
- <th class="text-right">
- <span>
- {% 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) }}
- </span>
- </th>
-</tr>
-<tr>
- <th class="text-right" colspan="2">
- {{ _("Applied Coupon Code") }}
- </th>
- <th class="text-right">
- <span>
- {%- for row in frappe.get_all(doctype="Coupon Code",
- fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
- <span>{{ row.coupon_code }}</span>
- {% endfor %}
- </span>
- </th>
-</tr>
-<tr>
- <th class="text-right" colspan="2">
- {{ _("Discount") }}
- </th>
- <th class="text-right">
- <span>
- {% 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) }}
- </span>
- </th>
-</tr>
-{% endif %}
+ {% if doc.coupon_code %}
+ <tr>
+ <td class="text-left total-discount" colspan="2" style="padding-right: 2rem;">
+ {{ _("Applied Coupon Code") }}
+ </td>
+ <td class="text-right total-discount">
+ <span>
+ {%- for row in frappe.get_all(doctype="Coupon Code",
+ fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
+ <span>{{ row.coupon_code }}</span>
+ {% endfor %}
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td class="text-left total-discount" colspan="2">
+ {{ _("Savings") }}
+ </td>
+ <td class="text-right total-discount">
+ <span>
+ {% 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) }}
+ </span>
+ </td>
+ </tr>
+ {% endif %}
{% endif %}
<tr>
- <th></th>
- <th class="item-grand-total">
+ <th class="text-left item-grand-total" colspan="1">
{{ _("Grand Total") }}
</th>
- <th class="text-right item-grand-total">
+ <th class="text-right item-grand-total totals" colspan="3">
{{ doc.get_formatted("grand_total") }}
</th>
</tr>
diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html
index 0c7993b..262b86f 100644
--- a/erpnext/templates/pages/cart.html
+++ b/erpnext/templates/pages/cart.html
@@ -21,7 +21,7 @@
{% if doc.items %}
<div class="cart-container">
<div class="row m-0">
- <div class="col-md-8 frappe-card p-5">
+ <div class="col-md-8 frappe-card p-5 mb-4">
<div>
<div id="cart-error" class="alert alert-danger" style="display: none;"></div>
<div class="cart-items-header">
@@ -30,11 +30,12 @@
<table class="table mt-3 cart-table">
<thead>
<tr>
- <th width="60%">{{ _('Item') }}</th>
+ <th class="item-column">{{ _('Item') }}</th>
<th width="20%">{{ _('Quantity') }}</th>
{% if cart_settings.enable_checkout %}
- <th width="20%" class="text-right">{{ _('Subtotal') }}</th>
+ <th width="20" class="text-right column-sm-view">{{ _('Subtotal') }}</th>
{% endif %}
+ <th width="10%" class="column-sm-view"></th>
</tr>
</thead>
<tbody class="cart-items">
@@ -47,31 +48,32 @@
{% endif %}
</table>
</div>
- <div class="row">
- <div class="col-4">
+
+ <div class="row mt-2">
+ <div class="col-3">
{% if cart_settings.enable_checkout %}
- <a class="btn btn-outline-primary" href="/orders">
- {{ _('See past orders') }}
- </a>
+ <a class="btn btn-outline-primary" href="/orders">
+ {{ _('Past Orders') }}
+ </a>
{% else %}
- <a class="btn btn-outline-primary" href="/quotations">
- {{ _('See past quotations') }}
- </a>
+ <a class="btn btn-outline-primary" href="/quotations">
+ {{ _('See past quotations') }}
+ </a>
{% endif %}
</div>
- <div class="col-8">
+ <div class="col-9">
{% if doc.items %}
<div class="place-order-container">
<a class="btn btn-primary-light mr-2" href="/all-products">
- {{ _("Continue Shopping") }}
+ {{ _('Back to Shop') }}
</a>
{% if cart_settings.enable_checkout %}
<button class="btn btn-primary btn-place-order" type="button">
- {{ _("Place Order") }}
+ {{ _('Place Order') }}
</button>
{% else %}
<button class="btn btn-primary btn-request-for-quotation" type="button">
- {{ _("Request for Quotation") }}
+ {{ _('Request for Quotation') }}
</button>
{% endif %}
</div>
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index 336aa9a..ef8c210 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -8,7 +8,7 @@
let view_type = "List View";
// Render Product Views and setup Filters
- frappe.require('assets/js/e-commerce.min.js', function() {
+ frappe.require('/assets/js/e-commerce.min.js', function() {
new erpnext.ProductView({
view_type: view_type,
products_section: $('#product-listing'),
@@ -23,20 +23,6 @@
e_commerce.shopping_cart.bind_add_to_cart_action();
e_commerce.wishlist.bind_wishlist_action();
}
-
- // bind_search() {
- // $('input[type=search]').on('keydown', (e) => {
- // if (e.keyCode === 13) {
- // // Enter
- // const value = e.target.value;
- // if (value) {
- // window.location.search = 'search=' + e.target.value;
- // } else {
- // window.location.search = '';
- // }
- // }
- // });
- // }
}
new ProductListing();