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)]}">