diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 365bcd9869e..8e4e3c95d14 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1338,6 +1338,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: @@ -1723,6 +1724,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']) @@ -1730,7 +1732,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 76641cdeb74..07052fe5740 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/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 881eb69f32b..8d562328c4f 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -68,6 +68,7 @@ class ResConfigSettings(models.TransientModel): 'company_id': self.company_id.id, 'chart_template_id': self.chart_template_id.id, 'transfer_account_id': self.chart_template_id.transfer_account_id.id, + 'code_digits': self.chart_template_id.code_digits, 'sale_tax_rate': 15.0, 'purchase_tax_rate': 15.0, 'code_digits': self.chart_template_id.code_digits, 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 @@ + + + + + diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index df69b49e3f4..7e4c2060a9d 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 3a9aec7d78e..82fb55adaf7 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('
' + _.str.escapeHTML("The value is invalid.") + '
'); + wrong_input = true; + } + else if ($(element).closest('div.form-group').hasClass('has-error')) { + wrong_input = true; + $(element).closest('div.form-group').append('
' + _.str.escapeHTML("The value is invalid.") + '
'); } }); - if (empty_inputs) { - this.displayError( - _t('Missing values'), - _t('

Please fill all the inputs required.

') - ); + if (wrong_input) { return; } $(button).attr('disabled', true); + $(button).children('.fa-plus-circle').removeClass('fa-plus-circle') $(button).prepend(' '); 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("

We are not able to add your payment method at the moment.

")); + 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("

We are not able to add your payment method at the moment.

") + - "

" + - (config.debug ? (data.data.message.replace(/\n/g, "
")): '') + - "

" + _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("

We are not able to redirect you to the payment form.

") + _t("We are not able to redirect you to the payment form.") ); } }).fail(function (message, data) { self.displayError( _t('Server Error'), - _t("

We are not able to redirect you to the payment form.

") + - "

" + - (config.debug ? (data.data.message.replace(/\n/g, "
")): '') + - "

" + _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("

We're unable to process your payment.

") + _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('

Please select a payment method.

') + _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 = '
' + _.str.escapeHTML("The value is invalid.") + '
'; + $(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 = '
' + _.str.escapeHTML("The value is invalid.") + '
'; + $(element).closest('div.form-group').append(message); } }); - if (empty_inputs) { - this.displayError( - _t('Missing values'), - _t('

Please fill all the inputs required.

') - ); + 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(' '); // 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("

We are not able to add your payment method at the moment.

") - ); + 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("

We are not able to add your payment method at the moment.

") + - "

" + - (config.debug ? (data.data.message.replace(/\n/g, "
")): '') + - "

" + _t("We are not able to add your payment method at the moment.

") + + data.data.message ); }); } else { this.displayError( _t('No payment method selected'), - _t('

Please select the option to add a new payment method.

') + _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("

We are not able to delete your payment method at the moment.

") + _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 += '

' + _.str.escapeHTML(sub.name) + '

'; + content += '

' + sub.name + '

'; }); content = $('

').html(_t('

This card is currently linked to the following records:

') + content); @@ -344,7 +365,7 @@ odoo.define('payment.payment_form', function (require) { }, function (type, err) { self.displayError( _t('Server Error'), - _t("

We are not able to delete your payment method at the moment.

") + (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: "

" + (_.str.escapeHTML(message) || "") + "

" , + buttons: [ + {text: _t('Ok'), close: true}]}).open(); + } else { + // removed if exist error message + this.$('#payment_error').remove(); + var messageResult = '
'; + if (title != '') { + messageResult = messageResult + '' + _.str.escapeHTML(title) + ':
'; + } + messageResult = messageResult + _.str.escapeHTML(message) + '
'; + $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 @@ + + 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 @@
+