From 2fa469c5e5ec66da262702d8c9e2415c72f7f6b9 Mon Sep 17 00:00:00 2001 From: Joren Van Onder Date: Thu, 5 Apr 2018 15:24:06 -0700 Subject: [PATCH] [IMP] sale, website_quote: allow portal payment after signature Both features already existed but it was not possible to enable both. This makes that possible. When both are enabled the quotation first has to be signed, then it can be paid. Purpose ======= No way to combine digital signature and online payments to confirm orders. A customer signature matters for big deals, in order to keep an official print of customer agreement. Specs ===== - when the customer signs online, a new pdf is generated with the customer signature on it and posted as a note in the chatter, replacing the current signature jpg. - show the signature in the portal view, with both sale_management and website_quote module - change in settings: Digital Signature and Electronic Payment are now 2 checkboxes instead of a radio button - these settings must be company-specific (add company icon if multi-companies, like other company-specific settings) - when both modes are activated, it first request the signature, then the payment - the order is confirmed after payment opw-1818837 Closes #24174 --- addons/sale/controllers/portal.py | 29 ++++++---- addons/sale/data/mail_data.xml | 10 +++- addons/sale/models/res_company.py | 2 + addons/sale/models/res_config_settings.py | 23 ++------ addons/sale/models/sale.py | 18 +++++- addons/sale/report/sale_report_templates.xml | 13 +++++ .../sale/views/res_config_settings_views.xml | 43 ++++++++------ addons/sale_payment/views/settings.xml | 4 +- addons/website_quote/controllers/main.py | 9 ++- addons/website_quote/models/sale_order.py | 58 ++++++++++++++----- addons/website_quote/models/sale_quote.py | 19 +++--- .../website_quote/views/sale_order_views.xml | 3 +- .../website_quote/views/sale_quote_views.xml | 3 +- .../views/website_quote_templates.xml | 34 ++++++++--- .../views/res_config_settings_views.xml | 2 +- 15 files changed, 180 insertions(+), 90 deletions(-) diff --git a/addons/sale/controllers/portal.py b/addons/sale/controllers/portal.py index c33e7b79860..a72c647844b 100644 --- a/addons/sale/controllers/portal.py +++ b/addons/sale/controllers/portal.py @@ -200,16 +200,11 @@ class CustomerPortal(CustomerPortal): ] return request.make_response(pdf, headers=pdfhttpheaders) - def _portal_quote_user_can_accept(self, order_id): - return request.env['ir.config_parameter'].sudo().get_param('sale.sale_portal_confirmation_options', default='none') in ('pay', 'sign') + def _portal_quote_user_can_accept(self, order): + return order.company_id.portal_confirmation_sign or order.company_id.portal_confirmation_pay @http.route(['/my/quotes/accept'], type='json', auth="public", website=True) def portal_quote_accept(self, res_id, access_token=None, partner_name=None, signature=None): - if not self._portal_quote_user_can_accept(res_id): - return {'error': _('Operation not allowed')} - if not signature: - return {'error': _('Signature is missing.')} - try: order_sudo = self._order_check_access(res_id, access_token=access_token) except AccessError: @@ -217,15 +212,29 @@ class CustomerPortal(CustomerPortal): if order_sudo.state != 'sent': return {'error': _('Order is not in a state requiring customer validation.')} - order_sudo.action_confirm() + 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=[('signature.png', base64.b64decode(signature))] if signature else [], + attachments=[('%s.pdf' % order_sudo.name, pdf)], **({'token': access_token} if access_token else {})) + return { - 'success': _('Your Order has been confirmed.'), + 'success': success_message, 'redirect_url': '/my/orders/%s?%s' % (order_sudo.id, access_token and 'access_token=%s' % order_sudo.access_token or ''), } diff --git a/addons/sale/data/mail_data.xml b/addons/sale/data/mail_data.xml index ce8dcc651a1..cc3a3291a2d 100644 --- a/addons/sale/data/mail_data.xml +++ b/addons/sale/data/mail_data.xml @@ -69,8 +69,16 @@ % set is_online = access_action and access_action['type'] == 'ir.actions.act_url' % set doc_name = 'quotation' if object.state in ('draft', 'sent') else 'order confirmation' % set pay_sign_name = object.get_portal_confirmation_action() -% set access_name = is_online and object.state in ('draft', 'sent') and pay_sign_name in ('pay', 'sign') and 'Accept and %s online' % pay_sign_name or 'View %s' % doc_name % set access_url = is_online and object.get_mail_url() or '' +% set access_name = 'View %s' % doc_name + +% if is_online and object.state in ('draft', 'sent') and pay_sign_name in ('pay', 'sign'): + % if pay_sign_name == 'sign' and object.has_to_be_paid(): + % set access_name = 'Sign and pay online' + % else: + % set access_name = 'Accept and %s online' % pay_sign_name + % endif +% endif % if object.partner_id.parent_id: (${object.partner_id.parent_id.name}) diff --git a/addons/sale/models/res_company.py b/addons/sale/models/res_company.py index 54a389d315e..d868ca91638 100644 --- a/addons/sale/models/res_company.py +++ b/addons/sale/models/res_company.py @@ -8,3 +8,5 @@ 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', default=True) + portal_confirmation_pay = fields.Boolean(string='Electronic Payment') diff --git a/addons/sale/models/res_config_settings.py b/addons/sale/models/res_config_settings.py index d8fe8daeba5..c42b10d9b52 100644 --- a/addons/sale/models/res_config_settings.py +++ b/addons/sale/models/res_config_settings.py @@ -18,12 +18,9 @@ class ResConfigSettings(models.TransientModel): module_sale_margin = fields.Boolean("Margins") group_sale_layout = fields.Boolean("Sections on Sales Orders", implied_group='sale.group_sale_layout') group_warning_sale = fields.Boolean("Sale Order Warnings", implied_group='sale.group_warning_sale') - portal_confirmation = fields.Boolean('Online Signature & Payment') - portal_confirmation_options = fields.Selection([ - ('sign', 'Signature'), - ('pay', 'Payment')], string="Online Signature & Payment options", - config_parameter='sale.sale_portal_confirmation_options') - module_sale_payment = fields.Boolean("Sale Payment", help='Technical field implied by user choice of online_confirmation') + portal_confirmation_sign = fields.Boolean(related='company_id.portal_confirmation_sign', string='Digital 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") @@ -131,16 +128,9 @@ class ResConfigSettings(models.TransientModel): 'group_pricelist_item': False, }) - @api.onchange('portal_confirmation') - def _onchange_portal_confirmation(self): - if not self.portal_confirmation: - self.portal_confirmation_options = False - elif not self.portal_confirmation_options: - self.portal_confirmation_options = 'sign' - - @api.onchange('portal_confirmation_options') - def _onchange_portal_confirmation_options(self): - if self.portal_confirmation_options == 'pay': + @api.onchange('portal_confirmation_pay') + def _onchange_portal_confirmation_pay(self): + if self.portal_confirmation_pay: self.module_sale_payment = True @api.model @@ -152,6 +142,5 @@ class ResConfigSettings(models.TransientModel): multi_sales_price=sale_pricelist_setting in ['percentage', 'formula'], multi_sales_price_method=sale_pricelist_setting in ['percentage', 'formula'] and sale_pricelist_setting or False, sale_pricelist_setting=sale_pricelist_setting, - portal_confirmation=ICPSudo.get_param('sale.sale_portal_confirmation_options', default=False) in ('pay', 'sign'), ) return res diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 1f0eeb79071..676f10c30be 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -163,6 +163,9 @@ class SaleOrder(models.Model): company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('sale.order')) team_id = fields.Many2one('crm.team', 'Sales Channel', change_default=True, default=_get_default_team, oldname='section_id') + signature = fields.Binary('Signature', help='Signature received through the portal.', copy=False, attachment=True) + signed_by = fields.Char('Signed by', help='Name of the person that signed the SO.', copy=False) + product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product') def _compute_portal_url(self): @@ -488,6 +491,8 @@ class SaleOrder(models.Model): orders = self.filtered(lambda s: s.state in ['cancel', 'sent']) return orders.write({ 'state': 'draft', + 'signature': False, + 'signed_by': False, }) @api.multi @@ -673,7 +678,18 @@ class SaleOrder(models.Model): return self.get_share_url() def get_portal_confirmation_action(self): - return self.env['ir.config_parameter'].sudo().get_param('sale.sale_portal_confirmation_options', default='none') + if self.company_id.portal_confirmation_sign and not self.signature: + return 'sign' + if self.company_id.portal_confirmation_pay: + return 'pay' + + return 'none' + + def has_to_be_signed(self): + return self.company_id.portal_confirmation_sign + + def has_to_be_paid(self): + return self.company_id.portal_confirmation_pay @api.multi def _notify_get_groups(self, message, groups): diff --git a/addons/sale/report/sale_report_templates.xml b/addons/sale/report/sale_report_templates.xml index 3b948aef951..a5da58ea770 100644 --- a/addons/sale/report/sale_report_templates.xml +++ b/addons/sale/report/sale_report_templates.xml @@ -202,6 +202,19 @@ Fiscal Position Remark:

+ +
+
+ Signature +
+
+ +
+
+

+

+
+
diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml index 558bd567c01..cafce853411 100644 --- a/addons/sale/views/res_config_settings_views.xml +++ b/addons/sale/views/res_config_settings_views.xml @@ -139,6 +139,31 @@

Quotations & Orders

+
+
+ +
+
+
+
+
+
+ +
+
+ +
+
@@ -154,24 +179,6 @@
-
-
- -
-
- -
-
diff --git a/addons/sale_payment/views/settings.xml b/addons/sale_payment/views/settings.xml index a0379056a6f..bd214525c4b 100644 --- a/addons/sale_payment/views/settings.xml +++ b/addons/sale_payment/views/settings.xml @@ -5,8 +5,8 @@ res.config.settings - -
+ +
diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py index 50d448405bf..86e5920c712 100644 --- a/addons/website_quote/controllers/main.py +++ b/addons/website_quote/controllers/main.py @@ -24,11 +24,10 @@ class CustomerPortal(CustomerPortal): return request.redirect('/quote/%s/%s' % (order, access_token or '')) return super(CustomerPortal, self).portal_order_page(order=order, access_token=access_token, **kw) - def _portal_quote_user_can_accept(self, order_id): - result = super(CustomerPortal, self)._portal_quote_user_can_accept(order_id) - order_sudo = request.env['sale.order'].sudo().browse(order_id) + 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 not order_sudo.require_payment if order_sudo.template_id else result + return order.require_signature if order.template_id else result class sale_quote(http.Controller): @@ -85,7 +84,7 @@ class sale_quote(http.Controller): 'partner_id': order_sudo.partner_id.id, } - if order_sudo.require_payment or values['need_payment']: + 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])] diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py index 294cce55d26..031a7a3d65b 100644 --- a/addons/website_quote/models/sale_order.py +++ b/addons/website_quote/models/sale_order.py @@ -6,6 +6,7 @@ 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 werkzeug.urls import url_encode @@ -51,16 +52,19 @@ class SaleOrder(models.Model): 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_online_payment(self): + def _get_default_require_signature(self): default_template = self._get_default_template() - if self.template_id: - return self.template_id.require_payment - elif default_template: - return default_template.require_payment - elif self.env['ir.config_parameter'].sudo().get_param('sale.sale_portal_confirmation_options', default='none') == 'pay': - return 1 + if default_template: + return default_template.require_signature else: - return 0 + return False + + def _get_default_require_payment(self): + default_template = self._get_default_template() + if default_template: + return default_template.require_payment + else: + return False template_id = fields.Many2one( 'sale.quote.template', 'Quotation Template', @@ -75,12 +79,12 @@ class SaleOrder(models.Model): amount_undiscounted = fields.Float( 'Amount Before Discount', compute='_compute_amount_undiscounted', digits=0) quote_viewed = fields.Boolean('Quotation Viewed') - require_payment = fields.Selection([ - (0, 'Online Signature'), - (1, 'Online Payment')], default=_get_default_online_payment, string='Confirmation Mode', - help="Choose how you want to confirm an order to launch the delivery process. You can either " - "request a digital signature or an upfront payment. With a digital signature, you can " - "request the payment when issuing the invoice.") + require_signature = fields.Boolean('Digital 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.') + 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.') @api.multi @api.returns('self', lambda value: value.id) @@ -113,6 +117,8 @@ class SaleOrder(models.Model): @api.onchange('template_id') def onchange_template_id(self): if not self.template_id: + self.require_signature = False + self.require_payment = False return template = self.template_id.with_context(lang=self.partner_id.lang) @@ -170,11 +176,18 @@ class SaleOrder(models.Model): 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): self.ensure_one() @@ -209,10 +222,23 @@ class SaleOrder(models.Model): def get_portal_confirmation_action(self): """ Template override default behavior of pay / sign chosen in sales settings """ - if self.require_payment is not None or self.require_payment is not False: - return 'pay' if self.require_payment == 1 else 'sign' + if self.template_id: + if self.require_signature and not self.signature: + return 'sign' + elif self.require_payment: + return 'pay' + else: + return 'none' return super(SaleOrder, self).get_portal_confirmation_action() + def has_to_be_signed(self): + res = super(SaleOrder, self).has_to_be_signed() + return self.require_signature if self.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 + @api.multi def action_confirm(self): res = super(SaleOrder, self).action_confirm() diff --git a/addons/website_quote/models/sale_quote.py b/addons/website_quote/models/sale_quote.py index 820c9ac8f24..6456cb8e762 100644 --- a/addons/website_quote/models/sale_quote.py +++ b/addons/website_quote/models/sale_quote.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +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 class SaleQuoteTemplate(models.Model): @@ -17,18 +18,20 @@ class SaleQuoteTemplate(models.Model): options = fields.One2many('sale.quote.option', 'template_id', 'Optional Products Lines', copy=True) number_of_days = fields.Integer('Quotation Duration', help='Number of days for the validity date computation of the quotation') - require_payment = fields.Selection([ - (0, 'Online Signature'), - (1, 'Online Payment')], default=0, string='Confirmation Mode', - help="Choose how you want to confirm an order to launch the delivery process. You can either " - "request a digital signature or an upfront payment. With a digital signature, you can " - "request the payment when issuing the invoice.") + require_signature = fields.Boolean('Digital Signature', default=True, help='Request a digital signature to the customer in order to confirm orders automatically.') + require_payment = fields.Boolean('Electronic 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() diff --git a/addons/website_quote/views/sale_order_views.xml b/addons/website_quote/views/sale_order_views.xml index ba9df11b65d..62fcc573eef 100644 --- a/addons/website_quote/views/sale_order_views.xml +++ b/addons/website_quote/views/sale_order_views.xml @@ -37,7 +37,8 @@
- + +
diff --git a/addons/website_quote/views/sale_quote_views.xml b/addons/website_quote/views/sale_quote_views.xml index 5750dc207c3..e71c6f993ba 100644 --- a/addons/website_quote/views/sale_quote_views.xml +++ b/addons/website_quote/views/sale_quote_views.xml @@ -94,7 +94,8 @@ - + + diff --git a/addons/website_quote/views/website_quote_templates.xml b/addons/website_quote/views/website_quote_templates.xml index ab4a4414c5b..c9c1d856185 100644 --- a/addons/website_quote/views/website_quote_templates.xml +++ b/addons/website_quote/views/website_quote_templates.xml @@ -145,6 +145,20 @@ + + diff --git a/addons/website_sale/views/res_config_settings_views.xml b/addons/website_sale/views/res_config_settings_views.xml index 28e73649064..bb9b702b86a 100644 --- a/addons/website_sale/views/res_config_settings_views.xml +++ b/addons/website_sale/views/res_config_settings_views.xml @@ -11,7 +11,7 @@ Show the automatic invoices setting in sales settings. -->
+ attrs="{'invisible': ['|', ('default_invoice_policy', '!=', 'order'), '&', ('portal_confirmation_pay', '=', False), ('module_website_quote', '=', False)]}">