diff options
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'}"/>&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'}"/>&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 & </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 & 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 > 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 > 1"> + days + </t> + <t t-if="sale_order.remaining_validity_days <= 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 > 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">&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">&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">&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">&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">&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 & 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">&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">&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>&nbsp;<span>on</span>&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 & </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&model=%s&id=%s&action=%s&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&model=%s&id=%s&action=%s&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() + '&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 & 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>&nbsp;<span>on</span>&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 & 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','>=',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','>=',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 Binary files differindex cc03d0d7c35..cc03d0d7c35 100644 --- a/addons/website_quote/static/src/img/online_training.png +++ b/addons/sale_management/static/src/img/online_training.png diff --git a/addons/website_quote/static/src/img/openerp_gold_partner.png b/addons/sale_management/static/src/img/openerp_gold_partner.png Binary files differindex 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 diff --git a/addons/website_quote/static/src/img/partner_icon_01.png b/addons/sale_management/static/src/img/partner_icon_01.png Binary files differindex 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 diff --git a/addons/website_quote/static/src/img/partner_icon_02.png b/addons/sale_management/static/src/img/partner_icon_02.png Binary files differindex 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 diff --git a/addons/website_quote/static/src/img/partner_icon_04.png b/addons/sale_management/static/src/img/partner_icon_04.png Binary files differindex 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 diff --git a/addons/website_quote/static/src/img/partner_sc_01.png b/addons/sale_management/static/src/img/partner_sc_01.png Binary files differindex 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 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 & 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) + '&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) + '&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&model=%s&id=%s&action=%s&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">&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">&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">&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 & 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 Binary files differdeleted file mode 100644 index ffba280217c..00000000000 --- a/addons/website_quote/static/description/icon.png +++ /dev/null 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 & 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 }&unlink=True&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>&nbsp;<span>on</span>&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 & 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 }&remove=True&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 }&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">&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">&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">&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">&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">&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 & </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 > 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 > 1"> - days - </t> - <t t-if="days_valid <= 1"> - day - </t> - </div> - </div> - - - <div class="text-center mb16" t-if="quotation.amount_undiscounted > 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">&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">&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">&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">&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">&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">&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 & 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">&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">&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 & 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 & 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&model=%s&id=%s&action=%s&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&model=%s&id=%s&action=%s&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&model=%s&id=%s&action=%s&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">&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">&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">&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 & 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&utm_medium=website">Odoo</a>, - an awesome <a target="_blank" href="http://www.odoo.com/page/crm?utm_source=db&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"> |
