summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/account/models/account_invoice.py22
-rw-r--r--addons/account/views/account_invoice_view.xml1
-rw-r--r--addons/account_cancel/views/account_views.xml4
-rw-r--r--addons/portal/static/src/js/portal_sidebar.js16
-rw-r--r--addons/portal/static/src/js/portal_signature.js6
-rw-r--r--addons/portal/static/src/scss/portal.scss3
-rw-r--r--addons/portal/static/src/xml/portal_chatter.xml2
-rw-r--r--addons/portal/views/portal_templates.xml6
-rw-r--r--addons/sale/__manifest__.py3
-rw-r--r--addons/sale/controllers/portal.py115
-rw-r--r--addons/sale/models/__init__.py1
-rw-r--r--addons/sale/models/payment.py (renamed from addons/sale_payment/models/payment.py)4
-rw-r--r--addons/sale/models/res_company.py5
-rw-r--r--addons/sale/models/res_config_settings.py33
-rw-r--r--addons/sale/models/sale.py145
-rw-r--r--addons/sale/report/sale_report.xml13
-rw-r--r--addons/sale/report/sale_report_templates.xml15
-rw-r--r--addons/sale/static/src/js/sale_portal_sidebar.js146
-rw-r--r--addons/sale/static/src/js/tour.js (renamed from addons/sale_management/static/src/js/tour.js)0
-rw-r--r--addons/sale/static/src/scss/sale_portal.scss9
-rw-r--r--addons/sale/views/payment_views.xml (renamed from addons/sale_payment/views/payment_views.xml)2
-rw-r--r--addons/sale/views/res_config_settings_views.xml48
-rw-r--r--addons/sale/views/sale_portal_templates.xml920
-rw-r--r--addons/sale/views/sale_views.xml12
-rw-r--r--addons/sale_expense/tests/test_reinvoice.py2
-rw-r--r--addons/sale_management/__init__.py1
-rw-r--r--addons/sale_management/__manifest__.py14
-rw-r--r--addons/sale_management/controllers/__init__.py (renamed from addons/website_quote/controllers/__init__.py)2
-rw-r--r--addons/sale_management/controllers/portal.py165
-rw-r--r--addons/sale_management/data/sale_order_template_data.xml11
-rw-r--r--addons/sale_management/data/sale_order_template_demo.xml (renamed from addons/website_quote/data/website_quote_demo.xml)10
-rw-r--r--addons/sale_management/models/__init__.py4
-rw-r--r--addons/sale_management/models/res_config_settings.py17
-rw-r--r--addons/sale_management/models/res_users.py13
-rw-r--r--addons/sale_management/models/sale_order.py (renamed from addons/website_quote/models/sale_order.py)237
-rw-r--r--addons/sale_management/models/sale_order_template.py (renamed from addons/website_quote/models/sale_quote.py)72
-rw-r--r--addons/sale_management/report/sale_report_templates.xml (renamed from addons/website_quote/report/sale_order_templates.xml)4
-rw-r--r--addons/sale_management/security/ir.model.access.csv9
-rw-r--r--addons/sale_management/security/sale_management_security.xml8
-rw-r--r--addons/sale_management/static/src/img/online_training.png (renamed from addons/website_quote/static/src/img/online_training.png)bin11682 -> 11682 bytes
-rw-r--r--addons/sale_management/static/src/img/openerp_gold_partner.png (renamed from addons/website_quote/static/src/img/openerp_gold_partner.png)bin23252 -> 23252 bytes
-rw-r--r--addons/sale_management/static/src/img/partner_icon_01.png (renamed from addons/website_quote/static/src/img/partner_icon_01.png)bin25055 -> 25055 bytes
-rw-r--r--addons/sale_management/static/src/img/partner_icon_02.png (renamed from addons/website_quote/static/src/img/partner_icon_02.png)bin14792 -> 14792 bytes
-rw-r--r--addons/sale_management/static/src/img/partner_icon_04.png (renamed from addons/website_quote/static/src/img/partner_icon_04.png)bin17968 -> 17968 bytes
-rw-r--r--addons/sale_management/static/src/img/partner_sc_01.png (renamed from addons/website_quote/static/src/img/partner_sc_01.png)bin51068 -> 51068 bytes
-rw-r--r--addons/sale_management/static/src/js/sale_management.js51
-rw-r--r--addons/sale_management/views/res_config_settings_views.xml48
-rw-r--r--addons/sale_management/views/sale_management_templates.xml9
-rw-r--r--addons/sale_management/views/sale_order_template_views.xml162
-rw-r--r--addons/sale_management/views/sale_order_views.xml (renamed from addons/website_quote/views/sale_order_views.xml)24
-rw-r--r--addons/sale_management/views/sale_portal_templates.xml112
-rw-r--r--addons/sale_payment/__init__.py5
-rw-r--r--addons/sale_payment/__manifest__.py27
-rw-r--r--addons/sale_payment/controllers/payment.py89
-rw-r--r--addons/sale_payment/controllers/portal.py25
-rw-r--r--addons/sale_payment/models/res_config_settings.py19
-rw-r--r--addons/sale_payment/models/sale_order.py113
-rw-r--r--addons/sale_payment/views/res_config_settings_views.xml30
-rw-r--r--addons/sale_payment/views/sale_order_views.xml25
-rw-r--r--addons/sale_payment/views/sale_portal_templates.xml111
-rw-r--r--addons/sale_payment/views/settings.xml15
-rw-r--r--addons/sale_quotation_builder/__init__.py (renamed from addons/website_quote/__init__.py)0
-rw-r--r--addons/sale_quotation_builder/__manifest__.py19
-rw-r--r--addons/sale_quotation_builder/controllers/__init__.py (renamed from addons/sale_payment/controllers/__init__.py)3
-rw-r--r--addons/sale_quotation_builder/controllers/portal.py14
-rw-r--r--addons/sale_quotation_builder/data/sale_order_template_data.xml78
-rw-r--r--addons/sale_quotation_builder/models/__init__.py (renamed from addons/sale_payment/models/__init__.py)6
-rw-r--r--addons/sale_quotation_builder/models/product_template.py25
-rw-r--r--addons/sale_quotation_builder/models/sale_order.py80
-rw-r--r--addons/sale_quotation_builder/models/sale_order_template.py63
-rw-r--r--addons/sale_quotation_builder/views/sale_order_template_views.xml32
-rw-r--r--addons/sale_quotation_builder/views/sale_order_views.xml21
-rw-r--r--addons/sale_quotation_builder/views/sale_portal_templates.xml97
-rw-r--r--addons/sale_stock/views/sale_stock_portal_template.xml12
-rw-r--r--addons/sale_timesheet/tests/test_reinvoice.py2
-rw-r--r--addons/sale_timesheet/tests/test_reporting.py2
-rw-r--r--addons/website/models/res_config_settings.py2
-rw-r--r--addons/website_quote/README.md21
-rw-r--r--addons/website_quote/__manifest__.py27
-rw-r--r--addons/website_quote/controllers/main.py225
-rw-r--r--addons/website_quote/data/website_quote_data.xml83
-rw-r--r--addons/website_quote/models/__init__.py7
-rw-r--r--addons/website_quote/models/product_template.py12
-rw-r--r--addons/website_quote/models/res_config_settings.py10
-rw-r--r--addons/website_quote/report/sale_order_reports.xml15
-rw-r--r--addons/website_quote/report/website_quote_templates.xml17
-rw-r--r--addons/website_quote/security/ir.model.access.csv9
-rw-r--r--addons/website_quote/static/description/icon.pngbin9067 -> 0 bytes
-rw-r--r--addons/website_quote/static/description/icon.svg1
-rw-r--r--addons/website_quote/static/src/css/website_quotation.css55
-rw-r--r--addons/website_quote/static/src/js/website_quotation.js139
-rw-r--r--addons/website_quote/static/src/scss/website_quotation.scss6
-rw-r--r--addons/website_quote/views/res_config_settings_views.xml23
-rw-r--r--addons/website_quote/views/sale_quote_views.xml167
-rw-r--r--addons/website_quote/views/website_quote_templates.xml745
-rw-r--r--addons/website_sale/__manifest__.py2
-rw-r--r--addons/website_sale/views/res_config_settings_views.xml19
-rw-r--r--addons/website_sale/views/templates.xml4
-rw-r--r--addons/website_sale_delivery/views/website_sale_delivery_templates.xml2
-rw-r--r--addons/website_sale_digital/views/website_sale_digital.xml6
100 files changed, 2296 insertions, 2720 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index f4fe6810f44..a64d0c906a2 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -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"
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index 4d22d4d9a93..a59d9410bd8 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -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>
diff --git a/addons/account_cancel/views/account_views.xml b/addons/account_cancel/views/account_views.xml
index 07a3bf9d2d8..01b1ab0a7e1 100644
--- a/addons/account_cancel/views/account_views.xml
+++ b/addons/account_cancel/views/account_views.xml
@@ -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>
diff --git a/addons/portal/static/src/js/portal_sidebar.js b/addons/portal/static/src/js/portal_sidebar.js
index 937655d1d97..fc5aa0ee809 100644
--- a/addons/portal/static/src/js/portal_sidebar.js
+++ b/addons/portal/static/src/js/portal_sidebar.js
@@ -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();
},
@@ -50,20 +49,6 @@ var PortalSidebar = Widget.extend({
});
},
/**
- * 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() : '',
});
},
diff --git a/addons/portal/static/src/js/portal_signature.js b/addons/portal/static/src/js/portal_signature.js
index 70e6974dc36..a7848748279 100644
--- a/addons/portal/static/src/js/portal_signature.js
+++ b/addons/portal/static/src/js/portal_signature.js
@@ -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 {
diff --git a/addons/portal/static/src/scss/portal.scss b/addons/portal/static/src/scss/portal.scss
index 60ed887fa46..ba6335bb0f1 100644
--- a/addons/portal/static/src/scss/portal.scss
+++ b/addons/portal/static/src/scss/portal.scss
@@ -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;
diff --git a/addons/portal/static/src/xml/portal_chatter.xml b/addons/portal/static/src/xml/portal_chatter.xml
index a1683dac803..a306cb246e1 100644
--- a/addons/portal/static/src/xml/portal_chatter.xml
+++ b/addons/portal/static/src/xml/portal_chatter.xml
@@ -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>
diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml
index e98e32ad19e..225c22c739d 100644
--- a/addons/portal/views/portal_templates.xml
+++ b/addons/portal/views/portal_templates.xml
@@ -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>
diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py
index a5ff679d745..5924941af76 100644
--- a/addons/sale/__manifest__.py
+++ b/addons/sale/__manifest__.py
@@ -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',
diff --git a/addons/sale/controllers/portal.py b/addons/sale/controllers/portal.py
index 99f4253e929..a7a7a48a47c 100644
--- a/addons/sale/controllers/portal.py
+++ b/addons/sale/controllers/portal.py
@@ -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)
-
- @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:
+ 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)
+
+ # 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)
-
- def _portal_quote_user_can_accept(self, order):
- return order.company_id.portal_confirmation_sign or order.company_id.portal_confirmation_pay
+ transaction = order_sudo.get_portal_last_transaction()
- @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)
diff --git a/addons/sale/models/__init__.py b/addons/sale/models/__init__.py
index cb33ba80b1a..293a9217d67 100644
--- a/addons/sale/models/__init__.py
+++ b/addons/sale/models/__init__.py
@@ -12,3 +12,4 @@ from . import sale
from . import res_config_settings
from . import sales_team
from . import res_users
+from . import payment
diff --git a/addons/sale_payment/models/payment.py b/addons/sale/models/payment.py
index 36287a051de..cfe87d570d5 100644
--- a/addons/sale_payment/models/payment.py
+++ b/addons/sale/models/payment.py
@@ -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")
diff --git a/addons/sale/models/res_company.py b/addons/sale/models/res_company.py
index 77264e51557..f9b62399911 100644
--- a/addons/sale/models/res_company.py
+++ b/addons/sale/models/res_company.py
@@ -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.')]
diff --git a/addons/sale/models/res_config_settings.py b/addons/sale/models/res_config_settings.py
index b99e547d6ad..208f05802f4 100644
--- a/addons/sale/models/res_config_settings.py
+++ b/addons/sale/models/res_config_settings.py
@@ -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()
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 2d6fa9e9cb1..75fef0fc29a 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -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'
diff --git a/addons/sale/report/sale_report.xml b/addons/sale/report/sale_report.xml
index 9c9addca79b..7b7835c61c2 100644
--- a/addons/sale/report/sale_report.xml
+++ b/addons/sale/report/sale_report.xml
@@ -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>
diff --git a/addons/sale/report/sale_report_templates.xml b/addons/sale/report/sale_report_templates.xml
index 23ec95c586d..1506629cb4b 100644
--- a/addons/sale/report/sale_report_templates.xml
+++ b/addons/sale/report/sale_report_templates.xml
@@ -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>
diff --git a/addons/sale/static/src/js/sale_portal_sidebar.js b/addons/sale/static/src/js/sale_portal_sidebar.js
new file mode 100644
index 00000000000..0a06e52cc85
--- /dev/null
+++ b/addons/sale/static/src/js/sale_portal_sidebar.js
@@ -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;
+});
diff --git a/addons/sale_management/static/src/js/tour.js b/addons/sale/static/src/js/tour.js
index fdc14629e09..fdc14629e09 100644
--- a/addons/sale_management/static/src/js/tour.js
+++ b/addons/sale/static/src/js/tour.js
diff --git a/addons/sale/static/src/scss/sale_portal.scss b/addons/sale/static/src/scss/sale_portal.scss
index dd3528e3628..5a78bfd966d 100644
--- a/addons/sale/static/src/scss/sale_portal.scss
+++ b/addons/sale/static/src/scss/sale_portal.scss
@@ -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;
+}
diff --git a/addons/sale_payment/views/payment_views.xml b/addons/sale/views/payment_views.xml
index a909d53afea..6b0163e2536 100644
--- a/addons/sale_payment/views/payment_views.xml
+++ b/addons/sale/views/payment_views.xml
@@ -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"/>
diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml
index 2d7402ec31d..bd559150e0a 100644
--- a/addons/sale/views/res_config_settings_views.xml
+++ b/addons/sale/views/res_config_settings_views.xml
@@ -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>
@@ -205,6 +196,23 @@
</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"/>
</div>
<div class="o_setting_right_pane">
@@ -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">
diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml
index b71f4ae9711..6045bd4eb0e 100644
--- a/addons/sale/views/sale_portal_templates.xml
+++ b/addons/sale/views/sale_portal_templates.xml
@@ -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>
- </t>
- <t t-if="order.state == 'done'">
- <span class="badge badge-success"><i class="fa fa-fw fa-check" /> Done</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>
- </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>
- </t>
- </div>
- </t>
- </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/>
+ <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>
- <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 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>
</div>
- <div t-if="display_discount" class="col-1 text-right" style='white-space: nowrap'>
- <span t-field="line.discount"/> %
+ </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="col-2 text-right">
- <span t-field="line.product_uom_qty"/>
+ <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>
- <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"
- />
+ <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>
- </t>
- <t t-elif="line.display_type == 'line_note'">
- <div class="col-12">
- <span t-field="line.name"/>
+
+ <!-- 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>
- </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 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>
- </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 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>
- </div>
- </t>
- </t>
- <hr/>
+ <t t-call="sale.sale_order_portal_content"/>
- <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-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>
- </div>
- <div class="row">
- <div class="col-lg-9 col-8 text-right">
- <strong>Total</strong>
+ </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 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>
+ <address t-field="sale_order.partner_invoice_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</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"/>
- </t>
+ <div id="shipping_address" class="col-lg-6" groups="sale.group_delivery_invoice_address">
+ <div>
+ <strong>Shipping Address</strong>
+ </div>
+ <div>
+ <address t-field="sale_order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
</div>
</div>
- </div>
+ <div class="row">
+ <div class="col-lg-6">
+ <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>
</div>
- <div class="oe_structure mb32"/>
- </t>
- </template>
+ <div class="oe_structure"/>
+ <!-- Pricing -->
+ <!-- Signature -->
+ <section id="details">
+ <h1 class="page-header" id="details">Details</h1>
- <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>
+ <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>
- </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 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>
+ </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>
+
</odoo>
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index 3e955ba02e6..2dcd3b3c0ee 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -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"/>
diff --git a/addons/sale_expense/tests/test_reinvoice.py b/addons/sale_expense/tests/test_reinvoice.py
index f0c028d69e6..4fee8c6831b 100644
--- a/addons/sale_expense/tests/test_reinvoice.py
+++ b/addons/sale_expense/tests/test_reinvoice.py
@@ -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',
diff --git a/addons/sale_management/__init__.py b/addons/sale_management/__init__.py
index 5b04ef45b70..69eb6a58901 100644
--- a/addons/sale_management/__init__.py
+++ b/addons/sale_management/__init__.py
@@ -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
diff --git a/addons/sale_management/__manifest__.py b/addons/sale_management/__manifest__.py
index ed70455cbf2..887fa210a7c 100644
--- a/addons/sale_management/__manifest__.py
+++ b/addons/sale_management/__manifest__.py
@@ -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',
diff --git a/addons/website_quote/controllers/__init__.py b/addons/sale_management/controllers/__init__.py
index 5d4b25db9c0..903b755e71e 100644
--- a/addons/website_quote/controllers/__init__.py
+++ b/addons/sale_management/controllers/__init__.py
@@ -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
diff --git a/addons/sale_management/controllers/portal.py b/addons/sale_management/controllers/portal.py
new file mode 100644
index 00000000000..4577a9f5ac7
--- /dev/null
+++ b/addons/sale_management/controllers/portal.py
@@ -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())
diff --git a/addons/sale_management/data/sale_order_template_data.xml b/addons/sale_management/data/sale_order_template_data.xml
new file mode 100644
index 00000000000..ba5b0af6d65
--- /dev/null
+++ b/addons/sale_management/data/sale_order_template_data.xml
@@ -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>
diff --git a/addons/website_quote/data/website_quote_demo.xml b/addons/sale_management/data/sale_order_template_demo.xml
index 5a1a21148b6..4e156a51220 100644
--- a/addons/website_quote/data/website_quote_demo.xml
+++ b/addons/sale_management/data/sale_order_template_demo.xml
@@ -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>
diff --git a/addons/sale_management/models/__init__.py b/addons/sale_management/models/__init__.py
index 5c69071ab19..658b0890cf3 100644
--- a/addons/sale_management/models/__init__.py
+++ b/addons/sale_management/models/__init__.py
@@ -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
diff --git a/addons/sale_management/models/res_config_settings.py b/addons/sale_management/models/res_config_settings.py
new file mode 100644
index 00000000000..ed3fc5caac5
--- /dev/null
+++ b/addons/sale_management/models/res_config_settings.py
@@ -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
diff --git a/addons/sale_management/models/res_users.py b/addons/sale_management/models/res_users.py
new file mode 100644
index 00000000000..1484e3c0cfa
--- /dev/null
+++ b/addons/sale_management/models/res_users.py
@@ -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')
diff --git a/addons/website_quote/models/sale_order.py b/addons/sale_management/models/sale_order.py
index 9c16318e51d..f2481bc7fc5 100644
--- a/addons/website_quote/models/sale_order.py
+++ b/addons/sale_management/models/sale_order.py
@@ -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.add_option_to_order()
+ return {'type': 'ir.actions.client', 'tag': 'reload'}
+
+ @api.multi
+ def add_option_to_order(self):
self.ensure_one()
- order = self.order_id
- if order.state not in ['draft', 'sent']:
- return False
- 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()
+ 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,
+ }
diff --git a/addons/website_quote/models/sale_quote.py b/addons/sale_management/models/sale_order_template.py
index c27d8a2368b..404f4503786 100644
--- a/addons/website_quote/models/sale_quote.py
+++ b/addons/sale_management/models/sale_order_template.py
@@ -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)]}
diff --git a/addons/website_quote/report/sale_order_templates.xml b/addons/sale_management/report/sale_report_templates.xml
index ddd9eca7eb7..c5e9e9abccc 100644
--- a/addons/website_quote/report/sale_order_templates.xml
+++ b/addons/sale_management/report/sale_report_templates.xml
@@ -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>
diff --git a/addons/sale_management/security/ir.model.access.csv b/addons/sale_management/security/ir.model.access.csv
new file mode 100644
index 00000000000..ddca9403663
--- /dev/null
+++ b/addons/sale_management/security/ir.model.access.csv
@@ -0,0 +1,9 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_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
diff --git a/addons/sale_management/security/sale_management_security.xml b/addons/sale_management/security/sale_management_security.xml
new file mode 100644
index 00000000000..b5ea06ed03f
--- /dev/null
+++ b/addons/sale_management/security/sale_management_security.xml
@@ -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>
diff --git a/addons/website_quote/static/src/img/online_training.png b/addons/sale_management/static/src/img/online_training.png
index cc03d0d7c35..cc03d0d7c35 100644
--- a/addons/website_quote/static/src/img/online_training.png
+++ b/addons/sale_management/static/src/img/online_training.png
Binary files differ
diff --git a/addons/website_quote/static/src/img/openerp_gold_partner.png b/addons/sale_management/static/src/img/openerp_gold_partner.png
index f0404e8ca6f..f0404e8ca6f 100644
--- a/addons/website_quote/static/src/img/openerp_gold_partner.png
+++ b/addons/sale_management/static/src/img/openerp_gold_partner.png
Binary files differ
diff --git a/addons/website_quote/static/src/img/partner_icon_01.png b/addons/sale_management/static/src/img/partner_icon_01.png
index 880752e3518..880752e3518 100644
--- a/addons/website_quote/static/src/img/partner_icon_01.png
+++ b/addons/sale_management/static/src/img/partner_icon_01.png
Binary files differ
diff --git a/addons/website_quote/static/src/img/partner_icon_02.png b/addons/sale_management/static/src/img/partner_icon_02.png
index c3b76637f25..c3b76637f25 100644
--- a/addons/website_quote/static/src/img/partner_icon_02.png
+++ b/addons/sale_management/static/src/img/partner_icon_02.png
Binary files differ
diff --git a/addons/website_quote/static/src/img/partner_icon_04.png b/addons/sale_management/static/src/img/partner_icon_04.png
index a1c2193a152..a1c2193a152 100644
--- a/addons/website_quote/static/src/img/partner_icon_04.png
+++ b/addons/sale_management/static/src/img/partner_icon_04.png
Binary files differ
diff --git a/addons/website_quote/static/src/img/partner_sc_01.png b/addons/sale_management/static/src/img/partner_sc_01.png
index 2b4872a2b4d..2b4872a2b4d 100644
--- a/addons/website_quote/static/src/img/partner_sc_01.png
+++ b/addons/sale_management/static/src/img/partner_sc_01.png
Binary files differ
diff --git a/addons/sale_management/static/src/js/sale_management.js b/addons/sale_management/static/src/js/sale_management.js
new file mode 100644
index 00000000000..77806630166
--- /dev/null
+++ b/addons/sale_management/static/src/js/sale_management.js
@@ -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);
+ });
+
+});
diff --git a/addons/sale_management/views/res_config_settings_views.xml b/addons/sale_management/views/res_config_settings_views.xml
new file mode 100644
index 00000000000..3820f5338c7
--- /dev/null
+++ b/addons/sale_management/views/res_config_settings_views.xml
@@ -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>
diff --git a/addons/sale_management/views/sale_management_templates.xml b/addons/sale_management/views/sale_management_templates.xml
deleted file mode 100644
index 50517640187..00000000000
--- a/addons/sale_management/views/sale_management_templates.xml
+++ /dev/null
@@ -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>
diff --git a/addons/sale_management/views/sale_order_template_views.xml b/addons/sale_management/views/sale_order_template_views.xml
new file mode 100644
index 00000000000..5365aee738b
--- /dev/null
+++ b/addons/sale_management/views/sale_order_template_views.xml
@@ -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>
diff --git a/addons/website_quote/views/sale_order_views.xml b/addons/sale_management/views/sale_order_views.xml
index e822ddcab60..c6a8972213f 100644
--- a/addons/website_quote/views/sale_order_views.xml
+++ b/addons/sale_management/views/sale_order_views.xml
@@ -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>
diff --git a/addons/sale_management/views/sale_portal_templates.xml b/addons/sale_management/views/sale_portal_templates.xml
new file mode 100644
index 00000000000..e946706df81
--- /dev/null
+++ b/addons/sale_management/views/sale_portal_templates.xml
@@ -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>
diff --git a/addons/sale_payment/__init__.py b/addons/sale_payment/__init__.py
deleted file mode 100644
index 3ea0f0cac45..00000000000
--- a/addons/sale_payment/__init__.py
+++ /dev/null
@@ -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 \ No newline at end of file
diff --git a/addons/sale_payment/__manifest__.py b/addons/sale_payment/__manifest__.py
deleted file mode 100644
index 1c489d8394b..00000000000
--- a/addons/sale_payment/__manifest__.py
+++ /dev/null
@@ -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,
-}
diff --git a/addons/sale_payment/controllers/payment.py b/addons/sale_payment/controllers/payment.py
deleted file mode 100644
index 5dbc8f5cad1..00000000000
--- a/addons/sale_payment/controllers/payment.py
+++ /dev/null
@@ -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))
diff --git a/addons/sale_payment/controllers/portal.py b/addons/sale_payment/controllers/portal.py
deleted file mode 100644
index 9b554c19c34..00000000000
--- a/addons/sale_payment/controllers/portal.py
+++ /dev/null
@@ -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
diff --git a/addons/sale_payment/models/res_config_settings.py b/addons/sale_payment/models/res_config_settings.py
deleted file mode 100644
index c8e85cf3a6a..00000000000
--- a/addons/sale_payment/models/res_config_settings.py
+++ /dev/null
@@ -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))
diff --git a/addons/sale_payment/models/sale_order.py b/addons/sale_payment/models/sale_order.py
deleted file mode 100644
index 17f3781b460..00000000000
--- a/addons/sale_payment/models/sale_order.py
+++ /dev/null
@@ -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()
diff --git a/addons/sale_payment/views/res_config_settings_views.xml b/addons/sale_payment/views/res_config_settings_views.xml
deleted file mode 100644
index 5c6c1f17058..00000000000
--- a/addons/sale_payment/views/res_config_settings_views.xml
+++ /dev/null
@@ -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>
diff --git a/addons/sale_payment/views/sale_order_views.xml b/addons/sale_payment/views/sale_order_views.xml
deleted file mode 100644
index e0cb9f5fc14..00000000000
--- a/addons/sale_payment/views/sale_order_views.xml
+++ /dev/null
@@ -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>
diff --git a/addons/sale_payment/views/sale_portal_templates.xml b/addons/sale_payment/views/sale_portal_templates.xml
deleted file mode 100644
index 185a0ebcf32..00000000000
--- a/addons/sale_payment/views/sale_portal_templates.xml
+++ /dev/null
@@ -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>
diff --git a/addons/sale_payment/views/settings.xml b/addons/sale_payment/views/settings.xml
deleted file mode 100644
index bd214525c4b..00000000000
--- a/addons/sale_payment/views/settings.xml
+++ /dev/null
@@ -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>
diff --git a/addons/website_quote/__init__.py b/addons/sale_quotation_builder/__init__.py
index 7d34c7c054a..7d34c7c054a 100644
--- a/addons/website_quote/__init__.py
+++ b/addons/sale_quotation_builder/__init__.py
diff --git a/addons/sale_quotation_builder/__manifest__.py b/addons/sale_quotation_builder/__manifest__.py
new file mode 100644
index 00000000000..7f4b1cd45b1
--- /dev/null
+++ b/addons/sale_quotation_builder/__manifest__.py
@@ -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,
+}
diff --git a/addons/sale_payment/controllers/__init__.py b/addons/sale_quotation_builder/controllers/__init__.py
index b3b6c6da572..903b755e71e 100644
--- a/addons/sale_payment/controllers/__init__.py
+++ b/addons/sale_quotation_builder/controllers/__init__.py
@@ -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 \ No newline at end of file
+from . import portal
diff --git a/addons/sale_quotation_builder/controllers/portal.py b/addons/sale_quotation_builder/controllers/portal.py
new file mode 100644
index 00000000000..55a8f4f84d9
--- /dev/null
+++ b/addons/sale_quotation_builder/controllers/portal.py
@@ -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)
diff --git a/addons/sale_quotation_builder/data/sale_order_template_data.xml b/addons/sale_quotation_builder/data/sale_order_template_data.xml
new file mode 100644
index 00000000000..829be772995
--- /dev/null
+++ b/addons/sale_quotation_builder/data/sale_order_template_data.xml
@@ -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>
diff --git a/addons/sale_payment/models/__init__.py b/addons/sale_quotation_builder/models/__init__.py
index ffb33c6a1c8..84a0f45f0b4 100644
--- a/addons/sale_payment/models/__init__.py
+++ b/addons/sale_quotation_builder/models/__init__.py
@@ -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 \ No newline at end of file
+from . import product_template
+from . import sale_order
+from . import sale_order_template
diff --git a/addons/sale_quotation_builder/models/product_template.py b/addons/sale_quotation_builder/models/product_template.py
new file mode 100644
index 00000000000..2c5d414b504
--- /dev/null
+++ b/addons/sale_quotation_builder/models/product_template.py
@@ -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 = ''
diff --git a/addons/sale_quotation_builder/models/sale_order.py b/addons/sale_quotation_builder/models/sale_order.py
new file mode 100644
index 00000000000..7a401379a03
--- /dev/null
+++ b/addons/sale_quotation_builder/models/sale_order.py
@@ -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
diff --git a/addons/sale_quotation_builder/models/sale_order_template.py b/addons/sale_quotation_builder/models/sale_order_template.py
new file mode 100644
index 00000000000..ba7aa199128
--- /dev/null
+++ b/addons/sale_quotation_builder/models/sale_order_template.py
@@ -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
diff --git a/addons/sale_quotation_builder/views/sale_order_template_views.xml b/addons/sale_quotation_builder/views/sale_order_template_views.xml
new file mode 100644
index 00000000000..0a14385bfa6
--- /dev/null
+++ b/addons/sale_quotation_builder/views/sale_order_template_views.xml
@@ -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>
diff --git a/addons/sale_quotation_builder/views/sale_order_views.xml b/addons/sale_quotation_builder/views/sale_order_views.xml
new file mode 100644
index 00000000000..e46a0cdd68b
--- /dev/null
+++ b/addons/sale_quotation_builder/views/sale_order_views.xml
@@ -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>
diff --git a/addons/sale_quotation_builder/views/sale_portal_templates.xml b/addons/sale_quotation_builder/views/sale_portal_templates.xml
new file mode 100644
index 00000000000..8a43ab4f1bd
--- /dev/null
+++ b/addons/sale_quotation_builder/views/sale_portal_templates.xml
@@ -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>
diff --git a/addons/sale_stock/views/sale_stock_portal_template.xml b/addons/sale_stock/views/sale_stock_portal_template.xml
index 5e31f575c3f..9069b6c1e27 100644
--- a/addons/sale_stock/views/sale_stock_portal_template.xml
+++ b/addons/sale_stock/views/sale_stock_portal_template.xml
@@ -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>
diff --git a/addons/sale_timesheet/tests/test_reinvoice.py b/addons/sale_timesheet/tests/test_reinvoice.py
index 1ccf022eb13..59ecf6c9463 100644
--- a/addons/sale_timesheet/tests/test_reinvoice.py
+++ b/addons/sale_timesheet/tests/test_reinvoice.py
@@ -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',
diff --git a/addons/sale_timesheet/tests/test_reporting.py b/addons/sale_timesheet/tests/test_reporting.py
index 4838cf1786a..0694125b20a 100644
--- a/addons/sale_timesheet/tests/test_reporting.py
+++ b/addons/sale_timesheet/tests/test_reporting.py
@@ -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",
diff --git a/addons/website/models/res_config_settings.py b/addons/website/models/res_config_settings.py
index 36ca15c2e58..f2d6f202e7d 100644
--- a/addons/website/models/res_config_settings.py
+++ b/addons/website/models/res_config_settings.py
@@ -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')
diff --git a/addons/website_quote/README.md b/addons/website_quote/README.md
deleted file mode 100644
index 26c08b84027..00000000000
--- a/addons/website_quote/README.md
+++ /dev/null
@@ -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.
diff --git a/addons/website_quote/__manifest__.py b/addons/website_quote/__manifest__.py
deleted file mode 100644
index c55d77ba0a5..00000000000
--- a/addons/website_quote/__manifest__.py
+++ /dev/null
@@ -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,
-}
diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py
deleted file mode 100644
index 034773a0821..00000000000
--- a/addons/website_quote/controllers/main.py
+++ /dev/null
@@ -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))
diff --git a/addons/website_quote/data/website_quote_data.xml b/addons/website_quote/data/website_quote_data.xml
deleted file mode 100644
index ff1af6ec595..00000000000
--- a/addons/website_quote/data/website_quote_data.xml
+++ /dev/null
@@ -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>
diff --git a/addons/website_quote/models/__init__.py b/addons/website_quote/models/__init__.py
deleted file mode 100644
index 08bb5a18917..00000000000
--- a/addons/website_quote/models/__init__.py
+++ /dev/null
@@ -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
diff --git a/addons/website_quote/models/product_template.py b/addons/website_quote/models/product_template.py
deleted file mode 100644
index 4c6c16e98a0..00000000000
--- a/addons/website_quote/models/product_template.py
+++ /dev/null
@@ -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)
diff --git a/addons/website_quote/models/res_config_settings.py b/addons/website_quote/models/res_config_settings.py
deleted file mode 100644
index 356606164ac..00000000000
--- a/addons/website_quote/models/res_config_settings.py
+++ /dev/null
@@ -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')
diff --git a/addons/website_quote/report/sale_order_reports.xml b/addons/website_quote/report/sale_order_reports.xml
deleted file mode 100644
index ce6877469fd..00000000000
--- a/addons/website_quote/report/sale_order_reports.xml
+++ /dev/null
@@ -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>
diff --git a/addons/website_quote/report/website_quote_templates.xml b/addons/website_quote/report/website_quote_templates.xml
deleted file mode 100644
index 25483fb8e02..00000000000
--- a/addons/website_quote/report/website_quote_templates.xml
+++ /dev/null
@@ -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>
diff --git a/addons/website_quote/security/ir.model.access.csv b/addons/website_quote/security/ir.model.access.csv
deleted file mode 100644
index 1b083313179..00000000000
--- a/addons/website_quote/security/ir.model.access.csv
+++ /dev/null
@@ -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
diff --git a/addons/website_quote/static/description/icon.png b/addons/website_quote/static/description/icon.png
deleted file mode 100644
index ffba280217c..00000000000
--- a/addons/website_quote/static/description/icon.png
+++ /dev/null
Binary files differ
diff --git a/addons/website_quote/static/description/icon.svg b/addons/website_quote/static/description/icon.svg
deleted file mode 100644
index 920c762ebe5..00000000000
--- a/addons/website_quote/static/description/icon.svg
+++ /dev/null
@@ -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> \ No newline at end of file
diff --git a/addons/website_quote/static/src/css/website_quotation.css b/addons/website_quote/static/src/css/website_quotation.css
deleted file mode 100644
index f52a2601381..00000000000
--- a/addons/website_quote/static/src/css/website_quotation.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/addons/website_quote/static/src/js/website_quotation.js b/addons/website_quote/static/src/js/website_quotation.js
deleted file mode 100644
index 214345b9dae..00000000000
--- a/addons/website_quote/static/src/js/website_quotation.js
+++ /dev/null
@@ -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(),
- },
- });
-});
diff --git a/addons/website_quote/static/src/scss/website_quotation.scss b/addons/website_quote/static/src/scss/website_quotation.scss
deleted file mode 100644
index 26ba0eaef64..00000000000
--- a/addons/website_quote/static/src/scss/website_quotation.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.o_website_quote{
- h1[id],h2[id]{
- margin-top: 0px;
- padding-top: $o-navbar-height;
- }
-}
diff --git a/addons/website_quote/views/res_config_settings_views.xml b/addons/website_quote/views/res_config_settings_views.xml
deleted file mode 100644
index c16cdabc055..00000000000
--- a/addons/website_quote/views/res_config_settings_views.xml
+++ /dev/null
@@ -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>
diff --git a/addons/website_quote/views/sale_quote_views.xml b/addons/website_quote/views/sale_quote_views.xml
deleted file mode 100644
index ca2d9a0333a..00000000000
--- a/addons/website_quote/views/sale_quote_views.xml
+++ /dev/null
@@ -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>
diff --git a/addons/website_quote/views/website_quote_templates.xml b/addons/website_quote/views/website_quote_templates.xml
deleted file mode 100644
index 28851eb32de..00000000000
--- a/addons/website_quote/views/website_quote_templates.xml
+++ /dev/null
@@ -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>
diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py
index f18756a08e0..91ce59ec5eb 100644
--- a/addons/website_sale/__manifest__.py
+++ b/addons/website_sale/__manifest__.py
@@ -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',
diff --git a/addons/website_sale/views/res_config_settings_views.xml b/addons/website_sale/views/res_config_settings_views.xml
index c51de31b48e..ed55df21e5b 100644
--- a/addons/website_sale/views/res_config_settings_views.xml
+++ b/addons/website_sale/views/res_config_settings_views.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">
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index cbc64b58754..daf3b4e9cf5 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -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>
diff --git a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
index 177df3b8a39..5b79b64a0c8 100644
--- a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
+++ b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
@@ -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"/>
diff --git a/addons/website_sale_digital/views/website_sale_digital.xml b/addons/website_sale_digital/views/website_sale_digital.xml
index 31789d47e42..622f3443be1 100644
--- a/addons/website_sale_digital/views/website_sale_digital.xml
+++ b/addons/website_sale_digital/views/website_sale_digital.xml
@@ -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">