diff --git a/addons/account/README.md b/addons/account/README.md index a2b8a4ce23..2cd9bcb645 100644 --- a/addons/account/README.md +++ b/addons/account/README.md @@ -1,7 +1,7 @@ -Odoo Accounting +Flectra Accounting --------------- -The Odoo Open Source Accounting app allows a better way to +The Flectra 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. Odoo's user interface is designed with productivity in mind. +in one place. Flectra'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; Odoo has all the features you need to sustain all your business +accounting; Flectra has all the features you need to sustain all your business activities. Scale With Your Organization ---------------------------- -Odoo supports multiple currencies, multiple users with different access rights, +Flectra supports multiple currencies, multiple users with different access rights, multiple companies with real time consolidation and unlimited analytic plans. diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 101c01fd96..414e44021d 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -8,12 +8,12 @@ 'description': """ Invoicing & Payments ==================== -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. +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. 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://flectrahq.com/billing', + 'website': 'https://www.flectrahq.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/models/account_account.py b/addons/account/models/account_account.py index 7793a3dc3c..fc9118e597 100644 --- a/addons/account/models/account_account.py +++ b/addons/account/models/account_account.py @@ -188,6 +188,32 @@ class AccountAccount(models.Model): if self._cr.fetchone(): raise ValidationError(_("The account is already in use in a 'sale' or 'purchase' journal. This means that the account's type couldn't be 'receivable' or 'payable'.")) + @api.constrains('reconcile') + def _check_used_as_journal_default_debit_credit_account(self): + accounts = self.filtered(lambda a: not a.reconcile) + if not accounts: + return + + self.flush(['reconcile']) + self._cr.execute(''' + SELECT journal.id + FROM account_journal journal + WHERE journal.payment_credit_account_id in %(credit_account)s + OR journal.payment_debit_account_id in %(debit_account)s ; + ''', { + 'credit_account': tuple(accounts.ids), + 'debit_account': tuple(accounts.ids) + }) + + rows = self._cr.fetchall() + if rows: + journals = self.env['account.journal'].browse([r[0] for r in rows]) + raise ValidationError(_( + "This account is configured in %(journal_names)s journal(s) (ids %(journal_ids)s) as payment debit or credit account. This means that this account's type should be reconcilable.", + journal_names=journals.mapped('display_name'), + journal_ids=journals.ids + )) + @api.depends('code') def _compute_account_root(self): # this computes the first 2 digits of the account. @@ -258,10 +284,12 @@ class AccountAccount(models.Model): account.is_off_balance = account.internal_group == "off_balance" def _set_opening_debit(self): - self._set_opening_debit_credit(self.opening_debit, 'debit') + for record in self: + record._set_opening_debit_credit(record.opening_debit, 'debit') def _set_opening_credit(self): - self._set_opening_debit_credit(self.opening_credit, 'credit') + for record in self: + record._set_opening_debit_credit(record.opening_credit, 'credit') def _set_opening_debit_credit(self, amount, field): """ Generic function called by both opening_debit and opening_credit's diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index fb0ec2406b..ca64dec1a9 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -406,7 +406,9 @@ class AccountBankStatement(models.Model): statement._set_next_sequence() self.write({'state': 'posted'}) - self.line_ids.move_id._post(soft=False) + lines_of_moves_to_post = self.line_ids.filtered(lambda line: line.move_id.state != 'posted') + if lines_of_moves_to_post: + lines_of_moves_to_post.move_id._post(soft=False) def button_validate(self): if any(statement.state != 'posted' or not statement.all_lines_reconciled for statement in self): diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py index 66fc9b5251..82533cef22 100644 --- a/addons/account/models/account_journal.py +++ b/addons/account/models/account_journal.py @@ -624,7 +624,8 @@ class AccountJournal(models.Model): :returns: the created invoice. """ - return self.create_invoice_from_attachment(attachment.ids) + invoice_action = self.create_invoice_from_attachment(attachment.ids) + return self.env['account.move'].browse(invoice_action['res_id']) def _create_secure_sequence(self, sequence_fields): """This function creates a no_gap sequence on each journal in self that will ensure diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index 0b24af1ba4..ca94f5e2d4 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -431,7 +431,7 @@ class account_journal(models.Model): def to_check_ids(self): self.ensure_one() domain = self.env['account.move.line']._get_suspense_moves_domain() - domain += [('journal_id', '=', self.id),('statement_line_id.is_reconciled', '=', False)] + domain.append(('journal_id', '=', self.id)) statement_line_ids = self.env['account.move.line'].search(domain).mapped('statement_line_id') return statement_line_ids diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index a03ea80714..2fb9a98abf 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -104,8 +104,10 @@ class AccountMove(models.Model): return journal + # TODO remove in master @api.model def _get_default_invoice_date(self): + warnings.warn("Method '_get_default_invoice_date()' is deprecated and has been removed.", DeprecationWarning) return fields.Date.context_today(self) if self._context.get('default_move_type', 'entry') in self.get_purchase_types(include_receipts=True) else False @api.model @@ -159,10 +161,11 @@ class AccountMove(models.Model): check_company=True, domain="[('id', 'in', suitable_journal_ids)]", default=_get_default_journal) suitable_journal_ids = fields.Many2many('account.journal', compute='_compute_suitable_journal_ids') - company_id = fields.Many2one(string='Company', store=True, readonly=True, - related='journal_id.company_id', change_default=True, default=lambda self: self.env.company) + company_id = fields.Many2one(comodel_name='res.company', string='Company', + store=True, readonly=True, + compute='_compute_company_id') company_currency_id = fields.Many2one(string='Company Currency', readonly=True, - related='journal_id.company_id.currency_id') + related='company_id.currency_id') currency_id = fields.Many2one('res.currency', store=True, readonly=True, tracking=True, required=True, states={'draft': [('readonly', False)]}, string='Currency', @@ -263,8 +266,7 @@ class AccountMove(models.Model): string='Salesperson', default=lambda self: self.env.user) invoice_date = fields.Date(string='Invoice/Bill Date', readonly=True, index=True, copy=False, - states={'draft': [('readonly', False)]}, - default=_get_default_invoice_date) + states={'draft': [('readonly', False)]}) invoice_date_due = fields.Date(string='Due Date', readonly=True, index=True, copy=False, states={'draft': [('readonly', False)]}) invoice_origin = fields.Char(string='Origin', readonly=True, tracking=True, @@ -686,8 +688,8 @@ class AccountMove(models.Model): balance = currency._convert( taxes_map_entry['amount'], - self.journal_id.company_id.currency_id, - self.journal_id.company_id, + self.company_currency_id, + self.company_id, self.date or fields.Date.context_today(self), ) to_write_on_line = { @@ -1037,6 +1039,11 @@ class AccountMove(models.Model): if invoice != invoice._origin: invoice.invoice_line_ids = invoice.line_ids.filtered(lambda line: not line.exclude_from_invoice_tab) + @api.depends('journal_id') + def _compute_company_id(self): + for move in self: + move.company_id = move.journal_id.company_id or move.company_id or self.env.company + def _get_lines_onchange_currency(self): # Override needed for COGS return self.line_ids @@ -1264,12 +1271,9 @@ class AccountMove(models.Model): total_residual_currency = 0.0 total = 0.0 total_currency = 0.0 - currencies = set() + currencies = move._get_lines_onchange_currency().currency_id for line in move.line_ids: - if line.currency_id and line in move._get_lines_onchange_currency(): - currencies.add(line.currency_id) - if move.is_invoice(include_receipts=True): # === Invoices === @@ -1309,7 +1313,7 @@ class AccountMove(models.Model): move.amount_total_signed = abs(total) if move.move_type == 'entry' else -total move.amount_residual_signed = total_residual - currency = len(currencies) == 1 and currencies.pop() or move.company_id.currency_id + currency = len(currencies) == 1 and currencies or move.company_id.currency_id # Compute 'payment_state'. new_pmt_state = 'not_paid' if move.move_type != 'entry' else False @@ -1649,7 +1653,11 @@ class AccountMove(models.Model): duplicated_moves = self.browse([r[0] for r in self._cr.fetchall()]) if duplicated_moves: raise ValidationError(_('Duplicated vendor reference detected. You probably encoded twice the same vendor bill/credit note:\n%s') % "\n".join( - duplicated_moves.mapped(lambda m: "%(partner)s - %(ref)s - %(date)s" % {'ref': m.ref, 'partner': m.partner_id.display_name, 'date': format_date(self.env, m.date)}) + duplicated_moves.mapped(lambda m: "%(partner)s - %(ref)s - %(date)s" % { + 'ref': m.ref, + 'partner': m.partner_id.display_name, + 'date': format_date(self.env, m.invoice_date), + }) )) def _check_balanced(self): @@ -1836,8 +1844,9 @@ class AccountMove(models.Model): vals_list = self._move_autocomplete_invoice_lines_create(vals_list) rslt = super(AccountMove, self).create(vals_list) - if 'line_ids' in vals_list: - rslt.update_lines_tax_exigibility() + for i, vals in enumerate(vals_list): + if 'line_ids' in vals: + rslt[i].update_lines_tax_exigibility() return rslt def write(self, vals): @@ -2424,6 +2433,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.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): raise UserError(_('You need to add a line before posting.')) if move.auto_post and move.date > fields.Date.context_today(self): @@ -2443,9 +2454,12 @@ class AccountMove(models.Model): # lines are recomputed accordingly. # /!\ 'check_move_validity' must be there since the dynamic lines will be recomputed outside the 'onchange' # environment. - if not move.invoice_date and move.is_invoice(include_receipts=True): - move.invoice_date = fields.Date.context_today(self) - move.with_context(check_move_validity=False)._onchange_invoice_date() + if not move.invoice_date: + if move.is_sale_document(include_receipts=True): + move.invoice_date = fields.Date.context_today(self) + move.with_context(check_move_validity=False)._onchange_invoice_date() + elif move.is_purchase_document(include_receipts=True): + raise UserError(_("The Bill/Refund date is required to validate this document.")) # When the accounting date is prior to the tax lock date, move it automatically to the next available date. # /!\ 'check_move_validity' must be there since the dynamic lines will be recomputed outside the 'onchange' @@ -2969,7 +2983,7 @@ class AccountMoveLine(models.Model): currency_id = fields.Many2one('res.currency', string='Currency', required=True) partner_id = fields.Many2one('res.partner', string='Partner', ondelete='restrict') product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]") - product_id = fields.Many2one('product.product', string='Product') + product_id = fields.Many2one('product.product', string='Product', ondelete='restrict') product_uom_category_id = fields.Many2one('uom.category', related='product_id.uom_id.category_id') # ==== Origin fields ==== @@ -3601,7 +3615,7 @@ class AccountMoveLine(models.Model): ) FROM %(from)s WHERE %(where)s - """ % {'from': from_clause, 'where': where_clause, 'order_by': order_string} + """ % {'from': from_clause, 'where': where_clause or 'TRUE', 'order_by': order_string} self.env.cr.execute(sql, where_clause_params) result = {r[0]: r[1] for r in self.env.cr.fetchall()} for record in self: diff --git a/addons/account/models/account_partial_reconcile.py b/addons/account/models/account_partial_reconcile.py index 4c6e6cc53a..087d3d7509 100644 --- a/addons/account/models/account_partial_reconcile.py +++ b/addons/account/models/account_partial_reconcile.py @@ -107,8 +107,16 @@ class AccountPartialReconcile(models.Model): if not self: return True - # Reverse all exchange moves at once. + # Retrieve the matching number to unlink. + full_to_unlink = self.full_reconcile_id + + # Retrieve the CABA entries to reverse. moves_to_reverse = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', self.ids)]) + + # Unlink partials before doing anything else to avoid 'Record has already been deleted' due to the recursion. + res = super().unlink() + + # Reverse CABA entries. today = fields.Date.context_today(self) default_values_list = [{ 'date': move.date if move.date > (move.company_id.period_lock_date or date.min) else today, @@ -116,10 +124,9 @@ class AccountPartialReconcile(models.Model): } for move in moves_to_reverse] moves_to_reverse._reverse_moves(default_values_list, cancel=True) - # Unlink partials then the full in this order to avoid a recursive call to the same partials. - full_to_unlink = self.full_reconcile_id - res = super().unlink() + # Remove the matching numbers. full_to_unlink.unlink() + return res # ------------------------------------------------------------------------- diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index 55ac17fbfc..f786071d67 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -24,7 +24,15 @@ class AccountReconcileModelPartnerMapping(models.Model): for record in self: if not (record.narration_regex or record.payment_ref_regex): raise ValidationError(_("Please set at least one of the match texts to create a partner mapping.")) - + try: + if record.payment_ref_regex: + current_regex = record.payment_ref_regex + re.compile(record.payment_ref_regex) + if record.narration_regex: + current_regex = record.narration_regex + re.compile(record.narration_regex) + except re.error: + raise ValidationError(_("The following regular expression is invalid to create a partner mapping: %s") % current_regex) class AccountReconcileModelLine(models.Model): _name = 'account.reconcile.model.line' @@ -882,6 +890,11 @@ class AccountReconcileModel(models.Model): the most prioritary are kept. """ candidates_by_priority = self._sort_reconciliation_candidates_by_priority(candidates, aml_ids_to_exclude, reconciled_amls_ids) + + # This can happen if the candidates were already reconciled at this point + if not candidates_by_priority: + return [], set() + max_priority = min(candidates_by_priority.keys()) filtered_candidates = candidates_by_priority[max_priority] diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index 8ec9ed30b5..f1fffce26e 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -370,22 +370,10 @@ class AccountTax(models.Model): # 1) Flatten the taxes. taxes, groups_map = self.flatten_taxes_hierarchy(create_map=True) - # 2) Avoid mixing taxes having price_include=False && include_base_amount=True - # with taxes having price_include=True. This use case is not supported as the - # computation of the total_excluded would be impossible. - base_excluded_flag = False # price_include=False && include_base_amount=True - included_flag = False # price_include=True - for tax in taxes: - if tax.price_include: - included_flag = True - elif tax.include_base_amount: - base_excluded_flag = True - if base_excluded_flag and included_flag: - raise UserError(_('Unable to mix any taxes being price included with taxes affecting the base amount but not included in price.')) - - # 3) Deal with the rounding methods + # 2) Deal with the rounding methods if not currency: currency = company.currency_id + # By default, for each tax, tax amount will first be computed # and rounded at the 'Account' decimal precision for each # PO/SO/invoice line and then these rounded amounts will be @@ -408,7 +396,7 @@ class AccountTax(models.Model): if not round_tax: prec *= 1e-5 - # 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation. + # 3) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation. # tax | base | amount | # /\ ---------------------------- # || tax_1 | XXXX | | <- we are looking for that, it's the total_excluded @@ -515,10 +503,15 @@ class AccountTax(models.Model): total_excluded = currency.round(recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount)) - # 5) Iterate the taxes in the sequence order to compute missing tax amounts. + # 4) Iterate the taxes in the sequence order to compute missing tax amounts. # Start the computation of accumulated amounts at the total_excluded value. base = total_included = total_void = total_excluded + # Flag indicating the checkpoint used in price_include to avoid rounding issue must be skipped since the base + # amount has changed because we are currently mixing price-included and price-excluded include_base_amount + # taxes. + skip_checkpoint = False + taxes_vals = [] i = 0 cumulated_tax_included_amount = 0 @@ -529,7 +522,7 @@ class AccountTax(models.Model): price_include = self._context.get('force_price_include', tax.price_include) #compute the tax_amount - if price_include and total_included_checkpoints.get(i): + if not skip_checkpoint and price_include and total_included_checkpoints.get(i): # 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 @@ -594,6 +587,8 @@ class AccountTax(models.Model): # Affect subsequent taxes if tax.include_base_amount: base += factorized_tax_amount + if not price_include: + skip_checkpoint = True total_included += factorized_tax_amount i += 1 @@ -642,9 +637,11 @@ class AccountTaxRepartitionLine(models.Model): help="Account on which to post the tax amount") tag_ids = fields.Many2many(string="Tax Grids", comodel_name='account.account.tag', domain=[('applicability', '=', 'taxes')], copy=True) invoice_tax_id = fields.Many2one(comodel_name='account.tax', + ondelete='cascade', check_company=True, help="The tax set to apply this distribution on invoices. Mutually exclusive with refund_tax_id") refund_tax_id = fields.Many2one(comodel_name='account.tax', + ondelete='cascade', check_company=True, help="The tax set to apply this distribution on refund invoices. Mutually exclusive with invoice_tax_id") tax_id = fields.Many2one(comodel_name='account.tax', compute='_compute_tax_id') diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 796f4280f0..1c3c84632c 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -162,15 +162,3 @@ class ResConfigSettings(models.TransientModel): 'Modify your taxes first before disabling this setting.') } return res - - @api.model - def create(self, values): - # Optimisation purpose, saving a res_config even without changing any values will trigger the write of all - # related values, including the currency_id field on res_company. This in turn will trigger the recomputation - # of account_move_line related field company_currency_id which can be slow depending on the number of entries - # in the database. Thus, if we do not explicitly change the currency_id, we should not write it on the company - if ('company_id' in values and 'currency_id' in values): - company = self.env['res.company'].browse(values.get('company_id')) - if company.currency_id.id == values.get('currency_id'): - values.pop('currency_id') - return super(ResConfigSettings, self).create(values) diff --git a/addons/account/tests/test_account_account.py b/addons/account/tests/test_account_account.py index ca59db0161..e3013ceb15 100644 --- a/addons/account/tests/test_account_account.py +++ b/addons/account/tests/test_account_account.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from flectra.addons.account.tests.common import AccountTestInvoicingCommon from flectra.tests import tagged -from flectra.exceptions import UserError +from flectra.exceptions import UserError, ValidationError @tagged('post_install', '-at_install') @@ -126,3 +126,12 @@ class TestAccountAccount(AccountTestInvoicingCommon): # Try to set the account as a not-reconcile one. with self.assertRaises(UserError), self.cr.savepoint(): account.reconcile = False + + def test_toggle_reconcile_outstanding_account(self): + ''' Test the feature when the user sets an account as not reconcilable when a journal + is configured with this account as the payment credit or debit account. + Since such an account should be reconcilable by nature, a ValidationError is raised.''' + with self.assertRaises(ValidationError), self.cr.savepoint(): + self.company_data['default_journal_bank'].payment_debit_account_id.reconcile = False + with self.assertRaises(ValidationError), self.cr.savepoint(): + self.company_data['default_journal_bank'].payment_credit_account_id.reconcile = False diff --git a/addons/account/tests/test_account_incoming_supplier_invoice.py b/addons/account/tests/test_account_incoming_supplier_invoice.py index 25f2de6ce3..a182d31131 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.flectra.com') + cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'test-company.flectrahq.com') cls.internal_user = cls.env['res.users'].create({ 'name': 'Internal User', - 'login': 'internal.user@test.flectra.com', - 'email': 'internal.user@test.flectra.com', + 'login': 'internal.user@test.flectrahq.com', + 'email': 'internal.user@test.flectrahq.com', }) cls.supplier_partner = cls.env['res.partner'].create({ diff --git a/addons/account/tests/test_account_move_entry.py b/addons/account/tests/test_account_move_entry.py index 5cbc924ffc..07033ccdc7 100644 --- a/addons/account/tests/test_account_move_entry.py +++ b/addons/account/tests/test_account_move_entry.py @@ -511,8 +511,9 @@ class TestAccountMove(AccountTestInvoicingCommon): next.action_post() self.assertEqual(next.name, '00000876-G 0002/2021') # Wait, I didn't want this! - next.journal_id.sequence_override_regex = r'^(?P\d*)(?P.*?)(?P(\d{4})?)(?P)$' - next.name = '/' + next.button_draft() + next.name = False + next.journal_id.sequence_override_regex = r'^(?P\d*)(?P.*?)(?P(\d{4})?)(?P)$' next.action_post() self.assertEqual(next.name, '00000877-G 0002/2020') # Pfew, better! next = self.test_move.copy({'date': self.test_move.date}) diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 0b8b2ab8d4..c915e89f0b 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -2122,3 +2122,29 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): # Check full reconciliation self.assertTrue(all(line.full_reconcile_id for line in lines_to_reconcile), "All tax lines should be fully reconciled") + + def test_caba_undo_reconciliation(self): + ''' Make sure there is no traceback like "Record has already been deleted" during the deletion of partials. ''' + self.cash_basis_transfer_account.reconcile = True + + bill = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'date': '2019-01-01', + 'invoice_line_ids': [(0, 0, { + 'name': 'line', + 'account_id': self.company_data['default_account_expense'].id, + 'price_unit': 1000.0, + 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)], + })], + }) + bill.action_post() + + # Register a payment creating the CABA journal entry on the fly and reconcile it with the tax line. + self.env['account.payment.register']\ + .with_context(active_ids=bill.ids, active_model='account.move')\ + .create({})\ + ._create_payments() + + bill.button_draft() diff --git a/addons/account/tests/test_reconciliation_matching_rules.py b/addons/account/tests/test_reconciliation_matching_rules.py index 7c4d11b60f..2250238b91 100644 --- a/addons/account/tests/test_reconciliation_matching_rules.py +++ b/addons/account/tests/test_reconciliation_matching_rules.py @@ -477,6 +477,49 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon): self.assertEqual(self.invoice_line_1.amount_residual, 60.0, "The invoice should have been partially reconciled") + def test_auto_reconcile_with_duplicate_match(self): + """ If multiple bank statement lines match with the same invoice, ensure the + correct line is auto-validated and no crashing happens. + """ + + # Only the invoice defined in this test should have this partner + partner = self.env['res.partner'].create({'name': "The Only One"}) + invoice_line = self._create_invoice_line( + 2000, partner, 'out_invoice', ref="REF 7788") + + # Enable auto-validation and don't restrict the partners that can be matched on + # so our newly created partner can be matched. + self.rule_1.write({ + 'auto_reconcile': True, + 'match_partner_ids': [(5, 0, 0)], + }) + self.rule_1.match_partner_ids = [] + + # This line has a matching payment reference and the exact amount of the + # invoice. As a result it should auto-validate. + self.bank_line_1.amount = 2000 + self.bank_line_1.partner_id = partner + self.bank_line_1.payment_ref = "REF 7788" + + # This line doesn't have a matching amount or reference, but it does have a + # matching partner. + self.bank_line_2.amount = 1800 + self.bank_line_2.partner_id = partner + self.bank_line_2.payment_ref = "something" + + # Verify the auto-validation happens with the first line, and no exceptions. + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: { + 'aml_ids': [invoice_line.id], + 'model': self.rule_1, + 'status': 'reconciled', + 'partner': self.bank_line_1.partner_id + }, + self.bank_line_2.id: { + 'aml_ids': [] + }, + }, statements=self.bank_st) + def test_auto_reconcile_with_tax(self): ''' Test auto reconciliation with a tax amount included in the bank statement line''' self.rule_1.write({ diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py index c53e2e2994..ee6b4660fe 100644 --- a/addons/account/tests/test_tax.py +++ b/addons/account/tests/test_tax.py @@ -1045,3 +1045,31 @@ class TestTax(TestTaxCommon): ], res3 ) + + def test_mixing_price_included_excluded_with_affect_base(self): + tax_10_fix = self.env['account.tax'].create({ + 'name': "tax_10_fix", + 'amount_type': 'fixed', + 'amount': 10.0, + 'include_base_amount': True, + }) + tax_21 = self.env['account.tax'].create({ + 'name': "tax_21", + 'amount_type': 'percent', + 'amount': 21.0, + 'price_include': True, + 'include_base_amount': True, + }) + + self._check_compute_all_results( + 1222.1, # 'total_included' + 1000.0, # 'total_excluded' + [ + # base , amount + # --------------- + (1000.0, 10.0), + (1010.0, 212.1), + # --------------- + ], + (tax_10_fix + tax_21).compute_all(1210), + ) diff --git a/addons/account/views/account_journal_views.xml b/addons/account/views/account_journal_views.xml index 7847f9ed1f..eed94ff42a 100644 --- a/addons/account/views/account_journal_views.xml +++ b/addons/account/views/account_journal_views.xml @@ -100,7 +100,7 @@
Keep empty for no control
- +
diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 3c22c07a19..b42af781f0 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -155,10 +155,10 @@ + - @@ -191,7 +191,7 @@ - + diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml index aba11c9877..d5e688f8f3 100644 --- a/addons/account/views/account_payment_view.xml +++ b/addons/account/views/account_payment_view.xml @@ -153,6 +153,7 @@ + diff --git a/addons/account/views/report_payment_receipt_templates.xml b/addons/account/views/report_payment_receipt_templates.xml index 3283e03bee..dc887ccb28 100644 --- a/addons/account/views/report_payment_receipt_templates.xml +++ b/addons/account/views/report_payment_receipt_templates.xml @@ -46,12 +46,14 @@ - - - - - - + + + + + + + + diff --git a/addons/account/wizard/account_resequence.py b/addons/account/wizard/account_resequence.py index c0a04d0864..296c28eeca 100644 --- a/addons/account/wizard/account_resequence.py +++ b/addons/account/wizard/account_resequence.py @@ -30,7 +30,12 @@ class ReSequenceWizard(models.TransientModel): active_move_ids = self.env['account.move'].browse(self.env.context['active_ids']) if len(active_move_ids.journal_id) > 1: raise UserError(_('You can only resequence items from the same journal')) - if active_move_ids.journal_id.refund_sequence and len(set(active_move_ids.mapped('move_type')) - {'out_receipt', 'in_receipt'}) > 1: + move_types = set(active_move_ids.mapped('move_type')) + if ( + active_move_ids.journal_id.refund_sequence + and ('in_refund' in move_types or 'out_refund' in move_types) + and len(move_types) > 1 + ): raise UserError(_('The sequences of this journal are different for Invoices and Refunds but you selected some of both types.')) values['move_ids'] = [(6, 0, active_move_ids.ids)] return values diff --git a/addons/account/wizard/account_tour_upload_bill.py b/addons/account/wizard/account_tour_upload_bill.py index 3ea2585028..b8edefdc9d 100644 --- a/addons/account/wizard/account_tour_upload_bill.py +++ b/addons/account/wizard/account_tour_upload_bill.py @@ -63,9 +63,9 @@ class AccountTourUploadBill(models.TransientModel): 'res_model': 'mail.compose.message', 'datas': self.sample_bill_preview, }) - bill = purchase_journal.with_context(default_journal_id=purchase_journal.id, default_move_type='in_invoice').create_invoice_from_attachment(attachment.ids) - if self.selection == 'sample': - bill.write({ + bill_action = purchase_journal.with_context(default_journal_id=purchase_journal.id, default_move_type='in_invoice').create_invoice_from_attachment(attachment.ids) + bill = self.env['account.move'].browse(bill_action['res_id']) + bill.write({ 'partner_id': self.env.ref('base.main_partner').id, 'ref': 'INV/2020/0011' }) diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py index 22fa2e1116..c4142584c7 100644 --- a/addons/account_check_printing/models/account_payment.py +++ b/addons/account_check_printing/models/account_payment.py @@ -122,6 +122,7 @@ class AccountPayment(models.Model): FROM account_payment payment JOIN account_move move ON movE.id = payment.move_id WHERE journal_id = %(journal_id)s + AND check_number IS NOT NULL ORDER BY check_number::INTEGER DESC LIMIT 1 """, { diff --git a/addons/account_edi_facturx/models/account_edi_format.py b/addons/account_edi_facturx/models/account_edi_format.py index 9403e365bf..7db3ae8a30 100644 --- a/addons/account_edi_facturx/models/account_edi_format.py +++ b/addons/account_edi_facturx/models/account_edi_format.py @@ -182,7 +182,8 @@ class AccountEdiFormat(models.Model): invoice.move_type = default_move_type # self could be a single record (editing) or be empty (new). - with Form(invoice.with_context(default_move_type=default_move_type)) as invoice_form: + with Form(invoice.with_context(default_move_type=default_move_type, + account_predictive_bills_disable_prediction=True)) as invoice_form: # Partner (first step to avoid warning 'Warning! You must first select a partner.'). partner_type = invoice_form.journal_id.type == 'purchase' and 'SellerTradeParty' or 'BuyerTradeParty' elements = tree.xpath('//ram:' + partner_type + '/ram:SpecifiedTaxRegistration/ram:ID', namespaces=tree.nsmap) diff --git a/addons/account_edi_ubl/data/ubl_templates.xml b/addons/account_edi_ubl/data/ubl_templates.xml index 81ee03dc57..6123094c1e 100644 --- a/addons/account_edi_ubl/data/ubl_templates.xml +++ b/addons/account_edi_ubl/data/ubl_templates.xml @@ -123,6 +123,7 @@ + diff --git a/addons/account_edi_ubl/models/account_edi_format.py b/addons/account_edi_ubl/models/account_edi_format.py index d9d84fd290..49d29bb4b7 100644 --- a/addons/account_edi_ubl/models/account_edi_format.py +++ b/addons/account_edi_ubl/models/account_edi_format.py @@ -16,6 +16,22 @@ _logger = logging.getLogger(__name__) class AccountEdiFormat(models.Model): _inherit = 'account.edi.format' + def _create_invoice_from_ubl(self, tree): + invoice = self.env['account.move'] + journal = invoice._get_default_journal() + + move_type = 'out_invoice' if journal.type == 'sale' else 'in_invoice' + element = tree.find('.//{*}InvoiceTypeCode') + if element is not None and element.text == '381': + move_type = 'in_refund' if move_type == 'in_invoice' else 'out_refund' + + invoice = invoice.with_context(default_move_type=move_type, default_journal_id=journal.id) + return self._import_ubl(tree, invoice) + + def _update_invoice_from_ubl(self, tree, invoice): + invoice = invoice.with_context(default_move_type=invoice.move_type, default_journal_id=invoice.journal_id.id) + return self._import_ubl(tree, invoice) + def _import_ubl(self, tree, invoice): """ Decodes an UBL invoice into an invoice. @@ -37,19 +53,9 @@ class AccountEdiFormat(models.Model): return namespaces namespaces = _get_ubl_namespaces() - if not invoice: - invoice = self.env['account.move'].create({}) - elements = tree.xpath('//cbc:InvoiceTypeCode', namespaces=namespaces) - if elements: - type_code = elements[0].text - move_type = 'in_refund' if type_code == '381' else 'in_invoice' - else: - move_type = 'in_invoice' + with Form(invoice.with_context(account_predictive_bills_disable_prediction=True)) as invoice_form: - default_journal = invoice.with_context(default_move_type=move_type)._get_default_journal() - - with Form(invoice.with_context(default_move_type=move_type, default_journal_id=default_journal.id)) as invoice_form: # Reference elements = tree.xpath('//cbc:ID', namespaces=namespaces) if elements: @@ -100,7 +106,7 @@ class AccountEdiFormat(models.Model): if elements: partner_mail = elements[0].text domains.append([('email', '=', partner_mail)]) - elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:ID', namespaces=namespaces) + elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:CompanyID', namespaces=namespaces) if elements: partner_id = elements[0].text domains.append([('vat', 'like', partner_id)]) diff --git a/addons/adyen_platforms/data/adyen_platforms_data.xml b/addons/adyen_platforms/data/adyen_platforms_data.xml index d60cbdce4f..a408cdbfd4 100644 --- a/addons/adyen_platforms/data/adyen_platforms_data.xml +++ b/addons/adyen_platforms/data/adyen_platforms_data.xml @@ -3,12 +3,12 @@ adyen_platforms.proxy_url - https://payment-adyen.flectra.com/payment_proxy_adyen/ + https://payment-adyen.flectrahq.com/payment_proxy_adyen/ adyen_platforms.onboarding_url - https://flectrahq.com/flectra_adyen/ + https://www.flectrahq.com/flectra_adyen/ diff --git a/addons/adyen_platforms/models/adyen_account.py b/addons/adyen_platforms/models/adyen_account.py index 117c93899d..7f134ee8bf 100644 --- a/addons/adyen_platforms/models/adyen_account.py +++ b/addons/adyen_platforms/models/adyen_account.py @@ -159,10 +159,10 @@ class AdyenAccount(models.Model): adyen_account_id = super(AdyenAccount, self).create(values) self.env.company.adyen_account_id = adyen_account_id.id - # Create account on flectra.com, proxy and Adyen + # Create account on flectrahq.com, proxy and Adyen response = adyen_account_id._adyen_rpc('create_account_holder', adyen_account_id._format_data()) - # Save adyen_uuid and proxy_token, that have been generated by flectra.com and the proxy + # Save adyen_uuid and proxy_token, that have been generated by flectrahq.com and the proxy adyen_account_id.with_context(update_from_adyen=True).write({ 'adyen_uuid': response['adyen_uuid'], 'proxy_token': response['proxy_token'], @@ -192,8 +192,8 @@ class AdyenAccount(models.Model): def action_create_redirect(self): ''' Accessing the FormView to create an Adyen account needs to be done through this action. - The action will redirect the user to accounts.flectra.com to link an Flectra user_id to the Adyen - account. After logging in on flectra.com the user will be redirected to his DB with a token in + The action will redirect the user to accounts.flectrahq.com to link an Flectra user_id to the Adyen + account. After logging in on flectrahq.com the user will be redirected to his DB with a token in the URL. This token is then needed to create the Adyen account. ''' if self.env.company.adyen_account_id: diff --git a/addons/adyen_platforms/static/src/xml/adyen_account_templates.xml b/addons/adyen_platforms/static/src/xml/adyen_account_templates.xml index a86f4a23d4..33fb744cca 100644 --- a/addons/adyen_platforms/static/src/xml/adyen_account_templates.xml +++ b/addons/adyen_platforms/static/src/xml/adyen_account_templates.xml @@ -71,7 +71,7 @@ services. You may not use the payment services for different types of product and services than as registered with your application. In particular you confirm that you will not use the payment services for any type of product or service appearing in the - + Prohibited Products and Services List of our Processor. If we or our Processor at any time discover that the information you provided about your business is incorrect or has changed without informing us or if you violate any of @@ -102,8 +102,8 @@ I confirm I have taken notice of and accept the following terms and restrictions:
    -
  • Adyen MarketPay Terms and Conditions (click here to download and review)
  • -
  • Adyen Restricted and Prohibited Products and Services list (click here to download and review)
  • +
  • Adyen MarketPay Terms and Conditions (click here to download and review)
  • +
  • Adyen Restricted and Prohibited Products and Services list (click here to download and review)
diff --git a/addons/analytic/__manifest__.py b/addons/analytic/__manifest__.py index 366e08829f..0f87f2edc5 100644 --- a/addons/analytic/__manifest__.py +++ b/addons/analytic/__manifest__.py @@ -10,7 +10,7 @@ Module for defining analytic accounting object. =============================================== -In Odoo, analytic accounts are linked to general accounts but are treated +In Flectra, analytic accounts are linked to general accounts but are treated totally independently. So, you can enter various different analytic operations that have no counterpart in the general financial accounts. """, diff --git a/addons/auth_ldap/README.rst b/addons/auth_ldap/README.rst index 64f802b01e..870b03948a 100644 --- a/addons/auth_ldap/README.rst +++ b/addons/auth_ldap/README.rst @@ -1,7 +1,7 @@ Adds support for authentication by LDAP server. =============================================== This module allows users to login with their LDAP username and password, and -will automatically create Odoo users for them on the fly. +will automatically create Flectra users for them on the fly. **Note:** This module only work on servers that have Python's ``python-ldap`` module installed. @@ -10,7 +10,7 @@ Configuration: After installing this module, you need to configure the LDAP parameters in the General Settings menu. Different companies may have different LDAP servers, as long as they have unique usernames (usernames need to be unique -in Odoo, even across multiple companies). +in Flectra, even across multiple companies). Anonymous LDAP binding is also supported (for LDAP servers that allow it), by simply keeping the LDAP user and password empty in the LDAP configuration. @@ -26,24 +26,24 @@ manpage: manpage:`ldap.conf(5)`. Security Considerations: ------------------------ -Users' LDAP passwords are never stored in the Odoo database, the LDAP server +Users' LDAP passwords are never stored in the Flectra database, the LDAP server is queried whenever a user needs to be authenticated. No duplication of the password occurs, and passwords are managed in one place only. -Odoo does not manage password changes in the LDAP, so any change of password +Flectra does not manage password changes in the LDAP, so any change of password should be conducted by other means in the LDAP directory directly (for LDAP users). -It is also possible to have local Odoo users in the database along with +It is also possible to have local Flectra users in the database along with LDAP-authenticated users (the Administrator account is one obvious example). Here is how it works: --------------------- - * The system first attempts to authenticate users against the local Odoo + * The system first attempts to authenticate users against the local Flectra database; * if this authentication fails (for example because the user has no local password), the system then attempts to authenticate against LDAP; -As LDAP users have blank passwords by default in the local Odoo database +As LDAP users have blank passwords by default in the local Flectra database (which means no access), the first step always fails and the LDAP server is queried to do the authentication. @@ -61,6 +61,6 @@ allows pre-setting the default groups and menus of the first-time users. assigned as local password for each new LDAP user, effectively setting a *master password* for these users (until manually changed). You usually do not want this. One easy way to setup a template user is to - login once with a valid LDAP user, let Odoo create a blank local + login once with a valid LDAP user, let Flectra create a blank local user with the same login (and a blank password), then rename this new user to a username that does not exist in LDAP, and setup its groups diff --git a/addons/auth_oauth/__manifest__.py b/addons/auth_oauth/__manifest__.py index 85d1357870..37324208db 100644 --- a/addons/auth_oauth/__manifest__.py +++ b/addons/auth_oauth/__manifest__.py @@ -8,7 +8,7 @@ Allow users to login through OAuth2 Provider. ============================================= """, - 'maintainer': 'Odoo S.A.', + 'maintainer': 'FlectraHQ, Odoo S.A.', 'depends': ['base', 'web', 'base_setup', 'auth_signup'], 'data': [ 'data/auth_oauth_data.xml', diff --git a/addons/auth_oauth/data/auth_oauth_data.xml b/addons/auth_oauth/data/auth_oauth_data.xml index 199d25dff7..0470bc748a 100644 --- a/addons/auth_oauth/data/auth_oauth_data.xml +++ b/addons/auth_oauth/data/auth_oauth_data.xml @@ -3,9 +3,9 @@ Flectrahq.com Accounts - https://accounts.flectra.com/oauth2/auth + https://accounts.flectrahq.com/oauth2/auth userinfo - https://accounts.flectra.com/oauth2/tokeninfo + https://accounts.flectrahq.com/oauth2/tokeninfo fa fa-fw o_custom_icon Log in with Flectrahq.com @@ -26,7 +26,7 @@ https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/oauth2/v1/tokeninfo https://www.googleapis.com/oauth2/v1/userinfo - fa fa-fw fa-google-plus-square + fa fa-fw fa-google Log in with Google diff --git a/addons/auth_oauth/views/res_config_settings_views.xml b/addons/auth_oauth/views/res_config_settings_views.xml index e93b509351..7739b022ea 100644 --- a/addons/auth_oauth/views/res_config_settings_views.xml +++ b/addons/auth_oauth/views/res_config_settings_views.xml @@ -30,7 +30,7 @@