diff --git a/addons/account/README.md b/addons/account/README.md index 2cd9bcb645..da16c7caf8 100644 --- a/addons/account/README.md +++ b/addons/account/README.md @@ -1,7 +1,7 @@ -Flectra Accounting +Odoo Accounting --------------- -The Flectra Open Source Accounting app allows a better way to +The Odoo Open Source Accounting app allows a better way to collaborate with your accountants, your customers and control your suppliers. Activate features on demand, from integrated analytic accounting to budget, @@ -11,7 +11,7 @@ A Smart User Interface ---------------------- Record transactions in a few clicks and easily manage all financial activities -in one place. Flectra's user interface is designed with productivity in mind. +in one place. Odoo's user interface is designed with productivity in mind. A Better Way To Work – Together ------------------------------- @@ -57,12 +57,12 @@ Everything you need to grow --------------------------- Manage your assets, track expenses, control budgets, multi-level analytic -accounting; Flectra has all the features you need to sustain all your business +accounting; Odoo has all the features you need to sustain all your business activities. Scale With Your Organization ---------------------------- -Flectra supports multiple currencies, multiple users with different access rights, +Odoo supports multiple currencies, multiple users with different access rights, multiple companies with real time consolidation and unlimited analytic plans. diff --git a/addons/account/__init__.py b/addons/account/__init__.py index a9bdf41705..07c28399c0 100644 --- a/addons/account/__init__.py +++ b/addons/account/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +# Part of Odoo. See LICENSE file for full copyright and licensing details. from . import controllers from . import models diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 915d252677..80e83f1987 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +# Part of Odoo. See LICENSE file for full copyright and licensing details. { 'name' : 'Invoicing', 'version' : '1.1', @@ -8,12 +8,12 @@ 'description': """ Invoicing & Payments ==================== -The specific and easy-to-use Invoicing system in Flectra allows you to keep track of your accounting, even when you are not an accountant. It provides an easy way to follow up on your vendors and customers. +The specific and easy-to-use Invoicing system in Odoo allows you to keep track of your accounting, even when you are not an accountant. It provides an easy way to follow up on your vendors and customers. You could use this simplified accounting in case you work with an (external) account to keep your books, and you still want to keep track of payments. This module also offers you an easy method of registering payments, without having to encode complete abstracts of account. """, 'category': 'Accounting/Accounting', - 'website': 'https://www.flectrahq.com/page/billing', + 'website': 'https://www.odoo.com/page/billing', 'images' : ['images/accounts.jpeg','images/bank_statement.jpeg','images/cash_register.jpeg','images/chart_of_accounts.jpeg','images/customer_invoice.jpeg','images/journal_entries.jpeg'], 'depends' : ['base_setup', 'product', 'analytic', 'portal', 'digest'], 'data': [ diff --git a/addons/account/controllers/__init__.py b/addons/account/controllers/__init__.py index 34c958ef3a..1a195b7175 100644 --- a/addons/account/controllers/__init__.py +++ b/addons/account/controllers/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +# Part of Odoo. See LICENSE file for full copyright and licensing details. from . import onboarding from . import portal diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py index 78eeb90973..d9457ae2a6 100644 --- a/addons/account/models/account_journal.py +++ b/addons/account/models/account_journal.py @@ -135,7 +135,7 @@ class AccountJournal(models.Model): compute='_compute_inbound_payment_method_ids', store=True, readonly=False, - help="Manual: Get paid by cash, check or any other method outside of Flectra.\n" + help="Manual: Get paid by cash, check or any other method outside of Odoo, Flectra.\n" "Electronic: Get paid automatically through a payment acquirer by requesting a transaction" " on a card saved by the customer when buying or subscribing online (payment token).\n" "Batch Deposit: Encase several customer checks at once by generating a batch deposit to" @@ -152,8 +152,8 @@ class AccountJournal(models.Model): compute='_compute_outbound_payment_method_ids', store=True, readonly=False, - help="Manual:Pay bill by cash or any other method outside of Flectra.\n" - "Check:Pay bill by check and print it from Flectra.\n" + help="Manual:Pay bill by cash or any other method outside of Odoo, Flectra.\n" + "Check:Pay bill by check and print it from Odoo, Flectra.\n" "SEPA Credit Transfer: Pay bill from a SEPA Credit Transfer file you submit to your" " bank. Enable this option from the settings." ) diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index 1c7f9bca66..ca1b81cb0c 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -131,7 +131,7 @@ class account_journal(models.Model): data[:0] = [build_graph_data(last_month, amount)] [graph_title, graph_key] = self._graph_title_and_key() - color = '#009EFB' if 'e' in version else '#7c7bad' + color = '#875A7B' if 'e' in version else '#7c7bad' is_sample_data = not last_stmt and len(query_result) == 0 if is_sample_data: @@ -231,9 +231,9 @@ class account_journal(models.Model): last_balance = last_statement.balance_end has_at_least_one_statement = bool(last_statement) bank_account_balance, nb_lines_bank_account_balance = self._get_journal_bank_account_balance( - domain=[('move_id.state', '=', 'posted')]) + domain=[('parent_state', '=', 'posted')]) outstanding_pay_account_balance, nb_lines_outstanding_pay_account_balance = self._get_journal_outstanding_payments_account_balance( - domain=[('move_id.state', '=', 'posted')]) + domain=[('parent_state', '=', 'posted')]) self._cr.execute(''' SELECT COUNT(st_line.id) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 4b12c463f9..71d1b8edc0 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -2124,7 +2124,7 @@ class AccountMove(models.Model): values['total_amount_currency'] += sign * line.amount_currency values['total_residual_currency'] += sign * line.amount_residual_currency - elif not line.tax_exigible: + elif not line.tax_exigible and not line.reconciled: values['to_process_lines'] += line currencies.add(line.currency_id or line.company_currency_id) @@ -2594,6 +2594,8 @@ class AccountMove(models.Model): if not self.env.su and not self.env.user.has_group('account.group_account_invoice'): raise AccessError(_("You don't have the access rights to post an invoice.")) for move in to_post: + if move.partner_bank_id and not move.partner_bank_id.active: + raise UserError(_("The recipient bank account link to this invoice is archived.\nSo you cannot confirm the invoice.")) if move.state == 'posted': raise UserError(_('The entry %s (id %s) is already posted.') % (move.name, move.id)) if not move.line_ids.filtered(lambda line: not line.display_type): diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 248935b416..3038f7ba05 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from lxml import etree from flectra import models, fields, api, _ from flectra.exceptions import UserError, ValidationError @@ -44,10 +45,14 @@ class AccountPayment(models.Model): is_matched = fields.Boolean(string="Is Matched With a Bank Statement", store=True, compute='_compute_reconciliation_status', help="Technical field indicating if the payment has been matched with a statement line.") + available_partner_bank_ids = fields.Many2many( + comodel_name='res.partner.bank', + compute='_compute_available_partner_bank_ids', + ) partner_bank_id = fields.Many2one('res.partner.bank', string="Recipient Bank Account", readonly=False, store=True, compute='_compute_partner_bank_id', - domain="[('partner_id', '=', partner_id)]", + domain="[('id', 'in', available_partner_bank_ids)]", check_company=True) is_internal_transfer = fields.Boolean(string="Is Internal Transfer", readonly=False, store=True, @@ -61,9 +66,9 @@ class AccountPayment(models.Model): readonly=False, store=True, compute='_compute_payment_method_id', domain="[('id', 'in', available_payment_method_ids)]", - help="Manual: Get paid by cash, check or any other method outside of Flectra.\n"\ + help="Manual: Get paid by cash, check or any other method outside of Odoo, Flectra.\n"\ "Electronic: Get paid automatically through a payment acquirer by requesting a transaction on a card saved by the customer when buying or subscribing online (payment token).\n"\ - "Check: Pay bill by check and print it from Flectra.\n"\ + "Check: Pay bill by check and print it from Odoo, Flectra.\n"\ "Batch Deposit: Encase several customer checks at once by generating a batch deposit to submit to your bank. When encoding the bank statement in Flectra, you are suggested to reconcile the transaction with the batch deposit.To enable batch deposit, module account_batch_payment must be installed.\n"\ "SEPA Credit Transfer: Pay bill from a SEPA Credit Transfer file you submit to your bank. To enable sepa credit transfer, module account_sepa must be installed ") available_payment_method_ids = fields.Many2many('account.payment.method', @@ -332,20 +337,19 @@ class AccountPayment(models.Model): payment.require_partner_bank_account = payment.state == 'draft' and payment.payment_method_code in self._get_method_codes_needing_bank_account() @api.depends('partner_id', 'company_id', 'payment_type') + def _compute_available_partner_bank_ids(self): + for pay in self: + if pay.payment_type == 'inbound': + pay.available_partner_bank_ids = pay.journal_id.bank_account_id + else: + pay.available_partner_bank_ids = pay.partner_id.bank_ids\ + .filtered(lambda x: x.company_id.id in (False, pay.company_id.id))._origin + + @api.depends('available_partner_bank_ids', 'journal_id') def _compute_partner_bank_id(self): ''' The default partner_bank_id will be the first available on the partner. ''' for pay in self: - if pay.payment_type == 'inbound': - bank_partner = pay.company_id.partner_id - else: - bank_partner = pay.partner_id - - available_partner_bank_accounts = bank_partner.bank_ids.filtered(lambda x: x.company_id.id in (False, pay.company_id.id)) - if available_partner_bank_accounts: - if pay.partner_bank_id not in available_partner_bank_accounts: - pay.partner_bank_id = available_partner_bank_accounts[0]._origin - else: - pay.partner_bank_id = False + pay.partner_bank_id = pay.available_partner_bank_ids[:1]._origin @api.depends('partner_id', 'destination_account_id', 'journal_id') def _compute_is_internal_transfer(self): @@ -573,6 +577,29 @@ class AccountPayment(models.Model): # LOW-LEVEL METHODS # ------------------------------------------------------------------------- + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + # OVERRIDE to add the 'available_partner_bank_ids' field dynamically inside the view. + # TO BE REMOVED IN MASTER + res = super().fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + if view_type == 'form': + form_view_id = self.env['ir.model.data'].xmlid_to_res_id('account.view_account_payment_form') + if res.get('view_id') == form_view_id: + tree = etree.fromstring(res['arch']) + if len(tree.xpath("//field[@name='available_partner_bank_ids']")) == 0: + # Don't force people to update the account module. + form_view = self.env.ref('account.view_account_payment_form') + arch_tree = etree.fromstring(form_view.arch) + if arch_tree.tag == 'form': + arch_tree.insert(0, etree.Element('field', attrib={ + 'name': 'available_partner_bank_ids', + 'invisible': '1', + })) + form_view.sudo().write({'arch': etree.tostring(arch_tree, encoding='unicode')}) + return super().fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + + return res + @api.model_create_multi def create(self, vals_list): # OVERRIDE diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index 671a50e4b8..5843f402d0 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -296,6 +296,7 @@ class AccountReconcileModel(models.Model): name = ' '.join([x for x in [base_line_dict.get('name', ''), tax_res['name']] if x]) new_aml_dicts.append({ 'account_id': tax_res['account_id'] or base_line_dict['account_id'], + 'journal_id': base_line_dict.get('journal_id', False), 'name': name, 'partner_id': base_line_dict.get('partner_id'), 'balance': balance, @@ -649,30 +650,42 @@ class AccountReconcileModel(models.Model): if partner: st_line_subquery += r" AND aml.partner_id = %s" % partner.id else: - st_line_subquery += r""" - AND - ( - substring(REGEXP_REPLACE(st_line.payment_ref, '[^0-9\s]', '', 'g'), '\S(?:.*\S)*') != '' - AND - ( - (""" + self._get_select_communication_flag() + """) - OR - (""" + self._get_select_payment_reference_flag() + """) - ) - ) - OR - ( - /* We also match statement lines without partners with amls - whose partner's name's parts (splitting on space) are all present - within the payment_ref, in any order, with any characters between them. */ + st_line_fields_consideration = [ + (self.match_text_location_label, 'st_line.payment_ref'), + (self.match_text_location_note, 'st_line_move.narration'), + (self.match_text_location_reference, 'st_line_move.ref'), + ] - aml_partner.name IS NOT NULL - AND """ + unaccent("st_line.payment_ref") + r""" ~* ('^' || ( - SELECT string_agg(concat('(?=.*\m', chunk[1], '\M)'), '') - FROM regexp_matches(""" + unaccent("aml_partner.name") + r""", '\w{3,}', 'g') AS chunk - )) - ) - """ + no_partner_query = " OR ".join([ + r""" + ( + substring(REGEXP_REPLACE(""" + sql_field + """, '[^0-9\s]', '', 'g'), '\S(?:.*\S)*') != '' + AND + ( + (""" + self._get_select_communication_flag() + """) + OR + (""" + self._get_select_payment_reference_flag() + """) + ) + ) + OR + ( + /* We also match statement lines without partners with amls + whose partner's name's parts (splitting on space) are all present + within the payment_ref, in any order, with any characters between them. */ + + aml_partner.name IS NOT NULL + AND """ + unaccent(sql_field) + r""" ~* ('^' || ( + SELECT string_agg(concat('(?=.*\m', chunk[1], '\M)'), '') + FROM regexp_matches(""" + unaccent("aml_partner.name") + r""", '\w{3,}', 'g') AS chunk + )) + ) + """ + for consider_field, sql_field in st_line_fields_consideration + if consider_field + ]) + + if no_partner_query: + st_line_subquery += " AND " + no_partner_query st_lines_queries.append(r"st_line.id = %s AND (%s)" % (st_line.id, st_line_subquery)) diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index a818cda607..3004ad5622 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -524,7 +524,7 @@ class AccountTax(models.Model): price_include = self._context.get('force_price_include', tax.price_include) #compute the tax_amount - if not skip_checkpoint and price_include and total_included_checkpoints.get(i): + if not skip_checkpoint and price_include and total_included_checkpoints.get(i) and sum_repartition_factor != 0: # We know the total to reach for that tax, so we make a substraction to avoid any rounding issues tax_amount = total_included_checkpoints[i] - (base + cumulated_tax_included_amount) cumulated_tax_included_amount = 0 diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 9f955b3722..fa5968c533 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -166,7 +166,7 @@ class AccountChartTemplate(models.Model): return self.env['account.account'].create({ 'name': _("Bank Suspense Account"), 'code': self.env['account.account']._search_new_account_code(company, code_digits, company.bank_account_code_prefix or ''), - 'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id, + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, 'company_id': company.id, }) diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index b7d60e2209..d96562e8fc 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -168,11 +168,17 @@ class AccountFiscalPosition(models.Model): # This can be easily overridden to apply more complex fiscal rules PartnerObj = self.env['res.partner'] partner = PartnerObj.browse(partner_id) + delivery = PartnerObj.browse(delivery_id) - # if no delivery use invoicing - if delivery_id: - delivery = PartnerObj.browse(delivery_id) - else: + company = self.env.company + eu_country_codes = set(self.env.ref('base.europe').country_ids.mapped('code')) + intra_eu = vat_exclusion = False + if company.vat and partner.vat: + intra_eu = company.vat[:2] in eu_country_codes and partner.vat[:2] in eu_country_codes + vat_exclusion = company.vat[:2] == partner.vat[:2] + + # If company and partner have the same vat prefix (and are both within the EU), use invoicing + if not delivery or (intra_eu and vat_exclusion): delivery = partner # partner manually set fiscal position always win diff --git a/addons/account/report/__init__.py b/addons/account/report/__init__.py index 75543d30b0..7c1966465e 100644 --- a/addons/account/report/__init__.py +++ b/addons/account/report/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +# Part of Odoo. See LICENSE file for full copyright and licensing details. from . import account_invoice_report from . import account_journal diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index d268c8c2ff..d3e5ca7f90 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -100,7 +100,7 @@ class AccountInvoiceReport(models.Model): -line.balance * currency_table.rate AS price_subtotal, -COALESCE( -- Average line price - (line.balance / NULLIF(line.quantity, 0.0)) + (line.balance / NULLIF(line.quantity, 0.0)) * (CASE WHEN move.move_type IN ('in_invoice','out_refund','in_receipt') THEN -1 ELSE 1 END) -- convert to template uom * (NULLIF(COALESCE(uom_line.factor, 1), 0.0) / NULLIF(COALESCE(uom_template.factor, 1), 0.0)), 0.0) * currency_table.rate AS price_average, diff --git a/addons/account/tests/test_account_incoming_supplier_invoice.py b/addons/account/tests/test_account_incoming_supplier_invoice.py index a182d31131..25f2de6ce3 100644 --- a/addons/account/tests/test_account_incoming_supplier_invoice.py +++ b/addons/account/tests/test_account_incoming_supplier_invoice.py @@ -12,12 +12,12 @@ class TestAccountIncomingSupplierInvoice(AccountTestInvoicingCommon): def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) - cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'test-company.flectrahq.com') + cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'test-company.flectra.com') cls.internal_user = cls.env['res.users'].create({ 'name': 'Internal User', - 'login': 'internal.user@test.flectrahq.com', - 'email': 'internal.user@test.flectrahq.com', + 'login': 'internal.user@test.flectra.com', + 'email': 'internal.user@test.flectra.com', }) cls.supplier_partner = cls.env['res.partner'].create({ diff --git a/addons/account/tests/test_account_invoice_report.py b/addons/account/tests/test_account_invoice_report.py index f341db0208..aba104a52f 100644 --- a/addons/account/tests/test_account_invoice_report.py +++ b/addons/account/tests/test_account_invoice_report.py @@ -103,6 +103,7 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon): 'price_subtotal': vals[1], 'quantity': vals[2], } for vals in expected_values_list] + self.assertRecordValues(reports, expected_values_dict) def test_invoice_report_multiple_types(self): @@ -112,7 +113,7 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon): [1000, 1000, 1], [250, 750, 3], [6, 6, 1], - [-20, -20, -1], - [-20, -20, -1], - [-600, -600, -1], + [20, -20, -1], + [20, -20, -1], + [600, -600, -1], ]) diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 87e517bdce..c8b37d4245 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -1942,6 +1942,121 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): (self.tax_account_1, -20.0, -13.33), ]) + def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries_4(self): + ''' Test the generation of the exchange difference for a tax cash basis journal entry when the tax + account is a reconcile one. + ''' + currency_id = self.currency_data['currency'].id + cash_basis_transition_account = self.env['account.account'].create({ + 'code': '209.01.01', + 'name': 'Cash Basis Transition Account', + 'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id, + 'company_id': self.company_data['company'].id, + 'reconcile': True, + }) + self.cash_basis_tax_a_third_amount.write({ + 'cash_basis_transition_account_id': cash_basis_transition_account.id, + }) + + # Rate 1/3 in 2016. + cash_basis_move = self.env['account.move'].create({ + 'move_type': 'entry', + 'date': '2016-01-01', + 'line_ids': [ + # Base Tax line + (0, 0, { + 'debit': 0.0, + 'credit': 100.0, + 'amount_currency': -300.0, + 'currency_id': currency_id, + 'account_id': self.company_data['default_account_revenue'].id, + 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)], + 'tax_exigible': False, + }), + + # Tax line + (0, 0, { + 'debit': 0.0, + 'credit': 33.33, + 'amount_currency': -100.0, + 'currency_id': currency_id, + 'account_id': cash_basis_transition_account.id, + 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id, + 'tax_exigible': False, + }), + + # Receivable lines + (0, 0, { + 'debit': 133.33, + 'credit': 0.0, + 'amount_currency': 400.0, + 'currency_id': currency_id, + 'account_id': self.extra_receivable_account_1.id, + }), + ] + }) + + # Rate 1/2 in 2017. + payment_move = self.env['account.move'].create({ + 'move_type': 'entry', + 'date': '2017-01-01', + 'line_ids': [ + (0, 0, { + 'debit': 0.0, + 'credit': 200.0, + 'amount_currency': -400.0, + 'currency_id': currency_id, + 'account_id': self.extra_receivable_account_1.id, + }), + (0, 0, { + 'debit': 200.0, + 'credit': 0.0, + 'amount_currency': 400.0, + 'currency_id': currency_id, + 'account_id': self.company_data['default_account_revenue'].id, + }), + ] + }) + + (cash_basis_move + payment_move).action_post() + + self.assertAmountsGroupByAccount([ + # Account Balance Amount Currency + (cash_basis_transition_account, -33.33, -100.0), + (self.tax_account_1, 0.0, 0.0), + ]) + + receivable_lines = (cash_basis_move + payment_move).line_ids\ + .filtered(lambda line: line.account_id == self.extra_receivable_account_1) + res = receivable_lines.reconcile() + + self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1) + + # Tax values based on payment + # Invoice amount 300 (amount currency) with payment rate 2 (400 payment amount divided by 200 invoice balance) + # - Base amount: 150 company currency + # - Tax amount: 50 company currency + self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [ + # Base amount: + {'debit': 150.0, 'credit': 0.0, 'amount_currency': 300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id}, + {'debit': 0.0, 'credit': 150.0, 'amount_currency': -300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id}, + # tax: + {'debit': 50.0, 'credit': 0.0, 'amount_currency': 100.0, 'currency_id': currency_id, 'account_id': cash_basis_transition_account.id}, + {'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'currency_id': currency_id, 'account_id': self.tax_account_1.id}, + ]) + + exchange_diff = res['full_reconcile'].exchange_move_id + + # Exchange difference + # 66.67 amount residual on the payment line after reconciling receivable line of the cash basis move with the payment counterpart + # 50.00 difference of the cash_basis_move base line and the CABA entry created by the system + self.assertRecordValues(exchange_diff.line_ids, [ + {'debit': 66.67, 'credit': 0.0, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_1.id}, + {'debit': 0.0, 'credit': 66.67, 'currency_id': currency_id, 'account_id': self.company_data['company'].income_currency_exchange_account_id.id}, + {'debit': 50.0, 'credit': 0.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id}, + {'debit': 0.0, 'credit': 50.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id}, + ]) + def test_reconcile_cash_basis_revert(self): ''' Ensure the cash basis journal entry can be reverted. ''' self.cash_basis_transfer_account.reconcile = True diff --git a/addons/account/tests/test_account_payment.py b/addons/account/tests/test_account_payment.py index c6f2f97d0d..4c8dd1c778 100644 --- a/addons/account/tests/test_account_payment.py +++ b/addons/account/tests/test_account_payment.py @@ -14,6 +14,9 @@ class TestAccountPayment(AccountTestInvoicingCommon): cls.payment_debit_account_id = cls.copy_account(cls.company_data['default_journal_bank'].payment_debit_account_id) cls.payment_credit_account_id = cls.copy_account(cls.company_data['default_journal_bank'].payment_credit_account_id) + cls.bank_journal_1 = cls.company_data['default_journal_bank'] + cls.bank_journal_2 = cls.company_data['default_journal_bank'].copy() + cls.partner_bank_account1 = cls.env['res.partner.bank'].create({ 'acc_number': "0123456789", 'partner_id': cls.partner_a.id, @@ -24,13 +27,18 @@ class TestAccountPayment(AccountTestInvoicingCommon): 'partner_id': cls.partner_a.id, 'acc_type': 'bank', }) - cls.comp_bank_account = cls.env['res.partner.bank'].create({ + cls.comp_bank_account1 = cls.env['res.partner.bank'].create({ 'acc_number': "985632147", 'partner_id': cls.env.company.partner_id.id, 'acc_type': 'bank', }) + cls.comp_bank_account2 = cls.env['res.partner.bank'].create({ + 'acc_number': "741258963", + 'partner_id': cls.env.company.partner_id.id, + 'acc_type': 'bank', + }) - cls.company_data['default_journal_bank'].write({ + cls.bank_journal_1.write({ 'payment_debit_account_id': cls.payment_debit_account_id.id, 'payment_credit_account_id': cls.payment_credit_account_id.id, 'inbound_payment_method_ids': [(6, 0, cls.env.ref('account.account_payment_method_manual_in').ids)], @@ -766,17 +774,34 @@ class TestAccountPayment(AccountTestInvoicingCommon): }, ]) - def test_payment_partner_bank_inbound(self): - """ Test the bank account is well recomputed for inbound payments. In that case, the recipient - bank account must be the one set on the company. + def test_suggested_default_partner_bank(self): + """ Ensure the 'partner_bank_id' is well computed on payments. When the payment is inbound, the money must be + received by a bank account linked to the company. In case of outbound payment, the bank account must be found + on the partner. """ payment = self.env['account.payment'].create({ + 'journal_id': self.bank_journal_1.id, 'amount': 50.0, 'payment_type': 'outbound', 'partner_type': 'supplier', 'partner_id': self.partner_a.id, }) - self.assertRecordValues(payment, [{'partner_bank_id': self.partner_bank_account1.id}]) + self.assertRecordValues(payment, [{ + 'available_partner_bank_ids': self.partner_a.bank_ids.ids, + 'partner_bank_id': self.partner_bank_account1.id, + }]) payment.payment_type = 'inbound' - self.assertRecordValues(payment, [{'partner_bank_id': self.comp_bank_account.id}]) + self.assertRecordValues(payment, [{ + 'available_partner_bank_ids': [], + 'partner_bank_id': False, + }]) + + self.bank_journal_2.bank_account_id = self.comp_bank_account2 + # A sequence is automatically added on the first move. We need to clean it before changing the journal. + payment.name = False + payment.journal_id = self.bank_journal_2 + self.assertRecordValues(payment, [{ + 'available_partner_bank_ids': self.comp_bank_account2.ids, + 'partner_bank_id': self.comp_bank_account2.id, + }]) diff --git a/addons/account/tests/test_account_payment_register.py b/addons/account/tests/test_account_payment_register.py index e1e37c6366..9e211dd29a 100644 --- a/addons/account/tests/test_account_payment_register.py +++ b/addons/account/tests/test_account_payment_register.py @@ -10,7 +10,7 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): @classmethod def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) - + cls.currency_data_3 = cls.setup_multi_currency_data({ 'name': "Umbrella", 'symbol': '☂', @@ -35,7 +35,10 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): }) cls.manual_payment_method_out = cls.env.ref('account.account_payment_method_manual_out') - cls.company_data['default_journal_bank'].write({ + cls.bank_journal_1 = cls.company_data['default_journal_bank'] + cls.bank_journal_2 = cls.company_data['default_journal_bank'].copy() + + cls.bank_journal_1.write({ 'payment_debit_account_id': cls.payment_debit_account_id.id, 'payment_credit_account_id': cls.payment_credit_account_id.id, 'inbound_payment_method_ids': [(6, 0, ( @@ -49,11 +52,16 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): ))], }) - cls.partner_bank_account = cls.env['res.partner.bank'].create({ + cls.partner_bank_account1 = cls.env['res.partner.bank'].create({ 'acc_number': "0123456789", 'partner_id': cls.partner_a.id, 'acc_type': 'bank', }) + cls.partner_bank_account2 = cls.env['res.partner.bank'].create({ + 'acc_number': "9876543210", + 'partner_id': cls.partner_a.id, + 'acc_type': 'bank', + }) cls.comp_bank_account1 = cls.env['res.partner.bank'].create({ 'acc_number': "985632147", 'partner_id': cls.env.company.partner_id.id, @@ -477,6 +485,9 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): ''' Choose to pay multiple batches, one with two customer invoices (1000 + 2000) and one with a vendor bill of 600, by splitting payments. ''' + self.in_invoice_1.partner_bank_id = self.partner_bank_account1 + self.in_invoice_2.partner_bank_id = self.partner_bank_account2 + active_ids = (self.in_invoice_1 + self.in_invoice_2 + self.in_invoice_3).ids payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({ 'group_payment': False, @@ -484,16 +495,22 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): self.assertRecordValues(payments, [ { + 'journal_id': self.bank_journal_1.id, 'ref': 'BILL/2017/01/0001', 'payment_method_id': self.manual_payment_method_out.id, + 'partner_bank_id': self.partner_bank_account1.id, }, { + 'journal_id': self.bank_journal_1.id, 'ref': 'BILL/2017/01/0002', 'payment_method_id': self.manual_payment_method_out.id, + 'partner_bank_id': self.partner_bank_account2.id, }, { + 'journal_id': self.bank_journal_1.id, 'ref': 'BILL/2017/01/0003', 'payment_method_id': self.manual_payment_method_out.id, + 'partner_bank_id': False, }, ]) self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance') + payments[2].line_ids.sorted('balance'), [ @@ -550,27 +567,6 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): }, ]) - def test_register_payment_custom_bank_account(self): - """ Ensure the user is able to select a custom bank account when registering a payment and this bank account - lands correctly on the generated payment. - """ - self.out_invoice_1.partner_bank_id = self.comp_bank_account1 - - ctx = {'active_model': 'account.move', 'active_ids': self.out_invoice_1.ids} - wizard_form = Form(self.env['account.payment.register'].with_context(**ctx)) - wizard = wizard_form.save() - - # The bank account set on the invoice must be the default suggested value. - self.assertRecordValues(wizard, [{'partner_bank_id': self.comp_bank_account1.id}]) - - wizard_form = Form(wizard) - wizard_form.partner_bank_id = self.comp_bank_account2 - wizard = wizard_form.save() - payments = wizard._create_payments() - - # The user should be able to set a custom bank account. - self.assertRecordValues(payments, [{'partner_bank_id': self.comp_bank_account2.id}]) - def test_register_payment_constraints(self): # Test to register a payment for a draft journal entry. self.out_invoice_1.button_draft() @@ -835,3 +831,82 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): 'reconciled': False, }, ]) + + def test_suggested_default_partner_bank_inbound_payment(self): + """ Test the suggested bank account on the wizard for inbound payment. """ + self.out_invoice_1.partner_bank_id = False + + ctx = {'active_model': 'account.move', 'active_ids': self.out_invoice_1.ids} + wizard = self.env['account.payment.register'].with_context(**ctx).create({}) + self.assertRecordValues(wizard, [{ + 'journal_id': self.bank_journal_1.id, + 'available_partner_bank_ids': [], + 'partner_bank_id': False, + }]) + + self.bank_journal_2.bank_account_id = self.out_invoice_1.partner_bank_id = self.comp_bank_account2 + wizard = self.env['account.payment.register'].with_context(**ctx).create({}) + self.assertRecordValues(wizard, [{ + 'journal_id': self.bank_journal_2.id, + 'available_partner_bank_ids': self.comp_bank_account2.ids, + 'partner_bank_id': self.comp_bank_account2.id, + }]) + + wizard.journal_id = self.bank_journal_1 + self.assertRecordValues(wizard, [{ + 'journal_id': self.bank_journal_1.id, + 'available_partner_bank_ids': [], + 'partner_bank_id': False, + }]) + + def test_suggested_default_partner_bank_outbound_payment(self): + """ Test the suggested bank account on the wizard for outbound payment. """ + self.in_invoice_1.partner_bank_id = False + + ctx = {'active_model': 'account.move', 'active_ids': self.in_invoice_1.ids} + wizard = self.env['account.payment.register'].with_context(**ctx).create({}) + self.assertRecordValues(wizard, [{ + 'journal_id': self.bank_journal_1.id, + 'available_partner_bank_ids': self.partner_a.bank_ids.ids, + 'partner_bank_id': self.partner_bank_account1.id, + }]) + + self.in_invoice_1.partner_bank_id = self.partner_bank_account2 + wizard = self.env['account.payment.register'].with_context(**ctx).create({}) + self.assertRecordValues(wizard, [{ + 'journal_id': self.bank_journal_1.id, + 'available_partner_bank_ids': self.partner_a.bank_ids.ids, + 'partner_bank_id': self.partner_bank_account2.id, + }]) + + wizard.journal_id = self.bank_journal_2 + self.assertRecordValues(wizard, [{ + 'journal_id': self.bank_journal_2.id, + 'available_partner_bank_ids': self.partner_a.bank_ids.ids, + 'partner_bank_id': self.partner_bank_account2.id, + }]) + + def test_register_payment_inbound_multiple_bank_account(self): + """ Pay customer invoices with different bank accounts. """ + self.out_invoice_1.partner_bank_id = self.comp_bank_account1 + self.out_invoice_2.partner_bank_id = self.comp_bank_account2 + self.bank_journal_2.bank_account_id = self.comp_bank_account2 + + ctx = {'active_model': 'account.move', 'active_ids': (self.out_invoice_1 + self.out_invoice_2).ids} + wizard = self.env['account.payment.register'].with_context(**ctx).create({'journal_id': self.bank_journal_2.id}) + payments = wizard._create_payments() + + self.assertRecordValues(payments, [ + { + 'journal_id': self.bank_journal_2.id, + 'ref': 'INV/2017/01/0001', + 'payment_method_id': self.manual_payment_method_in.id, + 'partner_bank_id': self.comp_bank_account2.id, + }, + { + 'journal_id': self.bank_journal_2.id, + 'ref': 'INV/2017/01/0002', + 'payment_method_id': self.manual_payment_method_in.id, + 'partner_bank_id': self.comp_bank_account2.id, + }, + ]) diff --git a/addons/account/tests/test_reconciliation_matching_rules.py b/addons/account/tests/test_reconciliation_matching_rules.py index a24207019b..19bf2d8de6 100644 --- a/addons/account/tests/test_reconciliation_matching_rules.py +++ b/addons/account/tests/test_reconciliation_matching_rules.py @@ -258,6 +258,51 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon): self.bank_line_5.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id}, }, statements=self.bank_st_2) + def test_matching_fields_match_text_location_no_partner(self): + self.bank_line_2.unlink() # One line is enough for this test + self.bank_line_1.partner_id = None + + self.partner_1.name = "Bernard Gagnant" + + self.rule_1.write({ + 'match_partner': False, + 'match_partner_ids': [(5, 0, 0)], + 'line_ids': [(5, 0, 0)], + }) + + st_line_initial_vals = {'ref': None, 'payment_ref': 'nothing', 'narration': None} + recmod_initial_vals = {'match_text_location_label': False, 'match_text_location_note': False, 'match_text_location_reference': False} + + rec_mod_options_to_fields = { + 'match_text_location_label': 'payment_ref', + 'match_text_location_note': 'narration', + 'match_text_location_reference': 'ref', + } + + for rec_mod_field, st_line_field in rec_mod_options_to_fields.items(): + self.rule_1.write({**recmod_initial_vals, rec_mod_field: True}) + # Fully reinitialize the statement line + self.bank_line_1.write(st_line_initial_vals) + + # Nothing should match + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + }, statements=self.bank_st) + + # Test matching with the invoice ref + self.bank_line_1.write({st_line_field: self.invoice_line_1.move_id.payment_reference}) + + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': self.invoice_line_1.ids, 'model': self.rule_1, 'partner': self.env['res.partner']}, + }, statements=self.bank_st) + + # Test matching with the partner name (reinitializing the statement line first) + self.bank_line_1.write({**st_line_initial_vals, st_line_field: self.partner_1.name}) + + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': self.invoice_line_1.ids, 'model': self.rule_1, 'partner': self.env['res.partner']}, + }, statements=self.bank_st) + def test_matching_fields_match_journal_ids(self): self.rule_1.match_journal_ids |= self.cash_st.journal_id self._check_statement_matching(self.rule_1, { diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py index ee6b4660fe..163fd07227 100644 --- a/addons/account/tests/test_tax.py +++ b/addons/account/tests/test_tax.py @@ -1073,3 +1073,61 @@ class TestTax(TestTaxCommon): ], (tax_10_fix + tax_21).compute_all(1210), ) + + def test_price_included_repartition_sum_0(self): + """ Tests the case where a tax with a non-zero value has a sum + of tax repartition factors of zero and is included in price. It + shouldn't behave in the same way as a 0% tax. + """ + test_tax = self.env['account.tax'].create({ + 'name': "Definitely not a 0% tax", + 'amount_type': 'percent', + 'amount': 42, + 'price_include': True, + 'invoice_repartition_line_ids': [ + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), + + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + }), + ], + 'refund_repartition_line_ids': [ + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), + + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + }), + ], + }) + + compute_all_res = test_tax.compute_all(100) + self._check_compute_all_results( + 100, # 'total_included' + 100, # 'total_excluded' + [ + # base , amount + # --------------- + (100, 42), + (100, -42), + # --------------- + ], + compute_all_res + ) diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 3605f7252e..481efce500 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -332,18 +332,18 @@ - - + + - - - - - + + + + + @@ -1300,7 +1300,7 @@ {'journal_type':'general', 'search_default_posted':1} Journal Items account.move.line - [('display_type', 'not in', ('line_section', 'line_note')), ('move_id.state', '!=', 'cancel')] + [('display_type', 'not in', ('line_section', 'line_note')), ('parent_state', '!=', 'cancel')] tree,pivot,graph,form,kanban diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml index 3ae67ec727..4ca5714ff6 100644 --- a/addons/account/views/account_payment_view.xml +++ b/addons/account/views/account_payment_view.xml @@ -158,6 +158,7 @@ + diff --git a/addons/account/views/account_reconcile_model_views.xml b/addons/account/views/account_reconcile_model_views.xml index b9c8e9e1b8..dcbd9606cb 100644 --- a/addons/account/views/account_reconcile_model_views.xml +++ b/addons/account/views/account_reconcile_model_views.xml @@ -156,7 +156,7 @@ attrs="{'invisible': [('match_partner', '=', False)]}"/> -