Merge branch 'shopping-cart' of https://github.com/frappe/erpnext into rebrand-ui
This commit is contained in:
commit
9f1a156307
@ -17,6 +17,7 @@ class ProductsSettings(Document):
|
||||
|
||||
self.validate_field_filters()
|
||||
self.validate_attribute_filters()
|
||||
frappe.clear_document_cache("Product Settings")
|
||||
|
||||
def validate_field_filters(self):
|
||||
if not (self.enable_field_filters and self.filter_fields): return
|
||||
|
@ -1,6 +1,7 @@
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
|
||||
from erpnext.shopping_cart.product_info import get_product_info_for_website
|
||||
|
||||
def get_field_filter_data():
|
||||
product_settings = get_product_settings()
|
||||
@ -356,10 +357,10 @@ def get_items(filters=None, search=None):
|
||||
|
||||
results = frappe.db.sql('''
|
||||
SELECT
|
||||
`tabItem`.`name`, `tabItem`.`item_name`,
|
||||
`tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`,
|
||||
`tabItem`.`website_image`, `tabItem`.`image`,
|
||||
`tabItem`.`web_long_description`, `tabItem`.`description`,
|
||||
`tabItem`.`route`
|
||||
`tabItem`.`route`, `tabItem`.`item_group`
|
||||
FROM
|
||||
`tabItem`
|
||||
{left_join}
|
||||
@ -384,6 +385,8 @@ def get_items(filters=None, search=None):
|
||||
for r in results:
|
||||
r.description = r.web_long_description or r.description
|
||||
r.image = r.website_image or r.image
|
||||
product_info = get_product_info_for_website(r.item_code, skip_quotation_creation=True).get('product_info')
|
||||
r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
|
||||
|
||||
return results
|
||||
|
||||
|
@ -13,7 +13,8 @@
|
||||
"public/js/shopping_cart.js"
|
||||
],
|
||||
"css/erpnext-web.css": [
|
||||
"public/scss/website.scss"
|
||||
"public/scss/website.scss",
|
||||
"public/scss/shopping_cart.scss"
|
||||
],
|
||||
"js/marketplace.min.js": [
|
||||
"public/js/hub/marketplace.js"
|
||||
|
BIN
erpnext/public/images/ui-states/cart-empty-state.png
Normal file
BIN
erpnext/public/images/ui-states/cart-empty-state.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
490
erpnext/public/scss/shopping_cart.scss
Normal file
490
erpnext/public/scss/shopping_cart.scss
Normal file
@ -0,0 +1,490 @@
|
||||
@import "frappe/public/scss/desk/variables";
|
||||
@import "frappe/public/scss/common/mixins";
|
||||
|
||||
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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.08), 0px 1px 2px 1px rgba(0, 0, 0, 0.06);
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.carousel-control-prev,
|
||||
.carousel-control-next {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.carousel-body {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.carousel-content {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.product-category-section {
|
||||
.card:hover {
|
||||
box-shadow: 0px 16px 45px 6px rgba(0, 0, 0, 0.08), 0px 8px 10px -10px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-gap: 15px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));;
|
||||
}
|
||||
}
|
||||
|
||||
.item-card-group-section {
|
||||
.card {
|
||||
height: 360px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04);
|
||||
transition: box-shadow 400ms;
|
||||
}
|
||||
}
|
||||
|
||||
// .card-body {
|
||||
// text-align: center;
|
||||
// }
|
||||
|
||||
// .featured-item {
|
||||
// .card-body {
|
||||
// text-align: left;
|
||||
// }
|
||||
// }
|
||||
|
||||
.card-img {
|
||||
max-height: 210px;
|
||||
object-fit: contain;
|
||||
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);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
font-size: 12px;
|
||||
color: var(--text-color);
|
||||
margin: 20px 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 6;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.product-category {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin: var(--margin-sm) 0;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin: var(--margin-sm) 0;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
padding: var(--padding-sm);
|
||||
}
|
||||
}
|
||||
|
||||
[data-doctype="Item Group"],
|
||||
#page-all-products {
|
||||
.page-header {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.filters-section {
|
||||
.title-section {
|
||||
border-bottom: 1px solid var(--table-border-color);
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.clear-filters {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.filter-block {
|
||||
border-bottom: 1px solid var(--table-border-color);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
.label-area {
|
||||
font-size: 13px;
|
||||
color: var(--gray-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-container {
|
||||
@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;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.product-code {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
font-size: 13px;
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
.product-image {
|
||||
border-color: var(--table-border-color) !important;
|
||||
padding: 15px;
|
||||
|
||||
@include media-breakpoint-between(xs, md) {
|
||||
height: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
height: 350px;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.item-slideshow {
|
||||
@include media-breakpoint-between(xs, md) {
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
max-height: 430px;
|
||||
}
|
||||
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.item-slideshow-image {
|
||||
height: 4rem;
|
||||
width: 6rem;
|
||||
object-fit: contain;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--table-border-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &.active {
|
||||
border-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.item-cart {
|
||||
.product-price {
|
||||
font-size: 20px;
|
||||
color: var(--text-color);
|
||||
font-weight: 600;
|
||||
|
||||
.formatted-price {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
}
|
||||
|
||||
.no-stock {
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-configurator-dialog {
|
||||
.modal-header {
|
||||
padding: var(--padding-md) var(--padding-xl);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-group-slideshow {
|
||||
.item-group-description {
|
||||
// max-width: 900px;
|
||||
}
|
||||
|
||||
.carousel-inner.rounded-carousel {
|
||||
border-radius: $card-border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,10 @@
|
||||
@import "frappe/public/scss/website/variables";
|
||||
|
||||
.product-image img {
|
||||
min-height: 20rem;
|
||||
max-height: 30rem;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.item-slideshow-image {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
object-fit: contain;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &.active {
|
||||
border-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.address-card {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
@ -43,10 +24,10 @@
|
||||
|
||||
.check {
|
||||
display: inline-flex;
|
||||
padding: 0.25rem;
|
||||
background: $primary;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 0.25rem;
|
||||
background: $primary;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
@ -61,6 +61,19 @@ frappe.ui.form.on("Item Group", {
|
||||
frappe.set_route("List", "Item", {"item_group": frm.doc.name});
|
||||
});
|
||||
}
|
||||
|
||||
frappe.model.with_doctype('Item', () => {
|
||||
const item_meta = frappe.get_meta('Item');
|
||||
|
||||
const valid_fields = item_meta.fields.filter(
|
||||
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
|
||||
).map(df => ({ label: df.label, value: df.fieldname }));
|
||||
|
||||
const field = frappe.meta.get_docfield("Website Filter Field", "fieldname", frm.docname);
|
||||
field.fieldtype = 'Select';
|
||||
field.options = valid_fields;
|
||||
frm.fields_dict.filter_fields.grid.refresh();
|
||||
});
|
||||
},
|
||||
|
||||
set_root_readonly: function(frm) {
|
||||
|
@ -24,8 +24,12 @@
|
||||
"route",
|
||||
"weightage",
|
||||
"slideshow",
|
||||
"website_title",
|
||||
"description",
|
||||
"website_specifications",
|
||||
"website_filters_section",
|
||||
"filter_fields",
|
||||
"filter_attributes",
|
||||
"lft",
|
||||
"rgt",
|
||||
"old_parent"
|
||||
@ -180,6 +184,28 @@
|
||||
"options": "Item Group",
|
||||
"print_hide": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "website_filters_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Website Filters"
|
||||
},
|
||||
{
|
||||
"fieldname": "filter_fields",
|
||||
"fieldtype": "Table",
|
||||
"label": "Item Fields",
|
||||
"options": "Website Filter Field"
|
||||
},
|
||||
{
|
||||
"fieldname": "filter_attributes",
|
||||
"fieldtype": "Table",
|
||||
"label": "Attributes",
|
||||
"options": "Website Attribute"
|
||||
},
|
||||
{
|
||||
"fieldname": "website_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-sitemap",
|
||||
@ -188,7 +214,7 @@
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"max_attachments": 3,
|
||||
"modified": "2020-03-18 18:10:34.383363",
|
||||
"modified": "2020-12-30 12:57:38.876956",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Item Group",
|
||||
|
@ -13,13 +13,16 @@ from frappe.website.doctype.website_slideshow.website_slideshow import get_slide
|
||||
from erpnext.shopping_cart.product_info import set_product_info_for_website
|
||||
from erpnext.utilities.product import get_qty_in_stock
|
||||
from six.moves.urllib.parse import quote
|
||||
from erpnext.shopping_cart.product_query import ProductQuery
|
||||
from erpnext.shopping_cart.filters import ProductFiltersBuilder
|
||||
|
||||
class ItemGroup(NestedSet, WebsiteGenerator):
|
||||
nsm_parent_field = 'parent_item_group'
|
||||
website = frappe._dict(
|
||||
condition_field = "show_in_website",
|
||||
template = "templates/generators/item_group.html",
|
||||
no_cache = 1
|
||||
no_cache = 1,
|
||||
no_breadcrumbs = 1
|
||||
)
|
||||
|
||||
def autoname(self):
|
||||
@ -70,18 +73,58 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
||||
context.page_length = cint(frappe.db.get_single_value('Products Settings', 'products_per_page')) or 6
|
||||
context.search_link = '/product_search'
|
||||
|
||||
start = int(frappe.form_dict.start or 0)
|
||||
if start < 0:
|
||||
if frappe.form_dict:
|
||||
search = frappe.form_dict.search
|
||||
field_filters = frappe.parse_json(frappe.form_dict.field_filters)
|
||||
attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
|
||||
start = frappe.parse_json(frappe.form_dict.start)
|
||||
else:
|
||||
search = None
|
||||
attribute_filters = None
|
||||
field_filters = {}
|
||||
start = 0
|
||||
|
||||
if not field_filters:
|
||||
field_filters = {}
|
||||
|
||||
# Ensure the query remains within current item group
|
||||
field_filters['item_group'] = self.name
|
||||
|
||||
engine = ProductQuery()
|
||||
context.items = engine.query(attribute_filters, field_filters, search, start)
|
||||
|
||||
filter_engine = ProductFiltersBuilder(self.name)
|
||||
|
||||
context.field_filters = filter_engine.get_field_filters()
|
||||
context.attribute_filters = filter_engine.get_attribute_fitlers()
|
||||
|
||||
context.update({
|
||||
"items": get_product_list_for_group(product_group = self.name, start=start,
|
||||
limit=context.page_length + 1, search=frappe.form_dict.get("search")),
|
||||
"parents": get_parent_item_groups(self.parent_item_group),
|
||||
"title": self.name
|
||||
})
|
||||
|
||||
if self.slideshow:
|
||||
context.update(get_slideshow(self))
|
||||
values = {
|
||||
'show_indicators': 1,
|
||||
'show_controls': 0,
|
||||
'rounded': 1,
|
||||
'slider_name': self.slideshow
|
||||
}
|
||||
slideshow = frappe.get_doc("Website Slideshow", self.slideshow)
|
||||
slides = slideshow.get({"doctype":"Website Slideshow Item"})
|
||||
for index, slide in enumerate(slides):
|
||||
values[f"slide_{index + 1}_image"] = slide.image
|
||||
values[f"slide_{index + 1}_title"] = slide.heading
|
||||
values[f"slide_{index + 1}_subtitle"] = slide.description
|
||||
values[f"slide_{index + 1}_theme"] = slide.theme or "Light"
|
||||
values[f"slide_{index + 1}_content_align"] = slide.content_align or "Centre"
|
||||
values[f"slide_{index + 1}_primary_action_label"] = slide.label
|
||||
values[f"slide_{index + 1}_primary_action"] = slide.url
|
||||
|
||||
context.slideshow = values
|
||||
|
||||
context.breadcrumbs = 0
|
||||
context.title = self.website_title or self.name
|
||||
|
||||
return context
|
||||
|
||||
|
@ -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
|
||||
|
82
erpnext/shopping_cart/filters.py
Normal file
82
erpnext/shopping_cart/filters.py
Normal file
@ -0,0 +1,82 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _dict
|
||||
|
||||
class ProductFiltersBuilder:
|
||||
def __init__(self, item_group=None):
|
||||
if not item_group or item_group == "Products Settings":
|
||||
self.doc = frappe.get_doc("Products Settings")
|
||||
else:
|
||||
self.doc = frappe.get_doc("Item Group", item_group)
|
||||
|
||||
self.item_group = item_group
|
||||
|
||||
def get_field_filters(self):
|
||||
filter_fields = [row.fieldname for row in self.doc.filter_fields]
|
||||
|
||||
meta = frappe.get_meta('Item')
|
||||
fields = [df for df in meta.fields if df.fieldname in filter_fields]
|
||||
|
||||
filter_data = []
|
||||
for df in fields:
|
||||
filters = {}
|
||||
if df.fieldtype == "Link":
|
||||
if self.item_group:
|
||||
filters['item_group'] = self.item_group
|
||||
|
||||
values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname)
|
||||
else:
|
||||
doctype = df.get_link_doctype()
|
||||
|
||||
# apply enable/disable/show_in_website filter
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
if meta.has_field('enabled'):
|
||||
filters['enabled'] = 1
|
||||
if meta.has_field('disabled'):
|
||||
filters['disabled'] = 0
|
||||
if meta.has_field('show_in_website'):
|
||||
filters['show_in_website'] = 1
|
||||
|
||||
values = [d.name for d in frappe.get_all(doctype, filters)]
|
||||
|
||||
# Remove None
|
||||
values = values.remove(None) if None in values else values
|
||||
if values:
|
||||
filter_data.append([df, values])
|
||||
|
||||
return filter_data
|
||||
|
||||
def get_attribute_fitlers(self):
|
||||
attributes = [row.attribute for row in self.doc.filter_attributes]
|
||||
attribute_docs = [
|
||||
frappe.get_doc('Item Attribute', attribute) for attribute in attributes
|
||||
]
|
||||
|
||||
valid_attributes = []
|
||||
|
||||
for attr_doc in attribute_docs:
|
||||
selected_attributes = []
|
||||
for attr in attr_doc.item_attribute_values:
|
||||
filters= [
|
||||
["Item Variant Attribute", "attribute", "=", attr.parent],
|
||||
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
|
||||
]
|
||||
if self.item_group:
|
||||
filters.append(["item_group", "=", self.item_group])
|
||||
|
||||
if frappe.db.get_all("Item", filters, limit=1):
|
||||
selected_attributes.append(attr)
|
||||
|
||||
if selected_attributes:
|
||||
valid_attributes.append(
|
||||
_dict(
|
||||
item_attribute_values=selected_attributes,
|
||||
name=attr_doc.name
|
||||
)
|
||||
)
|
||||
|
||||
return valid_attributes
|
120
erpnext/shopping_cart/product_query.py
Normal file
120
erpnext/shopping_cart/product_query.py
Normal file
@ -0,0 +1,120 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from erpnext.shopping_cart.product_info import get_product_info_for_website
|
||||
|
||||
class ProductQuery:
|
||||
"""Query engine for product listing
|
||||
|
||||
Attributes:
|
||||
cart_settings (Document): Settings for Cart
|
||||
fields (list): Fields to fetch in query
|
||||
filters (TYPE): Description
|
||||
or_filters (list): Description
|
||||
page_length (Int): Length of page for the query
|
||||
settings (Document): Products Settings DocType
|
||||
filters (list)
|
||||
or_filters (list)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.settings = frappe.get_doc("Products Settings")
|
||||
self.cart_settings = frappe.get_doc("Shopping Cart Settings")
|
||||
self.page_length = self.settings.products_per_page or 20
|
||||
self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route']
|
||||
self.filters = [['show_in_website', '=', 1]]
|
||||
self.or_filters = []
|
||||
|
||||
def query(self, attributes=None, fields=None, search_term=None, start=0):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
attributes (dict, optional): Item Attribute filters
|
||||
fields (dict, optional): Field level filters
|
||||
search_term (str, optional): Search term to lookup
|
||||
start (int, optional): Page start
|
||||
|
||||
Returns:
|
||||
list: List of results with set fields
|
||||
"""
|
||||
if fields: self.build_fields_filters(fields)
|
||||
if search_term: self.build_search_filters(search_term)
|
||||
|
||||
result = []
|
||||
|
||||
if attributes:
|
||||
all_items = []
|
||||
for attribute, values in attributes.items():
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
|
||||
items = frappe.get_all(
|
||||
"Item",
|
||||
fields=self.fields,
|
||||
filters=[
|
||||
*self.filters,
|
||||
["Item Variant Attribute", "attribute", "=", attribute],
|
||||
["Item Variant Attribute", "attribute_value", "in", values],
|
||||
],
|
||||
or_filters=self.or_filters,
|
||||
start=start,
|
||||
limit=self.page_length
|
||||
)
|
||||
|
||||
items_dict = {item.name: item for item in items}
|
||||
# TODO: Replace Variants by their parent templates
|
||||
|
||||
all_items.append(set(items_dict.keys()))
|
||||
|
||||
result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
|
||||
else:
|
||||
result = frappe.get_all("Item", fields=self.fields, filters=self.filters, or_filters=self.or_filters, start=start, limit=self.page_length)
|
||||
|
||||
for item in result:
|
||||
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
|
||||
item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
|
||||
|
||||
return result
|
||||
|
||||
def build_fields_filters(self, filters):
|
||||
"""Build filters for field values
|
||||
|
||||
Args:
|
||||
filters (dict): Filters
|
||||
"""
|
||||
for field, values in filters.items():
|
||||
if not values:
|
||||
continue
|
||||
|
||||
if isinstance(values, list):
|
||||
# If value is a list use `IN` query
|
||||
self.filters.append([field, 'IN', values])
|
||||
else:
|
||||
# `=` will be faster than `IN` for most cases
|
||||
self.filters.append([field, '=', values])
|
||||
|
||||
def build_search_filters(self, search_term):
|
||||
"""Query search term in specified fields
|
||||
|
||||
Args:
|
||||
search_term (str): Search candidate
|
||||
"""
|
||||
# Default fields to search from
|
||||
default_fields = {'name', 'item_name', 'description', 'item_group'}
|
||||
|
||||
# Get meta search fields
|
||||
meta = frappe.get_meta("Item")
|
||||
meta_fields = set(meta.get_search_fields())
|
||||
|
||||
# Join the meta fields and default fields set
|
||||
search_fields = default_fields.union(meta_fields)
|
||||
try:
|
||||
if frappe.db.count('Item', cache=True) > 50000:
|
||||
search_fields.remove('description')
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Build or filters for query
|
||||
search = '%{}%'.format(search_term)
|
||||
self.or_filters += [[field, 'like', search] for field in search_fields]
|
127
erpnext/shopping_cart/search.py
Normal file
127
erpnext/shopping_cart/search.py
Normal file
@ -0,0 +1,127 @@
|
||||
import frappe
|
||||
from frappe.search.full_text_search import FullTextSearch
|
||||
from whoosh.fields import TEXT, ID, KEYWORD, Schema
|
||||
from frappe.website.render import render_page
|
||||
from frappe.utils import strip_html_tags
|
||||
from whoosh.qparser import MultifieldParser, FieldsPlugin, WildcardPlugin
|
||||
from whoosh.analysis import StemmingAnalyzer
|
||||
from whoosh.query import Prefix
|
||||
|
||||
INDEX_NAME = "products"
|
||||
|
||||
class ProductSearch(FullTextSearch):
|
||||
""" Wrapper for WebsiteSearch """
|
||||
|
||||
def get_schema(self):
|
||||
return Schema(
|
||||
title=TEXT(stored=True, field_boost=1.5),
|
||||
name=ID(stored=True),
|
||||
path=ID(stored=True),
|
||||
content=TEXT(stored=True, analyzer=StemmingAnalyzer()),
|
||||
keywords=KEYWORD(stored=True, scorable=True, commas=True),
|
||||
)
|
||||
|
||||
def get_id(self):
|
||||
return "name"
|
||||
|
||||
def get_items_to_index(self):
|
||||
"""Get all routes to be indexed, this includes the static pages
|
||||
in www/ and routes from published documents
|
||||
|
||||
Returns:
|
||||
self (object): FullTextSearch Instance
|
||||
"""
|
||||
items = get_all_published_items()
|
||||
documents = [self.get_document_to_index(item) for item in items]
|
||||
return documents
|
||||
|
||||
def get_document_to_index(self, item):
|
||||
try:
|
||||
item = frappe.get_doc("Item", item)
|
||||
title = item.item_name
|
||||
keywords = [item.item_group]
|
||||
|
||||
if item.brand:
|
||||
keywords.append(item.brand)
|
||||
|
||||
if item.website_image_alt:
|
||||
keywords.append(item.website_image_alt)
|
||||
|
||||
if item.has_variants and item.variant_based_on == "Item Attribute":
|
||||
keywords = keywords + [attr.attribute for attr in item.attributes]
|
||||
|
||||
if item.web_long_description:
|
||||
content = strip_html_tags(item.web_long_description)
|
||||
elif description:
|
||||
content = strip_html_tags(item.description)
|
||||
|
||||
return frappe._dict(
|
||||
title=title,
|
||||
name=item.name,
|
||||
path=item.route,
|
||||
content=content,
|
||||
keywords=", ".join(keywords),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def search(self, text, scope=None, limit=20):
|
||||
"""Search from the current index
|
||||
|
||||
Args:
|
||||
text (str): String to search for
|
||||
scope (str, optional): Scope to limit the search. Defaults to None.
|
||||
limit (int, optional): Limit number of search results. Defaults to 20.
|
||||
|
||||
Returns:
|
||||
[List(_dict)]: Search results
|
||||
"""
|
||||
ix = self.get_index()
|
||||
|
||||
results = None
|
||||
out = []
|
||||
|
||||
with ix.searcher() as searcher:
|
||||
parser = MultifieldParser(["title", "content", "keywords"], ix.schema)
|
||||
parser.remove_plugin_class(FieldsPlugin)
|
||||
parser.remove_plugin_class(WildcardPlugin)
|
||||
query = parser.parse(text)
|
||||
|
||||
filter_scoped = None
|
||||
if scope:
|
||||
filter_scoped = Prefix(self.id, scope)
|
||||
results = searcher.search(query, limit=limit, filter=filter_scoped)
|
||||
|
||||
for r in results:
|
||||
out.append(self.parse_result(r))
|
||||
|
||||
return out
|
||||
|
||||
def parse_result(self, result):
|
||||
title_highlights = result.highlights("title")
|
||||
content_highlights = result.highlights("content")
|
||||
keyword_highlights = result.highlights("keywords")
|
||||
|
||||
return frappe._dict(
|
||||
title=result["title"],
|
||||
path=result["path"],
|
||||
keywords=result["keywords"],
|
||||
title_highlights=title_highlights,
|
||||
content_highlights=content_highlights,
|
||||
keyword_highlights=keyword_highlights,
|
||||
)
|
||||
|
||||
def get_all_published_items():
|
||||
return frappe.get_all("Item", filters={"variant_of": "", "show_in_website": 1},pluck="name")
|
||||
|
||||
def update_index_for_path(path):
|
||||
search = ProductSearch(INDEX_NAME)
|
||||
return search.update_index_by_name(path)
|
||||
|
||||
def remove_document_from_index(path):
|
||||
search = ProductSearch(INDEX_NAME)
|
||||
return search.remove_document_from_index(path)
|
||||
|
||||
def build_index_for_all_routes():
|
||||
search = ProductSearch(INDEX_NAME)
|
||||
return search.build()
|
0
erpnext/shopping_cart/web_template/__init__.py
Normal file
0
erpnext/shopping_cart/web_template/__init__.py
Normal file
@ -0,0 +1,85 @@
|
||||
{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%}
|
||||
{%- set align_class = resolve_class({
|
||||
'text-right': align == 'Right',
|
||||
'text-centre': align == 'Center',
|
||||
'text-left': align == 'Left',
|
||||
}) -%}
|
||||
|
||||
{%- set heading_class = resolve_class({
|
||||
'text-white': theme == 'Dark',
|
||||
'': theme == 'Light',
|
||||
}) -%}
|
||||
<div class="carousel-item {{ 'active' if index=='1' else ''}}" style="height: 450px;">
|
||||
<img class="d-block h-100 w-100" style="object-fit: cover;" src="{{ image }}" alt="{{ title }}">
|
||||
{%- if title or subtitle -%}
|
||||
<div class="carousel-body container d-flex {{ align_class }}">
|
||||
<div class="carousel-content align-self-center">
|
||||
{%- if title -%}<h1 class="{{ heading_class }}">{{ title }}</h1>{%- endif -%}
|
||||
{%- if subtitle -%}<p class="text-muted mt-2">{{ subtitle }}</p>{%- endif -%}
|
||||
{%- if action -%}
|
||||
<a href="{{ action }}" class="btn btn-primary mt-3">
|
||||
{{ label }}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{%- endmacro -%}
|
||||
|
||||
<div id="{{ slider_name }}" class="section-carousel carousel slide" data-ride="carousel">
|
||||
{%- if show_indicators -%}
|
||||
<ol class="carousel-indicators">
|
||||
{%- for index in ['1', '2', '3', '4', '5'] -%}
|
||||
{%- if values['slide_' + index + '_image'] -%}
|
||||
<li data-target="#{{ slider_name }}" data-slide-to="{{ frappe.utils.cint(index) - 1 }}" class="{{ 'active' if index=='1' else ''}}"></li>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</ol>
|
||||
{%- endif -%}
|
||||
<div class="carousel-inner {{ resolve_class({'rounded-carousel': rounded }) }}">
|
||||
{%- for index in ['1', '2', '3', '4', '5'] -%}
|
||||
{%- set image = values['slide_' + index + '_image'] -%}
|
||||
{%- set title = values['slide_' + index + '_title'] -%}
|
||||
{%- set subtitle = values['slide_' + index + '_subtitle'] -%}
|
||||
{%- set primary_action = values['slide_' + index + '_primary_action'] -%}
|
||||
{%- set primary_action_label = values['slide_' + index + '_primary_action_label'] -%}
|
||||
{%- set align = values['slide_' + index + '_content_align'] -%}
|
||||
{%- set theme = values['slide_' + index + '_theme'] -%}
|
||||
|
||||
{%- if image -%}
|
||||
{{ slide(image, title, subtitle, primary_action, primary_action_label, index, align, theme) }}
|
||||
{%- endif -%}
|
||||
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
{%- if show_controls -%}
|
||||
<a class="carousel-control-prev" href="#{{ slider_name }}" role="button" data-slide="prev">
|
||||
<div class="carousel-control">
|
||||
<svg class="mr-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.625 3.75L6.375 9L11.625 14.25" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#{{ slider_name }}" role="button" data-slide="next">
|
||||
<div class="carousel-control">
|
||||
<svg class="ml-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.375 14.25L11.625 9L6.375 3.75" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
$('.carousel').carousel({
|
||||
interval: false,
|
||||
pause: "hover",
|
||||
wrap: true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
284
erpnext/shopping_cart/web_template/hero_slider/hero_slider.json
Normal file
284
erpnext/shopping_cart/web_template/hero_slider/hero_slider.json
Normal file
@ -0,0 +1,284 @@
|
||||
{
|
||||
"creation": "2020-11-17 15:21:51.207221",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "slider_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Slider Name",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "show_indicators",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Indicators",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "show_controls",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Controls",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Slide 1",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_theme",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Theme",
|
||||
"options": "Dark\nLight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Slide 2",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image ",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title ",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Subtitle ",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label ",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action ",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"default": "Left",
|
||||
"fieldname": "slide_2_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_theme",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Theme",
|
||||
"options": "Dark\nLight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Slide 3",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_theme",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Theme",
|
||||
"options": "Dark\nLight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Slide 4",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_theme",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Theme",
|
||||
"options": "Dark\nLight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Slide 5",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_theme",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Theme",
|
||||
"options": "Dark\nLight",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 2,
|
||||
"modified": "2020-12-29 12:30:02.794994",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Shopping Cart",
|
||||
"name": "Hero Slider",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Section"
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
|
||||
|
||||
<div class="section-with-cards item-card-group-section">
|
||||
<div class="item-group-header d-flex justify-content-between">
|
||||
<div class="title-section">
|
||||
{%- if title -%}
|
||||
<h2 class="section-title">{{ title }}</h2>
|
||||
{%- endif -%}
|
||||
{%- if subtitle -%}
|
||||
<p class="section-description">{{ subtitle }}</p>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="primary-action-section">
|
||||
{%- if primary_action -%}
|
||||
<a href="{{ action }}" class="btn btn-primary pull-right">
|
||||
{{ primary_action_label }}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
|
||||
{%- set item = values['card_' + index + '_item'] -%}
|
||||
{%- if item -%}
|
||||
{%- set item = frappe.get_doc("Item", item) -%}
|
||||
{{ item_card(
|
||||
item.item_name, item.image, item.route, item.description,
|
||||
None, item.item_group, values['card_' + index + '_featured'],
|
||||
True, "Center"
|
||||
) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -0,0 +1,273 @@
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2020-11-17 15:35:05.285322",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subtitle",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"fieldname": "primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"__islocal": 1,
|
||||
"__unsaved": 1,
|
||||
"fieldname": "primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 1",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_1_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_1_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_2",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 2",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_2_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_2_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_3",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 3",
|
||||
"options": "",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_3_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_3_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_4",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 4",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_4_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_4_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 5",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_5_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_5_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_6",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 6",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_6_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_6_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 7",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_7_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_7_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_8",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 8",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_8_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_8_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_9",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 9",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_9_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_9_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_10",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 10",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_10_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_10_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_11",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 11",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_11_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_11_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_12",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 12",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_12_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_12_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-11-19 18:48:52.633045",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Shopping Cart",
|
||||
"name": "Item Card Group",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Section"
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2020-11-17 15:28:47.809342",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"fieldname": "item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"fieldname": "featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"options": "",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-11-17 15:33:34.982515",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Shopping Cart",
|
||||
"name": "Product Card",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Component"
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
{%- macro card(title, image, url, text_primary=False) -%}
|
||||
{%- set align_class = resolve_class({
|
||||
'text-right': text_primary,
|
||||
'text-centre': align == 'Center',
|
||||
'text-left': align == 'Left',
|
||||
}) -%}
|
||||
<div class="card h-100">
|
||||
{% if image %}
|
||||
<img class="card-img-top" src="{{ image }}" alt="{{ title }}">
|
||||
{% endif %}
|
||||
<div class="card-body text-center text-muted small">
|
||||
{{ title or '' }}
|
||||
</div>
|
||||
<a href="{{ url or '#' }}" class="stretched-link"></a>
|
||||
</div>
|
||||
{%- endmacro -%}
|
||||
|
||||
<div class="section-with-cards product-category-section">
|
||||
{%- if title -%}
|
||||
<h2 class="section-title">{{ title }}</h2>
|
||||
{%- endif -%}
|
||||
{%- if subtitle -%}
|
||||
<p class="section-description">{{ subtitle }}</p>
|
||||
{%- endif -%}
|
||||
<!-- {%- set card_size = card_size or 'Small' -%} -->
|
||||
<div class="{{ resolve_class({'mt-6': title}) }}">
|
||||
<div class="card-grid">
|
||||
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8'] -%}
|
||||
{%- set category = values['category_' + index] -%}
|
||||
{%- if category -%}
|
||||
{%- set category = frappe.get_doc("Item Group", category) -%}
|
||||
{{ card(category.name, category.image, category.route) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -0,0 +1,85 @@
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2020-11-17 15:25:50.855934",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subtitle",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_1",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_2",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_3",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_4",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_5",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_6",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_7",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_8",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-11-18 17:26:28.726260",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Shopping Cart",
|
||||
"name": "Product Category Cards",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Section"
|
||||
}
|
@ -81,7 +81,7 @@ frappe.ui.form.on("Item", {
|
||||
}, __('Create'));
|
||||
}
|
||||
|
||||
frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
// frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
if (frm.doc.variant_of) {
|
||||
frm.set_intro(__('This Item is a Variant of {0} (Template).',
|
||||
|
@ -317,6 +317,7 @@ class Item(WebsiteGenerator):
|
||||
context.search_link = '/product_search'
|
||||
|
||||
context.parents = get_parent_item_groups(self.item_group)
|
||||
context.body_class = "product-page"
|
||||
|
||||
self.set_variant_context(context)
|
||||
self.set_attribute_context(context)
|
||||
|
@ -3,21 +3,25 @@
|
||||
{% block title %} {{ title }} {% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="item-breadcrumbs small text-muted">
|
||||
{% include "templates/includes/breadcrumbs.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
{% from "erpnext/templates/includes/macros.html" import product_image %}
|
||||
<div class="item-content">
|
||||
<div class="product-page-content" itemscope itemtype="http://schema.org/Product">
|
||||
<div class="row mb-5">
|
||||
{% include "templates/generators/item/item_image.html" %}
|
||||
{% include "templates/generators/item/item_details.html" %}
|
||||
<div class="product-container">
|
||||
{% from "erpnext/templates/includes/macros.html" import product_image %}
|
||||
<div class="item-content">
|
||||
<div class="product-page-content" itemscope itemtype="http://schema.org/Product">
|
||||
<div class="row mb-5">
|
||||
{% include "templates/generators/item/item_image.html" %}
|
||||
{% include "templates/generators/item/item_details.html" %}
|
||||
</div>
|
||||
|
||||
{% include "templates/generators/item/item_specifications.html" %}
|
||||
|
||||
{{ doc.website_content or '' }}
|
||||
</div>
|
||||
|
||||
{% include "templates/generators/item/item_specifications.html" %}
|
||||
|
||||
{{ doc.website_content or '' }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -6,10 +6,10 @@
|
||||
<div class="item-cart row mt-2" data-variant-item-code="{{ item_code }}">
|
||||
<div class="col-md-12">
|
||||
{% if cart_settings.show_price and product_info.price %}
|
||||
<h4>
|
||||
<div class="product-price">
|
||||
{{ product_info.price.formatted_price_sales_uom }}
|
||||
<small class="text-muted">({{ product_info.price.formatted_price }} / {{ product_info.uom }})</small>
|
||||
</h4>
|
||||
<small class="formatted-price">({{ product_info.price.formatted_price }} / {{ product_info.uom }})</small>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ _("Unit of Measurement") }} : {{ product_info.uom }}
|
||||
{% endif %}
|
||||
@ -17,11 +17,11 @@
|
||||
{% if cart_settings.show_stock_availability %}
|
||||
<div>
|
||||
{% if product_info.in_stock == 0 %}
|
||||
<span class="text-danger">
|
||||
<span class="text-danger no-stock">
|
||||
{{ _('Not in stock') }}
|
||||
</span>
|
||||
{% elif product_info.in_stock == 1 %}
|
||||
<span class="text-success">
|
||||
<span class="text-success has-stock">
|
||||
{{ _('In stock') }}
|
||||
{% if product_info.show_stock_qty and product_info.stock_qty %}
|
||||
({{ product_info.stock_qty[0][0] }})
|
||||
@ -30,7 +30,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-3">
|
||||
<div class="mt-5 mb-5">
|
||||
{% 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 {% if not product_info.qty %}hidden{% endif %}"
|
||||
@ -40,8 +40,13 @@
|
||||
</a>
|
||||
<button
|
||||
data-item-code="{{item_code}}"
|
||||
class="btn btn-outline-primary btn-add-to-cart {% if product_info.qty %}hidden{% endif %}"
|
||||
class="btn btn-primary btn-add-to-cart {% if product_info.qty %}hidden{% endif %} w-100"
|
||||
>
|
||||
<span class="mr-2">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-assets"></use>
|
||||
</svg>
|
||||
</span>
|
||||
{{ _("Add to Cart") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -1,9 +1,9 @@
|
||||
{% if shopping_cart and shopping_cart.cart_settings.enabled %}
|
||||
{% set cart_settings = shopping_cart.cart_settings %}
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="mt-5 mb-6">
|
||||
{% if cart_settings.enable_variants | int %}
|
||||
<button class="btn btn-primary btn-configure"
|
||||
<button class="btn btn-primary-light btn-configure"
|
||||
data-item-code="{{ doc.name }}"
|
||||
data-item-name="{{ doc.item_name }}"
|
||||
>
|
||||
|
@ -187,42 +187,53 @@ class ItemConfigure {
|
||||
}
|
||||
|
||||
get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info }) {
|
||||
const exact_match_message = __('1 exact match.');
|
||||
const one_item = exact_match.length === 1 ?
|
||||
exact_match[0] :
|
||||
filtered_items_count === 1 ?
|
||||
filtered_items[0] : '';
|
||||
const one_item = exact_match.length === 1
|
||||
? exact_match[0]
|
||||
: filtered_items_count === 1
|
||||
? filtered_items[0]
|
||||
: '';
|
||||
|
||||
const item_add_to_cart = one_item ? `
|
||||
<div class="alert alert-success d-flex justify-content-between align-items-center" role="alert">
|
||||
<div>
|
||||
<div>${one_item} ${product_info && product_info.price ? '(' + product_info.price.formatted_price_sales_uom + ')' : ''}</div>
|
||||
</div>
|
||||
<a href data-action="btn_add_to_cart" data-item-code="${one_item}">
|
||||
${__('Add to cart')}
|
||||
</a>
|
||||
</div>
|
||||
`: '';
|
||||
<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")}s
|
||||
</button>
|
||||
` : '';
|
||||
|
||||
const items_found = filtered_items_count === 1 ?
|
||||
__('{0} item found.', [filtered_items_count]) :
|
||||
__('{0} items found.', [filtered_items_count]);
|
||||
|
||||
const item_found_status = `
|
||||
<div class="alert alert-warning d-flex justify-content-between align-items-center" role="alert">
|
||||
<span>
|
||||
${exact_match.length === 1 ? '' : items_found}
|
||||
${exact_match.length === 1 ? `<span>${exact_match_message}</span>` : ''}
|
||||
</span>
|
||||
<a href data-action="btn_clear_values">
|
||||
${__('Clear values')}
|
||||
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
|
||||
? '(' + product_info.price.formatted_price_sales_uom + ')'
|
||||
: ''
|
||||
}
|
||||
</div></div>
|
||||
<a href data-action="btn_clear_values" data-item-code="${one_item}">
|
||||
${__('Clear Values')}
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
</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>`;
|
||||
|
||||
return `
|
||||
${item_add_to_cart}
|
||||
${item_found_status}
|
||||
${item_add_to_cart}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -254,8 +265,8 @@ class ItemConfigure {
|
||||
}
|
||||
|
||||
append_status_area() {
|
||||
this.dialog.$status_area = $('<div class="status-area">');
|
||||
this.dialog.$wrapper.find('.modal-body').prepend(this.dialog.$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);
|
||||
@ -263,7 +274,7 @@ class ItemConfigure {
|
||||
const method = this[action];
|
||||
method.call(this, e);
|
||||
});
|
||||
this.dialog.$body.css({ maxHeight: '75vh', overflow: 'auto', overflowX: 'hidden' });
|
||||
this.dialog.$wrapper.addClass('item-configurator-dialog');
|
||||
}
|
||||
|
||||
get_next_attribute_and_values(selected_attributes) {
|
||||
|
@ -1,14 +1,21 @@
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-7 product-details">
|
||||
<!-- title -->
|
||||
<h1 itemprop="name">
|
||||
<h1 class="product-title" itemprop="name">
|
||||
{{ item_name }}
|
||||
</h1>
|
||||
<p class="text-muted">
|
||||
<p class="product-code">
|
||||
<span>{{ _("Item Code") }}:</span>
|
||||
<span itemprop="productID">{{ doc.name }}</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 itemprop="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 '') %}
|
||||
@ -17,12 +24,4 @@
|
||||
{{ _("No description given") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
@ -1,42 +1,42 @@
|
||||
<div class="col-md-4 h-100">
|
||||
{% if slides %}
|
||||
{{ product_image(slides[0].image, 'product-image') }}
|
||||
<div class="item-slideshow">
|
||||
{% for item in slides %}
|
||||
<img class="item-slideshow-image mt-2 {% if loop.first %}active{% endif %}"
|
||||
src="{{ item.image }}" alt="{{ item.heading }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- 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);
|
||||
<div class="col-md-5 h-100 d-flex">
|
||||
{% 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(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }}
|
||||
{% endif %}
|
||||
$('.item-slideshow-image').removeClass('active');
|
||||
$img.addClass('active');
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% else %}
|
||||
{{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }}
|
||||
{% endif %}
|
||||
|
||||
<!-- Simple image preview -->
|
||||
<!-- 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 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 {
|
||||
|
@ -1,42 +1,139 @@
|
||||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{% include "templates/includes/breadcrumbs.html" %}
|
||||
{% endblock %}
|
||||
{% block header %}
|
||||
<!-- <h2>{{ title }}</h2> -->
|
||||
{% endblock header %}
|
||||
|
||||
{% block header %}<h1>{{ name }}</h1>{% endblock %}
|
||||
{% block script %}
|
||||
<script type="text/javascript" src="/all-products/index.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="item-group-content" itemscope itemtype="http://schema.org/Product">
|
||||
<div class="item-group-slideshow">
|
||||
{% if slideshow %}<!-- slideshow -->
|
||||
{% include "templates/includes/slideshow.html" %}
|
||||
{{ web_block(
|
||||
"Hero Slider",
|
||||
values=slideshow,
|
||||
add_container=0,
|
||||
add_top_padding=0,
|
||||
add_bottom_padding=0,
|
||||
) }}
|
||||
{% endif %}
|
||||
<h2 class="mt-3">{{ title }}</h2>
|
||||
{% if description %}<!-- description -->
|
||||
<div class="mb-3" itemprop="description">{{ description or ""}}</div>
|
||||
<div class="item-group-description text-muted mb-5" itemprop="description">{{ description or ""}}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
{% if items %}
|
||||
<div id="search-list">
|
||||
{% for i in range(0, page_length) %}
|
||||
{% if items[i] %}
|
||||
{%- set item = items[i] %}
|
||||
<div class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
|
||||
<div class="row products-list">
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
{% include "erpnext/www/all-products/item_row.html" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% include "erpnext/www/all-products/not_found.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</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>
|
||||
{% for field_filter in field_filters %}
|
||||
{%- set item_field = field_filter[0] %}
|
||||
{%- set values = field_filter[1] %}
|
||||
<div class="mb-4 filter-block pb-5">
|
||||
<div class="filter-label mb-3">{{ item_field.label }}</div>
|
||||
|
||||
{% if values | len > 20 %}
|
||||
<!-- show inline filter if values more than 20 -->
|
||||
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
|
||||
{% endif %}
|
||||
|
||||
{% if values %}
|
||||
<div class="filter-options">
|
||||
{% for value in values %}
|
||||
<div class="checkbox" data-value="{{ value }}">
|
||||
<label for="{{value}}">
|
||||
<input type="checkbox"
|
||||
class="product-filter field-filter"
|
||||
id="{{value}}"
|
||||
data-filter-name="{{ item_field.fieldname }}"
|
||||
data-filter-value="{{ value }}"
|
||||
>
|
||||
<span class="label-area">{{ value }}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<i class="text-muted">{{ _('No values') }}</i>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for attribute in attribute_filters %}
|
||||
<div class="mb-4 filter-block pb-5">
|
||||
<div class="filter-label mb-3">{{ attribute.name}}</div>
|
||||
{% if values | len > 20 %}
|
||||
<!-- show inline filter if values more than 20 -->
|
||||
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
|
||||
{% endif %}
|
||||
|
||||
{% if attribute.item_attribute_values %}
|
||||
<div class="filter-options">
|
||||
{% for attr_value in attribute.item_attribute_values %}
|
||||
<div class="checkbox">
|
||||
<label data-value="{{ value }}">
|
||||
<input type="checkbox"
|
||||
class="product-filter attribute-filter"
|
||||
id="{{attr_value.name}}"
|
||||
data-attribute-name="{{ attribute.name }}"
|
||||
data-attribute-value="{{ attr_value.attribute_value }}"
|
||||
{% if attr_value.checked %} checked {% endif %}>
|
||||
<span class="label-area">{{ attr_value.attribute_value }}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<i class="text-muted">{{ _('No values') }}</i>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="item-group-nav-buttons">
|
||||
{% if frappe.form_dict.start|int > 0 %}
|
||||
<a class="btn btn-outline-secondary" href="/{{ pathname }}?start={{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</a>
|
||||
{% endif %}
|
||||
{% if items|length > page_length %}
|
||||
<a class="btn btn-outline-secondary" href="/{{ pathname }}?start={{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted">{{ _("No items listed") }}.</div>
|
||||
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
|
||||
const $input = $(e.target);
|
||||
const keyword = ($input.val() || '').toLowerCase();
|
||||
const $filter_options = $input.next('.filter-options');
|
||||
|
||||
$filter_options.find('.custom-control').show();
|
||||
$filter_options.find('.custom-control').each((i, el) => {
|
||||
const $el = $(el);
|
||||
const value = $el.data('value').toLowerCase();
|
||||
if (!value.includes(keyword)) {
|
||||
$el.hide();
|
||||
}
|
||||
});
|
||||
}, 300));
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% if frappe.form_dict.start|int > 0 %}
|
||||
<button class="btn btn-outline-secondary btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</button>
|
||||
{% endif %}
|
||||
{% if items|length >= page_length %}
|
||||
<button class="btn btn-outline-secondary btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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: `<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);
|
||||
@ -221,6 +269,7 @@ $.extend(shopping_cart, {
|
||||
|
||||
frappe.ready(function() {
|
||||
$(".cart-icon").hide();
|
||||
shopping_cart.parent = $(".cart-container");
|
||||
shopping_cart.bind_events();
|
||||
});
|
||||
|
||||
|
@ -1,12 +1,17 @@
|
||||
<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 class="btn btn-sm btn-default btn-change-address" style="position: absolute; right: 0; top: 0;">
|
||||
{{ _('Change') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ address.title }}</h5>
|
||||
<p class="card-text text-muted">
|
||||
<div class="card-body p-0">
|
||||
<div class="card-title">{{ address.title }}</div>
|
||||
<div class="card-text mb-2">
|
||||
{{ address.display }}
|
||||
</p>
|
||||
<a href="/addresses?name={{address.name}}" class="card-link">{{ _('Edit') }}</a>
|
||||
</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>
|
||||
|
12
erpnext/templates/includes/cart/address_picker_card.html
Normal file
12
erpnext/templates/includes/cart/address_picker_card.html
Normal file
@ -0,0 +1,12 @@
|
||||
<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>
|
@ -14,31 +14,39 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-3" data-section="shipping-address">
|
||||
<h6 class="text-uppercase">{{ _("Shipping Address") }}</h6>
|
||||
<div class="mb-3 frappe-card p-5" data-section="shipping-address">
|
||||
<h6>{{ _("Shipping Address") }}</h6>
|
||||
<hr>
|
||||
{% for address in shipping_addresses %}
|
||||
{% if doc.shipping_address_name == address.name %}
|
||||
<div class="row no-gutters" data-fieldname="shipping_address_name">
|
||||
{% for address in shipping_addresses %}
|
||||
<div class="mr-3 mb-3 w-25" data-address-name="{{address.name}}" data-address-type="shipping" {% if doc.shipping_address_name == address.name %} data-active {% endif %}>
|
||||
{% include "templates/includes/cart/address_card.html" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<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>
|
||||
<div class="mb-3" data-section="billing-address">
|
||||
<h6 class="text-uppercase">{{ _("Billing Address") }}</h6>
|
||||
<div class="row no-gutters" data-fieldname="customer_address">
|
||||
{% for address in billing_addresses %}
|
||||
<div class="mr-3 mb-3 w-25" data-address-name="{{address.name}}" data-address-type="billing" {% if doc.customer_address == address.name %} data-active {% endif %}>
|
||||
{% include "templates/includes/cart/address_card.html" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="checkbox ml-1 mb-2">
|
||||
<label for="input_same_billing">
|
||||
<input type="checkbox" id="input_same_billing" checked>
|
||||
<span class="label-area">{{ _('Billing Address is same as Shipping Address') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="input_same_billing" checked>
|
||||
<label class="custom-control-label" for="input_same_billing">{{ _('Billing Address is same as Shipping Address') }}</label>
|
||||
<div class="mb-3 frappe-card p-5" data-section="billing-address">
|
||||
<h6>{{ _("Billing Address") }}</h6>
|
||||
<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>
|
||||
<button class="btn btn-outline-primary btn-sm mt-3 btn-new-address">{{ _("Add a new address") }}</button>
|
||||
<button class="btn btn-outline-primary btn-sm mt-1 btn-new-address bg-white">{{ _("Add a new address") }}</button>
|
||||
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
@ -55,6 +63,7 @@ frappe.ready(() => {
|
||||
});
|
||||
|
||||
$('.btn-new-address').click(() => {
|
||||
console.log('clicked');
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('New Address'),
|
||||
fields: [
|
||||
@ -168,5 +177,12 @@ frappe.ready(() => {
|
||||
function toggle_billing_address_section(flag) {
|
||||
$('[data-section="billing-address"]').toggle(flag);
|
||||
}
|
||||
|
||||
$('.btn-change-address').click(() => {
|
||||
// const d = new frappe.ui.Dialog({
|
||||
// })
|
||||
|
||||
// d.show();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
4
erpnext/templates/includes/cart/cart_address_picker.html
Normal file
4
erpnext/templates/includes/cart/cart_address_picker.html
Normal file
@ -0,0 +1,4 @@
|
||||
<div class="mb-3 frappe-card p-5" data-section="shipping-address">
|
||||
<h6>{{ _("Shipping Address") }}</h6>
|
||||
</div>
|
||||
|
@ -1,15 +1,15 @@
|
||||
{% for d in doc.items %}
|
||||
<tr data-name="{{ d.name }}">
|
||||
<td>
|
||||
<div class="font-weight-bold">
|
||||
<div class="item-title mb-1">
|
||||
{{ d.item_name }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="item-subtitle">
|
||||
{{ d.item_code }}
|
||||
</div>
|
||||
{%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %}
|
||||
{% if variant_of %}
|
||||
<span class="text-muted">
|
||||
<span class="item-subtitle">
|
||||
{{ _('Variant of') }} <a href="{{frappe.db.get_value('Item', variant_of, 'route')}}">{{ variant_of }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
@ -20,20 +20,20 @@
|
||||
<td class="text-right">
|
||||
<div class="input-group number-spinner">
|
||||
<span class="input-group-prepend d-none d-sm-inline-block">
|
||||
<button class="btn btn-outline-secondary cart-btn" data-dir="dwn">–</button>
|
||||
<button class="btn cart-btn" data-dir="dwn">–</button>
|
||||
</span>
|
||||
<input class="form-control text-right cart-qty border-secondary" value="{{ d.get_formatted('qty') }}" data-item-code="{{ d.item_code }}">
|
||||
<input class="form-control text-center cart-qty" value="{{ d.get_formatted('qty') }}" data-item-code="{{ d.item_code }}">
|
||||
<span class="input-group-append d-none d-sm-inline-block">
|
||||
<button class="btn btn-outline-secondary cart-btn" data-dir="up">+</button>
|
||||
<button class="btn cart-btn" data-dir="up">+</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
{% if cart_settings.enable_checkout %}
|
||||
<td class="text-right">
|
||||
<td class="text-right item-subtotal">
|
||||
<div>
|
||||
{{ d.get_formatted('amount') }}
|
||||
</div>
|
||||
<span class="text-muted">
|
||||
<span class="item-rate">
|
||||
{{ _('Rate:') }} {{ d.get_formatted('rate') }}
|
||||
</span>
|
||||
</td>
|
||||
|
@ -8,9 +8,9 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro product_image(website_image, css_class="", alt="") %}
|
||||
<div class="border text-center rounded h-100 {{ css_class }}" style="overflow: hidden;">
|
||||
<div class="border text-center rounded {{ css_class }}" style="overflow: hidden;">
|
||||
<img itemprop="image" class="website-image h-100 w-100" alt="{{ alt }}" src="{{ frappe.utils.quoted(website_image or 'no-image.jpg') | abs_url }}">
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro media_image(website_image, name, css_class="") %}
|
||||
@ -18,13 +18,13 @@
|
||||
{% if not website_image -%}
|
||||
<div class="sidebar-standard-image"> <div class="standard-image" style="background-color: rgb(250, 251, 252);">{{name}}</div> </div>
|
||||
{%- endif %}
|
||||
{% if website_image -%}
|
||||
{% if website_image -%}
|
||||
<a href="{{ frappe.utils.quoted(website_image) }}">
|
||||
<img itemprop="image" src="{{ frappe.utils.quoted(website_image) | abs_url }}"
|
||||
class="img-responsive img-thumbnail sidebar-image" style="min-height:100%; min-width:100%;">
|
||||
</a>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_homepage_section(section) %}
|
||||
@ -57,4 +57,65 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
{%- macro item_card(title, image, url, description, rate, category, is_featured=False, is_full_width=False, align="Left") -%}
|
||||
{%- set align_items_class = resolve_class({
|
||||
'align-items-end': align == 'Right',
|
||||
'align-items-center': align == 'Center',
|
||||
'align-items-start': align == 'Left',
|
||||
}) -%}
|
||||
{%- set col_size = 3 if is_full_width else 4 -%}
|
||||
{% if is_featured %}
|
||||
<div class="col-sm-{{ col_size*2 }} item-card">
|
||||
<div class="card featured-item {{ align_items_class }}">
|
||||
{% if image %}
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-6">
|
||||
<img class="card-img" src="{{ image }}" alt="{{ title }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ item_card_body(title, description, url, rate, category, is_featured, align) }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-md-12">
|
||||
{{ item_card_body(title, description, url, rate, category, is_featured, align) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-sm-{{ col_size }} item-card">
|
||||
<div class="card {{ align_items_class }}">
|
||||
{% if image %}
|
||||
<img class="card-img" src="{{ image }}" alt="{{ title }}">
|
||||
{% else %}
|
||||
<div class="card-img-top no-image">
|
||||
{{ frappe.utils.get_abbr(title) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ item_card_body(title, description, url, rate, category, is_featured, align) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro item_card_body(title, description, url, rate, category, is_featured, align) -%}
|
||||
{%- set align_class = resolve_class({
|
||||
'text-right': align == 'Right',
|
||||
'text-center': align == 'Center' and not is_featured,
|
||||
'text-left': align == 'Left' or is_featured,
|
||||
}) -%}
|
||||
<div class="card-body {{ align_class }}">
|
||||
<div class="product-title">{{ title or '' }}</div>
|
||||
{% if is_featured %}
|
||||
<div class="product-price">{{ rate or '' }}</div>
|
||||
<div class="product-description ellipsis">{{ description or '' }}</div>
|
||||
{% else %}
|
||||
<div class="product-category">{{ category or '' }}</div>
|
||||
<div class="product-price">{{ rate or '' }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="/{{ url or '#' }}" class="stretched-link"></a>
|
||||
{%- endmacro -%}
|
@ -2,9 +2,11 @@
|
||||
|
||||
{% block navbar_right_extension %}
|
||||
<li class="shopping-cart cart-icon hidden">
|
||||
<a href="/cart" class="nav-link">
|
||||
{{ _("Cart") }}
|
||||
<span class="badge badge-primary" id="cart-count"></span>
|
||||
<a class="nav-link" href="/cart">
|
||||
<svg class="icon icon-lg">
|
||||
<use href="#icon-assets"></use>
|
||||
</svg>
|
||||
<span class="badge badge-primary cart-badge" id="cart-count"></span>
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
@ -29,12 +29,12 @@
|
||||
{{ _("Discount") }}
|
||||
</th>
|
||||
<th class="text-right tot_quotation_discount">
|
||||
{% set tot_quotation_discount = [] %}
|
||||
{%- for item in doc.items -%}
|
||||
{% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
|
||||
* item.discount_percentage) / 100)) %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }}
|
||||
{% set tot_quotation_discount = [] %}
|
||||
{%- for item in doc.items -%}
|
||||
{% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
|
||||
* item.discount_percentage) / 100)) %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }}
|
||||
</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
@ -47,51 +47,52 @@
|
||||
{{ _("Total Amount") }}
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span>
|
||||
{% set total_amount = [] %}
|
||||
{%- for item in doc.items -%}
|
||||
{% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }}
|
||||
</span>
|
||||
<span>
|
||||
{% set total_amount = [] %}
|
||||
{%- for item in doc.items -%}
|
||||
{% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-right" colspan="2">
|
||||
{{ _("Applied Coupon Code") }}
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span>
|
||||
{%- for row in frappe.get_all(doctype="Coupon Code",
|
||||
fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
|
||||
<span>{{ row.coupon_code }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-right" colspan="2">
|
||||
{{ _("Applied Coupon Code") }}
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span>
|
||||
{%- for row in frappe.get_all(doctype="Coupon Code",
|
||||
fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
|
||||
<span>{{ row.coupon_code }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-right" colspan="2">
|
||||
{{ _("Discount") }}
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span>
|
||||
{% set tot_SO_discount = [] %}
|
||||
{%- for item in doc.items -%}
|
||||
{% if tot_SO_discount.append((((item.price_list_rate * item.qty)
|
||||
* item.discount_percentage) / 100)) %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-right" colspan="2">
|
||||
{{ _("Discount") }}
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span>
|
||||
{% set tot_SO_discount = [] %}
|
||||
{%- for item in doc.items -%}
|
||||
{% if tot_SO_discount.append((((item.price_list_rate * item.qty)
|
||||
* item.discount_percentage) / 100)) %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<th class="text-right" colspan="2">
|
||||
<th></th>
|
||||
<th class="item-grand-total">
|
||||
{{ _("Grand Total") }}
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<th class="text-right item-grand-total">
|
||||
{{ doc.get_formatted("grand_total") }}
|
||||
</th>
|
||||
</tr>
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% from "erpnext/templates/includes/macros.html" import product_image_square %}
|
||||
{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
|
||||
|
||||
<a class="product-link product-list-link" href="{{ route|abs_url }}">
|
||||
<div class='row'>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% block title %} {{ _("Shopping Cart") }} {% endblock %}
|
||||
|
||||
{% block header %}<h1>{{ _("Shopping Cart") }}</h1>{% endblock %}
|
||||
{% block header %}<h3 class="shopping-cart-header mt-2 mb-6">{{ _("Shopping Cart") }}</h1>{% endblock %}
|
||||
|
||||
<!--
|
||||
{% block script %}
|
||||
@ -18,94 +18,119 @@
|
||||
|
||||
{% from "templates/includes/macros.html" import item_name_and_description %}
|
||||
|
||||
{% if doc.items %}
|
||||
<div class="cart-container">
|
||||
<div id="cart-error" class="alert alert-danger" style="display: none;"></div>
|
||||
<div class="row m-0">
|
||||
<div class="col-md-8 frappe-card p-5">
|
||||
<div>
|
||||
<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 width="60%">{{ _('Item') }}</th>
|
||||
<th width="20%">{{ _('Quantity') }}</th>
|
||||
{% if cart_settings.enable_checkout %}
|
||||
<th width="20%" class="text-right">{{ _('Subtotal') }}</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="cart-items">
|
||||
{% include "templates/includes/cart/cart_items.html" %}
|
||||
</tbody>
|
||||
{% if cart_settings.enable_checkout %}
|
||||
<tfoot class="cart-tax-items">
|
||||
{% include "templates/includes/order/order_taxes.html" %}
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
{% if cart_settings.enable_checkout %}
|
||||
<a class="btn btn-outline-primary" href="/orders">
|
||||
{{ _('See past orders') }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="btn btn-outline-primary" href="/quotations">
|
||||
{{ _('See past quotations') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-8">
|
||||
{% if doc.items %}
|
||||
<div class="place-order-container">
|
||||
{% if cart_settings.enable_checkout %}
|
||||
<button class="btn btn-primary btn-place-order" type="button">
|
||||
{{ _("Place Order") }}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-primary btn-request-for-quotation" type="button">
|
||||
{{ _("Request for Quotation") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if doc.items %}
|
||||
<table class="table table-bordered mt-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="60%">{{ _('Item') }}</th>
|
||||
<th width="20%" class="text-right">{{ _('Quantity') }}</th>
|
||||
{% if cart_settings.enable_checkout %}
|
||||
<th width="20%" class="text-right">{{ _('Subtotal') }}</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="cart-items">
|
||||
{% include "templates/includes/cart/cart_items.html" %}
|
||||
</tbody>
|
||||
{% if cart_settings.enable_checkout %}
|
||||
<tfoot class="cart-tax-items">
|
||||
{% include "templates/includes/order/order_taxes.html" %}
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-muted">{{ _('Your cart is Empty') }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if doc.items %}
|
||||
<div class="place-order-container">
|
||||
{% if cart_settings.enable_checkout %}
|
||||
<button class="btn btn-primary btn-place-order" type="button">
|
||||
{{ _("Place Order") }}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-primary btn-request-for-quotation" type="button">
|
||||
{{ _("Request for Quotation") }}
|
||||
</button>
|
||||
{% if doc.items %}
|
||||
{% if doc.tc_name %}
|
||||
<div class="terms-and-conditions-link">
|
||||
<a href class="link-terms-and-conditions" data-terms-name="{{ doc.tc_name }}">
|
||||
{{ _("Terms and Conditions") }}
|
||||
</a>
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$('.link-terms-and-conditions').click((e) => {
|
||||
e.preventDefault();
|
||||
const $link = $(e.target);
|
||||
const terms_name = $link.attr('data-terms-name');
|
||||
show_terms_and_conditions(terms_name);
|
||||
})
|
||||
});
|
||||
function show_terms_and_conditions(terms_name) {
|
||||
frappe.call('erpnext.shopping_cart.cart.get_terms_and_conditions', { terms_name })
|
||||
.then(r => {
|
||||
frappe.msgprint({
|
||||
title: terms_name,
|
||||
message: r.message
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if doc.items %}
|
||||
{% if doc.tc_name %}
|
||||
<div class="terms-and-conditions-link">
|
||||
<a href class="link-terms-and-conditions" data-terms-name="{{ doc.tc_name }}">
|
||||
{{ _("Terms and Conditions") }}
|
||||
</a>
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$('.link-terms-and-conditions').click((e) => {
|
||||
e.preventDefault();
|
||||
const $link = $(e.target);
|
||||
const terms_name = $link.attr('data-terms-name');
|
||||
show_terms_and_conditions(terms_name);
|
||||
})
|
||||
});
|
||||
function show_terms_and_conditions(terms_name) {
|
||||
frappe.call('erpnext.shopping_cart.cart.get_terms_and_conditions', { terms_name })
|
||||
.then(r => {
|
||||
frappe.msgprint({
|
||||
title: terms_name,
|
||||
message: r.message
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<div class="col-md-4">
|
||||
<div class="cart-addresses">
|
||||
{% include "templates/includes/cart/cart_address.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="cart-addresses mt-5">
|
||||
{% include "templates/includes/cart/cart_address.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-12">
|
||||
{% if cart_settings.enable_checkout %}
|
||||
<a href="/orders">
|
||||
{% 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">
|
||||
{{ _('See past orders') }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/quotations">
|
||||
<a class="btn btn-outline-primary" href="/quotations">
|
||||
{{ _('See past quotations') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block title %}{{ _('Products') }}{% endblock %}
|
||||
{% block header %}
|
||||
<h1>{{ _('Products') }}</h1>
|
||||
<div class="mb-6">{{ _('Products') }}</div>
|
||||
{% endblock header %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
<div class="row" style="display: none;">
|
||||
<div class="col-8">
|
||||
<div class="input-group input-group-sm mb-3">
|
||||
<input type="search" class="form-control" placeholder="{{_('Search')}}"
|
||||
@ -31,27 +30,34 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 order-2 col-md-8 order-md-1 products-list">
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
{% include "erpnext/www/all-products/item_row.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% include "erpnext/www/all-products/not_found.html" %}
|
||||
{% endif %}
|
||||
<div class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
|
||||
<div class="row products-list">
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
{% include "erpnext/www/all-products/item_row.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% include "erpnext/www/all-products/not_found.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 order-1 col-md-4 order-md-2">
|
||||
<div class="col-12 order-1 col-md-3 order-md-1">
|
||||
|
||||
{% if frappe.form_dict.start or frappe.form_dict.field_filters or frappe.form_dict.attribute_filters or frappe.form_dict.search %}
|
||||
<a class="mb-3 d-inline-block" href="/all-products">{{ _('Clear filters') }}</a>
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div class="collapse d-md-block" id="product-filters">
|
||||
<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="/all-products">{{ _('Clear All') }}</a>
|
||||
</div>
|
||||
{% for field_filter in field_filters %}
|
||||
{%- set item_field = field_filter[0] %}
|
||||
{%- set values = field_filter[1] %}
|
||||
<div class="mb-4">
|
||||
<h6>{{ item_field.label }}</h6>
|
||||
<div class="mb-4 filter-block pb-5">
|
||||
<div class="filter-label mb-3">{{ item_field.label }}</div>
|
||||
|
||||
{% if values | len > 20 %}
|
||||
<!-- show inline filter if values more than 20 -->
|
||||
@ -61,15 +67,15 @@
|
||||
{% if values %}
|
||||
<div class="filter-options">
|
||||
{% for value in values %}
|
||||
<div class="custom-control custom-checkbox" data-value="{{ value }}">
|
||||
<input type="checkbox"
|
||||
class="product-filter field-filter custom-control-input"
|
||||
id="{{value}}"
|
||||
data-filter-name="{{ item_field.fieldname }}"
|
||||
data-filter-value="{{ value }}"
|
||||
>
|
||||
<label class="custom-control-label" for="{{value}}">
|
||||
{{ value }}
|
||||
<div class="checkbox" data-value="{{ value }}">
|
||||
<label for="{{value}}">
|
||||
<input type="checkbox"
|
||||
class="product-filter field-filter"
|
||||
id="{{value}}"
|
||||
data-filter-name="{{ item_field.fieldname }}"
|
||||
data-filter-value="{{ value }}"
|
||||
>
|
||||
<span class="label-area">{{ value }}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@ -81,9 +87,8 @@
|
||||
{% endfor %}
|
||||
|
||||
{% for attribute in attribute_filters %}
|
||||
<div class="mb-4">
|
||||
<h6>{{ attribute.name }}</h6>
|
||||
|
||||
<div class="mb-4 filter-block pb-5">
|
||||
<div class="filter-label mb-3">{{ attribute.name}}</div>
|
||||
{% if values | len > 20 %}
|
||||
<!-- show inline filter if values more than 20 -->
|
||||
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
|
||||
@ -92,16 +97,15 @@
|
||||
{% if attribute.item_attribute_values %}
|
||||
<div class="filter-options">
|
||||
{% for attr_value in attribute.item_attribute_values %}
|
||||
<div class="custom-control custom-checkbox" data-value="{{ value }}">
|
||||
<input type="checkbox"
|
||||
class="product-filter attribute-filter custom-control-input"
|
||||
id="{{attr_value.name}}"
|
||||
data-attribute-name="{{ attribute.name }}"
|
||||
data-attribute-value="{{ attr_value.attribute_value }}"
|
||||
{% if attr_value.checked %} checked {% endif %}
|
||||
>
|
||||
<label class="custom-control-label" for="{{attr_value.name}}">
|
||||
{{ attr_value.attribute_value }}
|
||||
<div class="checkbox">
|
||||
<label data-value="{{ value }}">
|
||||
<input type="checkbox"
|
||||
class="product-filter attribute-filter"
|
||||
id="{{attr_value.name}}"
|
||||
data-attribute-name="{{ attribute.name }}"
|
||||
data-attribute-value="{{ attr_value.attribute_value }}"
|
||||
{% if attr_value.checked %} checked {% endif %}>
|
||||
<span class="label-area">{{ attr_value.attribute_value }}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@ -133,13 +137,15 @@
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="row product-paging-area mt-5">
|
||||
<div class="col-3">
|
||||
</div>
|
||||
<div class="col-9 text-right">
|
||||
{% if frappe.form_dict.start|int > 0 %}
|
||||
<button class="btn btn-outline-secondary btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</button>
|
||||
<button class="btn btn-default btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</button>
|
||||
{% endif %}
|
||||
{% if items|length >= page_length %}
|
||||
<button class="btn btn-outline-secondary btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
|
||||
<button class="btn btn-default btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -158,6 +164,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% endblock %}
|
@ -54,7 +54,7 @@ $(() => {
|
||||
field_filters: JSON.stringify(if_key_exists(this.field_filters)),
|
||||
attribute_filters: JSON.stringify(if_key_exists(this.attribute_filters)),
|
||||
});
|
||||
window.history.pushState('filters', '', '/all-products?' + query_string);
|
||||
window.history.pushState('filters', '', `${location.pathname}?` + query_string);
|
||||
|
||||
$('.page_content input').prop('disabled', true);
|
||||
this.get_items_with_filters()
|
||||
|
@ -1,6 +1,8 @@
|
||||
import frappe
|
||||
from erpnext.portal.product_configurator.utils import (get_products_for_website, get_product_settings,
|
||||
get_field_filter_data, get_attribute_filter_data)
|
||||
from erpnext.shopping_cart.product_query import ProductQuery
|
||||
from erpnext.shopping_cart.filters import ProductFiltersBuilder
|
||||
|
||||
sitemap = 1
|
||||
|
||||
@ -10,22 +12,25 @@ def get_context(context):
|
||||
search = frappe.form_dict.search
|
||||
field_filters = frappe.parse_json(frappe.form_dict.field_filters)
|
||||
attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
|
||||
start = frappe.parse_json(frappe.form_dict.start)
|
||||
else:
|
||||
search = field_filters = attribute_filters = None
|
||||
start = 0
|
||||
|
||||
context.items = get_products_for_website(field_filters, attribute_filters, search)
|
||||
engine = ProductQuery()
|
||||
context.items = engine.query(attribute_filters, field_filters, search, start)
|
||||
|
||||
# Add homepage as parent
|
||||
context.parents = [{"name": frappe._("Home"), "route":"/"}]
|
||||
|
||||
product_settings = get_product_settings()
|
||||
context.field_filters = get_field_filter_data() \
|
||||
if product_settings.enable_field_filters else []
|
||||
filter_engine = ProductFiltersBuilder()
|
||||
|
||||
context.attribute_filters = get_attribute_filter_data() \
|
||||
if product_settings.enable_attribute_filters else []
|
||||
context.field_filters = filter_engine.get_field_filters()
|
||||
context.attribute_filters = filter_engine.get_attribute_fitlers()
|
||||
|
||||
context.product_settings = product_settings
|
||||
context.page_length = product_settings.products_per_page
|
||||
context.body_class = "product-page"
|
||||
context.page_length = product_settings.products_per_page or 20
|
||||
|
||||
context.no_cache = 1
|
||||
|
@ -1,24 +1,7 @@
|
||||
<div class="card mb-3">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-3">
|
||||
<div class="card-body">
|
||||
<a class="no-underline" href="/{{ item.route }}">
|
||||
<img class="website-image" src="{{ item.website_image or item.image or 'no-image.jpg' }}" alt="{{ item.item_name }}">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<a class="text-dark" href="/{{ item.route }}">
|
||||
{{ item.item_name or item.name }}
|
||||
</a>
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
{{ item.website_description or item.description or '<i class="text-muted">No description</i>' }}
|
||||
</p>
|
||||
<a href="/{{ item.route }}" class="btn btn-sm btn-light">{{ _('More details') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
|
||||
|
||||
{{ item_card(
|
||||
item.item_name or item.name, item.website_image or item.image, item.route, item.website_description or item.description,
|
||||
item.formatted_price, item.item_group
|
||||
) }}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user