[REF] various: move sale_payment and website_quote into sale and sale_management

[MOV] move sale_payment into sale
=================================

Almost all use cases of sale require sale_payment down the line.

There is only one case which does not: using sale_management without website_sale or website_quote.

However, since we want to move most of the features of website_quote into sale_management, moving sale_payement first will make the task easier.

[MOV] move website_quote into sale and sale_management
======================================================

We want to use some features of website_quote without having to install the whole module.

The main issue with website_quote was that it had a requirement on website.

What is common to website_sale and sale_management has been moved into sale. This is primarily the sales order portal view.

What is only related to sale_management has been moved into it. This is notably: sales order templates, optional products, sign and pay, ...

Everything that requires website has been moved into a new module named sale_quotation_builder. This is related to using the HTML builder to customize the quotation view.

Models, files and routes have been renamed to better convey their purpose and to follow guidelines more closely.

[IMP] merge sales order portal views
====================================

The old sales order view on the portal has been merged into the one from website_quote. The new view contains features of the two.

A lot of fixes and improvements have also been done, related to BS4 issues but also to improve the sales order view in general.

[IMP] add new settings
======================

The commit adds new settings related to activating the features that previously were activated by installing website_quote.

PR: #26345
Task: 49205, 1838924
This commit is contained in:
jpr-odoo
2018-08-13 21:53:19 +02:00
committed by Sébastien Theys
parent 5cf167abc6
commit 4c31a758cf
100 changed files with 2306 additions and 2730 deletions
+20 -2
View File
@@ -427,8 +427,8 @@ class AccountInvoice(models.Model):
def _compute_access_url(self):
super(AccountInvoice, self)._compute_access_url()
for order in self:
order.access_url = '/my/invoices/%s' % (order.id)
for invoice in self:
invoice.access_url = '/my/invoices/%s' % (invoice.id)
@api.depends('state', 'journal_id', 'date_invoice')
def _get_sequence_prefix(self):
@@ -1505,6 +1505,24 @@ class AccountInvoice(models.Model):
len(res),
) for r in res]
@api.multi
def get_portal_url(self, suffix=None):
"""
Get a portal url for this invoice, including access_token.
- suffix: string to append to the url, before the query string
"""
self.ensure_one()
return self.access_url + '%s?access_token=%s' % (suffix if suffix else '', self._portal_ensure_token())
@api.multi
def preview_invoice(self):
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'target': 'self',
'url': self.get_portal_url(),
}
class AccountInvoiceLine(models.Model):
_name = "account.invoice.line"
@@ -408,6 +408,7 @@
string="Register Payment" groups="account.group_account_invoice" class="oe_highlight"/>
<button name="action_invoice_open" type="object" states="draft" string="Validate" class="oe_highlight o_invoice_validate" groups="account.group_account_invoice"/>
<button name="%(action_account_invoice_refund)d" type='action' string='Add Credit Note' groups="account.group_account_invoice" attrs="{'invisible': ['|',('type', '=', 'out_refund'), ('state', 'not in', ('open','paid'))]}"/>
<button name="preview_invoice" type="object" string="Preview"/>
<button name="action_invoice_draft" states="cancel" string="Reset to Draft" type="object" groups="account.group_account_invoice"/>
<field name="state" widget="statusbar" nolabel="1" statusbar_visible="draft,open,paid"/>
</header>
@@ -17,7 +17,7 @@
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_invoice_draft']" position="before">
<button name="action_invoice_cancel" type="object" states="draft,open,paid" string="Cancel Invoice" groups="account.group_account_invoice"/>
<button name="action_invoice_cancel" type="object" states="draft,open,paid" string="Cancel" groups="account.group_account_invoice"/>
</xpath>
</field>
</record>
@@ -28,7 +28,7 @@
<field name="inherit_id" ref="account.invoice_supplier_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_invoice_draft']" position="before">
<button name="action_invoice_cancel" type="object" states="draft,open,paid" string="Cancel Invoice" groups="account.group_account_invoice"/>
<button name="action_invoice_cancel" type="object" states="draft,open,paid" string="Cancel" groups="account.group_account_invoice"/>
</xpath>
</field>
</record>
@@ -18,7 +18,6 @@ var PortalSidebar = Widget.extend({
// Window Handlers
$(window).on('resize', _.throttle(self._onUpdateSidebarPosition.bind(self), 200, {leading: false}));
this._onUpdateSidebarPosition();
this._setAffix();
this._setDelayLabel();
},
@@ -49,20 +48,6 @@ var PortalSidebar = Widget.extend({
$(el).text(displayStr);
});
},
/**
* updates affix status of sidebar for window scrolle
*
* @private
*/
_setAffix : function () {
var $bsSidebar = this.$el.find('.bs-sidebar');
$bsSidebar.affix({
offset: {
top: 0,
bottom: $('#wrapwrap').outerHeight() - $('main').height(),
},
});
},
/**
* @private
* @param {string} href
@@ -97,7 +82,6 @@ var PortalSidebar = Widget.extend({
_onUpdateSidebarPosition: function () {
var $sidebar = this.$el.find('.bs-sidebar, .o_portal_brand');
$sidebar.css({
position: config.device.size_class >= config.device.SIZES.MD ? "fixed" : '',
width: config.device.size_class >= config.device.SIZES.MD ? $sidebar.outerWidth() : '',
});
},
@@ -41,6 +41,7 @@ odoo.define('portal.signature_form', function (require){
'color': '#000',
'background-color': '#fff',
'height': '142px',
'width': '100%', // prevent the signature from being too big
});
this.empty_sign = this.$("#o_portal_signature").jSignature('getData', 'image');
},
@@ -112,6 +113,11 @@ odoo.define('portal.signature_form', function (require){
var form = new SignatureForm(null, $elem.data());
form.appendTo($elem);
});
// Make the signature responsive when it is displayed in bootstrap modal.
// More precisely it is too small if this code is not here.
$('.o_portal_signature_form').parents('.modal').on('shown.bs.modal', function (ev) {
$('.o_portal_signature_form').trigger('resize');
});
});
return {
+1 -2
View File
@@ -275,7 +275,6 @@ img.o_we_custom_image {
.breadcrumb {
background-color: rgba(0, 0, 0, 0);
margin-bottom: 0px;
display: inline-block;
}
.record_pager {
@@ -383,6 +382,7 @@ img.o_we_custom_image {
.o_portal_chatter_avatar{
width: 45px;
height: 45px;
margin-right: 1rem;
}
.o_portal_chatter_header {
@@ -420,7 +420,6 @@ img.o_we_custom_image {
.o_portal_sidebar {
.bs-sidebar {
position: relative;
width: 100%;
border-radius: 5px;
background-color: $light;
@@ -97,7 +97,7 @@
</t>
<t t-name="portal.chatter">
<div class="o_portal_chatter">
<div class="o_portal_chatter p-0">
<div class="o_portal_chatter_header">
<t t-call="portal.chatter_message_count"/>
</div>
+3 -3
View File
@@ -72,11 +72,11 @@
<div class="oe_structure">
<div t-if="not no_breadcrumbs" class="row">
<div t-attf-class="col-md-12">
<t t-call='portal.record_pager'/>
<ol t-if="page_name != 'home'" class="breadcrumb o_portal_submenu">
<li class="breadcrumb-item"><a href="/my/home" aria-label="Home" title="Home"><i class="fa fa-home"/></a></li>
<li t-if="page_name == 'my_details'" class="breadcrumb-item">Your Details</li>
</ol>
<t t-call='portal.record_pager'/>
</div>
</div>
</div>
@@ -357,7 +357,7 @@
<template id="record_pager" name="Portal Record Pager">
<t t-if='prev_record or next_record'>
<div class="ml8 record_pager btn-group btn-group-xs float-right" role="group">
<div class="ml8 record_pager btn-group btn-group-xs float-md-right" role="group">
<a role="button" t-att-class="'btn btn-secondary %s' % ('disabled' if not prev_record else '')" t-att-href="prev_record or '#'" ><i class="fa fa-arrow-left" role="img" aria-label="Previous" title="Previous"></i></a>
<a role="button" t-att-class="'btn btn-secondary %s' % ('disabled' if not next_record else '')" t-att-href="next_record or '#'" ><i class="fa fa-arrow-right" role="img" aria-label="Next" title="Previous"></i></a>
</div>
@@ -395,7 +395,7 @@
the message will be posted with the identity of the partner_id of the object
-->
<template id="message_thread">
<div id="discussion" class="d-print-none o_portal_chatter o_not_editable"
<div id="discussion" class="d-print-none o_portal_chatter o_not_editable p-0"
t-att-data-token="token" t-att-data-res_model="object._name" t-att-data-pid="pid" t-att-data-hash="hash" t-att-data-res_id="object.id" t-att-data-pager_step="message_per_page or 10" t-att-data-allow_composer="'0' if disable_composer else '1'">
</div>
</template>
+2 -1
View File
@@ -9,7 +9,7 @@
'description': """
This module contains all the common features of Sales Management and eCommerce.
""",
'depends': ['sales_team', 'account', 'portal'],
'depends': ['sales_team', 'payment', 'portal'],
'data': [
'security/sale_security.xml',
'security/ir.model.access.csv',
@@ -31,6 +31,7 @@ This module contains all the common features of Sales Management and eCommerce.
'views/sale_onboarding_views.xml',
'views/res_config_settings_views.xml',
'views/res_users_views.xml',
'views/payment_views.xml',
],
'demo': [
'data/sale_demo.xml',
+48 -65
View File
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo import http, _
from odoo import fields, http, _
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.addons.portal.controllers.mail import _message_post_helper
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager, get_records_pager
from odoo.osv import expression
class CustomerPortal(CustomerPortal):
@@ -36,15 +35,6 @@ class CustomerPortal(CustomerPortal):
# Quotations and Sales Orders
#
def _order_get_page_view_values(self, order, access_token, **kwargs):
order_invoice_lines = {il.product_id.id: il.invoice_id for il in order.invoice_ids.mapped('invoice_line_ids')}
values = {
'order': order,
'order_invoice_lines': order_invoice_lines,
'portal_confirmation': order.get_portal_confirmation_action(),
}
return self._get_page_view_values(order, access_token, values, 'my_orders_history', True, **kwargs)
@http.route(['/my/quotes', '/my/quotes/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_quotes(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
values = self._prepare_portal_layout_values()
@@ -149,66 +139,59 @@ class CustomerPortal(CustomerPortal):
return request.render("sale.portal_my_orders", values)
@http.route(['/my/orders/<int:order_id>'], type='http', auth="public", website=True)
def portal_order_page(self, order_id=None, access_token=None, **kw):
def portal_order_page(self, order_id, pdf=None, access_token=None, message=False, **kw):
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
except AccessError:
return request.redirect('/my')
values = self._order_get_page_view_values(order_sudo, access_token, **kw)
return request.render("sale.portal_order_page", values)
if pdf:
pdf = request.env.ref('sale.report_web_quote').sudo().with_context(set_viewport_size=True).render_qweb_pdf([order_sudo.id])[0]
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders)
@http.route(['/my/orders/pdf/<int:order_id>'], type='http', auth="public", website=True)
def portal_order_report(self, order_id, access_token=None, **kw):
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token)
except AccessError:
# use sudo to allow accessing/viewing orders for public user
# only if he knows the private token
now = fields.Date.today()
# Log only once a day
if order_sudo and request.session.get('view_quote_%s' % order_sudo.id) != now and request.env.user.share and access_token:
request.session['view_quote_%s' % order_sudo.id] = now
body = _('Quotation viewed by customer')
_message_post_helper(res_model='sale.order', res_id=order_sudo.id, message=body, token=order_sudo.access_token, message_type='notification', subtype="mail.mt_note", partner_ids=order_sudo.user_id.sudo().partner_id.ids)
if not order_sudo:
return request.redirect('/my')
# print report as sudo, since it require access to taxes, payment term, ... and portal
# does not have those access rights.
pdf = request.env.ref('sale.action_report_saleorder').sudo().render_qweb_pdf([order_sudo.id])[0]
pdfhttpheaders = [
('Content-Type', 'application/pdf'),
('Content-Length', len(pdf)),
]
return request.make_response(pdf, headers=pdfhttpheaders)
transaction = order_sudo.get_portal_last_transaction()
def _portal_quote_user_can_accept(self, order):
return order.company_id.portal_confirmation_sign or order.company_id.portal_confirmation_pay
@http.route(['/my/quotes/accept'], type='json', auth="public", website=True)
def portal_quote_accept(self, res_id, access_token=None, partner_name=None, signature=None):
try:
order_sudo = self._document_check_access('sale.order', res_id, access_token=access_token)
except AccessError:
return {'error': _('Invalid order')}
if order_sudo.state != 'sent':
return {'error': _('Order is not in a state requiring customer validation.')}
if not self._portal_quote_user_can_accept(order_sudo):
return {'error': _('Operation not allowed')}
if not signature:
return {'error': _('Signature is missing.')}
success_message = _('Your order has been signed but still needs to be paid to be confirmed.')
if not order_sudo.has_to_be_paid():
order_sudo.action_confirm()
success_message = _('Your order has been confirmed.')
order_sudo.signature = signature
order_sudo.signed_by = partner_name
pdf = request.env.ref('sale.action_report_saleorder').sudo().render_qweb_pdf([order_sudo.id])[0]
_message_post_helper(
res_model='sale.order',
res_id=order_sudo.id,
message=_('Order signed by %s') % (partner_name,),
attachments=[('%s.pdf' % order_sudo.name, pdf)],
**({'token': access_token} if access_token else {}))
return {
'success': success_message,
'redirect_url': '/my/orders/%s?%s' % (order_sudo.id, access_token and 'access_token=%s' % order_sudo.access_token or ''),
# TODO SEB fix breadcrumbs -> should always be visible (or at least no home alone)
values = {
'sale_order': order_sudo,
'message': int(message) if message else False,
'action': request.env.ref('sale.action_quotations').id,
'no_breadcrumbs': request.env.user.partner_id.commercial_partner_id not in order_sudo.message_partner_ids,
'tx_state': transaction.state if transaction else False,
'need_payment': order_sudo.invoice_status == 'to invoice' and transaction.state in ['draft', 'cancel'],
'token': access_token,
'return_url': '/shop/payment/validate',
'bootstrap_formatting': True,
'partner_id': order_sudo.partner_id.id,
}
if order_sudo.has_to_be_paid() or values['need_payment']:
domain = expression.AND([
['&', ('website_published', '=', True), ('company_id', '=', order_sudo.company_id.id)],
['|', ('specific_countries', '=', False), ('country_ids', 'in', [order_sudo.partner_id.country_id.id])]
])
acquirers = request.env['payment.acquirer'].sudo().search(domain)
values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form' and acq.view_template_id]
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s' and acq.registration_view_template_id]
values['pms'] = request.env['payment.token'].search(
[('partner_id', '=', order_sudo.partner_id.id),
('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])])
history = request.session.get('my_quotes_history', [])
values.update(get_records_pager(history, order_sudo))
return request.render('sale.sale_order_portal_template', values)
+1
View File
@@ -12,3 +12,4 @@ from . import sale
from . import res_config_settings
from . import sales_team
from . import res_users
from . import payment
@@ -66,7 +66,7 @@ class PaymentTransaction(models.Model):
sales_orders.force_quotation_send()
sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state == 'sent')
sales_orders.action_confirm()
automatic_invoice = self.env['ir.config_parameter'].sudo().get_param('sale_payment.automatic_invoice')
automatic_invoice = self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice')
if automatic_invoice:
for trans in self.filtered(lambda t: t.sale_order_ids):
trans.sale_order_ids._force_lines_to_invoice_policy_order()
@@ -74,7 +74,7 @@ class PaymentTransaction(models.Model):
trans.invoice_ids = [(6, 0, invoices)]
res = super(PaymentTransaction, self)._set_transaction_done()
if automatic_invoice:
default_template = self.env['ir.config_parameter'].sudo().get_param('sale_payment.default_email_template')
default_template = self.env['ir.config_parameter'].sudo().get_param('sale.default_email_template')
if default_template:
for invoice in trans.invoice_ids:
invoice.with_context(mark_invoice_as_sent=True).message_post_with_template(int(default_template), notif_layout="mail.mail_notification_paynow")
+4 -1
View File
@@ -8,8 +8,9 @@ class ResCompany(models.Model):
_inherit = "res.company"
sale_note = fields.Text(string='Default Terms and Conditions', translate=True)
portal_confirmation_sign = fields.Boolean(string='Digital Signature')
portal_confirmation_sign = fields.Boolean(string='Online Signature')
portal_confirmation_pay = fields.Boolean(string='Electronic Payment')
quotation_validity_days = fields.Integer(default=30, string="Default Quotation Validity (Days)")
# sale quotation onboarding
sale_quotation_onboarding_state = fields.Selection([('not_done', "Not done"), ('just_done', "Just done"), ('done', "Done"), ('closed', "Closed")], string="State of the sale onboarding panel", default='not_done')
@@ -108,3 +109,5 @@ class ResCompany(models.Model):
action = self.env.ref('sale.action_open_sale_onboarding_quotation_layout').read()[0]
action['res_id'] = self.env.user.company_id.id
return action
_sql_constraints = [('check_quotation_validity_days', 'CHECK(quotation_validity_days > 0)', 'Quotation Validity is required and must be greater than 0.')]
+28 -5
View File
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from odoo import api, fields, models
@@ -16,11 +14,11 @@ class ResConfigSettings(models.TransientModel):
config_parameter='sale.use_sale_note')
group_discount_per_so_line = fields.Boolean("Discounts", implied_group='sale.group_discount_per_so_line')
module_sale_margin = fields.Boolean("Margins")
quotation_validity_days = fields.Integer(related='company_id.quotation_validity_days', string="Default Quotation Validity (Days)")
use_quotation_validity_days = fields.Boolean("Default Quotation Validity", config_parameter='sale.use_quotation_validity_days')
group_warning_sale = fields.Boolean("Sale Order Warnings", implied_group='sale.group_warning_sale')
portal_confirmation_sign = fields.Boolean(related='company_id.portal_confirmation_sign', string='Digital Signature')
portal_confirmation_sign = fields.Boolean(related='company_id.portal_confirmation_sign', string='Online Signature')
portal_confirmation_pay = fields.Boolean(related='company_id.portal_confirmation_pay', string='Electronic Payment')
module_sale_payment = fields.Boolean("Sale Payment", help='Technical field implied by user choice of portal_confirmation_pay.')
module_website_quote = fields.Boolean("Quotations Templates")
group_sale_delivery_address = fields.Boolean("Customer Addresses", implied_group='sale.group_delivery_invoice_address')
multi_sales_price = fields.Boolean("Multiple Sales Prices per Product")
multi_sales_price_method = fields.Selection([
@@ -67,6 +65,18 @@ class ResConfigSettings(models.TransientModel):
module_product_email_template = fields.Boolean("Specific Email")
module_sale_coupon = fields.Boolean("Coupons & Promotions")
automatic_invoice = fields.Boolean("Automatic Invoice",
help="The invoice is generated automatically and available in the customer portal "
"when the transaction is confirmed by the payment acquirer.\n"
"The invoice is marked as paid and the payment is registered in the payment journal "
"defined in the configuration of the payment acquirer.\n"
"This mode is advised if you issue the final invoice at the order and not after the delivery.",
config_parameter='sale.automatic_invoice')
template_id = fields.Many2one('mail.template', 'Email Template',
domain="[('model', '=', 'account.invoice')]",
config_parameter='sale.default_email_template',
default=lambda self: self.env.ref('account.email_template_edi_invoice', False))
def set_values(self):
super(ResConfigSettings, self).set_values()
if not self.group_discount_per_so_line:
@@ -107,6 +117,19 @@ class ResConfigSettings(models.TransientModel):
if self.portal_confirmation_pay:
self.module_sale_payment = True
@api.onchange('use_quotation_validity_days')
def _onchange_use_quotation_validity_days(self):
if self.quotation_validity_days <= 0:
self.quotation_validity_days = self.env['res.company'].default_get(['quotation_validity_days'])['quotation_validity_days']
@api.onchange('quotation_validity_days')
def _onchange_quotation_validity_days(self):
if self.quotation_validity_days <= 0:
self.quotation_validity_days = self.env['res.company'].default_get(['quotation_validity_days'])['quotation_validity_days']
return {
'warning': {'title': "Warning", 'message': "Quotation Validity is required and must be greater than 0."},
}
@api.model
def get_values(self):
res = super(ResConfigSettings, self).get_values()
+136 -9
View File
@@ -1,14 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import uuid
from datetime import datetime, timedelta
from functools import partial
from itertools import groupby
from odoo import api, fields, models, _
from odoo.exceptions import UserError, AccessError
from odoo.exceptions import UserError, ValidationError
from odoo.tools.misc import formatLang
from odoo.osv import expression
from odoo.tools import float_is_zero, float_compare
@@ -23,6 +21,13 @@ class SaleOrder(models.Model):
_description = "Quotation"
_order = 'date_order desc, id desc'
def _default_validity_date(self):
if self.env['ir.config_parameter'].sudo().get_param('sale.use_quotation_validity_days'):
days = self.env.user.company_id.quotation_validity_days
if days > 0:
return fields.Date.to_string(datetime.now() + timedelta(days))
return False
@api.depends('order_line.price_total')
def _amount_all(self):
"""
@@ -121,9 +126,10 @@ class SaleOrder(models.Model):
('cancel', 'Cancelled'),
], string='Status', readonly=True, copy=False, index=True, track_visibility='onchange', track_sequence=3, default='draft')
date_order = fields.Datetime(string='Order Date', required=True, readonly=True, index=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False, default=fields.Datetime.now)
validity_date = fields.Date(string='Quote Validity', readonly=True, copy=False, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
help="Validity date of the quotation, after this date, the customer won't be able to validate the quotation online.")
validity_date = fields.Date(string='Validity', readonly=True, copy=False, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
help="Validity date of the quotation, after this date, the customer won't be able to validate the quotation online.", default=_default_validity_date)
is_expired = fields.Boolean(compute='_compute_is_expired', string="Is expired")
remaining_validity_days = fields.Integer(compute='_compute_remaining_validity_days', string="Remaining Validity Days")
create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.")
confirmation_date = fields.Datetime(string='Confirmation Date', readonly=True, index=True, help="Date on which the sales order is confirmed.", oldname="date_confirm", copy=False)
user_id = fields.Many2one('res.users', string='Salesperson', index=True, track_visibility='onchange', track_sequence=2, default=lambda self: self.env.user)
@@ -168,6 +174,12 @@ class SaleOrder(models.Model):
"will be scheduled based on this date rather than product lead times.")
expected_date = fields.Datetime("Expected Date", compute='_compute_expected_date', store=False, oldname='commitment_date', # Note: can not be stored since depends on today()
help="Delivery date you can promise to the customer, computed from product lead times and from the shipping policy of the order.")
amount_undiscounted = fields.Float('Amount Before Discount', compute='_compute_amount_undiscounted', digits=0)
transaction_ids = fields.Many2many('payment.transaction', 'sale_order_transaction_rel', 'sale_order_id', 'transaction_id',
string='Transactions', copy=False, readonly=True)
authorized_transaction_ids = fields.Many2many('payment.transaction', compute='_compute_authorized_transaction_ids',
string='Authorized Transactions', copy=False, readonly=True)
def _compute_access_url(self):
super(SaleOrder, self)._compute_access_url()
@@ -197,9 +209,25 @@ class SaleOrder(models.Model):
if dates_list:
order.expected_date = fields.Datetime.to_string(min(dates_list))
@api.model
def _get_customer_lead(self, product_tmpl_id):
return False
@api.multi
def _compute_remaining_validity_days(self):
for record in self:
if record.validity_date:
record.remaining_validity_days = (record.validity_date - fields.Date.today()).days + 1
else:
record.remaining_validity_days = 0
@api.depends('transaction_ids')
def _compute_authorized_transaction_ids(self):
for trans in self:
trans.authorized_transaction_ids = trans.transaction_ids.filtered(lambda t: t.state == 'authorized')
@api.one
def _compute_amount_undiscounted(self):
total = 0.0
for line in self.order_line:
total += line.price_subtotal + line.price_unit * ((line.discount or 0.0) / 100.0) * line.product_uom_qty # why is there a discount in a field named amount_undiscounted ??
self.amount_undiscounted = total
@api.multi
def unlink(self):
@@ -401,7 +429,8 @@ class SaleOrder(models.Model):
'fiscal_position_id': self.fiscal_position_id.id or self.partner_invoice_id.property_account_position_id.id,
'company_id': self.company_id.id,
'user_id': self.user_id and self.user_id.id,
'team_id': self.team_id.id
'team_id': self.team_id.id,
'transaction_ids': [(6, 0, self.transaction_ids.ids)],
}
return invoice_vals
@@ -694,6 +723,104 @@ class SaleOrder(models.Model):
return groups
@api.multi
def _create_payment_transaction(self, vals):
'''Similar to self.env['payment.transaction'].create(vals) but the values are filled with the
current sales orders fields (e.g. the partner or the currency).
:param vals: The values to create a new payment.transaction.
:return: The newly created payment.transaction record.
'''
# Ensure the currencies are the same.
currency = self[0].pricelist_id.currency_id
if any([so.pricelist_id.currency_id != currency for so in self]):
raise ValidationError(_('A transaction can\'t be linked to sales orders having different currencies.'))
# Ensure the partner are the same.
partner = self[0].partner_id
if any([so.partner_id != partner for so in self]):
raise ValidationError(_('A transaction can\'t be linked to sales orders having different partners.'))
# Try to retrieve the acquirer. However, fallback to the token's acquirer.
acquirer_id = vals.get('acquirer_id')
acquirer = False
payment_token_id = vals.get('payment_token_id')
if payment_token_id:
payment_token = self.env['payment.token'].sudo().browse(payment_token_id)
# Check payment_token/acquirer matching or take the acquirer from token
if acquirer_id:
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
if payment_token and payment_token.acquirer_id != acquirer:
raise ValidationError(_('Invalid token found! Token acquirer %s != %s') % (
payment_token.acquirer_id.name, acquirer.name))
if payment_token and payment_token.partner_id != partner:
raise ValidationError(_('Invalid token found! Token partner %s != %s') % (
payment_token.partner.name, partner.name))
else:
acquirer = payment_token.acquirer_id
# Check an acquirer is there.
if not acquirer_id and not acquirer:
raise ValidationError(_('A payment acquirer is required to create a transaction.'))
if not acquirer:
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
# Check a journal is set on acquirer.
if not acquirer.journal_id:
raise ValidationError(_('A journal must be specified of the acquirer %s.' % acquirer.name))
if not acquirer_id and acquirer:
vals['acquirer_id'] = acquirer.id
vals.update({
'amount': sum(self.mapped('amount_total')),
'currency_id': currency.id,
'partner_id': partner.id,
'sale_order_ids': [(6, 0, self.ids)],
})
transaction = self.env['payment.transaction'].create(vals)
# Process directly if payment_token
if transaction.payment_token_id:
transaction.s2s_do_transaction()
return transaction
def _force_lines_to_invoice_policy_order(self):
for line in self.order_line:
if self.state in ['sale', 'done']:
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
else:
line.qty_to_invoice = 0
@api.multi
def payment_action_capture(self):
self.authorized_transaction_ids.s2s_capture_transaction()
@api.multi
def payment_action_void(self):
self.authorized_transaction_ids.s2s_void_transaction()
@api.multi
def get_portal_url(self, suffix=None):
"""
Get a portal url for this sale order, including access_token.
- suffix: string to append to the url, before the query string
"""
self.ensure_one()
return self.access_url + '%s?access_token=%s' % (suffix if suffix else '', self._portal_ensure_token())
@api.multi
def get_portal_last_transaction(self):
self.ensure_one()
return self.transaction_ids.get_last_transaction()
@api.model
def _get_customer_lead(self, product_tmpl_id):
return False
class SaleOrderLine(models.Model):
_name = 'sale.order.line'
+13
View File
@@ -21,5 +21,18 @@
name="sale.report_saleorder_pro_forma"
print_report_name="'PRO-FORMA - %s' % (object.name)"
/>
<report
id="report_web_quote"
string="Web Quotation"
model="sale.order"
report_type="qweb-pdf"
file="sale.report_quotation"
name="sale.report_quotation"
menu="False"
print_report_name="
(object.state in ('draft', 'sent') and 'Quotation - %s' % object.name or
'Order - %s' % (object.name))"
/>
</data>
</odoo>
@@ -254,4 +254,19 @@
</t>
</template>
<template id="report_quotation">
<t t-call="web.html_container">
<t t-set="data_report_margin_top" t-value="45"/>
<t t-set="data_report_header_spacing" t-value="40"/>
<t t-call="web.external_layout">
<t t-set="o" t-value="docs[0]"/>
<t t-foreach="docs" t-as="sale_order">
<div class="page">
<t t-call="sale.sale_order_portal_content"/>
</div>
</t>
</t>
</t>
</template>
</odoo>
@@ -0,0 +1,146 @@
odoo.define('sale.SalePortalSidebar.instance', function (require) {
"use strict";
require('web.dom_ready');
var SalePortalSidebar = require('sale.SalePortalSidebar');
if (!$('.o_portal_sale_sidebar').length) {
return $.Deferred().reject("DOM doesn't contain '.o_portal_sale_sidebar'");
}
var $spyWatch = $('body[data-target=".navspy"]'),
sale_portal_sidebar = new SalePortalSidebar($spyWatch);
return sale_portal_sidebar.attachTo($('.o_portal_sale_sidebar')).then(function () {
return sale_portal_sidebar;
});
});
//==============================================================================
odoo.define('sale.SalePortalSidebar', function (require) {
"use strict";
var PortalSidebar = require('portal.PortalSidebar');
var SalePortalSidebar = PortalSidebar.extend({
events: {
'click .o_portal_sale_print': '_onPrintSaleOrder',
},
/**
* @override
* @param {Object} $watched_selector
*/
init: function ($watched_selector) {
this._super.apply(this, arguments);
this.authorizedTextTag = ['em', 'b', 'i', 'u'];
this.spyWatched = $watched_selector;
},
/**
* @override
*/
start: function () {
this._super.apply(this, arguments);
var $spyWatcheElement = this.$el.find('[data-id="portal_sidebar"]');
this._setElementId($spyWatcheElement);
// Nav Menu ScrollSpy
this._generateMenu();
},
//--------------------------------------------------------------------------
// Private
//---------------------------------------------------------------------------
/**
* create an unique id and added as a attribute of spyWatched element
*
* @private
* @param {string} prefix
* @param {Object} $el
*
*/
_setElementId: function (prefix, $el) {
var id = _.uniqueId(prefix);
this.spyWatched.find($el).attr('id', id);
return id;
},
/**
* generate the new spy menu
*
* @private
*
*/
_generateMenu: function () {
var self = this,
lastLI = false,
lastUL = null,
$bsSidenav = this.$el.find('.bs-sidenav');
$("[id^=quote_header_], [id^=quote_]", this.spyWatched).attr("id", "");
_.each(this.spyWatched.find("h1, h2"), function (el) {
var id, text;
switch (el.tagName.toLowerCase()) {
case "h1":
id = self._setElementId('quote_header_', el);
text = self._extractText($(el));
if (!text) {
break;
}
lastLI = $("<li class='nav-item'>").append($('<a class="nav-link" href="#' + id + '"/>').text(text)).appendTo($bsSidenav);
lastUL = false;
break;
case "h2":
id = self._setElementId('quote_', el);
text = self._extractText($(el));
if (!text) {
break;
}
if (lastLI) {
if (!lastUL) {
lastUL = $("<ul class='nav'>").appendTo(lastLI);
}
$("<li class='nav-item'>").append($('<a class="nav-link" href="#' + id + '"/>').text(text)).appendTo(lastUL);
}
break;
}
});
},
/**
* extract text of menu title for sidebar
*
* @private
* @param {Object} $node
*
*/
_extractText: function ($node) {
var self = this;
var rawText = [];
_.each($node.contents(), function (el) {
var current = $(el);
if ($.trim(current.text())) {
var tagName = current.prop("tagName");
if (_.isUndefined(tagName) || (!_.isUndefined(tagName) && _.contains(self.authorizedTextTag, tagName.toLowerCase()))) {
rawText.push($.trim(current.text()));
}
}
});
return rawText.join(' ');
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} ev
*/
_onPrintSaleOrder: function (ev) {
ev.preventDefault();
var href = $(ev.currentTarget).attr('href');
this._printIframeContent(href);
},
});
return SalePortalSidebar;
});
@@ -8,3 +8,12 @@
.orders_label_text_align {
vertical-align: 15%;
}
tr.o_is_line_note {
color: #999;
}
tr.o_is_line_section {
font-weight: bold;
background-color: #EEE;
}
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="transaction_form_inherit_sale_payment" model="ir.ui.view">
<record id="transaction_form_inherit_sale" model="ir.ui.view">
<field name="name">payment.transaction.form.inherit.sale.payment</field>
<field name="model">payment.transaction</field>
<field name="inherit_id" ref="payment.transaction_form"/>
+36 -12
View File
@@ -147,7 +147,7 @@
<label for="portal_confirmation_sign"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
<div class="text-muted">
Request a digital signature to confirm orders
Request an online signature to confirm orders
</div>
</div>
</div>
@@ -156,22 +156,13 @@
<field name="portal_confirmation_pay"/>
</div>
<div class="o_setting_right_pane">
<field name="module_sale_payment" invisible="1"/>
<label for="portal_confirmation_pay"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
<div class="text-muted">
Request an electronic payment to confirm orders
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_website_quote"/>
</div>
<div class="o_setting_right_pane">
<label for="module_website_quote"/>
<div class="text-muted" id="sale_config_website_quote">
Design standardized offers
<div attrs="{'invisible': [('portal_confirmation_pay', '=', False)]}">
<button name='%(payment.action_payment_acquirer)d' icon="fa-arrow-right" type="action" string="Payment Acquirers" class="btn-link"/>
</div>
</div>
</div>
@@ -203,6 +194,23 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="use_quotation_validity_days"/>
</div>
<div class="o_setting_right_pane">
<label for="use_quotation_validity_days"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
<div class="text-muted">
Set a default validity on your quotations
</div>
<div class="content-group" attrs="{'invisible': [('use_quotation_validity_days','=',False)]}">
<div class="mt16">
<span class="col-lg-3">Default Limit: <field name="quotation_validity_days" attrs="{'required': [('use_quotation_validity_days', '=', True)]}"/> days</span>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="group_warning_sale"/>
@@ -363,6 +371,22 @@
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box"
attrs="{'invisible': ['|', ('default_invoice_policy', '!=', 'order'), ('portal_confirmation_pay', '=', False)]}">
<div class="o_setting_left_pane">
<field name="automatic_invoice"/>
</div>
<div class="o_setting_right_pane">
<label for="automatic_invoice"/>
<div class="text-muted">
Generate the invoice automatically when the electronic payment is confirmed
</div>
<div attrs="{'invisible': [('automatic_invoice','=',False)]}">
<label for="template_id" class="o_light_label"/>
<field name="template_id" class="oe_inline" options="{'no_create': True}"/>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" groups="base.group_no_one">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
+597 -343
View File
@@ -1,15 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="web.assets_frontend" name="Sale frontend assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/sale/static/src/js/sale_portal_sidebar.js"></script>
</xpath>
</template>
<template id="assets_backend" name="project assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/sale/static/src/js/tour.js"></script>
</xpath>
</template>
<template id="portal_my_home_menu_sale" name="Portal layout : sales menu entries" inherit_id="portal.portal_layout" priority="20">
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
<li t-if="page_name == 'quote' or sales_order and sales_order.state in ('sent', 'cancel')">
<li t-if="page_name == 'quote' or sales_order and sales_order.state in ('sent', 'cancel')" class="breadcrumb-item">
<a t-if="sales_order" t-attf-href="/my/quotes?{{ keep_query() }}">Quotations</a>
<t t-else="">Quotations</t>
</li>
<li t-if="page_name == 'order' or sales_order and sales_order.state not in ('sent', 'cancel')">
<li t-if="page_name == 'order' or sales_order and sales_order.state not in ('sent', 'cancel')" class="breadcrumb-item">
<a t-if="sales_order" t-attf-href="/my/orders?{{ keep_query() }}">Sales Orders</a>
<t t-else="">Sales Orders</t>
</li>
<li t-if="sales_order">
<li t-if="sales_order" class="breadcrumb-item">
<t t-if="sales_order.state in ('sent', 'cancel')">Quotation</t>
<t t-else="">Order</t>
<t t-esc="sales_order.name"/>
@@ -31,369 +45,609 @@
</template>
<template id="portal_my_quotations" name="My Quotations">
<t t-call="portal.portal_layout">
<t t-call="portal.portal_searchbar">
<t t-set="title">Quotations</t>
</t>
<t t-if="not quotations">
<p>There are currently no quotations for your account.</p>
</t>
<div t-if="quotations" class="card">
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
<thead>
<tr class="active">
<th>Quotation #</th>
<th>Order Date</th>
<th>Valid Until</th>
<th></th>
<th>Total</th>
</tr>
</thead>
<t t-foreach="quotations" t-as="quotation">
<tr>
<td>
<a t-attf-href="/my/orders/#{quotation.id}?#{keep_query()}"><t t-esc="quotation.name"/></a>
</td>
<td><span t-field="quotation.date_order"/></td>
<td><span t-field="quotation.validity_date"/></td>
<td>
<t t-if="quotation.state == 'cancel'">
<span class="badge badge-secondary"><i class="fa fa-fw fa-remove"/> Cancelled</span>
</t>
<t t-if="quotation.is_expired">
<span class="badge badge-secondary"><i class="fa fa-fw fa-clock-o"/> Expired</span>
</t>
</td>
<td><span t-field="quotation.amount_total" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
</td>
</tr>
</t>
</table></div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="portal.pager"/>
<t t-call="portal.portal_layout">
<t t-call="portal.portal_searchbar">
<t t-set="title">Quotations</t>
</t>
<t t-if="not quotations">
<p>There are currently no quotations for your account.</p>
</t>
<div t-if="quotations" class="card">
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
<thead>
<tr class="active">
<th>Quotation #</th>
<th>Order Date</th>
<th>Valid Until</th>
<th></th>
<th>Total</th>
</tr>
</thead>
<t t-foreach="quotations" t-as="quotation">
<tr>
<td>
<a t-att-href="quotation.get_portal_url()"><t t-esc="quotation.name"/></a>
</td>
<td><span t-field="quotation.date_order"/></td>
<td><span t-field="quotation.validity_date"/></td>
<td>
<t t-if="quotation.state == 'cancel'">
<span class="badge badge-secondary"><i class="fa fa-fw fa-remove"/> Cancelled</span>
</t>
<t t-if="quotation.is_expired">
<span class="badge badge-secondary"><i class="fa fa-fw fa-clock-o"/> Expired</span>
</t>
</td>
<td>
<span t-field="quotation.amount_total" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
</td>
</tr>
</t>
</table></div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="portal.pager"/>
</div>
</div>
</div>
</t>
</t>
</template>
<template id="portal_my_orders" name="My Sales Orders">
<t t-call="portal.portal_layout">
<t t-call="portal.portal_searchbar">
<t t-set="title">Sales Orders</t>
</t>
<t t-if="not orders">
<p>There are currently no orders for your account.</p>
</t>
<div t-if="orders" class="card">
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
<thead>
<tr class="active">
<th>
<span class='d-none d-md-inline'>Sales Order #</span>
<span class='d-block d-md-none'>Ref.</span>
</th>
<th>Order Date</th>
<th ></th>
<th>Total</th>
</tr>
</thead>
<t t-foreach="orders" t-as="order">
<tr>
<td>
<a t-attf-href="/my/orders/{{order.id}}?{{keep_query()}}"><t t-esc="order.name"/></a>
</td>
<td>
<span t-field="order.date_order" t-options="{'widget': 'date'}"/>&amp;nbsp;
<span class='d-none d-md-inline' t-field="order.date_order" t-options="{'time_only': True}"/>
</td>
<td>
<t t-if="order.state == 'done'">
<span class="badge badge-success d-none d-md-inline-block"><i class="fa fa-fw fa-check" role="img" aria-label="Done" title="Done"></i><span class="d-none d-md-inline"> Done</span></span>
</t>
</td>
<td><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></td>
</tr>
</t>
</table></div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="portal.pager"/>
<t t-call="portal.portal_layout">
<t t-call="portal.portal_searchbar">
<t t-set="title">Sales Orders</t>
</t>
<t t-if="not orders">
<p>There are currently no orders for your account.</p>
</t>
<div t-if="orders" class="card">
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
<thead>
<tr class="active">
<th>
<span class='d-none d-md-inline'>Sales Order #</span>
<span class='d-block d-md-none'>Ref.</span>
</th>
<th>Order Date</th>
<th ></th>
<th>Total</th>
</tr>
</thead>
<t t-foreach="orders" t-as="order">
<tr>
<td>
<a t-attf-href="quotation.get_portal_url()"><t t-esc="order.name"/></a>
</td>
<td>
<span t-field="order.date_order" t-options="{'widget': 'date'}"/>&amp;nbsp;
<span class='d-none d-md-inline' t-field="order.date_order" t-options="{'time_only': True}"/>
</td>
<td>
<t t-if="order.state == 'done'">
<span class="badge badge-success d-none d-md-inline-block"><i class="fa fa-fw fa-check" role="img" aria-label="Done" title="Done"></i><span class="d-none d-md-inline"> Done</span></span>
</t>
</td>
<td><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></td>
</tr>
</t>
</table></div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="portal.pager"/>
</div>
</div>
</div>
</t>
</t>
</template>
<template id="portal_order_page" name="Sales Order">
<t t-call="portal.portal_layout">
<t t-set="sales_order" t-value="order"/> <!-- rename to ensure breadcrumb compatibility (differentiate with purchase orders) -->
<t t-set="invoices" t-value="[i for i in order.invoice_ids if i.state not in ['draft', 'cancel']]"/>
<t t-set="invoices_amount" t-value="sum(i.amount_total for i in invoices)"/>
<div id="optional_placeholder"></div>
<div class="container">
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-lg-6 col-9 col-md-9">
<h4>
<t t-if="order.state in ('sent', 'cancel')">Quotation</t>
<t t-else="">Order</t>
<span t-esc="order.name"/>
<t t-if="order.state == 'cancel'">
<span class="badge badge-secondary"><i class="fa fa-fw fa-remove"/> Cancelled</span>
<!-- Complete page of the sale_order -->
<template id="sale_order_portal_template" name="Sales Order Portal Template" inherit_id="portal.portal_sidebar" primary="True">
<xpath expr="//div[hasclass('o_portal_sidebar')]" position="inside">
<div class="row mt16 o_portal_sale_sidebar">
<!-- Sidebar -->
<div class="col-lg-3 d-print-none">
<div class="bs-sidebar">
<!-- TODO SEB add sale order total here, like invoice for the style -->
<t t-if="sale_order.team_id.team_type != 'website'">
<div class="text-center" t-if="sale_order.state in ('draft', 'sent', 'waiting_date')" style="padding: 10px">
<t t-set="show_hr" t-value="False"/>
<a role="button" t-if="not sale_order.is_expired and not sale_order.signature and sale_order.has_to_be_signed()" class="btn btn-success btn-block" data-toggle="modal" data-target="#modalaccept" href="#">
<i class="fa fa-check"/> Accept
<t t-set="show_hr" t-value="True"/>
</a>
<a role="button" t-elif="not sale_order.is_expired and sale_order.has_to_be_paid()" class="btn btn-success btn-block" data-toggle="modal" data-target="#modalaccept" href="#">
<i class="fa fa-check"/> <t t-if="not sale_order.signature">Accept &amp; </t>Pay
<t t-set="show_hr" t-value="True"/>
</a>
<a role="button" t-if="sale_order.is_expired" href="#discussion" class="btn btn-info btn-block o_normalwhitespace">
<strong>This offer expired!</strong><br/>
Contact us for new quotation.
<t t-set="show_hr" t-value="True"/>
</a>
<hr class="mb0" t-if="show_hr"/>
</div>
<div class="text-center" t-if="need_payment or (sale_order.state in ('sale') and sale_order.has_to_be_paid() and tx_state != 'done')" style="padding: 10px">
<a role="button" class="btn btn-success btn-block" data-toggle="modal" data-target="#modalaccept" href="#">
<i class="fa fa-check"></i> Pay Now
</a>
<hr class="mb0"/>
</div>
</t>
<t t-if="order.state == 'done'">
<span class="badge badge-success"><i class="fa fa-fw fa-check" /> Done</span>
</t>
</h4>
</div>
<div class="col-lg-6 col-md-3 col-3 d-print-none" id="o_portal_order_actions">
<t t-if="order.state == 'sent' and portal_confirmation == 'sign'" name="portal_confirmation_sign">
<a role="button" class="btn btn-primary ml8 float-right" href="#portal_sign">
<i class="fa fa-arrow-circle-right"/> Accept &amp; Sign
</a>
</t>
<t t-if="order.state == 'sale' and portal_confirmation == 'sign'">
<a role="button" class="btn btn-success ml8 float-right" disabled="disabled"
t-attf-title="Order signed on #{order.confirmation_date} by #{order.partner_id.name}">
<i class="fa fa-check-circle"/> Signed
</a>
</t>
<a target="_blank" t-att-href="'/my/orders/pdf/%s?%s' % (order.id, keep_query())" class="ml8 float-right mt4 mr8" title="Download">
<i class="fa fa-download fa-2x" role="img" aria-label="Download"/>
</a>
</div>
</div>
</div>
<div class="card-body">
<t t-if="error or warning" t-call="sale.portal_order_error"/>
<t t-if="success and (not error and not warning)" t-call="sale.portal_order_success"/>
<div class="mb8">
<strong>Date:</strong> <span t-field="order.create_date" t-options='{"widget": "date"}'/>
</div>
<div class="mb8" t-if="order.validity_date">
<strong>Expiration Date:</strong> <span t-field="order.validity_date" t-options='{"widget": "date"}'/>
</div>
<div class='row'>
<div class="col-lg-6">
<div>
<strong>Invoicing Address</strong>
</div>
<div>
<address t-field="order.partner_invoice_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
<t t-if="invoices">
<div>
<strong>Invoices</strong>
</div>
<div>
<t t-foreach="invoices" t-as="i">
<t t-set="report_url" t-value="'/my/invoices/pdf/%s?%s' % (i.id, 'access_token=%s' % i.access_token if access_token else '')"/>
<div>
<a t-att-href="report_url"><span class="fa fa-download" role="img" aria-label="Download" title="Download"/></a>
<a t-att-href="report_url"><span t-esc="i.number"/></a>
<span class="text-muted" t-field="i.date_invoice"/>
<t t-if="i.state == 'paid'">
<span class="badge badge-success orders_label_text_align"><i class="fa fa-fw fa-check"/> Paid</span>
</t>
<t t-if="i.state != 'paid'">
<span class="badge badge-info orders_label_text_align"><i class="fa fa-fw fa-clock-o"/> Waiting</span>
</t>
<div class="navspy" t-ignore="true" role="complementary">
<ul class="nav flex-column bs-sidenav" data-id="quote_sidebar"></ul>
</div>
<div t-if="not sale_order.is_expired and sale_order.state in ['draft', 'sent']" class="pl-3 pr-3">
<input type="hidden" t-att-value="sale_order.validity_date" id="validity_date"/>
<div class="mt8" t-if="sale_order.remaining_validity_days &gt; 0">
<hr class="mb0"/>
<strong>This offer expires in</strong>
<div class="day_counter mt8 mb16">
<i class="fa fa-clock-o" aria-title="Dates" title="Dates"></i>
<t t-esc="sale_order.remaining_validity_days"/>
<t t-if="sale_order.remaining_validity_days &gt; 1">
days
</t>
<t t-if="sale_order.remaining_validity_days &lt;= 1">
day
</t>
</div>
</div>
<div class="mb16" t-if="sale_order.amount_undiscounted &gt; sale_order.amount_untaxed">
<hr class="mb0"/>
<p class="text-muted mb8">Your advantage:</p>
<t t-if="sale_order.amount_untaxed == sale_order.amount_total">
<strong t-field="sale_order.amount_total"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
<strong t-field="sale_order.amount_undiscounted"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
style="text-decoration: line-through"
class="text-danger"/>
</t>
<t t-if="sale_order.amount_untaxed != sale_order.amount_total">
<strong t-field="sale_order.amount_untaxed"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
<strong t-field="sale_order.amount_undiscounted"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
style="text-decoration: line-through"
class="text-danger"/>
<br />
(<span t-field="sale_order.amount_total"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/> Incl. tax)
</t>
</div>
</div>
<div class="pb8 ml16 mr16" t-if="sale_order.user_id">
<hr class="mb0"/>
<div><strong>Your Contact:</strong></div>
<img class="rounded-circle mr4 float-left o_portal_contact_img" t-if="sale_order.user_id.image" t-attf-src="data:image/png;base64,#{sale_order.user_id.image}" alt="Contact"/>
<img class="rounded-circle mr4 float-left o_portal_contact_img" t-if="not sale_order.user_id.image" src="/web/static/src/img/placeholder.png" alt="Contact"/>
<span t-field="sale_order.user_id" t-options='{"widget": "contact", "fields": ["name", "phone"], "no_marker": True}'/>
<a href="#discussion"><i class="fa fa-comment"></i> Contact us</a>
</div>
</t>
</div>
</t>
</div>
<!-- Page content -->
<div id="quote_content" class="col-lg-9">
<div class="alert alert-success alert-dismissable" t-if="message==1" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
Your message has been successfully sent!
</div>
<div class="alert alert-warning alert-dismissable" t-if="message==3" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
This order has been validated. Thanks for your trust
and do not hesitate to <a href="#discussion">contact us</a> for
any question.
</div>
<t t-if="sale_order.transaction_ids">
<t t-call="payment.payment_confirmation_status">
<t t-set="payment_tx_id" t-value="sale_order.get_portal_last_transaction()"/>
</t>
</t>
<div class="alert alert-warning alert-dismissable" t-if="message==4 and sale_order.state != 'sent'" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
This order
<t t-if="sale_order.state=='draft'">has not yet been sent!</t>
<t t-if="sale_order.state=='cancel'">has already been cancelled!</t>
<t t-if="sale_order.state not in ('cancel','draft')">has already been validated!</t>
You can <a href="#discussion">contact us</a> for
any questions.
</div>
<!-- modal relative to the actions Accept/Reject/Cancel -->
<div role="dialog" class="modal fade" id="modalaccept">
<div class="modal-dialog" t-if="not sale_order.signature and sale_order.has_to_be_signed()">
<form id="accept" method="POST" t-att-data-order-id="sale_order.id" t-att-data-token="sale_order.access_token" class="js_accept_json modal-content js_website_submit_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<header class="modal-header">
<h4 class="modal-title">Validate Order</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&amp;times;</button>
</header>
<main class="modal-body" id="sign-dialog">
<p>
<span>I agree that by signing this proposal, I accept it on the behalf of </span>
<b t-field="sale_order.partner_id.commercial_partner_id"/>
<span>, for an amount of </span>
<b data-id="total_amount" t-field="sale_order.amount_total" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
<span>with payment terms: </span><b t-field="sale_order.payment_term_id"/>.
</p>
<t t-call="portal.portal_signature">
<t t-set="object" t-value="sale_order"/>
<t t-set="partner_name" t-value="sale_order.partner_id.name"/>
<t t-set="callUrl" t-value="sale_order.get_portal_url('/accept')"/>
<t t-set="accessToken" t-value="sale_order.access_token"/>
</t>
</main>
</form>
</div>
<div class="modal-dialog modal-content" t-if="not (sale_order.has_to_be_signed() and not sale_order.signature) and (sale_order.has_to_be_paid() or need_payment)">
<header class="modal-header">
<h4 class="modal-title">Validate Order</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&amp;times;</button>
</header>
<main class="modal-body" id="sign-dialog">
<p>
<span>I agree that by paying this proposal, I accept it on the behalf of </span>
<b t-field="sale_order.partner_id.commercial_partner_id"/><span>
, for an amount of </span>
<b data-id="total_amount" t-field="sale_order.amount_total" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
<span>with payment terms: </span><b t-field="sale_order.payment_term_id"/>.
</p>
</main>
<footer class="modal-footer">
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left">
<h3 class="mb24">Pay with</h3>
<t t-call="payment.payment_tokens_list">
<t t-set="mode" t-value="'payment'"/>
<t t-set="submit_txt" t-value="'Pay &amp; Confirm'"/>
<t t-set="icon_class" t-value="'fa-lock'"/>
<t t-set="form_action" t-value="sale_order.get_portal_url('/transaction/token')"/>
<t t-set="prepare_tx_url" t-value="sale_order.get_portal_url('/transaction/')"/>
<t t-set="access_token" t-value="sale_order.access_token"/>
</t>
</div>
</footer>
</div>
</div>
<div role="dialog" class="modal fade" id="modaldecline">
<div class="modal-dialog">
<form id="decline" method="POST" t-attf-action="/my/orders/#{sale_order.id}/decline?access_token=#{sale_order.access_token}" class="modal-content">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<header class="modal-header">
<h4 class="modal-title">Reject This Quotation</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&amp;times;</button>
</header>
<main class="modal-body">
<p>
Tell us why you are refusing this quotation, this will help us improve our services.
</p>
<textarea rows="4" name="decline_message" placeholder="Your feedback....." class="form-control"/>
</main>
<footer class="modal-footer">
<button type="submit" t-att-id="sale_order.id" class="btn btn-primary">Reject</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
</footer>
</form>
</div>
</div>
<div class="alert alert-danger alert-dismissable d-print-none" t-if="sale_order.state == 'cancel'" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="close">&amp;times;</button>
<strong>This quotation has been canceled.</strong> Contact us to get a new quotation.
</div>
<t t-call="sale.sale_order_portal_content"/>
<t t-if="sale_order.team_id.team_type != 'website'">
<div class="text-center d-print-none mt16 mb16" id="quotation_toolbar" t-if="sale_order.state in ('draft', 'sent', 'waiting_date')">
<t t-set="show_buttons" t-value="False"/>
<a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-if="not sale_order.is_expired and not sale_order.signature and sale_order.has_to_be_signed()" href="#">
<i class="fa fa-check"></i> Accept
<t t-set="show_buttons" t-value="True"/>
</a>
<a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-elif="not sale_order.is_expired and sale_order.has_to_be_paid()" href="#">
<i class="fa fa-check"/> <t t-if="not sale_order.signature">Accept &amp; </t>Pay
<t t-set="show_buttons" t-value="True"/>
</a>
<a role="button" class="btn btn-info" type="submit" href="#discussion" t-if="show_buttons">
<i class="fa fa-comment"></i> Feedback
</a>
<a role="button" class="btn btn-danger" data-toggle="modal" data-target="#modaldecline" href="#" t-if="show_buttons">
<i class="fa fa-times"></i> Reject
</a>
</div>
<div class="text-center" t-if="need_payment or (sale_order.state in ('sale') and sale_order.has_to_be_paid() and tx_state != 'done')" style="padding: 10px">
<a role="button" class="btn btn-primary" data-toggle="modal" data-target="#modalaccept" href="#">
<i class="fa fa-check"></i> Pay Now
</a>
</div>
</t>
<div id="sale_order_communication">
<h1 class="page-header">Communication</h1>
<t t-call="portal.message_thread">
<t t-set="object" t-value="sale_order"/>
</t>
</div>
</div><!-- //col-lg-9 -->
</div>
</xpath>
</template>
<!--
Sales Order content : intro, informations, order lines, remarks, descriptions ....
This template should contains all the printable element of the SO. This is the
template rendered in PDF with the report engine.
-->
<template id="sale_order_portal_content" name="Sales Order Portal Content">
<!-- Intro -->
<div id="introduction" class="page-header mt16">
<h1>
<t t-if="sale_order.state in ('draft','sent','cancel')">Quotation</t>
<t t-if="sale_order.state not in ('draft','sent','cancel')">Sales Order</t>
<em t-esc="sale_order.name"/>
<div t-ignore="true" class="float-md-right css_editable_mode_hidden d-print-none">
<ul class="list-inline">
<li groups="sales_team.group_sale_salesman" class="list-inline-item">
<a role="button" t-if="sale_order.state not in ('manual')" class="btn btn-info" t-att-href="'/web#return_label=Website&amp;model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % (sale_order._name, sale_order.id, action)">Edit Quotation</a>
<a role="button" t-if="sale_order.state in ('manual')" class=" btn btn-info" t-att-href="'/web#return_label=Website&amp;model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % (sale_order._name, sale_order.id, action)">Back to Sales Order</a>
</li>
<li class="list-inline-item">
<a role="button" class="btn btn-info" target="_blank" t-att-href="sale_order.get_portal_url() + '&amp;pdf=True'">Print</a>
</li>
</ul>
</div>
</h1>
</div>
<!-- Informations -->
<div id="informations">
<div t-if="sale_order.transaction_ids and not invoices and sale_order.state in ('sent', 'sale') and portal_confirmation == 'pay' and not success and not error" t-att-data-order-id="sale_order.id">
<t t-if="sale_order.transaction_ids">
<t t-call="payment.payment_confirmation_status">
<t t-set="payment_tx_id" t-value="sale_order.get_portal_last_transaction()"/>
</t>
</t>
</div>
<div class="mb8">
<strong>Date:</strong> <span t-field="sale_order.create_date" t-options='{"widget": "date"}'/>
</div>
<div class="mb8" t-if="sale_order.validity_date">
<strong>Expiration Date:</strong> <span t-field="sale_order.validity_date" t-options='{"widget": "date"}'/>
</div>
<div class='row'>
<div class="col-lg-6">
<div>
<strong>Invoicing Address</strong>
</div>
<div>
<address t-field="sale_order.partner_invoice_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
</div>
<div id="shipping_address" class="col-lg-6" groups="sale.group_delivery_invoice_address">
<div>
<strong>Shipping Address</strong>
</div>
<div>
<address t-field="order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
</div>
</div>
<hr/>
<t t-set="display_discount" t-value="any([l.discount for l in order.order_line])"/>
<div class="row">
<div t-att-class="'col-xl-4 col-md-4 col-5' if display_discount else 'col-md-5 col-6'">
<strong>Product</strong>
</div>
<div class="col-md-3 d-none d-md-block text-right">
<strong>Unit Price</strong>
</div>
<div t-if="display_discount" class="col-1 text-right" >
<strong>Discount</strong>
</div>
<div class="d-none d-md-block col-md-2 text-right">
<strong>Quantity</strong>
</div>
<div class="col-2 d-md-none text-right">
<strong>Qty.</strong>
</div>
<div class="col-md-2 col-4 text-right">
<strong>Subtotal</strong>
</div>
</div>
<t t-set="current_subtotal" t-value="0"/>
<t t-foreach="order.order_line" t-as="line">
<t t-set="current_subtotal" t-value="current_subtotal + line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
<t t-set="current_subtotal" t-value="current_subtotal + line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
<div t-attf-class="row orders_vertical_align {{ 'o_is_' + line.display_type if line.display_type else '' }} pt8 pb8">
<t t-if="not line.display_type">
<div t-if="not request.env.user._is_public()" class="d-none d-xl-block col-xl-1 text-center">
<span t-field="line.product_image" t-options="{'widget': 'image', 'class': 'rounded', 'style':'width: 48px;height:48px'}" />
</div>
<div id='product_name' t-att-class="'col-xl-3 col-md-4 col-5' if display_discount else 'col-xl-4 col-md-5 col-6'">
<span t-field="line.product_id.name"/>
</div>
<div t-if="request.env.user._is_public()" class="d-none d-xl-block col-xl-1 text-center" /> <!-- Just for alignment -->
<div class="col-md-3 d-none d-md-block text-right">
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
<div>
<strong>Shipping Address</strong>
</div>
<div t-if="display_discount" class="col-1 text-right" style='white-space: nowrap'>
<span t-field="line.discount"/> %
<div>
<address t-field="sale_order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
<div class="col-2 text-right">
<span t-field="line.product_uom_qty"/>
</div>
<div class="col-md-2 col-4 text-right">
<span
t-field="line.price_subtotal"
t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'
groups="account.group_show_line_subtotals_tax_excluded"
/>
<span
t-field="line.price_total"
t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'
groups="account.group_show_line_subtotals_tax_included"
/>
</div>
</t>
<t t-elif="line.display_type == 'line_note'">
<div class="col-12">
<span t-field="line.name"/>
</div>
</t>
<t t-elif="line.display_type == 'line_section'">
<t t-set="current_section" t-value="line"/>
<t t-set="current_subtotal" t-value="0"/>
<div class="col-12">
<span t-field="line.name"/>
</div>
</t>
</div>
<!--
If we have a currently running subtotal, we need to display it
when we find a new section or when we reach the end of the loop.
-->
<t t-if="current_section and (line_last or order.order_line[line_index+1].display_type == 'line_section')">
<div t-attf-class="row orders_vertical_align is-subtotal pt8 pb8">
<div class="col-12 text-right">
<strong>Subtotal:</strong> <span
t-esc="current_subtotal"
t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'
/>
</div>
</div>
</t>
</t>
<hr/>
<div class="row">
</div>
<div class="row">
<div class="col-lg-6">
<div>
<strong>Contact</strong>
</div>
<div t-field="order.user_id.partner_id" t-options='{"widget": "contact", "fields": ["email", "phone"]}'/>
<br/>
</div>
<div class="col-lg-6">
<div class="row mb4">
<div class="col-lg-9 col-8 text-right">
<strong>Subtotal</strong>
</div>
<div class="col-lg-3 col-4 text-right">
<strong>
<span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</strong>
</div>
</div>
<div class="row mb4">
<t t-set="tax_groups" t-value="order.amount_by_group"/>
<t t-foreach="tax_groups" t-as="amount_by_group">
<t t-if="len(tax_groups) == 1 and order.amount_untaxed == amount_by_group[2]">
<div class="col-lg-9 col-8 text-right"><span t-esc="amount_by_group[0]"/></div>
<div class="col-lg-3 col-4 text-right">
<span t-esc="amount_by_group[1]"
t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</div>
</t>
<t t-else ="">
<div class="col-lg-9 col-8 text-right">
<span t-esc="amount_by_group[0]"/>
<span>&amp;nbsp;<span>on</span>&amp;nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></span>
</div>
<div class="col-lg-3 col-4 text-right">
<span t-esc="amount_by_group[1]"
t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
</div>
</t>
<t t-set="invoices" t-value="[i for i in sale_order.invoice_ids if i.state not in ['draft', 'cancel']]"/>
<t t-if="invoices">
<div>
<strong>Invoices</strong>
</div>
<div>
<t t-foreach="invoices" t-as="i">
<t t-set="report_url" t-value="'/my/invoices/pdf/%s?%s' % (i.id, 'access_token=%s' % i.access_token if access_token else '')"/>
<div>
<a t-att-href="report_url"><span class="fa fa-download" role="img" aria-label="Download" title="Download"/></a>
<a t-att-href="report_url"><span t-esc="i.number"/></a>
<span class="text-muted" t-field="i.date_invoice"/>
<t t-if="i.state == 'paid'">
<span class="badge badge-success orders_label_text_align"><i class="fa fa-fw fa-check"/> Paid</span>
</t>
<t t-if="i.state != 'paid'">
<span class="badge badge-info orders_label_text_align"><i class="fa fa-fw fa-clock-o"/> Waiting Payment</span>
</t>
</div>
</t>
</div>
</t>
</div>
<div class="row">
<div class="col-lg-9 col-8 text-right">
<strong>Total</strong>
</div>
<div class="col-lg-3 col-4 text-right">
<strong><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></strong>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="row" t-if="order.state == 'sent' and portal_confirmation == 'sign'" id="portal_sign">
<div class="offset-lg-3 col-lg-6">
<h4>Accept &amp; Sign</h4>
<t t-call="portal.portal_signature">
<t t-set="object" t-value="order"/>
<t t-set="partner_name" t-value="order.partner_id.name"/>
<t t-set="callUrl" t-value="'/my/quotes/accept'"/>
<t t-set="signLabel" t-value="'Accept Order'"/>
<t t-set="accessToken" t-value="order.access_token"/>
</div>
<div class="oe_structure"/>
<!-- Pricing -->
<!-- Signature -->
<section id="details">
<h1 class="page-header" id="details">Details</h1>
<t t-set="display_taxamount" t-value="any([line.tax_id for line in sale_order.order_line])"/>
<t t-set="display_discount" t-value="True in [line.discount > 0 for line in sale_order.order_line]"/>
<div style="overflow-x:auto;">
<table class="table" id="sales_order_table">
<thead>
<tr>
<t t-set="colspan" t-value="6"/>
<th></th>
<th>Products</th>
<th>Quantity</th>
<th t-if="display_taxamount">
<span>Taxes</span>
<t t-set="colspan" t-value="colspan+1"/>
</th>
<th t-if="display_discount">
<span>Discount</span>
<t t-set="colspan" t-value="colspan+1"/>
</th>
<th class="text-right">Unit Price</th>
<th class="text-right" >
<span groups="account.group_show_line_subtotals_tax_excluded">Amount</span>
<span groups="account.group_show_line_subtotals_tax_included">Total Price</span>
</th>
</tr>
</thead>
<tbody class="sale_tbody">
<t t-set="current_subtotal" t-value="0"/>
<t t-foreach="sale_order.order_line" t-as="line">
<t t-set="current_subtotal" t-value="current_subtotal + line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
<t t-set="current_subtotal" t-value="current_subtotal + line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
<tr t-attf-class="{{ 'o_is_' + line.display_type if line.display_type else '' }}">
<t t-if="not line.display_type">
<td class="text-center">
<div class="d-none d-xl-block">
<span t-field="line.product_image" t-options="{'widget': 'image', 'class': 'rounded', 'style':'width: 48px;height:48px'}" />
</div>
</td>
<td id="product_name"><span t-field="line.name"/></td>
<td>
<div id="quote_qty">
<span t-field="line.product_uom_qty"/>
<span t-field="line.product_uom" groups="uom.group_uom"/>
</div>
</td>
<td t-if="display_taxamount">
<span t-esc="', '.join(map(lambda x: (x.description or x.name), line.tax_id))"/>
</td>
<td t-if="display_discount">
<strong t-if="line.discount" class="text-info">
<t t-esc="((line.discount % 1) and '%s' or '%d') % line.discount"/>% discount
</strong>
</td>
<td class="text-right">
<div
t-field="line.price_unit"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
t-att-style="line.discount and 'text-decoration: line-through' or None"
t-att-class="(line.discount and 'text-danger' or '') + ' text-right'"
/>
<div t-if="line.discount">
<t t-esc="(1-line.discount / 100.0) * line.price_unit" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
</div>
</td>
<td class="text-right">
<span
t-field="line.price_subtotal"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
groups="account.group_show_line_subtotals_tax_excluded"
/>
<span
t-field="line.price_total"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
groups="account.group_show_line_subtotals_tax_included"
/>
</td>
</t>
<t t-if="line.display_type == 'line_section'">
<td t-att-colspan="colspan">
<span t-field="line.name"/>
</td>
<t t-set="current_section" t-value="line"/>
<t t-set="current_subtotal" t-value="0"/>
</t>
<t t-if="line.display_type == 'line_note'">
<td t-att-colspan="colspan">
<span t-field="line.name"/>
</td>
</t>
</tr>
<t t-if="current_section and (line_last or sale_order.order_line[line_index+1].display_type == 'line_section')">
<tr class="is-subtotal text-right">
<td t-att-colspan="colspan">
<strong>Subtotal:</strong> <span
t-esc="current_subtotal"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
/>
</td>
</tr>
</t>
</t>
</tbody>
</table>
</div>
<div class="clearfix">
<div id="total" class="row" name="total">
<div class="col-8 col-sm-6 col-md-4 ml-auto">
<table class="table table-sm">
<tr class="border-black" style="border-bottom:1px solid #dddddd;">
<td><strong>Subtotal</strong></td>
<td class="text-right">
<span
data-id="total_amount"
t-field="sale_order.amount_untaxed"
t-options='{"widget": "monetary","display_currency": sale_order.pricelist_id.currency_id}'
/>
</td>
</tr>
<t t-foreach="sale_order.amount_by_group" t-as="amount_by_group">
<tr style="border-bottom:1px solid #dddddd;">
<t t-if="amount_by_group[3] == 1 and sale_order.amount_untaxed == amount_by_group[2]">
<td>
<span t-esc="amount_by_group[0]"/>
<span>&amp;nbsp;<span>on</span>&amp;nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/></span>
</td>
<td class="text-right">
<span t-esc="amount_by_group[1]"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
</td>
</t>
<t t-else ="">
<td>
<span t-esc="amount_by_group[0]"/>
</td>
<td class="text-right">
<span t-esc="amount_by_group[1]"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
</td>
</t>
</tr>
</t>
<tr class="border-black">
<td><strong>Total</strong></td>
<td class="text-right">
<span data-id="total_amount" t-field="sale_order.amount_total" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="oe_structure mb32"/>
</t>
</section>
<section id="terms" class="container" t-if="sale_order.note">
<h2 class="page-header">Terms &amp; Conditions</h2>
<p t-field="sale_order.note"/>
</section>
<section t-if="sale_order.signature" id="signature" name="Signature">
<div class="row mt64" name="signature">
<div class="offset-8 col-4">
<strong>Signature</strong>
</div>
<div class="offset-8 col-4">
<img t-att-src="'data:image/png;base64,%s' % to_text(sale_order.signature)" style="max-height: 4cm; max-width: 8cm;"/>
</div>
<div class="offset-8 col-4 text-center">
<p t-field="sale_order.signed_by"/>
</div>
</div>
</section>
</template>
<template id="portal_order_error" name="Order error/warning display">
<div class="row mr16">
<div t-attf-class="'col-lg-12 mr16 ml16 alert alert-dismissable' #{'alert-danger' if error else 'alert-warning'}" role="alert">
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
<t t-if="error == 'generic'" name="generic">
There was an error processing this page.
</t>
</div>
</div>
</template>
<template id="portal_order_success" name="Order success display">
<div class="row mr16">
<div class="col-lg-12 mr16 ml16 alert alert-dismissable alert-success" role="status">
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
</div>
</div>
</template>
</odoo>
+10 -2
View File
@@ -209,14 +209,22 @@
<field name="arch" type="xml">
<form string="Sales Order" class="o_sale_order">
<header>
<field name="authorized_transaction_ids" invisible="1"/>
<button name="payment_action_capture" type="object"
string="Capture Transaction" class="oe_highlight"
attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/>
<button name="payment_action_void" type="object"
string="Void Transaction"
confirm="Are you sure you want to void the authorized transaction? This action can't be undone."
attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/>
<button name="action_quotation_send" string="Send by Email" type="object" states="draft" class="btn-primary"/>
<button name="action_quotation_send" type="object" string="Send PRO-FORMA Invoice" groups="sale.group_proforma_sales" class="btn-primary" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('invoice_count','&gt;=',1)]}" context="{'proforma': True}"/>
<button name="print_quotation" string="Print" type="object" states="draft" class="btn-primary o_sale_print"/>
<button name="action_confirm" id="action_confirm"
string="Confirm Sale" class="btn-primary" type="object"
string="Confirm" class="btn-primary" type="object"
attrs="{'invisible': [('state', 'not in', ['sent'])]}"/>
<button name="action_confirm"
string="Confirm Sale" type="object"
string="Confirm" type="object"
attrs="{'invisible': [('state', 'not in', ['draft'])]}"/>
<button name="action_quotation_send" type="object" string="Send PRO-FORMA Invoice" groups="sale.group_proforma_sales" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('invoice_count','&gt;=',1)]}" context="{'proforma': True}"/>
<button name="print_quotation" string="Print" type="object" states="sent,sale" class="o_sale_print"/>
@@ -13,6 +13,8 @@ class TestReInvoice(TestExpenseCommon, TestCommonSaleNoChart):
cls.setUpExpenseProducts()
cls.env.ref('product.list0').currency_id = cls.env.user.company_id.currency_id
# partner and SO
cls.partner_customer = cls.env['res.partner'].create({
'name': 'Ze Client',
+1
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers
from odoo.api import Environment, SUPERUSER_ID
+12 -2
View File
@@ -41,12 +41,22 @@ The Dashboard for the Sales Manager will include
* Monthly Turnover (Graph)
""",
'website': 'https://www.odoo.com/page/sales',
'depends': ['sale', 'account', 'digest'],
'depends': ['sale', 'digest'],
'data': [
'security/sale_management_security.xml',
'data/sale_order_template_data.xml',
'views/sale_portal_templates.xml',
'views/sale_order_template_views.xml',
'security/ir.model.access.csv',
'views/res_config_settings_views.xml',
'data/digest_data.xml',
'views/sale_management_views.xml',
'views/sale_management_templates.xml',
'views/digest_views.xml',
'views/sale_order_views.xml',
'report/sale_report_templates.xml',
],
'demo': [
'data/sale_order_template_demo.xml',
],
'application': True,
'uninstall_hook': 'uninstall_hook',
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
from . import portal
@@ -0,0 +1,165 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http, _
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.addons.sale.controllers.portal import CustomerPortal
from odoo.addons.portal.controllers.mail import _message_post_helper
class CustomerPortal(CustomerPortal):
@http.route(['/my/orders/<int:order_id>/accept'], type='json', auth="public", website=True)
def portal_quote_accept(self, res_id, access_token=None, partner_name=None, signature=None, order_id=None):
try:
order_sudo = self._document_check_access('sale.order', res_id, access_token=access_token)
except AccessError:
return {'error': _('Invalid order')}
if order_sudo.state != 'sent':
return {'error': _('Order is not in a state requiring customer validation.')}
if not order_sudo.has_to_be_signed() and not order_sudo.has_to_be_paid():
return {'error': _('Operation not allowed')}
if not signature:
return {'error': _('Signature is missing.')}
success_message = _('Your order has been signed but still needs to be paid to be confirmed.')
if not order_sudo.has_to_be_paid():
order_sudo.action_confirm()
success_message = _('Your order has been confirmed.')
order_sudo.signature = signature
order_sudo.signed_by = partner_name
pdf = request.env.ref('sale.action_report_saleorder').sudo().render_qweb_pdf([order_sudo.id])[0]
_message_post_helper(
res_model='sale.order',
res_id=order_sudo.id,
message=_('Order signed by %s') % (partner_name,),
attachments=[('%s.pdf' % order_sudo.name, pdf)],
**({'token': access_token} if access_token else {}))
return {
'success': success_message,
'redirect_url': order_sudo.get_portal_url(),
}
@http.route(['/my/orders/<int:order_id>/decline'], type='http', auth="public", methods=['POST'], website=True)
def decline(self, order_id, access_token=None, **post):
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
except AccessError:
return request.redirect('/my')
if order_sudo.state != 'sent':
return request.redirect(order_sudo.get_portal_url() + "&message=4")
order_sudo.action_cancel()
message = post.get('decline_message')
if message:
_message_post_helper(message=message, res_id=order_id, res_model='sale.order', **{'token': access_token} if access_token else {})
return request.redirect(order_sudo.get_portal_url())
@http.route(['/my/orders/<int:order_id>/update_line'], type='json', auth="public", website=True)
def update(self, line_id, remove=False, unlink=False, order_id=None, access_token=None, **post):
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
except AccessError:
return request.redirect('/my')
if order_sudo.state not in ('draft', 'sent'):
return False
order_line = request.env['sale.order.line'].sudo().browse(int(line_id))
if order_line.order_id != order_sudo:
return False
if unlink:
order_line.unlink()
return False # return False to reload the page, the line must move back to options and the JS doesn't handle it
number = -1 if remove else 1
quantity = order_line.product_uom_qty + number
order_line.write({'product_uom_qty': quantity})
return [str(quantity), str(order_sudo.amount_total)]
@http.route(["/my/orders/<int:order_id>/add_option/<int:option_id>"], type='http', auth="public", website=True)
def add(self, order_id, option_id, access_token=None, **post):
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
except AccessError:
return request.redirect('/my')
option_sudo = request.env['sale.order.option'].sudo().browse(option_id)
if order_sudo != option_sudo.order_id:
return request.redirect(order_sudo.get_portal_url())
option_sudo.add_option_to_order()
return request.redirect(option_sudo.order_id.get_portal_url() + "#details")
# note dbo: website_sale code
@http.route(['/my/orders/<int:order_id>/transaction/'], type='json', auth="public", website=True)
def payment_transaction_token(self, acquirer_id, order_id, save_token=False, access_token=None, **kwargs):
""" Json method that creates a payment.transaction, used to create a
transaction when the user clicks on 'pay now' button. After having
created the transaction, the event continues and the user is redirected
to the acquirer website.
:param int acquirer_id: id of a payment.acquirer record. If not set the
user is redirected to the checkout page
"""
# Ensure a payment acquirer is selected
if not acquirer_id:
return False
try:
acquirer_id = int(acquirer_id)
except:
return False
order = request.env['sale.order'].sudo().browse(order_id)
if not order or not order.order_line:
return False
# Create transaction
vals = {
'acquirer_id': acquirer_id,
'type': order._get_payment_type(),
}
transaction = order._create_payment_transaction(vals)
return transaction.render_sale_button(
order,
order.get_portal_url(),
submit_txt=_('Pay & Confirm'),
render_values={
'type': order._get_payment_type(),
'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'),
}
)
@http.route('/my/orders/<int:order_id>/transaction/token', type='http', auth='public', website=True)
def payment_token(self, order_id, pm_id=None, **kwargs):
order = request.env['sale.order'].sudo().browse(order_id)
if not order:
return request.redirect("/my/orders")
if not order.order_line or pm_id is None:
return request.redirect(order.get_portal_url())
# try to convert pm_id into an integer, if it doesn't work redirect the user to the quote
try:
pm_id = int(pm_id)
except ValueError:
return request.redirect(order.get_portal_url())
# Create transaction
vals = {
'payment_token_id': pm_id,
'type': 'server2server',
}
order._create_payment_transaction(vals)
return request.redirect(order.get_portal_url())
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Mails & quote templates is done in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="1">
<record id="sale_order_template_default" model="sale.order.template">
<field name="name">Default Template</field>
<field name="number_of_days">30</field>
</record>
</data>
</odoo>
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="website_quote_template_1" model="sale.quote.template">
<record id="sale_order_template_1" model="sale.order.template">
<field name="name">4 Person Desk</field>
<field name="number_of_days">45</field>
</record>
<record id="website_sale_order_line_1" model="sale.quote.line">
<field name="quote_id" ref="website_quote_template_1"/>
<record id="sale_order_template_line_1" model="sale.order.template.line">
<field name="sale_order_template_id" ref="sale_order_template_1"/>
<field name="name">4 Person Desk</field>
<field name="product_id" ref="product.consu_delivery_03"/>
<field name="product_uom_qty">1</field>
@@ -15,8 +15,8 @@
<field name="discount">10.00</field>
</record>
<record id="website_sale_option_line_1" model="sale.quote.option">
<field name="template_id" ref="website_quote_template_1"/>
<record id="sale_order_template_option_1" model="sale.order.template.option">
<field name="sale_order_template_id" ref="sale_order_template_1"/>
<field name="name">Office Chair</field>
<field name="product_id" ref="product.product_delivery_01"/>
<field name="quantity">4</field>
@@ -1,3 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import digest
from . import res_config_settings
from . import res_users
from . import sale_order
from . import sale_order_template
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
group_sale_order_template = fields.Boolean("Quotation Templates", implied_group='sale_management.group_sale_order_template')
default_sale_order_template_id = fields.Many2one('sale.order.template', default_model='sale.order', string='Default Template')
module_sale_quotation_builder = fields.Boolean("Quotation Builder")
@api.onchange('group_sale_order_template')
def _onchange_group_sale_order_template(self):
if not self.group_sale_order_template:
self.module_sale_quotation_builder = False
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResUsers(models.Model):
_inherit = 'res.users'
has_group_sale_order_template = fields.Boolean(
"Quotation Templates",
compute='_compute_groups_id', inverse='_inverse_groups_id',
group_xml_id='sale_management.group_sale_order_template')
@@ -4,86 +4,47 @@
from datetime import datetime, timedelta
from odoo import api, fields, models, _
from odoo.tools.translate import html_translate
from odoo.addons import decimal_precision as dp
from odoo.exceptions import ValidationError
from odoo.exceptions import UserError
from werkzeug.urls import url_encode
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
_description = "Sales Order Line"
website_description = fields.Html('Line Description', sanitize=False, translate=html_translate)
option_line_id = fields.One2many('sale.order.option', 'line_id', 'Optional Products Lines')
# Take the description on the order template if the product is present in it
@api.onchange('product_id')
def product_id_change(self):
domain = super(SaleOrderLine, self).product_id_change()
if self.product_id and self.order_id.template_id:
for quote_line in self.order_id.template_id.quote_line:
if quote_line.product_id == self.product_id:
self.name = quote_line.name
break
return domain
@api.model
def create(self, values):
values = self._inject_quote_description(values)
return super(SaleOrderLine, self).create(values)
@api.multi
def write(self, values):
values = self._inject_quote_description(values)
return super(SaleOrderLine, self).write(values)
def _inject_quote_description(self, values):
values = dict(values or {})
if not values.get('website_description') and values.get('product_id'):
product = self.env['product.product'].browse(values['product_id'])
values['website_description'] = product.quote_description or product.website_description
return values
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _get_default_template(self):
template = self.env.ref('website_quote.website_quote_template_default', raise_if_not_found=False)
return template and template.active and template or False
def _get_default_sale_order_template(self):
if not self.env.user.has_group('sale_management.group_sale_order_template'):
return False
template = self.env.ref('sale_management.sale_order_template_default', raise_if_not_found=False)
return template if template and template.active else False
def _get_default_require_signature(self):
default_template = self._get_default_template()
default_template = self._get_default_sale_order_template()
if default_template:
return default_template.require_signature
else:
return False
def _get_default_require_payment(self):
default_template = self._get_default_template()
default_template = self._get_default_sale_order_template()
if default_template:
return default_template.require_payment
else:
return False
template_id = fields.Many2one(
'sale.quote.template', 'Quotation Template',
sale_order_template_id = fields.Many2one(
'sale.order.template', 'Quotation Template',
readonly=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
default=_get_default_template)
website_description = fields.Html('Description', sanitize_attributes=False, translate=html_translate)
options = fields.One2many(
default=_get_default_sale_order_template)
sale_order_option_ids = fields.One2many(
'sale.order.option', 'order_id', 'Optional Products Lines',
copy=True, readonly=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]})
amount_undiscounted = fields.Float(
'Amount Before Discount', compute='_compute_amount_undiscounted', digits=0)
quote_viewed = fields.Boolean('Quotation Viewed')
require_signature = fields.Boolean('Digital Signature', default=_get_default_require_signature,
require_signature = fields.Boolean('Online Signature', default=_get_default_require_signature,
states={'sale': [('readonly', True)], 'done': [('readonly', True)]},
help='Request a digital signature to the customer in order to confirm orders automatically.')
help='Request a online signature to the customer in order to confirm orders automatically.')
require_payment = fields.Boolean('Electronic Payment', default=_get_default_require_payment,
states={'sale': [('readonly', True)], 'done': [('readonly', True)]},
help='Request an electronic payment to the customer in order to confirm orders automatically.')
@@ -91,47 +52,48 @@ class SaleOrder(models.Model):
@api.multi
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
if self.template_id and self.template_id.number_of_days > 0:
if self.sale_order_template_id and self.sale_order_template_id.number_of_days > 0:
default = dict(default or {})
default['validity_date'] = fields.Date.to_string(datetime.now() + timedelta(self.template_id.number_of_days))
default['validity_date'] = fields.Date.to_string(datetime.now() + timedelta(self.sale_order_template_id.number_of_days))
return super(SaleOrder, self).copy(default=default)
@api.one
def _compute_amount_undiscounted(self):
total = 0.0
for line in self.order_line:
total += line.price_subtotal + line.price_unit * ((line.discount or 0.0) / 100.0) * line.product_uom_qty # why is there a discount in a field named amount_undiscounted ??
self.amount_undiscounted = total
@api.onchange('partner_id')
def onchange_partner_id(self):
super(SaleOrder, self).onchange_partner_id()
self.note = self.template_id.note or self.note
self.note = self.sale_order_template_id.note or self.note
@api.onchange('partner_id')
def onchange_update_description_lang(self):
if not self.template_id:
return
def _compute_line_data_for_template_change(self, line):
return {
'display_type': line.display_type,
'name': line.name,
'state': 'draft',
}
def _compute_option_data_for_template_change(self, option):
if self.pricelist_id:
price = self.pricelist_id.with_context(uom=option.uom_id.id).get_product_price(option.product_id, 1, False)
else:
template = self.template_id.with_context(lang=self.partner_id.lang)
self.website_description = template.website_description
price = option.price_unit
return {
'product_id': option.product_id.id,
'name': option.name,
'quantity': option.quantity,
'uom_id': option.uom_id.id,
'price_unit': price,
'discount': option.discount,
}
@api.onchange('template_id')
def onchange_template_id(self):
if not self.template_id:
@api.onchange('sale_order_template_id')
def onchange_sale_order_template_id(self):
if not self.sale_order_template_id:
self.require_signature = False
self.require_payment = False
return
template = self.template_id.with_context(lang=self.partner_id.lang)
template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
order_lines = [(5, 0, 0)]
for line in template.quote_line:
data = {
'display_type': line.display_type,
'name': line.name,
'website_description': line.website_description,
'state': 'draft',
}
for line in template.sale_order_template_line_ids:
data = self._compute_line_data_for_template_change(line)
if line.product_id:
discount = 0
if self.pricelist_id:
@@ -145,7 +107,7 @@ class SaleOrder(models.Model):
data.update({
'price_unit': price,
'discount': 100 - ((100 - discount) * (100 - line.discount)/100),
'discount': 100 - ((100 - discount) * (100 - line.discount) / 100),
'product_uom_qty': line.product_uom_qty,
'product_id': line.product_id.id,
'product_uom': line.product_uom_id.id,
@@ -159,47 +121,27 @@ class SaleOrder(models.Model):
self.order_line._compute_tax_id()
option_lines = []
for option in template.options:
if self.pricelist_id:
price = self.pricelist_id.with_context(uom=option.uom_id.id).get_product_price(option.product_id, 1, False)
else:
price = option.price_unit
data = {
'product_id': option.product_id.id,
'name': option.name,
'quantity': option.quantity,
'uom_id': option.uom_id.id,
'price_unit': price,
'discount': option.discount,
'website_description': option.website_description,
}
for option in template.sale_order_template_option_ids:
data = self._compute_option_data_for_template_change(option)
option_lines.append((0, 0, data))
self.options = option_lines
self.sale_order_option_ids = option_lines
if template.number_of_days > 0:
self.validity_date = fields.Date.to_string(datetime.now() + timedelta(template.number_of_days))
self.website_description = template.website_description
self.require_signature = template.require_signature
self.require_payment = template.require_payment
if template.note:
self.note = template.note
@api.constrains('template_id', 'require_signature', 'require_payment')
def _check_portal_confirmation(self):
for order in self.sudo().filtered('template_id'):
if not order.require_signature and not order.require_payment:
raise ValidationError(_('Please select a confirmation mode in Other Information: Digital Signature, Electronic Payment or both.'))
@api.multi
def open_quotation(self):
def preview_sale_order(self):
self.ensure_one()
self.write({'quote_viewed': True})
return {
'type': 'ir.actions.act_url',
'target': 'self',
'url': '/quote/%s/%s' % (self.id, self.access_token)
'url': self.get_portal_url(),
}
@api.multi
@@ -208,11 +150,11 @@ class SaleOrder(models.Model):
self.ensure_one()
user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user
if not self.template_id or (not user.share and not self.env.context.get('force_website')):
if not self.sale_order_template_id or (not user.share and not self.env.context.get('force_website')):
return super(SaleOrder, self).get_access_action(access_uid)
return {
'type': 'ir.actions.act_url',
'url': '/quote/%s/%s' % (self.id, self.access_token),
'url': self.get_portal_url(),
'target': 'self',
'res_id': self.id,
}
@@ -221,12 +163,12 @@ class SaleOrder(models.Model):
self.ensure_one()
if self.state not in ['sale', 'done']:
auth_param = url_encode(self.partner_id.signup_get_auth_param()[self.partner_id.id])
return '/quote/%s/%s?' % (self.id, self.access_token) + auth_param
return self.get_portal_url() + '&%s' % auth_param
return super(SaleOrder, self)._get_share_url(redirect)
def get_portal_confirmation_action(self):
""" Template override default behavior of pay / sign chosen in sales settings """
if self.template_id:
if self.sale_order_template_id:
if self.require_signature and not self.signature:
return 'sign'
elif self.require_payment:
@@ -237,18 +179,18 @@ class SaleOrder(models.Model):
def has_to_be_signed(self):
res = super(SaleOrder, self).has_to_be_signed()
return self.require_signature if self.template_id else res
return self.require_signature if self.sale_order_template_id else res
def has_to_be_paid(self):
res = super(SaleOrder, self).has_to_be_paid()
return self.require_payment if self.template_id else res
return self.require_payment if self.sale_order_template_id else res
@api.multi
def action_confirm(self):
res = super(SaleOrder, self).action_confirm()
for order in self:
if order.template_id and order.template_id.mail_template_id:
self.template_id.mail_template_id.send_mail(order.id)
if order.sale_order_template_id and order.sale_order_template_id.mail_template_id:
self.sale_order_template_id.mail_template_id.send_mail(order.id)
return res
@api.multi
@@ -257,6 +199,24 @@ class SaleOrder(models.Model):
return 'form_save' if self.require_payment else 'form'
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
_description = "Sales Order Line"
sale_order_option_ids = fields.One2many('sale.order.option', 'line_id', 'Optional Products Lines')
# Take the description on the order template if the product is present in it
@api.onchange('product_id')
def product_id_change(self):
domain = super(SaleOrderLine, self).product_id_change()
if self.product_id and self.order_id.sale_order_template_id:
for line in self.order_id.sale_order_template_id.sale_order_template_line_ids:
if line.product_id == self.product_id:
self.name = line.name
break
return domain
class SaleOrderOption(models.Model):
_name = "sale.order.option"
_description = "Sale Options"
@@ -266,7 +226,6 @@ class SaleOrderOption(models.Model):
line_id = fields.Many2one('sale.order.line', on_delete="set null")
name = fields.Text('Description', required=True)
product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)])
website_description = fields.Html('Line Description', sanitize_attributes=False, translate=html_translate)
price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'))
discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount'))
uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True)
@@ -279,7 +238,6 @@ class SaleOrderOption(models.Model):
return
product = self.product_id.with_context(lang=self.order_id.partner_id.lang)
self.price_unit = product.list_price
self.website_description = product.quote_description or product.website_description
self.name = product.name
if product.description_sale:
self.name += '\n' + product.description_sale
@@ -293,28 +251,33 @@ class SaleOrderOption(models.Model):
@api.multi
def button_add_to_order(self):
self.ensure_one()
order = self.order_id
if order.state not in ['draft', 'sent']:
return False
self.add_option_to_order()
return {'type': 'ir.actions.client', 'tag': 'reload'}
order_line = order.order_line.filtered(lambda line: line.product_id == self.product_id)
if order_line:
order_line = order_line[0]
order_line.product_uom_qty += 1
else:
vals = {
'price_unit': self.price_unit,
'website_description': self.website_description,
'name': self.name,
'order_id': order.id,
'product_id': self.product_id.id,
'product_uom_qty': self.quantity,
'product_uom': self.uom_id.id,
'discount': self.discount,
}
order_line = self.env['sale.order.line'].create(vals)
order_line._compute_tax_id()
@api.multi
def add_option_to_order(self):
self.ensure_one()
sale_order = self.order_id
if sale_order.state not in ['draft', 'sent']:
raise UserError(_('You cannot add options to a confirmed order.'))
values = self._get_values_to_add_to_order()
order_line = self.env['sale.order.line'].create(values)
order_line._compute_tax_id()
self.write({'line_id': order_line.id})
return {'type': 'ir.actions.client', 'tag': 'reload'}
@api.multi
def _get_values_to_add_to_order(self):
self.ensure_one()
return {
'order_id': self.order_id.id,
'price_unit': self.price_unit,
'name': self.name,
'product_id': self.product_id.id,
'product_uom_qty': self.quantity,
'product_uom': self.uom_id.id,
'discount': self.discount,
}
@@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools.translate import html_translate
from odoo import api, fields, models
from odoo.addons import decimal_precision as dp
from odoo.exceptions import ValidationError, UserError
from odoo.exceptions import UserError
class SaleQuoteTemplate(models.Model):
_name = "sale.quote.template"
_description = "Sale Quotation Template"
class SaleOrderTemplate(models.Model):
_name = "sale.order.template"
_description = "Quotation Template"
def _get_default_require_signature(self):
# A confirmation mode (sign or pay) is mandatory on a quotation template
@@ -21,49 +21,31 @@ class SaleQuoteTemplate(models.Model):
return self.env.user.company_id.portal_confirmation_pay
name = fields.Char('Quotation Template', required=True)
website_description = fields.Html('Description', translate=html_translate, sanitize_attributes=False)
quote_line = fields.One2many('sale.quote.line', 'quote_id', 'Quotation Template Lines', copy=True)
sale_order_template_line_ids = fields.One2many('sale.order.template.line', 'sale_order_template_id', 'Lines', copy=True)
note = fields.Text('Terms and conditions')
options = fields.One2many('sale.quote.option', 'template_id', 'Optional Products Lines', copy=True)
sale_order_template_option_ids = fields.One2many('sale.order.template.option', 'sale_order_template_id', 'Optional Lines', copy=True)
number_of_days = fields.Integer('Quotation Duration',
help='Number of days for the validity date computation of the quotation')
require_signature = fields.Boolean('Digital Signature', default=_get_default_require_signature, help='Request a digital signature to the customer in order to confirm orders automatically.')
require_payment = fields.Boolean('Electronic Payment', default=_get_default_require_payment,help='Request an electronic payment to the customer in order to confirm orders automatically.')
require_signature = fields.Boolean('Online Signature', default=_get_default_require_signature, help='Request a online signature to the customer in order to confirm orders automatically.')
require_payment = fields.Boolean('Electronic Payment', default=_get_default_require_payment, help='Request an electronic payment to the customer in order to confirm orders automatically.')
mail_template_id = fields.Many2one(
'mail.template', 'Confirmation Mail',
domain=[('model', '=', 'sale.order')],
help="This e-mail template will be sent on confirmation. Leave empty to send nothing.")
active = fields.Boolean(default=True, help="If unchecked, it will allow you to hide the quotation template without removing it.")
@api.constrains('require_signature', 'require_payment')
def _check_confirmation(self):
for template in self:
if not template.require_signature and not template.require_payment:
raise ValidationError(_('Please select a confirmation mode in Confirmation: Digital Signature, Electronic Payment or both.'))
@api.multi
def open_template(self):
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'target': 'self',
'url': '/quote/template/%d' % self.id
}
class SaleQuoteLine(models.Model):
_name = "sale.quote.line"
_description = "Quotation Template Lines"
_order = 'quote_id, sequence, id'
class SaleOrderTemplateLine(models.Model):
_name = "sale.order.template.line"
_description = "Quotation Template Line"
_order = 'sale_order_template_id, sequence, id'
sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of sale quote lines.",
default=10)
quote_id = fields.Many2one('sale.quote.template', 'Quotation Template Reference', required=True,
sale_order_template_id = fields.Many2one('sale.order.template', 'Quotation Template Reference', required=True,
ondelete='cascade', index=True)
name = fields.Text('Description', required=True, translate=True)
product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)])
website_description = fields.Html('Line Description', related='product_id.product_tmpl_id.quote_description',
translate=html_translate)
price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'))
discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount'), default=0.0)
product_uom_qty = fields.Float('Quantity', required=True, digits=dp.get_precision('Product UoS'), default=1)
@@ -83,7 +65,6 @@ class SaleQuoteLine(models.Model):
self.name = name
self.price_unit = self.product_id.lst_price
self.product_uom_id = self.product_id.uom_id.id
self.website_description = self.product_id.quote_description or self.product_id.website_description or ''
domain = {'product_uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
return {'domain': domain}
@@ -96,22 +77,13 @@ class SaleQuoteLine(models.Model):
def create(self, values):
if values.get('display_type', self.default_get(['display_type'])['display_type']):
values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom_id=False)
values = self._inject_quote_description(values)
return super(SaleQuoteLine, self).create(values)
return super(SaleOrderTemplateLine, self).create(values)
@api.multi
def write(self, values):
if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
raise UserError("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type.")
values = self._inject_quote_description(values)
return super(SaleQuoteLine, self).write(values)
def _inject_quote_description(self, values):
values = dict(values or {})
if not values.get('website_description') and values.get('product_id'):
product = self.env['product.product'].browse(values['product_id'])
values['website_description'] = product.quote_description or product.website_description or ''
return values
return super(SaleOrderTemplateLine, self).write(values)
_sql_constraints = [
('accountable_product_id_required',
@@ -124,15 +96,14 @@ class SaleQuoteLine(models.Model):
]
class SaleQuoteOption(models.Model):
_name = "sale.quote.option"
_description = "Quotation Option"
class SaleOrderTemplateOption(models.Model):
_name = "sale.order.template.option"
_description = "Quotation Template Option"
template_id = fields.Many2one('sale.quote.template', 'Quotation Template Reference', ondelete='cascade',
sale_order_template_id = fields.Many2one('sale.order.template', 'Quotation Template Reference', ondelete='cascade',
index=True, required=True)
name = fields.Text('Description', required=True, translate=True)
product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True)
website_description = fields.Html('Option Description', translate=html_translate, sanitize_attributes=False)
price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'))
discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount'))
uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True)
@@ -144,7 +115,6 @@ class SaleQuoteOption(models.Model):
return
product = self.product_id
self.price_unit = product.list_price
self.website_description = product.product_tmpl_id.quote_description
self.name = product.name
self.uom_id = product.uom_id
domain = {'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
@@ -2,7 +2,7 @@
<odoo>
<template id="report_saleorder_validity_date" inherit_id="sale.report_saleorder_document">
<xpath expr="//div[hasclass('page')]/p[@id='fiscal_position_remark']" position="after">
<div t-if="doc.options and doc.state in ['draft', 'sent']" style="page-break-before:always;">
<div t-if="doc.sale_order_option_ids and doc.state in ['draft', 'sent']" style="page-break-before:always;">
<h2>
<span>Suggested Products</span>
</h2>
@@ -16,7 +16,7 @@
</tr>
</thead>
<tbody class="sale_tbody">
<tr t-foreach="doc.options" t-as="option">
<tr t-foreach="doc.sale_order_option_ids" t-as="option">
<td>
<span t-field="option.product_id.name"/>
</td>
@@ -0,0 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sale_order_template,sale.order.template,model_sale_order_template,sales_team.group_sale_salesman,1,0,0,0
access_sale_order_template_manager,sale.order.template,model_sale_order_template,sales_team.group_sale_manager,1,1,1,1
access_sale_order_template_line,sale.order.template.line,model_sale_order_template_line,sales_team.group_sale_salesman,1,0,0,0
access_sale_order_template_line_manager,sale.order.template.line,model_sale_order_template_line,sales_team.group_sale_manager,1,1,1,1
access_sale_order_template_option,sale.order.template.option,model_sale_order_template_option,sales_team.group_sale_salesman,1,0,0,0
access_sale_order_template_option_manager,sale.order.template.option,model_sale_order_template_option,sales_team.group_sale_manager,1,1,1,1
access_sale_order_option,sale.order.option,model_sale_order_option,sales_team.group_sale_salesman,1,1,1,1
access_sale_order_option_all,sale.order.option,model_sale_order_option,,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sale_order_template sale.order.template model_sale_order_template sales_team.group_sale_salesman 1 0 0 0
3 access_sale_order_template_manager sale.order.template model_sale_order_template sales_team.group_sale_manager 1 1 1 1
4 access_sale_order_template_line sale.order.template.line model_sale_order_template_line sales_team.group_sale_salesman 1 0 0 0
5 access_sale_order_template_line_manager sale.order.template.line model_sale_order_template_line sales_team.group_sale_manager 1 1 1 1
6 access_sale_order_template_option sale.order.template.option model_sale_order_template_option sales_team.group_sale_salesman 1 0 0 0
7 access_sale_order_template_option_manager sale.order.template.option model_sale_order_template_option sales_team.group_sale_manager 1 1 1 1
8 access_sale_order_option sale.order.option model_sale_order_option sales_team.group_sale_salesman 1 1 1 1
9 access_sale_order_option_all sale.order.option model_sale_order_option 1 0 0 0
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="group_sale_order_template" model="res.groups">
<field name="name">Quotation Templates</field>
</record>
</odoo>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

@@ -0,0 +1,51 @@
odoo.define('sale_management.sale_management', function (require) {
'use strict';
require('web.dom_ready');
var ajax = require('web.ajax');
var Widget = require('web.Widget');
if (!$('.o_portal_sale_sidebar').length) {
return $.Deferred().reject("DOM doesn't contain '.o_portal_sale_sidebar'");
}
// Add to SO button
var UpdateLineButton = Widget.extend({
events: {
'click' : 'onClick',
},
onClick: function (ev) {
ev.preventDefault();
var self = this;
var href = this.$el.attr("href");
var order_id = href.match(/my\/orders\/([0-9]+)/);
var line_id = href.match(/update_line\/([0-9]+)/);
var params = {
'line_id': line_id[1],
'remove': self.$el.is('[href*="remove"]'),
'unlink': self.$el.is('[href*="unlink"]'),
};
var token = href.match(/token=(.*)/);
if (token) {
params.access_token = token;
}
var url = "/my/orders/" + parseInt(order_id[1]) + "/update_line";
ajax.jsonRpc(url, 'call', params).then(function (data) {
if (!data) {
window.location.reload();
}
self.$el.parents('.input-group:first').find('.js_quantity').val(data[0]);
$('[data-id="total_amount"]>span').html(data[1]);
});
return false;
},
});
var update_button_list = [];
$('a.js_update_line_json').each(function (index) {
var button = new UpdateLineButton();
button.setElement($(this)).start();
update_button_list.push(button);
});
});
@@ -0,0 +1,48 @@
<?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.sale.management</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="sale.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='sale_config_online_confirmation_pay']" position="after">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="group_sale_order_template"/>
<field name="module_sale_quotation_builder" invisible="1"/>
</div>
<div class="o_setting_right_pane">
<label for="group_sale_order_template"/>
<div class="text-muted">
Create standardized offers
</div>
<div class="content-group" attrs="{'invisible': [('group_sale_order_template', '=', False)]}">
<div class="mt16">
<label for="default_sale_order_template_id" class="o_light_label"/>
<field name="default_sale_order_template_id" class="oe_inline"/>
</div>
<div>
<button name="%(sale_management.sale_order_template_action)d" icon="fa-arrow-right" type="action" string="Quotation Templates" class="btn-link"/>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" attrs="{'invisible': [('group_sale_order_template','=',False)]}">
<div class="o_setting_left_pane">
<field name="module_sale_quotation_builder"/>
</div>
<div class="o_setting_right_pane">
<label for="module_sale_quotation_builder"/>
<div class="text-muted">
Design your quotation templates using building blocks<br/>
<!-- TODO SEB don't show this after the module is installed -->
<em attrs="{'invisible': [('module_sale_quotation_builder','=',False)]}">Warning: this option will install the Website app.</em>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- inherits from sales_team.assets_backend to override certain attributes defined in sales_team -->
<template id="assets_backend" name="project assets" inherit_id="sales_team.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/sale_management/static/src/js/tour.js"></script>
</xpath>
</template>
</odoo>
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_template_view_search" model="ir.ui.view">
<field name="name">sale.order.template.search</field>
<field name="model">sale.order.template</field>
<field name="arch" type="xml">
<search string="Search Quotation Template">
<filter string="Archived" name="inactive" domain="[('active','=', False)]"/>
</search>
</field>
</record>
<record id="sale_order_template_view_form" model="ir.ui.view">
<field name="name">sale.order.template.form</field>
<field name="model">sale.order.template</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Quotation Template">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="Quotation Template"/>
</h1>
</div>
<group>
<group>
<label for="number_of_days" string="Quotation expires after"/>
<div id="number_of_days">
<field name="number_of_days" class="oe_inline"/> days
</div>
</group>
</group>
<notebook name="main_book">
<page string="Lines">
<field name="sale_order_template_line_ids" widget="section_and_note_one2many">
<form string="Quotation Template Lines">
<!--
We need the sequence field to be here for new lines to be added at the correct position.
TODO: at some point we want to fix this in the framework so that an invisible field is not required.
-->
<field name="sequence" invisible="1"/>
<field name="display_type" invisible="1"/>
<group attrs="{'invisible': [('display_type', '!=', False)]}">
<group>
<field
name="product_id"
attrs="{
'required': [('display_type', '=', False)]
}"
/>
<label for="product_uom_qty"/>
<div>
<field
name="product_uom_qty" class="oe_inline"/>
</div>
<field name="price_unit" invisible="1"/>
<label for="discount" groups="sale.group_discount_per_so_line"/>
<div groups="sale.group_discount_per_so_line">
<field name="discount" class="oe_inline"/> %%
</div>
</group>
</group>
<notebook colspan="4" name="description">
<page string="Description" attrs="{'invisible': [('display_type', '!=', False)]}">
<field name="name" />
</page>
<page string="Section" attrs="{'invisible': [('display_type', '!=', 'line_section')]}">
<field name="name" />
</page>
<page string="Note" attrs="{'invisible': [('display_type', '!=', 'line_note')]}">
<field name="name" />
</page>
</notebook>
</form>
<tree string="Quotation Template Lines" editable="bottom">
<control>
<create string="Add a product"/>
<create string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="display_type" invisible="1"/>
<field name="sequence" widget="handle"/>
<field name="product_id"/>
<field name="name" widget="section_and_note_text"/>
<field name="product_uom_qty"/>
<field
name="product_uom_id"
groups="uom.group_uom"
attrs="{
'required': [('display_type', '=', False)]
}"
/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
<field name="price_unit" invisible="1"/>
</tree>
</field>
</page>
<page string="Optional Products">
<field name="sale_order_template_option_ids">
<tree string="Quotation Template Lines" editable="bottom">
<field name="product_id"/>
<field name="name"/>
<field name="quantity"/>
<field name="uom_id" groups="uom.group_uom"/>
<field name="price_unit" invisible="1"/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
</tree>
</field>
</page>
<page string="Confirmation">
<group>
<field name="require_signature"/>
<field name="require_payment"/>
<field name="mail_template_id"/>
</group>
</page>
</notebook>
<field name="note" nolabel="1"
placeholder="The Administrator can set default Terms &amp; Conditions in Sales Settings. Terms set here will show up instead if you select this quotation template."/>
</sheet>
</form>
</field>
</record>
<record id="sale_order_template_view_tree" model="ir.ui.view">
<field name="name">sale.order.template.tree</field>
<field name="model">sale.order.template</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Quotation Template">
<field name="name"/>
</tree>
</field>
</record>
<record id="sale_order_template_action" model="ir.actions.act_window">
<field name="name">Quotation Templates</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order.template</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your quotation template
</p><p>
Use templates to create polished, professional quotes in minutes.
Send these quotes by email and let your customers sign online.
Use cross-selling and discounts to push and boost your sales.
</p>
</field>
</record>
<menuitem id="sale_order_template_menu" action="sale_order_template_action" parent="sale.menu_sales_config" sequence="1" name="Quotation Templates" groups="sale_management.group_sale_order_template"/>
</odoo>
@@ -6,14 +6,11 @@
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//header/button[@name='action_confirm'][1]" position="after">
<button name="open_quotation" type="object"
class="o_open_quotation" string="Preview"
attrs="{'invisible': [('template_id', '=', False)]}"/>
<button name="preview_sale_order" type="object" string="Preview"/>
</xpath>
<xpath expr="//page/field[@name='order_line']/.." position="after">
<page string="Suggested Products">
<field name="quote_viewed" invisible="1"/>
<field name="options">
<page string="Optional Products">
<field name="sale_order_option_ids">
<tree string="Sales Quotation Template Lines" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="product_id"/>
@@ -23,22 +20,19 @@
<field name="price_unit"/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
<button name="button_add_to_order" class="oe_link" icon="fa-shopping-cart" string="Add to order lines" type="object"/>
<field name="website_description" invisible="1"/>
</tree>
</field>
</page>
</xpath>
<xpath expr="//field[@name='partner_shipping_id']" position="after">
<label for="template_id"/>
<div>
<field name="template_id" context="{'company_id': company_id}"
options="{'no_create': True, 'no_open': True}"/>
</div>
<field name="sale_order_template_id" context="{'company_id': company_id}"
options="{'no_create': True, 'no_open': True}"
groups="sale_management.group_sale_order_template"
/>
</xpath>
<xpath expr="//field[@name='client_order_ref']" position="after">
<field name="require_signature" attrs="{'invisible': [('template_id', '=', False)]}"/>
<field name="require_payment" attrs="{'invisible': [('template_id', '=', False)]}"/>
<field name="website_description" invisible="1"/>
<field name="require_signature" attrs="{'invisible': [('sale_order_template_id', '=', False)]}"/>
<field name="require_payment" attrs="{'invisible': [('sale_order_template_id', '=', False)]}"/>
</xpath>
</field>
</record>
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="web.assets_frontend" name="Website Quote frontend assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/sale_management/static/src/js/sale_management.js"></script>
</xpath>
</template>
<template id="sale_order_portal_content_inherit_sale_management" name="Order Options" inherit_id="sale.sale_order_portal_content">
<xpath expr="//section[@id='details']//t[@t-set='display_discount']" position="after">
<t t-set="display_remove" t-value="any([line.sale_order_option_ids for line in sale_order.order_line])"/>
</xpath>
<xpath expr="//section[@id='details']//table[@id='sales_order_table']/thead/tr" position="inside">
<th t-if="display_remove">
<t t-set="colspan" t-value="colspan+1"/>
</th>
</xpath>
<xpath expr="//section[@id='details']//t[@t-if='not line.display_type']" position="inside">
<td class="text-center" t-if="display_remove">
<a t-att-href="sale_order.get_portal_url('/update_line/%s' % line.id) + '&amp;unlink=True'" class="mb8 js_update_line_json d-print-none" t-if="line.sale_order_option_ids" aria-label="Remove" title="Remove">
<span class="fa fa-trash-o"></span>
</a>
</td>
</xpath>
<xpath expr="//section[@id='signature']" position="after">
<t t-if="any([(not option.line_id) for option in sale_order.sale_order_option_ids])">
<section>
<h1 class="page-header">Optional Products</h1>
</section>
<section>
<div style="overflow-x:auto;">
<table class="table table-hover">
<thead>
<tr>
<th>Product</th>
<th></th>
<th class="text-right">Price</th>
<th t-if="sale_order.state in ['draft', 'sent']"></th>
</tr>
</thead>
<tbody>
<tr t-foreach="sale_order.sale_order_option_ids" t-as="option">
<t t-if="not option.line_id">
<td>
<div t-field="option.name"/>
</td>
<td>
<strong t-if="option.discount" class="text-info">
<t t-esc="((option.discount % 1) and '%s' or '%d') % option.discount"/>% discount
</strong>
</td>
<td>
<strong class="text-right">
<div t-field="option.price_unit"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
t-att-style="option.discount and 'text-decoration: line-through' or None"
t-att-class="option.discount and 'text-danger' or None"/>
<div t-if="option.discount">
<t t-esc="(1-option.discount / 100.0) * option.price_unit" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
</div>
</strong>
</td>
<td class="text-center" t-if="sale_order.state in ['draft', 'sent']">
<a t-att-href="sale_order.get_portal_url('/add_option/%s' % option.id)" class="mb8 d-print-none" aria-label="Add to cart" title="Add to cart">
<span class="fa fa-shopping-cart"/>
</a>
</td>
</t>
</tr>
</tbody>
</table>
</div>
</section>
</t>
</xpath>
<xpath expr="//section[@id='details']//div[@id='quote_qty']" position="replace">
<t t-if="sale_order.state in ['draft', 'sent'] and line.sale_order_option_ids">
<div class="input-group oe_website_spinner">
<div class="input-group-prepend d-print-none">
<span class="input-group-text">
<a t-att-href="sale_order.get_portal_url('/update_line/%s' % line.id) + '&amp;remove=True'" class="mb8 js_update_line_json" aria-label="Remove one" title="Remove one">
<span class="fa fa-minus"/>
</a>
</span>
</div>
<!-- TODO add uom in this case too -->
<input type="text" class="js_quantity form-control" t-att-data-id="line.id" t-att-value="line.product_uom_qty"/>
<div class="input-group-append d-print-none">
<span class="input-group-text">
<a t-att-href="sale_order.get_portal_url('/update_line/%s' % line.id)" class="mb8 js_update_line_json" aria-label="Add one" title="Add one">
<span class="fa fa-plus"/>
</a>
</span>
</div>
</div>
</t>
<t t-else="">
<span t-field="line.product_uom_qty"/>
<span t-field="line.product_uom" groups="uom.group_uom"/>
</t>
</xpath>
</template>
</odoo>
-5
View File
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
-27
View File
@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': 'Payment - Sale',
'category': 'Sales',
'summary': 'Link Sales and Payment',
'version': '1.0',
'description': """Link Sales and Payment
Provide tools for sale-related payment
* specific management of transactions like sale order confirmation
* tools method to handle transactions when allowing to pay / sell
* JS code to handle a payment form (eCommerce
* add payment-related fields on SO
""",
'depends': ['payment', 'sale'],
'data': [
'views/sale_portal_templates.xml',
'views/settings.xml',
'views/payment_views.xml',
'views/res_config_settings_views.xml',
'views/sale_order_views.xml',
],
'installable': True,
'auto_install': False,
}
@@ -1,89 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http, _
from odoo.addons.portal.controllers.portal import _build_url_w_params
from odoo.http import request, route
class PaymentPortal(http.Controller):
@route('/pay/sale/<int:order_id>/form_tx', type='json', auth="public", website=True)
def sale_pay_form(self, acquirer_id, order_id, save_token=False, access_token=None, **kwargs):
""" Json method that creates a payment.transaction, used to create a
transaction when the user clicks on 'pay now' button on the payment
form.
:return html: form containing all values related to the acquirer to
redirect customers to the acquirer website """
success_url = '/my/orders/%s?access_token=%s' % (order_id, access_token) if access_token else '/my/orders/%s' % order_id
success_url = kwargs.get('success_url', success_url)
order_sudo = request.env['sale.order'].sudo().browse(order_id)
if not order_sudo:
return False
try:
acquirer_id = int(acquirer_id)
except:
return False
if request.env.user == request.env.ref('base.public_user'):
save_token = False
# Create transaction
vals = {
'acquirer_id': acquirer_id,
}
if save_token:
vals['type'] = 'form_save'
transaction = order_sudo._create_payment_transaction(vals)
return transaction.render_sale_button(
order_sudo,
success_url,
submit_txt=_('Pay'),
render_values={
'type': 'form_save' if save_token else 'form',
'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'),
}
)
@http.route('/pay/sale/<int:order_id>/s2s_token_tx', type='http', auth='public', website=True)
def sale_pay_token(self, order_id, pm_id=None, **kwargs):
""" Use a token to perform a s2s transaction """
error_url = kwargs.get('error_url', '/my')
success_url = kwargs.get('success_url', '/my')
access_token = kwargs.get('access_token')
params = {}
if access_token:
params['access_token'] = access_token
order_sudo = request.env['sale.order'].sudo().browse(order_id).exists()
if not order_sudo:
params['error'] = 'pay_sale_invalid_doc'
return request.redirect(_build_url_w_params(error_url, params))
try:
token = request.env['payment.token'].sudo().browse(int(pm_id))
except (ValueError, TypeError):
token = False
token_owner = order_sudo.partner_id if request.env.user == request.env.ref('base.public_user') else request.env.user.partner_id
if not token or token.partner_id != token_owner:
params['error'] = 'pay_sale_invalid_token'
return request.redirect(_build_url_w_params(error_url, params))
# Create transaction
vals = {
'payment_token_id': token.id,
'type': 'server2server',
}
order_sudo._create_payment_transaction(vals)
params['success'] = 'pay_sale'
return request.redirect(_build_url_w_params(success_url, params))
-25
View File
@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale.controllers.portal import CustomerPortal
from odoo.http import request
class CustomerPortal(CustomerPortal):
def _order_get_page_view_values(self, order, access_token, **kwargs):
values = super(CustomerPortal, self)._order_get_page_view_values(order, access_token, **kwargs)
if values['portal_confirmation'] == 'pay':
payment_inputs = request.env['payment.acquirer']._get_available_payment_input(order.partner_id, order.company_id)
# if not connected (using public user), the method _get_available_payment_input will return public user tokens
is_public_user = request.env.ref('base.public_user') == request.env.user
if is_public_user:
# we should not display payment tokens owned by the public user
payment_inputs.pop('pms', None)
token_count = request.env['payment.token'].sudo().search_count([('acquirer_id.company_id', '=', order.company_id.id),
('partner_id', '=', order.partner_id.id),
])
values['existing_token'] = token_count > 0
values.update(payment_inputs)
values['partner_id'] = order.partner_id if is_public_user else request.env.user.partner_id
return values
@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
automatic_invoice = fields.Boolean("Automatic Invoice",
help="The invoice is generated automatically and available in the customer portal "
"when the transaction is confirmed by the payment acquirer.\n"
"The invoice is marked as paid and the payment is registered in the payment journal "
"defined in the configuration of the payment acquirer.\n"
"This mode is advised if you issue the final invoice at the order and not after the delivery.",
config_parameter='sale_payment.automatic_invoice')
template_id = fields.Many2one('mail.template', 'Email Template',
domain="[('model', '=', 'account.invoice')]",
config_parameter='sale_payment.default_email_template',
default=lambda self: self.env.ref('account.email_template_edi_invoice', False))
-113
View File
@@ -1,113 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class SaleOrder(models.Model):
_inherit = 'sale.order'
transaction_ids = fields.Many2many('payment.transaction', 'sale_order_transaction_rel', 'sale_order_id', 'transaction_id',
string='Transactions', copy=False, readonly=True)
authorized_transaction_ids = fields.Many2many('payment.transaction', compute='_compute_authorized_transaction_ids',
string='Authorized Transactions', copy=False, readonly=True)
@api.depends('transaction_ids')
def _compute_authorized_transaction_ids(self):
for trans in self:
trans.authorized_transaction_ids = trans.transaction_ids.filtered(lambda t: t.state == 'authorized')
@api.multi
def get_portal_last_transaction(self):
self.ensure_one()
return self.transaction_ids.get_last_transaction()
@api.multi
def _create_payment_transaction(self, vals):
'''Similar to self.env['payment.transaction'].create(vals) but the values are filled with the
current sales orders fields (e.g. the partner or the currency).
:param vals: The values to create a new payment.transaction.
:return: The newly created payment.transaction record.
'''
# Ensure the currencies are the same.
currency = self[0].pricelist_id.currency_id
if any([so.pricelist_id.currency_id != currency for so in self]):
raise ValidationError(_('A transaction can\'t be linked to sales orders having different currencies.'))
# Ensure the partner are the same.
partner = self[0].partner_id
if any([so.partner_id != partner for so in self]):
raise ValidationError(_('A transaction can\'t be linked to sales orders having different partners.'))
# Try to retrieve the acquirer. However, fallback to the token's acquirer.
acquirer_id = vals.get('acquirer_id')
acquirer = False
payment_token_id = vals.get('payment_token_id')
if payment_token_id:
payment_token = self.env['payment.token'].sudo().browse(payment_token_id)
# Check payment_token/acquirer matching or take the acquirer from token
if acquirer_id:
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
if payment_token and payment_token.acquirer_id != acquirer:
raise ValidationError(_('Invalid token found! Token acquirer %s != %s') % (
payment_token.acquirer_id.name, acquirer.name))
if payment_token and payment_token.partner_id != partner:
raise ValidationError(_('Invalid token found! Token partner %s != %s') % (
payment_token.partner.name, partner.name))
else:
acquirer = payment_token.acquirer_id
# Check an acquirer is there.
if not acquirer_id and not acquirer:
raise ValidationError(_('A payment acquirer is required to create a transaction.'))
if not acquirer:
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
# Check a journal is set on acquirer.
if not acquirer.journal_id:
raise ValidationError(_('A journal must be specified of the acquirer %s.' % acquirer.name))
if not acquirer_id and acquirer:
vals['acquirer_id'] = acquirer.id
vals.update({
'amount': sum(self.mapped('amount_total')),
'currency_id': currency.id,
'partner_id': partner.id,
'sale_order_ids': [(6, 0, self.ids)],
})
transaction = self.env['payment.transaction'].create(vals)
# Process directly if payment_token
if transaction.payment_token_id:
transaction.s2s_do_transaction()
return transaction
def _force_lines_to_invoice_policy_order(self):
for line in self.order_line:
if self.state in ['sale', 'done']:
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
else:
line.qty_to_invoice = 0
@api.multi
def _prepare_invoice(self):
# Override
# Add the transactions in the SO to the invoices.
invoice_vals = super(SaleOrder, self)._prepare_invoice()
invoice_vals['transaction_ids'] = [(6, 0, self.transaction_ids.ids)]
return invoice_vals
@api.multi
def payment_action_capture(self):
self.authorized_transaction_ids.s2s_capture_transaction()
@api.multi
def payment_action_void(self):
self.authorized_transaction_ids.s2s_void_transaction()
@@ -1,30 +0,0 @@
<?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.sale.payment</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="sale.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@id='sales_settings_invoicing_policy']" position="after">
<div class="col-xs-12 col-md-6 o_setting_box"
attrs="{'invisible': ['|', ('default_invoice_policy', '!=', 'order'), ('portal_confirmation_pay', '=', False)]}">
<div class="o_setting_left_pane">
<field name="automatic_invoice"/>
</div>
<div class="o_setting_right_pane">
<label for="automatic_invoice"/>
<div class="text-muted">
Generate the invoice automatically when the electronic payment is confirmed
</div>
<div attrs="{'invisible': [('automatic_invoice','=',False)]}">
<label for="template_id" class="o_light_label"/>
<field name="template_id" class="oe_inline" options="{'no_create': True}"/>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -1,25 +0,0 @@
<?xml version="1.0"?>
<odoo>
<data>
<record id="sale_order_view_form_inherit_sale_payment" model="ir.ui.view">
<field name="name">sale.order.view.form.inherit.sale.payment</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<!--
Allow the user to capture/void the authorized transactions in the sale order.
-->
<xpath expr="//button[1]" position="before">
<field name="authorized_transaction_ids" invisible="1"/>
<button name="payment_action_capture" type="object"
string="Capture Transaction" class="oe_highlight"
attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/>
<button name="payment_action_void" type="object"
string="Void Transaction"
confirm="Are you sure you want to void the authorized transaction? This action can't be undone."
attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -1,111 +0,0 @@
<odoo>
<template id="portal_my_orders_payment" name="Payment on my orders" inherit_id="sale.portal_order_page">
<xpath expr="//t[@name='portal_confirmation_sign']" position="after">
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
<t t-if="order.amount_total and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and payment_tx_id.state != 'done'" name="portal_confirmation_pay">
<a class="btn btn-primary ml8 float-right" href="#portal_pay" role="button">
<i class="fa fa-arrow-circle-right"/> Pay Now
</a>
</t>
<t t-if="invoices and order.state in ('sale', 'done') and portal_confirmation == 'pay'">
<a role="button" class="btn btn-primary ml8 float-right" disabled="disabled">
<i class="fa fa-check-circle"/> Done
</a>
</t>
</xpath>
<xpath expr="//t[@t-call='sale.portal_order_success']" position="after">
<div t-if="order.transaction_ids and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and not success and not error" class="o_sale_payment_tx_status" t-att-data-order-id="order.id">
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
<t t-call="payment.payment_confirmation_status"/>
</div>
</xpath>
<xpath expr="//div[@id='portal_sign']" position="after">
<div class="row" t-if="order.amount_total and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and order.get_portal_last_transaction().state != 'done'" id="portal_pay">
<div class="offset-lg-3 col-lg-6">
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left">
<h4 class="mb24">Pay with</h4>
<t t-call="payment.payment_tokens_list">
<t t-set="mode" t-value="'payment'"/>
<t t-set="partner_id" t-value="order.partner_id.id"/>
<t t-set="access_token" t-value="access_token"/>
<t t-set="success_url" t-value="'/my/orders/%s%s' % (order.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="error_url" t-value="'/my/orders/%s%s' % (order.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="callback_method" t-value="''"/>
<t t-set="submit_txt" t-value="'Pay Now'"/>
<t t-set="icon_class" t-value="'fa-lock'"/>
<t t-set="form_action" t-value="'/pay/sale/' + str(order.id) + '/s2s_token_tx/'"/>
<t t-set="prepare_tx_url" t-value="'/pay/sale/' + str(order.id) + '/form_tx/'"/>
</t>
</div>
</div>
<div class="panel-body" t-if="existing_token"><!-- FIXME -->
<div class="offset-lg-3 col-lg-6">
<i class="fa fa-info"></i> You have credits card registered, you can log-in to be able to use them.
</div>
</div>
</div>
</xpath>
<xpath expr="//t[@t-if='invoices']" position="before">
<t t-set="tx_ids" t-value="order.transaction_ids.filtered(lambda tx: tx.state != 'draft')"/>
<t t-if="tx_ids">
<div>
<strong>Transactions</strong>
</div>
<div>
<t t-foreach="tx_ids" t-as="tx">
<div>
<span t-esc="tx.reference"/>
<span class="text-muted" t-field="tx.create_date"/>
<t t-if="tx.state == 'done'">
<span class="badge badge-success orders_label_text_align"><i class="fa fa-fw fa-check"/> Done</span>
</t>
<t t-if="tx.state != 'done'">
<span class="badge badge-info orders_label_text_align"><i class="fa fa-fw fa-clock-o"/> Waiting</span>
<t t-if="tx.state_message"><br /><span t-esc="tx.state_message"/></t>
</t>
</div>
</t>
</div>
</t>
</xpath>
</template>
<template id="portal_order_error" name="Order error display: payment errors"
inherit_id="sale.portal_order_error">
<xpath expr="//t[@name='generic']" position="after">
<t t-set="tx_id" t-value="order.get_portal_last_transaction()"/>
<t t-if="error == 'pay_sale_invalid_doc'">
There was an error processing your payment: invalid order.
</t>
<t t-if="error == 'pay_sale_invalid_token'">
There was en error processing your payment: invalid credit card ID.
</t>
<t t-if="error == 'pay_sale_tx_fail'">
There was an error processing your payment: transaction failed.<br />
<t t-if="tx_id and tx_id.state_message">
<t t-esc="tx_id.state_message"/>
</t>
</t>
<t t-if="error == 'pay_sale_tx_state'">
There was an error processing your payment: transaction issue.<br />
<t t-if="tx_id and tx_id.state_message">
<t t-esc="tx_id.state_message"/>
</t>
</t>
<t t-if="error == 'pay_sale_tx_token'">
There was an error processing your payment: issue with credit card ID validation.
</t>
</xpath>
</template>
<template id="portal_order_success" name="Order success display: payment success"
inherit_id="sale.portal_order_success">
<xpath expr="//a[hasclass('close')]" position="after">
<t t-if="success == 'pay_sale'">
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
<span t-if='payment_tx_id and payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</t>
</xpath>
</template>
</odoo>
-15
View File
@@ -1,15 +0,0 @@
<?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.sale_payment</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="sale.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//label[@for='portal_confirmation_pay']/.." position="inside">
<div attrs="{'invisible': [('portal_confirmation_pay', '=', False)]}">
<button name='%(payment.action_payment_acquirer)d' icon="fa-arrow-right" type="action" string="Payment Acquirers" class="btn-link"/>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Quotation Builder',
'category': 'Website',
'summary': 'Sales',
'website': 'https://www.odoo.com/page/quote-builder',
'version': '1.0',
'description': "",
'depends': ['website', 'sale_management', 'website_mail'],
'data': [
'data/sale_order_template_data.xml',
'views/sale_portal_templates.xml',
'views/sale_order_template_views.xml',
'views/sale_order_views.xml',
],
'installable': True,
}
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment
from . import portal
from . import portal
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal
class CustomerPortal(CustomerPortal):
@http.route(["/sale_quotation_builder/template/<model('sale.order.template'):template>"], type='http', auth="user", website=True)
def sale_quotation_builder_template_view(self, template, **post):
values = {'template': template}
return request.render('sale_quotation_builder.so_template', values)
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_management.sale_order_template_default" model="sale.order.template">
<field name="website_description" type="xml">
<section data-snippet-id="title">
<h1 class="page-header">About us</h1>
</section>
<section data-snippet-id="text-block">
<div class="row">
<div class="col-lg-12">
<p>
This is a <strong>sample quotation template</strong>. You should
customize it to fit your own needs from the <i>Sale</i>
application, using the menu: Configuration /
Quotation Templates.
</p><p>
Great quotation templates will significantly
<strong>boost your success rate</strong>. The
first section is usually about your company,
your references, your methodology or
guarantees, your team, SLA, terms and conditions, etc.
</p>
</div>
</div>
</section>
<section data-snippet-id="quality">
<div class="row mt32">
<div class="col-lg-4">
<div class="card bg-info text-white">
<h3 class="card-header text-center">Our Quality</h3>
<div class="card-body">
Product quality is the foundation we
stand on; we build it with a relentless
focus on fabric, performance and craftsmanship.
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card bg-info text-white">
<h3 class="card-header text-center">Our Service</h3>
<div class="card-body">
As a leading professional services firm,
we know that success is all about the
commitment we put on strong services.
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card bg-info text-white">
<h3 class="card-header text-center">Price</h3>
<div class="card-body">
We always ensure that our products are
set at a fair price so that you will be
happy to buy them.
</div>
</div>
</div>
</div>
</section>
<section data-snippet-id="text-block">
<h1 class="page-header">Our Offer</h1>
<p>
You can <strong>set a description per product</strong>. Odoo will
automatically create a quotation using the descriptions
of all products in the proposal. The table of content
on the left is generated automatically using the styles you
used in your description (heading 1, heading 2, ...)
</p><p>
If edit a quotation from the 'Preview' of a quotation, you will
update the quotation for this customer only. If you edit the quotation
template (from the Configuration menu), all future quotations will
use this modified template.
</p>
</section>
</field>
</record>
</odoo>
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment
from . import res_config_settings
from . import sale_order
from . import product_template
from . import sale_order
from . import sale_order_template
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
from odoo.tools.translate import html_translate
class ProductTemplate(models.Model):
_inherit = "product.template"
quotation_only_description = fields.Html('Quotation Only Description', sanitize_attributes=False,
translate=html_translate, help="The quotation description (not used on eCommerce)")
quotation_description = fields.Html('Quotation Description', compute='_compute_quotation_description',
help="This field uses the Quotation Only Description if it is defined, otherwise it will try to read the eCommerce Description.")
@api.multi
def _compute_quotation_description(self):
for record in self:
if record.quotation_only_description:
record.quotation_description = record.quotation_only_description
elif hasattr(record, 'website_description') and record.website_description:
record.quotation_description = record.website_description
else:
record.quotation_description = ''
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.tools.translate import html_translate
class SaleOrder(models.Model):
_inherit = 'sale.order'
website_description = fields.Html('Website Description', sanitize_attributes=False, translate=html_translate)
@api.onchange('partner_id')
def onchange_update_description_lang(self):
if not self.sale_order_template_id:
return
else:
template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
self.website_description = template.website_description
def _compute_line_data_for_template_change(self, line):
vals = super(SaleOrder, self)._compute_line_data_for_template_change(line)
vals.update(website_description=line.website_description)
return vals
def _compute_option_data_for_template_change(self, option):
vals = super(SaleOrder, self)._compute_option_data_for_template_change(option)
vals.update(website_description=option.website_description)
return vals
@api.onchange('sale_order_template_id')
def onchange_sale_order_template_id(self):
ret = super(SaleOrder, self).onchange_sale_order_template_id()
if self.sale_order_template_id:
template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
self.website_description = template.website_description
return ret
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
website_description = fields.Html('Website Description', sanitize=False, translate=html_translate)
@api.model
def create(self, values):
values = self._inject_quotation_description(values)
return super(SaleOrderLine, self).create(values)
@api.multi
def write(self, values):
values = self._inject_quotation_description(values)
return super(SaleOrderLine, self).write(values)
def _inject_quotation_description(self, values):
values = dict(values or {})
if not values.get('website_description') and values.get('product_id'):
product = self.env['product.product'].browse(values['product_id'])
values.update(website_description=product.quotation_description)
return values
class SaleOrderOption(models.Model):
_inherit = "sale.order.option"
website_description = fields.Html('Website Description', sanitize_attributes=False, translate=html_translate)
@api.onchange('product_id', 'uom_id')
def _onchange_product_id(self):
ret = super(SaleOrderOption, self)._onchange_product_id()
if self.product_id:
product = self.product_id.with_context(lang=self.order_id.partner_id.lang)
self.website_description = product.quotation_description
return ret
@api.multi
def _get_values_to_add_to_order(self):
values = super(SaleOrderOption, self)._get_values_to_add_to_order()
values.update(website_description=self.website_description)
return values
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.tools.translate import html_translate
class SaleOrderTemplate(models.Model):
_inherit = "sale.order.template"
website_description = fields.Html('Website Description', translate=html_translate, sanitize_attributes=False)
@api.multi
def open_template(self):
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'target': 'self',
'url': '/sale_quotation_builder/template/%d' % self.id
}
class SaleOrderTemplateLine(models.Model):
_inherit = "sale.order.template.line"
website_description = fields.Html('Website Description', related='product_id.product_tmpl_id.quotation_only_description', translate=html_translate)
@api.onchange('product_id')
def _onchange_product_id(self):
ret = super(SaleOrderTemplateLine, self)._onchange_product_id()
if self.product_id:
self.website_description = self.product_id.quotation_description
return ret
@api.model
def create(self, values):
values = self._inject_quotation_description(values)
return super(SaleOrderTemplateLine, self).create(values)
@api.multi
def write(self, values):
values = self._inject_quotation_description(values)
return super(SaleOrderTemplateLine, self).write(values)
def _inject_quotation_description(self, values):
values = dict(values or {})
if not values.get('website_description') and values.get('product_id'):
product = self.env['product.product'].browse(values['product_id'])
values['website_description'] = product.quotation_description
return values
class SaleOrderTemplateOption(models.Model):
_inherit = "sale.order.template.option"
website_description = fields.Html('Website Description', translate=html_translate, sanitize_attributes=False)
@api.onchange('product_id')
def _onchange_product_id(self):
ret = super(SaleOrderTemplateOption, self)._onchange_product_id()
if self.product_id:
self.website_description = self.product_id.quotation_description
return ret
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_template_view_form_inherit_sale_quotation_builder" model="ir.ui.view">
<field name="name">sale.order.template.form.inherit.sale_quotation_builder</field>
<field name="inherit_id" ref="sale_management.sale_order_template_view_form"/>
<field name="model">sale.order.template</field>
<field name="type">form</field>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button string="Edit Template" type="object" name="open_template" class="oe_stat_button" icon="fa-pencil"/>
</xpath>
<xpath expr="//notebook[@name='description']" position="inside">
<page string="Website Description">
<field name="website_description" />
</page>
</xpath>
<xpath expr="//tree/field[@name='price_unit']" position="after">
<field name="website_description" invisible="1"/>
</xpath>
<xpath expr="//notebook[@name='main_book']" position="inside">
<field name="website_description" invisible="1"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_form_quote_design" model="ir.ui.view">
<field name="name">sale.order.form.sale_quotation_builder</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale_management.sale_order_form_quote"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='button_add_to_order']" position="after">
<field name="website_description" invisible="1"/>
</xpath>
<xpath expr="//field[@name='require_payment']" position="after">
<field name="website_description" invisible="1"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="sale_order_portal_content_inherit_sale_quotation_builder" name="Order Design" inherit_id="sale.sale_order_portal_content">
<xpath expr="//div[@id='informations']" position="after">
<div t-field="sale_order.website_description" class="oe_no_empty"/>
<t t-set="product_tmpl_ids" t-value="[]"/>
<t t-foreach="sale_order.order_line" t-as="line">
<t t-if="line.product_id.product_tmpl_id.id not in product_tmpl_ids">
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [line.product_id.product_tmpl_id.id]"/>
<a t-att-id="line.id"/>
<div t-att-class="'oe_no_empty' if line.website_description else 'oe_no_empty d-print-none'" t-field="line.website_description"/>
</t>
</t>
</xpath>
</template>
<!-- Template to edit the quotation template with the website editor -->
<template id="so_template" name="SO Template">
<t t-call="website.layout">
<body data-spy="scroll" data-target=".navspy" data-offset="50">
<div class="container o_sale_order">
<div class="row mt16">
<div class="col-lg-3 d-print-none">
<div class="bs-sidebar">
<div class="navspy" role="complementary" t-ignore="True">
<ul class="nav bs-sidenav" data-id="quote_sidebar"/>
</div>
</div>
</div>
<div class="col-lg-9">
<div class="row mb8">
<div t-ignore="true" class="css_editable_mode_hidden offset-lg-11">
<a role="button" class="btn btn-info d-print-none" t-att-href="'/web#return_label=Website&amp;model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % (template._name, template.id, request.env.ref('sale_management.sale_order_template_action').id)">Back</a>
</div>
</div>
<div class="alert alert-info alert-dismissable" t-ignore="True" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
<p>
<strong>Template Header:</strong> this content
will appear on all quotations using this
template.
</p>
<p class="text-muted">
Titles with style <i>Heading 1</i> and
<i>Heading 2</i> will be used to generate the
table of content automatically.
</p>
</div>
<div id="template_introduction" t-field="template.website_description" class="oe_no_empty"/>
<t t-set="product_tmpl_ids" t-value="[]"/>
<t t-foreach="template.sale_order_template_line_ids" t-as="line">
<t t-if="line.product_id.product_tmpl_id.id not in product_tmpl_ids">
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [line.product_id.product_tmpl_id.id]"/>
<div class="alert alert-info alert-dismissable mt16" t-ignore="True" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
Product: <strong t-esc="line.product_id.name"/>:
this content will appear on the quotation only if this
product is put on the quote.
</div>
<div t-field="line.website_description" class="oe_no_empty"/>
</t>
</t>
<t t-set="product_tmpl_ids" t-value="[]"/>
<t t-foreach="template.sale_order_template_option_ids" t-as="option_line">
<t t-if="option_line.product_id.product_tmpl_id.id not in product_tmpl_ids">
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [option_line.product_id.product_tmpl_id.id]"/>
<div class="alert alert-info alert-dismissable mt16" t-ignore="True" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
Optional Product: <strong t-esc="option_line.product_id.name"/>:
this content will appear on the quotation only if this
product is used in the quote.
</div>
<div t-field="option_line.website_description" class="oe_no_empty"/>
</t>
</t>
<section id="terms" class="container" t-if="template.note">
<h1 t-ignore="True">Terms &amp; Conditions</h1>
<p t-field="template.note"/>
</section>
</div>
</div>
</div>
</body>
</t>
</template>
<template id="sale.brand_promotion" inherit_id="website.brand_promotion">
<xpath expr="//div[hasclass('o_brand_promotion')]" position="replace">
<div class="o_brand_promotion">
Powered by <a target="_blank" class="badge badge-danger" href="http://www.odoo.com/page/website-builder">Odoo</a>,
an awesome <a target="_blank" href="http://www.odoo.com/page/crm">Open Source CRM</a>.
</div>
</xpath>
</template>
</odoo>
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="portal_order_page_shipping" name="Orders Shipping Followup" inherit_id="sale.portal_order_page">
<template id="sale_order_portal_content_inherit_sale_stock" name="Orders Shipping Followup" inherit_id="sale.sale_order_portal_content">
<xpath expr="//div[@id='shipping_address']" position="inside">
<t t-if="order.picking_ids">
<t t-if="sale_order.picking_ids">
<div>
<strong>Delivery Orders</strong>
</div>
<div>
<t t-foreach="order.picking_ids.filtered(lambda picking: picking.picking_type_id.code != 'internal')" t-as="i">
<t t-set="report_url" t-value="'/my/picking/pdf/%s?%s' % (i.id, keep_query())"/>
<t t-foreach="sale_order.picking_ids.filtered(lambda picking: picking.picking_type_id.code != 'internal')" t-as="i">
<t t-set="delivery_report_url" t-value="'/my/picking/pdf/%s?%s' % (i.id, keep_query())"/>
<div id='picking_info'>
<a t-att-href="report_url"><span class="fa fa-download" role="img" aria-label="Download" title="Download"/></a>
<a t-att-href="report_url"><span t-field="i.name"/></a>
<a t-att-href="delivery_report_url"><span class="fa fa-download" role="img" aria-label="Download" title="Download"/></a>
<a t-att-href="delivery_report_url"><span t-field="i.name"/></a>
<span class="text-muted" t-field="i.date" t-options='{"widget": "date"}'/>
<t t-if="i.state == 'done'">
<span class="badge badge-success label-text-align"><i class="fa fa-fw fa-truck"/> Shipped</span>
@@ -15,6 +15,8 @@ class TestReInvoice(TestCommonSaleTimesheetNoChart):
cls.setUpAdditionalAccounts()
cls.setUpAccountJournal()
cls.env.ref('product.list0').currency_id = cls.env.user.company_id.currency_id
# patch expense products to make them services creating task/project
service_values = {
'type': 'service',
@@ -15,6 +15,8 @@ class TestReporting(TestCommonSaleTimesheetNoChart):
cls.setUpAdditionalAccounts()
cls.setUpAccountJournal()
cls.env.ref('product.list0').currency_id = cls.env.user.company_id.currency_id
# expense product
cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'name': "Expense service",
+1 -1
View File
@@ -14,7 +14,7 @@ class ResConfigSettings(models.TransientModel):
return self.env['website'].search([], limit=1)
website_id = fields.Many2one('website', string="website",
default=_default_website, required=True, ondelete='cascade')
default=_default_website, ondelete='cascade')
website_name = fields.Char('Website Name', related='website_id.name')
website_domain = fields.Char('Website Domain', related='website_id.domain')
website_country_group_ids = fields.Many2many(related='website_id.country_group_ids')
-21
View File
@@ -1,21 +0,0 @@
Send Quotes Online
------------------
### Create polished, professional quotes in minutes
Use templates to create polished, professional quotes in minutes. Send these quotes by email and let your customer sign online. Use cross-selling and discounts to push boost your sales.
Electronic Signature
--------------------
### No more fax or scans in emails
Let your customers sign proposals online. Use up-selling techniques to propose them some options.
Spend the extra time focusing on selling, not recording data.
Communicate Efficiently
-----------------------
Have clear pricing strategies.
-27
View File
@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Online Proposals',
'category': 'Sales',
'summary': 'Build quotation templates',
'website': 'https://www.odoo.com/page/quote-builder',
'version': '1.0',
'description': "This module allows to create polished, professional proposals using templates. Templates come with default products, options, terms & conditions and confirmation methods (sign, pay).",
'depends': ['website', 'sale_management', 'mail', 'sale_payment', 'website_mail'],
'data': [
'data/website_quote_data.xml',
'report/sale_order_reports.xml',
'report/sale_order_templates.xml',
'report/website_quote_templates.xml',
'views/sale_order_views.xml',
'views/sale_quote_views.xml',
'views/website_quote_templates.xml',
'views/res_config_settings_views.xml',
'security/ir.model.access.csv',
],
'demo': [
'data/website_quote_demo.xml'
],
'qweb': ['static/src/xml/*.xml'],
'installable': True,
}
-225
View File
@@ -1,225 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug
from odoo import exceptions, fields, http, _
from odoo.http import request
from odoo.addons.portal.controllers.portal import get_records_pager
from odoo.addons.sale.controllers.portal import CustomerPortal
from odoo.addons.portal.controllers.mail import _message_post_helper
from odoo.osv import expression
class CustomerPortal(CustomerPortal):
@http.route()
def portal_order_page(self, order_id=None, access_token=None, **kw):
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
except exceptions.AccessError:
pass
else:
if order_sudo.template_id and order_sudo.template_id.active:
return request.redirect('/quote/%s/%s' % (order_id, access_token or ''))
return super(CustomerPortal, self).portal_order_page(order_id=order_id, access_token=access_token, **kw)
def _portal_quote_user_can_accept(self, order):
result = super(CustomerPortal, self)._portal_quote_user_can_accept(order)
# either use quote template settings or fallback on default behavior
return order.require_signature if order.template_id else result
class sale_quote(http.Controller):
@http.route("/quote/<int:order_id>", type='http', auth="user", website=True)
def view_user(self, *args, **kwargs):
return self.view(*args, **kwargs)
@http.route("/quote/<int:order_id>/<token>", type='http', auth="public", website=True)
def view(self, order_id, pdf=None, token=None, message=False, **post):
# use sudo to allow accessing/viewing orders for public user
# only if he knows the private token
now = fields.Date.today()
if token:
Order = request.env['sale.order'].sudo().search([('id', '=', order_id), ('access_token', '=', token)])
else:
Order = request.env['sale.order'].search([('id', '=', order_id)])
# Log only once a day
if Order and request.session.get('view_quote_%s' % Order.id) != now and request.env.user.share and token:
request.session['view_quote_%s' % Order.id] = now
body = _('Quotation viewed by customer')
_message_post_helper(res_model='sale.order', res_id=Order.id, message=body, token=Order.access_token, message_type='notification', subtype="mail.mt_note", partner_ids=Order.user_id.sudo().partner_id.ids)
if not Order:
return request.render('website.404')
# Token or not, sudo the order, since portal user has not access on
# taxes, required to compute the total_amout of SO.
order_sudo = Order.sudo()
days = 0
if order_sudo.validity_date:
days = (fields.Date.from_string(order_sudo.validity_date) - fields.Date.from_string(fields.Date.today())).days + 1
if pdf:
pdf = request.env.ref('website_quote.report_web_quote').sudo().with_context(set_viewport_size=True).render_qweb_pdf([order_sudo.id])[0]
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders)
transaction = order_sudo.get_portal_last_transaction()
values = {
'quotation': order_sudo,
'message': message and int(message) or False,
'option': any(not x.line_id for x in order_sudo.options),
'order_valid': (not order_sudo.validity_date) or (now <= order_sudo.validity_date),
'days_valid': days,
'action': request.env.ref('sale.action_quotations').id,
'no_breadcrumbs': request.env.user.partner_id.commercial_partner_id not in order_sudo.message_partner_ids,
'tx_id': transaction.id if transaction else False,
'tx_state': transaction.state if transaction else False,
'payment_tx': transaction,
'tx_post_msg': transaction.acquirer_id.post_msg if transaction else False,
'need_payment': order_sudo.invoice_status == 'to invoice' and transaction.state in ['draft', 'cancel'],
'token': token,
'return_url': '/shop/payment/validate',
'bootstrap_formatting': True,
'partner_id': order_sudo.partner_id.id,
}
if order_sudo.has_to_be_paid() or values['need_payment']:
domain = expression.AND([
['&', ('website_published', '=', True), ('company_id', '=', order_sudo.company_id.id)],
['|', ('specific_countries', '=', False), ('country_ids', 'in', [order_sudo.partner_id.country_id.id])]
])
acquirers = request.env['payment.acquirer'].sudo().search(domain)
values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form' and acq.view_template_id]
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s' and acq.registration_view_template_id]
values['pms'] = request.env['payment.token'].search(
[('partner_id', '=', order_sudo.partner_id.id),
('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])])
history = request.session.get('my_quotes_history', [])
values.update(get_records_pager(history, order_sudo))
return request.render('website_quote.so_quotation', values)
@http.route(['/quote/<int:order_id>/<token>/decline'], type='http', auth="public", methods=['POST'], website=True)
def decline(self, order_id, token, **post):
Order = request.env['sale.order'].sudo().browse(order_id)
if token != Order.access_token:
return request.render('website.404')
if Order.state != 'sent':
return werkzeug.utils.redirect("/quote/%s/%s?message=4" % (order_id, token))
Order.action_cancel()
message = post.get('decline_message')
if message:
_message_post_helper(message=message, res_id=order_id, res_model='sale.order', **{'token': token} if token else {})
return werkzeug.utils.redirect("/quote/%s/%s?message=2" % (order_id, token))
@http.route(['/quote/update_line'], type='json', auth="public", website=True)
def update(self, line_id, remove=False, unlink=False, order_id=None, token=None, **post):
Order = request.env['sale.order'].sudo().browse(int(order_id))
if token != Order.access_token:
return request.render('website.404')
if Order.state not in ('draft', 'sent'):
return False
OrderLine = request.env['sale.order.line'].sudo().browse(int(line_id))
if unlink:
OrderLine.unlink()
return False
number = -1 if remove else 1
quantity = OrderLine.product_uom_qty + number
OrderLine.write({'product_uom_qty': quantity})
return [str(quantity), str(Order.amount_total)]
@http.route(["/quote/template/<model('sale.quote.template'):quote>"], type='http', auth="user", website=True)
def template_view(self, quote, **post):
values = {'template': quote}
return request.render('website_quote.so_template', values)
@http.route(["/quote/add_line/<int:option_id>/<int:order_id>/<token>"], type='http', auth="public", website=True)
def add(self, option_id, order_id, token, **post):
Order = request.env['sale.order'].sudo().browse(order_id)
if token != Order.access_token:
return request.render('website.404')
if Order.state not in ['draft', 'sent']:
return request.render('website.http_error', {'status_code': 'Forbidden', 'status_message': _('You cannot add options to a confirmed order.')})
Option = request.env['sale.order.option'].sudo().browse(option_id)
vals = {
'price_unit': Option.price_unit,
'website_description': Option.website_description,
'name': Option.name,
'order_id': Order.id,
'product_id': Option.product_id.id,
'product_uom_qty': Option.quantity,
'product_uom': Option.uom_id.id,
'discount': Option.discount,
}
OrderLine = request.env['sale.order.line'].sudo().create(vals)
OrderLine._compute_tax_id()
Option.write({'line_id': OrderLine.id})
return werkzeug.utils.redirect("/quote/%s/%s#pricing" % (Order.id, token))
# note dbo: website_sale code
@http.route(['/quote/<int:order_id>/transaction/'], type='json', auth="public", website=True)
def payment_transaction_token(self, acquirer_id, order_id, save_token=False,access_token=None, **kwargs):
""" Json method that creates a payment.transaction, used to create a
transaction when the user clicks on 'pay now' button. After having
created the transaction, the event continues and the user is redirected
to the acquirer website.
:param int acquirer_id: id of a payment.acquirer record. If not set the
user is redirected to the checkout page
"""
# Ensure a payment acquirer is selected
if not acquirer_id:
return False
try:
acquirer_id = int(acquirer_id)
except:
return False
order = request.env['sale.order'].sudo().browse(order_id)
if not order or not order.order_line:
return False
# Create transaction
vals = {
'acquirer_id': acquirer_id,
'type': order._get_payment_type(),
}
transaction = order._create_payment_transaction(vals)
return transaction.render_sale_button(
order,
'/quote/%s/%s' % (order_id, access_token) if access_token else '/quote/%s' % order_id,
submit_txt=_('Pay & Confirm'),
render_values={
'type': order._get_payment_type(),
'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'),
}
)
@http.route('/quote/<int:order_id>/transaction/token', type='http', auth='public', website=True)
def payment_token(self, order_id, pm_id=None, **kwargs):
order = request.env['sale.order'].sudo().browse(order_id)
if not order or not order.order_line or pm_id is None:
return request.redirect("/quote/%s" % order_id)
# try to convert pm_id into an integer, if it doesn't work redirect the user to the quote
try:
pm_id = int(pm_id)
except ValueError:
return request.redirect('/quote/%s' % order_id)
# Create transaction
vals = {
'payment_token_id': pm_id,
'type': 'server2server',
}
order._create_payment_transaction(vals)
return request.redirect('/quote/%s/%s' % (order_id, order.access_token))
@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Mails & quote templates is done in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="1">
<record id="website_quote_template_default" model="sale.quote.template">
<field name="name">Default Template</field>
<field name="number_of_days">30</field>
<field name="website_description" type="xml">
<section data-snippet-id="title">
<h1 class="page-header">About us</h1>
</section>
<section data-snippet-id="text-block">
<div class="row">
<div class="col-lg-12">
<p>
This is a <strong>sample quotation template</strong>. You should
customize it to fit your own needs from the <i>Sale</i>
application, using the menu: Configuration /
Quotation Templates.
</p><p>
Great quotation templates will significantly
<strong>boost your success rate</strong>. The
first section is usually about your company,
your references, your methodology or
guarantees, your team, SLA, terms and conditions, etc.
</p>
</div>
</div>
</section>
<section data-snippet-id="quality">
<div class="row mt32">
<div class="col-lg-4">
<div class="card bg-info text-white">
<h3 class="card-header text-center">Our Quality</h3>
<div class="card-body">
Product quality is the foundation we
stand on; we build it with a relentless
focus on fabric, performance and craftsmanship.
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card bg-info text-white">
<h3 class="card-header text-center">Our Service</h3>
<div class="card-body">
As a leading professional services firm,
we know that success is all about the
commitment we put on strong services.
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card bg-info text-white">
<h3 class="card-header text-center">Price</h3>
<div class="card-body">
We always ensure that our products are
set at a fair price so that you will be
happy to buy them.
</div>
</div>
</div>
</div>
</section>
<section data-snippet-id="text-block">
<h1 class="page-header">Our Offer</h1>
<p>
You can <strong>set a description per product</strong>. Odoo will
automatically create a quotation using the descriptions
of all products in the proposal. The table of content
on the left is generated automatically using the styles you
used in your description (heading 1, heading 2, ...)
</p><p>
If edit a quotation from the 'Preview' of a quotation, you will
update the quotation for this customer only. If you edit the quotation
template (from the Configuration menu), all future quotations will
use this modified template.
</p>
</section>
</field>
</record>
</data>
</odoo>
-7
View File
@@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from .import sale_order
from .import sale_quote
from .import product_template
from .import res_config_settings
@@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.tools.translate import html_translate
class ProductTemplate(models.Model):
_inherit = "product.template"
website_description = fields.Html('Description for the website', sanitize_attributes=False) # hack, if website_sale is not installed
quote_description = fields.Html('Description for the quote', sanitize_attributes=False, translate=html_translate)
@@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
default_template_id = fields.Many2one('sale.quote.template', default_model='sale.order', string='Default Template')
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<report
id="report_web_quote"
string="Web Quotation"
model="sale.order"
report_type="qweb-pdf"
file="website_quote.report_quotation"
name="website_quote.report_quote"
menu="False"
print_report_name="
(object.state in ('draft', 'sent') and 'Quotation - %s' % object.name or
'Order - %s' % (object.name))"
/>
</odoo>
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_quote">
<t t-call="web.html_container">
<t t-set="data_report_margin_top" t-value="45"/>
<t t-set="data_report_header_spacing" t-value="40"/>
<t t-call="web.external_layout">
<t t-set="o" t-value="docs[0]"/>
<t t-foreach="docs" t-as="quotation">
<div class="page">
<t t-call="website_quote.so_quotation_content"/>
</div>
</t>
</t>
</t>
</template>
</odoo>
@@ -1,9 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sale_quote_template,sale.quote.template,model_sale_quote_template,sales_team.group_sale_salesman,1,0,0,0
access_sale_quote_template_manager,sale.quote.template,model_sale_quote_template,sales_team.group_sale_manager,1,1,1,1
access_sale_quote_line,sale.quote.line,model_sale_quote_line,sales_team.group_sale_salesman,1,0,0,0
access_sale_quote_line_manager,sale.quote.line,model_sale_quote_line,sales_team.group_sale_manager,1,1,1,1
access_sale_quote_option,sale.quote.option,model_sale_quote_option,sales_team.group_sale_salesman,1,0,0,0
access_sale_quote_option_manager,sale.quote.option,model_sale_quote_option,sales_team.group_sale_manager,1,1,1,1
access_sale_order_option,sale.order.option,model_sale_order_option,sales_team.group_sale_salesman,1,1,1,1
access_sale_order_option_all,sale.order.option,model_sale_order_option,,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sale_quote_template sale.quote.template model_sale_quote_template sales_team.group_sale_salesman 1 0 0 0
3 access_sale_quote_template_manager sale.quote.template model_sale_quote_template sales_team.group_sale_manager 1 1 1 1
4 access_sale_quote_line sale.quote.line model_sale_quote_line sales_team.group_sale_salesman 1 0 0 0
5 access_sale_quote_line_manager sale.quote.line model_sale_quote_line sales_team.group_sale_manager 1 1 1 1
6 access_sale_quote_option sale.quote.option model_sale_quote_option sales_team.group_sale_salesman 1 0 0 0
7 access_sale_quote_option_manager sale.quote.option model_sale_quote_option sales_team.group_sale_manager 1 1 1 1
8 access_sale_order_option sale.order.option model_sale_order_option sales_team.group_sale_salesman 1 1 1 1
9 access_sale_order_option_all sale.order.option model_sale_order_option 1 0 0 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

@@ -1 +0,0 @@
<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="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#7CC098"/><stop offset="100%" stop-color="#5F8A71"/></linearGradient></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="M44.923 69H4c-2 0-4-1-4-4V39.096L16.68 18H54v27.131L49.097 51h7.862L44.923 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"/><path fill="#000" d="M14 50h43v3H14v-3zm37.167-1.375H19.833c-2.154 0-3.916-1.969-3.916-4.375V22.375c0-2.406 1.762-4.375 3.916-4.375h31.334c2.154 0 3.916 1.969 3.916 4.375V44.25c0 2.406-1.762 4.375-3.916 4.375zM18.875 21.187v24.25h33.25v-24.25h-33.25zm6.54 9.963c.829 2.053 6.405-.807 7.982 3.096.755 1.869.133 3.977-1.791 5.142l.625 1.547a.414.414 0 0 1-.218.543l-1.253.506a.414.414 0 0 1-.534-.24l-.635-1.572c-1.326.281-2.668.132-3.725-.314a.435.435 0 0 1-.25-.5l.417-1.71a.395.395 0 0 1 .509-.287c1.044.337 2.261.447 3.224.058 1.122-.453 1.352-1.355 1.09-2.002-.771-1.91-6.39.843-8.018-3.185-.695-1.719-.045-3.598 1.616-4.773l-.656-1.625a.414.414 0 0 1 .218-.543l1.253-.506a.414.414 0 0 1 .534.24l.622 1.54c1.056-.286 2.314-.316 3.357.077.18.067.293.253.274.446l-.164 1.718a.397.397 0 0 1-.497.353c-.912-.241-1.931-.294-2.768.044-1.044.422-1.52 1.186-1.213 1.947zM37 34h11v2H37v-2zm7 4h4v2h-4v-2zm-7-11h11v2H37v-2zm4 3h7v2h-7v-2z" opacity=".3"/><path fill="#FFF" d="M15.006 48H56c.667 0 1 .333 1 1v2H14v-2c0-.667.335-1 1.006-1zm36.16-1.375H19.834c-2.154 0-3.916-1.969-3.916-4.375V20.375c0-2.406 1.762-4.375 3.916-4.375h31.334c2.154 0 3.916 1.969 3.916 4.375V42.25c0 2.406-1.762 4.375-3.916 4.375zM18.876 19.187v24.25h33.25v-24.25h-33.25zm6.54 9.468c.829 2.053 6.405-.806 7.982 3.096.755 1.869.133 3.978-1.791 5.142l.625 1.547a.414.414 0 0 1-.218.543l-1.253.506a.414.414 0 0 1-.534-.24l-.635-1.571c-1.326.28-2.668.132-3.725-.315a.435.435 0 0 1-.25-.5l.417-1.71a.395.395 0 0 1 .509-.287c1.044.337 2.261.447 3.224.058 1.122-.453 1.352-1.355 1.09-2.001-.771-1.911-6.39.842-8.018-3.186-.695-1.718-.045-3.598 1.616-4.773l-.656-1.624a.414.414 0 0 1 .218-.544l1.253-.506a.414.414 0 0 1 .534.24l.622 1.54c1.056-.286 2.314-.316 3.357.077.18.068.293.253.274.446l-.164 1.718a.397.397 0 0 1-.497.353c-.912-.24-1.931-.294-2.768.044-1.044.422-1.52 1.186-1.213 1.947zM37 32h11v2H37v-2zm7 4h4v2h-4v-2zm-7-11h11v2H37v-2zm4 3h7v2h-7v-2z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

@@ -1,55 +0,0 @@
.o_website_quote .bs-sidebar {
position: relative;
width: 100%;
z-index : 1;
background-color: #f7f5fa;
border-radius: 5px;
}
.o_website_quote .bs-sidenav {
padding-top: 10px;
padding-bottom: 10px;
}
.o_website_quote .bs-sidebar .nav > li > a {
display: block;
color: #716b7a;
padding: 5px 20px;
}
.o_website_quote .bs-sidebar .nav > .active > a,
.o_website_quote .bs-sidebar .nav > .active:hover > a,
.o_website_quote .bs-sidebar .nav > .active:focus > a {
font-weight: bold;
color: #563d7c;
background-color: transparent;
border-right: 1px solid #563d7c;
}
.o_website_quote .bs-sidebar .nav .nav {
display: none;
margin-bottom: 8px;
}
.o_website_quote .bs-sidebar .nav .nav > li > a {
padding-top: 3px;
padding-bottom: 3px;
padding-left: 30px;
font-size: 90%;
}
/* Table Styling */
.wq-table {
table-layout: auto !important;
}
@media (min-width: 992px) {
.o_website_quote .bs-sidebar .nav > .active > ul {
display: block;
}
}
@media print {
body {
padding : 0 !important;
}
}
@@ -1,139 +0,0 @@
odoo.define('website_quote.website_quote', function (require) {
'use strict';
require('web.dom_ready');
var ajax = require('web.ajax');
var config = require('web.config');
var Widget = require('web.Widget');
if(!$('.o_website_quote').length) {
return $.Deferred().reject("DOM doesn't contain '.o_website_quote'");
}
// Add to SO button
var UpdateLineButton = Widget.extend({
events: {
'click' : 'onClick',
},
onClick: function(ev){
ev.preventDefault();
var self = this;
var href = this.$el.attr("href");
var order_id = href.match(/order_id=([0-9]+)/);
var line_id = href.match(/update_line\/([0-9]+)/);
var token = href.match(/token=(.*)/);
ajax.jsonRpc("/quote/update_line", 'call', {
'line_id': line_id[1],
'order_id': parseInt(order_id[1]),
'token': token[1],
'remove': self.$el.is('[href*="remove"]'),
'unlink': self.$el.is('[href*="unlink"]')
}).then(function (data) {
if(!data){
window.location.reload();
}
self.$el.parents('.input-group:first').find('.js_quantity').val(data[0]);
$('[data-id="total_amount"]>span').html(data[1]);
});
return false;
},
});
var update_button_list = [];
$('a.js_update_line_json').each(function( index ) {
var button = new UpdateLineButton();
button.setElement($(this)).start();
update_button_list.push(button);
});
// Nav Menu ScrollSpy
var NavigationSpyMenu = Widget.extend({
start: function(watched_selector){
this.authorized_text_tag = ['em', 'b', 'i', 'u'];
this.spy_watched = $(watched_selector);
this.generateMenu();
},
generateMenu: function(){
var self = this;
// reset ids
$("[id^=quote_header_], [id^=quote_]", this.spy_watched).attr("id", "");
// generate the new spy menu
var last_li = false;
var last_ul = null;
_.each(this.spy_watched.find("h1, h2"), function(el){
var id, text;
switch (el.tagName.toLowerCase()) {
case "h1":
id = self.setElementId('quote_header_', el);
text = self.extractText($(el));
if (!text) {
break;
}
last_li = $("<li>").append($('<a href="#'+id+'"/>').text(text)).appendTo(self.$el);
last_ul = false;
break;
case "h2":
id = self.setElementId('quote_', el);
text = self.extractText($(el));
if (!text) {
break;
}
if (last_li) {
if (!last_ul) {
last_ul = $("<ul class='nav'>").appendTo(last_li);
}
$("<li>").append($('<a href="#'+id+'"/>').text(text)).appendTo(last_ul);
}
break;
}
});
},
setElementId: function(prefix, $el){
var id = _.uniqueId(prefix);
this.spy_watched.find($el).attr('id', id);
return id;
},
extractText: function($node){
var self = this;
var raw_text = [];
_.each($node.contents(), function(el){
var current = $(el);
if($.trim(current.text())){
var tagName = current.prop("tagName");
if(_.isUndefined(tagName) || (!_.isUndefined(tagName) && _.contains(self.authorized_text_tag, tagName.toLowerCase()))){
raw_text.push($.trim(current.text()));
}
}
});
return raw_text.join(' ');
}
});
var nav_menu = new NavigationSpyMenu();
nav_menu.setElement($('[data-id="quote_sidebar"]'));
nav_menu.start($('body[data-target=".navspy"]'));
var $bs_sidebar = $(".o_website_quote .bs-sidebar");
$(window).on('resize', _.throttle(adapt_sidebar_position, 200, {leading: false}));
adapt_sidebar_position();
function adapt_sidebar_position() {
$bs_sidebar.css({
position: "",
width: "",
});
if (config.device.size_class >= config.device.SIZES.LG) {
$bs_sidebar.css({
position: "fixed",
width: $bs_sidebar.outerWidth(),
});
}
}
$bs_sidebar.affix({
offset: {
top: 0,
bottom: $('body').height() - $('#wrapwrap').outerHeight() + $("footer").outerHeight(),
},
});
});
@@ -1,6 +0,0 @@
.o_website_quote{
h1[id],h2[id]{
margin-top: 0px;
padding-top: $o-navbar-height;
}
}
@@ -1,23 +0,0 @@
<?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.quote</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="sale.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='sale_config_website_quote']" position="after">
<div class="content-group" attrs="{'invisible': [('module_website_quote','=',False)]}">
<div class="mt16">
<label for="default_template_id" class="o_light_label"/>
<field name="default_template_id" class="oe_inline"/>
</div>
<div>
<button name="%(website_quote.action_sale_quotation_template)d" icon="fa-arrow-right" type="action" string="Quotation Templates" class="btn-link"/>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -1,167 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sale_quote_template_search" model="ir.ui.view">
<field name="name">sale.quote.template.search</field>
<field name="model">sale.quote.template</field>
<field name="arch" type="xml">
<search string="Search Quotation Template">
<filter string="Archived" name="inactive" domain="[('active','=', False)]"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_sale_quote_template_form">
<field name="name">sale.quote.template.form</field>
<field name="model">sale.quote.template</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Sale Quotation Template">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
<button string="Edit Template" type="object" name="open_template" class="oe_stat_button" icon="fa-pencil"/>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="Quotation Template"/>
</h1>
</div>
<group>
<group>
<label for="number_of_days" string="Quotation expires after"/>
<div id="number_of_days">
<field name="number_of_days" class="oe_inline"/> days
</div>
</group>
</group>
<notebook>
<page string="Lines">
<field name="quote_line" widget="section_and_note_one2many">
<form string="Sales Quotation Template Lines">
<!--
We need the sequence field to be here for new lines to be added at the correct position.
TODO: at some point we want to fix this in the framework so that an invisible field is not required.
-->
<field name="sequence" invisible="1"/>
<field name="display_type" invisible="1"/>
<group attrs="{'invisible': [('display_type', '!=', False)]}">
<group>
<field
name="product_id"
attrs="{
'required': [('display_type', '=', False)]
}"
/>
<label for="product_uom_qty"/>
<div>
<field
name="product_uom_qty" class="oe_inline"/>
</div>
<field name="price_unit" invisible="1"/>
<label for="discount" groups="sale.group_discount_per_so_line"/>
<div groups="sale.group_discount_per_so_line">
<field name="discount" class="oe_inline"/> %%
</div>
</group>
</group>
<notebook colspan="4">
<page string="Description" attrs="{'invisible': [('display_type', '!=', False)]}">
<field name="name" />
</page>
<page string="Section" attrs="{'invisible': [('display_type', '!=', 'line_section')]}">
<field name="name" />
</page>
<page string="Note" attrs="{'invisible': [('display_type', '!=', 'line_note')]}">
<field name="name" />
</page>
<page string="Website Description">
<field name="website_description" />
</page>
</notebook>
</form>
<tree string="Sales Quotation Template Lines" editable="bottom">
<control>
<create string="Add a product"/>
<create string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="display_type" invisible="1"/>
<field name="sequence" widget="handle"/>
<field name="product_id"/>
<field name="name" widget="section_and_note_text"/>
<field name="product_uom_qty"/>
<field
name="product_uom_id"
groups="uom.group_uom"
attrs="{
'required': [('display_type', '=', False)]
}"
/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
<field name="price_unit" invisible="1"/>
<field name="website_description" invisible="1"/>
</tree>
</field>
</page>
<page string="Suggested Products">
<field name="options">
<tree string="Sales Quotation Template Lines" editable="bottom">
<field name="product_id"/>
<field name="name"/>
<field name="quantity"/>
<field name="uom_id" groups="uom.group_uom"/>
<field name="price_unit" invisible="1"/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
<field name="website_description" invisible="1"/>
</tree>
</field>
</page>
<page string="Confirmation">
<group>
<field name="require_signature"/>
<field name="require_payment"/>
<field name="mail_template_id"/>
</group>
</page>
</notebook>
<field name="website_description" invisible="1"/>
<field name="note" nolabel="1"
placeholder="The Administrator can set default Terms &amp; Conditions in Sales Settings. Terms set here will show up instead if you select this quotation template."/>
</sheet>
</form>
</field>
</record>
<record id="view_sale_quote_template_tree" model="ir.ui.view">
<field name="name">sale.quote.template.tree</field>
<field name="model">sale.quote.template</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Sale Quotation Template">
<field name="name"/>
</tree>
</field>
</record>
<record id="action_sale_quotation_template" model="ir.actions.act_window">
<field name="name">Quotation Templates</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.quote.template</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your quotation template
</p><p>
Use templates to create polished, professional quotes in minutes.
Send these quotes by email and let your customers sign online.
Use cross-selling and discounts to push and boost your sales.
</p>
</field>
</record>
<menuitem id="menu_sale_quote_template" action="action_sale_quotation_template" parent="sale.menu_sales_config" sequence="1" name="Quotation Templates" groups="sales_team.group_sale_salesman,sales_team.group_sale_manager"/>
<menuitem id='payment_acquirer_menu_sale' action='payment.action_payment_acquirer' parent='sale.menu_sales_config' sequence='3' name='Payment Acquirers' groups="sales_team.group_sale_salesman"/>
</odoo>
@@ -1,745 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Website Quote frontend assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/website_quote/static/src/js/website_quotation.js"></script>
<link rel='stylesheet' href='/website_quote/static/src/css/website_quotation.css'/>
</xpath>
</template>
<template id="assets_editor" inherit_id="web_editor.assets_editor" name="Website Quote Editor assets">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/website_quote/static/src/scss/website_quotation.scss"/>
</xpath>
</template>
<template id="pricing" name="Price">
<section id="quote">
<h1 class="page-header">Pricing</h1>
<t t-set="display_taxamount" t-value="any([line.tax_id for line in quotation.order_line])"/>
<t t-set="display_discount" t-value="True in [line.discount > 0 for line in quotation.order_line]"/>
<table class="table wq-table">
<thead>
<tr>
<t t-set="colspan" t-value="5"/>
<th>Products</th>
<th>Quantity</th>
<th t-if="display_taxamount">
<span>Taxes</span>
<t t-set="colspan" t-value="colspan+1"/>
</th>
<th t-if="display_discount">
<span>Discount</span>
<t t-set="colspan" t-value="colspan+1"/>
</th>
<th class="text-right">Unit Price</th>
<th class="text-right" >
<span groups="account.group_show_line_subtotals_tax_excluded">Amount</span>
<span groups="account.group_show_line_subtotals_tax_included">Total Price</span>
</th>
<th></th>
</tr>
</thead>
<tbody class="sale_tbody">
<t t-set="current_subtotal" t-value="0"/>
<t t-foreach="quotation.order_line" t-as="line">
<t t-set="current_subtotal" t-value="current_subtotal + line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
<t t-set="current_subtotal" t-value="current_subtotal + line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
<tr t-attf-class="{{ 'o_is_' + line.display_type if line.display_type else '' }}">
<t t-if="not line.display_type">
<td><span t-field="line.name"/></td>
<td>
<div id="quote_qty">
<span t-field="line.product_uom_qty"/>
<span t-field="line.product_uom" groups="uom.group_uom"/>
</div>
</td>
<td t-if="display_taxamount">
<span t-esc="', '.join(map(lambda x: (x.description or x.name), line.tax_id))"/>
</td>
<td t-if="display_discount">
<strong t-if="line.discount" class="text-info">
<t t-esc="((line.discount % 1) and '%s' or '%d') % line.discount"/>% discount
</strong>
</td>
<td class="text-right">
<div
t-field="line.price_unit"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
t-att-style="line.discount and 'text-decoration: line-through' or None"
t-att-class="(line.discount and 'text-danger' or '') + ' text-right'"
/>
<div t-if="line.discount">
<t t-esc="(1-line.discount / 100.0) * line.price_unit" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
</div>
</td>
<td class="text-right">
<span
t-field="line.price_subtotal"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
groups="account.group_show_line_subtotals_tax_excluded"
/>
<span
t-field="line.price_total"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
groups="account.group_show_line_subtotals_tax_included"
/>
</td>
<td class="text-center">
<a t-attf-href="./update_line/#{ line.id }/?order_id=#{ quotation.id }&amp;unlink=True&amp;token=#{ quotation.access_token }" class="mb8 js_update_line_json d-print-none" t-if="line.option_line_id" aria-label="Remove" title="Remove">
<span class="fa fa-trash-o"></span>
</a>
</td>
</t>
<t t-if="line.display_type == 'line_section'">
<td t-att-colspan="colspan">
<span t-field="line.name"/>
</td>
<t t-set="current_section" t-value="line"/>
<t t-set="current_subtotal" t-value="0"/>
</t>
<t t-if="line.display_type == 'line_note'">
<td t-att-colspan="colspan">
<span t-field="line.name"/>
</td>
</t>
</tr>
<t t-if="current_section and (line_last or quotation.order_line[line_index+1].display_type == 'line_section')">
<tr class="is-subtotal text-right">
<td t-att-colspan="colspan">
<strong>Subtotal:</strong> <span
t-esc="current_subtotal"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
/>
</td>
</tr>
</t>
</t>
</tbody>
</table>
<div class="clearfix">
<div id="total" class="row" name="total">
<div t-attf-class="col-md-4 float-right #{'col-4' if not type_html else ''}">
<table class="table table-sm" style="min-width: 250px;max-width: 400px;">
<tr class="border-black" style="border-bottom:1px solid #dddddd;">
<td><strong>Subtotal</strong></td>
<td class="text-right">
<span
data-id="total_amount"
t-field="quotation.amount_untaxed"
t-options='{"widget": "monetary","display_currency": quotation.pricelist_id.currency_id}'
/>
</td>
</tr>
<t t-foreach="quotation.amount_by_group" t-as="amount_by_group">
<tr style="border-bottom:1px solid #dddddd;">
<t t-if="amount_by_group[3] == 1 and quotation.amount_untaxed == amount_by_group[2]">
<td>
<span t-esc="amount_by_group[0]"/>
<span>&amp;nbsp;<span>on</span>&amp;nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/></span>
</td>
<td class="text-right">
<span t-esc="amount_by_group[1]"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
</td>
</t>
<t t-else ="">
<td>
<span t-esc="amount_by_group[0]"/>
</td>
<td class="text-right">
<span t-esc="amount_by_group[1]"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
</td>
</t>
</tr>
</t>
<tr class="border-black">
<td><strong>Total</strong></td>
<td class="text-right">
<span data-id="total_amount" t-field="quotation.amount_total" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
</td>
</tr>
</table>
</div>
</div>
</div>
</section>
<section id="terms" class="container" t-if="quotation.note">
<h2 class="page-header">Terms &amp; Conditions</h2>
<p t-field="quotation.note"/>
</section>
</template>
<template id="signature" name="Signature">
<div t-if="quotation.signature" class="row mt64" name="signature">
<div class="offset-8 col-4">
<strong>Signature</strong>
</div>
<div class="offset-8 col-4">
<img t-att-src="'data:image/png;base64,%s' % to_text(quotation.signature)" style="max-height: 4cm; max-width: 8cm;"/>
</div>
<div class="offset-8 col-4 text-center">
<p t-field="quotation.signed_by"/>
</div>
</div>
</template>
<template id="change_quantity" inherit_id="website_quote.pricing" active="False" customize_show="True" name="Change Quantity">
<xpath expr="//div[@id='quote_qty']" position="replace">
<div class="input-group oe_website_spinner">
<div class="input-group-prepend d-print-none">
<span class="input-group-text">
<a t-attf-href="./update_line/#{ line.id }/?order_id=#{ quotation.id }&amp;remove=True&amp;token=#{ quotation.access_token }" class="mb8 js_update_line_json" aria-label="Remove one" title="Remove one">
<span class="fa fa-minus"/>
</a>
</span>
</div>
<input type="text" class="js_quantity form-control" t-att-data-id="line.id" t-att-value="line.product_uom_qty"/>
<div class="input-group-append d-print-none">
<span class="input-group-text">
<a t-attf-href="./update_line/#{ line.id }/?order_id=#{ quotation.id }&amp;token=#{ quotation.access_token }" class="mb8 js_update_line_json" aria-label="Add one" title="Add one">
<span class="fa fa-plus"/>
</a>
</span>
</div>
</div>
</xpath>
</template>
<!-- duplicata of the template payment.payment_confirmation_status.
The duplication avoid to break an existing installation in stable version-->
<template id="payment_confirmation_status">
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
Your payment has been authorized.
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
</template>
<!-- Complete page of the quotation -->
<template id="so_quotation" name="Product Quotation">
<t t-call="portal.portal_layout">
<t t-set="sales_order" t-value="quotation"/> <!-- rename to ensure breadcrumb compatibility with website_portal_sale's portal_layout inheritance -->
<body data-spy="scroll" data-target=".navspy" data-offset="50">
<div class="container o_website_quote">
<div class="row mt16">
<!-- Sidebar -->
<div class="col-lg-3 d-print-none">
<div class="bs-sidebar">
<div class="text-center" t-if="quotation.state in ('draft', 'sent', 'waiting_date')" style="padding: 10px">
<a role="button" t-if="order_valid and not quotation.signature and quotation.has_to_be_signed()" class="btn btn-primary btn-block" data-toggle="modal" data-target="#modalaccept">
<i class="fa fa-check"/> Accept Order
</a>
<a role="button" t-elif="order_valid and quotation.has_to_be_paid()" class="btn btn-primary btn-block" data-toggle="modal" data-target="#modalaccept">
<i class="fa fa-check"/> <t t-if="not quotation.signature">Accept &amp; </t>Pay
</a>
<a role="button" t-if="not order_valid" href="#discussion" class="btn btn-info btn-block o_normalwhitespace">
<strong>This offer expired!</strong><br/>
Contact us for new quote.
</a>
<div class="mt8" t-if="order_valid">
<a data-toggle="modal" data-target="#modaldecline">
Reject
</a>
</div>
</div>
<div class="text-center" t-if="need_payment or (quotation.state in ('sale') and quotation.has_to_be_paid() and tx_state != 'done')" style="padding: 10px">
<a role="button" class="btn btn-primary btn-block" data-toggle="modal" data-target="#modalaccept">
<i class="fa fa-check"></i> Pay Now
</a>
</div>
<hr class="mt0 mb0"/>
<t t-call="website_quote.navigation_menu"/>
<hr class="mt0 mb0"/>
<div t-if="order_valid and quotation.state in ['draft', 'sent']" class="text-center">
<input type="hidden" t-att-value="quotation.validity_date" id="validity_date"/>
<div class="mt8" t-if="days_valid &gt; 0">
<strong>This offer expires in</strong>
<div class="day_counter mt8 mb16">
<i class="fa fa-clock-o" aria-title="Dates" title="Dates"></i>
<t t-esc="days_valid"/>
<t t-if="days_valid &gt; 1">
days
</t>
<t t-if="days_valid &lt;= 1">
day
</t>
</div>
</div>
<div class="text-center mb16" t-if="quotation.amount_undiscounted &gt; quotation.amount_untaxed">
<p class="text-muted mb8">Your advantage:</p>
<t t-if="quotation.amount_untaxed == quotation.amount_total">
<strong t-field="quotation.amount_total"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
<strong t-field="quotation.amount_undiscounted"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
style="text-decoration: line-through"
class="text-danger"/>
</t>
<t t-if="quotation.amount_untaxed != quotation.amount_total">
<strong t-field="quotation.amount_untaxed"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
<strong t-field="quotation.amount_undiscounted"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
style="text-decoration: line-through"
class="text-danger"/>
<br />
(<span t-field="quotation.amount_total"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/> Incl. tax)
</t>
</div>
</div>
</div>
</div>
<!-- Page content -->
<div id="quote_content" class="col-lg-9">
<div class="alert alert-success alert-dismissable" t-if="message==1" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
Your message has been successfully sent!
</div>
<div class="alert alert-warning alert-dismissable" t-if="message==2" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
This quotation has been rejected. <a href="#discussion">Contact us</a> if you want a new one.
</div>
<div class="alert alert-warning alert-dismissable" t-if="message==3" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
This order has been validated. Thanks for your trust
and do not hesitate to <a href="#discussion">contact us</a> for
any question.
</div>
<t t-if="payment_tx">
<t t-set="payment_tx_id" t-value="payment_tx"/>
<t t-call="website_quote.payment_confirmation_status"/>
</t>
<div class="alert alert-warning alert-dismissable" t-if="message==4 and quotation.state != 'sent'" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
This order
<t t-if="quotation.state=='draft'">has not yet been sent!</t>
<t t-if="quotation.state=='cancel'">has already been cancelled!</t>
<t t-if="quotation.state not in ('cancel','draft')">has already been validated!</t>
You can <a href="#discussion">contact us</a> for
any questions.
</div>
<!-- modal relative to the actions Accept/Reject/Cancel -->
<div role="dialog" class="modal fade" id="modalaccept">
<div class="modal-dialog" t-if="not quotation.signature and quotation.has_to_be_signed()">
<form id="accept" method="POST" t-att-data-order-id="quotation.id" t-att-data-token="quotation.access_token" class="js_accept_json modal-content js_website_submit_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<header class="modal-header">
<h4 class="modal-title">Validate Order</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&amp;times;</button>
</header>
<main class="modal-body" id="sign-dialog">
<p>
<span>I agree that by signing this proposal, I
accept it on the behalf of </span><b t-field="quotation.partner_id.commercial_partner_id"/><span>, for an amount of </span>
<b data-id="total_amount" t-field="quotation.amount_total"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
<span>with payment terms: </span><b t-field="quotation.payment_term_id"/>.
</p>
<t t-call="portal.portal_signature">
<t t-set="object" t-value="quotation"/>
<t t-set="partner_name" t-value="quotation.partner_id.name"/>
<t t-set="callUrl" t-value="'/my/quotes/accept'"/>
<t t-set="accessToken" t-value="quotation.access_token"/>
</t>
</main>
</form>
</div>
<div class="modal-dialog modal-content" t-if="not (quotation.has_to_be_signed() and not quotation.signature) and (quotation.has_to_be_paid() or need_payment)">
<header class="modal-header">
<h4 class="modal-title">Validate Order</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&amp;times;</button>
</header>
<main class="modal-body" id="sign-dialog">
<p>
<span>I agree that by paying this proposal, I
accept it on the behalf of </span><b t-field="quotation.partner_id.commercial_partner_id"/><span>, for an amount of </span>
<b data-id="total_amount" t-field="quotation.amount_total"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
<span>with payment terms: </span><b t-field="quotation.payment_term_id"/>.
</p>
</main>
<footer class="modal-footer">
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left">
<h3 class="mb24">Pay with</h3>
<t t-call="payment.payment_tokens_list">
<t t-set="mode" t-value="'payment'"/>
<t t-set="submit_txt" t-value="'Pay &amp; Confirm'"/>
<t t-set="icon_class" t-value="'fa-lock'"/>
<t t-set="form_action" t-value="'/quote/' + str(quotation.id) + '/transaction/token'"/>
<t t-set="prepare_tx_url" t-value="'/quote/' + str(quotation.id) + '/transaction/'"/>
<t t-set="access_token" t-value="quotation.access_token"/>
</t>
</div>
</footer>
</div>
</div>
<div role="dialog" class="modal fade" id="modaldecline">
<div class="modal-dialog">
<form id="decline" method="POST" t-attf-action="/quote/#{quotation.id}/#{quotation.access_token}/decline" class="modal-content">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<header class="modal-header">
<h4 class="modal-title">Reject This Quote</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&amp;times;</button>
</header>
<main class="modal-body">
<p>
Tell us why you are refusing this quotation, this will help us improve our services.
</p>
<textarea rows="4" name="decline_message" placeholder="Your feedback....." class="form-control"/>
</main>
<footer class="modal-footer">
<button type="submit" t-att-id="quotation.id" class="btn btn-primary">Reject</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
</footer>
</form>
</div>
</div>
<div class="alert alert-warning alert-dismissable d-print-none" t-if="quotation.state == 'cancel'" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="close">&amp;times;</button>
<strong>This quotation has been canceled.</strong> Contact us to get a new quote.
</div>
<t t-call="website_quote.so_quotation_content"/>
<div class="text-center d-print-none mt16 mb16" id="quotation_toolbar" t-if="quotation.state in ('draft', 'sent', 'waiting_date')">
<a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-if="order_valid and not quotation.has_to_be_paid()">
<i class="fa fa-check"></i> Accept
</a>
<a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-if="order_valid and quotation.has_to_be_paid()">
<i class="fa fa-check"></i> Accept &amp; Pay
</a>
<a role="button" class="btn btn-danger" data-toggle="modal" data-target="#modaldecline">
<i class="fa fa-times"></i> Reject
</a>
</div>
<div class="text-center" t-if="need_payment or (quotation.state in ('sale') and quotation.has_to_be_paid() and tx_state != 'done')" style="padding: 10px">
<a role="button" class="btn btn-primary" data-toggle="modal" data-target="#modalaccept">
<i class="fa fa-check"></i> Pay Now
</a>
</div>
</div><!-- //col-lg-9 -->
</div>
</div>
</body>
</t>
</template>
<template id="chatter" name="Allow Comments &amp; History" customize_show="True" inherit_id="website_quote.so_quotation">
<xpath expr="//div[@id='quote_content']" position="inside">
<h1 class="page-header">History</h1>
<!-- Options:Quotation Chatter: user can reply -->
<t t-call="portal.message_thread">
<t t-set="object" t-value="quotation"/>
</t>
</xpath>
<xpath expr="//a[@data-target='#modaldecline']" position="before">
<a type="submit" href="#discussion">
Ask Changes
</a>
</xpath>
<xpath expr="//div[@id='quotation_toolbar']/a[@data-target='#modaldecline']" position="before">
<a role="button" class="btn btn-info" type="submit" href="#discussion">
<i class="fa fa-comment"></i> Feedback
</a>
</xpath>
</template>
<!--
Quotation content : intro, informations, order lines, remarks, descriptions ....
This template should contains all the printable element of the SO. This is the
template rendered in PDF with the report engine.
-->
<template id="so_quotation_content" name="Quotation Content">
<!-- Intro -->
<div id="introduction" class="page-header mt16">
<h1>
<t t-if="quotation.state in ('draft','sent','cancel')">Your Quotation</t>
<t t-if="quotation.state not in ('draft','sent','cancel')">Your Order</t>
<em t-esc="quotation.name"/>
<small t-field="quotation.state"/>
<div t-ignore="true" class="float-right css_editable_mode_hidden d-print-none">
<ul class="list-inline">
<li groups="sales_team.group_sale_salesman" class="list-inline-item">
<a role="button" t-if="quotation.state not in ('manual')" class="btn btn-info" t-att-href="'/web#return_label=Website&amp;model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % (quotation._name, quotation.id, action)">Edit Quote</a>
<a role="button" t-if="quotation.state in ('manual')" class=" btn btn-info" t-att-href="'/web#return_label=Website&amp;model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % (quotation._name, quotation.id, action)">Back to Sales Order</a>
</li>
<li class="list-inline-item">
<a role="button" class="btn btn-info" target="_blank" t-att-href="'/quote/%s/%s' % (quotation.id,quotation.access_token)+'?pdf=True'">Print</a>
</li>
</ul>
</div>
</h1>
</div>
<!-- Informations -->
<div class="row mt32">
<div class="col-md-6 col-12">
<div class="row">
<label class="col-md-4 col-4 text-right">Customer:</label>
<div class="col-md-8 col-8">
<div t-field="quotation.partner_id"/>
</div>
</div>
<div class="row">
<label class="col-md-4 col-4 text-right">Bill To:</label>
<div class="col-md-8 col-8">
<div t-field="quotation.partner_invoice_id" t-options='{
"widget": "contact",
"fields": ["address", "name", "phone", "email"]
}'/>
</div>
</div>
<div t-if="quotation.partner_shipping_id.id != quotation.partner_invoice_id.id" class="row">
<label class="col-md-4 col-4 text-right">Ship To:</label>
<div class="col-md-8 col-8">
<div t-field="quotation.partner_shipping_id" t-options='{
"widget": "contact",
"fields": ["address", "name", "phone"]
}'/>
</div>
</div>
</div>
<div class="col-md-6 col-12">
<div class="row">
<label class="col-md-5 col-4 text-right">Your Contact:</label>
<div class="col-md-7 col-8">
<div t-field="quotation.user_id" t-options='{
"widget": "contact",
"fields": ["name", "phone", "email"]
}'/>
</div>
</div>
<div class="row">
<label class="col-md-5 col-4 text-right">Quote Date:</label>
<div class="col-md-7 col-8">
<span t-field="quotation.date_order"/>
</div>
<div class="clearfix"/>
<div t-if="quotation.validity_date and quotation.state in ['draft', 'sent']">
<label class="col-md-5 col-4 text-right">Expiration Date:</label>
<div class="col-md-7 col-8">
<span t-field="quotation.validity_date"/>
</div>
</div>
<div class="clearfix"/>
<div t-if="quotation.client_order_ref">
<label class="col-md-5 col-4 text-right">Your Reference:</label>
<div class="col-md-7 col-8">
<span t-field="quotation.client_order_ref"/>
</div>
</div>
<div class="clearfix"/>
<div t-if="quotation.payment_term_id">
<label class="col-md-5 col-4 text-right">Payment Terms:</label>
<div class="col-md-7 col-8">
<span t-field="quotation.payment_term_id"/>
</div>
</div>
</div>
</div>
</div>
<!-- Descriptions -->
<div t-field="quotation.website_description" class="oe_no_empty"/>
<t t-set="product_tmpl_ids" t-value="[]"/>
<t t-foreach="quotation.order_line" t-as="line">
<t t-if="line.product_id.product_tmpl_id.id not in product_tmpl_ids">
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [line.product_id.product_tmpl_id.id]"/>
<a t-att-id="line.id"/>
<div t-att-class="'oe_no_empty' if line.website_description else 'oe_no_empty d-print-none'" t-field="line.website_description"/>
</t>
</t>
<div class="oe_structure"/>
<!-- Pricing -->
<t t-call="website_quote.pricing"/>
<!-- Signature -->
<t t-call="website_quote.signature"/>
<!-- Options -->
<t t-call="website_quote.optional_products"/>
</template>
<template id="navigation_menu">
<div class="navspy" t-ignore="true" role="complementary">
<ul class="nav bs-sidenav" data-id="quote_sidebar">
<li class="nav-item"><a href="#introduction" class="nav-link">Introduction</a></li>
</ul>
</div>
</template>
<template id="optional_products">
<t t-if="any([(not option.line_id) for option in quotation.options])">
<section>
<h1 class="page-header">Suggested Products</h1>
</section>
<section>
<table class="table table-hover wq-table">
<thead>
<tr>
<th>Product</th>
<th></th>
<th class="text-right">Price</th>
<th></th>
</tr>
</thead>
<tbody>
<tr t-foreach="quotation.options" t-as="option">
<t t-if="not option.line_id">
<td>
<div t-field="option.name"/>
</td>
<td>
<strong t-if="option.discount" class="text-info">
<t t-esc="((option.discount % 1) and '%s' or '%d') % option.discount"/>% discount
</strong>
</td>
<td>
<strong class="text-right">
<div t-field="option.price_unit"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
t-att-style="option.discount and 'text-decoration: line-through' or None"
t-att-class="option.discount and 'text-danger' or None"/>
<div t-if="option.discount">
<t t-esc="(1-option.discount / 100.0) * option.price_unit" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
</div>
</strong>
</td>
<td class="text-center" t-if="quotation.state in ['draft', 'sent']">
<a t-attf-href="/quote/add_line/#{ option.id }/#{ quotation.id }/#{ quotation.access_token }" class="mb8 d-print-none" aria-label="Add to cart" title="Add to cart">
<span class="fa fa-shopping-cart"/>
</a>
</td>
</t>
</tr>
</tbody>
</table>
</section>
</t>
</template>
<!-- Template to edit the quotation template with the website editor -->
<template id="so_template" name="SO Template">
<t t-call="website.layout">
<body data-spy="scroll" data-target=".navspy" data-offset="50">
<div class="container o_website_quote">
<div class="row mt16">
<div class="col-lg-3 d-print-none">
<div class="bs-sidebar">
<div class="navspy" role="complementary" t-ignore="True">
<ul class="nav bs-sidenav" data-id="quote_sidebar"/>
</div>
</div>
</div>
<div class="col-lg-9">
<div class="row mb8">
<div t-ignore="true" class="css_editable_mode_hidden offset-lg-11">
<a role="button" class="btn btn-info d-print-none" t-att-href="'/web#return_label=Website&amp;model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % (template._name, template.id, request.env.ref('website_quote.action_sale_quotation_template').id)">Back</a>
</div>
</div>
<div class="alert alert-info alert-dismissable" t-ignore="True" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
<p>
<strong>Template Header:</strong> this content
will appear on all quotations using this
template.
</p>
<p class="text-muted">
Titles with style <i>Heading 1</i> and
<i>Heading 2</i> will be used to generate the
table of content automatically.
</p>
</div>
<div id="template_introduction" t-field="template.website_description" class="oe_no_empty"/>
<t t-set="product_tmpl_ids" t-value="[]"/>
<t t-foreach="template.quote_line" t-as="line">
<t t-if="line.product_id.product_tmpl_id.id not in product_tmpl_ids">
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [line.product_id.product_tmpl_id.id]"/>
<div class="alert alert-info alert-dismissable mt16" t-ignore="True" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
Product: <strong t-esc="line.product_id.name"/>:
this content will appear on the quotation only if this
product is put on the quote.
</div>
<div t-field="line.website_description" class="oe_no_empty"/>
</t>
</t>
<t t-set="product_tmpl_ids" t-value="[]"/>
<t t-foreach="template.options" t-as="option_line">
<t t-if="option_line.product_id.product_tmpl_id.id not in product_tmpl_ids">
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [option_line.product_id.product_tmpl_id.id]"/>
<div class="alert alert-info alert-dismissable mt16" t-ignore="True" role="status">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
Optional Product: <strong t-esc="option_line.product_id.name"/>:
this content will appear on the quotation only if this
product is used in the quote.
</div>
<div t-field="option_line.website_description" class="oe_no_empty"/>
</t>
</t>
<section id="terms" class="container" t-if="template.note">
<h1 t-ignore="True">Terms &amp; Conditions</h1>
<p t-field="template.note"/>
</section>
</div>
</div>
</div>
</body>
</t>
</template>
<template id="website_quote.brand_promotion" inherit_id="website.brand_promotion">
<xpath expr="//div[hasclass('o_brand_promotion')]" position="replace">
<div class="o_brand_promotion">
Powered by <a target="_blank" class="badge badge-danger" href="http://www.odoo.com/page/website-builder?utm_source=db&amp;utm_medium=website">Odoo</a>,
an awesome <a target="_blank" href="http://www.odoo.com/page/crm?utm_source=db&amp;utm_medium=website">Open Source CRM</a>.
</div>
</xpath>
</template>
<template id="portal_my_quotations_inherit_quote" inherit_id="sale.portal_my_quotations">
<xpath expr="//t[@t-foreach='quotations']/tr/td/a" position="replace">
<t t-if="quotation.template_id and quotation.template_id.active">
<a t-attf-href="/quote/#{quotation.id}?#{keep_query()}"><t t-esc="quotation.name"/></a>
</t>
<t t-else="">
<a t-attf-href="/my/orders/#{quotation.id}?#{keep_query()}"><t t-esc="quotation.name"/></a>
</t>
</xpath>
</template>
</odoo>
+1 -1
View File
@@ -6,7 +6,7 @@
'website': 'https://www.odoo.com/page/e-commerce',
'version': '1.0',
'description': "",
'depends': ['website', 'sale_payment', 'website_payment', 'website_mail', 'website_form', 'website_rating', 'digest', 'account'],
'depends': ['website', 'sale', 'website_payment', 'website_mail', 'website_form', 'website_rating', 'digest'],
'data': [
'security/ir.model.access.csv',
'security/website_sale.xml',
@@ -6,6 +6,25 @@
<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='sales_settings_invoicing_policy']" position="after">
<!--
Show the automatic invoices setting in sales settings.
-->
<div class="col-12 col-lg-6 o_setting_box"
attrs="{'invisible': ['|', ('default_invoice_policy', '!=', 'order'), ('portal_confirmation_pay', '=', False)]}">
<div class="o_setting_left_pane">
<field name="automatic_invoice" nolabel="1"/>
</div>
<div class="o_setting_right_pane">
<label for="automatic_invoice"/>
<div class="text-muted">
Generate the invoice automatically when the payment is received
</div>
</div>
</div>
</xpath>
<div id="webmaster_settings" position="after">
<h2>Product Catalog</h2>
<div class="row mt16 o_settings_container" id="sale_product_catalog_settings">
+2 -2
View File
@@ -1626,8 +1626,8 @@
</xpath>
</template>
<template id="portal_order_page_products_links" name="Orders Followup Products Links" inherit_id="sale.portal_order_page">
<xpath expr="//div[@id='product_name']/*" position="replace">
<template id="sale_order_portal_content_inherit_website_sale" name="Orders Followup Products Links" inherit_id="sale.sale_order_portal_content">
<xpath expr="//section[@id='details']//td[@id='product_name']/*" position="replace">
<a t-if="line.product_id.website_published" t-att-href="line.product_id.website_url">
<span t-field="line.name" />
</a>
@@ -69,7 +69,7 @@
</xpath>
</template>
<template id="portal_order_page_shipping_tracking" name="Shipping tracking on orders followup" inherit_id="sale_stock.portal_order_page_shipping">
<template id="sale_order_portal_content_inherit_sale_stock_inherit_website_sale_delivery" name="Shipping tracking on orders followup" inherit_id="sale_stock.sale_order_portal_content_inherit_sale_stock">
<xpath expr="//div[@id='picking_info']" position="after">
<div t-if="i.carrier_tracking_ref">
Tracking: <span t-field="i.carrier_id.name"/>
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="portal_order_page_downloads" name="Orders Downloads Followup" inherit_id="sale.portal_order_page">
<xpath expr="//div[@id='product_name']" position="inside">
<t t-set="attachments" t-value="digital_attachments.get(line.product_id.id)"/>
<template id="sale_order_portal_content_inherit_website_sale_digital" name="Orders Downloads Followup" inherit_id="sale.sale_order_portal_content">
<xpath expr="//section[@id='details']//td[@id='product_name']" position="inside">
<t t-if="digital_attachments" t-set="attachments" t-value="digital_attachments.get(line.product_id.id)"/>
<t t-if="attachments">
<span class="dropdown">
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown">