[MRG] website_sale_coupon: move to community

This commit is contained in:
Jeremy Kersten
2019-04-01 14:11:05 +02:00
15 changed files with 514 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "Coupons & Promotions for eCommerce",
'summary': """Use coupon & promotion programs in your eCommerce store""",
'description': """
Create coupon and promotion codes to share in order to boost your sales (free products, discounts, etc.). Shoppers can use them in the eCommerce checkout.
Coupon & promotion programs can be edited in the Catalog menu of the Website app.
""",
'category': 'Website',
'version': '1.0',
'depends': ['website_sale', 'sale_coupon'],
'data': [
'views/website_sale_templates.xml',
'views/res_config_settings_views.xml',
'views/sale_coupon_program_views.xml',
],
'auto_install': True,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
from odoo import http
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.http import request
class WebsiteSale(WebsiteSale):
@http.route(['/shop/pricelist'])
def pricelist(self, promo, **post):
order = request.website.sale_get_order()
coupon_status = request.env['sale.coupon.apply.code'].sudo().apply_coupon(order, promo)
if coupon_status.get('not_found'):
return super(WebsiteSale, self).pricelist(promo, **post)
elif coupon_status.get('error'):
request.session['error_promo_code'] = coupon_status['error']
return request.redirect(post.get('r', '/shop/cart'))
@http.route(['/shop/payment'], type='http', auth="public", website=True)
def payment(self, **post):
order = request.website.sale_get_order()
order.recompute_coupon_lines()
return super(WebsiteSale, self).payment(**post)
@http.route(['/shop/cart'], type='http', auth="public", website=True)
def cart(self, **post):
order = request.website.sale_get_order()
order.recompute_coupon_lines()
return super(WebsiteSale, self).cart(**post)
def _update_website_sale_coupon(self, **post):
order = request.website.sale_get_order()
order.recompute_coupon_lines()
free_shipping_lines = order._get_free_shipping_lines()
currency = order.currency_id
result = {}
if free_shipping_lines:
amount_free_shipping = sum(free_shipping_lines.mapped('price_subtotal'))
result.update({
'new_amount_delivery': self._format_amount(0.0, currency),
'new_amount_untaxed': self._format_amount(order.amount_untaxed, currency),
'new_amount_tax': self._format_amount(order.amount_tax, currency),
'new_amount_total': self._format_amount(order.amount_total, currency),
'new_amount_order_discounted': self._format_amount(order.reward_amount - amount_free_shipping, currency)
})
return result
# Override
# Add in the rendering the free_shipping_line
def _get_shop_payment_values(self, order, **kwargs):
values = super(WebsiteSale, self)._get_shop_payment_values(order, **kwargs)
values['free_shipping_lines'] = order._get_free_shipping_lines()
return values
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import sale_coupon_program
from . import sale_order
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, _
from odoo.exceptions import ValidationError
class SaleCouponProgram(models.Model):
_name = 'sale.coupon.program'
_inherit = ['sale.coupon.program', 'website.multi.mixin']
@api.constrains('promo_code', 'website_id')
def _check_promo_code_constraint(self):
""" Only case where multiple same code could coexists is if they all belong to their own website.
If the program is website generic, we should ensure there is no generic and no specific (even for other website) already
If the program is website specific, we should ensure there is no existing code for this website or False
"""
for program in self.filtered(lambda p: p.promo_code):
domain = [('id', '!=', program.id), ('promo_code', '=', program.promo_code)]
if program.website_id:
domain += program.website_id.website_domain()
if self.search(domain):
raise ValidationError(_('The program code must be unique by website!'))
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.http import request
class SaleOrder(models.Model):
_inherit = "sale.order"
@api.one
def _compute_website_order_line(self):
""" This method will merge multiple discount lines generated by a same program
into a single one (temporary line with `new()`).
This case will only occur when the program is a discount applied on multiple
products with different taxes.
In this case, each taxes will have their own discount line. This is required
to have correct amount of taxes according to the discount.
But we wan't these lines to be `visually` merged into a single one in the
e-commerce since the end user should only see one discount line.
This is only possible since we don't show taxes in cart.
eg:
line 1: 10% discount on product with tax `A` - $15
line 2: 10% discount on product with tax `B` - $11.5
line 3: 10% discount on product with tax `C` - $10
would be `hidden` and `replaced` by
line 1: 10% discount - $36.5
Note: The line will be created without tax(es) and the amount will be computed
depending if B2B or B2C is enabled.
"""
super(SaleOrder, self)._compute_website_order_line()
for program in self._get_applied_programs_with_rewards_on_current_order():
program_lines = self.order_line.filtered(lambda line: line.product_id == program.discount_line_product_id)
if len(program_lines) > 1:
price_unit = sum(l.price_subtotal if self.env.user.has_group('sale.group_show_price_subtotal') else l.price_total for l in program_lines)
self.website_order_line += self.env['sale.order.line'].new({
'product_id': program_lines[0].product_id.id,
'price_unit': price_unit,
'name': program_lines[0].name,
'product_uom_qty': 1,
'product_uom': program_lines[0].product_uom.id,
'order_id': self.id,
'is_reward_line': True,
})
self.website_order_line -= program_lines
def _compute_cart_info(self):
super(SaleOrder, self)._compute_cart_info()
for order in self:
reward_lines = order.website_order_line.filtered(lambda line: line.is_reward_line)
order.cart_quantity -= int(sum(reward_lines.mapped('product_uom_qty')))
def get_promo_code_error(self, delete=True):
error = request.session.get('error_promo_code')
if error and delete:
request.session.pop('error_promo_code')
return error
@api.multi
def _cart_update(self, product_id=None, line_id=None, add_qty=0, set_qty=0, **kwargs):
res = super(SaleOrder, self)._cart_update(product_id=product_id, line_id=line_id, add_qty=add_qty, set_qty=set_qty, **kwargs)
self.recompute_coupon_lines()
return res
def _get_free_shipping_lines(self):
self.ensure_one()
free_shipping_prgs_ids = self.no_code_promo_program_ids.filtered(lambda p: p.reward_type == 'free_shipping')
if not free_shipping_prgs_ids:
return self.env['sale.order.line']
free_shipping_product_ids = free_shipping_prgs_ids.mapped('discount_line_product_id')
return self.order_line.filtered(lambda l: l.product_id in free_shipping_product_ids)
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="98.162%" x2="0%" y1="1.838%" y2="100%"><stop offset="0%" stop-color="#797DA5"/><stop offset="50.799%" stop-color="#6D7194"/><stop offset="100%" stop-color="#626584"/></linearGradient><path id="d" d="M50 31.035a3.5 3.5 0 1 0 0 6.93v3.06a1.633 1.633 0 0 1-1.588 1.286L26.974 43l.998 4h18.955c.998 0 .998 2 0 2h-20.95L19.99 25h-1.996v1c0 .667-.332 1-.997 1S16 26.667 16 26v-2c.066-.667.398-1 .998-1h3.99c.516 0 .848.333.998 1l.998 3.281 25.428.037c1.045 0 1.588.761 1.588 2.018v1.7zM45.43 55a2.497 2.497 0 0 1-2.493-2.5c0-1.38 1.116-2.5 2.494-2.5a2.497 2.497 0 0 1 2.494 2.5c0 1.38-1.117 2.5-2.494 2.5zm-18.955 0a2.497 2.497 0 0 1-2.494-2.5c0-1.38 1.117-2.5 2.494-2.5a2.497 2.497 0 0 1 2.495 2.5c0 1.38-1.117 2.5-2.495 2.5zm12.7-24.996a.51.51 0 0 0-.409.2l-7.668 9.056c-.233.31.004.738.41.738l.696-.002a.51.51 0 0 0 .409-.2l7.63-9.056c.234-.312-.005-.74-.41-.738l-.658.002zm-5.841 4.371c1.29 0 2.334-.979 2.334-2.188 0-1.208-1.044-2.187-2.334-2.187S31 30.979 31 32.188c0 1.208 1.044 2.187 2.334 2.187zm0-3.125c.552 0 1 .42 1 .938 0 .517-.448.937-1 .937s-1-.42-1-.938c0-.517.448-.937 1-.937zm4.668 4.375c-1.29 0-2.334.979-2.334 2.188 0 1.208 1.044 2.187 2.334 2.187s2.334-.979 2.334-2.188c0-1.208-1.044-2.187-2.334-2.187zm0 3.125c-.553 0-1-.42-1-.938 0-.517.447-.937 1-.937.552 0 1 .42 1 .938 0 .517-.448.937-1 .937z"/><path id="e" d="M50 30a3 3 0 0 0 0 6v3.025a1.633 1.633 0 0 1-1.588 1.286L26.974 41l.998 4h18.955c.998 0 .998 2 0 2h-20.95L19.99 23h-1.996v1c0 .667-.332 1-.997 1S16 24.667 16 24v-2c.066-.667.398-1 .998-1h3.99c.516 0 .848.333.998 1l.998 3.281 25.428.037c1.045 0 1.588.761 1.588 2.018V30zm-4.57 23a2.497 2.497 0 0 1-2.493-2.5c0-1.38 1.116-2.5 2.494-2.5a2.497 2.497 0 0 1 2.494 2.5c0 1.38-1.117 2.5-2.494 2.5zm-18.955 0a2.497 2.497 0 0 1-2.494-2.5c0-1.38 1.117-2.5 2.494-2.5a2.497 2.497 0 0 1 2.495 2.5c0 1.38-1.117 2.5-2.495 2.5zM48 26a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm0 11a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm-8.825-8.996a.51.51 0 0 0-.409.2l-7.668 9.056c-.233.31.004.738.41.738l.696-.002a.51.51 0 0 0 .409-.2l7.63-9.056c.234-.312-.005-.74-.41-.738l-.658.002zm-5.841 4.371c1.29 0 2.334-.979 2.334-2.188 0-1.208-1.044-2.187-2.334-2.187S31 28.979 31 30.188c0 1.208 1.044 2.187 2.334 2.187zm0-3.125c.552 0 1 .42 1 .938 0 .517-.448.937-1 .937s-1-.42-1-.938c0-.517.448-.937 1-.937zm4.668 4.375c-1.29 0-2.334.979-2.334 2.188 0 1.208 1.044 2.187 2.334 2.187s2.334-.979 2.334-2.188c0-1.208-1.044-2.187-2.334-2.187zm0 3.125c-.553 0-1-.42-1-.938 0-.517.447-.937 1-.937.552 0 1 .42 1 .938 0 .517-.448.937-1 .937z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M33.317 69H4c-2 0-4-1-4-4V38.29l16.258-16.95L21 21l2 5h26.583l.371 3.987-3.862 4.883 3.764 1.445-.11 3.27L45.076 45H47l.507 1.61-1.993 1.943 1.815 3.434L33.317 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#d"/><use fill="#FFF" fill-rule="nonzero" xlink:href="#e"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,171 @@
odoo.define('website_sale_coupon.test', function (require) {
'use strict';
require("website_sale.tour");
var tour = require("web_tour.tour");
var ajax = require('web.ajax');
tour.register('shop_sale_coupon', {
test: true,
url: '/shop?search=Large%20Cabinet',
},
[
{
content: "open customize menu",
extra_trigger: '.oe_website_sale .o_wsale_products_searchbar_form',
trigger: '#customize-menu > a',
},
{
content: "enable 'Show # found' if needed",
trigger: "#customize-menu a:contains(Show # found)",
run: function () {
if (!$('#customize-menu a:contains(Show # found) input').prop('checked')) {
$('#customize-menu a:contains(Show # found)').click();
}
}
},
/* 1. Buy 1 Large Cabinet, enable coupon code & insert 10% code */
{
content: "select Large Cabinet",
extra_trigger: '.oe_search_found',
trigger: '.oe_product_cart a:contains("Large Cabinet")',
},
{
content: "add 2 Large Cabinet into cart",
trigger: '#product_details input[name="add_qty"]',
run: "text 2",
},
{
content: "click on 'Add to Cart' button",
trigger: "a:contains(Add to Cart)",
},
{
content: "open customize menu",
extra_trigger: '.oe_website_sale .oe_cart',
trigger: '#customize-menu > a',
},
{
content: "enable 'Promo Code' if needed",
trigger: "#customize-menu a:contains(Promo Code)",
run: function () {
if (!$('#customize-menu a:contains(Promo Code) input').prop('checked')) {
$('#customize-menu a:contains(Promo Code)').click();
}
}
},
{
content: "click on 'I have a promo code'",
extra_trigger: '.show_coupon',
trigger: '.show_coupon',
},
{
content: "insert promo code '10pc'",
extra_trigger: 'form[name="coupon_code"]',
trigger: 'form[name="coupon_code"] input[name="promo"]',
run: "text 10pc",
},
{
content: "validate the coupon",
trigger: 'form[name="coupon_code"] .a-submit',
},
{
content: "check reward product",
trigger: '.reward_product:contains("10.0% discount on total amount")',
run: function () {}, // it's a check
},
/* 2. Add some cabinet to get a free one, play with quantity */
{
content: "go to shop",
trigger: '.reward_product:contains("10.0% discount on total amount")',
run: function () {
ajax.jsonRpc('/web/dataset/call', 'call', {
model: 'account.tax',
method: 'create',
args: [{
'name':'15% tax incl ' + _.now(),
'amount': 15,
}],
}).then(function (tax_id) {
ajax.jsonRpc('/web/dataset/call', 'call', {
model: 'product.template',
method: 'create',
args: [{
'name': 'Taxed Product',
'taxes_id': [([6, false, [tax_id]])],
'list_price': 100,
'website_published': true,
}],
}).then(function (data) {
location.href = '/shop';
});
});
},
},
{
content: "type Taxed Product in search",
trigger: 'form input[name="search"]',
run: "text Taxed Product",
},
{
content: "start search",
trigger: 'form:has(input[name="search"]) .oe_search_button',
},
{
content: "select Taxed Product",
extra_trigger: '.oe_search_found', // Wait to be on search results or it sometimes throws concurent error (sent search form + click on product on /shop)
trigger: '.oe_product_cart a:containsExact("Taxed Product")',
},
{
content: "click on 'Add to Cart' button",
trigger: "a:contains(Add to Cart)",
},
{
content: "check reduction amount got recomputed and merged both discount lines into one only",
extra_trigger: '.oe_currency_value:contains("-75.50"):not(#cart_total .oe_currency_value:contains("-75.50"))',
trigger: '.oe_website_sale .oe_cart',
run: function () {}, // it's a check
},
/* 3. Add some cabinet to get a free one, play with quantity */
{
content: "add one Large Cabinet",
trigger: '#cart_products input.js_quantity',
run: "text 3",
},
{
content: "check reduction amount got recomputed when changing qty",
trigger: '.oe_currency_value:contains("-107.50")',
run: function () {}, // it's a check
},
{
content: "add more Large Cabinet into cart",
trigger: '#cart_products input.js_quantity',
run: "text 4",
},
{
content: "check free product is added",
trigger: '#wrap:has(.reward_product:contains("Free Product - Large Cabinet"))',
run: function () {}, // it's a check
},
{
content: "remove one cabinet from cart",
trigger: '#cart_products input.js_quantity[value="4"]',
run: "text 3",
},
{
content: "check free product is removed",
trigger: '#wrap:not(:has(.reward_product:contains("Free Product - Large Cabinet")))',
run: function () {}, // it's a check
},
/* 4. Check /shop/payment does not break the `merged discount lines split per tax` (eg: with _compute_tax_id) */
{
content: "go to checkout",
trigger: 'a[href="/shop/checkout?express=1"]',
},
{
content: "check total is unchanged",
trigger: 'tr#order_total .oe_currency_value:contains("967.50")',
run: function () {}, // it's a check
},
]
);
});
@@ -0,0 +1 @@
from . import test_shop_sale_coupon
@@ -0,0 +1,19 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
class TestUi(odoo.tests.HttpCase):
post_install = True
at_install = False
def test_01_admin_shop_sale_coupon_tour(self):
# pre enable "Show # found" option to avoid race condition...
self.env.ref("website_sale.search count").write({"active": True})
self.phantom_js(
"/",
"odoo.__DEBUG__.services['web_tour.tour'].run('shop_sale_coupon')",
"odoo.__DEBUG__.services['web_tour.tour'].tours.shop_sale_coupon.ready",
login="admin",
)
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.website.sale.coupon</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='website_sale_coupon']" position="after">
<div class="content-group">
<div class="mt16" attrs="{'invisible': [('module_sale_coupon', '=', False)]}">
<button name="%(sale_coupon.sale_coupon_program_action_promo_program)d" icon="fa-arrow-right" type="action" string="Promotion Programs" class="btn-link"/>
</div>
<div attrs="{'invisible': [('module_sale_coupon', '=', False)]}">
<button name="%(sale_coupon.sale_coupon_program_action_coupon_program)d" icon="fa-arrow-right" type="action" string="Coupon Programs" class="btn-link"/>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem action="sale_coupon.sale_coupon_program_action_promo_program" id="menu_promotion_type_config" name="Promotion Programs" parent="website_sale.menu_catalog" groups="sales_team.group_sale_manager" sequence="50"/>
<menuitem id="menu_coupon_type_config" action="sale_coupon.sale_coupon_program_action_coupon_program" name="Coupon Programs" parent="website_sale.menu_catalog" groups="sales_team.group_sale_manager" sequence="51"/>
<record model="ir.ui.view" id="sale_coupon_program_view_form_common_website">
<field name="name">sale.coupon.program.common.form</field>
<field name="model">sale.coupon.program</field>
<field name="inherit_id" ref="sale_coupon.sale_coupon_program_view_form_common"/>
<field name="arch" type="xml">
<group name="validity" position="inside">
<label for="website_id" groups="website.group_multi_website"/>
<div>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
</div>
</group>
</field>
</record>
<record id="sale_coupon_program_view_tree_website" model="ir.ui.view">
<field name="name">sale.coupon.program.tree</field>
<field name="model">sale.coupon.program</field>
<field name="inherit_id" ref="sale_coupon.sale_coupon_program_view_tree"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="website_id" groups="website.group_multi_website"/>
</field>
</field>
</record>
</odoo>
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Shop Sale Coupon">
<xpath expr="." position="inside">
<script type="text/javascript" src="/website_sale_coupon/static/src/js/website_sale_coupon.test.js"></script>
</xpath>
</template>
<template id="website_sale_coupon_cart_hide_qty" inherit_id="website_sale.cart_lines" active="True">
<xpath expr="//td[hasclass('td-qty')]/div" position="attributes">
<attribute name="t-att-style">line.is_reward_line and "display:none;"</attribute>
</xpath>
<xpath expr="//table[@id='cart_products']/tbody//tr//td[hasclass('td-product_name')]//div" position="attributes">
<!-- Can't xpath "//table[@id='cart_products']/tbody//tr" since optional_product already xpath that one.
There will be the same xpath attribute class on the same element. Last installed module will only be taken into account -->
<attribute name="t-att-class">'reward_product' if line.is_reward_line else None</attribute>
</xpath>
<xpath expr="//td[hasclass('td-qty')]/div" position="after">
<t t-if="line.is_reward_line">
<span class="js_quantity text-muted" t-esc="int(line.product_uom_qty)"/>
</t>
</xpath>
</template>
<template id="sale_coupon_result" inherit_id="website_sale.coupon_form">
<xpath expr="//form[@name='coupon_code']" position="after">
<t t-if="website_sale_order and website_sale_order.applied_coupon_ids">
<t t-foreach="website_sale_order.applied_coupon_ids" t-as="coupon">
<div class="card bg-success text-white mt16">
<div class="card-header clearfix">
<span class="float-left">You have successfully applied following promo code: <b t-esc="coupon.code"/></span>
</div>
</div>
</t>
</t>
<t t-if="website_sale_order and website_sale_order.promo_code">
<div class="card bg-success text-white mt16">
<div class="card-header clearfix">
<span class="float-left">You have successfully applied following promo code: <b t-esc="website_sale_order.promo_code"/></span>
</div>
</div>
</t>
<t t-if="website_sale_order and website_sale_order.generated_coupon_ids">
<t t-foreach="website_sale_order.generated_coupon_ids.filtered(lambda c: c.state != 'expired')" t-as="coupon">
<div class="card bg-success text-white mt16">
<div class="card-header clearfix">
<span class="float-left">Your reward <b t-esc="coupon.discount_line_product_id.name"/> is available on a next order with this promo code: <b t-esc="coupon.code"/></span>
</div>
</div>
</t>
</t>
<t t-if="request.params.get('code_not_available')">
<div class="card bg-danger text-white mt16">
<div class="card-header clearfix">
<span class="float-left">Invalid or expired promo code</span>
</div>
</div>
</t>
<t t-if="website_sale_order.get_promo_code_error(delete=False)">
<div class="card bg-danger text-white mt16">
<div class="card-header clearfix">
<span class="float-left"><t t-esc="website_sale_order.get_promo_code_error()" /></span>
</div>
</div>
</t>
</xpath>
<xpath expr="//t[@name='code_not_available']" position="replace"/>
</template>
<template id="cart_discount" name="Show Discount in Subtotal" customize_show="True" active="False" inherit_id="website_sale.total">
<xpath expr="//tr[@id='order_total_untaxed']" position="before">
<tr id="order_discounted" t-if="website_sale_order and website_sale_order.reward_amount">
<td class="text-right noborder text-muted" title="Discounted amount">Discount:</td>
<td class="text-xl-right noborder text-muted">
<span t-field="website_sale_order.reward_amount" style="white-space: nowrap;" t-options='{
"widget": "monetary",
"display_currency": website_sale_order.currency_id,
}'/>
</td>
</tr>
</xpath>
</template>
<template id="cart_summary" name="Payment" inherit_id="website_sale.cart_summary">
<xpath expr="//table[@id='cart_products']/tbody/tr/td[hasclass('td-price')]/child::*" position="attributes">
<attribute name="t-att-style">'display: None;' if free_shipping_lines and line in free_shipping_lines else ''</attribute>
</xpath>
</template>
</odoo>