diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-01-25 15:41:45 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-01-25 15:41:45 +0100 |
| commit | cb50970f3f88c5e5e4f0608c6d07e66eb4698eb9 (patch) | |
| tree | fae668c499403db72b172b3c8802b869b1040ae7 | |
| parent | ffaca543c47adf5d0286ff8e9eeb99b193948679 (diff) | |
| parent | eefe879a37aba221bda84adfd880fb7876955920 (diff) | |
[MERGE] forward port branch 11.0 up to eefe879a37a
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">&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">&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">&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">&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">&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> </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> </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">&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">&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">&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">&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">&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 < 3) else 'hidden'"> + <li t-att-class="'' if (i < 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 < 3) else 'hidden'"> + <li t-att-class=" '' if (i < 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 & 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 & 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">&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">&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">&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">&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">&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">&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">&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">&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">&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">&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">&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">&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">&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">&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&id=%s&action=%s&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): |
