diff --git a/ssi_product_website/README.rst b/ssi_product_website/README.rst new file mode 100644 index 0000000..ed8436f --- /dev/null +++ b/ssi_product_website/README.rst @@ -0,0 +1,47 @@ +.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +================================= +Product App - website Integration +================================= + + +Installation +============ + +To install this module, you need to: + +1. Clone the branch 14.0 of the repository https://github.com/open-synergy/ssi-product-attribute +2. Add the path to this repository in your configuration (addons-path) +3. Update the module list +4. Go to menu *Apps -> Apps -> Main Apps* +5. Search For *Product App - website Integration* +6. Install the module + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed +and welcomed feedback. + + +Credits +======= + +Contributors +------------ + +* Miftahussalam + +Maintainer +---------- + +.. image:: https://simetri-sinergi.id/logo.png + :alt: PT. Simetri Sinergi Indonesia + :target: https://simetri-sinergi.id.com + +This module is maintained by the PT. Simetri Sinergi Indonesia. diff --git a/ssi_product_website/__init__.py b/ssi_product_website/__init__.py new file mode 100644 index 0000000..7003d91 --- /dev/null +++ b/ssi_product_website/__init__.py @@ -0,0 +1,8 @@ +# Copyright 2023 OpenSynergy Indonesia +# Copyright 2023 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import ( + controllers, + models, +) diff --git a/ssi_product_website/__manifest__.py b/ssi_product_website/__manifest__.py new file mode 100644 index 0000000..5b84fd2 --- /dev/null +++ b/ssi_product_website/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2023 OpenSynergy Indonesia +# Copyright 2023 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +{ + "name": "Product App - website Integration", + "version": "14.0.1.0.0", + "website": "https://simetri-sinergi.id", + "author": "PT. Simetri Sinergi Indonesia, OpenSynergy Indonesia", + "license": "LGPL-3", + "installable": True, + "application": False, + "auto_install": True, + "depends": [ + "ssi_product", + "website", + ], + "data": [ + "security/ir.model.access.csv", + "data/data.xml", + "views/templates.xml", + "views/product_template_views.xml", + ], + "demo": [], +} diff --git a/ssi_product_website/controllers/__init__.py b/ssi_product_website/controllers/__init__.py new file mode 100644 index 0000000..da9cef5 --- /dev/null +++ b/ssi_product_website/controllers/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2023 OpenSynergy Indonesia +# Copyright 2023 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import ( + main, +) diff --git a/ssi_product_website/controllers/main.py b/ssi_product_website/controllers/main.py new file mode 100644 index 0000000..8cbcfc6 --- /dev/null +++ b/ssi_product_website/controllers/main.py @@ -0,0 +1,211 @@ +# Copyright 2023 OpenSynergy Indonesia +# Copyright 2023 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import json +import logging +from datetime import datetime +from werkzeug.exceptions import Forbidden, NotFound + +from odoo import fields, http, SUPERUSER_ID, tools, _ +from odoo.http import request +from odoo.addons.base.models.ir_qweb_fields import nl2br +from odoo.addons.http_routing.models.ir_http import slug +from odoo.addons.payment.controllers.portal import PaymentProcessing +from odoo.addons.website.controllers.main import QueryURL +from odoo.addons.website.models.ir_http import sitemap_qs2dom +from odoo.exceptions import ValidationError +from odoo.addons.portal.controllers.portal import _build_url_w_params +from odoo.addons.website.controllers.main import Website +from odoo.addons.website_form.controllers.main import WebsiteForm +from odoo.osv import expression +_logger = logging.getLogger(__name__) + + +class TableCompute(object): + + def __init__(self): + self.table = {} + + def _check_place(self, posx, posy, sizex, sizey, ppr): + res = True + for y in range(sizey): + for x in range(sizex): + if posx + x >= ppr: + res = False + break + row = self.table.setdefault(posy + y, {}) + if row.setdefault(posx + x) is not None: + res = False + break + for x in range(ppr): + self.table[posy + y].setdefault(x, None) + return res + + def process(self, products, ppg=20, ppr=4): + # Compute products positions on the grid + minpos = 0 + index = 0 + maxy = 0 + x = 0 + for p in products: + x = min(1, ppr) + y = min(1, ppr) + if index >= ppg: + x = y = 1 + + pos = minpos + while not self._check_place(pos % ppr, pos // ppr, x, y, ppr): + pos += 1 + # if 21st products (index 20) and the last line is full (ppr products in it), break + # (pos + 1.0) / ppr is the line where the product would be inserted + # maxy is the number of existing lines + # + 1.0 is because pos begins at 0, thus pos 20 is actually the 21st block + # and to force python to not round the division operation + if index >= ppg and ((pos + 1.0) // ppr) > maxy: + break + + if x == 1 and y == 1: # simple heuristic for CPU optimization + minpos = pos // ppr + + for y2 in range(y): + for x2 in range(x): + self.table[(pos // ppr) + y2][(pos % ppr) + x2] = False + self.table[pos // ppr][pos % ppr] = { + 'product': p, 'x': x, 'y': y, + } + if index <= ppg: + maxy = max(maxy, y + (pos // ppr)) + index += 1 + + # Format table according to HTML needs + rows = sorted(self.table.items()) + rows = [r[1] for r in rows] + for col in range(len(rows)): + cols = sorted(rows[col].items()) + x += len(cols) + rows[col] = [r[1] for r in cols if r[1]] + + return rows + + +class ProductWebsite(http.Controller): + + def _get_search_order(self, post): + # OrderBy will be parsed in orm and so no direct sql injection + # id is added to be sure that order is a unique sort key + order = post.get('order') or 'name ASC' + return order + + def _get_search_domain(self, search, search_in_description=True): + domains = [[("product_catalog", "=", True)]] + if search: + for srch in search.split(" "): + subdomains = [ + [('name', 'ilike', srch)], + [('product_variant_ids.default_code', 'ilike', srch)] + ] + if search_in_description: + subdomains.append([('description', 'ilike', srch)]) + domains.append(expression.OR(subdomains)) + + return expression.AND(domains) + + def sitemap_product_catalog(env, rule, qs): + if not qs or qs.lower() in '/product_catalog': + yield {'loc': '/product_catalog'} + + @http.route([ + '''/product_catalog''', + '''/product_catalog/page/''', + ], type='http', auth="public", website=True, sitemap=sitemap_product_catalog) + def product_catalog(self, page=0, search='', ppg=False, **post): + add_qty = int(post.get('add_qty', 1)) + + if ppg: + try: + ppg = int(ppg) + post['ppg'] = ppg + except ValueError: + ppg = False + if not ppg: + ppg = 20 + + ppr = 4 + + attrib_list = request.httprequest.args.getlist('attrib') + attrib_values = [[int(x) for x in v.split("-")] for v in attrib_list if v] + attributes_ids = {v[0] for v in attrib_values} + + domain = self._get_search_domain(search) + + keep = QueryURL('/product_catalog', search=search, + order=post.get('order')) + + request.context = dict(request.context, partner=request.env.user.partner_id) + + url = "/product_catalog" + if search: + post["search"] = search + + Product = request.env['product.template'].with_context(bin_size=True) + + search_product = Product.search(domain, order=self._get_search_order(post)) + + product_count = len(search_product) + pager = request.website.pager(url=url, total=product_count, page=page, step=ppg, scope=7, url_args=post) + offset = pager['offset'] + products = search_product[offset: offset + ppg] + + ProductAttribute = request.env['product.attribute'] + if products: + # get all products without limit + attributes = ProductAttribute.search([('product_tmpl_ids', 'in', search_product.ids)]) + else: + attributes = ProductAttribute.browse(attributes_ids) + + layout_mode = 'grid' + + values = { + 'search': search, + 'order': post.get('order', ''), + 'pager': pager, + 'add_qty': add_qty, + 'products': products, + 'search_count': product_count, # common for all searchbox + 'bins': TableCompute().process(products, ppg, ppr), + 'ppg': ppg, + 'ppr': ppr, + 'attributes': attributes, + 'keep': keep, + 'layout_mode': layout_mode, + } + return request.render("ssi_product_website.products", values) + + @http.route(['/product_catalog/'], type='http', auth="public", website=True, sitemap=True) + def product(self, product, search='', **kwargs): + if not product.product_catalog: + raise NotFound() + + return request.render("ssi_product_website.product", self._prepare_product_values(product, search, **kwargs)) + + def _prepare_product_values(self, product, search, **kwargs): + add_qty = int(kwargs.get('add_qty', 1)) + + product_context = dict(request.env.context, quantity=add_qty, + active_id=product.id, + partner=request.env.user.partner_id) + + keep = QueryURL('/product_catalog', search=search) + + # Needed to trigger the recently viewed product rpc + view_track = request.website.viewref("ssi_product_website.product").track + + return { + 'search': search, + 'keep': keep, + 'main_object': product, + 'product': product, + 'add_qty': add_qty, + 'view_track': view_track, + } diff --git a/ssi_product_website/data/data.xml b/ssi_product_website/data/data.xml new file mode 100644 index 0000000..7b60fb1 --- /dev/null +++ b/ssi_product_website/data/data.xml @@ -0,0 +1,13 @@ + + + + + + Product Catalog + /product_catalog + + 25 + + + + diff --git a/ssi_product_website/models/__init__.py b/ssi_product_website/models/__init__.py new file mode 100644 index 0000000..5f79f62 --- /dev/null +++ b/ssi_product_website/models/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2023 OpenSynergy Indonesia +# Copyright 2023 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import ( + product_template, +) diff --git a/ssi_product_website/models/product_template.py b/ssi_product_website/models/product_template.py new file mode 100644 index 0000000..b1947f6 --- /dev/null +++ b/ssi_product_website/models/product_template.py @@ -0,0 +1,148 @@ +# Copyright 2023 OpenSynergy Indonesia +# Copyright 2023 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models, tools, _ +from odoo.exceptions import ValidationError, UserError +from odoo.addons.http_routing.models.ir_http import slug +from odoo.addons.website.models import ir_http +from odoo.tools.translate import html_translate +from odoo.osv import expression +from psycopg2.extras import execute_values + + +class ProductTemplate(models.Model): + _inherit = ["product.template", "website.seo.metadata"] + _name = 'product.template' + _mail_post_access = 'read' + _check_company_auto = True + + def _compute_catalog_url(self): + for product in self: + if product.id: + product.catalog_url = "/product_catalog/%s" % slug(product) + + catalog_url = fields.Char( + string='Catalog URL', + compute='_compute_catalog_url') + product_catalog = fields.Boolean( + string='Show in Product Catalog', + default=True, + required=False) + + def _get_combination_info_catalog(self, combination=False, product_id=False, add_qty=1, + parent_combination=False, only_template=False): + self.ensure_one() + # get the name before the change of context to benefit from prefetch + display_name = self.display_name + + display_image = True + quantity = self.env.context.get('quantity', add_qty) + context = dict(self.env.context, quantity=quantity) + product_template = self.with_context(context) + + combination = combination or product_template.env['product.template.attribute.value'] + + if not product_id and not combination and not only_template: + combination = product_template._get_first_possible_combination(parent_combination) + + if only_template: + product = product_template.env['product.product'] + elif product_id and not combination: + product = product_template.env['product.product'].browse(product_id) + else: + product = product_template._get_variant_for_combination(combination) + + if product: + # We need to add the price_extra for the attributes that are not + # in the variant, typically those of type no_variant, but it is + # possible that a no_variant attribute is still in a variant if + # the type of the attribute has been changed after creation. + no_variant_attributes_price_extra = [ + ptav.price_extra for ptav in combination.filtered( + lambda ptav: + ptav.price_extra and + ptav not in product.product_template_attribute_value_ids + ) + ] + if no_variant_attributes_price_extra: + product = product.with_context( + no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra) + ) + list_price = product.price_compute('list_price')[product.id] + price = list_price + display_image = bool(product.image_128) + display_name = product.display_name + price_extra = (product.price_extra or 0.0) + (sum(no_variant_attributes_price_extra) or 0.0) + else: + current_attributes_price_extra = [v.price_extra or 0.0 for v in combination] + product_template = product_template.with_context( + current_attributes_price_extra=current_attributes_price_extra) + price_extra = sum(current_attributes_price_extra) + list_price = product_template.price_compute('list_price')[product_template.id] + price = list_price + display_image = bool(product_template.image_128) + + combination_name = combination._get_combination_name() + if combination_name: + display_name = "%s (%s)" % (display_name, combination_name) + + combination_info = { + 'product_id': product.id, + 'product_template_id': product_template.id, + 'currency_id': product_template.currency_id, + 'display_name': display_name, + 'display_image': display_image, + 'price': price, + 'list_price': list_price, + 'price_extra': price_extra, + } + + if self.env.context.get('website_id'): + current_website = self.env['website'].browse(self.env.context['website_id']) + partner = self.env.user.partner_id + company_id = current_website.company_id + product = self.env['product.product'].browse(combination_info['product_id']) or self + + tax_display = self.user_has_groups( + 'account.group_show_line_subtotals_tax_excluded') and 'total_excluded' or 'total_included' + fpos = self.env['account.fiscal.position'].sudo().get_fiscal_position(partner.id) + taxes = fpos.map_tax(product.sudo().taxes_id.filtered(lambda x: x.company_id == company_id), product, + partner) + + # The list_price is always the price of one. + quantity_1 = 1 + combination_info['price'] = self.env['account.tax']._fix_tax_included_price_company( + combination_info['price'], product.sudo().taxes_id, taxes, company_id) + price = taxes.compute_all(combination_info['price'], product.currency_id, quantity_1, product, partner)[ + tax_display] + list_price = price + combination_info['price_extra'] = self.env['account.tax']._fix_tax_included_price_company( + combination_info['price_extra'], product.sudo().taxes_id, taxes, company_id) + price_extra = \ + taxes.compute_all(combination_info['price_extra'], product.currency_id, quantity_1, product, partner)[ + tax_display] + has_discounted_price = product.currency_id.compare_amounts(list_price, price) == 1 + + combination_info.update( + price=price, + list_price=list_price, + price_extra=price_extra, + has_discounted_price=has_discounted_price, + ) + return combination_info + + def _get_image_holder_catalog(self): + """Returns the holder of the image to use as default representation. + If the product template has an image it is the product template, + otherwise if the product has variants it is the first variant + + :return: this product template or the first product variant + :rtype: recordset of 'product.template' or recordset of 'product.product' + """ + self.ensure_one() + if self.image_128: + return self + variant = self.env['product.product'].browse(self._get_first_possible_variant_id()) + # if the variant has no image anyway, spare some queries by using template + return variant if variant.image_variant_128 else self diff --git a/ssi_product_website/security/ir.model.access.csv b/ssi_product_website/security/ir.model.access.csv new file mode 100644 index 0000000..c4da61b --- /dev/null +++ b/ssi_product_website/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_product_product_public,product.product.public,product.model_product_product,,1,0,0,0 +access_product_template_public,product.template.public,product.model_product_template,,1,0,0,0 +access_product_category_public,product.category.public,product.model_product_category,,1,0,0,0 +access_product_attribute_public,product.attribute public,product.model_product_attribute,,1,0,0,0 +access_product_attribute_value_public,product.attribute value public,product.model_product_attribute_value,,1,0,0,0 +access_product_product_attribute,product.template.attribute value public,product.model_product_template_attribute_value,,1,0,0,0 +access_product_template_attribute_exclusion,product.template.attribute exclusion public,product.model_product_template_attribute_exclusion,,1,0,0,0 +access_product_template_attribute_line_public,product.template.attribute line public,product.model_product_template_attribute_line,,1,0,0,0 \ No newline at end of file diff --git a/ssi_product_website/static/description/icon.png b/ssi_product_website/static/description/icon.png new file mode 100644 index 0000000..fb45154 Binary files /dev/null and b/ssi_product_website/static/description/icon.png differ diff --git a/ssi_product_website/static/src/scss/primary_variables.scss b/ssi_product_website/static/src/scss/primary_variables.scss new file mode 100644 index 0000000..7aceaa0 --- /dev/null +++ b/ssi_product_website/static/src/scss/primary_variables.scss @@ -0,0 +1 @@ +$o-pcatalog-products-layout-grid-ratio: 1.0 !default; diff --git a/ssi_product_website/static/src/scss/ssi_product_website.scss b/ssi_product_website/static/src/scss/ssi_product_website.scss new file mode 100644 index 0000000..4d2f1c0 --- /dev/null +++ b/ssi_product_website/static/src/scss/ssi_product_website.scss @@ -0,0 +1,526 @@ +// Prevent grid gutter to be higher that bootstrap gutter width to make sure +// the negative margin layout does not overflow on elements. This prevents the +// use of an ugly overflow: hidden which would break box-shadows. +$o-pcatalog-products-layout-grid-gutter-width: $grid-gutter-width / 2 !default; +$o-pcatalog-products-layout-grid-gutter-width: min($grid-gutter-width / 2, $o-pcatalog-products-layout-grid-gutter-width); + +@mixin pcatalog-break-table-to-list() { + .o_pcatalog_products_grid_table_wrapper > table, + .o_pcatalog_products_grid_table_wrapper > table > tbody, + .o_pcatalog_products_grid_table_wrapper > table > tbody > tr, + .o_pcatalog_products_grid_table_wrapper > table > tbody > tr > td { + display: block; + width: 100%; + } +} + +.oe_ssi_product_website { + ul ul { + margin-left: 1.5rem; + } + .o_payment_form .card { + border-radius: 4px !important; + } + .address-inline address { + display: inline-block; + } + + h1[itemprop="name"], .td-product_name { + word-break: break-word; + word-wrap: break-word; + overflow-wrap: break-word; + } + + @include media-breakpoint-down(sm) { + .td-img { + display: none; + } + } + + .toggle_summary_div { + @include media-breakpoint-up(xl) { + max-width: 400px; + } + } + input.js_quantity { + min-width: 48px; + text-align: center; + } + input.quantity { + padding: 0; + } +} + +.o_alternative_product { + margin: auto; +} + +// Base style for a product card with image/description +.oe_product_catalog { + .oe_product_image { + height: 0; + text-align: center; + + img { + height: 100%; + width: 100%; + object-fit: scale-down; + } + } + .o_pcatalog_product_information { + position: relative; + flex: 0 0 auto; + transition: .3s ease; + } + .oe_subdescription { + max-height: 0; + overflow: hidden; + font-size: $font-size-sm; + margin-bottom: map-get($spacers, 1); + transform: scale(1, 0); + transition: all ease 0.3s; + } + .o_pcatalog_product_btn { + @include o-position-absolute(auto, 0, 100%, 0); + padding-bottom: map-get($spacers, 1); + + .btn { + transform: scale(0); + transition: transform ease 200ms 0s; + } + + &:empty { + display: none !important; + } + } + + &:hover { + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1); + + .o_pcatalog_product_information { + background-color: gray('200') !important; + } + .oe_subdescription { + max-height: $line-height-base * 1em; // Max 1 line + @include media-breakpoint-up(lg) { + max-height: $line-height-base * 2em; // Max 2 lines + } + @include media-breakpoint-up(xl) { + max-height: $line-height-base * 3em; // Max 3 lines + } + } + .oe_subdescription, + .o_pcatalog_product_btn .btn { + transform: scale(1); + } + } + + @include media-breakpoint-down(sm) { + &, &:hover { + .oe_subdescription { + max-height: $line-height-base * 3em; // Max 3 lines + } + } + .oe_subdescription, + .o_pcatalog_product_btn .btn { + transform: scale(1); + } + } +} + +// Options relative to where the product card is put +.oe_product { + // Image full option + &.oe_image_full { + .oe_product_image { + @include border-bottom-radius($card-inner-border-radius); + } + .o_pcatalog_product_information { + @include o-position-absolute(auto, 0, 0, 0); // The wrapper is always relatively positioned + } + } +} + +#products_grid { + .o_pcatalog_products_grid_table_wrapper > .table { + table-layout: fixed; + + > tbody { + > tr > td { + margin-top: $o-pcatalog-products-layout-grid-gutter-width; // For list and mobile design + padding: 0; + + @if $o-pcatalog-products-layout-grid-gutter-width <= 0 { + border: $card-border-width solid $card-border-color; + } + } + + > tr:first-child > td:first-child { + margin-top: 0; // For list and mobile design + } + } + + .o_pcatalog_product_grid_wrapper { + position: relative; + + @for $x from 1 through 4 { + @for $y from 1 through 4 { + &.o_pcatalog_product_grid_wrapper_#{$x}_#{$y} { + padding-top: 100% * $o-pcatalog-products-layout-grid-ratio * $y / $x; + } + } + } + + > * { + $-pos: ($o-pcatalog-products-layout-grid-gutter-width / 2); + @include o-position-absolute($-pos, $-pos, $-pos, $-pos); + + @if $o-pcatalog-products-layout-grid-gutter-width <= 0 { + &.card { + border: none; + + &, .card-body { + border-radius: 0; + } + } + } + } + } + } + + .o_pcatalog_products_grid_table_wrapper { + // Necessary to compensate the outer border-spacing of the table. No + // overflow will occur as the gutter width cannot be higher than the + // BS4 grid gutter and the vertical margins of the wrapper's parent are + // set accordingly. + // Note: a possible layout could also be ok by removing the wrapper + // related spacings and setting a background to it, thus including the + // outer border spacing as part of the design. + margin: (-$o-pcatalog-products-layout-grid-gutter-width / 2); + } + + @include media-breakpoint-down(sm) { + @include pcatalog-break-table-to-list(); + + .table .o_pcatalog_product_grid_wrapper { + padding-top: 100% !important; + } + } + + &.o_pcatalog_layout_list { + @include media-breakpoint-up(sm) { + @include pcatalog-break-table-to-list(); + + .o_pcatalog_products_grid_table_wrapper { + margin: 0; + } + + .table .o_pcatalog_product_grid_wrapper { + padding-top: 0 !important; + + > * { + @include o-position-absolute(0, 0, 0, 0); + position: relative; + } + } + + .oe_product_catalog { + $-pcatalog-list-layout-height: 10rem; + + flex-flow: row nowrap; + min-height: $-pcatalog-list-layout-height; + + .oe_product_image { + flex: 0 0 auto; + width: $-pcatalog-list-layout-height; + max-width: 35%; + min-width: 100px; + height: auto; + } + .o_pcatalog_product_information { + position: static; + display: flex; + flex: 1 1 auto; + text-align: left !important; + } + .o_pcatalog_product_information_text { + flex: 1 1 auto; + } + .o_pcatalog_product_btn { + flex: 0 0 auto; + position: static; + display: flex; + flex-flow: column nowrap; + align-items: center; + padding: map-get($spacers, 2); + background-color: gray('200'); + + .btn + .btn { + margin-top: map-get($spacers, 2); + } + } + + .oe_subdescription { + max-height: none !important; + } + .oe_subdescription, + .o_pcatalog_product_btn .btn { + transform: scale(1) !important; + } + + &:hover { + .o_pcatalog_product_information { + background-color: $white !important; + } + } + } + } + } +} + +.o_pcatalog_products_main_row { + margin-top: $grid-gutter-width / 2; + margin-bottom: $grid-gutter-width / 2; +} + +div#payment_method { + div.list-group { + margin-left: 40px; + } + + .list-group-item { + padding-top: 5px; + padding-bottom: 5px; + } +} + +ul.wizard { + padding: 0; + margin-top: 20px; + list-style: none outside none; + border-radius: 4px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.065); + + li { + border: 1px solid gray('200'); + border-right-width: 0; + position: relative; + float: left; + padding: 0 10px 0 20px; + margin: 0; + line-height: 38px; + background: #fbfbfb; + + .chevron { + position: absolute; + top: 0; + right: -10px; + z-index: 1; + display: block; + border: 20px solid transparent; + border-right: 0; + border-left: 10px solid gray('200'); + } + + .chevron:before { + position: absolute; + top: -20px; + right: 1px; + display: block; + border: 20px solid transparent; + border-right: 0; + border-left: 10px solid #fbfbfb; + content: ""; + } + + .o_link_disable { + text-decoration: none; + color: inherit; + cursor: text; + } + + &.text-success { + background: #f3f4f5; + } + + &.text-success .chevron:before { + border-left: 10px solid #f5f5f5; + } + + &.text-primary { + background: #f1f6fc; + } + + &.text-primary .chevron:before { + border-left: 10px solid #f1f6fc; + } + + &:first-child { + padding-left: 15px; + border-radius: 4px 0 0 4px; + } + + &:last-child { + border-radius: 0 4px 4px 0; + border-right-width: 1px; + + .chevron { + display: none; + } + } + } +} + +tr#empty { + display: none; +} + +.js_change_shipping { + cursor: pointer; +} + +a.no-decoration { + cursor: pointer; + text-decoration: none !important; +} + +#o-carousel-product { + + &.css_not_available { + opacity: 0.2; + } + + .carousel-outer { + height: 400px; + max-height: 90vh; + + .carousel-inner { + img { + height: 100%; + width: 100%; + object-fit: scale-down; + } + } + } + + .carousel-control-prev, .carousel-control-next { + height: 70%; + top: 15%; + opacity: 0.5; + cursor: pointer; + &:focus { + opacity: 0.65; + } + &:hover { + opacity: 0.8; + } + > span { + background: rgba(0, 0, 0, 0.8); + } + } + + .carousel-indicators { + li { + width: 64px; + height: 64px; + text-indent: unset; + border: 1px solid gray('600'); + opacity: 0.5; + position: relative; + + .o_product_video_thumb { + @include o-position-absolute($top: 50%, $left: 50%); + transform: translate(-50%, -50%); + color: gray('400'); + } + &.active { + opacity: 1; + border: 1px solid theme-color('primary'); + } + } + } +} + +.ecom-zoomable { + &:not(.ecom-autozoom) { + img[data-zoom] { + cursor: zoom-in; + } + } + &.ecom-autozoom { + img[data-zoom] { + cursor: crosshair; + } + } + .o_editable img[data-zoom] { + cursor: pointer; + } +} + +#coupon_box form { + max-width: 300px; +} + +.o_ssi_product_website_animate { + opacity: 0.7; + position: absolute !important; + height: 150px; + width: 150px; + z-index: 1020; +} + +.o_red_highlight { + background: theme-color('danger') !important; + box-shadow: 0 0 0 0 rgba(240,8,0,0.4); + transition: all 0.5s linear; +} + +.o_shadow_animation { + box-shadow: 0 0 5px 10px rgba(240,8,0,0.4)!important; +} + +/* product recently viewed snippet */ + +.o_carousel_product_card { + .o_carousel_product_card_img_top { + object-fit: scale-down; + @include media-breakpoint-down(sm) { + height: 12rem; + } + @include media-breakpoint-up(md) { + height: 8rem; + } + @include media-breakpoint-up(lg) { + height: 12rem; + } + } + .o_carousel_product_img_link:hover + .o_carousel_product_remove { + display: block; + } +} + +.o_carousel_product_card_wrap { + @include media-breakpoint-up(sm) { + float: left; + } +} + +.o_carousel_product_control { + top: percentage(1/3); + bottom: percentage(1/3); + width: 2rem; + border-radius: 5px; + background-color: $o-enterprise-primary-color; +} + +.o_carousel_product_remove { + position: absolute; + display: none; + cursor: pointer; + right: 5%; + top: 5%; +} + +.o_carousel_product_remove:hover { + display: block; +} diff --git a/ssi_product_website/static/src/scss/ssi_product_website_frontend.scss b/ssi_product_website/static/src/scss/ssi_product_website_frontend.scss new file mode 100644 index 0000000..5416858 --- /dev/null +++ b/ssi_product_website/static/src/scss/ssi_product_website_frontend.scss @@ -0,0 +1,140 @@ +//## Product Catalog frontent design +//## ---------------------------- + +// Theming variables +$o-pcatalog-wizard-thickness: 0.125rem; +$o-pcatalog-wizard-dot-size: 0.625rem; +$o-pcatalog-wizard-dot-active-glow: 0.25rem; + +$o-pcatalog-wizard-color-inner: white; +$o-pcatalog-wizard-color-default: gray('200'); + +$o-pcatalog-wizard-dot-active: theme-color('primary'); +$o-pcatalog-wizard-dot-completed: theme-color('success'); + +$o-pcatalog-wizard-label-default: $text-muted; +$o-pcatalog-wizard-label-active: $body-color; +$o-pcatalog-wizard-label-completed: $success; + +.progress-wizard { + // Scoped variables + $tmp-dot-radius: ($o-pcatalog-wizard-dot-size + $o-pcatalog-wizard-thickness)*0.5; + $tmp-check-size: max($font-size-base, $o-pcatalog-wizard-dot-size + $o-pcatalog-wizard-thickness + $o-pcatalog-wizard-dot-active-glow*2); + $tmp-check-pos: $o-pcatalog-wizard-dot-size*0.5 - $tmp-check-size*0.5; + + margin-top: $grid-gutter-width*0.5; + padding: 0 $grid-gutter-width*0.5; + + @include media-breakpoint-up(md) { + padding: 0; + } + + .progress-wizard-step { + position: relative; + + @include media-breakpoint-up(md) { + margin-top: $tmp-dot-radius + $o-pcatalog-wizard-thickness*3.5; + float: left; + width: percentage(1/3); + + .o_wizard_has_extra_step + & { + width: percentage(1/4); + } + } + @include media-breakpoint-down(sm) { + &.disabled, &.complete { + display:none; + } + } + .progress-wizard-dot { + width: $o-pcatalog-wizard-dot-size; + height: $o-pcatalog-wizard-dot-size; + position: relative; + display: inline-block; + background-color: $o-pcatalog-wizard-color-inner; + border-radius: 50%; + box-shadow: 0 0 0 $o-pcatalog-wizard-thickness $o-pcatalog-wizard-color-default; + + @include media-breakpoint-up(md) { + @include o-position-absolute($left: 50%); + margin: (-$tmp-dot-radius) 0 0 (-$o-pcatalog-wizard-dot-size*0.5); + } + } + + .progress-wizard-steplabel { + color: $o-pcatalog-wizard-label-default; + margin: 5px 0 5px 5px; + font-size: $font-size-base; + display: inline-block; + + @include media-breakpoint-up(md) { + display: block; + margin: (0.625rem + $tmp-dot-radius) 0 20px 0; + } + @include media-breakpoint-down(sm) { + margin-left: -15px; + font-size: 24px; + } + } + + .progress-wizard-bar { + height: $o-pcatalog-wizard-thickness; + background-color: $o-pcatalog-wizard-color-default; + } + + &.active { + .progress-wizard-dot { + animation: fadeIn 1s ease 0s 1 normal none running; + background: $o-pcatalog-wizard-dot-active; + box-shadow: 0 0 0 ($o-pcatalog-wizard-dot-active-glow - 0.0625rem) $o-pcatalog-wizard-color-inner, + 0 0 0 $o-pcatalog-wizard-dot-active-glow rgba($o-pcatalog-wizard-dot-active, 0.5); + } + + .progress-wizard-steplabel { + color: $o-pcatalog-wizard-label-active; + font-weight: bolder; + } + } + + &.complete { + .progress-wizard-dot { + background: none; + box-shadow: none; + + &:after { + @include o-position-absolute($tmp-check-pos, $left: $tmp-check-pos); + width: $tmp-check-size; + height: $tmp-check-size; + border-radius: 100%; + + background: $o-pcatalog-wizard-color-inner; + color: $o-pcatalog-wizard-dot-completed; + text-align: center; + line-height: 1; + font-size: $tmp-check-size; + font-family: FontAwesome; + + content: "\f058"; + } + } + + .progress-wizard-steplabel { + color: $o-pcatalog-wizard-label-completed; + } + + &:hover:not(.disabled) { + .progress-wizard-dot:after { + color: $o-pcatalog-wizard-label-completed; + } + + .progress-wizard-steplabel { + color: $o-pcatalog-wizard-label-active; + } + } + } + + &.disabled { + cursor: default; + } + } +} diff --git a/ssi_product_website/views/product_template_views.xml b/ssi_product_website/views/product_template_views.xml new file mode 100644 index 0000000..7d65b58 --- /dev/null +++ b/ssi_product_website/views/product_template_views.xml @@ -0,0 +1,25 @@ + + + + + + + product.template.form + product.template + + + + +
+ +
+
+ +
+
+ +
+
diff --git a/ssi_product_website/views/templates.xml b/ssi_product_website/views/templates.xml new file mode 100644 index 0000000..cbf5aac --- /dev/null +++ b/ssi_product_website/views/templates.xml @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +