diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 5f9a2cf26baf0..d7916aaa826d9 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1212,6 +1212,11 @@ def remove_move_reconcile(self): account_move_line.payment_id.write({'invoice_ids': [(3, invoice.id, None)]}) rec_move_ids += account_move_line.matched_debit_ids rec_move_ids += account_move_line.matched_credit_ids + if self.env.context.get('invoice_id'): + current_invoice = self.env['account.invoice'].browse(self.env.context['invoice_id']) + rec_move_ids = rec_move_ids.filtered( + lambda r: (r.debit_move_id + r.credit_move_id) & current_invoice.move_id.line_ids + ) return rec_move_ids.unlink() #################################################### diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 9894dc6a9b46a..7b80e0bbaa741 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -633,7 +633,7 @@ var StatementModel = BasicModel.extend({ handles = [handle]; } else { _.each(this.lines, function (line, handle) { - if (!line.reconciled && !line.balance.amount && line.reconciliation_proposition.length) { + if (!line.reconciled && line.balance && !line.balance.amount && line.reconciliation_proposition.length) { handles.push(handle); } }); diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 4840e40f41a1d..09945c6e15baf 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -625,6 +625,42 @@ def test_partial_reconcile_currencies_01(self): full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa) self.assertEqual(full_rec_payable.balance, 18.75) + def test_unreconcile(self): + # Use case: + # 2 invoices paid with a single payment. Unreconcile the payment with one invoice, the + # other invoice should remain reconciled. + inv1 = self.create_invoice(invoice_amount=10, currency_id=self.currency_usd_id) + inv2 = self.create_invoice(invoice_amount=20, currency_id=self.currency_usd_id) + payment = self.env['account.payment'].create({ + 'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait_id, + 'amount': 100, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_usd.id, + }) + payment.post() + credit_aml = payment.move_line_ids.filtered('credit') + + # Check residual before assignation + self.assertAlmostEquals(inv1.residual, 10) + self.assertAlmostEquals(inv2.residual, 20) + + # Assign credit and residual + inv1.assign_outstanding_credit(credit_aml.id) + inv2.assign_outstanding_credit(credit_aml.id) + self.assertAlmostEquals(inv1.residual, 0) + self.assertAlmostEquals(inv2.residual, 0) + + # Unreconcile one invoice at a time and check residual + credit_aml.with_context(invoice_id=inv1.id).remove_move_reconcile() + self.assertAlmostEquals(inv1.residual, 10) + self.assertAlmostEquals(inv2.residual, 0) + credit_aml.with_context(invoice_id=inv2.id).remove_move_reconcile() + self.assertAlmostEquals(inv1.residual, 10) + self.assertAlmostEquals(inv2.residual, 20) + def test_partial_reconcile_currencies_02(self): #### # Day 1: Invoice Cust/001 to customer (expressed in USD) diff --git a/addons/account_payment/models/payment.py b/addons/account_payment/models/payment.py index 0ad11d39788a8..389cfa206e1d2 100644 --- a/addons/account_payment/models/payment.py +++ b/addons/account_payment/models/payment.py @@ -175,3 +175,10 @@ def _check_or_create_invoice_tx(self, invoice, acquirer, payment_token=None, tx_ }) return tx + + def _post_process_after_done(self, **kwargs): + # set invoice id in payment transaction when payment being done from sale order + res = super(PaymentTransaction, self)._post_process_after_done() + if kwargs.get('invoice_id'): + self.account_invoice_id = kwargs['invoice_id'] + return res diff --git a/addons/barcodes/static/src/js/barcode_events.js b/addons/barcodes/static/src/js/barcode_events.js index e02d3aafdb7cc..71e07030ffd34 100644 --- a/addons/barcodes/static/src/js/barcode_events.js +++ b/addons/barcodes/static/src/js/barcode_events.js @@ -28,6 +28,12 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { // Keys from a barcode scanner are usually processed as quick as possible, // but some scanners can use an intercharacter delay (we support <= 50 ms) max_time_between_keys_in_ms: session.max_time_between_keys_in_ms || 55, + // To be able to receive the barcode value, an input must be focused. + // On mobile devices, this causes the virtual keyboard to open. + // Unfortunately it is not possible to avoid this behavior... + // To avoid keyboard flickering at each detection of a barcode value, + // we want to keep it open for a while (800 ms). + inputTimeOut: 800, init: function() { mixins.PropertiesMixin.init.call(this); @@ -38,6 +44,30 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { // Bind event handler once the DOM is loaded // TODO: find a way to be active only when there are listeners on the bus $(_.bind(this.start, this, false)); + + // Mobile device detection + var isMobile = navigator.userAgent.match(/Android/i) || + navigator.userAgent.match(/webOS/i) || + navigator.userAgent.match(/iPhone/i) || + navigator.userAgent.match(/iPad/i) || + navigator.userAgent.match(/iPod/i) || + navigator.userAgent.match(/BlackBerry/i) || + navigator.userAgent.match(/Windows Phone/i); + this.isChromeMobile = isMobile && window.chrome; + + // Creates an input who will receive the barcode scanner value. + if (this.isChromeMobile) { + this.$barcodeInput = $('', { + name: 'barcode', + type: 'text', + css: { + 'position': 'absolute', + 'opacity': 0, + }, + }); + } + + this.__removeBarcodeField = _.debounce(this._removeBarcodeField, this.inputTimeOut); }, handle_buffered_keys: function() { @@ -109,7 +139,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Escape" || e.key === "Tab" || e.key === "Backspace" || e.key === "Delete" || - /F\d\d?/.test(e.key)) { + e.key === "Unidentified" || /F\d\d?/.test(e.key)) { return true; } else { return false; @@ -167,8 +197,84 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { } }, + /** + * Try to detect the barcode value by listening all keydown events: + * Checks if a dom element who may contains text value has the focus. + * If not, it's probably because these events are triggered by a barcode scanner. + * To be able to handle this value, a focused input will be created. + * + * This function also has the responsibility to detect the end of the barcode value. + * (1) In most of cases, an optional key (tab or enter) is sent to mark the end of the value. + * So, we direclty handle the value. + * (2) If no end key is configured, we have to calculate the delay between each keydowns. + * 'max_time_between_keys_in_ms' depends of the device and may be configured. + * Exceeded this timeout, we consider that the barcode value is entirely sent. + * + * @private + * @param {jQuery.Event} e keydown event + */ + _listenBarcodeScanner: function (e) { + if (!$('input:text:focus, textarea:focus, [contenteditable]:focus').length) { + $('body').append(this.$barcodeInput); + this.$barcodeInput.focus(); + } + if (this.$barcodeInput.is(":focus")) { + // Handle buffered keys immediately if the keypress marks the end + // of a barcode or after x milliseconds without a new keypress. + clearTimeout(this.timeout); + // On chrome mobile, e.which only works for some special characters like ENTER or TAB. + if (String.fromCharCode(e.which).match(this.suffix)) { + this._handleBarcodeValue(e); + } else { + this.timeout = setTimeout(this._handleBarcodeValue.bind(this, e), + this.max_time_between_keys_in_ms); + } + // if the barcode input doesn't receive keydown for a while, remove it. + this.__removeBarcodeField(); + } + }, + + /** + * Retrieves the barcode value from the temporary input element. + * This checks this value and trigger it on the bus. + * + * @private + * @param {jQuery.Event} keydown event + */ + _handleBarcodeValue: function (e) { + var barcodeValue = this.$barcodeInput.val(); + if (barcodeValue.match(this.regexp)) { + core.bus.trigger('barcode_scanned', barcodeValue, $(e.target).parent()[0]); + this.$barcodeInput.val(''); + } + }, + + /** + * Remove the temporary input created to store the barcode value. + * If nothing happens, this input will be removed, so the focus will be lost + * and the virtual keyboard on mobile devices will be closed. + * + * @private + */ + _removeBarcodeField: function () { + if (this.$barcodeInput) { + // Reset the value and remove from the DOM. + this.$barcodeInput.val('').remove(); + } + }, + start: function(prevent_key_repeat){ - $('body').bind("keypress", this.__handler); + // Chrome Mobile isn't triggering keypress event. + // This is marked as Legacy in the DOM-Level-3 Standard. + // See: https://www.w3.org/TR/uievents/#legacy-keyboardevent-event-types + // This fix is only applied for Google Chrome Mobile but it should work for + // all other cases. + // In master, we could remove the behavior with keypress and only use keydown. + if (this.isChromeMobile) { + $('body').on("keydown", this._listenBarcodeScanner.bind(this)); + } else { + $('body').bind("keypress", this.__handler); + } if (prevent_key_repeat === true) { $('body').bind("keydown", this.__keydown_handler); $('body').bind('keyup', this.__keyup_handler); diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index f5af6b8948350..ff59aff918a38 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -622,9 +622,11 @@ def _parse_import_data_recursive(self, model, prefix, data, import_fields, optio # versions, for both data and pattern user_format = pycompat.to_native(options.get('%s_format' % field['type'])) for num, line in enumerate(data): + if line[index]: + line[index] = line[index].strip() if line[index]: try: - line[index] = dt.strftime(dt.strptime(pycompat.to_native(line[index].strip()), user_format), server_format) + line[index] = dt.strftime(dt.strptime(pycompat.to_native(line[index]), user_format), server_format) except ValueError as e: raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, e)) except Exception as e: diff --git a/addons/hr/models/mail_alias.py b/addons/hr/models/mail_alias.py index f98fce32f5ae9..c224d21df074b 100644 --- a/addons/hr/models/mail_alias.py +++ b/addons/hr/models/mail_alias.py @@ -14,7 +14,7 @@ class MailAlias(models.AbstractModel): _inherit = 'mail.alias.mixin' def _alias_check_contact_on_record(self, record, message, message_dict, alias): - if alias.alias_contact == 'employees' and record.ids: + if alias.alias_contact == 'employees': email_from = tools.decode_message_header(message, 'From') email_address = tools.email_split(email_from)[0] employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1) diff --git a/addons/l10n_ch/i18n_extra/de_CH.po b/addons/l10n_ch/i18n_extra/de_CH.po index 23e7511b4729b..0f9c0b787265e 100644 --- a/addons/l10n_ch/i18n_extra/de_CH.po +++ b/addons/l10n_ch/i18n_extra/de_CH.po @@ -604,7 +604,7 @@ msgstr "Bankverbindlichkeiten" #: model:account.account,name:l10n_ch.1_ch_coa_2160 #: model:account.account.template,name:l10n_ch.ch_coa_2160 msgid "Dettes envers l'actionnaire" -msgstr "" +msgstr "Gesellschafterverbindlichkeiten" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_3806 @@ -612,7 +612,7 @@ msgstr "" #: model:account.account.template,name:l10n_ch.ch_coa_3806 #: model:account.account.template,name:l10n_ch.ch_coa_4906 msgid "Différences de change" -msgstr "" +msgstr "Währungsdifferenzen" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_2261 @@ -624,7 +624,7 @@ msgstr "Beschlossene Ausschüttungen" #: model:account.account,name:l10n_ch.1_ch_coa_4071 #: model:account.account.template,name:l10n_ch.ch_coa_4071 msgid "Droits de douanes à l'importation" -msgstr "" +msgstr "Einfuhrzölle" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_1109 @@ -648,7 +648,7 @@ msgstr "Abrechnungskonto MWST" #: model:account.account,name:l10n_ch.1_ch_coa_4009 #: model:account.account.template,name:l10n_ch.ch_coa_4009 msgid "Déductions obtenues sur achats" -msgstr "" +msgstr "Einkaufsrabatte" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_3009 @@ -714,7 +714,7 @@ msgstr "Unterhalt, Reparaturen, Ersatz mobile Sachanlagen" #: model:account.account,name:l10n_ch.1_ch_coa_1570 #: model:account.account.template,name:l10n_ch.ch_coa_1570 msgid "Equipements et Installations" -msgstr "" +msgstr "Feste Einrichtungen und Installationen" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_3800 @@ -722,7 +722,7 @@ msgstr "" #: model:account.account.template,name:l10n_ch.ch_coa_3800 #: model:account.account.template,name:l10n_ch.ch_coa_4900 msgid "Escomptes" -msgstr "" +msgstr "Erlösminderungen" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_4530 @@ -734,31 +734,31 @@ msgstr "Benzin" #: model:account.account,name:l10n_ch.1_ch_coa_3804 #: model:account.account.template,name:l10n_ch.ch_coa_3804 msgid "Frais d'encaissement" -msgstr "Frais d'encaissement" +msgstr "Inkassoaufwand" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_3807 #: model:account.account.template,name:l10n_ch.ch_coa_3807 msgid "Frais d'expédition" -msgstr "Frais d'expédition" +msgstr "Versandkosten" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_4072 #: model:account.account.template,name:l10n_ch.ch_coa_4072 msgid "Frais de transport à l'achat" -msgstr "Frais de transport à l'achat" +msgstr "Transportkosten" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_4070 #: model:account.account.template,name:l10n_ch.ch_coa_4070 msgid "Frêts à l'achat" -msgstr "" +msgstr "Frachtkosten" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_4510 #: model:account.account.template,name:l10n_ch.ch_coa_4510 msgid "Gaz" -msgstr "Gaz" +msgstr "Gas" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_1770 @@ -992,7 +992,7 @@ msgstr "Beteiligungen" #: model:account.account,name:l10n_ch.1_ch_coa_4086 #: model:account.account.template,name:l10n_ch.ch_coa_4086 msgid "Pertes de matières" -msgstr "" +msgstr "Warenschwund" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_3805 @@ -1125,13 +1125,13 @@ msgstr "Werbeaufwand" #: model:account.account,name:l10n_ch.1_ch_coa_3801 #: model:account.account.template,name:l10n_ch.ch_coa_3801 msgid "Rabais et réduction de prix" -msgstr "" +msgstr "Rabatte und Preisreduktionen" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_4901 #: model:account.account.template,name:l10n_ch.ch_coa_4901 msgid "Rabais et réductions de prix" -msgstr "" +msgstr "Rabatte und Preisreduktionen" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_3802 @@ -1139,7 +1139,7 @@ msgstr "" #: model:account.account.template,name:l10n_ch.ch_coa_3802 #: model:account.account.template,name:l10n_ch.ch_coa_4092 msgid "Ristournes" -msgstr "" +msgstr "Rückvergütungen" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_2940 @@ -1473,13 +1473,13 @@ msgstr "Wertschriften" #: model:account.account,name:l10n_ch.1_transfer_account_id #: model:account.account.template,name:l10n_ch.transfer_account_id msgid "Transferts internes" -msgstr "" +msgstr "Interne Verrechnungen" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_1280 #: model:account.account.template,name:l10n_ch.ch_coa_1280 msgid "Travaux en cours" -msgstr "Unfertige Erzeugnisse" +msgstr "Nicht fakturierte Dienstleistungen" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_3940 @@ -1491,19 +1491,19 @@ msgstr "Bestandesänderungen nicht fakturierte Dienstleistungen" #: model:account.account,name:l10n_ch.1_ch_coa_1287 #: model:account.account.template,name:l10n_ch.ch_coa_1287 msgid "Variation de la valeur des travaux en cours" -msgstr "" +msgstr "Bestandesänderungen nicht fakturierte Dienstleistungen" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_1277 #: model:account.account.template,name:l10n_ch.ch_coa_1277 msgid "Variation de stock produits semi-ouvrés" -msgstr "" +msgstr "Bestandesänderungen unfertige Erzeugnisse" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_1267 #: model:account.account.template,name:l10n_ch.ch_coa_1267 msgid "Variation de stocks de produits finis" -msgstr "" +msgstr "Bestandesänderungen fertige Erzeugnisse" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_1207 @@ -1511,13 +1511,13 @@ msgstr "" #: model:account.account.template,name:l10n_ch.ch_coa_1207 #: model:account.account.template,name:l10n_ch.ch_coa_4800 msgid "Variation des stocks de marchandises" -msgstr "" +msgstr "Bestandesänderungen Handelsware" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_4801 #: model:account.account.template,name:l10n_ch.ch_coa_4801 msgid "Variation des stocks de matières premières" -msgstr "" +msgstr "Bestandesänderungen Rohstoffe" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_3901 @@ -1535,7 +1535,7 @@ msgstr "Bestandesänderungen unfertige Erzeugnisse" #: model:account.account,name:l10n_ch.1_ch_coa_1217 #: model:account.account.template,name:l10n_ch.ch_coa_1217 msgid "Variation des stocks des matières premières" -msgstr "" +msgstr "Bestandesänderungen Rohstoffe" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_4008 @@ -1543,7 +1543,7 @@ msgstr "" #: model:account.account.template,name:l10n_ch.ch_coa_4008 #: model:account.account.template,name:l10n_ch.ch_coa_4080 msgid "Variations de stocks" -msgstr "" +msgstr "Inventurdifferenzen" #. module: l10n_ch #: model:account.account,name:l10n_ch.1_ch_coa_3200 diff --git a/addons/l10n_ch/static/src/less/report_isr.less b/addons/l10n_ch/static/src/less/report_isr.less index 913404727d25c..83de6851409a7 100644 --- a/addons/l10n_ch/static/src/less/report_isr.less +++ b/addons/l10n_ch/static/src/less/report_isr.less @@ -19,12 +19,18 @@ body.l10n_ch_isr { } } } -} -/* content outside isr needs margins to not overlap header */ -body.l10n_ch_isr #content_outside_isr { - padding: 15px; - padding-top: 150px; + /* content outside isr needs margins to not overlap header */ + #content_outside_isr { + padding: 15px; + padding-top: 150px; + } + + /* ISR is intended for pre-printed paper, we don't want stylistic background */ + .o_report_layout_background { + background: none; + min-height: 0; + } } body.l10n_ch_isr #isr { diff --git a/addons/mail/i18n/zh_CN.po b/addons/mail/i18n/zh_CN.po index 94e1807945b12..416f31bcffc31 100644 --- a/addons/mail/i18n/zh_CN.po +++ b/addons/mail/i18n/zh_CN.po @@ -144,10 +144,6 @@ msgid "" "object.parent_id.subject) or (object.parent_id and " "object.parent_id.record_name and 'Re: %s' % object.parent_id.record_name)}" msgstr "" -"${object.subject 或 (object.record_name 和 'Re: %s' % object.record_name) 或 " -"(object.parent_id and object.parent_id.subject 和 'Re: %s' % " -"object.parent_id.subject) 或 (object.parent_id 和 object.parent_id.record_name" -" 和 'Re: %s' % object.parent_id.record_name)}" #. module: mail #. openerp-web diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index c35bb46cdd396..f3dcca4acbbb3 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -508,22 +508,19 @@ - - - - - + - + + + diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 6989cf2b1a6e6..5eeef7641c2fb 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -794,6 +794,8 @@ def convert_links(self): def _process_mass_mailing_queue(self): mass_mailings = self.search([('state', 'in', ('in_queue', 'sending')), '|', ('schedule_date', '<', fields.Datetime.now()), ('schedule_date', '=', False)]) for mass_mailing in mass_mailings: + user = mass_mailing.write_uid or self.env.user + mass_mailing = mass_mailing.with_context(**user.sudo(user=user).context_get()) if len(mass_mailing.get_remaining_recipients()) > 0: mass_mailing.state = 'sending' mass_mailing.send_mail() diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index d9617a633a28b..a67aea945d792 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -686,6 +686,10 @@ def form_feedback(self, data, acquirer_name): return True + @api.multi + def _post_process_after_done(self, **kwargs): + return True + # -------------------------------------------------- # SERVER2SERVER RELATED METHODS # -------------------------------------------------- diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index d93ca1f6d276a..64f24c9527150 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -281,7 +281,7 @@