feat: (minor) Backorder indicator and fixed inconsistencies

- Checkbox in website item to indicate if item is on backorder
- Indicator on listing on full page if availbale on backorder.
- fix: Allow provision to add any valid field from Website Item in Search Index
- fix: Settings filter fields are as per Item, make as per Website Item
- "Add to quote/ Go to Quote" if cart checkout is disabled
This commit is contained in:
marination 2021-09-02 14:07:59 +05:30
parent 45f64bd930
commit bbcbcf7a20
11 changed files with 129 additions and 59 deletions

View File

@ -25,9 +25,9 @@ frappe.ui.form.on("E Commerce Settings", {
} }
frappe.model.with_doctype("Item", () => { frappe.model.with_doctype("Item", () => {
const item_meta = frappe.get_meta('Item'); const web_item_meta = frappe.get_meta('Website Item');
const valid_fields = item_meta.fields.filter( const valid_fields = web_item_meta.fields.filter(
df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
).map(df => ({ label: df.label, value: df.fieldname })); ).map(df => ({ label: df.label, value: df.fieldname }));

View File

@ -91,7 +91,7 @@
"depends_on": "enable_field_filters", "depends_on": "enable_field_filters",
"fieldname": "filter_fields", "fieldname": "filter_fields",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Item Fields", "label": "Website Item Fields",
"options": "Website Filter Field" "options": "Website Filter Field"
}, },
{ {
@ -370,7 +370,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-08-31 12:23:06.187619", "modified": "2021-09-02 14:02:44.785824",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "E-commerce", "module": "E-commerce",
"name": "E Commerce Settings", "name": "E Commerce Settings",

View File

@ -29,11 +29,14 @@
"column_break_13", "column_break_13",
"slideshow", "slideshow",
"thumbnail", "thumbnail",
"stock_information_section",
"website_warehouse",
"column_break_24",
"on_backorder",
"section_break_17", "section_break_17",
"short_description", "short_description",
"web_long_description", "web_long_description",
"column_break_27", "column_break_27",
"website_warehouse",
"website_specifications", "website_specifications",
"copy_from_item_group", "copy_from_item_group",
"display_additional_information_section", "display_additional_information_section",
@ -326,13 +329,29 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Recommended/Similar Items", "label": "Recommended/Similar Items",
"options": "Recommended Items" "options": "Recommended Items"
},
{
"fieldname": "stock_information_section",
"fieldtype": "Section Break",
"label": "Stock Information"
},
{
"fieldname": "column_break_24",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Indicate that Item is available on backorder and not usually pre-stocked",
"fieldname": "on_backorder",
"fieldtype": "Check",
"label": "On Backorder"
} }
], ],
"has_web_view": 1, "has_web_view": 1,
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-07-12 21:00:04.065803", "modified": "2021-09-02 13:08:41.942726",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "E-commerce", "module": "E-commerce",
"name": "Website Item", "name": "Website Item",

View File

@ -26,9 +26,11 @@ class ProductQuery:
self.or_filters = [] self.or_filters = []
self.filters = [["published", "=", 1]] self.filters = [["published", "=", 1]]
self.fields = ['web_item_name', 'name', 'item_name', 'item_code', 'website_image', self.fields = [
'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', "web_item_name", "name", "item_name", "item_code", "website_image",
'short_description', 'route', 'website_warehouse', 'ranking'] "variant_of", "has_variants", "item_group", "image", "web_long_description",
"short_description", "route", "website_warehouse", "ranking", "on_backorder"
]
def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
""" """
@ -239,6 +241,9 @@ class ProductQuery:
warehouse = item.get("website_warehouse") warehouse = item.get("website_warehouse")
is_stock_item = frappe.get_cached_value("Item", item.item_code, "is_stock_item") is_stock_item = frappe.get_cached_value("Item", item.item_code, "is_stock_item")
if item.get("on_backorder"):
return
if not is_stock_item: if not is_stock_item:
if warehouse: if warehouse:
# product bundle case # product bundle case

View File

@ -142,9 +142,22 @@ erpnext.ProductGrid = class {
} }
get_stock_availability(item, settings) { get_stock_availability(item, settings) {
if (settings.show_stock_availability && !item.has_variants && !item.in_stock) { if (settings.show_stock_availability && !item.has_variants) {
return `<span class="out-of-stock mb-2 mt-1">${ __("Out of stock") }</span>`; if (item.on_backorder) {
return `
<span class="out-of-stock mb-2 mt-1" style="color: var(--primary-color)">
${ __("Available on backorder") }
</span>
`;
} else if (!item.in_stock) {
return `
<span class="out-of-stock mb-2 mt-1">
${ __("Out of stock") }
</span>
`;
}
} }
return ``; return ``;
} }
@ -168,7 +181,7 @@ erpnext.ProductGrid = class {
<use href="#icon-assets"></use> <use href="#icon-assets"></use>
</svg> </svg>
</span> </span>
${ __('Add to Cart') } ${ settings.enable_checkout ? __('Add to Cart') : __('Add to Quote') }
</div> </div>
<a href="/cart"> <a href="/cart">
@ -177,7 +190,7 @@ erpnext.ProductGrid = class {
w-100 mt-4 go-to-cart-grid w-100 mt-4 go-to-cart-grid
${ item.in_cart ? '' : 'hidden' }" ${ item.in_cart ? '' : 'hidden' }"
data-item-code="${ item.item_code }"> data-item-code="${ item.item_code }">
${ __('Go to Cart') } ${ settings.enable_checkout ? __('Go to Cart') : __('Go to Quote') }
</div> </div>
</a> </a>
`; `;

View File

@ -125,11 +125,20 @@ erpnext.ProductList = class {
} }
get_stock_availability(item, settings) { get_stock_availability(item, settings) {
if (settings.show_stock_availability && !item.has_variants && !item.in_stock) { if (settings.show_stock_availability && !item.has_variants) {
return ` if (item.on_backorder) {
<br> return `
<span class="out-of-stock mt-2">${ __("Out of stock") }</span> <br>
`; <span class="out-of-stock mt-2" style="color: var(--primary-color)">
${ __("Available on backorder") }
</span>
`;
} else if (!item.in_stock) {
return `
<br>
<span class="out-of-stock mt-2">${ __("Out of stock") }</span>
`;
}
} }
return ``; return ``;
} }
@ -169,7 +178,7 @@ erpnext.ProductList = class {
<use href="#icon-assets"></use> <use href="#icon-assets"></use>
</svg> </svg>
</span> </span>
${ __('Add to Cart') } ${ settings.enable_checkout ? __('Add to Cart') : __('Add to Quote') }
</div> </div>
<div class="cart-indicator list-indicator ${item.in_cart ? '' : 'hidden'}"> <div class="cart-indicator list-indicator ${item.in_cart ? '' : 'hidden'}">
@ -183,7 +192,7 @@ erpnext.ProductList = class {
${ item.in_cart ? '' : 'hidden' }" ${ item.in_cart ? '' : 'hidden' }"
data-item-code="${ item.item_code }" data-item-code="${ item.item_code }"
style="padding: 0.25rem 1rem; min-width: 135px;"> style="padding: 0.25rem 1rem; min-width: 135px;">
${ __('Go to Cart') } ${ settings.enable_checkout ? __('Go to Cart') : __('Go to Quote') }
</div> </div>
</a> </a>
`; `;

View File

@ -10,15 +10,17 @@ WEBSITE_ITEM_KEY_PREFIX = 'website_item:'
WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict' WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict'
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict' WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict'
ALLOWED_INDEXABLE_FIELDS_SET = { def get_indexable_web_fields():
'web_item_name', "Return valid fields from Website Item that can be searched for."
'item_code', web_item_meta = frappe.get_meta("Website Item", cached=True)
'item_name', valid_fields = filter(
'item_group', lambda df: df.fieldtype in ("Link", "Table MultiSelect", "Data", "Small Text", "Text Editor"),
'brand', web_item_meta.fields)
'description',
'web_long_description' return [df.fieldname for df in valid_fields]
}
ALLOWED_INDEXABLE_FIELDS_SET = get_indexable_web_fields()
def is_search_module_loaded(): def is_search_module_loaded():
cache = frappe.cache() cache = frappe.cache()
@ -30,8 +32,8 @@ def is_search_module_loaded():
return "search" in parsed_output return "search" in parsed_output
# Decorator for checking wether Redisearch is there or not
def if_redisearch_loaded(function): def if_redisearch_loaded(function):
"Decorator to check if Redisearch is loaded."
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if is_search_module_loaded(): if is_search_module_loaded():
func = function(*args, **kwargs) func = function(*args, **kwargs)
@ -45,7 +47,8 @@ def make_key(key):
@if_redisearch_loaded @if_redisearch_loaded
def create_website_items_index(): def create_website_items_index():
'''Creates Index Definition''' "Creates Index Definition."
# CREATE index # CREATE index
client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache()) client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache())
@ -197,7 +200,7 @@ def get_fields_indexed():
) )
fields_to_index = fields_to_index.split(',') if fields_to_index else [] fields_to_index = fields_to_index.split(',') if fields_to_index else []
mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail'] mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail', 'ranking']
fields_to_index = fields_to_index + mandatory_fields fields_to_index = fields_to_index + mandatory_fields
return fields_to_index return fields_to_index

View File

@ -38,8 +38,13 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
) )
stock_status = None stock_status = None
if cart_settings.show_stock_availability: if cart_settings.show_stock_availability:
stock_status = get_web_item_qty_in_stock(item_code, "website_warehouse") on_backorder = frappe.get_cached_value("Website Item", {"item_code": item_code}, "on_backorder")
if on_backorder:
stock_status = frappe._dict({"on_backorder": True})
else:
stock_status = get_web_item_qty_in_stock(item_code, "website_warehouse")
product_info = { product_info = {
"price": price, "price": price,
@ -49,9 +54,12 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
} }
if stock_status: if stock_status:
product_info["stock_qty"] = stock_status.stock_qty if stock_status.on_backorder:
product_info["in_stock"] = stock_status.in_stock if stock_status.is_stock_item else get_non_stock_item_status(item_code, "website_warehouse") product_info["on_backorder"] = True
product_info["show_stock_qty"] = show_quantity_in_website() else:
product_info["stock_qty"] = stock_status.stock_qty
product_info["in_stock"] = stock_status.in_stock if stock_status.is_stock_item else get_non_stock_item_status(item_code, "website_warehouse")
product_info["show_stock_qty"] = show_quantity_in_website()
if product_info["price"]: if product_info["price"]:
if frappe.session.user != "Guest": if frappe.session.user != "Guest":

View File

@ -1354,8 +1354,12 @@ body.product-page {
font-weight: 500; font-weight: 500;
} }
.has-stock {
font-weight: 400 !important;
}
.out-of-stock { .out-of-stock {
font-weight: 500; font-weight: 400;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
color: #F47A7A; color: #F47A7A;

View File

@ -34,17 +34,21 @@
{% if cart_settings.show_stock_availability %} {% if cart_settings.show_stock_availability %}
<div class="mt-2"> <div class="mt-2">
{% if product_info.in_stock == 0 %} {% if product_info.get("on_backorder") %}
<span class="no-stock out-of-stock"> <span class="no-stock out-of-stock" style="color: var(--primary-color);">
{{ _('Out of stock') }} {{ _('Available on backorder') }}
</span> </span>
{% elif product_info.in_stock == 0 %}
<span class="no-stock out-of-stock">
{{ _('Out of stock') }}
</span>
{% elif product_info.in_stock == 1 %} {% elif product_info.in_stock == 1 %}
<span class="in-green has-stock"> <span class="in-green has-stock">
{{ _('In stock') }} {{ _('In stock') }}
{% if product_info.show_stock_qty and product_info.stock_qty %} {% if product_info.show_stock_qty and product_info.stock_qty %}
({{ product_info.stock_qty[0][0] }}) ({{ product_info.stock_qty[0][0] }})
{% endif %} {% endif %}
</span> </span>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@ -88,17 +92,21 @@
<div class="mb-4 d-flex"> <div class="mb-4 d-flex">
<!-- Add to Cart --> <!-- Add to Cart -->
{% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %} {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
<a href="/cart" class="btn btn-light btn-view-in-cart hidden mr-2 font-md" role="button"> <a href="/cart" class="btn btn-light btn-view-in-cart hidden mr-2 font-md"
{{ _("View in Cart") }} role="button">
</a> {{ _("View in Cart") if cart_settings.enable_checkout else _("View in Quote") }}
<button data-item-code="{{item_code}}" class="btn btn-primary btn-add-to-cart mr-2 w-30-40"> </a>
<span class="mr-2"> <button
<svg class="icon icon-md"> data-item-code="{{item_code}}"
<use href="#icon-assets"></use> class="btn btn-primary btn-add-to-cart mr-2 w-30-40"
</svg> >
</span> <span class="mr-2">
{{ _("Add to Cart") }} <svg class="icon icon-md">
</button> <use href="#icon-assets"></use>
</svg>
</span>
{{ _("Add to Cart") if cart_settings.enable_checkout else _("Add to Quote") }}
</button>
{% endif %} {% endif %}
<!-- Contact Us --> <!-- Contact Us -->

View File

@ -52,7 +52,7 @@ def get_product_data(search=None, start=0, limit=12):
search = "%" + cstr(search) + "%" search = "%" + cstr(search) + "%"
# order by # order by
query += """ ORDER BY ranking asc, modified desc limit %s, %s""" % (cint(start), cint(limit)) query += """ ORDER BY ranking desc, modified desc limit %s, %s""" % (cint(start), cint(limit))
return frappe.db.sql(query, { return frappe.db.sql(query, {
"search": search "search": search
@ -102,6 +102,7 @@ def product_search(query, limit=10, fuzzy_search=True):
results = client.search(q) results = client.search(q)
search_results['results'] = list(map(convert_to_dict, results.docs)) search_results['results'] = list(map(convert_to_dict, results.docs))
search_results['results'] = sorted(search_results['results'], key=lambda k: frappe.utils.cint(k['ranking']), reverse=True)
return search_results return search_results