summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-01-25 15:41:45 +0100
committerChristophe Simonis <chs@odoo.com>2018-01-25 15:41:45 +0100
commitcb50970f3f88c5e5e4f0608c6d07e66eb4698eb9 (patch)
treefae668c499403db72b172b3c8802b869b1040ae7
parentffaca543c47adf5d0286ff8e9eeb99b193948679 (diff)
parenteefe879a37aba221bda84adfd880fb7876955920 (diff)
[MERGE] forward port branch 11.0 up to eefe879a37a
-rw-r--r--addons/account/models/account_invoice.py4
-rw-r--r--addons/account/models/account_move.py2
-rw-r--r--addons/account/views/account_portal_templates.xml8
-rw-r--r--addons/account_payment/controllers/payment.py10
-rw-r--r--addons/account_payment/views/account_portal_templates.xml44
-rw-r--r--addons/hr/models/hr.py12
-rw-r--r--addons/l10n_fr/migrations/2.0/post-migrate_tags_on_taxes.py8
-rw-r--r--addons/mass_mailing/models/mass_mailing.py2
-rw-r--r--addons/payment/models/payment_acquirer.py6
-rw-r--r--addons/payment/static/src/css/portal_payment.css9
-rw-r--r--addons/payment/static/src/js/payment_form.js156
-rw-r--r--addons/payment/static/src/js/payment_portal.js3
-rw-r--r--addons/payment/static/src/less/payment_form.less7
-rw-r--r--addons/payment/views/payment_portal_templates.xml28
-rw-r--r--addons/payment/views/payment_templates.xml13
-rw-r--r--addons/payment/views/payment_views.xml2
-rw-r--r--addons/payment_authorize/controllers/main.py29
-rw-r--r--addons/payment_authorize/models/authorize_request.py5
-rw-r--r--addons/payment_authorize/views/payment_authorize_templates.xml2
-rw-r--r--addons/payment_ogone/controllers/main.py4
-rw-r--r--addons/payment_ogone/views/payment_ogone_templates.xml2
-rw-r--r--addons/payment_stripe/controllers/main.py4
-rw-r--r--addons/payment_stripe/static/src/js/stripe.js8
-rw-r--r--addons/payment_stripe/views/payment_stripe_templates.xml2
-rw-r--r--addons/point_of_sale/models/pos_order.py13
-rw-r--r--addons/point_of_sale/security/ir.model.access.csv2
-rw-r--r--addons/portal/static/src/xml/portal_signature.xml4
-rw-r--r--addons/project/models/res_company.py1
-rw-r--r--addons/project/models/res_config_settings.py5
-rw-r--r--addons/project/views/res_config_settings_views.xml4
-rw-r--r--addons/resource/models/resource.py2
-rw-r--r--addons/sale/views/sale_portal_templates.xml8
-rw-r--r--addons/sale_payment/controllers/payment.py10
-rw-r--r--addons/sale_payment/models/payment.py6
-rw-r--r--addons/sale_payment/views/sale_portal_templates.xml45
-rw-r--r--addons/sale_timesheet/models/account.py5
-rw-r--r--addons/sms/wizard/send_sms.py8
-rw-r--r--addons/stock/models/stock_move.py13
-rw-r--r--addons/stock/models/stock_move_line.py2
-rw-r--r--addons/stock/tests/test_move2.py45
-rw-r--r--addons/stock/views/stock_picking_views.xml3
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js32
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js203
-rw-r--r--addons/web/static/src/js/widgets/debug_manager.js4
-rw-r--r--addons/web/static/src/less/kanban_view.less4
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js32
-rw-r--r--addons/web_editor/static/src/js/widgets/ace.js2
-rw-r--r--addons/website_blog/models/website_blog.py11
-rw-r--r--addons/website_event_track/controllers/main.py4
-rw-r--r--addons/website_event_track/views/event_track_templates.xml4
-rw-r--r--addons/website_quote/controllers/main.py1
-rw-r--r--addons/website_quote/models/sale_order.py5
-rw-r--r--addons/website_quote/views/website_quote_templates.xml58
-rw-r--r--addons/website_sale/controllers/main.py10
-rw-r--r--addons/website_sale/models/__init__.py1
-rw-r--r--addons/website_sale/models/payment.py25
-rw-r--r--addons/website_sale/views/templates.xml4
-rw-r--r--addons/website_sale_delivery/controllers/main.py6
-rw-r--r--addons/website_sale_delivery/static/src/js/website_sale_delivery.js2
-rw-r--r--doc/cla/corporate/VanRoey.md2
-rw-r--r--doc/cla/corporate/vauxoo.md1
-rw-r--r--doc/cla/individual/sergeiruzkiicode.md11
-rw-r--r--odoo/addons/base/data/res_country_data.xml4
-rw-r--r--odoo/addons/base/data/res_currency_data.xml11
-rw-r--r--odoo/addons/base/data/res_currency_rate_demo.xml6
-rw-r--r--odoo/addons/base/models/res_users.py8
-rw-r--r--odoo/http.py16
67 files changed, 712 insertions, 301 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index b89c22d529f..6461db208b4 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -1340,6 +1340,7 @@ class AccountInvoice(models.Model):
values['state'] = 'draft'
values['number'] = False
values['origin'] = invoice.number
+ values['payment_term_id'] = False
values['refund_invoice_id'] = invoice.id
if date:
@@ -1725,6 +1726,7 @@ class AccountPaymentTerm(models.Model):
def compute(self, value, date_ref=False):
date_ref = date_ref or fields.Date.today()
amount = value
+ sign = value < 0 and -1 or 1
result = []
if self.env.context.get('currency_id'):
currency = self.env['res.currency'].browse(self.env.context['currency_id'])
@@ -1732,7 +1734,7 @@ class AccountPaymentTerm(models.Model):
currency = self.env.user.company_id.currency_id
for line in self.line_ids:
if line.value == 'fixed':
- amt = currency.round(line.value_amount)
+ amt = sign * currency.round(line.value_amount)
elif line.value == 'percent':
amt = currency.round(value * (line.value_amount / 100.0))
elif line.value == 'balance':
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 5018b1e28e2..33c7c376e5d 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -950,7 +950,7 @@ class AccountMoveLine(models.Model):
elif self._context.get('skip_full_reconcile_check') == 'amount_currency_only':
field = 'amount_residual_currency'
#target the pair of move in self that are the oldest
- sorted_moves = sorted(self, key=lambda a: a.date)
+ sorted_moves = sorted(self, key=lambda a: a.date_maturity or a.date)
debit = credit = False
for aml in sorted_moves:
if credit and debit:
diff --git a/addons/account/views/account_portal_templates.xml b/addons/account/views/account_portal_templates.xml
index 51ac3563c2e..3bf6aa8dd7f 100644
--- a/addons/account/views/account_portal_templates.xml
+++ b/addons/account/views/account_portal_templates.xml
@@ -253,8 +253,8 @@
</template>
<template id="portal_invoice_error" name="Invoice error/warning display">
- <div class="row">
- <div t-att-class="'col-md-6 col-md-offset-3 text-center alert alert-dismissable %s' % ('alert-danger' if error else 'alert-warning')">
+ <div class="row mr16">
+ <div t-attf-class="'col-md-12 mr16 ml16 alert alert-dismissable' #{'alert-danger' if error else 'alert-warning'}">
<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.
@@ -264,8 +264,8 @@
</template>
<template id="portal_invoice_success" name="Invoice success display">
- <div class="row">
- <div class="col-md-6 col-md-offset-3 text-center alert alert-dismissable alert-success">
+ <div class="row mr16">
+ <div class="col-md-12 mr16 ml16 alert alert-dismissable alert-success">
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
</div>
</div>
diff --git a/addons/account_payment/controllers/payment.py b/addons/account_payment/controllers/payment.py
index 80924b910bd..2916a512b51 100644
--- a/addons/account_payment/controllers/payment.py
+++ b/addons/account_payment/controllers/payment.py
@@ -94,9 +94,9 @@ class PaymentPortal(http.Controller):
# proceed to the payment
res = tx.confirm_invoice_token()
- if res is not True:
- params['error'] = res
- return request.redirect(_build_url_w_params(error_url, params))
-
- params['success'] = 'pay_invoice'
+ if tx.state != 'authorized' or not tx.acquirer_id.capture_manually:
+ if res is not True:
+ params['error'] = res
+ return request.redirect(_build_url_w_params(error_url, params))
+ params['success'] = 'pay_invoice'
return request.redirect(_build_url_w_params(success_url, params))
diff --git a/addons/account_payment/views/account_portal_templates.xml b/addons/account_payment/views/account_portal_templates.xml
index 46e745e146b..37366eba917 100644
--- a/addons/account_payment/views/account_portal_templates.xml
+++ b/addons/account_payment/views/account_portal_templates.xml
@@ -12,6 +12,36 @@
</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">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ Your payment has been authorized.
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ </template>
+
+
<template id="portal_invoice_page_inherit_payment" name="Payment on My Invoices" inherit_id="account.portal_invoice_page">
<xpath expr="//div[@id='o_portal_account_actions']/a" position="before">
<t t-set="tx_ids" t-value="invoice.payment_tx_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))"/>
@@ -28,6 +58,13 @@
<i class="fa fa-check-circle"/> Paid
</a>
</xpath>
+ <xpath expr="//t[@t-call='account.portal_invoice_success']" position="after">
+ <div t-if="invoice.payment_tx_ids and invoice.amount_total and not success and not error" class="o_account_payment_tx_status" t-att-data-invoice-id="invoice.id">
+ <t t-call="account_payment.payment_confirmation_status">
+ <t t-set="payment_tx_id" t-value="invoice.payment_tx_id"/>
+ </t>
+ </div>
+ </xpath>
<xpath expr="//div[hasclass('panel-body')]" position="after">
<div class="panel-body" t-if="not tx_ids and invoice.state == 'open' and invoice.amount_total" id="portal_pay">
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="col-md-offset-3 col-md-6">
@@ -35,8 +72,8 @@
<t t-call="payment.payment_tokens_list">
<t t-set="mode" t-value="'payment'"/>
<t t-set="partner_id" t-value="invoice.partner_id.id"/>
- <t t-set="success_url" t-value="'/my/invoices/%s?%s' % (invoice.id, keep_query())"/>
- <t t-set="error_url" t-value="'/my/invoices/%s?%s' % (invoice.id, keep_query())"/>
+ <t t-set="success_url" t-value="'/my/invoices/%s%s' % (invoice.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
+ <t t-set="error_url" t-value="'/my/invoices/%s%s' % (invoice.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="access_token" t-value="access_token or ''"/>
<t t-set="callback_method" t-value="''"/>
<t t-set="form_action" t-value="'/invoice/pay/' + str(invoice.id) + '/s2s_token_tx/'"/>
@@ -119,7 +156,8 @@
inherit_id="account.portal_invoice_success">
<xpath expr="//a[hasclass('close')]" position="after">
<t t-if="success == 'pay_invoice'">
- Invoice successfully paid.
+ <span t-if='invoice.payment_acquirer_id.done_msg' t-raw="invoice.payment_acquirer_id.done_msg"/>
+ <span t-if='invoice.payment_acquirer_id.post_msg' t-raw="invoice.payment_acquirer_id.post_msg"/>
</t>
</xpath>
</template>
diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py
index ea1716326a7..6a87d1f74f4 100644
--- a/addons/hr/models/hr.py
+++ b/addons/hr/models/hr.py
@@ -111,13 +111,15 @@ class Employee(models.Model):
active = fields.Boolean('Active', related='resource_id.active', default=True, store=True)
# private partner
address_home_id = fields.Many2one(
- 'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.')
+ 'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.',
+ groups="hr.group_hr_user")
is_address_home_a_company = fields.Boolean(
'The employee adress has a company linked',
compute='_compute_is_address_home_a_company',
+ groups="hr.group_hr_user",
)
country_id = fields.Many2one(
- 'res.country', 'Nationality (Country)')
+ 'res.country', 'Nationality (Country)', groups="hr.group_hr_user")
gender = fields.Selection([
('male', 'Male'),
('female', 'Female'),
@@ -139,9 +141,9 @@ class Employee(models.Model):
domain="[('partner_id', '=', address_home_id)]",
groups="hr.group_hr_user",
help='Employee bank salary account')
- permit_no = fields.Char('Work Permit No')
- visa_no = fields.Char('Visa No')
- visa_expire = fields.Date('Visa Expire Date')
+ permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user")
+ visa_no = fields.Char('Visa No', groups="hr.group_hr_user")
+ visa_expire = fields.Date('Visa Expire Date', groups="hr.group_hr_user")
# image: all image fields are base64 encoded and PIL-supported
image = fields.Binary(
diff --git a/addons/l10n_fr/migrations/2.0/post-migrate_tags_on_taxes.py b/addons/l10n_fr/migrations/2.0/post-migrate_tags_on_taxes.py
index a37770d2c1c..44367687c8f 100644
--- a/addons/l10n_fr/migrations/2.0/post-migrate_tags_on_taxes.py
+++ b/addons/l10n_fr/migrations/2.0/post-migrate_tags_on_taxes.py
@@ -1,7 +1,5 @@
-from openerp.modules.registry import RegistryManager
-
+# -*- coding: utf-8 -*-
+from odoo.addons.account.models.chart_template import migrate_tags_on_taxes
def migrate(cr, version):
- registry = RegistryManager.get(cr.dbname)
- from openerp.addons.account.models.chart_template import migrate_tags_on_taxes
- migrate_tags_on_taxes(cr, registry)
+ migrate_tags_on_taxes(cr, None)
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index 8dd42b61b67..598b2db8837 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -397,7 +397,7 @@ class MassMailing(models.Model):
mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True)
mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)],
default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id)
- mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name')
+ mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name', readonly=True, related_sudo=True)
mailing_domain = fields.Char(string='Domain', oldname='domain', default=[])
contact_list_ids = fields.Many2many('mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
string='Mailing Lists')
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index 1a3ba86034d..879c7b60d08 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -152,9 +152,9 @@ class PaymentAcquirer(models.Model):
"Use this field anywhere a small image is required.")
payment_icon_ids = fields.Many2many('payment.icon', string='Supported Payment Icons')
- payment_flow = fields.Selection(selection=[('s2s','The customer encode his payment details on the website.'),
- ('form', 'The customer is redirected to the website of the acquirer.')],
- default='form', required=True, string='Payment flow',
+ payment_flow = fields.Selection(selection=[('form', 'Redirection to the acquirer website'),
+ ('s2s','Payment from Odoo')],
+ default='form', required=True, string='Payment Flow',
help="""Note: Subscriptions does not take this field in account, it uses server to server by default.""")
def _search_is_tokenized(self, operator, value):
diff --git a/addons/payment/static/src/css/portal_payment.css b/addons/payment/static/src/css/portal_payment.css
index e9db1fd8d28..eeb40dbd3a3 100644
--- a/addons/payment/static/src/css/portal_payment.css
+++ b/addons/payment/static/src/css/portal_payment.css
@@ -14,6 +14,15 @@ div.card_placeholder {
transition: 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955);
pointer-events: none; }
+/* if s2s form not in bootstrap_formatting */
+div.o_card_brand_detail {
+ position: relative;
+}
+
+div.o_card_brand_detail div.card_placeholder {
+ right: 5px;
+}
+
div.amex {
background-image: url("/website_payment/static/src/img/amex.png");
background-repeat: no-repeat; }
diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js
index 5cfe9b1d88a..20216c4cdad 100644
--- a/addons/payment/static/src/js/payment_form.js
+++ b/addons/payment/static/src/js/payment_form.js
@@ -58,29 +58,34 @@ odoo.define('payment.payment_form', function (require) {
console.warn('payment_form: unset partner_id when adding new token; things could go wrong');
}
var form_data = this.getFormData(inputs_form);
- var empty_inputs = false;
+ var wrong_input = false;
inputs_form.toArray().forEach(function (element) {
- if (element.dataset.isRequired) {
- if (element.value.length === 0) {
+ //skip the check of non visible inputs
+ if ($(element).attr('type') == 'hidden') {
+ return true;
+ }
+ $(element).closest('div.form-group').removeClass('has-error');
+ $(element).siblings( ".o_invalid_field" ).remove();
+ //force check of forms validity (useful for Firefox that refill forms automatically on f5)
+ $(element).trigger("focusout");
+ if (element.dataset.isRequired && element.value.length === 0) {
$(element).closest('div.form-group').addClass('has-error');
- empty_inputs = true;
- }
- else {
- $(element).closest('div.form-group').removeClass('has-error');
- }
+ $(element).closest('div.form-group').append('<div style="color: red" class="o_invalid_field">' + _.str.escapeHTML("The value is invalid.") + '</div>');
+ wrong_input = true;
+ }
+ else if ($(element).closest('div.form-group').hasClass('has-error')) {
+ wrong_input = true;
+ $(element).closest('div.form-group').append('<div style="color: red" class="o_invalid_field">' + _.str.escapeHTML("The value is invalid.") + '</div>');
}
});
- if (empty_inputs) {
- this.displayError(
- _t('Missing values'),
- _t('<p>Please fill all the inputs required.</p>')
- );
+ if (wrong_input) {
return;
}
$(button).attr('disabled', true);
+ $(button).children('.fa-plus-circle').removeClass('fa-plus-circle')
$(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>');
var verify_validity = this.$el.find('input[name="verify_validity"]');
@@ -105,24 +110,30 @@ odoo.define('payment.payment_form', function (require) {
}
// if the server has returned false, we display an error
else {
- self.displayError(
- _t('Server Error'),
- _t("<p>We are not able to add your payment method at the moment.</p>"));
+ if (data.error) {
+ self.displayError(
+ '',
+ data.error);
+ } else { // if the server doesn't provide an error message
+ self.displayError(
+ _t('Server Error'),
+ _t('e.g. Your credit card details are wrong. Please verify.'));
+ }
}
// here we remove the 'processing' icon from the 'add a new payment' button
$(button).attr('disabled', false);
+ $(button).children('.fa').addClass('fa-plus-circle')
$(button).find('span.o_loader').remove();
}).fail(function (message, data) {
// if the rpc fails, pretty obvious
$(button).attr('disabled', false);
+ $(button).children('.fa').addClass('fa-plus-circle')
$(button).find('span.o_loader').remove();
self.displayError(
_t('Server Error'),
- _t("<p>We are not able to add your payment method at the moment.</p>") +
- "<p>" +
- (config.debug ? (data.data.message.replace(/\n/g, "<br />")): '') +
- "</p>"
+ _t("We are not able to add your payment method at the moment.") +
+ data.data.message
);
});
}
@@ -132,7 +143,7 @@ odoo.define('payment.payment_form', function (require) {
// if there's a prepare tx url set
if ($tx_url.length === 1) {
// if the user wants to save his credit card info
- var form_save_token = $('input[name="o_payment_form_save_token"]').prop('checked');
+ var form_save_token = acquirer_form.find('input[name="o_payment_form_save_token"]').prop('checked');
// then we call the route to prepare the transaction
ajax.jsonRpc($tx_url[0].value, 'call', {
'acquirer_id': parseInt(acquirer_id),
@@ -160,16 +171,14 @@ odoo.define('payment.payment_form', function (require) {
else {
self.displayError(
_t('Server Error'),
- _t("<p>We are not able to redirect you to the payment form.</p>")
+ _t("We are not able to redirect you to the payment form.")
);
}
}).fail(function (message, data) {
self.displayError(
_t('Server Error'),
- _t("<p>We are not able to redirect you to the payment form.</p>") +
- "<p>" +
- (config.debug ? (data.data.message.replace(/\n/g, "<br />")): '') +
- "</p>"
+ _t("We are not able to redirect you to the payment form.<") +
+ data.data.message
);
});
}
@@ -177,7 +186,7 @@ odoo.define('payment.payment_form', function (require) {
// we append the form to the body and send it.
this.displayError(
_t("Cannot set-up the payment"),
- _t("<p>We're unable to process your payment.</p>")
+ _t("We're unable to process your payment.")
);
}
}
@@ -188,7 +197,7 @@ odoo.define('payment.payment_form', function (require) {
else {
this.displayError(
_t('No payment method selected'),
- _t('<p>Please select a payment method.</p>')
+ _t('Please select a payment method.')
);
}
},
@@ -210,29 +219,36 @@ odoo.define('payment.payment_form', function (require) {
var inputs_form = $('input', acquirer_form);
var form_data = this.getFormData(inputs_form);
var ds = $('input[name="data_set"]', acquirer_form)[0];
- var empty_inputs = false;
-
+ var wrong_input = false;
+
inputs_form.toArray().forEach(function (element) {
- if (element.dataset.isRequired) {
- if (element.value.length === 0) {
+ //skip the check of non visible inputs
+ if ($(element).attr('type') == 'hidden') {
+ return true;
+ }
+ $(element).closest('div.form-group').removeClass('has-error');
+ $(element).siblings( ".o_invalid_field" ).remove();
+ //force check of forms validity (useful for Firefox that refill forms automatically on f5)
+ $(element).trigger("focusout");
+ if (element.dataset.isRequired && element.value.length === 0) {
$(element).closest('div.form-group').addClass('has-error');
- empty_inputs = true;
- }
- else {
- $(element).closest('div.form-group').removeClass('has-error');
- }
+ var message = '<div style="color: red" class="o_invalid_field">' + _.str.escapeHTML("The value is invalid.") + '</div>';
+ $(element).closest('div.form-group').append(message);
+ wrong_input = true;
+ }
+ else if ($(element).closest('div.form-group').hasClass('has-error')) {
+ wrong_input = true;
+ var message = '<div style="color: red" class="o_invalid_field">' + _.str.escapeHTML("The value is invalid.") + '</div>';
+ $(element).closest('div.form-group').append(message);
}
});
- if (empty_inputs) {
- this.displayError(
- _t('Missing values'),
- _t('<p>Please fill all the inputs required.</p>')
- );
+ if (wrong_input) {
return;
}
// We add a 'processing' icon into the 'add a new payment' button
$(button).attr('disabled', true);
+ $(button).children('.fa-plus-circle').removeClass('fa-plus-circle')
$(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>');
// we force the check when adding a card trough here
@@ -260,32 +276,37 @@ odoo.define('payment.payment_form', function (require) {
}
// if the server has returned false, we display an error
else {
- self.displayError(
- _t('Server Error'),
- _t("<p>We are not able to add your payment method at the moment.</p>")
- );
+ if (data.error) {
+ self.displayError(
+ '',
+ data.error);
+ } else { // if the server doesn't provide an error message
+ self.displayError(
+ _t('Server Error'),
+ _t('e.g. Your credit card details are wrong. Please verify.'));
+ }
}
// here we remove the 'processing' icon from the 'add a new payment' button
$(button).attr('disabled', false);
+ $(button).children('.fa').addClass('fa-plus-circle')
$(button).find('span.o_loader').remove();
}).fail(function (message, data) {
// if the rpc fails, pretty obvious
$(button).attr('disabled', false);
+ $(button).children('.fa').addClass('fa-plus-circle')
$(button).find('span.o_loader').remove();
self.displayError(
_t('Server error'),
- _t("<p>We are not able to add your payment method at the moment.</p>") +
- "<p>" +
- (config.debug ? (data.data.message.replace(/\n/g, "<br />")): '') +
- "</p>"
+ _t("We are not able to add your payment method at the moment.</p>") +
+ data.data.message
);
});
}
else {
this.displayError(
_t('No payment method selected'),
- _t('<p>Please select the option to add a new payment method.</p>')
+ _t('Please select the option to add a new payment method.')
);
}
},
@@ -309,7 +330,7 @@ odoo.define('payment.payment_form', function (require) {
}, function () {
self.displayError(
_t('Server Error'),
- _t("<p>We are not able to delete your payment method at the moment.</p>")
+ _t("We are not able to delete your payment method at the moment.")
);
});
};
@@ -323,7 +344,7 @@ odoo.define('payment.payment_form', function (require) {
// if there's records linked to this payment method
var content = '';
result[pm_id].forEach(function (sub) {
- content += '<p><a href="' + sub.url + '" title="' + _.str.escapeHTML(sub.description) + '">' + _.str.escapeHTML(sub.name) + '</a><p/>';
+ content += '<p><a href="' + sub.url + '" title="' + sub.description + '">' + sub.name + '</a><p/>';
});
content = $('<div>').html(_t('<p>This card is currently linked to the following records:<p/>') + content);
@@ -344,7 +365,7 @@ odoo.define('payment.payment_form', function (require) {
}, function (type, err) {
self.displayError(
_t('Server Error'),
- _t("<p>We are not able to delete your payment method at the moment.</p>") + (config.debug ? err.data.message : '')
+ _t("We are not able to delete your payment method at the moment.") + err.data.message
);
});
},
@@ -392,12 +413,27 @@ odoo.define('payment.payment_form', function (require) {
return $(element).data('acquirer-id');
},
displayError: function (title, message) {
- return new Dialog(null, {
- title: _t('Error: ') + title,
- size: 'medium',
- $content: message || "",
- buttons: [
- {text: _t('Ok'), close: true}]}).open();
+ var $checkedRadio = this.$('input[type="radio"]:checked'),
+ acquirerID = this.getAcquirerIdFromRadio($checkedRadio[0]),
+ $acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID);
+
+ if ($checkedRadio.length === 0) {
+ return new Dialog(null, {
+ title: _t('Error: ') + _.str.escapeHTML(title),
+ size: 'medium',
+ $content: "<p>" + (_.str.escapeHTML(message) || "") + "</p>" ,
+ buttons: [
+ {text: _t('Ok'), close: true}]}).open();
+ } else {
+ // removed if exist error message
+ this.$('#payment_error').remove();
+ var messageResult = '<div class="alert alert-danger mb4" id="payment_error">';
+ if (title != '') {
+ messageResult = messageResult + '<b>' + _.str.escapeHTML(title) + ':</b></br>';
+ }
+ messageResult = messageResult + _.str.escapeHTML(message) + '</div>';
+ $acquirerForm.append(messageResult);
+ }
},
getFormData: function ($form) {
var unindexed_array = $form.serializeArray();
diff --git a/addons/payment/static/src/js/payment_portal.js b/addons/payment/static/src/js/payment_portal.js
index d7397f1b04d..efab5f0a8e0 100644
--- a/addons/payment/static/src/js/payment_portal.js
+++ b/addons/payment/static/src/js/payment_portal.js
@@ -17,6 +17,7 @@ $(document).ready(function () {
if (valid_value) {
$(this).parent('.form-group').addClass('has-success');
$(this).parent('.form-group').removeClass('has-error');
+ $(this).siblings('.o_invalid_field').remove();
}
else {
$(this).parent('.form-group').addClass('has-error');
@@ -31,6 +32,7 @@ $(document).ready(function () {
if (valid_value) {
$(this).parent('.form-group').addClass('has-success');
$(this).parent('.form-group').removeClass('has-error');
+ $(this).siblings('.o_invalid_field').remove();
}
else {
$(this).parent('.form-group').addClass('has-error');
@@ -46,6 +48,7 @@ $(document).ready(function () {
if (valid_value) {
$(this).parent('.form-group').addClass('has-success');
$(this).parent('.form-group').removeClass('has-error');
+ $(this).siblings('.o_invalid_field').remove();
}
else {
$(this).parent('.form-group').addClass('has-error');
diff --git a/addons/payment/static/src/less/payment_form.less b/addons/payment/static/src/less/payment_form.less
index b5aeb2e928e..5e3bb794160 100644
--- a/addons/payment/static/src/less/payment_form.less
+++ b/addons/payment/static/src/less/payment_form.less
@@ -1,6 +1,6 @@
.o_payment_form {
label > input[type="radio"], input[type="checkbox"]{
- vertical-align: bottom;
+ vertical-align: top;
margin-right: 5px;
}
@@ -14,6 +14,7 @@
label {
font-weight: normal;
+ margin-top: 5px;
}
.panel-body:first-child {
@@ -35,16 +36,18 @@
}
.payment_icon_list {
+ position: relative;
li {
padding-left: 5px !important;
padding-right: 0px !important;
}
.more_option {
+ .o-position-absolute(@right: 0px, @bottom: -10px);
font-size:10px;
}
margin-top: 0px !important;
margin-bottom: -5px !important;
}
-} \ No newline at end of file
+}
diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml
index f599329e5e1..d2fa3c1c4cd 100644
--- a/addons/payment/views/payment_portal_templates.xml
+++ b/addons/payment/views/payment_portal_templates.xml
@@ -34,7 +34,7 @@
<!-- TDE FIXME: was customize_show=True -->
<template id="pay_meth_link" inherit_id="portal.portal_layout">
<xpath expr="//div[hasclass('o_portal_my_details')]" position="inside">
- <t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False)])">
+ <t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False), ('payment_flow', '=', 's2s')])">
<div class='manage_payment_method mt16'><a href="/my/payment_method">Manage your payment methods</a></div>
</t>
</xpath>
@@ -122,5 +122,31 @@
</t>
</template>
+ <template id="payment_confirmation_status">
+ <div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ Your payment has been authorized.
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ </template>
+
</data>
</odoo>
diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml
index d85ae8a135e..1b6246290fa 100644
--- a/addons/payment/views/payment_templates.xml
+++ b/addons/payment/views/payment_templates.xml
@@ -41,6 +41,7 @@
<div class="panel panel-default">
<t t-set="acquirers_count" t-value="(len(s2s_acquirers) if s2s_acquirers else 0) + (len(form_acquirers) if form_acquirers else 0)"/>
<t t-set="pms_count" t-value="len(pms) if pms else 0"/>
+ <t t-set="MAX_BRAND_LINE" t-value="3"/>
<t t-foreach="s2s_acquirers" t-as="acq">
<div class="panel-body">
<label>
@@ -65,13 +66,13 @@
<ul class="pull-right list-inline payment_icon_list">
<t t-set="i" t-value="0"/>
<t t-foreach="acq.payment_icon_ids" t-as="pm_icon">
- <li t-att-class="'' if (i &lt; 3) else 'hidden'">
+ <li t-att-class="'' if (i &lt; MAX_BRAND_LINE) else 'hidden'">
<span t-field="pm_icon.image_payment_form"
t-options='{"widget": "image", "alt-field": "name"}' />
</li>
- <li t-if="i==3" style="display:block;">
+ <li t-if="i==MAX_BRAND_LINE" style="display:block;">
<span class="pull-right more_option text-info">
- <a href="#" class="o_payment_form_pay_icon_more" data-toggle="tooltip" t-att-title="', '.join([opt.name for opt in acq.payment_icon_ids[3:]])">and more</a>
+ <a href="#" class="o_payment_form_pay_icon_more" data-toggle="tooltip" t-att-title="', '.join([opt.name for opt in acq.payment_icon_ids[MAX_BRAND_LINE:]])">and more</a>
</span>
</li>
<t t-set="i" t-value="i+1"/>
@@ -102,13 +103,13 @@
<ul class="pull-right list-inline payment_icon_list">
<t t-set="i" t-value="0"/>
<t t-foreach="acq.payment_icon_ids" t-as="pm_icon">
- <li t-att-class=" '' if (i &lt; 3) else 'hidden'">
+ <li t-att-class=" '' if (i &lt; MAX_BRAND_LINE) else 'hidden'">
<span t-field="pm_icon.image_payment_form"
t-options='{"widget": "image", "alt-field": "name"}' />
</li>
- <li t-if="i==3" style="display:block;">
+ <li t-if="i==MAX_BRAND_LINE" style="display:block;">
<span class="pull-right more_option text-info">
- <a href="#" class="o_payment_form_pay_icon_more" data-toggle="tooltip" t-att-title="', '.join([opt.name for opt in acq.payment_icon_ids[3:]])">and more</a>
+ <a href="#" class="o_payment_form_pay_icon_more" data-toggle="tooltip" t-att-title="', '.join([opt.name for opt in acq.payment_icon_ids[MAX_BRAND_LINE:]])">and more</a>
</span>
</li>
<t t-set="i" t-value="i+1"/>
diff --git a/addons/payment/views/payment_views.xml b/addons/payment/views/payment_views.xml
index 21f63f8cf14..6f536f270d5 100644
--- a/addons/payment/views/payment_views.xml
+++ b/addons/payment/views/payment_views.xml
@@ -83,7 +83,7 @@
<group name="acquirer_config">
<field name="journal_id" context="{'default_type': 'bank'}"/>
<field name="capture_manually" attrs="{'invisible': [('authorize_implemented', '=', False)]}"/>
- <field name="save_token" widget="radio" attrs="{'invisible': [('token_implemented', '=', False)]}"/>
+ <field name="save_token" widget="radio" attrs="{'invisible': ['|', ('token_implemented', '=', False), ('payment_flow', '=', 's2s')]}"/>
<field name="fees_active" attrs="{'invisible': [('fees_implemented', '=', False)]}"/>
<field name="fees_dom_fixed" attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_dom_var" attrs="{'invisible': [('fees_active', '=', False)]}"/>
diff --git a/addons/payment_authorize/controllers/main.py b/addons/payment_authorize/controllers/main.py
index 6b0aae9bf6a..9614327401a 100644
--- a/addons/payment_authorize/controllers/main.py
+++ b/addons/payment_authorize/controllers/main.py
@@ -3,8 +3,9 @@ import pprint
import logging
from werkzeug import urls, utils
-from odoo import http
+from odoo import http, _
from odoo.http import request
+from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
@@ -35,11 +36,35 @@ class AuthorizeController(http.Controller):
def authorize_s2s_create_json(self, **kwargs):
acquirer_id = int(kwargs.get('acquirer_id'))
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
+ if not kwargs.get('partner_id'):
+ kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
return acquirer.s2s_process(kwargs).id
@http.route(['/payment/authorize/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def authorize_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
- token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
+ token = False
+ acquirer = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id')))
+
+ try:
+ if not kwargs.get('partner_id'):
+ kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
+ token = acquirer.s2s_process(kwargs)
+ except ValidationError as e:
+ message = e.args[0]
+ if isinstance(message, dict) and 'missing_fields' in message:
+ msg = _("The transaction cannot be processed because some contact details are missing or invalid: ")
+ message = msg + ', '.join(message['missing_fields']) + '. '
+ if request.env.user._is_public():
+ message += _("Please sign in to complete your profile.")
+ # update message if portal mode = b2b
+ if request.env['ir.config_parameter'].sudo().get_param('auth_signup.allow_uninvited', 'False').lower() == 'false':
+ message += _("If you don't have any account, please ask your salesperson to update your profile. ")
+ else:
+ message += _("Please complete your profile. ")
+
+ return {
+ 'error': message
+ }
if not token:
res = {
diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py
index 18c535aedcd..af710b280ba 100644
--- a/addons/payment_authorize/models/authorize_request.py
+++ b/addons/payment_authorize/models/authorize_request.py
@@ -154,6 +154,11 @@ class AuthorizeAPI():
etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual'
billTo = etree.SubElement(payment_profile, "billTo")
etree.SubElement(billTo, "address").text = (partner.street or '' + (partner.street2 if partner.street2 else '')) or None
+
+ missing_fields = [partner._fields[field].string for field in ['city', 'country_id', 'zip'] if not partner[field]]
+ if missing_fields:
+ raise ValidationError({'missing_fields': missing_fields})
+
etree.SubElement(billTo, "city").text = partner.city
etree.SubElement(billTo, "state").text = partner.state_id.name or None
etree.SubElement(billTo, "zip").text = partner.zip
diff --git a/addons/payment_authorize/views/payment_authorize_templates.xml b/addons/payment_authorize/views/payment_authorize_templates.xml
index 1e56c309317..dcc88b9df37 100644
--- a/addons/payment_authorize/views/payment_authorize_templates.xml
+++ b/addons/payment_authorize/views/payment_authorize_templates.xml
@@ -51,7 +51,7 @@
<template id="authorize_s2s_form">
<input type="hidden" name="data_set" data-create-route="/payment/authorize/s2s/create_json_3ds"/>
- <div class="mt8">
+ <div t-attf-class="mt8 #{'' if bootstrap_formatting else 'o_card_brand_detail'}">
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number" data-is-required="true"/>
<div class="card_placeholder"></div>
diff --git a/addons/payment_ogone/controllers/main.py b/addons/payment_ogone/controllers/main.py
index 9c81761a70e..8680e99fda5 100644
--- a/addons/payment_ogone/controllers/main.py
+++ b/addons/payment_ogone/controllers/main.py
@@ -30,11 +30,15 @@ class OgoneController(http.Controller):
@http.route(['/payment/ogone/s2s/create_json'], type='json', auth='public', csrf=False)
def ogone_s2s_create_json(self, **kwargs):
+ if not kwargs.get('partner_id'):
+ kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
new_id = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
return new_id.id
@http.route(['/payment/ogone/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def ogone_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
+ if not kwargs.get('partner_id'):
+ kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
if not token:
diff --git a/addons/payment_ogone/views/payment_ogone_templates.xml b/addons/payment_ogone/views/payment_ogone_templates.xml
index aec5274981d..28dca693a4a 100644
--- a/addons/payment_ogone/views/payment_ogone_templates.xml
+++ b/addons/payment_ogone/views/payment_ogone_templates.xml
@@ -40,7 +40,7 @@
<input type="hidden" name="acquirer_id" t-att-value="id"/>
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
- <div class="mt8">
+ <div t-attf-class="mt8 #{'' if bootstrap_formatting else 'o_card_brand_detail'}">
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number" data-is-required="true"/>
<div class="card_placeholder"></div>
diff --git a/addons/payment_stripe/controllers/main.py b/addons/payment_stripe/controllers/main.py
index 61c1f003b59..f446375d429 100644
--- a/addons/payment_stripe/controllers/main.py
+++ b/addons/payment_stripe/controllers/main.py
@@ -15,6 +15,8 @@ class StripeController(http.Controller):
def stripe_s2s_create_json(self, **kwargs):
acquirer_id = int(kwargs.get('acquirer_id'))
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
+ if not kwargs.get('partner_id'):
+ kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
return acquirer.s2s_process(kwargs).id
@http.route(['/payment/stripe/s2s/create'], type='http', auth='public')
@@ -36,6 +38,8 @@ class StripeController(http.Controller):
@http.route(['/payment/stripe/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def stripe_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
+ if not kwargs.get('partner_id'):
+ kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
if not token:
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index d616ddfe0be..13f8f91bfcd 100644
--- a/addons/payment_stripe/static/src/js/stripe.js
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -120,6 +120,14 @@ odoo.define('payment_stripe.stripe', function(require) {
}).then(function (data) {
try { provider_form[0].innerHTML = data; } catch (e) {};
});
+ } else if (window.location.href.includes("/my/orders/")) {
+ var create_tx = ajax.jsonRpc('/pay/sale/' + acquirer_id + '/form_tx/', 'call', {
+ so_id: so_id,
+ access_token: access_token,
+ acquirer_id: acquirer_id
+ }).then(function (data) {
+ try { provider_form.innerHTML = data; } catch (e) {};
+ });
}
else {
var create_tx = ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', {
diff --git a/addons/payment_stripe/views/payment_stripe_templates.xml b/addons/payment_stripe/views/payment_stripe_templates.xml
index 6060bb1f758..2fc054f97ae 100644
--- a/addons/payment_stripe/views/payment_stripe_templates.xml
+++ b/addons/payment_stripe/views/payment_stripe_templates.xml
@@ -26,7 +26,7 @@
<template id="stripe_s2s_form">
<input type="hidden" name="data_set" data-create-route="/payment/stripe/s2s/create_json_3ds"/>
- <div class="mt8">
+ <div t-attf-class="mt8 #{'' if bootstrap_formatting else 'o_card_brand_detail'}">
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number" data-is-required="true"/>
<div class="card_placeholder"></div>
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index e7ce561d56f..ba856c4cfab 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -364,15 +364,22 @@ class PosOrder(models.Model):
for order in self:
aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids | order.invoice_id.move_id.line_ids
aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id.commercial_partner_id)
+
+ # Reconcile returns first
+ # to avoid mixing up the credit of a payment and the credit of a return
+ # in the receivable account
+ aml_returns = aml.filtered(lambda l: (l.journal_id.type == 'sale' and l.credit) or (l.journal_id.type != 'sale' and l.debit))
try:
- aml.reconcile()
+ aml_returns.reconcile()
+ (aml - aml_returns).reconcile()
except Exception:
# There might be unexpected situations where the automatic reconciliation won't
# work. We don't want the user to be blocked because of this, since the automatic
# reconciliation is introduced for convenience, not for mandatory accounting
# reasons.
- _logger.error('Reconciliation did not work for order %s', order.name)
- continue
+ # It may be interesting to have the Traceback logged anyway
+ # for debugging and support purposes
+ _logger.exception('Reconciliation did not work for order %s', order.name)
def _default_session(self):
return self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1)
diff --git a/addons/point_of_sale/security/ir.model.access.csv b/addons/point_of_sale/security/ir.model.access.csv
index 2d3215c627e..cf767d1962f 100644
--- a/addons/point_of_sale/security/ir.model.access.csv
+++ b/addons/point_of_sale/security/ir.model.access.csv
@@ -28,7 +28,7 @@ access_product_pricelist_manager,product.pricelist manager,product.model_product
access_product_pricelist_user,product.pricelist user,product.model_product_pricelist,group_pos_user,1,0,0,0
access_pos_session_user,pos.session user,model_pos_session,group_pos_user,1,1,1,0
access_pos_config_user,pos.config user,model_pos_config,group_pos_user,1,1,0,0
-access_pos_config_manager,pos.config user,model_pos_config,group_pos_manager,1,1,1,0
+access_pos_config_manager,pos.config manager,model_pos_config,group_pos_manager,1,1,1,1
access_product_category_pos_manager,pos.category manager,model_pos_category,group_pos_manager,1,1,1,1
access_product_category_pos_user,pos.category user,model_pos_category,group_pos_user,1,0,0,0
access_barcode_nomenclature_pos_user,barcode.nomenclature.pos.user,barcodes.model_barcode_nomenclature,group_pos_user,1,0,0,0
diff --git a/addons/portal/static/src/xml/portal_signature.xml b/addons/portal/static/src/xml/portal_signature.xml
index 4b0ca5344d1..3ef5c8f1ea0 100644
--- a/addons/portal/static/src/xml/portal_signature.xml
+++ b/addons/portal/static/src/xml/portal_signature.xml
@@ -15,8 +15,8 @@
</div>
<div id="o_portal_signature" class="panel-body" style="padding: 0"/>
</div>
- <div class="mt16 pull-right">
- <button type="submit" class="btn btn-primary o_portal_sign_submit"><t t-esc="widget.options.signLabel or 'Accept &amp; Sign'"/></button>
+ <div class="mt16 clearfix">
+ <button type="submit" class="btn btn-primary o_portal_sign_submit pull-right"><t t-esc="widget.options.signLabel or 'Accept &amp; Sign'"/></button>
</div>
</form>
</t>
diff --git a/addons/project/models/res_company.py b/addons/project/models/res_company.py
index 282b611ca75..bdcfa1b2c7e 100644
--- a/addons/project/models/res_company.py
+++ b/addons/project/models/res_company.py
@@ -8,6 +8,7 @@ class ResCompany(models.Model):
_inherit = 'res.company'
project_time_mode_id = fields.Many2one('product.uom', string='Project Time Unit',
+ default=lambda s: s.env.ref('product.product_uom_hour', raise_if_not_found=False),
help="This will set the unit of measure used in projects and tasks.\n"
"If you use the timesheet linked to projects, don't "
"forget to setup the right unit of measure in your employees.")
diff --git a/addons/project/models/res_config_settings.py b/addons/project/models/res_config_settings.py
index ea1eb675904..657a75ff6a1 100644
--- a/addons/project/models/res_config_settings.py
+++ b/addons/project/models/res_config_settings.py
@@ -11,3 +11,8 @@ class ResConfigSettings(models.TransientModel):
module_project_forecast = fields.Boolean(string="Forecasts")
group_subtask_project = fields.Boolean("Sub-tasks", implied_group="project.group_subtask_project")
group_project_rating = fields.Boolean("Use Rating on Project", implied_group='project.group_project_rating')
+ project_time_mode_id = fields.Many2one(
+ 'product.uom', related='company_id.project_time_mode_id', string='Project Time Unit',
+ help="This will set the unit of measure used in projects and tasks.\n"
+ "If you use the timesheet linked to projects, don't "
+ "forget to setup the right unit of measure in your employees.")
diff --git a/addons/project/views/res_config_settings_views.xml b/addons/project/views/res_config_settings_views.xml
index e11f4511284..0692e4c7298 100644
--- a/addons/project/views/res_config_settings_views.xml
+++ b/addons/project/views/res_config_settings_views.xml
@@ -60,6 +60,10 @@
<div class="text-muted">
Track time spent on projects and tasks
</div>
+ <div class="row mt16" attrs="{'invisible':['|', ('module_hr_timesheet', '=', False), ('project_time_mode_id', '!=', False)]}">
+ <label for="project_time_mode_id" class="col-md-3 o_light_label"/>
+ <field name="project_time_mode_id" options="{'no_create': True, 'no_open': True}"/>
+ </div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box">
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py
index 2506a819c74..bcf7799f161 100644
--- a/addons/resource/models/resource.py
+++ b/addons/resource/models/resource.py
@@ -34,7 +34,7 @@ def to_naive_utc(datetime, record):
def to_tz(datetime, tz_name):
- tz = pytz.timezone(tz_name)
+ tz = pytz.timezone(tz_name) if tz_name else pytz.UTC
return pytz.UTC.localize(datetime.replace(tzinfo=None), is_dst=False).astimezone(tz).replace(tzinfo=None)
diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml
index d38b93cb821..46cc2639fa5 100644
--- a/addons/sale/views/sale_portal_templates.xml
+++ b/addons/sale/views/sale_portal_templates.xml
@@ -335,8 +335,8 @@
</template>
<template id="portal_order_error" name="Order error/warning display">
- <div class="row">
- <div t-att-class="'col-md-6 col-md-offset-3 text-center alert alert-dismissable %s' % ('alert-danger' if error else 'alert-warning')">
+ <div class="row mr16">
+ <div t-attf-class="'col-md-12 mr16 ml16 alert alert-dismissable' #{'alert-danger' if error else 'alert-warning'}">
<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.
@@ -346,8 +346,8 @@
</template>
<template id="portal_order_success" name="Order success display">
- <div class="row">
- <div class="col-md-6 col-md-offset-3 text-center alert alert-dismissable alert-success">
+ <div class="row mr16">
+ <div class="col-md-12 mr16 ml16 alert alert-dismissable alert-success">
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
</div>
</div>
diff --git a/addons/sale_payment/controllers/payment.py b/addons/sale_payment/controllers/payment.py
index 54b89cf577e..1d00409a204 100644
--- a/addons/sale_payment/controllers/payment.py
+++ b/addons/sale_payment/controllers/payment.py
@@ -94,9 +94,9 @@ class PaymentPortal(http.Controller):
# proceed to the payment
res = tx.confirm_sale_token()
- if res is not True:
- params['error'] = res
- return request.redirect(_build_url_w_params(error_url, params))
-
- params['success'] = 'pay_sale'
+ if tx.state != 'authorized' or not tx.acquirer_id.capture_manually:
+ if res is not True:
+ params['error'] = res
+ return request.redirect(_build_url_w_params(error_url, params))
+ params['success'] = 'pay_sale'
return request.redirect(_build_url_w_params(success_url, params))
diff --git a/addons/sale_payment/models/payment.py b/addons/sale_payment/models/payment.py
index 410d287b247..857ff857dfb 100644
--- a/addons/sale_payment/models/payment.py
+++ b/addons/sale_payment/models/payment.py
@@ -64,12 +64,10 @@ class PaymentTransaction(models.Model):
_logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
if self.sale_order_id.state in ('draft', 'sent'):
self.sale_order_id.with_context(send_email=True).action_confirm()
-
- if self.state == 'done':
- _logger.info('<%s> transaction completed, auto-confirming order %s (ID %s) and generating invoice', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
+ elif self.state == 'done':
+ _logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
if self.sale_order_id.state in ('draft', 'sent'):
self.sale_order_id.with_context(send_email=True).action_confirm()
- self._generate_and_pay_invoice()
elif self.state not in ['cancel', 'error'] and self.sale_order_id.state == 'draft':
_logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
self.sale_order_id.force_quotation_send()
diff --git a/addons/sale_payment/views/sale_portal_templates.xml b/addons/sale_payment/views/sale_portal_templates.xml
index df86ce75998..e8c5afab5db 100644
--- a/addons/sale_payment/views/sale_portal_templates.xml
+++ b/addons/sale_payment/views/sale_portal_templates.xml
@@ -1,4 +1,34 @@
<odoo>
+
+ <!-- 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">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ Your payment has been authorized.
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ </template>
+
+
<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-if="not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and order.payment_tx_id.state != 'done'" name="portal_confirmation_pay">
@@ -12,8 +42,14 @@
</a>
</t>
</xpath>
+ <xpath expr="//t[@t-call='sale.portal_order_success']" position="after">
+ <div t-if="order.payment_tx_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.payment_tx_id"/>
+ <t t-call="sale_payment.payment_confirmation_status"/>
+ </div>
+ </xpath>
<xpath expr="//div[@id='portal_sign']" position="after">
- <div class="row" t-if="not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay'" id="portal_pay">
+ <div class="row" t-if="not invoices and order.state in ('sent') and portal_confirmation == 'pay'" id="portal_pay">
<div class="col-md-offset-3 col-md-6">
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left col-md-13">
<h4 class="mb24">Pay with</h4>
@@ -21,8 +57,8 @@
<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, keep_query())"/>
- <t t-set="error_url" t-value="'/my/orders/%s?%s' % (order.id, keep_query())"/>
+ <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'"/>
@@ -101,7 +137,8 @@
inherit_id="sale.portal_order_success">
<xpath expr="//a[hasclass('close')]" position="after">
<t t-if="success == 'pay_sale'">
- Order successfully paid.
+ <span t-if='order.payment_acquirer_id.done_msg' t-raw="order.payment_acquirer_id.done_msg"/>
+ <span t-if='order.payment_acquirer_id.post_msg' t-raw="order.payment_acquirer_id.post_msg"/>
</t>
</xpath>
</template>
diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py
index 2f7d1021a94..417fb1950d1 100644
--- a/addons/sale_timesheet/models/account.py
+++ b/addons/sale_timesheet/models/account.py
@@ -42,6 +42,11 @@ class AccountAnalyticLine(models.Model):
if 'task_id' in values:
task = self.env['project.task'].sudo().browse(values['task_id'])
values['so_line'] = task.sale_line_id.id or values.get('so_line', False)
+
+ # Set product_uom_id now so delivered qty is computed in SO line
+ if not 'product_uom_id' in values and all([v in values for v in ['employee_id', 'project_id']]):
+ employee = self.env['hr.employee'].sudo().browse(values['employee_id'])
+ values['product_uom_id'] = employee.company_id.project_time_mode_id.id
return values
@api.multi
diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py
index a637b43133b..844933d092a 100644
--- a/addons/sms/wizard/send_sms.py
+++ b/addons/sms/wizard/send_sms.py
@@ -15,8 +15,10 @@ try:
except ImportError:
_sms_phonenumbers_lib_imported = False
- _logger.warning(
- "The `phonenumbers` Python module is not installed. Try: pip install phonenumbers."
+ _logger.info(
+ "The `phonenumbers` Python module is not available. "
+ "Phone number validation will be skipped. "
+ "Try `pip3 install phonenumbers` to install it."
)
@@ -70,7 +72,7 @@ class SendSMS(models.TransientModel):
no_phone_partners = []
for partner in partners:
default_field_name = 'mobile' if partner.mobile else 'phone'
- number = self._sms_sanitization(partner, self.env.context('field_name') or default_field_name)
+ number = self._sms_sanitization(partner, self.env.context.get('field_name') or default_field_name)
if number:
phone_numbers.append(number)
else:
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 2ac3bd05415..a93e24f84e8 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1009,17 +1009,12 @@ class StockMove(models.Model):
move._action_cancel()
# Create extra moves where necessary
- for move in moves.filtered(lambda m: m.state != 'cancel'):
- # Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However,
- # it is possible that the user changed the value before posting the inventory by a value that should be
- # rounded according to the move's UOM. In this specific case, we chose to round up the value, because it
- # is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be
- # consumed/produced and the moves are split correctly).
- # FIXME: move rounding to move line
- # rounding = move.product_uom.rounding
- # move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP')
+ for move in moves:
+ if move.state == 'cancel' or move.quantity_done <= 0:
+ continue
moves_todo |= move
moves_todo |= move._create_extra_move()
+
# Split moves where necessary and move quants
for move in moves_todo:
rounding = move.product_uom.rounding
diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py
index cb9244e4511..a0692a50688 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -90,7 +90,7 @@ class StockMoveLine(models.Model):
@api.constrains('product_uom_qty')
def check_reserved_done_quantity(self):
for move_line in self:
- if move_line.state == 'done' and not float_is_zero(move_line.product_uom_qty, precision_rounding=self.env['decimal.precision'].precision_get('Product Unit of Measure')):
+ if move_line.state == 'done' and not float_is_zero(move_line.product_uom_qty, precision_digits=self.env['decimal.precision'].precision_get('Product Unit of Measure')):
raise ValidationError(_('A done move line should never have a reserved quantity.'))
@api.onchange('product_id', 'product_uom_id')
diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py
index a3a6b0ae46b..45e898856b7 100644
--- a/addons/stock/tests/test_move2.py
+++ b/addons/stock/tests/test_move2.py
@@ -743,6 +743,51 @@ class TestSinglePicking(TestStockCommon):
backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
self.assertEqual(backorder.state, 'confirmed')
+ def test_backorder_3(self):
+ """ Check the good behavior of creating a backorder for an available move on a picking with
+ two available moves.
+ """
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'partner_id': self.partner_delta_id,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ # make some stock
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
+ self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
+
+ # assign to partially available
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+
+ delivery_order.move_lines[0].move_line_ids[0].qty_done = 2
+ delivery_order.do_transfer()
+
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
+ self.assertEqual(backorder.state, 'confirmed')
+
def test_extra_move_1(self):
""" Check the good behavior of creating an extra move in a delivery order. This usecase
simulates the delivery of 2 item while the initial stock move had to move 1 and there's
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 7da23c0b44d..126da0a781d 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -200,6 +200,7 @@
<field name="arch" type="xml">
<form string="Transfer">
<header>
+ <field name="id" invisible="1"/>
<field name="is_locked" invisible="1"/>
<field name="show_mark_as_todo" invisible="1"/>
<field name="show_check_availability" invisible="1"/>
@@ -245,7 +246,7 @@
<field name="backorder_id" readonly="1" attrs="{'invisible': [('backorder_id','=',False)]}"/>
</group>
<group>
- <field name="scheduled_date"/>
+ <field name="scheduled_date" attrs="{'readonly': [('id', '=', False)]}"/>
<field name="origin" placeholder="e.g. PO0032"/>
<field name="owner_id" groups="stock.group_tracking_owner"/>
<div groups="stock.group_tracking_owner" colspan="2" col="2">
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index 75029d4cc27..3f3cec9ec7f 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -87,12 +87,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
this.active_view = null;
this.registry = view_registry;
this.title = this.action.name;
- var actionGroupBy = self.action.context.group_by;
- if (!actionGroupBy) {
- actionGroupBy = [];
- } else if (typeof actionGroupBy === 'string') {
- actionGroupBy = [actionGroupBy];
- }
_.each(views, function (view) {
var view_type = view[1] || view.view_type;
var View = self.registry.get(view_type); //.prototype.config.Controller;
@@ -114,7 +108,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
action: self.action,
limit: self.action.limit,
views: self.action.views,
- groupBy: actionGroupBy,
}, self.flags, self.flags[view_type], view.options),
searchable: View.prototype.searchable,
title: self.title,
@@ -271,16 +264,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
if (!view.loaded) {
view_options = _.extend({}, view.options, view_options, self.env);
- if (view_options.groupBy && !view_options.groupBy.length) {
- var actionContext = view_options ? view_options.action.context : {};
- var actionGroupBy = actionContext.group_by;
- if (!actionGroupBy) {
- actionGroupBy = [];
- } else if (typeof actionGroupBy === 'string') {
- actionGroupBy = [actionGroupBy];
- }
- view_options.groupBy = actionGroupBy;
- }
view.loaded = $.Deferred();
self.create_view(view, view_options).then(function(controller) {
view.controller = controller;
@@ -512,18 +495,15 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
JSON.stringify(results.error)));
}
- // this.dataset._model = new Model(this.dataset.model, results.context, results.domain);
- // var groupby = results.group_by.length ? results.group_by : action_context.group_by;
- // if (_.isString(groupby)) {
- // groupby = [groupby];
- // }
- // if (!controller.grouped && !_.isEmpty(groupby)){
- // this.dataset.set_sort([]);
- // }
+ // FORWARDPORT THIS UP TO SAAS-11.1 ONLY, NOT LATER
+ var groupby = results.group_by.length ? results.group_by : action_context.group_by;
+ if (_.isString(groupby)) {
+ groupby = [groupby];
+ }
return {
context: results.context,
domain: results.domain,
- groupBy: results.group_by,
+ groupBy: groupby || [],
};
},
do_push_state: function(state) {
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 5296c6db7d5..b5b668a43a1 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -3238,92 +3238,7 @@ var BasicModel = AbstractModel.extend({
defs.push(self._fetchNameGet(dp));
record._changes[name] = dp.id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
- var fieldInfo = record.fieldsInfo[record.viewType][name];
- var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
- var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo;
- var fields = view ? view.fields : fieldInfo.relatedFields;
- var viewType = view ? view.type : fieldInfo.viewType;
-
- var x2manyList = self._makeDataPoint({
- context: record.context,
- fieldsInfo: fieldsInfo,
- fields: fields,
- limit: fieldInfo.limit,
- modelName: field.relation,
- parentID: record.id,
- rawContext: fieldInfo && fieldInfo.context,
- relationField: field.relation_field,
- res_ids: [],
- static: true,
- type: 'list',
- viewType: viewType,
- });
- record._changes[name] = x2manyList.id;
- x2manyList._changes = [];
- var many2ones = {};
- var r;
- result[name] = result[name] || []; // handle false value
- var isCommandList = result[name].length && _.isArray(result[name][0]);
- if (!isCommandList) {
- result[name] = [[6, false, result[name]]];
- }
- _.each(result[name], function (value) {
- // value is a command
- if (value[0] === 0) {
- // CREATE
- r = self._makeDataPoint({
- modelName: x2manyList.model,
- context: x2manyList.context,
- fieldsInfo: fieldsInfo,
- fields: fields,
- parentID: x2manyList.id,
- viewType: viewType,
- });
- x2manyList._changes.push({operation: 'ADD', id: r.id});
- x2manyList._cache[r.res_id] = r.id;
-
- // this is necessary so the fields are initialized
- _.each(r.getFieldNames(), function (fieldName) {
- r.data[fieldName] = null;
- });
-
- r._changes = _.defaults(value[2], r.data);
- for (var name in r._changes) {
- if (!r._changes[name]) {
- continue;
- }
- var isFieldInView = name in r.fields;
- if (isFieldInView && r.fields[name].type === 'many2one') {
- var rec = self._makeDataPoint({
- context: r.context,
- modelName: r.fields[name].relation,
- data: {id: r._changes[name]},
- parentID: r.id,
- });
- r._changes[name] = rec.id;
- many2ones[name] = true;
- }
- }
- }
- if (value[0] === 6) {
- // REPLACE_WITH
- _.each(value[2], function (res_id) {
- x2manyList._changes.push({operation: 'ADD', resID: res_id});
- });
- var def = self._readUngroupedList(x2manyList).then(function () {
- return $.when(
- self._fetchX2ManysBatched(x2manyList),
- self._fetchReferencesBatched(x2manyList)
- );
- });
- defs.push(def);
- }
- });
-
- // fetch many2ones display_name
- _.each(_.keys(many2ones), function (name) {
- defs.push(self._fetchNameGets(x2manyList, name));
- });
+ defs.push(self._processX2ManyCommands(record, name, result[name]));
} else {
record._changes[name] = self._parseServerValue(field, result[name]);
}
@@ -3498,6 +3413,122 @@ var BasicModel = AbstractModel.extend({
});
},
/**
+ * Process x2many commands in a default record by transforming the list of
+ * commands in operations (pushed in _changes) and fetch the related
+ * records fields.
+ *
+ * Note that this method can be called recursively.
+ *
+ * @todo in master: factorize this code with the postprocessing of x2many in
+ * _applyOnChange
+ *
+ * @private
+ * @param {Object} record
+ * @param {string} fieldName
+ * @param {Array[Array]} commands
+ * @returns {Deferred}
+ */
+ _processX2ManyCommands: function (record, fieldName, commands) {
+ var self = this;
+ var defs = [];
+ var field = record.fields[fieldName];
+ var fieldInfo = record.fieldsInfo[record.viewType][fieldName];
+ var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
+ var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo;
+ var fields = view ? view.fields : fieldInfo.relatedFields;
+ var viewType = view ? view.type : fieldInfo.viewType;
+
+ var x2manyList = self._makeDataPoint({
+ context: record.context,
+ fieldsInfo: fieldsInfo,
+ fields: fields,
+ limit: fieldInfo.limit,
+ modelName: field.relation,
+ parentID: record.id,
+ rawContext: fieldInfo && fieldInfo.context,
+ relationField: field.relation_field,
+ res_ids: [],
+ static: true,
+ type: 'list',
+ viewType: viewType,
+ });
+ record._changes[fieldName] = x2manyList.id;
+ x2manyList._changes = [];
+ var many2ones = {};
+ var r;
+ commands = commands || []; // handle false value
+ var isCommandList = commands.length && _.isArray(commands[0]);
+ if (!isCommandList) {
+ commands = [[6, false, commands]];
+ }
+ _.each(commands, function (value) {
+ // value is a command
+ if (value[0] === 0) {
+ // CREATE
+ r = self._makeDataPoint({
+ modelName: x2manyList.model,
+ context: x2manyList.context,
+ fieldsInfo: fieldsInfo,
+ fields: fields,
+ parentID: x2manyList.id,
+ viewType: viewType,
+ });
+ x2manyList._changes.push({operation: 'ADD', id: r.id});
+ x2manyList._cache[r.res_id] = r.id;
+
+ // this is necessary so the fields are initialized
+ _.each(r.getFieldNames(), function (fieldName) {
+ r.data[fieldName] = null;
+ });
+
+ r._changes = _.defaults(value[2], r.data);
+ for (var fieldName in r._changes) {
+ if (!r._changes[fieldName]) {
+ continue;
+ }
+ var isFieldInView = fieldName in r.fields;
+ if (isFieldInView) {
+ var field = r.fields[fieldName];
+ var fieldType = field.type;
+ if (fieldType === 'many2one') {
+ var rec = self._makeDataPoint({
+ context: r.context,
+ modelName: field.relation,
+ data: {id: r._changes[fieldName]},
+ parentID: r.id,
+ });
+ r._changes[fieldName] = rec.id;
+ many2ones[fieldName] = true;
+ } else if (_.contains(['one2many', 'many2many'], fieldType)) {
+ var x2mCommands = commands[0][2][fieldName];
+ defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands));
+ }
+ }
+ }
+ }
+ if (value[0] === 6) {
+ // REPLACE_WITH
+ _.each(value[2], function (res_id) {
+ x2manyList._changes.push({operation: 'ADD', resID: res_id});
+ });
+ var def = self._readUngroupedList(x2manyList).then(function () {
+ return $.when(
+ self._fetchX2ManysBatched(x2manyList),
+ self._fetchReferencesBatched(x2manyList)
+ );
+ });
+ defs.push(def);
+ }
+ });
+
+ // fetch many2ones display_name
+ _.each(_.keys(many2ones), function (name) {
+ defs.push(self._fetchNameGets(x2manyList, name));
+ });
+
+ return $.when.apply($, defs);
+ },
+ /**
* Reads data from server for all missing fields.
*
* @private
diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js
index 2c9cf3d4457..732fd7dc157 100644
--- a/addons/web/static/src/js/widgets/debug_manager.js
+++ b/addons/web/static/src/js/widgets/debug_manager.js
@@ -470,7 +470,7 @@ DebugManager.include({
$defaults.parent().addClass('o_form_invalid');
return;
}
- var allUsers = d.$el.find('#formview_default_all').is(':checked');
+ var selfUser = d.$el.find('#formview_default_self').is(':checked');
var condition = d.$el.find('#formview_default_conditions').val();
var value = _.find(self.fields, function (field) {
return field.name === fieldToSet;
@@ -482,7 +482,7 @@ DebugManager.include({
self._active_view.fields_view.model,
fieldToSet,
value,
- allUsers,
+ selfUser,
true,
condition || false,
],
diff --git a/addons/web/static/src/less/kanban_view.less b/addons/web/static/src/less/kanban_view.less
index d95a518fdda..153f6997d4d 100644
--- a/addons/web/static/src/less/kanban_view.less
+++ b/addons/web/static/src/less/kanban_view.less
@@ -208,7 +208,9 @@
.o_kanban_record_headings {
line-height: 1.2;
.o-flex(1, 1, auto);
-
+ // Ensure long word doesn't break out of container
+ word-wrap: break-word;
+ overflow: hidden;
}
}
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index ff9a9c63621..8938755a8f3 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -6201,6 +6201,38 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('nested x2many default values', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.model === 'partner' && args.method === 'default_get') {
+ return $.when({turtles: [[0, 0, {
+ partner_ids: [[6, 0, [4]]],
+ }]]});
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_list_view .o_field_many2manytags[name="partner_ids"] .badge').length, 1,
+ "m2mtags should contain one tag");
+ assert.strictEqual(form.$('.o_list_view .o_field_many2manytags[name="partner_ids"] .o_badge_text').text(),
+ 'aaa', "tag name should have been correctly loaded");
+
+ form.destroy();
+ });
+
QUnit.test('one2many (who contains display_name) with tree view and without form view', function (assert) {
assert.expect(1);
diff --git a/addons/web_editor/static/src/js/widgets/ace.js b/addons/web_editor/static/src/js/widgets/ace.js
index 2112468fa95..80bb28a4c01 100644
--- a/addons/web_editor/static/src/js/widgets/ace.js
+++ b/addons/web_editor/static/src/js/widgets/ace.js
@@ -192,7 +192,6 @@ var ViewEditor = Widget.extend({
*/
start: function () {
this.$viewEditor = this.$('#ace-view-editor');
- this.$editor = this.$('.ace_editor');
this.$typeSwitcherChoices = this.$('.o_ace_type_switcher_choice');
this.$typeSwitcherBtn = this.$('.o_ace_type_switcher > .dropdown-toggle');
@@ -210,6 +209,7 @@ var ViewEditor = Widget.extend({
this.aceEditor = window.ace.edit(this.$viewEditor[0]);
this.aceEditor.setTheme('ace/theme/monokai');
+ this.$editor = this.$('.ace_editor');
var refX = 0;
var resizing = false;
diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py
index e3aa0f60f90..64928682533 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -207,11 +207,12 @@ class BlogPost(models.Model):
@api.multi
def write(self, vals):
- self.ensure_one()
- if 'website_published' in vals and 'published_date' not in vals:
- if (self.published_date or '') <= fields.Datetime.now():
- vals['published_date'] = vals['website_published'] and fields.Datetime.now()
- result = super(BlogPost, self).write(vals)
+ result = True
+ for post in self:
+ copy_vals = vals
+ if 'website_published' in vals and 'published_date' not in vals and (post.published_date or '') <= fields.Datetime.now():
+ copy_vals['published_date'] = vals['website_published'] and fields.Datetime.now() or False
+ result &= super(BlogPost, self).write(copy_vals)
self._check_for_publication(vals)
return result
diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py
index 819176096b2..eaf17861741 100644
--- a/addons/website_event_track/controllers/main.py
+++ b/addons/website_event_track/controllers/main.py
@@ -15,7 +15,7 @@ class WebsiteEventTrackController(http.Controller):
@http.route(['''/event/<model("event.event"):event>/track/<model("event.track", "[('event_id','=',event[0])]"):track>'''], type='http', auth="public", website=True)
def event_track_view(self, event, track, **post):
- track = track.sudo()
+ track = track.sudo().with_context(tz=event.date_tz or 'UTC')
values = {'track': track, 'event': track.event_id, 'main_object': track}
return request.render("website_event_track.track_view", values)
@@ -67,6 +67,7 @@ class WebsiteEventTrackController(http.Controller):
@http.route(['''/event/<model("event.event"):event>/agenda'''], type='http', auth="public", website=True, sitemap=False)
def event_agenda(self, event, tag=None, **post):
+ event = event.with_context(tz=event.date_tz or 'UTC')
local_tz = pytz.timezone(event.date_tz or 'UTC')
days_tracks = collections.defaultdict(lambda: [])
for track in event.track_ids.sorted(lambda track: (track.date or '', bool(track.location_id))):
@@ -93,6 +94,7 @@ class WebsiteEventTrackController(http.Controller):
'''/event/<model("event.event"):event>/track/tag/<model("event.track.tag"):tag>'''
], type='http', auth="public", website=True, sitemap=False)
def event_tracks(self, event, tag=None, **post):
+ event = event.with_context(tz=event.date_tz or 'UTC')
searches = {}
if tag:
searches.update(tag=tag.id)
diff --git a/addons/website_event_track/views/event_track_templates.xml b/addons/website_event_track/views/event_track_templates.xml
index 9d5bee401ba..70ed14e680a 100644
--- a/addons/website_event_track/views/event_track_templates.xml
+++ b/addons/website_event_track/views/event_track_templates.xml
@@ -255,7 +255,9 @@
</div>
<div class="panel-body">
<b>Date</b><br/>
- <span t-field="track.date" t-options='{"hide_seconds":"True"}'/><br/>
+ <span t-field="track.date" t-options='{"hide_seconds":"True"}'/>
+ <t t-if="event.date_tz">(<span t-field="event.date_tz"/>)</t>
+ <br/>
<b>Duration</b><br/>
<span t-field="track.duration" t-options='{"widget": "duration", "unit": "hour", "round": "minute"}'/><br/>
<b>Location</b><br/>
diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py
index 6a3a3b71f9a..f7ba4bc160b 100644
--- a/addons/website_quote/controllers/main.py
+++ b/addons/website_quote/controllers/main.py
@@ -79,6 +79,7 @@ class sale_quote(http.Controller):
'tx_id': Transaction.id if Transaction else False,
'tx_state': Transaction.state if Transaction else False,
'tx_post_msg': Transaction.acquirer_id.post_msg if Transaction else False,
+ 'payment_tx': Transaction,
'need_payment': order_sudo.invoice_status == 'to invoice' and Transaction.state in ['draft', 'cancel', 'error'],
'token': token,
'return_url': '/shop/payment/validate',
diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py
index 3e96864c45c..6824284b84e 100644
--- a/addons/website_quote/models/sale_order.py
+++ b/addons/website_quote/models/sale_order.py
@@ -51,6 +51,9 @@ 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):
+ return 1 if self.env['ir.config_parameter'].sudo().get_param('sale.sale_portal_confirmation_options', default='none') == 'pay' else 0
+
template_id = fields.Many2one(
'sale.quote.template', 'Quotation Template',
readonly=True,
@@ -66,7 +69,7 @@ class SaleOrder(models.Model):
quote_viewed = fields.Boolean('Quotation Viewed')
require_payment = fields.Selection([
(0, 'Online Signature'),
- (1, 'Online Payment')], default=0, string='Confirmation Mode',
+ (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.")
diff --git a/addons/website_quote/views/website_quote_templates.xml b/addons/website_quote/views/website_quote_templates.xml
index c56945c94d5..41a67c31b21 100644
--- a/addons/website_quote/views/website_quote_templates.xml
+++ b/addons/website_quote/views/website_quote_templates.xml
@@ -163,6 +163,34 @@
</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">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ <span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
+ </div>
+ <div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
+ Your payment has been authorized.
+ <span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
+ </div>
+ </template>
+
<!-- Complete page of the quotation -->
<template id="so_quotation" name="Product Quotation">
<t t-call="portal.portal_layout">
@@ -257,29 +285,18 @@
and do not hesitate to <a href="#discussion">contact us</a> for
any question.
</div>
- <div class="alert alert-warning alert-dismissable" t-if="tx_id and tx_state == 'done'">
- <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
- Your payment has been received, thank you for your trust.
- <span t-if='tx_post_msg' t-raw="tx_post_msg"/>
- </div>
- <div class="alert alert-warning alert-dismissable" t-if="tx_id and tx_state == 'pending'">
- <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
- Your transaction is waiting confirmation.
- <span t-if='tx_post_msg' t-raw="tx_post_msg"/>
- </div>
- <div class="alert alert-success alert-dismissable" t-if="tx_id and tx_state == 'authorized'">
- <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
- Your payment has been authorized.
- <span t-if='tx_post_msg' t-raw="tx_post_msg"/>
- </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'">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
This order
- <t t-if="quotation.state=='draft'">has not yet been sent</t>
- <t t-if="quotation.state=='cancel'">has already been cancelled</t>
- <t t-if="quotation.state not in ('cancel','draft')">has already been validated</t>
- ! You can <a href="#discussion">contact us</a> for
- any question.
+ <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 -->
@@ -332,6 +349,7 @@
<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>
</div>
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index f978430f938..d3a44363fab 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -749,7 +749,7 @@ class WebsiteSale(http.Controller):
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s' and acq.registration_view_template_id]
values['tokens'] = request.env['payment.token'].search(
[('partner_id', '=', order.partner_id.id),
- ('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])])
+ ('acquirer_id', 'in', acquirers.ids)])
return values
@@ -846,9 +846,11 @@ class WebsiteSale(http.Controller):
# we proceed the s2s payment
res = tx.confirm_sale_token()
# we then redirect to the page that validates the payment by giving it error if there's one
- if res is not True:
- return request.redirect('/shop/payment/validate?success=False&error=%s' % res)
- return request.redirect('/shop/payment/validate?success=True')
+ if tx.state != 'authorized' or not tx.acquirer_id.capture_manually:
+ if res is not True:
+ return request.redirect('/shop/payment/validate?success=False&error=%s' % res)
+ return request.redirect('/shop/payment/validate?success=True')
+ return request.redirect('/shop/payment/validate')
@http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
def payment_get_status(self, sale_order_id, **post):
diff --git a/addons/website_sale/models/__init__.py b/addons/website_sale/models/__init__.py
index 8cc73414b40..3a394e73151 100644
--- a/addons/website_sale/models/__init__.py
+++ b/addons/website_sale/models/__init__.py
@@ -7,6 +7,7 @@ from . import mail_compose_message
from . import product
from . import res_country
from . import res_partner
+from . import payment
from . import sale_order
from . import ir_model_fields
from . import website
diff --git a/addons/website_sale/models/payment.py b/addons/website_sale/models/payment.py
new file mode 100644
index 00000000000..3bebb9dfff8
--- /dev/null
+++ b/addons/website_sale/models/payment.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+
+from odoo import models
+
+_logger = logging.getLogger(__name__)
+
+
+class PaymentTransaction(models.Model):
+ _inherit = 'payment.transaction'
+
+ # --------------------------------------------------
+ # Sale management
+ # --------------------------------------------------
+
+ def _confirm_so(self):
+ """Generate the invoice automatically when option is enabled from ecommerce setting"""
+ result = super(PaymentTransaction, self)._confirm_so()
+ IrConfigParameter = self.env['ir.config_parameter'].sudo()
+ if self.state == 'done' and IrConfigParameter.get_param('website_sale.automatic_invoice', default=False):
+ _logger.info('<%s> transaction completed, generating invoice for order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
+ self._generate_and_pay_invoice()
+ return result
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 9fd121bd471..6e9781da5f3 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -1537,6 +1537,7 @@
<div t-att-class="'panel %s' % (
(order.payment_tx_id.state == 'pending' and 'panel-info') or
(order.payment_tx_id.state == 'done' and 'panel-success') or
+ (order.payment_tx_id.state == 'authorized' and 'panel-success') or
'panel-danger')">
<a groups="base.group_system" class="btn pull-right" target="_blank"
t-att-href="'/web#model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % ('payment.acquirer', order.payment_acquirer_id.id, 'payment.action_payment_acquirer')">
@@ -1555,6 +1556,9 @@
<t t-if="order.payment_tx_id.state == 'error'">
<t t-raw="order.payment_acquirer_id.error_msg"/>
</t>
+ <t t-if="order.payment_tx_id.state == 'authorized'">
+ Your payment has been authorized.
+ </t>
</div>
<div class="panel-body" t-if="order.payment_acquirer_id.post_msg">
<t t-raw="order.payment_acquirer_id.post_msg" />
diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py
index 1e94a0362ce..36ee8bd651f 100644
--- a/addons/website_sale_delivery/controllers/main.py
+++ b/addons/website_sale_delivery/controllers/main.py
@@ -65,6 +65,8 @@ class WebsiteSaleDelivery(WebsiteSale):
return {'status': order.delivery_rating_success,
'error_message': order.delivery_message,
'carrier_id': carrier_id,
- 'new_amount_delivery': order.delivery_price,
+ 'new_amount_delivery': order.currency_id.round(order.delivery_price),
'new_amount_untaxed': order.amount_untaxed,
- 'new_amount_tax': order.amount_tax}
+ 'new_amount_tax': order.amount_tax,
+ 'new_amount_total': order.amount_total,
+ }
diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
index bbdece5fe55..3cb0df152c1 100644
--- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
+++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
@@ -18,7 +18,7 @@ odoo.define('website_sale_delivery.checkout', function (require) {
$amount_delivery.text(result.new_amount_delivery);
$amount_untaxed.text(result.new_amount_untaxed);
$amount_tax.text(result.new_amount_tax);
- $amount_total.text(result.new_amount_delivery + result.new_amount_untaxed + result.new_amount_tax);
+ $amount_total.text(result.new_amount_total);
$carrier_badge.children('span').text(result.new_amount_delivery);
$carrier_badge.removeClass('hidden');
$compute_badge.addClass('hidden');
diff --git a/doc/cla/corporate/VanRoey.md b/doc/cla/corporate/VanRoey.md
index 6fd5828e02c..d5fc3b3cfb8 100644
--- a/doc/cla/corporate/VanRoey.md
+++ b/doc/cla/corporate/VanRoey.md
@@ -12,7 +12,7 @@ Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666
List of contributors:
-Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666
+Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666 (up to 2018-01-26)
Vincent Adriaensen vincent.adriaensen@vanroey.be https://github.com/vincentadriaensen
Pol Van Dingenen pol.vandingenen@vanroey.be https://github.com/Cyhexy
Ward Marissen Ward.Marissen@vanroey.be https://github.com/Wardm95
diff --git a/doc/cla/corporate/vauxoo.md b/doc/cla/corporate/vauxoo.md
index ff9f5036b40..f250546a6ef 100644
--- a/doc/cla/corporate/vauxoo.md
+++ b/doc/cla/corporate/vauxoo.md
@@ -41,3 +41,4 @@ Mariano Fernandez mariano@vauxoo.com https://github.com/Batuto
Edgar Rivero edgar@vauxoo.com https://github.com/egrivero
Alexander Olivares alexander@vauxoo.com https://github.com/alxolivares
Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906
+Erick Birbe erick@vauxoo.com https://github.com/ebirbe
diff --git a/doc/cla/individual/sergeiruzkiicode.md b/doc/cla/individual/sergeiruzkiicode.md
new file mode 100644
index 00000000000..44af0359a97
--- /dev/null
+++ b/doc/cla/individual/sergeiruzkiicode.md
@@ -0,0 +1,11 @@
+Belarus, 2017-01-17
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Sergei Ruzki sergei.ruzki@icode.by https://github.com/sergeiruzkiicode
diff --git a/odoo/addons/base/data/res_country_data.xml b/odoo/addons/base/data/res_country_data.xml
index 819082fb27d..008f8b7e995 100644
--- a/odoo/addons/base/data/res_country_data.xml
+++ b/odoo/addons/base/data/res_country_data.xml
@@ -254,8 +254,8 @@
<field name="name">Belarus</field>
<field name="code">by</field>
<field file="base/static/img/country_flags/by.png" name="image" type="base64" />
- <field name="currency_id" ref="BYR" />
- <field eval="0" name="phone_code" />
+ <field name="currency_id" ref="BYN" />
+ <field eval="375" name="phone_code" />
</record>
<record id="bz" model="res.country">
<field name="name">Belize</field>
diff --git a/odoo/addons/base/data/res_currency_data.xml b/odoo/addons/base/data/res_currency_data.xml
index f286190872d..2d32fedb9c5 100644
--- a/odoo/addons/base/data/res_currency_data.xml
+++ b/odoo/addons/base/data/res_currency_data.xml
@@ -516,10 +516,19 @@
<field name="symbol">BR</field>
<field name="rounding">0.01</field>
<field name="active" eval="False"/>
- <field name="currency_unit_label">Ruble</field>
+ <field name="currency_unit_label">Ruble BYR</field>
<field name="currency_subunit_label">Kapeyka</field>
</record>
+ <record id="BYN" model="res.currency">
+ <field name="name">BYN</field>
+ <field name="symbol">Br</field>
+ <field name="rounding">0.01</field>
+ <field name="active" eval="False"/>
+ <field name="currency_unit_label">Rubles</field>
+ <field name="currency_subunit_label">Kopeks</field>
+ </record>
+
<record id="BZD" model="res.currency">
<field name="name">BZD</field>
<field name="symbol">BZ$</field>
diff --git a/odoo/addons/base/data/res_currency_rate_demo.xml b/odoo/addons/base/data/res_currency_rate_demo.xml
index 8ca63297a3d..220e7849668 100644
--- a/odoo/addons/base/data/res_currency_rate_demo.xml
+++ b/odoo/addons/base/data/res_currency_rate_demo.xml
@@ -349,6 +349,12 @@
<field name="rate">10228.19</field>
</record>
+ <record forcecreate="0" id="rateBYN" model="res.currency.rate">
+ <field name="currency_id" ref="BYN" />
+ <field name="name">2016-07-01</field>
+ <field name="rate">2.23151</field>
+ </record>
+
<record forcecreate="0" id="rateBZD" model="res.currency.rate">
<field name="currency_id" ref="BZD" />
<field name="name">2010-01-01</field>
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 53a3438b21a..2915d9a0086 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -15,6 +15,7 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro
from odoo.osv import expression
from odoo.service.db import check_super
from odoo.tools import partition, pycompat
+from odoo.http import root
_logger = logging.getLogger(__name__)
@@ -372,6 +373,8 @@ class Users(models.Model):
if any(key.startswith('context_') or key in ('lang', 'tz') for key in values):
self.context_get.clear_cache(self)
if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS):
+ # force deletion of all sessions for these users
+ root.session_store.delete_sessions_for_uids(self.ids)
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
@@ -385,7 +388,10 @@ class Users(models.Model):
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
- return super(Users, self).unlink()
+ res = super(Users, self).unlink()
+ # force deletion of all sessions for these users
+ root.session_store.delete_sessions_for_uids(self.ids)
+ return res
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
diff --git a/odoo/http.py b/odoo/http.py
index 33b4b58aac8..63d7ece05d5 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -1029,7 +1029,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.db = db
self.uid = uid
self.login = login
- self.password = password
request.uid = uid
request.disable_db = False
@@ -1044,7 +1043,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
"""
if not self.db or not self.uid:
raise SessionExpiredException("Session expired")
- security.check(self.db, self.uid, self.password)
def logout(self, keep_db=False):
for k in list(self):
@@ -1057,7 +1055,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.setdefault("db", None)
self.setdefault("uid", None)
self.setdefault("login", None)
- self.setdefault("password", None)
self.setdefault("context", {})
def get_context(self):
@@ -1271,6 +1268,17 @@ class DisableCacheMiddleware(object):
start_response(status, new_headers)
return self.app(environ, start_wrapped)
+class OdooSessionStore(werkzeug.contrib.sessions.FilesystemSessionStore):
+ def delete_sessions_for_uids(self, uids):
+ # pretty expensive on large session stores, especially non-local!
+ uids = set(uids)
+ _logger.info('Deleting all HTTP sessions for UIDs %s', uids)
+ for sid in self.list():
+ s = self.get(sid)
+ if s.uid and s.uid in uids:
+ _logger.debug('Deleting session %s', sid)
+ self.delete(s)
+
class Root(object):
"""Root WSGI application for the OpenERP Web Client.
"""
@@ -1282,7 +1290,7 @@ class Root(object):
# Setup http sessions
path = odoo.tools.config.session_dir
_logger.debug('HTTP sessions stored in: %s', path)
- return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
+ return OdooSessionStore(path, session_class=OpenERPSession)
@lazy_property
def nodb_routing_map(self):