diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py index a697e35098..d42d1579fd 100644 --- a/addons/account/models/account_journal.py +++ b/addons/account/models/account_journal.py @@ -356,10 +356,22 @@ class AccountJournal(models.Model): self.refund_sequence = self.type in ('sale', 'purchase') def _get_alias_values(self, type, alias_name=None): + """ This function verifies that the user-given mail alias (or its fallback) doesn't contain non-ascii chars. + The fallbacks are the journal's name, code, or type - these are suffixed with the + company's name or id (in case the company's name is not ascii either). + """ + def get_company_suffix(): + if self.company_id != self.env.ref('base.main_company'): + try: + remove_accents(self.company_id.name).encode('ascii') + return '-' + str(self.company_id.name) + except UnicodeEncodeError: + return '-' + str(self.company_id.id) + return '' + if not alias_name: alias_name = self.name - if self.company_id != self.env.ref('base.main_company'): - alias_name += '-' + str(self.company_id.name) + alias_name += get_company_suffix() try: remove_accents(alias_name).encode('ascii') except UnicodeEncodeError: @@ -368,6 +380,7 @@ class AccountJournal(models.Model): safe_alias_name = self.code except UnicodeEncodeError: safe_alias_name = self.type + safe_alias_name += get_company_suffix() _logger.warning("Cannot use '%s' as email alias, fallback to '%s'", alias_name, safe_alias_name) alias_name = safe_alias_name diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index c4f71d9b4d..094c3d5b89 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -2503,17 +2503,12 @@ class AccountMove(models.Model): if cancel: reverse_moves.with_context(move_reverse_cancel=cancel)._post(soft=False) for move, reverse_move in zip(self, reverse_moves): - lines = move.line_ids.filtered( - lambda x: (x.account_id.reconcile or x.account_id.internal_type == 'liquidity') - and not x.reconciled - ) - for line in lines: - counterpart_lines = reverse_move.line_ids.filtered( - lambda x: x.account_id == line.account_id - and x.currency_id == line.currency_id - and not x.reconciled - ) - (line + counterpart_lines).with_context(move_reverse_cancel=cancel).reconcile() + group = defaultdict(list) + for line in move.line_ids + reverse_move.line_ids: + group[(line.account_id, line.currency_id)].append(line.id) + for (account, dummy), line_ids in group.items(): + if account.reconcile or account.internal_type == 'liquidity': + self.env['account.move.line'].browse(line_ids).reconcile() return reverse_moves diff --git a/addons/account/tests/test_account_journal.py b/addons/account/tests/test_account_journal.py index e8c68dccbf..8aaf100565 100644 --- a/addons/account/tests/test_account_journal.py +++ b/addons/account/tests/test_account_journal.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import flectra.tools from flectra.addons.account.tests.common import AccountTestInvoicingCommon from flectra.tests import tagged from flectra.exceptions import UserError, ValidationError @@ -82,3 +83,22 @@ class TestAccountJournal(AccountTestInvoicingCommon): # Assigning both should be allowed self.company_data['default_journal_misc'].account_control_ids = \ self.company_data['default_account_revenue'] + self.company_data['default_account_expense'] + + @flectra.tools.mute_logger('flectra.addons.account.models.account_journal') + def test_account_journal_alias_name(self): + journal = self.company_data['default_journal_misc'] + self.assertEqual(journal._get_alias_values(journal.type)['alias_name'], 'Miscellaneous Operations-company_1_data') + self.assertEqual(journal._get_alias_values(journal.type, 'ぁ')['alias_name'], 'MISC-company_1_data') + journal.name = 'ぁ' + self.assertEqual(journal._get_alias_values(journal.type)['alias_name'], 'MISC-company_1_data') + journal.code = 'ぁ' + self.assertEqual(journal._get_alias_values(journal.type)['alias_name'], 'general-company_1_data') + + self.company_data_2['company'].name = 'ぁ' + company_2_id = str(self.company_data_2['company'].id) + journal_2 = self.company_data_2['default_journal_sale'] + self.assertEqual(journal_2._get_alias_values(journal_2.type)['alias_name'], 'Customer Invoices-' + company_2_id) + journal_2.name = 'ぁ' + self.assertEqual(journal_2._get_alias_values(journal_2.type)['alias_name'], 'INV-' + company_2_id) + journal_2.code = 'ぁ' + self.assertEqual(journal_2._get_alias_values(journal_2.type)['alias_name'], 'sale-' + company_2_id) diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 530d481299..1e55c5fd85 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -905,6 +905,63 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, ]) + def test_reverse_with_multiple_lines(self): + """ + Test if all lines from a reversed entry are fully reconciled + """ + move = self.env['account.move'].create({ + 'move_type': 'entry', + 'line_ids': [ + (0, 0, { + 'debit': 1200.0, + 'credit': 0.0, + 'amount_currency': 3600.0, + 'account_id': self.company_data['default_account_receivable'].id, + }), + (0, 0, { + 'debit': 0.0, + 'credit': 200.0, + 'account_id': self.company_data['default_account_payable'].id, + }), + (0, 0, { + 'debit': 0.0, + 'credit': 400.0, + 'account_id': self.company_data['default_account_payable'].id, + }), + (0, 0, { + 'debit': 0.0, + 'credit': 600.0, + 'account_id': self.company_data['default_account_payable'].id, + }), + ], + }) + + move.action_post() + + lines_to_reconcile = move.line_ids.filtered(lambda x: (x.account_id.reconcile or x.account_id.internal_type == 'liquidity') and not x.reconciled) + + self.assertRecordValues(lines_to_reconcile, [ + {'debit': 1200.0, 'credit': 0.0, 'reconciled': False}, + {'debit': 0.0, 'credit': 200.0, 'reconciled': False}, + {'debit': 0.0, 'credit': 400.0, 'reconciled': False}, + {'debit': 0.0, 'credit': 600.0, 'reconciled': False}, + ]) + + reversed_move = move._reverse_moves(cancel=True) + + reversed_lines = reversed_move.line_ids.filtered(lambda x: ( + x.account_id.reconcile or x.account_id.internal_type == 'liquidity' + )) + + self.assertRecordValues(reversed_lines, [ + {'debit': 0.0, 'credit': 1200.0, 'reconciled': True}, + {'debit': 200.0, 'credit': 0.0, 'reconciled': True}, + {'debit': 400.0, 'credit': 0.0, 'reconciled': True}, + {'debit': 600.0, 'credit': 0.0, 'reconciled': True}, + ]) + + self.assertTrue(all([line.full_reconcile_id for line in reversed_lines])) + def test_reverse_exchange_difference_same_foreign_currency(self): move_2016 = self.env['account.move'].create({ 'move_type': 'entry', diff --git a/addons/account_check_printing/models/account_journal.py b/addons/account_check_printing/models/account_journal.py index 4661389e74..7136d47585 100644 --- a/addons/account_check_printing/models/account_journal.py +++ b/addons/account_check_printing/models/account_journal.py @@ -36,7 +36,7 @@ class AccountJournal(models.Model): for journal in self: sequence = journal.check_sequence_id if sequence: - journal.check_next_number = sequence.next_by_id() + journal.check_next_number = sequence.get_next_char(sequence.number_next_actual) else: journal.check_next_number = 1 diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py index 9a4a1560c3..7d1fbb1ecb 100644 --- a/addons/account_check_printing/models/account_payment.py +++ b/addons/account_check_printing/models/account_payment.py @@ -85,12 +85,14 @@ class AccountPayment(models.Model): else: pay.check_amount_in_words = False - @api.depends('journal_id', 'payment_method_code', 'state') + @api.depends('journal_id', 'payment_method_code') def _compute_check_number(self): - for pay in self.filtered(lambda p: p.state == 'posted'): - if pay.journal_id.check_manual_sequencing and pay.payment_method_code == 'check_printing' and not pay.check_number: + for pay in self: + if pay.journal_id.check_manual_sequencing and pay.payment_method_code == 'check_printing': sequence = pay.journal_id.check_sequence_id - pay.check_number = sequence.next_by_id() + pay.check_number = sequence.get_next_char(sequence.number_next_actual) + else: + pay.check_number = False def _inverse_check_number(self): for payment in self: @@ -106,6 +108,14 @@ class AccountPayment(models.Model): if record.payment_type == 'outbound' and preferred in record.journal_id.outbound_payment_method_ids: record.payment_method_id = preferred + def action_post(self): + res = super(AccountPayment, self).action_post() + payment_method_check = self.env.ref('account_check_printing.account_payment_method_check') + for payment in self.filtered(lambda p: p.payment_method_id == payment_method_check and p.check_manual_sequencing): + sequence = payment.journal_id.check_sequence_id + payment.check_number = sequence.next_by_id() + return res + def print_checks(self): """ Check that the recordset is valid, set the payments state to sent and call print_checks() """ # Since this method can be called via a client_action_multi, we need to make sure the received records are what we expect diff --git a/addons/account_check_printing/tests/test_print_check.py b/addons/account_check_printing/tests/test_print_check.py index 25f8a52de5..63ef2c0805 100644 --- a/addons/account_check_printing/tests/test_print_check.py +++ b/addons/account_check_printing/tests/test_print_check.py @@ -134,30 +134,3 @@ class TestPrintCheck(AccountTestInvoicingCommon): 'amount_paid': f'150.000{NON_BREAKING_SPACE}☺', 'currency': invoice.currency_id, }]]) - - def test_in_invoice_check_manual_sequencing_with_multiple_payments(self): - """ - Test the check generation for vendor bills with multiple payments. - """ - nb_invoices_to_test = INV_LINES_PER_STUB + 1 - - self.company_data['default_journal_bank'].write({ - 'check_manual_sequencing': True, - 'check_next_number': '11111', - }) - - in_invoices = self.env['account.move'].create([{ - 'move_type': 'in_invoice', - 'partner_id': self.partner_a.id, - 'date': '2017-01-01', - 'invoice_date': '2017-01-01', - 'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 100.0})] - } for i in range(nb_invoices_to_test)]) - in_invoices.action_post() - - payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=in_invoices.ids).create({ - 'group_payment': False, - 'payment_method_id': self.payment_method_check.id, - })._create_payments() - - self.assertEqual(set(payments.mapped('check_number')), {str(x) for x in range(11111, 11121)}) diff --git a/addons/l10n_id_efaktur/models/account_move.py b/addons/l10n_id_efaktur/models/account_move.py index ed632a6634..e82ab3c07d 100644 --- a/addons/l10n_id_efaktur/models/account_move.py +++ b/addons/l10n_id_efaktur/models/account_move.py @@ -5,6 +5,7 @@ import base64 import re from flectra import api, fields, models, _ from flectra.exceptions import UserError, ValidationError +from flectra.tools import float_round FK_HEAD_LIST = ['FK', 'KD_JENIS_TRANSAKSI', 'FG_PENGGANTI', 'NOMOR_FAKTUR', 'MASA_PAJAK', 'TAHUN_PAJAK', 'TANGGAL_FAKTUR', 'NPWP', 'NAMA', 'ALAMAT_LENGKAP', 'JUMLAH_DPP', 'JUMLAH_PPN', 'JUMLAH_PPNBM', 'ID_KETERANGAN_TAMBAHAN', 'FG_UANG_MUKA', 'UANG_MUKA_DPP', 'UANG_MUKA_PPN', 'UANG_MUKA_PPNBM', 'REFERENSI', 'KODE_DOKUMEN_PENDUKUNG'] @@ -172,16 +173,16 @@ class AccountMove(models.Model): eTax['NPWP'] = invoice_npwp eTax['NAMA'] = move.partner_id.name if eTax['NPWP'] == '000000000000000' else move.partner_id.l10n_id_tax_name or move.partner_id.name eTax['ALAMAT_LENGKAP'] = move.partner_id.contact_address.replace('\n', '') if eTax['NPWP'] == '000000000000000' else move.partner_id.l10n_id_tax_address or street - eTax['JUMLAH_DPP'] = int(round(move.amount_untaxed, 0)) # currency rounded to the unit - eTax['JUMLAH_PPN'] = int(round(move.amount_tax, 0)) + eTax['JUMLAH_DPP'] = int(float_round(move.amount_untaxed, 0)) # currency rounded to the unit + eTax['JUMLAH_PPN'] = int(float_round(move.amount_tax, 0)) eTax['ID_KETERANGAN_TAMBAHAN'] = '1' if move.l10n_id_kode_transaksi == '07' else '' eTax['REFERENSI'] = number_ref eTax['KODE_DOKUMEN_PENDUKUNG'] = '0' lines = move.line_ids.filtered(lambda x: x.product_id.id == int(dp_product_id) and x.price_unit < 0 and not x.display_type) eTax['FG_UANG_MUKA'] = 0 - eTax['UANG_MUKA_DPP'] = int(abs(sum(lines.mapped(lambda l: round(l.price_subtotal, 0))))) - eTax['UANG_MUKA_PPN'] = int(abs(sum(lines.mapped(lambda l: round(l.price_total - l.price_subtotal, 0))))) + eTax['UANG_MUKA_DPP'] = int(abs(sum(lines.mapped(lambda l: float_round(l.price_subtotal, 0))))) + eTax['UANG_MUKA_PPN'] = int(abs(sum(lines.mapped(lambda l: float_round(l.price_total - l.price_subtotal, 0))))) company_npwp = company_id.partner_id.vat or '000000000000000' @@ -214,10 +215,10 @@ class AccountMove(models.Model): line_dict = { 'KODE_OBJEK': line.product_id.default_code or '', 'NAMA': line.product_id.name or '', - 'HARGA_SATUAN': int(round(invoice_line_unit_price, 0)), + 'HARGA_SATUAN': int(float_round(invoice_line_unit_price, 0)), 'JUMLAH_BARANG': line.quantity, - 'HARGA_TOTAL': int(round(invoice_line_total_price, 0)), - 'DPP': int(round(line.price_subtotal, 0)), + 'HARGA_TOTAL': int(float_round(invoice_line_total_price, 0)), + 'DPP': int(float_round(line.price_subtotal, 0)), 'product_id': line.product_id.id, } @@ -226,16 +227,16 @@ class AccountMove(models.Model): free_tax_line += (line.price_subtotal * (tax.amount / 100.0)) * -1.0 line_dict.update({ - 'DISKON': int(round(invoice_line_total_price - line.price_subtotal, 0)), - 'PPN': int(round(free_tax_line, 0)), + 'DISKON': int(float_round(invoice_line_total_price - line.price_subtotal, 0)), + 'PPN': int(float_round(free_tax_line, 0)), }) free.append(line_dict) elif line.price_subtotal != 0.0: invoice_line_discount_m2m = invoice_line_total_price - line.price_subtotal line_dict.update({ - 'DISKON': int(round(invoice_line_discount_m2m, 0)), - 'PPN': int(round(tax_line, 0)), + 'DISKON': int(float_round(invoice_line_discount_m2m, 0)), + 'PPN': int(float_round(tax_line, 0)), }) sales.append(line_dict) @@ -258,14 +259,14 @@ class AccountMove(models.Model): if tax.amount > 0: tax_line += sale['DPP'] * (tax.amount / 100.0) - sale['PPN'] = int(round(tax_line, 0)) + sale['PPN'] = int(float_round(tax_line, 0)) free.remove(f) sub_total_before_adjustment += sale['DPP'] sub_total_ppn_before_adjustment += sale['PPN'] bruto_total += sale['DISKON'] - total_discount += round(sale['DISKON'], 2) + total_discount += float_round(sale['DISKON'], 2) output_head += _csv_row(fk_values_list, delimiter) output_head += _csv_row(lt_values_list, delimiter) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 00ff3f7d3a..7c457a53f6 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -1107,8 +1107,15 @@ class PurchaseOrderLine(models.Model): price_unit = seller.product_uom._compute_price(price_unit, self.product_uom) self.price_unit = price_unit - product_ctx = {'seller_id': seller.id, 'lang': get_lang(self.env, self.partner_id.lang).code} - if seller.product_name: + + default_names = [] + vendors = self.product_id._prepare_sellers({}) + for vendor in vendors: + product_ctx = {'seller_id': vendor.id, 'lang': get_lang(self.env, self.partner_id.lang).code} + default_names.append(self._get_product_purchase_description(self.product_id.with_context(product_ctx))) + + if (self.name in default_names or not self.name): + product_ctx = {'seller_id': seller.id, 'lang': get_lang(self.env, self.partner_id.lang).code} self.name = self._get_product_purchase_description(self.product_id.with_context(product_ctx)) @api.depends('product_uom', 'product_qty', 'product_id.uom_id') diff --git a/addons/purchase/tests/test_purchase.py b/addons/purchase/tests/test_purchase.py index 701405da1b..247db170a5 100644 --- a/addons/purchase/tests/test_purchase.py +++ b/addons/purchase/tests/test_purchase.py @@ -217,3 +217,21 @@ class TestPurchase(AccountTestInvoicingCommon): self.assertEqual(po.order_line[0].price_unit, 200) self.assertEqual(po.order_line[1].price_unit, 1200) + + def test_on_change_quantity_description(self): + """ + When a user changes the quantity of a product in a purchase order it + should not change the description if the descritpion was changed by + the user before + """ + self.env.user.write({'company_id': self.company_data['company'].id}) + + po = Form(self.env['purchase.order']) + po.partner_id = self.partner_a + with po.order_line.new() as pol: + pol.product_id = self.product_a + pol.product_qty = 1 + + pol.name = "New custom description" + pol.product_qty += 1 + self.assertEqual(pol.name, "New custom description") diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index 9bf5bfa8f5..1194dbcbc7 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -13,6 +13,7 @@ const dom = require('web.dom'); var mixins = require('web.mixins'); var publicWidget = require('web.public.widget'); var utils = require('web.utils'); +const wUtils = require('website.utils'); var qweb = core.qweb; @@ -1166,6 +1167,73 @@ registry.BottomFixedElement = publicWidget.Widget.extend({ }, }); +/** + * The websites, by default, use image lazy loading via the loading="lazy" + * attribute on elements. However, this does not work great on all + * browsers. This widget fixes the behaviors with as less code as possible. + */ +registry.ImagesLazyLoading = publicWidget.Widget.extend({ + selector: '#wrapwrap', + + /** + * @override + */ + start() { + // For each image on the page, force a 1px min-height so that Chrome + // understands the image exists on different zoom sizes of the browser. + // Indeed, without this, on a 90% zoom, some images were never loaded. + // Once the image has been loaded, the 1px min-height is removed. + // Note: another possible solution without JS would be this CSS rule: + // ``` + // [loading="lazy"] { + // min-height: 1px; + // } + // ``` + // This would solve the problem the same way with a CSS rule with a + // very small priority (any class setting a min-height would still have + // priority). However, the min-height would always be forced even once + // the image is loaded, which could mess with some layouts relying on + // the image intrinsic min-height. + const imgEls = this.$target[0].querySelectorAll('img[loading="lazy"]'); + for (const imgEl of imgEls) { + // Write initial min-height on the dataset, so that it can also + // be properly restored on widget destroy. + imgEl.dataset.lazyLoadingInitialMinHeight = imgEl.style.minHeight; + imgEl.style.minHeight = '1px'; + wUtils.onceAllImagesLoaded($(imgEl)).then(() => { + if (this.isDestroyed()) { + return; + } + this._restoreImage(imgEl); + }); + } + return this._super(...arguments); + }, + /** + * @override + */ + destroy() { + this._super(...arguments); + const imgEls = this.$target[0].querySelectorAll('img[data-lazy-loading-initial-min-height]'); + for (const imgEl of imgEls) { + this._restoreImage(imgEl); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {HTMLImageElement} imgEl + */ + _restoreImage(imgEl) { + imgEl.style.minHeight = imgEl.dataset.lazyLoadingInitialMinHeight; + delete imgEl.dataset.lazyLoadingInitialMinHeight; + }, +}); + return { Widget: publicWidget.Widget, Animation: Animation,