[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
This commit is contained in:
Joren Van Onder
2018-06-28 18:47:51 +02:00
committed by Romain Derie
parent 803162537b
commit 2fa469c5e5
15 changed files with 180 additions and 90 deletions
+19 -10
View File
@@ -200,16 +200,11 @@ class CustomerPortal(CustomerPortal):
] ]
return request.make_response(pdf, headers=pdfhttpheaders) return request.make_response(pdf, headers=pdfhttpheaders)
def _portal_quote_user_can_accept(self, order_id): def _portal_quote_user_can_accept(self, order):
return request.env['ir.config_parameter'].sudo().get_param('sale.sale_portal_confirmation_options', default='none') in ('pay', 'sign') 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) @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): 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: try:
order_sudo = self._order_check_access(res_id, access_token=access_token) order_sudo = self._order_check_access(res_id, access_token=access_token)
except AccessError: except AccessError:
@@ -217,15 +212,29 @@ class CustomerPortal(CustomerPortal):
if order_sudo.state != 'sent': if order_sudo.state != 'sent':
return {'error': _('Order is not in a state requiring customer validation.')} 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( _message_post_helper(
res_model='sale.order', res_model='sale.order',
res_id=order_sudo.id, res_id=order_sudo.id,
message=_('Order signed by %s') % (partner_name,), 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 {})) **({'token': access_token} if access_token else {}))
return { 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 ''), 'redirect_url': '/my/orders/%s?%s' % (order_sudo.id, access_token and 'access_token=%s' % order_sudo.access_token or ''),
} }
+9 -1
View File
@@ -69,8 +69,16 @@
% set is_online = access_action and access_action['type'] == 'ir.actions.act_url' % 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 doc_name = 'quotation' if object.state in ('draft', 'sent') else 'order confirmation'
% set pay_sign_name = object.get_portal_confirmation_action() % 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_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: % if object.partner_id.parent_id:
(<i>${object.partner_id.parent_id.name}</i>) (<i>${object.partner_id.parent_id.name}</i>)
+2
View File
@@ -8,3 +8,5 @@ class ResCompany(models.Model):
_inherit = "res.company" _inherit = "res.company"
sale_note = fields.Text(string='Default Terms and Conditions', translate=True) 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')
+6 -17
View File
@@ -18,12 +18,9 @@ class ResConfigSettings(models.TransientModel):
module_sale_margin = fields.Boolean("Margins") module_sale_margin = fields.Boolean("Margins")
group_sale_layout = fields.Boolean("Sections on Sales Orders", implied_group='sale.group_sale_layout') 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') group_warning_sale = fields.Boolean("Sale Order Warnings", implied_group='sale.group_warning_sale')
portal_confirmation = fields.Boolean('Online Signature & Payment') portal_confirmation_sign = fields.Boolean(related='company_id.portal_confirmation_sign', string='Digital Signature')
portal_confirmation_options = fields.Selection([ portal_confirmation_pay = fields.Boolean(related='company_id.portal_confirmation_pay', string='Electronic Payment')
('sign', 'Signature'), module_sale_payment = fields.Boolean("Sale Payment", help='Technical field implied by user choice of portal_confirmation_pay.')
('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')
module_website_quote = fields.Boolean("Quotations Templates") module_website_quote = fields.Boolean("Quotations Templates")
group_sale_delivery_address = fields.Boolean("Customer Addresses", implied_group='sale.group_delivery_invoice_address') 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 = fields.Boolean("Multiple Sales Prices per Product")
@@ -131,16 +128,9 @@ class ResConfigSettings(models.TransientModel):
'group_pricelist_item': False, 'group_pricelist_item': False,
}) })
@api.onchange('portal_confirmation') @api.onchange('portal_confirmation_pay')
def _onchange_portal_confirmation(self): def _onchange_portal_confirmation_pay(self):
if not self.portal_confirmation: if self.portal_confirmation_pay:
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':
self.module_sale_payment = True self.module_sale_payment = True
@api.model @api.model
@@ -152,6 +142,5 @@ class ResConfigSettings(models.TransientModel):
multi_sales_price=sale_pricelist_setting in ['percentage', 'formula'], 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, multi_sales_price_method=sale_pricelist_setting in ['percentage', 'formula'] and sale_pricelist_setting or False,
sale_pricelist_setting=sale_pricelist_setting, sale_pricelist_setting=sale_pricelist_setting,
portal_confirmation=ICPSudo.get_param('sale.sale_portal_confirmation_options', default=False) in ('pay', 'sign'),
) )
return res return res
+17 -1
View File
@@ -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')) 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') 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') product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
def _compute_portal_url(self): def _compute_portal_url(self):
@@ -488,6 +491,8 @@ class SaleOrder(models.Model):
orders = self.filtered(lambda s: s.state in ['cancel', 'sent']) orders = self.filtered(lambda s: s.state in ['cancel', 'sent'])
return orders.write({ return orders.write({
'state': 'draft', 'state': 'draft',
'signature': False,
'signed_by': False,
}) })
@api.multi @api.multi
@@ -673,7 +678,18 @@ class SaleOrder(models.Model):
return self.get_share_url() return self.get_share_url()
def get_portal_confirmation_action(self): 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 @api.multi
def _notify_get_groups(self, message, groups): def _notify_get_groups(self, message, groups):
@@ -202,6 +202,19 @@
<strong>Fiscal Position Remark:</strong> <strong>Fiscal Position Remark:</strong>
<span t-field="doc.fiscal_position_id.note"/> <span t-field="doc.fiscal_position_id.note"/>
</p> </p>
<div t-if="doc.signature" class="row mt32" name="signature">
<div class="col-xs-offset-8 col-xs-4">
<strong>Signature</strong>
</div>
<div class="col-xs-offset-8 col-xs-4">
<img t-att-src="'data:image/png;base64,%s' % to_text(doc.signature)" style="max-height: 4cm; max-width: 8cm;"/>
</div>
<div class="col-xs-offset-8 col-xs-4 text-center">
<p t-field="doc.signed_by"/>
</div>
</div>
<div class="oe_structure"/> <div class="oe_structure"/>
</div> </div>
</t> </t>
+25 -18
View File
@@ -139,6 +139,31 @@
</div> </div>
<h2>Quotations &amp; Orders</h2> <h2>Quotations &amp; Orders</h2>
<div class="row mt16 o_settings_container"> <div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box" id="sale_config_online_confirmation_sign">
<div class="o_setting_left_pane">
<field name="portal_confirmation_sign"/>
</div>
<div class="o_setting_right_pane">
<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
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="sale_config_online_confirmation_pay">
<div class="o_setting_left_pane">
<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-xs-12 col-md-6 o_setting_box"> <div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane"> <div class="o_setting_left_pane">
</div> </div>
@@ -154,24 +179,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-xs-12 col-md-6 o_setting_box" id="sale_config_online_confirmation">
<div class="o_setting_left_pane">
<field name="portal_confirmation"/>
</div>
<div class="o_setting_right_pane">
<field name="module_sale_payment" invisible="1"/>
<label for="portal_confirmation"/>
<div class="text-muted">
Let your customers sign &amp; pay online
</div>
<div class="content-group" attrs="{'invisible': [('portal_confirmation', '=', False)]}">
<div class="mt16">
<field name="portal_confirmation_options" class="o_light_label" widget="radio"
attrs="{'required': [('portal_confirmation', '=', True)]}"/>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box"> <div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane"> <div class="o_setting_left_pane">
<field name="module_website_quote"/> <field name="module_website_quote"/>
+2 -2
View File
@@ -5,8 +5,8 @@
<field name="model">res.config.settings</field> <field name="model">res.config.settings</field>
<field name="inherit_id" ref="sale.res_config_settings_view_form" /> <field name="inherit_id" ref="sale.res_config_settings_view_form" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='portal_confirmation_options']/.." position="after"> <xpath expr="//label[@for='portal_confirmation_pay']/.." position="inside">
<div attrs="{'invisible': [('portal_confirmation_options', '!=', 'pay')]}"> <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"/> <button name='%(payment.action_payment_acquirer)d' icon="fa-arrow-right" type="action" string="Payment Acquirers" class="btn-link"/>
</div> </div>
</xpath> </xpath>
+4 -5
View File
@@ -24,11 +24,10 @@ class CustomerPortal(CustomerPortal):
return request.redirect('/quote/%s/%s' % (order, access_token or '')) return request.redirect('/quote/%s/%s' % (order, access_token or ''))
return super(CustomerPortal, self).portal_order_page(order=order, access_token=access_token, **kw) return super(CustomerPortal, self).portal_order_page(order=order, access_token=access_token, **kw)
def _portal_quote_user_can_accept(self, order_id): def _portal_quote_user_can_accept(self, order):
result = super(CustomerPortal, self)._portal_quote_user_can_accept(order_id) result = super(CustomerPortal, self)._portal_quote_user_can_accept(order)
order_sudo = request.env['sale.order'].sudo().browse(order_id)
# either use quote template settings or fallback on default behavior # 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): class sale_quote(http.Controller):
@@ -85,7 +84,7 @@ class sale_quote(http.Controller):
'partner_id': order_sudo.partner_id.id, '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([ domain = expression.AND([
['&', ('website_published', '=', True), ('company_id', '=', order_sudo.company_id.id)], ['&', ('website_published', '=', True), ('company_id', '=', order_sudo.company_id.id)],
['|', ('specific_countries', '=', False), ('country_ids', 'in', [order_sudo.partner_id.country_id.id])] ['|', ('specific_countries', '=', False), ('country_ids', 'in', [order_sudo.partner_id.country_id.id])]
+42 -16
View File
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
from odoo import api, fields, models, _ from odoo import api, fields, models, _
from odoo.tools.translate import html_translate from odoo.tools.translate import html_translate
from odoo.addons import decimal_precision as dp from odoo.addons import decimal_precision as dp
from odoo.exceptions import ValidationError
from werkzeug.urls import url_encode 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) template = self.env.ref('website_quote.website_quote_template_default', raise_if_not_found=False)
return template and template.active and template or 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() default_template = self._get_default_template()
if self.template_id: if default_template:
return self.template_id.require_payment return default_template.require_signature
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
else: 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( template_id = fields.Many2one(
'sale.quote.template', 'Quotation Template', 'sale.quote.template', 'Quotation Template',
@@ -75,12 +79,12 @@ class SaleOrder(models.Model):
amount_undiscounted = fields.Float( amount_undiscounted = fields.Float(
'Amount Before Discount', compute='_compute_amount_undiscounted', digits=0) 'Amount Before Discount', compute='_compute_amount_undiscounted', digits=0)
quote_viewed = fields.Boolean('Quotation Viewed') quote_viewed = fields.Boolean('Quotation Viewed')
require_payment = fields.Selection([ require_signature = fields.Boolean('Digital Signature', default=_get_default_require_signature,
(0, 'Online Signature'), states={'sale': [('readonly', True)], 'done': [('readonly', True)]},
(1, 'Online Payment')], default=_get_default_online_payment, string='Confirmation Mode', help='Request a digital signature to the customer in order to confirm orders automatically.')
help="Choose how you want to confirm an order to launch the delivery process. You can either " require_payment = fields.Boolean('Electronic Payment', default=_get_default_require_payment,
"request a digital signature or an upfront payment. With a digital signature, you can " states={'sale': [('readonly', True)], 'done': [('readonly', True)]},
"request the payment when issuing the invoice.") help='Request an electronic payment to the customer in order to confirm orders automatically.')
@api.multi @api.multi
@api.returns('self', lambda value: value.id) @api.returns('self', lambda value: value.id)
@@ -113,6 +117,8 @@ class SaleOrder(models.Model):
@api.onchange('template_id') @api.onchange('template_id')
def onchange_template_id(self): def onchange_template_id(self):
if not self.template_id: if not self.template_id:
self.require_signature = False
self.require_payment = False
return return
template = self.template_id.with_context(lang=self.partner_id.lang) 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.validity_date = fields.Date.to_string(datetime.now() + timedelta(template.number_of_days))
self.website_description = template.website_description self.website_description = template.website_description
self.require_signature = template.require_signature
self.require_payment = template.require_payment self.require_payment = template.require_payment
if template.note: if template.note:
self.note = 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 @api.multi
def open_quotation(self): def open_quotation(self):
self.ensure_one() self.ensure_one()
@@ -209,10 +222,23 @@ class SaleOrder(models.Model):
def get_portal_confirmation_action(self): def get_portal_confirmation_action(self):
""" Template override default behavior of pay / sign chosen in sales settings """ """ 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: if self.template_id:
return 'pay' if self.require_payment == 1 else 'sign' 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() 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 @api.multi
def action_confirm(self): def action_confirm(self):
res = super(SaleOrder, self).action_confirm() res = super(SaleOrder, self).action_confirm()
+10 -7
View File
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # 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.tools.translate import html_translate
from odoo.addons import decimal_precision as dp from odoo.addons import decimal_precision as dp
from odoo.exceptions import ValidationError
class SaleQuoteTemplate(models.Model): 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) options = fields.One2many('sale.quote.option', 'template_id', 'Optional Products Lines', copy=True)
number_of_days = fields.Integer('Quotation Duration', number_of_days = fields.Integer('Quotation Duration',
help='Number of days for the validity date computation of the quotation') help='Number of days for the validity date computation of the quotation')
require_payment = fields.Selection([ require_signature = fields.Boolean('Digital Signature', default=True, help='Request a digital signature to the customer in order to confirm orders automatically.')
(0, 'Online Signature'), require_payment = fields.Boolean('Electronic Payment', help='Request an electronic payment to the customer in order to confirm orders automatically.')
(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.")
mail_template_id = fields.Many2one( mail_template_id = fields.Many2one(
'mail.template', 'Confirmation Mail', 'mail.template', 'Confirmation Mail',
domain=[('model', '=', 'sale.order')], domain=[('model', '=', 'sale.order')],
help="This e-mail template will be sent on confirmation. Leave empty to send nothing.") 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.") 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 @api.multi
def open_template(self): def open_template(self):
self.ensure_one() self.ensure_one()
@@ -37,7 +37,8 @@
</div> </div>
</xpath> </xpath>
<xpath expr="//field[@name='client_order_ref']" position="after"> <xpath expr="//field[@name='client_order_ref']" position="after">
<field name="require_payment" widget="radio"/> <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="website_description" invisible="1"/>
</xpath> </xpath>
</field> </field>
@@ -94,7 +94,8 @@
</page> </page>
<page string="Confirmation"> <page string="Confirmation">
<group> <group>
<field name="require_payment" widget="radio"/> <field name="require_signature"/>
<field name="require_payment"/>
<field name="mail_template_id"/> <field name="mail_template_id"/>
</group> </group>
</page> </page>
@@ -145,6 +145,20 @@
</section> </section>
</template> </template>
<template id="signature" name="Signature">
<div t-if="quotation.signature" class="row mt64" name="signature">
<div class="col-xs-offset-8 col-xs-4">
<strong>Signature</strong>
</div>
<div class="col-xs-offset-8 col-xs-4">
<img t-att-src="'data:image/png;base64,%s' % to_text(quotation.signature)" style="max-height: 4cm; max-width: 8cm;"/>
</div>
<div class="col-xs-offset-8 col-xs-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"> <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"> <xpath expr="//div[@id='quote_qty']" position="replace">
<div class="input-group oe_website_spinner"> <div class="input-group oe_website_spinner">
@@ -202,11 +216,11 @@
<div class="col-md-3 hidden-print"> <div class="col-md-3 hidden-print">
<div class="bs-sidebar"> <div class="bs-sidebar">
<div class="text-center" t-if="quotation.state in ('draft', 'sent', 'waiting_date')" style="padding: 10px"> <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.require_payment" class="btn btn-primary btn-block" data-toggle="modal" data-target="#modalaccept"> <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">
<span class="fa fa-check"> Accept Order</span> <span class="fa fa-check"> Accept Order</span>
</a> </a>
<a role="button" t-if="order_valid and quotation.require_payment" class="btn btn-primary btn-block" data-toggle="modal" data-target="#modalaccept"> <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">
<span class="fa fa-check"> Accept &amp; Pay</span> <span class="fa fa-check"> <t t-if="not quotation.signature">Accept &amp; </t>Pay</span>
</a> </a>
<a role="button" t-if="not order_valid" href="#discussion" class="btn btn-info btn-block o_normalwhitespace"> <a role="button" t-if="not order_valid" href="#discussion" class="btn btn-info btn-block o_normalwhitespace">
<strong>This offer expired!</strong><br/> <strong>This offer expired!</strong><br/>
@@ -218,7 +232,7 @@
</a> </a>
</div> </div>
</div> </div>
<div class="text-center" t-if="need_payment or (quotation.state in ('sale') and quotation.require_payment and tx_state != 'done')" style="padding: 10px"> <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"> <a role="button" class="btn btn-primary btn-block" data-toggle="modal" data-target="#modalaccept">
<span class="fa fa-check"> Pay Now</span> <span class="fa fa-check"> Pay Now</span>
</a> </a>
@@ -301,7 +315,7 @@
<!-- modal relative to the actions Accept/Reject/Cancel --> <!-- modal relative to the actions Accept/Reject/Cancel -->
<div role="dialog" class="modal fade" id="modalaccept"> <div role="dialog" class="modal fade" id="modalaccept">
<div class="modal-dialog" t-if="not quotation.require_payment and not need_payment"> <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"> <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()"/> <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<header class="modal-header"> <header class="modal-header">
@@ -326,7 +340,7 @@
</form> </form>
</div> </div>
<div class="modal-dialog modal-content" t-if="quotation.require_payment or need_payment"> <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"> <header class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&amp;times;</button> <button type="button" class="close" data-dismiss="modal" aria-label="Close">&amp;times;</button>
<h4 class="modal-title">Validate Order</h4> <h4 class="modal-title">Validate Order</h4>
@@ -386,17 +400,17 @@
<t t-call="website_quote.so_quotation_content"/> <t t-call="website_quote.so_quotation_content"/>
<div class="text-center hidden-print mt16 mb16" id="quotation_toolbar" t-if="quotation.state in ('draft', 'sent', 'waiting_date')"> <div class="text-center hidden-print 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.require_payment"> <a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-if="order_valid and not quotation.has_to_be_paid()">
<span class="fa fa-check"> Accept</span> <span class="fa fa-check"> Accept</span>
</a> </a>
<a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-if="order_valid and quotation.require_payment"> <a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-if="order_valid and quotation.has_to_be_paid()">
<span class="fa fa-check"> Accept &amp; Pay</span> <span class="fa fa-check"> Accept &amp; Pay</span>
</a> </a>
<a role="button" class="btn btn-danger" data-toggle="modal" data-target="#modaldecline"> <a role="button" class="btn btn-danger" data-toggle="modal" data-target="#modaldecline">
<span class="fa fa-times"> Reject</span> <span class="fa fa-times"> Reject</span>
</a> </a>
</div> </div>
<div class="text-center" t-if="need_payment or (quotation.state in ('sale') and quotation.require_payment and tx_state != 'done')" style="padding: 10px"> <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"> <a role="button" class="btn btn-primary" data-toggle="modal" data-target="#modalaccept">
<span class="fa fa-check"> Pay Now</span> <span class="fa fa-check"> Pay Now</span>
</a> </a>
@@ -535,6 +549,8 @@
<div class="oe_structure"/> <div class="oe_structure"/>
<!-- Pricing --> <!-- Pricing -->
<t t-call="website_quote.pricing"/> <t t-call="website_quote.pricing"/>
<!-- Signature -->
<t t-call="website_quote.signature"/>
<!-- Options --> <!-- Options -->
<t t-call="website_quote.optional_products"/> <t t-call="website_quote.optional_products"/>
</template> </template>
@@ -11,7 +11,7 @@
Show the automatic invoices setting in sales settings. Show the automatic invoices setting in sales settings.
--> -->
<div class="col-xs-12 col-md-6 o_setting_box" <div class="col-xs-12 col-md-6 o_setting_box"
attrs="{'invisible': ['|', ('default_invoice_policy', '!=', 'order'), '&amp;', ('portal_confirmation_options', '!=', 'pay'), ('module_website_quote', '=', False)]}"> attrs="{'invisible': ['|', ('default_invoice_policy', '!=', 'order'), '&amp;', ('portal_confirmation_pay', '=', False), ('module_website_quote', '=', False)]}">
<div class="o_setting_left_pane"> <div class="o_setting_left_pane">
<field name="automatic_invoice" nolabel="1"/> <field name="automatic_invoice" nolabel="1"/>
</div> </div>