refactor!: drop ecommerce in favor of webshop (#33265)

* refactor!: remove ecommerce item group field check

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: remove `e_commerce` directory

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: remove `get_context` from `item_group`

https://frappeframework.com/docs/v14/user/en/guides/portal-development/context

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: remove related `./templates`

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(navbar): remove wishlist (ecommerce)

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(js): remove js from scripts

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: remove `www/all-products`

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: remove pages and js

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: remove js/customer_reviews

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(portal utils): remove shopping cart debtor account

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: remove e_commerce events from hooks

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(web): remove e_commerce js from bundle

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(setup): remove shopping cart setup

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: remove pages

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor(item): remove website item button

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(payment request): remove `on_payment_authorized`

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: @staticmethod `get_gateway_details`

to avoid monkey patching, in custom apps
https://discuss.erpnext.com/t/how-to-override-method-in-frappe/28786/36

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(pages): remove product page

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(homepage): do not setup website items

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor(workspace): remove link to ecommerce settings

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(www): remove shop-by-category

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(homepage): remove featured product

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor: remove products in homepage

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor(homepage): remove explore button

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor: remove products fields from homepage

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* Revert "refactor!: @staticmethod `get_gateway_details`"

This reverts commit 561bcd96680a930bb92627869502d9346b10611b.

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: remove payment gateway e_commerce import

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* chore: pre-commit

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!: pass `party` into `get_price`

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor: move `get_item_codes_by_attributes` to `utilities/product`

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* refactor!(quotation): input customer group

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* chore: pre-commit

* refactor: remove custom `navbar_items.html`

* refactor!(item): remove `published_in_website`

* refactor: move `validate_duplicate_website_item` before rename

* test: remove `test_shopping_cart_without_website_item`

* chore: add doctype drop patch

* refactor: removed website item related code

* refactor: removed shopping_cart code

* refactor: removed e-commerce related patches

* refactor: removed website related fields from item group

* fix: patch create_asset_depreciation_schedules_from_assets, KeyError: '0K BU64 AUY'

---------

Signed-off-by: Sabu Siyad <hello@ssiyad.com>
Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
This commit is contained in:
Sabu Siyad
2023-10-17 17:05:44 +05:30
committed by GitHub
parent 9d687ca6e9
commit f900a78995
160 changed files with 630 additions and 15222 deletions

View File

@@ -1,80 +0,0 @@
{% extends "templates/web.html" %}
{% from "erpnext/templates/includes/macros.html" import recommended_item_row %}
{% block title %} {{ title }} {% endblock %}
{% block breadcrumbs %}
<div class="item-breadcrumbs small text-muted">
{% include "templates/includes/breadcrumbs.html" %}
</div>
{% endblock %}
{% block page_content %}
<div class="product-container item-main">
{% from "erpnext/templates/includes/macros.html" import product_image %}
<div class="item-content">
<div class="product-page-content" itemscope itemtype="http://schema.org/Product">
<!-- Image, Description, Add to Cart -->
<div class="row mb-5">
{% include "templates/generators/item/item_image.html" %}
{% include "templates/generators/item/item_details.html" %}
</div>
</div>
</div>
</div>
<!-- Additional Info/Reviews, Recommendations -->
<div class="d-flex">
{% set show_recommended_items = recommended_items and shopping_cart.cart_settings.enable_recommendations %}
{% set info_col = 'col-9' if show_recommended_items else 'col-12' %}
{% set padding_top = 'pt-0' if (show_tabs and tabs) else '' %}
<div class="product-container mt-4 {{ padding_top }} {{ info_col }}">
<div class="item-content {{ 'mt-minus-2' if (show_tabs and tabs) else '' }}">
<div class="product-page-content">
<!-- Product Specifications Table Section -->
{% if show_tabs and tabs %}
<div class="category-tabs">
<!-- tabs -->
{{ web_block("Section with Tabs", values=tabs, add_container=0,
add_top_padding=0, add_bottom_padding=0)
}}
</div>
{% elif website_specifications %}
{% include "templates/generators/item/item_specifications.html"%}
{% endif %}
<!-- Advanced Custom Website Content -->
{{ doc.website_content or '' }}
<!-- Reviews and Comments -->
{% if shopping_cart.cart_settings.enable_reviews and not doc.has_variants %}
{% include "templates/generators/item/item_reviews.html"%}
{% endif %}
</div>
</div>
</div>
<!-- Recommended Items -->
{% if show_recommended_items %}
<div class="mt-4 col-3 recommended-item-section">
<span class="recommendation-header">Recommended</span>
<div class="product-container mt-2 recommendation-container">
{% for item in recommended_items %}
{{ recommended_item_row(item) }}
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block base_scripts %}
<!-- js should be loaded in body! -->
<script type="text/javascript" src="/assets/frappe/js/lib/jquery/jquery.min.js"></script>
{{ include_script("frappe-web.bundle.js") }}
{{ include_script("controls.bundle.js") }}
{{ include_script("dialog.bundle.js") }}
{% endblock %}

View File

@@ -1,180 +0,0 @@
{% if shopping_cart and shopping_cart.cart_settings.enabled %}
{% set cart_settings = shopping_cart.cart_settings %}
{% set product_info = shopping_cart.product_info %}
<div class="item-cart row mt-2" data-variant-item-code="{{ item_code }}">
<div class="col-md-12">
<!-- Price and Availability -->
{% if cart_settings.show_price and product_info.price %}
{% set price_info = product_info.price %}
<div class="product-price">
<!-- Final Price -->
<span itemprop="offers" itemscope itemtype="https://schema.org/Offer">
<span itemprop="price" content="{{ price_info.price_list_rate }}">{{ price_info.formatted_price_sales_uom }}</span>
<span style="display:none;" itemprop="priceCurrency" content="{{ price_info.currency }}">{{ price_info.currency }}</span>
</span>
<!-- Striked Price and Discount -->
{% if price_info.formatted_mrp %}
<small class="formatted-price">
<s>MRP {{ price_info.formatted_mrp }}</s>
</small>
<small class="ml-1 formatted-price in-green">
-{{ price_info.get("formatted_discount_percent") or price_info.get("formatted_discount_rate")}}
</small>
{% endif %}
<!-- Price per UOM -->
<small class="formatted-price ml-2">
({{ price_info.formatted_price }} / {{ product_info.uom }})
</small>
</div>
{% else %}
{{ _("UOM") }} : {{ product_info.uom }}
{% endif %}
{% if cart_settings.show_stock_availability %}
<div class="mt-2">
{% if product_info.get("on_backorder") %}
<span class="no-stock out-of-stock" style="color: var(--primary-color);">
{{ _('Available on backorder') }}
</span>
{% elif product_info.in_stock == 0 %}
<span class="no-stock out-of-stock">
{{ _('Out of stock') }}
</span>
{% elif product_info.in_stock == 1 %}
<span class="in-green has-stock">
{{ _('In stock') }}
{% if product_info.show_stock_qty and product_info.stock_qty %}
({{ product_info.stock_qty }})
{% endif %}
</span>
{% endif %}
</div>
{% endif %}
<!-- Offers -->
{% if doc.offers %}
<br>
<div class="offers-heading mb-4">
<span class="mr-1 tag-icon">
<svg class="icon icon-lg"><use href="#icon-tag"></use></svg>
</span>
<b>Available Offers</b>
</div>
<div class="offer-container">
{% for offer in doc.offers %}
<div class="mt-2 d-flex">
<div class="mr-2" >
<svg width="24" height="24" viewBox="0 0 24 24" stroke="var(--yellow-500)" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 15.6213C19 15.2235 19.158 14.842 19.4393 14.5607L20.9393 13.0607C21.5251 12.4749 21.5251 11.5251 20.9393 10.9393L19.4393 9.43934C19.158 9.15804 19 8.7765 19 8.37868V6.5C19 5.67157 18.3284 5 17.5 5H15.6213C15.2235 5 14.842 4.84196 14.5607 4.56066L13.0607 3.06066C12.4749 2.47487 11.5251 2.47487 10.9393 3.06066L9.43934 4.56066C9.15804 4.84196 8.7765 5 8.37868 5H6.5C5.67157 5 5 5.67157 5 6.5V8.37868C5 8.7765 4.84196 9.15804 4.56066 9.43934L3.06066 10.9393C2.47487 11.5251 2.47487 12.4749 3.06066 13.0607L4.56066 14.5607C4.84196 14.842 5 15.2235 5 15.6213V17.5C5 18.3284 5.67157 19 6.5 19H8.37868C8.7765 19 9.15804 19.158 9.43934 19.4393L10.9393 20.9393C11.5251 21.5251 12.4749 21.5251 13.0607 20.9393L14.5607 19.4393C14.842 19.158 15.2235 19 15.6213 19H17.5C18.3284 19 19 18.3284 19 17.5V15.6213Z" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 9L9 15" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 9.5C10.5 10.0523 10.0523 10.5 9.5 10.5C8.94772 10.5 8.5 10.0523 8.5 9.5C8.5 8.94772 8.94772 8.5 9.5 8.5C10.0523 8.5 10.5 8.94772 10.5 9.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5 14.5C15.5 15.0523 15.0523 15.5 14.5 15.5C13.9477 15.5 13.5 15.0523 13.5 14.5C13.5 13.9477 13.9477 13.5 14.5 13.5C15.0523 13.5 15.5 13.9477 15.5 14.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<p class="mr-1 mb-1">
{{ _(offer.offer_title) }}:
{{ _(offer.offer_subtitle) if offer.offer_subtitle else '' }}
<a class="offer-details" href="#"
data-offer-title="{{ offer.offer_title }}" data-offer-id="{{ offer.name }}"
role="button">
{{ _("More") }}
</a>
</p>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Add to Cart / View in Cart, Contact Us -->
<div class="mt-6 mb-5">
<div class="mb-4 d-flex">
<!-- Add to Cart -->
{% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
<a href="/cart" class="btn btn-light btn-view-in-cart hidden mr-2 font-md"
role="button">
{{ _("View in Cart") if cart_settings.enable_checkout else _("View in Quote") }}
</a>
<button
data-item-code="{{item_code}}"
class="btn btn-primary btn-add-to-cart mr-2 w-30-40"
>
<span class="mr-2">
<svg class="icon icon-md">
<use href="#icon-assets"></use>
</svg>
</span>
{{ _("Add to Cart") if cart_settings.enable_checkout else _("Add to Quote") }}
</button>
{% endif %}
<!-- Contact Us -->
{% if cart_settings.show_contact_us_button %}
{% include "templates/generators/item/item_inquiry.html" %}
{% endif %}
</div>
</div>
</div>
</div>
<script>
frappe.ready(() => {
$('.page_content').on('click', '.btn-add-to-cart', (e) => {
// Bind action on add to cart button
const $btn = $(e.currentTarget);
$btn.prop('disabled', true);
const item_code = $btn.data('item-code');
erpnext.e_commerce.shopping_cart.update_cart({
item_code,
qty: 1,
callback(r) {
$btn.prop('disabled', false);
if (r.message) {
$('.btn-add-to-cart, .btn-view-in-cart').toggleClass('hidden');
}
}
});
});
$('.page_content').on('click', '.offer-details', (e) => {
// Bind action on More link in Offers
const $btn = $(e.currentTarget);
$btn.prop('disabled', true);
var d = new frappe.ui.Dialog({
title: __($btn.data('offer-title')),
fields: [
{
fieldname: 'offer_details',
fieldtype: 'HTML'
},
{
fieldname: 'section_break',
fieldtype: 'Section Break'
}
]
});
frappe.call({
method: 'erpnext.e_commerce.doctype.website_offer.website_offer.get_offer_details',
args: {
offer_id: $btn.data('offer-id')
},
callback: (value) => {
d.set_value("offer_details", value.message);
d.show();
$btn.prop('disabled', false);
}
})
});
});
</script>
{% endif %}

View File

@@ -1,20 +0,0 @@
{% if shopping_cart and shopping_cart.cart_settings.enabled %}
{% set cart_settings = shopping_cart.cart_settings %}
<div class="mt-5 mb-6">
{% if cart_settings.enable_variants | int %}
<button class="btn btn-primary-light btn-configure font-md mr-2"
data-item-code="{{ doc.item_code }}"
data-item-name="{{ doc.web_item_name }}"
>
{{ _('Select Variant') }}
</button>
{% endif %}
{% if cart_settings.show_contact_us_button %}
{% include "templates/generators/item/item_inquiry.html" %}
{% endif %}
</div>
<script>
{% include "templates/generators/item/item_configure.js" %}
</script>
{% endif %}

View File

@@ -1,343 +0,0 @@
class ItemConfigure {
constructor(item_code, item_name) {
this.item_code = item_code;
this.item_name = item_name;
this.get_attributes_and_values()
.then(attribute_data => {
this.attribute_data = attribute_data;
this.show_configure_dialog();
});
}
show_configure_dialog() {
const fields = this.attribute_data.map(a => {
return {
fieldtype: 'Select',
label: a.attribute,
fieldname: a.attribute,
options: a.values.map(v => {
return {
label: v,
value: v
};
}),
change: (e) => {
this.on_attribute_selection(e);
}
};
});
this.dialog = new frappe.ui.Dialog({
title: __('Select Variant for {0}', [this.item_name]),
fields,
on_hide: () => {
set_continue_configuration();
}
});
this.attribute_data.forEach(a => {
const field = this.dialog.get_field(a.attribute);
const $a = $(`<a href>${__("Clear")}</a>`);
$a.on('click', (e) => {
e.preventDefault();
this.dialog.set_value(a.attribute, '');
});
field.$wrapper.find('.help-box').append($a);
});
this.append_status_area();
this.dialog.show();
this.dialog.set_values(JSON.parse(localStorage.getItem(this.get_cache_key())));
$('.btn-configure').prop('disabled', false);
}
on_attribute_selection(e) {
if (e) {
const changed_fieldname = $(e.target).data('fieldname');
this.show_range_input_if_applicable(changed_fieldname);
} else {
this.show_range_input_for_all_fields();
}
const values = this.dialog.get_values();
if (Object.keys(values).length === 0) {
this.clear_status();
localStorage.removeItem(this.get_cache_key());
return;
}
// save state
localStorage.setItem(this.get_cache_key(), JSON.stringify(values));
// show
this.set_loading_status();
this.get_next_attribute_and_values(values)
.then(data => {
const {
valid_options_for_attributes,
} = data;
this.set_item_found_status(data);
for (let attribute in valid_options_for_attributes) {
const valid_options = valid_options_for_attributes[attribute];
const options = this.dialog.get_field(attribute).df.options;
const new_options = options.map(o => {
o.disabled = !valid_options.includes(o.value);
return o;
});
this.dialog.set_df_property(attribute, 'options', new_options);
this.dialog.get_field(attribute).set_options();
}
});
}
show_range_input_for_all_fields() {
this.dialog.fields.forEach(f => {
this.show_range_input_if_applicable(f.fieldname);
});
}
show_range_input_if_applicable(fieldname) {
const changed_field = this.dialog.get_field(fieldname);
const changed_value = changed_field.get_value();
if (changed_value && changed_value.includes(' to ')) {
// possible range input
let numbers = changed_value.split(' to ');
numbers = numbers.map(number => parseFloat(number));
if (!numbers.some(n => isNaN(n))) {
numbers.sort((a, b) => a - b);
if (changed_field.$input_wrapper.find('.range-selector').length) {
return;
}
const parent = $('<div class="range-selector">')
.insertBefore(changed_field.$input_wrapper.find('.help-box'));
const control = frappe.ui.form.make_control({
df: {
fieldtype: 'Int',
label: __('Enter value betweeen {0} and {1}', [numbers[0], numbers[1]]),
change: () => {
const value = control.get_value();
if (value < numbers[0] || value > numbers[1]) {
control.$wrapper.addClass('was-validated');
control.set_description(
__('Value must be between {0} and {1}', [numbers[0], numbers[1]]));
control.$input[0].setCustomValidity('error');
} else {
control.$wrapper.removeClass('was-validated');
control.set_description('');
control.$input[0].setCustomValidity('');
this.update_range_values(fieldname, value);
}
}
},
render_input: true,
parent
});
control.$wrapper.addClass('mt-3');
}
}
}
update_range_values(attribute, range_value) {
this.range_values = this.range_values || {};
this.range_values[attribute] = range_value;
}
show_remaining_optional_attributes() {
// show all attributes if remaining
// unselected attributes are all optional
const unselected_attributes = this.dialog.fields.filter(df => {
const value_selected = this.dialog.get_value(df.fieldname);
return !value_selected;
});
const is_optional_attribute = df => {
const optional_attributes = this.attribute_data
.filter(a => a.optional).map(a => a.attribute);
return optional_attributes.includes(df.fieldname);
};
if (unselected_attributes.every(is_optional_attribute)) {
unselected_attributes.forEach(df => {
this.dialog.fields_dict[df.fieldname].$wrapper.show();
});
}
}
set_loading_status() {
this.dialog.$status_area.html(`
<div class="alert alert-warning d-flex justify-content-between align-items-center" role="alert">
${__('Loading...')}
</div>
`);
}
set_item_found_status(data) {
const html = this.get_html_for_item_found(data);
this.dialog.$status_area.html(html);
}
clear_status() {
this.dialog.$status_area.empty();
}
get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info, available_qty, settings }) {
const one_item = exact_match.length === 1
? exact_match[0]
: filtered_items_count === 1
? filtered_items[0]
: '';
let item_add_to_cart = one_item ? `
<button data-item-code="${one_item}"
class="btn btn-primary btn-add-to-cart w-100"
data-action="btn_add_to_cart"
>
<span class="mr-2">
${frappe.utils.icon('assets', 'md')}
</span>
${__("Add to Cart")}
</button>
` : '';
const items_found = filtered_items_count === 1 ?
__('{0} item found.', [filtered_items_count]) :
__('{0} items found.', [filtered_items_count]);
/* eslint-disable indent */
const item_found_status = exact_match.length === 1
? `<div class="alert alert-success d-flex justify-content-between align-items-center" role="alert">
<div><div>
${one_item}
${product_info && product_info.price && !$.isEmptyObject(product_info.price)
? '(' + product_info.price.formatted_price_sales_uom + ')'
: ''
}
${available_qty === 0 && product_info && product_info?.is_stock_item
? '<span class="text-danger">(' + __('Out of Stock') + ')</span>' : ''}
</div></div>
<a href data-action="btn_clear_values" data-item-code="${one_item}">
${__('Clear Values')}
</a>
</div>`
: `<div class="alert alert-warning d-flex justify-content-between align-items-center" role="alert">
<span>
${items_found}
</span>
<a href data-action="btn_clear_values">
${__('Clear values')}
</a>
</div>`;
/* eslint-disable indent */
if (!product_info?.allow_items_not_in_stock && available_qty === 0
&& product_info && product_info?.is_stock_item) {
item_add_to_cart = '';
}
return `
${item_found_status}
${item_add_to_cart}
`;
}
btn_add_to_cart(e) {
if (frappe.session.user !== 'Guest') {
localStorage.removeItem(this.get_cache_key());
}
const item_code = $(e.currentTarget).data('item-code');
const additional_notes = Object.keys(this.range_values || {}).map(attribute => {
return `${attribute}: ${this.range_values[attribute]}`;
}).join('\n');
erpnext.e_commerce.shopping_cart.update_cart({
item_code,
additional_notes,
qty: 1
});
this.dialog.hide();
}
btn_clear_values() {
this.dialog.fields_list.forEach(f => {
if (f.df?.options) {
f.df.options = f.df.options.map(option => {
option.disabled = false;
return option;
});
}
});
this.dialog.clear();
this.dialog.$status_area.empty();
this.on_attribute_selection();
}
append_status_area() {
this.dialog.$status_area = $('<div class="status-area mt-5">');
this.dialog.$wrapper.find('.modal-body').append(this.dialog.$status_area);
this.dialog.$wrapper.on('click', '[data-action]', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
const action = $target.data('action');
const method = this[action];
method.call(this, e);
});
this.dialog.$wrapper.addClass('item-configurator-dialog');
}
get_next_attribute_and_values(selected_attributes) {
return this.call('erpnext.e_commerce.variant_selector.utils.get_next_attribute_and_values', {
item_code: this.item_code,
selected_attributes
});
}
get_attributes_and_values() {
return this.call('erpnext.e_commerce.variant_selector.utils.get_attributes_and_values', {
item_code: this.item_code
});
}
get_cache_key() {
return `configure:${this.item_code}`;
}
call(method, args) {
// promisified frappe.call
return new Promise((resolve, reject) => {
frappe.call(method, args)
.then(r => resolve(r.message))
.fail(reject);
});
}
}
function set_continue_configuration() {
const $btn_configure = $('.btn-configure');
const { itemCode } = $btn_configure.data();
if (localStorage.getItem(`configure:${itemCode}`)) {
$btn_configure.text(__('Continue Selection'));
} else {
$btn_configure.text(__('Select Variant'));
}
}
frappe.ready(() => {
const $btn_configure = $('.btn-configure');
if (!$btn_configure.length) return;
const { itemCode, itemName } = $btn_configure.data();
set_continue_configuration();
$btn_configure.on('click', () => {
$btn_configure.prop('disabled', true);
new ItemConfigure(itemCode, itemName);
});
});

View File

@@ -1,63 +0,0 @@
{% set width_class = "expand" if not slides else "" %}
{% set cart_settings = shopping_cart.cart_settings %}
{% set product_info = shopping_cart.product_info %}
{% set price_info = product_info.get('price') or {} %}
<div class="col-md-7 product-details {{ width_class }}">
<div class="d-flex">
<!-- title -->
<div class="product-title col-11" itemprop="name">
{{ doc.web_item_name }}
</div>
<!-- Wishlist -->
{% if cart_settings.enable_wishlist %}
<div class="like-action-item-fp like-action {{ 'like-action-wished' if wished else ''}} ml-2"
data-item-code="{{ doc.item_code }}">
<svg class="icon sm">
<use class="{{ 'wished' if wished else 'not-wished' }} wish-icon" href="#icon-heart"></use>
</svg>
</div>
{% endif %}
</div>
<p class="product-code">
<span class="product-item-group">
{{ _(doc.item_group) }}
</span>
<span class="product-item-code">
{{ _("Item Code") }}:
</span>
<span itemprop="productID">{{ doc.item_code }}</span>
</p>
{% if has_variants %}
<!-- configure template -->
{% include "templates/generators/item/item_configure.html" %}
{% else %}
<!-- add variant to cart -->
{% include "templates/generators/item/item_add_to_cart.html" %}
{% endif %}
<!-- description -->
<div class="product-description" itemprop="description">
{% if frappe.utils.strip_html(doc.web_long_description or '') %}
{{ doc.web_long_description | safe }}
{% elif frappe.utils.strip_html(doc.description or '') %}
{{ doc.description | safe }}
{% else %}
{{ "" }}
{% endif %}
</div>
</div>
{% block base_scripts %}
<!-- js should be loaded in body! -->
<script type="text/javascript" src="/assets/frappe/js/lib/jquery/jquery.min.js"></script>
{% endblock %}
<script>
$('.page_content').on('click', '.like-action-item-fp', (e) => {
// Bind action on wishlist button
const $btn = $(e.currentTarget);
erpnext.e_commerce.wishlist.wishlist_action($btn);
});
</script>

View File

@@ -1,108 +0,0 @@
{% set column_size = 5 if slides else 4 %}
<div class="col-md-{{ column_size }} h-100 d-flex mb-4">
{% if slides %}
<div class="item-slideshow d-flex flex-column mr-3">
{% for item in slides %}
<img class="item-slideshow-image mb-2 {% if loop.first %}active{% endif %}"
src="{{ item.image }}" alt="{{ item.heading }}">
{% endfor %}
</div>
{{ product_image(slides[0].image, 'product-image') }}
<!-- Simple image slideshow -->
<script>
frappe.ready(() => {
$('.page_content').on('click', '.item-slideshow-image', (e) => {
const $img = $(e.currentTarget);
const link = $img.prop('src');
const $product_image = $('.product-image');
$product_image.find('a').prop('href', link);
$product_image.find('img').prop('src', link);
$('.item-slideshow-image').removeClass('active');
$img.addClass('active');
});
})
</script>
{% else %}
{{ product_image(doc.website_image, alt=doc.website_image_alt or doc.item_name) }}
{% endif %}
<!-- Simple image preview -->
<div class="image-zoom-view" style="display: none;">
<button type="button" class="close" aria-label="Close">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<style>
.website-image {
cursor: pointer;
}
.image-zoom-view {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.8);
z-index: 1080;
}
.image-zoom-view img {
max-height: 100%;
max-width: 100%;
}
.image-zoom-view button {
position: absolute;
right: 3rem;
top: 2rem;
}
.image-zoom-view svg {
color: var(--white);
}
</style>
<script>
frappe.ready(() => {
const $zoom_wrapper = $('.image-zoom-view');
$('.website-image').on('click', (e) => {
e.preventDefault();
const $img = $(e.target);
const src = $img.prop('src');
if (!src) return;
show_preview(src);
});
$zoom_wrapper.on('click', 'button', hide_preview);
$(document).on('keydown', (e) => {
if (e.key === 'Escape') {
hide_preview();
}
});
function show_preview(src) {
$zoom_wrapper.show();
const $img = $(`<img src="${src}">`)
$zoom_wrapper.append($img);
}
function hide_preview() {
$zoom_wrapper.find('img').remove();
$zoom_wrapper.hide();
}
})
</script>

View File

@@ -1,11 +0,0 @@
{% if shopping_cart and shopping_cart.cart_settings.enabled %}
{% set cart_settings = shopping_cart.cart_settings %}
{% if cart_settings.show_contact_us_button | int %}
<button class="btn btn-inquiry font-md w-30-40" data-item-code="{{ doc.name }}">
{{ _('Contact Us') }}
</button>
{% endif %}
<script>
{% include "templates/generators/item/item_inquiry.js" %}
</script>
{% endif %}

View File

@@ -1,77 +0,0 @@
frappe.ready(() => {
const d = new frappe.ui.Dialog({
title: __('Contact Us'),
fields: [
{
fieldtype: 'Data',
label: __('Full Name'),
fieldname: 'lead_name',
reqd: 1
},
{
fieldtype: 'Data',
label: __('Organization Name'),
fieldname: 'company_name',
},
{
fieldtype: 'Data',
label: __('Email'),
fieldname: 'email_id',
options: 'Email',
reqd: 1
},
{
fieldtype: 'Data',
label: __('Phone Number'),
fieldname: 'phone',
options: 'Phone',
reqd: 1
},
{
fieldtype: 'Data',
label: __('Subject'),
fieldname: 'subject',
reqd: 1
},
{
fieldtype: 'Text',
label: __('Message'),
fieldname: 'message',
reqd: 1
}
],
primary_action: send_inquiry,
primary_action_label: __('Send')
});
function send_inquiry() {
const values = d.get_values();
const doc = Object.assign({}, values);
delete doc.subject;
delete doc.message;
d.hide();
frappe.call('erpnext.e_commerce.shopping_cart.cart.create_lead_for_item_inquiry', {
lead: doc,
subject: values.subject,
message: values.message
}).then(r => {
if (r.message) {
d.clear();
}
});
}
$('.btn-inquiry').click((e) => {
const $btn = $(e.target);
const item_code = $btn.data('item-code');
d.set_value('subject', 'Inquiry about ' + item_code);
if (!['Administrator', 'Guest'].includes(frappe.session.user)) {
d.set_value('email_id', frappe.session.user);
d.set_value('lead_name', frappe.get_cookie('full_name'));
}
d.show();
});
});

View File

@@ -1,88 +0,0 @@
{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %}
<div class="mt-4 ratings-reviews-section">
<!-- Title and Action -->
<div class="w-100 mt-4 mb-2 d-flex">
<div class="reviews-header col-9">
{{ _("Customer Reviews") }}
</div>
<div class="write-a-review-btn col-3">
<!-- Write a Review for legitimate users -->
{% if frappe.session.user != "Guest" and user_is_customer %}
<button class="btn btn-write-review"
data-web-item="{{ doc.name }}">
{{ _("Write a Review") }}
</button>
{% endif %}
</div>
</div>
<!-- Summary -->
{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }}
<!-- Reviews and Comments -->
<div class="mt-8">
{% if reviews %}
{{ user_review(reviews) }}
{% if total_reviews > 4 %}
<div class="mt-6 mb-6"style="color: var(--primary);">
<a href="/customer_reviews?web_item={{ doc.name }}">{{ _("View all reviews") }}</a>
</div>
{% endif %}
{% else %}
<h6 class="text-muted mt-6">
{{ _("No Reviews") }}
</h6>
{% endif %}
</div>
</div>
<script>
frappe.ready(() => {
$('.page_content').on('click', '.btn-write-review', (e) => {
// Bind action on write a review button
const $btn = $(e.currentTarget);
let d = new frappe.ui.Dialog({
title: __("Write a Review"),
fields: [
{fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1},
{fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1},
{fieldtype: "Section Break"},
{fieldname: "comment", fieldtype: "Small Text", label: "Your Review"}
],
primary_action: function() {
var data = d.get_values();
frappe.call({
method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review",
args: {
web_item: "{{ doc.name }}",
title: data.title,
rating: data.rating,
comment: data.comment
},
freeze: true,
freeze_message: __("Submitting Review ..."),
callback: function(r) {
if(!r.exc) {
frappe.msgprint({
message: __("Thank you for the review"),
title: __("Review Submitted"),
indicator: "green"
});
d.hide();
location.reload();
}
}
});
},
primary_action_label: __('Submit')
});
d.show();
});
});
</script>

View File

@@ -1,20 +0,0 @@
<!-- Is reused to render within tabs as well as independently -->
{% if website_specifications %}
<div class="{{ 'mt-2' if not show_tabs else 'mt-5'}} item-website-specification">
<div class="col-md-11">
{% if not show_tabs %}
<div class="product-title mb-5 mt-4">
Product Details
</div>
{% endif %}
<table class="table">
{% for d in website_specifications -%}
<tr>
<td class="spec-label">{{ d.label }}</td>
<td class="spec-content">{{ d.description }}</td>
</tr>
{%- endfor %}
</table>
</div>
</div>
{% endif %}

View File

@@ -1,72 +0,0 @@
{% from "erpnext/templates/includes/macros.html" import field_filter_section, attribute_filter_section, discount_range_filters %}
{% extends "templates/web.html" %}
{% block header %}
<div class="mb-6">{{ _(item_group_name) }}</div>
{% endblock header %}
{% block script %}
<script type="text/javascript" src="/all-products/index.js"></script>
{% endblock %}
{% block breadcrumbs %}
<div class="item-breadcrumbs small text-muted">
{% include "templates/includes/breadcrumbs.html" %}
</div>
{% endblock %}
{% block page_content %}
<div class="item-group-content" itemscope itemtype="http://schema.org/Product"
data-item-group="{{ name }}">
<div class="item-group-slideshow">
{% if slideshow %} <!-- slideshow -->
{{ web_block(
"Hero Slider",
values=slideshow,
add_container=0,
add_top_padding=0,
add_bottom_padding=0,
) }}
{% endif %}
{% if description %} <!-- description -->
<div class="item-group-description text-muted mb-5" itemprop="description">{{ description or ""}}</div>
{% endif %}
</div>
<div class="row">
<div id="product-listing" class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
<!-- Products Rendered in all-products/index.js-->
</div>
<div class="col-12 order-1 col-md-3 order-md-1">
<div class="collapse d-md-block mr-4 filters-section" id="product-filters">
<div class="d-flex justify-content-between align-items-center mb-5 title-section">
<div class="mb-4 filters-title" > {{ _('Filters') }} </div>
<a class="mb-4 clear-filters" href="/{{ doc.route }}">{{ _('Clear All') }}</a>
</div>
<!-- field filters -->
{{ field_filter_section(field_filters) }}
<!-- attribute filters -->
{{ attribute_filter_section(attribute_filters) }}
</div>
</div>
</div>
</div>
<script>
frappe.ready(() => {
$('.btn-prev, .btn-next').click((e) => {
const $btn = $(e.target);
$btn.prop('disabled', true);
const start = $btn.data('start');
let query_params = frappe.utils.get_query_params();
query_params.start = start;
let path = window.location.pathname + '?' + frappe.utils.get_url_from_dict(query_params);
window.location.href = path;
});
});
</script>
{% endblock %}

View File

@@ -1,17 +0,0 @@
<div class="card address-card h-100">
<div class="btn btn-sm btn-default btn-change-address font-md" style="position: absolute; right: 0; top: 0;">
{{ _('Change') }}
</div>
<div class="card-body p-0">
<div class="card-title">{{ address.title }}</div>
<div class="card-text mb-2">
{{ address.display }}
</div>
<a href="/addresses?name={{address.name}}" class="card-link">
<svg class="icon icon-sm">
<use href="#icon-edit"></use>
</svg>
{{ _('Edit') }}
</a>
</div>
</div>

View File

@@ -1,12 +0,0 @@
<div class="card address-card h-100">
<div class="check" style="position: absolute; right: 15px; top: 15px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
<div class="card-body">
<h5 class="card-title">{{ address.title }}</h5>
<p class="card-text text-muted">
{{ address.display }}
</p>
<a href="/addresses?name={{address.name}}" class="card-link">{{ _('Edit') }}</a>
</div>
</div>

View File

@@ -1,189 +0,0 @@
{% from "erpnext/templates/includes/cart/cart_macros.html" import show_address %}
{% if addresses | length == 1%}
{% set select_address = True %}
{% endif %}
<div class="mb-3 frappe-card p-5" data-section="shipping-address">
<div class="d-flex">
<div class="col-6 address-header"><h6>{{ _("Shipping Address") }}</h6></div>
<div class="col-6" style="padding: 0;">
<a class="ml-4 btn-new-address" role="button">{{ _("Add a new address") }}</a>
</div>
</div>
<hr>
{% for address in shipping_addresses %}
{% if doc.shipping_address_name == address.name %}
<div class="row no-gutters" data-fieldname="shipping_address_name">
<div class="w-100 address-container" data-address-name="{{address.name}}" data-address-type="shipping" data-active>
{% include "templates/includes/cart/address_card.html" %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
<!-- Billing Address -->
<div class="checkbox ml-1 mb-2">
<label for="input_same_billing">
<input type="checkbox" class="product-filter" id="input_same_billing" checked style="width: 14px !important">
<span class="label-area font-md">{{ _('Billing Address is same as Shipping Address') }}</span>
</label>
</div>
{% if billing_addresses %}
<div class="mb-3 frappe-card p-5" data-section="billing-address">
<div class="d-flex">
<div class="col-6 address-header"><h6>{{ _("Billing Address") }}</h6></div>
<div class="col-6" style="padding: 0;">
<a class="ml-4 btn-new-address" role="button">{{ _("Add a new address") }}</a>
</div>
</div>
<hr>
{% for address in billing_addresses %}
{% if doc.customer_address == address.name %}
<div class="row no-gutters" data-fieldname="customer_address">
<div class="w-100 address-container" data-address-name="{{address.name}}" data-address-type="billing" data-active>
{% include "templates/includes/cart/address_card.html" %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
<script>
frappe.boot = {{ boot }}
frappe.ready(() => {
$(document).on('click', '.address-card', (e) => {
const $target = $(e.currentTarget);
const $section = $target.closest('[data-section]');
$section.find('.address-card').removeClass('active');
$target.addClass('active');
});
$('#input_same_billing').change((e) => {
const $check = $(e.target);
toggle_billing_address_section(!$check.is(':checked'));
});
$('.btn-new-address').click(() => {
const d = new frappe.ui.Dialog({
title: __('New Address'),
fields: [
{
label: __('Address Title'),
fieldname: 'address_title',
fieldtype: 'Data',
reqd: 1
},
{
label: __('Address Line 1'),
fieldname: 'address_line1',
fieldtype: 'Data',
reqd: 1
},
{
label: __('Address Line 2'),
fieldname: 'address_line2',
fieldtype: 'Data'
},
{
label: __('City/Town'),
fieldname: 'city',
fieldtype: 'Data',
reqd: 1
},
{
label: __('State'),
fieldname: 'state',
fieldtype: 'Data'
},
{
label: __('Country'),
fieldname: 'country',
fieldtype: 'Link',
options: 'Country',
only_select: true,
reqd: 1
},
{
fieldname: "column_break0",
fieldtype: "Column Break",
width: "50%"
},
{
label: __('Address Type'),
fieldname: 'address_type',
fieldtype: 'Select',
options: [
'Billing',
'Shipping'
],
reqd: 1
},
{
label: __('Postal Code'),
fieldname: 'pincode',
fieldtype: 'Data'
},
{
fieldname: "phone",
fieldtype: "Data",
label: "Phone",
reqd: 1
},
],
primary_action_label: __('Save'),
primary_action: (values) => {
frappe.call('erpnext.e_commerce.shopping_cart.cart.add_new_address', { doc: values })
.then(r => {
frappe.call({
method: "erpnext.e_commerce.shopping_cart.cart.update_cart_address",
args: {
address_type: r.message.address_type,
address_name: r.message.name
},
callback: function (r) {
d.hide();
window.location.reload();
}
});
});
}
})
d.show();
});
function setup_state() {
const shipping_address = $('[data-section="shipping-address"]')
.find('[data-address-name][data-active]').attr('data-address-name');
const billing_address = $('[data-section="billing-address"]')
.find('[data-address-name][data-active]').attr('data-address-name');
$('#input_same_billing').prop('checked', shipping_address === billing_address).trigger('change');
if (!shipping_address && !billing_address) {
$('#input_same_billing').prop('checked', true).trigger('change');
}
if (shipping_address) {
$(`[data-section="shipping-address"] [data-address-name="${shipping_address}"] .address-card`).addClass('active');
}
if (billing_address) {
$(`[data-section="billing-address"] [data-address-name="${billing_address}"] .address-card`).addClass('active');
}
}
setup_state();
function toggle_billing_address_section(flag) {
$('[data-section="billing-address"]').toggle(flag);
}
});
</script>

View File

@@ -1,3 +0,0 @@
<div class="mb-3 frappe-card p-5" data-section="shipping-address">
<h6>{{ _("Shipping Address") }}</h6>
</div>

View File

@@ -1,27 +0,0 @@
<div class="cart-dropdown-container">
<div id="cart-error" class="alert alert-danger"
style="display: none;"></div>
<div class="row checkout-btn">
<div class="col-sm-12 col-xs-12">
<a href="/cart" class="btn btn-block btn-primary">{{ _("Checkout") }}</a>
</div>
</div>
<div class="row cart-items-dropdown cart-item-header text-muted">
<div class="col-sm-6 col-xs-6 h6 text-uppercase">
{{ _("Item") }}
</div>
<div class="col-sm-6 col-xs-6 text-right h6 text-uppercase">
{{ _("Price") }}
</div>
</div>
{% if doc.items %}
<div class="row cart-items-dropdown">
<div class="col-sm-12 col-xs-12">
{% include "templates/includes/cart/cart_items_dropdown.html" %}
</div>
</div>
{% else %}
<p>{{ _("Cart is Empty") }}</p>
{% endif %}
</div>

View File

@@ -1,113 +0,0 @@
{% from "erpnext/templates/includes/macros.html" import product_image %}
{% macro item_subtotal(item) %}
<div>
{{ item.get_formatted('amount') }}
</div>
{% if item.is_free_item %}
<div class="text-success mt-4">
<span class="free-tag">
{{ _('FREE') }}
</span>
</div>
{% else %}
<span class="item-rate">
{{ _('Rate:') }} {{ item.get_formatted('rate') }}
</span>
{% endif %}
{% endmacro %}
{% for d in doc.items %}
<tr data-name="{{ d.name }}">
<td style="width: 60%;">
<div class="d-flex">
<div class="cart-item-image mr-4">
{% if d.thumbnail %}
{{ product_image(d.thumbnail, alt="d.web_item_name", no_border=True) }}
{% else %}
<div class = "no-image-cart-item">
{{ frappe.utils.get_abbr(d.web_item_name) or "NA" }}
</div>
{% endif %}
</div>
<div class="d-flex w-100" style="flex-direction: column;">
<div class="item-title mb-1 mr-3">
{{ d.get("web_item_name") or 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('Website Item', {'item_code': variant_of}, 'route') or '#'}}">
{{ 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>
</div>
</div>
</td>
<!-- Qty column -->
<td class="text-right" style="width: 25%;">
<div class="d-flex">
{% set disabled = 'disabled' if d.is_free_item else '' %}
<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" {{ disabled }}>
{{ '' if not d.is_free_item else ''}}
</button>
</span>
<input class="form-control text-center cart-qty" value="{{ d.get_formatted('qty') }}" data-item-code="{{ d.item_code }}"
style="max-width: 70px;" {{ disabled }}>
<span class="input-group-append d-sm-inline-block">
<button class="btn cart-btn" data-dir="up" {{ disabled }}>
{{ '+' if not d.is_free_item else ''}}
</button>
</span>
</div>
<div>
{% if not d.is_free_item %}
<div class="remove-cart-item column-sm-view d-flex" 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 %}
</div>
</div>
<!-- Shown on mobile view, else hidden -->
{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
<div class="text-right sm-item-subtotal">
{{ item_subtotal(d) }}
</div>
{% endif %}
</td>
<!-- Subtotal column -->
{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
<td class="text-right item-subtotal column-sm-view w-100">
{{ item_subtotal(d) }}
</td>
{% endif %}
</tr>
{% endfor %}

View File

@@ -1,12 +0,0 @@
{% from "erpnext/templates/includes/order/order_macros.html" import item_name_and_description_cart %}
{% for d in doc.items %}
<div class="row cart-dropdown">
<div class="col-sm-8 col-xs-8 col-name-description">
{{ item_name_and_description_cart(d) }}
</div>
<div class="col-sm-4 col-xs-4 text-right col-amount">
{{ d.get_formatted("amount") }}
</div>
</div>
{% endfor %}

View File

@@ -1,10 +0,0 @@
<!-- Total at the end of the cart items -->
<tr>
<th></th>
<th class="text-left item-grand-total" colspan="1">
{{ _("Total") }}
</th>
<th class="text-left item-grand-total totals" colspan="3">
{{ doc.get_formatted("total") }}
</th>
</tr>

View File

@@ -1,22 +0,0 @@
{% macro show_address(address, doc, fieldname, select_address=False) %}
{% set selected=address.name==doc.get(fieldname) %}
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-sm-10 address-title"
data-address-name="{{ address.name }}">
<strong>{{ address.name }}</strong></div>
<div class="col-sm-2 text-right">
<input type="checkbox"
data-fieldname="{{ fieldname }}"
data-address-name="{{ address.name}}"
{{ "checked" if selected else "" }}></div>
</div>
</div>
<div class="panel-collapse"
data-address-name="{{ address.name }}">
<div class="panel-body text-muted small">{{ address.display }}</div>
</div>
</div>
{% endmacro %}

View File

@@ -1,84 +0,0 @@
<!-- Payment -->
{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
<h6>
{{ _("Payment Summary") }}
</h6>
{% endif %}
<div class="card h-100">
<div class="card-body p-0">
{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
<table class="table w-100">
<tr>
{% set total_items = frappe.utils.cstr(frappe.utils.flt(doc.total_qty, 0)) %}
<td class="bill-label">{{ _("Net Total (") + total_items + _(" Items)") }}</td>
<td class="bill-content net-total text-right">{{ doc.get_formatted("net_total") }}</td>
</tr>
<!-- taxes -->
{% for d in doc.taxes %}
{% if d.base_tax_amount %}
<tr>
<td class="bill-label">
{{ d.description }}
</td>
<td class="bill-content text-right">
{{ d.get_formatted("base_tax_amount") }}
</td>
</tr>
{% endif %}
{% endfor %}
</table>
<!-- TODO: Apply Coupon Dialog-->
<!-- {% set show_coupon_code = cart_settings.show_apply_coupon_code_in_website and cart_settings.enable_checkout %}
{% if show_coupon_code %}
<button class="btn btn-coupon-code w-100 text-left">
<svg width="24" height="24" viewBox="0 0 24 24" stroke="var(--gray-600)" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 15.6213C19 15.2235 19.158 14.842 19.4393 14.5607L20.9393 13.0607C21.5251 12.4749 21.5251 11.5251 20.9393 10.9393L19.4393 9.43934C19.158 9.15804 19 8.7765 19 8.37868V6.5C19 5.67157 18.3284 5 17.5 5H15.6213C15.2235 5 14.842 4.84196 14.5607 4.56066L13.0607 3.06066C12.4749 2.47487 11.5251 2.47487 10.9393 3.06066L9.43934 4.56066C9.15804 4.84196 8.7765 5 8.37868 5H6.5C5.67157 5 5 5.67157 5 6.5V8.37868C5 8.7765 4.84196 9.15804 4.56066 9.43934L3.06066 10.9393C2.47487 11.5251 2.47487 12.4749 3.06066 13.0607L4.56066 14.5607C4.84196 14.842 5 15.2235 5 15.6213V17.5C5 18.3284 5.67157 19 6.5 19H8.37868C8.7765 19 9.15804 19.158 9.43934 19.4393L10.9393 20.9393C11.5251 21.5251 12.4749 21.5251 13.0607 20.9393L14.5607 19.4393C14.842 19.158 15.2235 19 15.6213 19H17.5C18.3284 19 19 18.3284 19 17.5V15.6213Z" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 9L9 15" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 9.5C10.5 10.0523 10.0523 10.5 9.5 10.5C8.94772 10.5 8.5 10.0523 8.5 9.5C8.5 8.94772 8.94772 8.5 9.5 8.5C10.0523 8.5 10.5 8.94772 10.5 9.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5 14.5C15.5 15.0523 15.0523 15.5 14.5 15.5C13.9477 15.5 13.5 15.0523 13.5 14.5C13.5 13.9477 13.9477 13.5 14.5 13.5C15.0523 13.5 15.5 13.9477 15.5 14.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="ml-2">Apply Coupon</span>
</button>
{% endif %} -->
<table class="table w-100 grand-total mt-6">
<tr>
<td class="bill-content net-total">{{ _("Grand Total") }}</td>
<td class="bill-content net-total text-right">{{ doc.get_formatted("grand_total") }}</td>
</tr>
</table>
{% endif %}
{% if cart_settings.enable_checkout %}
<button class="btn btn-primary btn-place-order font-md w-100" type="button">
{{ _('Place Order') }}
</button>
{% else %}
<button class="btn btn-primary btn-request-for-quotation font-md w-100" type="button">
{{ _('Request for Quote') }}
</button>
{% endif %}
</div>
</div>
<!-- TODO: Apply Coupon Dialog-->
<!-- <script>
frappe.ready(() => {
$('.btn-coupon-code').click((e) => {
const $btn = $(e.currentTarget);
const d = new frappe.ui.Dialog({
title: __('Coupons'),
fields: [
{
fieldname: 'coupons_area',
fieldtype: 'HTML'
}
]
});
d.show();
});
});
</script> -->

View File

@@ -1,22 +0,0 @@
{% extends 'frappe/templates/includes/navbar/navbar_items.html' %}
{% block navbar_right_extension %}
<li class="shopping-cart cart-icon hidden">
<a class="nav-link" href="/cart">
<svg class="icon icon-lg">
<use href="#icon-assets"></use>
</svg>
<span class="badge badge-primary shopping-badge" id="cart-count"></span>
</a>
</li>
{% if frappe.db.get_single_value("E Commerce Settings", "enable_wishlist") %}
<li class="wishlist wishlist-icon hidden">
<a class="nav-link" href="/wishlist">
<svg class="icon icon-lg">
<use href="#icon-heart"></use>
</svg>
<span class="badge badge-primary shopping-badge" id="wish-count"></span>
</a>
</li>
{% endif %}
{% endblock %}

View File

@@ -1,52 +0,0 @@
{% from "erpnext/templates/includes/macros.html" import product_image %}
{% macro item_name_and_description(d) %}
<div class="row item_name_and_description">
<div class="col-xs-4 col-sm-2 order-image-col">
<div class="order-image h-100">
{% if d.thumbnail or d.image %}
{{ product_image(d.thumbnail or d.image, no_border=True) }}
{% else %}
<div class="no-image-cart-item" style="min-height: 100px;">
{{ frappe.utils.get_abbr(d.item_name) or "NA" }}
</div>
{% endif %}
</div>
</div>
<div class="col-xs-8 col-sm-10">
{{ d.item_code }}
<div class="text-muted small item-description">
{{ html2text(d.description) | truncate(140) }}
</div>
<span class="text-muted mt-2 d-l-n order-qty">
{{ _("Qty ") }}({{ d.get_formatted("qty") }})
</span>
</div>
</div>
{% endmacro %}
{% macro item_name_and_description_cart(d) %}
<div class="row item_name_dropdown">
<div class="col-xs-4 col-sm-4 order-image-col">
<div class="order-image">
{{ product_image_square(d.thumbnail or d.image) }}
</div>
</div>
<div class="col-xs-8 col-sm-8">
{{ d.item_name|truncate(25) }}
<div class="input-group number-spinner">
<span class="input-group-btn">
<button class="btn btn-light cart-btn" data-dir="dwn">
</button>
</span>
<input class="form-control text-right cart-qty"
value = "{{ d.get_formatted('qty') }}"
data-item-code="{{ d.item_code }}">
<span class="input-group-btn">
<button class="btn btn-light cart-btn" data-dir="up">
+</button>
</span>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -1,217 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.ready(function() {
window.item_code = $('[itemscope] [itemprop="productID"]').text().trim();
var qty = 0;
frappe.call({
type: "POST",
method: "erpnext.e_commerce.shopping_cart.product_info.get_product_info_for_website",
args: {
item_code: get_item_code()
},
callback: function(r) {
if(r.message) {
if(r.message.cart_settings.enabled) {
let hide_add_to_cart = !r.message.product_info.price
|| (!r.message.product_info.in_stock && !r.message.cart_settings.allow_items_not_in_stock);
$(".item-cart, .item-price, .item-stock").toggleClass('hide', hide_add_to_cart);
}
if(r.message.cart_settings.show_price) {
$(".item-price").toggleClass("hide", false);
}
if(r.message.cart_settings.show_stock_availability) {
$(".item-stock").toggleClass("hide", false);
}
if(r.message.product_info.price) {
$(".item-price")
.html(r.message.product_info.price.formatted_price_sales_uom + "<div style='font-size: small'>\
(" + r.message.product_info.price.formatted_price + " / " + r.message.product_info.uom + ")</div>");
if(r.message.product_info.in_stock===0) {
$(".item-stock").html("<div style='color: red'> <i class='fa fa-close'></i> {{ _("Not in stock") }}</div>");
}
else if(r.message.product_info.in_stock===1 && r.message.cart_settings.show_stock_availability) {
var qty_display = "{{ _("In stock") }}";
if (r.message.product_info.show_stock_qty) {
qty_display += " ("+r.message.product_info.stock_qty+")";
}
$(".item-stock").html("<div style='color: green'>\
<i class='fa fa-check'></i> "+qty_display+"</div>");
}
if(r.message.product_info.qty) {
qty = r.message.product_info.qty;
toggle_update_cart(r.message.product_info.qty);
} else {
toggle_update_cart(0);
}
}
}
}
})
$("#item-add-to-cart button").on("click", function() {
frappe.provide('erpnext.shopping_cart');
erpnext.shopping_cart.update_cart({
item_code: get_item_code(),
qty: $("#item-spinner .cart-qty").val(),
callback: function(r) {
if(!r.exc) {
toggle_update_cart(1);
qty = 1;
}
},
btn: this,
});
});
$("#item-spinner").on('click', '.number-spinner button', function () {
var btn = $(this),
input = btn.closest('.number-spinner').find('input'),
oldValue = input.val().trim(),
newVal = 0;
if (btn.attr('data-dir') == 'up') {
newVal = Number.parseInt(oldValue) + 1;
} else if (btn.attr('data-dir') == 'dwn') {
if (Number.parseInt(oldValue) > 1) {
newVal = Number.parseInt(oldValue) - 1;
}
else {
newVal = Number.parseInt(oldValue);
}
}
input.val(newVal);
});
$("[itemscope] .item-view-attribute .form-control").on("change", function() {
try {
var item_code = encodeURIComponent(get_item_code());
} catch(e) {
// unable to find variant
// then chose the closest available one
var attribute = $(this).attr("data-attribute");
var attribute_value = $(this).val();
var item_code = find_closest_match(attribute, attribute_value);
if (!item_code) {
frappe.msgprint(__("Cannot find a matching Item. Please select some other value for {0}.", [attribute]))
throw e;
}
}
if (window.location.search == ("?variant=" + item_code) || window.location.search.includes(item_code)) {
return;
}
window.location.href = window.location.pathname + "?variant=" + item_code;
});
// change the item image src when alternate images are hovered
$(document.body).on('mouseover', '.item-alternative-image', (e) => {
const $alternative_image = $(e.currentTarget);
const src = $alternative_image.find('img').prop('src');
$('.item-image img').prop('src', src);
});
});
var toggle_update_cart = function(qty) {
$("#item-add-to-cart").toggle(qty ? false : true);
$("#item-update-cart")
.toggle(qty ? true : false)
.find("input").val(qty);
$("#item-spinner").toggle(qty ? false : true);
}
function get_item_code() {
var variant_info = window.variant_info;
if(variant_info) {
var attributes = get_selected_attributes();
var no_of_attributes = Object.keys(attributes).length;
for(var i in variant_info) {
var variant = variant_info[i];
if (variant.attributes.length < no_of_attributes) {
// the case when variant has less attributes than template
continue;
}
var match = true;
for(var j in variant.attributes) {
if(attributes[variant.attributes[j].attribute]
!= variant.attributes[j].attribute_value
) {
match = false;
break;
}
}
if(match) {
return variant.name;
}
}
throw "Unable to match variant";
} else {
return window.item_code;
}
}
function find_closest_match(selected_attribute, selected_attribute_value) {
// find the closest match keeping the selected attribute in focus and get the item code
var attributes = get_selected_attributes();
var previous_match_score = 0;
var previous_no_of_attributes = 0;
var matched;
var variant_info = window.variant_info;
for(var i in variant_info) {
var variant = variant_info[i];
var match_score = 0;
var has_selected_attribute = false;
for(var j in variant.attributes) {
if(attributes[variant.attributes[j].attribute]===variant.attributes[j].attribute_value) {
match_score = match_score + 1;
if (variant.attributes[j].attribute==selected_attribute && variant.attributes[j].attribute_value==selected_attribute_value) {
has_selected_attribute = true;
}
}
}
if (has_selected_attribute
&& ((match_score > previous_match_score) || (match_score==previous_match_score && previous_no_of_attributes < variant.attributes.length))) {
previous_match_score = match_score;
matched = variant;
previous_no_of_attributes = variant.attributes.length;
}
}
if (matched) {
for (var j in matched.attributes) {
var attr = matched.attributes[j];
$('[itemscope]')
.find(repl('.item-view-attribute .form-control[data-attribute="%(attribute)s"]', attr))
.val(attr.attribute_value);
}
return matched.name;
}
}
function get_selected_attributes() {
var attributes = {};
$('[itemscope]').find(".item-view-attribute .form-control").each(function() {
attributes[$(this).attr('data-attribute')] = $(this).val();
});
return attributes;
}

View File

@@ -1,132 +0,0 @@
{% extends "templates/web.html" %}
{% block title %} {{ _("Shopping Cart") }} {% endblock %}
{% block header %}<h3 class="shopping-cart-header mt-2 mb-6">{{ _("Shopping Cart") }}</h1>{% endblock %}
{% block header_actions %}
{% endblock %}
{% block page_content %}
{% from "templates/includes/macros.html" import item_name_and_description %}
{% if doc.items %}
<div class="cart-container">
<div class="row m-0">
<!-- Left section -->
<div class="col-md-8">
<div class="frappe-card p-5 mb-4">
<div id="cart-error" class="alert alert-danger" style="display: none;"></div>
<div class="cart-items-header">
{{ _('Items') }}
</div>
<table class="table mt-3 cart-table">
<thead>
<tr>
<th class="item-column">{{ _('Item') }}</th>
<th width="20%">{{ _('Quantity') }}</th>
{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
<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">
{% include "templates/includes/cart/cart_items.html" %}
</tbody>
{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
<tfoot class="cart-tax-items">
{% include "templates/includes/cart/cart_items_total.html" %}
</tfoot>
{% endif %}
</table>
<div class="row mt-2">
<div class="col-3">
{% if cart_settings.enable_checkout %}
<a class="btn btn-primary-light font-md" href="/orders">
{{ _('Past Orders') }}
</a>
{% else %}
<a class="btn btn-primary-light font-md" href="/quotations">
{{ _('Past Quotes') }}
</a>
{% endif %}
</div>
<div class="col-9">
{% if doc.items %}
<div class="place-order-container">
<a class="btn btn-primary-light mr-2 font-md" href="/all-products">
{{ _('Continue Shopping') }}
</a>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Terms and Conditions -->
{% if doc.items %}
{% if doc.terms %}
<div class="t-and-c-container mt-4 frappe-card">
<h5>{{ _("Terms and Conditions") }}</h5>
<div class="t-and-c-terms mt-2">
{{ doc.terms }}
</div>
</div>
{% endif %}
</div>
<!-- Right section -->
<div class="col-md-4">
<div class="cart-payment-addresses">
<!-- Apply Coupon Code -->
{% set show_coupon_code = cart_settings.show_apply_coupon_code_in_website and cart_settings.enable_checkout %}
{% if show_coupon_code == 1%}
<div class="mb-3">
<div class="row no-gutters">
<input type="text" class="txtcoupon form-control mr-3 w-50 font-md" placeholder="Enter Coupon Code" name="txtcouponcode" ></input>
<button class="btn btn-primary btn-sm bt-coupon font-md">{{ _("Apply Coupon Code") }}</button>
<input type="hidden" class="txtreferral_sales_partner font-md" placeholder="Enter Sales Partner" name="txtreferral_sales_partner" type="text"></input>
</div>
</div>
{% endif %}
<div class="mb-3 frappe-card p-5 payment-summary">
{% include "templates/includes/cart/cart_payment_summary.html" %}
</div>
{% include "templates/includes/cart/cart_address.html" %}
</div>
</div>
{% endif %}
</div>
</div>
{% else %}
<div class="cart-empty frappe-card">
<div class="cart-empty-state">
<img src="/assets/erpnext/images/ui-states/cart-empty-state.png" alt="Empty State">
</div>
<div class="cart-empty-message mt-4">{{ _('Your cart is Empty') }}</p>
{% if cart_settings.enable_checkout %}
<a class="btn btn-outline-primary" href="/orders" style="font-size: 16px;">
{{ _('See past orders') }}
</a>
{% else %}
<a class="btn btn-outline-primary" href="/quotations" style="font-size: 16px;">
{{ _('See past quotations') }}
</a>
{% endif %}
</div>
{% endif %}
{% endblock %}
{% block base_scripts %}
<!-- js should be loaded in body! -->
{{ include_script("frappe-web.bundle.js") }}
{{ include_script("controls.bundle.js") }}
{{ include_script("dialog.bundle.js") }}
{% endblock %}

View File

@@ -1,303 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// JS exclusive to /cart page
frappe.provide("erpnext.e_commerce.shopping_cart");
var shopping_cart = erpnext.e_commerce.shopping_cart;
$.extend(shopping_cart, {
show_error: function(title, text) {
$("#cart-container").html('<div class="msg-box"><h4>' +
title + '</h4><p class="text-muted">' + text + '</p></div>');
},
bind_events: function() {
shopping_cart.bind_address_picker_dialog();
shopping_cart.bind_place_order();
shopping_cart.bind_request_quotation();
shopping_cart.bind_change_qty();
shopping_cart.bind_remove_cart_item();
shopping_cart.bind_change_notes();
shopping_cart.bind_coupon_code();
},
bind_address_picker_dialog: function() {
const d = this.get_update_address_dialog();
this.parent.find('.btn-change-address').on('click', (e) => {
const type = $(e.currentTarget).parents('.address-container').attr('data-address-type');
$(d.get_field('address_picker').wrapper).html(
this.get_address_template(type)
);
d.show();
});
},
get_update_address_dialog() {
let d = new frappe.ui.Dialog({
title: "Select Address",
fields: [{
'fieldtype': 'HTML',
'fieldname': 'address_picker',
}],
primary_action_label: __('Set Address'),
primary_action: () => {
const $card = d.$wrapper.find('.address-card.active');
const address_type = $card.closest('[data-address-type]').attr('data-address-type');
const address_name = $card.closest('[data-address-name]').attr('data-address-name');
frappe.call({
type: "POST",
method: "erpnext.e_commerce.shopping_cart.cart.update_cart_address",
freeze: true,
args: {
address_type,
address_name
},
callback: function(r) {
d.hide();
if (!r.exc) {
$(".cart-tax-items").html(r.message.total);
shopping_cart.parent.find(
`.address-container[data-address-type="${address_type}"]`
).html(r.message.address);
}
}
});
}
});
return d;
},
get_address_template(type) {
return {
shipping: `<div class="mb-3" data-section="shipping-address">
<div class="row no-gutters" data-fieldname="shipping_address_name">
{% for address in shipping_addresses %}
<div class="mr-3 mb-3 w-100" data-address-name="{{address.name}}" data-address-type="shipping"
{% if doc.shipping_address_name == address.name %} data-active {% endif %}>
{% include "templates/includes/cart/address_picker_card.html" %}
</div>
{% endfor %}
</div>
</div>`,
billing: `<div class="mb-3" data-section="billing-address">
<div class="row no-gutters" data-fieldname="customer_address">
{% for address in billing_addresses %}
<div class="mr-3 mb-3 w-100" data-address-name="{{address.name}}" data-address-type="billing"
{% if doc.shipping_address_name == address.name %} data-active {% endif %}>
{% include "templates/includes/cart/address_picker_card.html" %}
</div>
{% endfor %}
</div>
</div>`,
}[type];
},
bind_place_order: function() {
$(".btn-place-order").on("click", function() {
shopping_cart.place_order(this);
});
},
bind_request_quotation: function() {
$('.btn-request-for-quotation').on('click', function() {
shopping_cart.request_quotation(this);
});
},
bind_change_qty: function() {
// bind update button
$(".cart-items").on("change", ".cart-qty", function() {
var item_code = $(this).attr("data-item-code");
var newVal = $(this).val();
shopping_cart.shopping_cart_update({item_code, qty: newVal});
});
$(".cart-items").on('click', '.number-spinner button', function () {
var btn = $(this),
input = btn.closest('.number-spinner').find('input'),
oldValue = input.val().trim(),
newVal = 0;
if (btn.attr('data-dir') == 'up') {
newVal = parseInt(oldValue) + 1;
} else {
if (oldValue > 1) {
newVal = parseInt(oldValue) - 1;
}
}
input.val(newVal);
let notes = input.closest("td").siblings().find(".notes").text().trim();
var item_code = input.attr("data-item-code");
shopping_cart.shopping_cart_update({
item_code,
qty: newVal,
additional_notes: notes
});
});
},
bind_change_notes: function() {
$('.cart-items').on('change', 'textarea', function() {
const $textarea = $(this);
const item_code = $textarea.attr('data-item-code');
const qty = $textarea.closest('tr').find('.cart-qty').val();
const notes = $textarea.val();
shopping_cart.shopping_cart_update({
item_code,
qty,
additional_notes: notes
});
});
},
bind_remove_cart_item: function() {
$(".cart-items").on("click", ".remove-cart-item", (e) => {
const $remove_cart_item_btn = $(e.currentTarget);
var item_code = $remove_cart_item_btn.data("item-code");
shopping_cart.shopping_cart_update({
item_code: item_code,
qty: 0
});
});
},
render_tax_row: function($cart_taxes, doc, shipping_rules) {
var shipping_selector;
if(shipping_rules) {
shipping_selector = '<select class="form-control">' + $.map(shipping_rules, function(rule) {
return '<option value="' + rule[0] + '">' + rule[1] + '</option>' }).join("\n") +
'</select>';
}
var $tax_row = $(repl('<div class="row">\
<div class="col-md-9 col-sm-9">\
<div class="row">\
<div class="col-md-9 col-md-offset-3">' +
(shipping_selector || '<p>%(description)s</p>') +
'</div>\
</div>\
</div>\
<div class="col-md-3 col-sm-3 text-right">\
<p' + (shipping_selector ? ' style="margin-top: 5px;"' : "") + '>%(formatted_tax_amount)s</p>\
</div>\
</div>', doc)).appendTo($cart_taxes);
if(shipping_selector) {
$tax_row.find('select option').each(function(i, opt) {
if($(opt).html() == doc.description) {
$(opt).attr("selected", "selected");
}
});
$tax_row.find('select').on("change", function() {
shopping_cart.apply_shipping_rule($(this).val(), this);
});
}
},
apply_shipping_rule: function(rule, btn) {
return frappe.call({
btn: btn,
type: "POST",
method: "erpnext.e_commerce.shopping_cart.cart.apply_shipping_rule",
args: { shipping_rule: rule },
callback: function(r) {
if(!r.exc) {
shopping_cart.render(r.message);
}
}
});
},
place_order: function(btn) {
shopping_cart.freeze();
return frappe.call({
type: "POST",
method: "erpnext.e_commerce.shopping_cart.cart.place_order",
btn: btn,
callback: function(r) {
if(r.exc) {
shopping_cart.unfreeze();
var msg = "";
if(r._server_messages) {
msg = JSON.parse(r._server_messages || []).join("<br>");
}
$("#cart-error")
.empty()
.html(msg || frappe._("Something went wrong!"))
.toggle(true);
} else {
$(btn).hide();
window.location.href = '/orders/' + encodeURIComponent(r.message);
}
}
});
},
request_quotation: function(btn) {
shopping_cart.freeze();
return frappe.call({
type: "POST",
method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation",
btn: btn,
callback: function(r) {
if(r.exc) {
shopping_cart.unfreeze();
var msg = "";
if(r._server_messages) {
msg = JSON.parse(r._server_messages || []).join("<br>");
}
$("#cart-error")
.empty()
.html(msg || frappe._("Something went wrong!"))
.toggle(true);
} else {
$(btn).hide();
window.location.href = '/quotations/' + encodeURIComponent(r.message);
}
}
});
},
bind_coupon_code: function() {
$(".bt-coupon").on("click", function() {
shopping_cart.apply_coupon_code(this);
});
},
apply_coupon_code: function(btn) {
return frappe.call({
type: "POST",
method: "erpnext.e_commerce.shopping_cart.cart.apply_coupon_code",
btn: btn,
args : {
applied_code : $('.txtcoupon').val(),
applied_referral_sales_partner: $('.txtreferral_sales_partner').val()
},
callback: function(r) {
if (r && r.message){
location.reload();
}
}
});
}
});
frappe.ready(function() {
if (window.location.pathname === "/cart") {
$(".cart-icon").hide();
}
shopping_cart.parent = $(".cart-container");
shopping_cart.bind_events();
});
function show_terms() {
var html = $(".cart-terms").html();
frappe.msgprint(html);
}

View File

@@ -1,11 +0,0 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
no_cache = 1
from erpnext.e_commerce.shopping_cart.cart import get_cart_quotation
def get_context(context):
context.body_class = "product-page"
context.update(get_cart_quotation())

View File

@@ -1,67 +0,0 @@
{% extends "templates/web.html" %}
{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %}
{% block title %} {{ _("Customer Reviews") }} {% endblock %}
{% block page_content %}
<div class="product-container reviews-full-page col-md-12">
{% if enable_reviews %}
<!-- Title and Action -->
<div class="w-100 mb-6 d-flex">
<div class="reviews-header col-9">
{{ _("Customer Reviews") }}
</div>
<div class="write-a-review-btn col-3">
<!-- Write a Review for legitimate users -->
{% if frappe.session.user != "Guest" and user_is_customer %}
<button class="btn btn-write-review"
data-web-item="{{ web_item }}">
{{ _("Write a Review") }}
</button>
{% endif %}
</div>
</div>
<!-- Summary -->
{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }}
<!-- Reviews and Comments -->
<div class="mt-8">
{% if reviews %}
{{ user_review(reviews) }}
{% if not reviews | len >= total_reviews %}
<button class="btn btn-light btn-view-more mr-2 mt-4 mb-4 w-30"
data-web-item="{{ web_item }}">
{{ _("View More") }}
</button>
{% endif %}
{% else %}
<h6 class="text-muted mt-6">
{{ _("No Reviews") }}
</h6>
{% endif %}
</div>
{% else %}
<!-- If reviews are disabled -->
<div class="text-center">
<h3 class="text-muted mt-8">
{{ _("No Reviews") }}
</h3>
</div>
{% endif %}
</div>
{% endblock %}
{% block base_scripts %}
<!-- js should be loaded in body! -->
<script type="text/javascript" src="/assets/frappe/js/lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/assets/js/frappe-web.min.js"></script>
<script type="text/javascript" src="/assets/js/control.min.js"></script>
<script type="text/javascript" src="/assets/js/dialog.min.js"></script>
<script type="text/javascript" src="/assets/js/bootstrap-4-web.min.js"></script>
{% endblock %}

View File

@@ -1,25 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
)
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
from erpnext.e_commerce.doctype.website_item.website_item import check_if_user_is_customer
def get_context(context):
context.body_class = "product-page"
context.no_cache = 1
context.full_page = True
context.reviews = None
if frappe.form_dict and frappe.form_dict.get("web_item"):
context.web_item = frappe.form_dict.get("web_item")
context.user_is_customer = check_if_user_is_customer()
context.enable_reviews = get_shopping_cart_settings().enable_reviews
if context.enable_reviews:
reviews_data = get_item_reviews(context.web_item)
context.update(reviews_data)

View File

@@ -17,9 +17,6 @@
<h3 class="d-block d-sm-none">{{ homepage.description }}</h3>
</div>
<div class="container">
<a href="{{ explore_link }}" class="mb-5 btn btn-primary">{{ _('Explore') }}</a>
</div>
</section>
{% elif homepage.hero_section_based_on == 'Slideshow' and slideshow %}
<section class="hero-section">
@@ -29,26 +26,6 @@
{{ render_homepage_section(homepage.hero_section_doc) }}
{% endif %}
{% if homepage.products %}
<section class="container section-products my-5">
<h3>{{ _('Products') }}</h3>
<div class="row">
{% for item in homepage.products %}
<div class="col-md-4 mb-4">
<div class="card h-100 justify-content-between">
<img class="card-img-top website-image-extra-large" src="{{ item.image }}" loading="lazy" alt="{{ item.item_name }}"></img>
<div class="card-body flex-grow-0">
<h5 class="card-title">{{ item.item_name }}</h5>
<a href="{{ item.route }}" class="card-link">{{ _('More details') }}</a>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
{% if blogs %}
<section class="container my-5">
<h3>{{ _('Publications') }}</h3>

View File

@@ -10,11 +10,6 @@ no_cache = 1
def get_context(context):
homepage = frappe.get_cached_doc("Homepage")
for item in homepage.products:
route = frappe.db.get_value("Website Item", {"item_code": item.item_code}, "route")
if route:
item.route = "/" + route
homepage.title = homepage.title or homepage.company
context.title = homepage.title
context.homepage = homepage
@@ -52,5 +47,3 @@ def get_context(context):
context.metatags = context.metatags or frappe._dict({})
context.metatags.image = homepage.hero_image or None
context.metatags.description = homepage.description or None
context.explore_link = "/all-products"

View File

@@ -1,42 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ready(function(){
var loyalty_points_input = document.getElementById("loyalty-point-to-redeem");
var loyalty_points_status = document.getElementById("loyalty-points-status");
if (loyalty_points_input) {
loyalty_points_input.onblur = apply_loyalty_points;
}
function apply_loyalty_points() {
var loyalty_points = parseInt(loyalty_points_input.value);
if (loyalty_points) {
frappe.call({
method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_redeemption_factor",
args: {
"customer": doc_info.customer
},
callback: function(r) {
if (r) {
var message = ""
let loyalty_amount = flt(r.message*loyalty_points);
if (doc_info.grand_total && doc_info.grand_total < loyalty_amount) {
let redeemable_amount = parseInt(doc_info.grand_total/r.message);
message = "You can only redeem max " + redeemable_amount + " points in this order.";
frappe.msgprint(__(message));
} else {
message = loyalty_points + " Loyalty Points of amount "+ loyalty_amount + " is applied."
frappe.msgprint(__(message));
var remaining_amount = flt(doc_info.grand_total) - flt(loyalty_amount);
var payment_button = document.getElementById("pay-for-order");
payment_button.innerHTML = __("Pay Remaining");
payment_button.href = "/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn="+doc_info.doctype_name+"&dt="+doc_info.doctype+"&loyalty_points="+loyalty_points+"&submit_doc=1&order_type=Shopping Cart";
}
loyalty_points_status.innerHTML = message;
}
}
});
}
}
})

View File

@@ -4,8 +4,6 @@
import frappe
from frappe import _
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import show_attachments
def get_context(context):
context.no_cache = 1
@@ -14,17 +12,12 @@ def get_context(context):
if hasattr(context.doc, "set_indicator"):
context.doc.set_indicator()
if show_attachments():
context.attachments = get_attachments(frappe.form_dict.doctype, frappe.form_dict.name)
context.parents = frappe.form_dict.parents
context.title = frappe.form_dict.name
context.payment_ref = frappe.db.get_value(
"Payment Request", {"reference_name": frappe.form_dict.name}, "name"
)
context.enabled_checkout = frappe.get_doc("E Commerce Settings").enable_checkout
default_print_format = frappe.db.get_value(
"Property Setter",
dict(property="default_print_format", doc_type=frappe.form_dict.doctype),

View File

@@ -1,32 +0,0 @@
{% extends "templates/web.html" %}
{% block title %} {{ _("Product Search") }} {% endblock %}
{% block header %}<h2>{{ _("Product Search") }}</h2>{% endblock %}
{% block page_content %}
<script>{% include "templates/includes/product_list.js" %}</script>
<script>
frappe.ready(function() {
var txt = frappe.utils.get_url_arg("search");
$(".search-results").html('{{ _("Search results for") + ": " + html2text(frappe.form_dict.search or "") | e | trim }}');
window.search = txt;
window.start = 0;
window.get_product_list();
});
</script>
<div class="product-search-content">
<h3 class="search-results">{{ _("Search Results") }}</h3>
<div id="search-list" class="row">
</div>
<div style="text-align: center;">
<div class="more-btn"
style="display: none; text-align: center;">
<button class="btn btn-light">{{ _("More...") }}</button>
</div>
</div>
</div>
{% endblock %}

View File

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

View File

@@ -1,28 +0,0 @@
{% extends "templates/web.html" %}
{% block title %} {{ _("Wishlist") }} {% endblock %}
{% block header %}<h3 class="shopping-cart-header mt-2 mb-6">{{ _("Wishlist") }}</h1>{% endblock %}
{% block page_content %}
{% if items %}
<div class="row">
<div class="col-md-12 item-card-group-section">
<div class="row products-list">
{% from "erpnext/templates/includes/macros.html" import wishlist_card %}
{% for item in items %}
{{ wishlist_card(item, settings) }}
{% endfor %}
</div>
</div>
</div>
{% else %}
<div class="cart-empty frappe-card">
<div class="cart-empty-state">
<img src="/assets/erpnext/images/ui-states/cart-empty-state.png" alt="Empty Cart">
</div>
<div class="cart-empty-message mt-4">{{ _('Wishlist is empty!') }}</p>
</div>
{% endif %}
{% endblock %}

View File

@@ -1,81 +0,0 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
)
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
from erpnext.utilities.product import get_price
def get_context(context):
is_guest = frappe.session.user == "Guest"
settings = get_shopping_cart_settings()
items = get_wishlist_items() if not is_guest else []
selling_price_list = _set_price_list(settings) if not is_guest else None
items = set_stock_price_details(items, settings, selling_price_list)
context.body_class = "product-page"
context.items = items
context.settings = settings
context.no_cache = 1
def get_stock_availability(item_code, warehouse):
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1:
warehouses = get_child_warehouses(warehouse)
else:
warehouses = [warehouse] if warehouse else []
stock_qty = 0.0
for warehouse in warehouses:
stock_qty += frappe.utils.flt(
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty")
)
return bool(stock_qty)
def get_wishlist_items():
if not frappe.db.exists("Wishlist", frappe.session.user):
return []
return frappe.db.get_all(
"Wishlist Item",
filters={"parent": frappe.session.user},
fields=[
"web_item_name",
"item_code",
"item_name",
"website_item",
"warehouse",
"image",
"item_group",
"route",
],
)
def set_stock_price_details(items, settings, selling_price_list):
for item in items:
if settings.show_stock_availability:
item.available = get_stock_availability(item.item_code, item.get("warehouse"))
price_details = get_price(
item.item_code, selling_price_list, settings.default_customer_group, settings.company
)
if price_details:
item.formatted_price = price_details.get("formatted_price")
item.formatted_mrp = price_details.get("formatted_mrp")
if item.formatted_mrp:
item.discount = price_details.get("formatted_discount_percent") or price_details.get(
"formatted_discount_rate"
)
return items