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: `
+