1# -*- coding: utf-8 -*-
2
3from odoo import api, fields, models, _
4from odoo.exceptions import RedirectWarning, UserError, ValidationError, AccessError
5from odoo.tools import float_compare, date_utils, email_split, email_re
6from odoo.tools.misc import formatLang, format_date, get_lang
7
8from datetime import date, timedelta
9from collections import defaultdict
10from itertools import zip_longest
11from hashlib import sha256
12from json import dumps
13
14import ast
15import json
16import re
17import warnings
18
19#forbidden fields
20INTEGRITY_HASH_MOVE_FIELDS = ('date', 'journal_id', 'company_id')
21INTEGRITY_HASH_LINE_FIELDS = ('debit', 'credit', 'account_id', 'partner_id')
22
23
24def calc_check_digits(number):
25    """Calculate the extra digits that should be appended to the number to make it a valid number.
26    Source: python-stdnum iso7064.mod_97_10.calc_check_digits
27    """
28    number_base10 = ''.join(str(int(x, 36)) for x in number)
29    checksum = int(number_base10) % 97
30    return '%02d' % ((98 - 100 * checksum) % 97)
31
32
33class AccountMove(models.Model):
34    _name = "account.move"
35    _inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin', 'sequence.mixin']
36    _description = "Journal Entry"
37    _order = 'date desc, name desc, id desc'
38    _mail_post_access = 'read'
39    _check_company_auto = True
40    _sequence_index = "journal_id"
41
42    @property
43    def _sequence_monthly_regex(self):
44        return self.journal_id.sequence_override_regex or super()._sequence_monthly_regex
45
46    @property
47    def _sequence_yearly_regex(self):
48        return self.journal_id.sequence_override_regex or super()._sequence_yearly_regex
49
50    @property
51    def _sequence_fixed_regex(self):
52        return self.journal_id.sequence_override_regex or super()._sequence_fixed_regex
53
54    @api.model
55    def _search_default_journal(self, journal_types):
56        company_id = self._context.get('default_company_id', self.env.company.id)
57        domain = [('company_id', '=', company_id), ('type', 'in', journal_types)]
58
59        journal = None
60        if self._context.get('default_currency_id'):
61            currency_domain = domain + [('currency_id', '=', self._context['default_currency_id'])]
62            journal = self.env['account.journal'].search(currency_domain, limit=1)
63
64        if not journal:
65            journal = self.env['account.journal'].search(domain, limit=1)
66
67        if not journal:
68            company = self.env['res.company'].browse(company_id)
69
70            error_msg = _(
71                "No journal could be found in company %(company_name)s for any of those types: %(journal_types)s",
72                company_name=company.display_name,
73                journal_types=', '.join(journal_types),
74            )
75            raise UserError(error_msg)
76
77        return journal
78
79    @api.model
80    def _get_default_journal(self):
81        ''' Get the default journal.
82        It could either be passed through the context using the 'default_journal_id' key containing its id,
83        either be determined by the default type.
84        '''
85        move_type = self._context.get('default_move_type', 'entry')
86        if move_type in self.get_sale_types(include_receipts=True):
87            journal_types = ['sale']
88        elif move_type in self.get_purchase_types(include_receipts=True):
89            journal_types = ['purchase']
90        else:
91            journal_types = self._context.get('default_move_journal_types', ['general'])
92
93        if self._context.get('default_journal_id'):
94            journal = self.env['account.journal'].browse(self._context['default_journal_id'])
95
96            if move_type != 'entry' and journal.type not in journal_types:
97                raise UserError(_(
98                    "Cannot create an invoice of type %(move_type)s with a journal having %(journal_type)s as type.",
99                    move_type=move_type,
100                    journal_type=journal.type,
101                ))
102        else:
103            journal = self._search_default_journal(journal_types)
104
105        return journal
106
107    # TODO remove in master
108    @api.model
109    def _get_default_invoice_date(self):
110        warnings.warn("Method '_get_default_invoice_date()' is deprecated and has been removed.", DeprecationWarning)
111        return fields.Date.context_today(self) if self._context.get('default_move_type', 'entry') in self.get_purchase_types(include_receipts=True) else False
112
113    @api.model
114    def _get_default_currency(self):
115        ''' Get the default currency from either the journal, either the default journal's company. '''
116        journal = self._get_default_journal()
117        return journal.currency_id or journal.company_id.currency_id
118
119    @api.model
120    def _get_default_invoice_incoterm(self):
121        ''' Get the default incoterm for invoice. '''
122        return self.env.company.incoterm_id
123
124    # ==== Business fields ====
125    name = fields.Char(string='Number', copy=False, compute='_compute_name', readonly=False, store=True, index=True, tracking=True)
126    highest_name = fields.Char(compute='_compute_highest_name')
127    show_name_warning = fields.Boolean(store=False)
128    date = fields.Date(
129        string='Date',
130        required=True,
131        index=True,
132        readonly=True,
133        states={'draft': [('readonly', False)]},
134        copy=False,
135        default=fields.Date.context_today
136    )
137    ref = fields.Char(string='Reference', copy=False, tracking=True)
138    narration = fields.Text(string='Terms and Conditions')
139    state = fields.Selection(selection=[
140            ('draft', 'Draft'),
141            ('posted', 'Posted'),
142            ('cancel', 'Cancelled'),
143        ], string='Status', required=True, readonly=True, copy=False, tracking=True,
144        default='draft')
145    posted_before = fields.Boolean(help="Technical field for knowing if the move has been posted before", copy=False)
146    move_type = fields.Selection(selection=[
147            ('entry', 'Journal Entry'),
148            ('out_invoice', 'Customer Invoice'),
149            ('out_refund', 'Customer Credit Note'),
150            ('in_invoice', 'Vendor Bill'),
151            ('in_refund', 'Vendor Credit Note'),
152            ('out_receipt', 'Sales Receipt'),
153            ('in_receipt', 'Purchase Receipt'),
154        ], string='Type', required=True, store=True, index=True, readonly=True, tracking=True,
155        default="entry", change_default=True)
156    type_name = fields.Char('Type Name', compute='_compute_type_name')
157    to_check = fields.Boolean(string='To Check', default=False,
158        help='If this checkbox is ticked, it means that the user was not sure of all the related information at the time of the creation of the move and that the move needs to be checked again.')
159    journal_id = fields.Many2one('account.journal', string='Journal', required=True, readonly=True,
160        states={'draft': [('readonly', False)]},
161        check_company=True, domain="[('id', 'in', suitable_journal_ids)]",
162        default=_get_default_journal)
163    suitable_journal_ids = fields.Many2many('account.journal', compute='_compute_suitable_journal_ids')
164    company_id = fields.Many2one(comodel_name='res.company', string='Company',
165                                 store=True, readonly=True,
166                                 compute='_compute_company_id')
167    company_currency_id = fields.Many2one(string='Company Currency', readonly=True,
168        related='company_id.currency_id')
169    currency_id = fields.Many2one('res.currency', store=True, readonly=True, tracking=True, required=True,
170        states={'draft': [('readonly', False)]},
171        string='Currency',
172        default=_get_default_currency)
173    line_ids = fields.One2many('account.move.line', 'move_id', string='Journal Items', copy=True, readonly=True,
174        states={'draft': [('readonly', False)]})
175    partner_id = fields.Many2one('res.partner', readonly=True, tracking=True,
176        states={'draft': [('readonly', False)]},
177        check_company=True,
178        string='Partner', change_default=True)
179    commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', store=True, readonly=True,
180        compute='_compute_commercial_partner_id')
181    country_code = fields.Char(related='company_id.country_id.code', readonly=True)
182    user_id = fields.Many2one(string='User', related='invoice_user_id',
183        help='Technical field used to fit the generic behavior in mail templates.')
184    is_move_sent = fields.Boolean(
185        readonly=True,
186        default=False,
187        copy=False,
188        tracking=True,
189        help="It indicates that the invoice/payment has been sent.",
190    )
191    partner_bank_id = fields.Many2one('res.partner.bank', string='Recipient Bank',
192        help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Vendor Credit Note, otherwise a Partner bank account number.',
193        check_company=True)
194    payment_reference = fields.Char(string='Payment Reference', index=True, copy=False,
195        help="The payment reference to set on journal items.")
196    payment_id = fields.Many2one(
197        index=True,
198        comodel_name='account.payment',
199        string="Payment", copy=False, check_company=True)
200    statement_line_id = fields.Many2one(
201        comodel_name='account.bank.statement.line',
202        string="Statement Line", copy=False, check_company=True)
203
204    # === Amount fields ===
205    amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, tracking=True,
206        compute='_compute_amount')
207    amount_tax = fields.Monetary(string='Tax', store=True, readonly=True,
208        compute='_compute_amount')
209    amount_total = fields.Monetary(string='Total', store=True, readonly=True,
210        compute='_compute_amount',
211        inverse='_inverse_amount_total')
212    amount_residual = fields.Monetary(string='Amount Due', store=True,
213        compute='_compute_amount')
214    amount_untaxed_signed = fields.Monetary(string='Untaxed Amount Signed', store=True, readonly=True,
215        compute='_compute_amount', currency_field='company_currency_id')
216    amount_tax_signed = fields.Monetary(string='Tax Signed', store=True, readonly=True,
217        compute='_compute_amount', currency_field='company_currency_id')
218    amount_total_signed = fields.Monetary(string='Total Signed', store=True, readonly=True,
219        compute='_compute_amount', currency_field='company_currency_id')
220    amount_residual_signed = fields.Monetary(string='Amount Due Signed', store=True,
221        compute='_compute_amount', currency_field='company_currency_id')
222    amount_by_group = fields.Binary(string="Tax amount by group",
223        compute='_compute_invoice_taxes_by_group',
224        help='Edit Tax amounts if you encounter rounding issues.')
225    payment_state = fields.Selection(selection=[
226        ('not_paid', 'Not Paid'),
227        ('in_payment', 'In Payment'),
228        ('paid', 'Paid'),
229        ('partial', 'Partially Paid'),
230        ('reversed', 'Reversed'),
231        ('invoicing_legacy', 'Invoicing App Legacy')],
232        string="Payment Status", store=True, readonly=True, copy=False, tracking=True,
233        compute='_compute_amount')
234
235    # ==== Cash basis feature fields ====
236    tax_cash_basis_rec_id = fields.Many2one(
237        'account.partial.reconcile',
238        string='Tax Cash Basis Entry of',
239        help="Technical field used to keep track of the tax cash basis reconciliation. "
240             "This is needed when cancelling the source: it will post the inverse journal entry to cancel that part too.")
241    tax_cash_basis_move_id = fields.Many2one(
242        comodel_name='account.move',
243        string="Origin Tax Cash Basis Entry",
244        help="The journal entry from which this tax cash basis journal entry has been created.")
245
246    # ==== Auto-post feature fields ====
247    auto_post = fields.Boolean(string='Post Automatically', default=False, copy=False,
248        help='If this checkbox is ticked, this entry will be automatically posted at its date.')
249
250    # ==== Reverse feature fields ====
251    reversed_entry_id = fields.Many2one('account.move', string="Reversal of", readonly=True, copy=False,
252        check_company=True)
253    reversal_move_id = fields.One2many('account.move', 'reversed_entry_id')
254
255    # =========================================================
256    # Invoice related fields
257    # =========================================================
258
259    # ==== Business fields ====
260    fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', readonly=True,
261        states={'draft': [('readonly', False)]},
262        check_company=True,
263        domain="[('company_id', '=', company_id)]", ondelete="restrict",
264        help="Fiscal positions are used to adapt taxes and accounts for particular customers or sales orders/invoices. "
265             "The default value comes from the customer.")
266    invoice_user_id = fields.Many2one('res.users', copy=False, tracking=True,
267        string='Salesperson',
268        default=lambda self: self.env.user)
269    invoice_date = fields.Date(string='Invoice/Bill Date', readonly=True, index=True, copy=False,
270        states={'draft': [('readonly', False)]})
271    invoice_date_due = fields.Date(string='Due Date', readonly=True, index=True, copy=False,
272        states={'draft': [('readonly', False)]})
273    invoice_origin = fields.Char(string='Origin', readonly=True, tracking=True,
274        help="The document(s) that generated the invoice.")
275    invoice_payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms',
276        check_company=True,
277        readonly=True, states={'draft': [('readonly', False)]})
278    # /!\ invoice_line_ids is just a subset of line_ids.
279    invoice_line_ids = fields.One2many('account.move.line', 'move_id', string='Invoice lines',
280        copy=False, readonly=True,
281        domain=[('exclude_from_invoice_tab', '=', False)],
282        states={'draft': [('readonly', False)]})
283    invoice_incoterm_id = fields.Many2one('account.incoterms', string='Incoterm',
284        default=_get_default_invoice_incoterm,
285        help='International Commercial Terms are a series of predefined commercial terms used in international transactions.')
286    display_qr_code = fields.Boolean(string="Display QR-code", related='company_id.qr_code')
287    qr_code_method = fields.Selection(string="Payment QR-code",
288        selection=lambda self: self.env['res.partner.bank'].get_available_qr_methods_in_sequence(),
289        help="Type of QR-code to be generated for the payment of this invoice, when printing it. If left blank, the first available and usable method will be used.")
290
291    # ==== Payment widget fields ====
292    invoice_outstanding_credits_debits_widget = fields.Text(groups="account.group_account_invoice,account.group_account_readonly",
293        compute='_compute_payments_widget_to_reconcile_info')
294    invoice_has_outstanding = fields.Boolean(groups="account.group_account_invoice,account.group_account_readonly",
295        compute='_compute_payments_widget_to_reconcile_info')
296    invoice_payments_widget = fields.Text(groups="account.group_account_invoice,account.group_account_readonly",
297        compute='_compute_payments_widget_reconciled_info')
298
299    # ==== Vendor bill fields ====
300    invoice_vendor_bill_id = fields.Many2one('account.move', store=False,
301        check_company=True,
302        string='Vendor Bill',
303        help="Auto-complete from a past bill.")
304    invoice_source_email = fields.Char(string='Source Email', tracking=True)
305    invoice_partner_display_name = fields.Char(compute='_compute_invoice_partner_display_info', store=True)
306
307    # ==== Cash rounding fields ====
308    invoice_cash_rounding_id = fields.Many2one('account.cash.rounding', string='Cash Rounding Method',
309        readonly=True, states={'draft': [('readonly', False)]},
310        help='Defines the smallest coinage of the currency that can be used to pay by cash.')
311
312    # ==== Display purpose fields ====
313    invoice_filter_type_domain = fields.Char(compute='_compute_invoice_filter_type_domain',
314        help="Technical field used to have a dynamic domain on journal / taxes in the form view.")
315    bank_partner_id = fields.Many2one('res.partner', help='Technical field to get the domain on the bank', compute='_compute_bank_partner_id')
316    invoice_has_matching_suspense_amount = fields.Boolean(compute='_compute_has_matching_suspense_amount',
317        groups='account.group_account_invoice,account.group_account_readonly',
318        help="Technical field used to display an alert on invoices if there is at least a matching amount in any supsense account.")
319    tax_lock_date_message = fields.Char(
320        compute='_compute_tax_lock_date_message',
321        help="Technical field used to display a message when the invoice's accounting date is prior of the tax lock date.")
322    # Technical field to hide Reconciled Entries stat button
323    has_reconciled_entries = fields.Boolean(compute="_compute_has_reconciled_entries")
324    show_reset_to_draft_button = fields.Boolean(compute='_compute_show_reset_to_draft_button')
325
326    # ==== Hash Fields ====
327    restrict_mode_hash_table = fields.Boolean(related='journal_id.restrict_mode_hash_table')
328    secure_sequence_number = fields.Integer(string="Inalteralbility No Gap Sequence #", readonly=True, copy=False)
329    inalterable_hash = fields.Char(string="Inalterability Hash", readonly=True, copy=False)
330    string_to_hash = fields.Char(compute='_compute_string_to_hash', readonly=True)
331
332    @api.model
333    def _field_will_change(self, record, vals, field_name):
334        if field_name not in vals:
335            return False
336        field = record._fields[field_name]
337        if field.type == 'many2one':
338            return record[field_name].id != vals[field_name]
339        if field.type == 'many2many':
340            current_ids = set(record[field_name].ids)
341            after_write_ids = set(record.new({field_name: vals[field_name]})[field_name].ids)
342            return current_ids != after_write_ids
343        if field.type == 'one2many':
344            return True
345        if field.type == 'monetary' and record[field.currency_field]:
346            return not record[field.currency_field].is_zero(record[field_name] - vals[field_name])
347        if field.type == 'float':
348            record_value = field.convert_to_cache(record[field_name], record)
349            to_write_value = field.convert_to_cache(vals[field_name], record)
350            return record_value != to_write_value
351        return record[field_name] != vals[field_name]
352
353    @api.model
354    def _cleanup_write_orm_values(self, record, vals):
355        cleaned_vals = dict(vals)
356        for field_name, value in vals.items():
357            if not self._field_will_change(record, vals, field_name):
358                del cleaned_vals[field_name]
359        return cleaned_vals
360
361    # -------------------------------------------------------------------------
362    # ONCHANGE METHODS
363    # -------------------------------------------------------------------------
364
365    def _get_accounting_date(self, invoice_date, has_tax):
366        """Get correct accounting date for previous periods, taking tax lock date into account.
367
368        When registering an invoice in the past, we still want the sequence to be increasing.
369        We then take the last day of the period, depending on the sequence format.
370        If there is a tax lock date and there are taxes involved, we register the invoice at the
371        last date of the first open period.
372
373        :param invoice_date (datetime.date): The invoice date
374        :param has_tax (bool): Iff any taxes are involved in the lines of the invoice
375        :return (datetime.date):
376        """
377        tax_lock_date = self.company_id.tax_lock_date
378        today = fields.Date.today()
379        if invoice_date and tax_lock_date and has_tax and invoice_date <= tax_lock_date:
380            invoice_date = tax_lock_date + timedelta(days=1)
381
382        if self.is_sale_document(include_receipts=True):
383            return invoice_date
384        elif self.is_purchase_document(include_receipts=True):
385            highest_name = self.highest_name or self._get_last_sequence(relaxed=True)
386            number_reset = self._deduce_sequence_number_reset(highest_name)
387            if not highest_name or number_reset == 'month':
388                if (today.year, today.month) > (invoice_date.year, invoice_date.month):
389                    return date_utils.get_month(invoice_date)[1]
390                else:
391                    return max(invoice_date, today)
392            elif number_reset == 'year':
393                if today.year > invoice_date.year:
394                    return date(invoice_date.year, 12, 31)
395                else:
396                    return max(invoice_date, today)
397        return invoice_date
398
399    @api.onchange('invoice_date', 'highest_name', 'company_id')
400    def _onchange_invoice_date(self):
401        if self.invoice_date:
402            if not self.invoice_payment_term_id and (not self.invoice_date_due or self.invoice_date_due < self.invoice_date):
403                self.invoice_date_due = self.invoice_date
404
405            has_tax = bool(self.line_ids.tax_ids or self.line_ids.tax_tag_ids)
406            accounting_date = self._get_accounting_date(self.invoice_date, has_tax)
407            if accounting_date != self.date:
408                self.date = accounting_date
409                self._onchange_currency()
410
411    @api.onchange('journal_id')
412    def _onchange_journal(self):
413        if self.journal_id and self.journal_id.currency_id:
414            new_currency = self.journal_id.currency_id
415            if new_currency != self.currency_id:
416                self.currency_id = new_currency
417                self._onchange_currency()
418        if self.state == 'draft' and self._get_last_sequence() and self.name and self.name != '/':
419            self.name = '/'
420
421    @api.onchange('partner_id')
422    def _onchange_partner_id(self):
423        self = self.with_company(self.journal_id.company_id)
424
425        warning = {}
426        if self.partner_id:
427            rec_account = self.partner_id.property_account_receivable_id
428            pay_account = self.partner_id.property_account_payable_id
429            if not rec_account and not pay_account:
430                action = self.env.ref('account.action_account_config')
431                msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
432                raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
433            p = self.partner_id
434            if p.invoice_warn == 'no-message' and p.parent_id:
435                p = p.parent_id
436            if p.invoice_warn and p.invoice_warn != 'no-message':
437                # Block if partner only has warning but parent company is blocked
438                if p.invoice_warn != 'block' and p.parent_id and p.parent_id.invoice_warn == 'block':
439                    p = p.parent_id
440                warning = {
441                    'title': _("Warning for %s", p.name),
442                    'message': p.invoice_warn_msg
443                }
444                if p.invoice_warn == 'block':
445                    self.partner_id = False
446                    return {'warning': warning}
447
448        if self.is_sale_document(include_receipts=True) and self.partner_id:
449            self.invoice_payment_term_id = self.partner_id.property_payment_term_id or self.invoice_payment_term_id
450            new_term_account = self.partner_id.commercial_partner_id.property_account_receivable_id
451        elif self.is_purchase_document(include_receipts=True) and self.partner_id:
452            self.invoice_payment_term_id = self.partner_id.property_supplier_payment_term_id or self.invoice_payment_term_id
453            new_term_account = self.partner_id.commercial_partner_id.property_account_payable_id
454        else:
455            new_term_account = None
456
457        for line in self.line_ids:
458            line.partner_id = self.partner_id.commercial_partner_id
459
460            if new_term_account and line.account_id.user_type_id.type in ('receivable', 'payable'):
461                line.account_id = new_term_account
462
463        self._compute_bank_partner_id()
464        self.partner_bank_id = self.bank_partner_id.bank_ids and self.bank_partner_id.bank_ids[0]
465
466        # Find the new fiscal position.
467        delivery_partner_id = self._get_invoice_delivery_partner_id()
468        self.fiscal_position_id = self.env['account.fiscal.position'].get_fiscal_position(
469            self.partner_id.id, delivery_id=delivery_partner_id)
470        self._recompute_dynamic_lines()
471        if warning:
472            return {'warning': warning}
473
474    @api.onchange('date', 'currency_id')
475    def _onchange_currency(self):
476        currency = self.currency_id or self.company_id.currency_id
477
478        if self.is_invoice(include_receipts=True):
479            for line in self._get_lines_onchange_currency():
480                line.currency_id = currency
481                line._onchange_currency()
482        else:
483            for line in self.line_ids:
484                line._onchange_currency()
485
486        self._recompute_dynamic_lines(recompute_tax_base_amount=True)
487
488    @api.onchange('payment_reference')
489    def _onchange_payment_reference(self):
490        for line in self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')):
491            line.name = self.payment_reference or ''
492
493    @api.onchange('invoice_vendor_bill_id')
494    def _onchange_invoice_vendor_bill(self):
495        if self.invoice_vendor_bill_id:
496            # Copy invoice lines.
497            for line in self.invoice_vendor_bill_id.invoice_line_ids:
498                copied_vals = line.copy_data()[0]
499                copied_vals['move_id'] = self.id
500                new_line = self.env['account.move.line'].new(copied_vals)
501                new_line.recompute_tax_line = True
502
503            # Copy payment terms.
504            self.invoice_payment_term_id = self.invoice_vendor_bill_id.invoice_payment_term_id
505
506            # Copy currency.
507            if self.currency_id != self.invoice_vendor_bill_id.currency_id:
508                self.currency_id = self.invoice_vendor_bill_id.currency_id
509
510            # Reset
511            self.invoice_vendor_bill_id = False
512            self._recompute_dynamic_lines()
513
514    @api.onchange('move_type')
515    def _onchange_type(self):
516        ''' Onchange made to filter the partners depending of the type. '''
517        if self.is_sale_document(include_receipts=True):
518            if self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms'):
519                self.narration = self.company_id.invoice_terms or self.env.company.invoice_terms
520
521    @api.onchange('invoice_line_ids')
522    def _onchange_invoice_line_ids(self):
523        current_invoice_lines = self.line_ids.filtered(lambda line: not line.exclude_from_invoice_tab)
524        others_lines = self.line_ids - current_invoice_lines
525        if others_lines and current_invoice_lines - self.invoice_line_ids:
526            others_lines[0].recompute_tax_line = True
527        self.line_ids = others_lines + self.invoice_line_ids
528        self._onchange_recompute_dynamic_lines()
529
530    @api.onchange('line_ids', 'invoice_payment_term_id', 'invoice_date_due', 'invoice_cash_rounding_id', 'invoice_vendor_bill_id')
531    def _onchange_recompute_dynamic_lines(self):
532        self._recompute_dynamic_lines()
533
534    @api.model
535    def _get_tax_grouping_key_from_tax_line(self, tax_line):
536        ''' Create the dictionary based on a tax line that will be used as key to group taxes together.
537        /!\ Must be consistent with '_get_tax_grouping_key_from_base_line'.
538        :param tax_line:    An account.move.line being a tax line (with 'tax_repartition_line_id' set then).
539        :return:            A dictionary containing all fields on which the tax will be grouped.
540        '''
541        return {
542            'tax_repartition_line_id': tax_line.tax_repartition_line_id.id,
543            'account_id': tax_line.account_id.id,
544            'currency_id': tax_line.currency_id.id,
545            'analytic_tag_ids': [(6, 0, tax_line.tax_line_id.analytic and tax_line.analytic_tag_ids.ids or [])],
546            'analytic_account_id': tax_line.tax_line_id.analytic and tax_line.analytic_account_id.id,
547            'tax_ids': [(6, 0, tax_line.tax_ids.ids)],
548            'tax_tag_ids': [(6, 0, tax_line.tax_tag_ids.ids)],
549        }
550
551    @api.model
552    def _get_tax_grouping_key_from_base_line(self, base_line, tax_vals):
553        ''' Create the dictionary based on a base line that will be used as key to group taxes together.
554        /!\ Must be consistent with '_get_tax_grouping_key_from_tax_line'.
555        :param base_line:   An account.move.line being a base line (that could contains something in 'tax_ids').
556        :param tax_vals:    An element of compute_all(...)['taxes'].
557        :return:            A dictionary containing all fields on which the tax will be grouped.
558        '''
559        tax_repartition_line = self.env['account.tax.repartition.line'].browse(tax_vals['tax_repartition_line_id'])
560        account = base_line._get_default_tax_account(tax_repartition_line) or base_line.account_id
561        return {
562            'tax_repartition_line_id': tax_vals['tax_repartition_line_id'],
563            'account_id': account.id,
564            'currency_id': base_line.currency_id.id,
565            'analytic_tag_ids': [(6, 0, tax_vals['analytic'] and base_line.analytic_tag_ids.ids or [])],
566            'analytic_account_id': tax_vals['analytic'] and base_line.analytic_account_id.id,
567            'tax_ids': [(6, 0, tax_vals['tax_ids'])],
568            'tax_tag_ids': [(6, 0, tax_vals['tag_ids'])],
569        }
570
571    def _get_tax_force_sign(self):
572        """ The sign must be forced to a negative sign in case the balance is on credit
573            to avoid negatif taxes amount.
574            Example - Customer Invoice :
575            Fixed Tax  |  unit price  |   discount   |  amount_tax  | amount_total |
576            -------------------------------------------------------------------------
577                0.67   |      115      |     100%     |    - 0.67    |      0
578            -------------------------------------------------------------------------"""
579        self.ensure_one()
580        return -1 if self.move_type in ('out_invoice', 'in_refund', 'out_receipt') else 1
581
582    def _recompute_tax_lines(self, recompute_tax_base_amount=False):
583        ''' Compute the dynamic tax lines of the journal entry.
584
585        :param lines_map: The line_ids dispatched by type containing:
586            * base_lines: The lines having a tax_ids set.
587            * tax_lines: The lines having a tax_line_id set.
588            * terms_lines: The lines generated by the payment terms of the invoice.
589            * rounding_lines: The cash rounding lines of the invoice.
590        '''
591        self.ensure_one()
592        in_draft_mode = self != self._origin
593
594        def _serialize_tax_grouping_key(grouping_dict):
595            ''' Serialize the dictionary values to be used in the taxes_map.
596            :param grouping_dict: The values returned by '_get_tax_grouping_key_from_tax_line' or '_get_tax_grouping_key_from_base_line'.
597            :return: A string representing the values.
598            '''
599            return '-'.join(str(v) for v in grouping_dict.values())
600
601        def _compute_base_line_taxes(base_line):
602            ''' Compute taxes amounts both in company currency / foreign currency as the ratio between
603            amount_currency & balance could not be the same as the expected currency rate.
604            The 'amount_currency' value will be set on compute_all(...)['taxes'] in multi-currency.
605            :param base_line:   The account.move.line owning the taxes.
606            :return:            The result of the compute_all method.
607            '''
608            move = base_line.move_id
609
610            if move.is_invoice(include_receipts=True):
611                handle_price_include = True
612                sign = -1 if move.is_inbound() else 1
613                quantity = base_line.quantity
614                is_refund = move.move_type in ('out_refund', 'in_refund')
615                price_unit_wo_discount = sign * base_line.price_unit * (1 - (base_line.discount / 100.0))
616            else:
617                handle_price_include = False
618                quantity = 1.0
619                tax_type = base_line.tax_ids[0].type_tax_use if base_line.tax_ids else None
620                is_refund = (tax_type == 'sale' and base_line.debit) or (tax_type == 'purchase' and base_line.credit)
621                price_unit_wo_discount = base_line.amount_currency
622
623            balance_taxes_res = base_line.tax_ids._origin.with_context(force_sign=move._get_tax_force_sign()).compute_all(
624                price_unit_wo_discount,
625                currency=base_line.currency_id,
626                quantity=quantity,
627                product=base_line.product_id,
628                partner=base_line.partner_id,
629                is_refund=is_refund,
630                handle_price_include=handle_price_include,
631            )
632
633            if move.move_type == 'entry':
634                repartition_field = is_refund and 'refund_repartition_line_ids' or 'invoice_repartition_line_ids'
635                repartition_tags = base_line.tax_ids.flatten_taxes_hierarchy().mapped(repartition_field).filtered(lambda x: x.repartition_type == 'base').tag_ids
636                tags_need_inversion = (tax_type == 'sale' and not is_refund) or (tax_type == 'purchase' and is_refund)
637                if tags_need_inversion:
638                    balance_taxes_res['base_tags'] = base_line._revert_signed_tags(repartition_tags).ids
639                    for tax_res in balance_taxes_res['taxes']:
640                        tax_res['tag_ids'] = base_line._revert_signed_tags(self.env['account.account.tag'].browse(tax_res['tag_ids'])).ids
641
642            return balance_taxes_res
643
644        taxes_map = {}
645
646        # ==== Add tax lines ====
647        to_remove = self.env['account.move.line']
648        for line in self.line_ids.filtered('tax_repartition_line_id'):
649            grouping_dict = self._get_tax_grouping_key_from_tax_line(line)
650            grouping_key = _serialize_tax_grouping_key(grouping_dict)
651            if grouping_key in taxes_map:
652                # A line with the same key does already exist, we only need one
653                # to modify it; we have to drop this one.
654                to_remove += line
655            else:
656                taxes_map[grouping_key] = {
657                    'tax_line': line,
658                    'amount': 0.0,
659                    'tax_base_amount': 0.0,
660                    'grouping_dict': False,
661                }
662        if not recompute_tax_base_amount:
663            self.line_ids -= to_remove
664
665        # ==== Mount base lines ====
666        for line in self.line_ids.filtered(lambda line: not line.tax_repartition_line_id):
667            # Don't call compute_all if there is no tax.
668            if not line.tax_ids:
669                if not recompute_tax_base_amount:
670                    line.tax_tag_ids = [(5, 0, 0)]
671                continue
672
673            compute_all_vals = _compute_base_line_taxes(line)
674
675            # Assign tags on base line
676            if not recompute_tax_base_amount:
677                line.tax_tag_ids = compute_all_vals['base_tags'] or [(5, 0, 0)]
678
679            tax_exigible = True
680            for tax_vals in compute_all_vals['taxes']:
681                grouping_dict = self._get_tax_grouping_key_from_base_line(line, tax_vals)
682                grouping_key = _serialize_tax_grouping_key(grouping_dict)
683
684                tax_repartition_line = self.env['account.tax.repartition.line'].browse(tax_vals['tax_repartition_line_id'])
685                tax = tax_repartition_line.invoice_tax_id or tax_repartition_line.refund_tax_id
686
687                if tax.tax_exigibility == 'on_payment':
688                    tax_exigible = False
689
690                taxes_map_entry = taxes_map.setdefault(grouping_key, {
691                    'tax_line': None,
692                    'amount': 0.0,
693                    'tax_base_amount': 0.0,
694                    'grouping_dict': False,
695                })
696                taxes_map_entry['amount'] += tax_vals['amount']
697                taxes_map_entry['tax_base_amount'] += self._get_base_amount_to_display(tax_vals['base'], tax_repartition_line, tax_vals['group'])
698                taxes_map_entry['grouping_dict'] = grouping_dict
699            if not recompute_tax_base_amount:
700                line.tax_exigible = tax_exigible
701
702        # ==== Process taxes_map ====
703        for taxes_map_entry in taxes_map.values():
704            # The tax line is no longer used in any base lines, drop it.
705            if taxes_map_entry['tax_line'] and not taxes_map_entry['grouping_dict']:
706                if not recompute_tax_base_amount:
707                    self.line_ids -= taxes_map_entry['tax_line']
708                continue
709
710            currency = self.env['res.currency'].browse(taxes_map_entry['grouping_dict']['currency_id'])
711
712            # Don't create tax lines with zero balance.
713            if currency.is_zero(taxes_map_entry['amount']):
714                if taxes_map_entry['tax_line'] and not recompute_tax_base_amount:
715                    self.line_ids -= taxes_map_entry['tax_line']
716                continue
717
718            # tax_base_amount field is expressed using the company currency.
719            tax_base_amount = currency._convert(taxes_map_entry['tax_base_amount'], self.company_currency_id, self.company_id, self.date or fields.Date.context_today(self))
720
721            # Recompute only the tax_base_amount.
722            if recompute_tax_base_amount:
723                if taxes_map_entry['tax_line']:
724                    taxes_map_entry['tax_line'].tax_base_amount = tax_base_amount
725                continue
726
727            balance = currency._convert(
728                taxes_map_entry['amount'],
729                self.company_currency_id,
730                self.company_id,
731                self.date or fields.Date.context_today(self),
732            )
733            to_write_on_line = {
734                'amount_currency': taxes_map_entry['amount'],
735                'currency_id': taxes_map_entry['grouping_dict']['currency_id'],
736                'debit': balance > 0.0 and balance or 0.0,
737                'credit': balance < 0.0 and -balance or 0.0,
738                'tax_base_amount': tax_base_amount,
739            }
740
741            if taxes_map_entry['tax_line']:
742                # Update an existing tax line.
743                taxes_map_entry['tax_line'].update(to_write_on_line)
744            else:
745                create_method = in_draft_mode and self.env['account.move.line'].new or self.env['account.move.line'].create
746                tax_repartition_line_id = taxes_map_entry['grouping_dict']['tax_repartition_line_id']
747                tax_repartition_line = self.env['account.tax.repartition.line'].browse(tax_repartition_line_id)
748                tax = tax_repartition_line.invoice_tax_id or tax_repartition_line.refund_tax_id
749                taxes_map_entry['tax_line'] = create_method({
750                    **to_write_on_line,
751                    'name': tax.name,
752                    'move_id': self.id,
753                    'partner_id': line.partner_id.id,
754                    'company_id': line.company_id.id,
755                    'company_currency_id': line.company_currency_id.id,
756                    'tax_base_amount': tax_base_amount,
757                    'exclude_from_invoice_tab': True,
758                    'tax_exigible': tax.tax_exigibility == 'on_invoice',
759                    **taxes_map_entry['grouping_dict'],
760                })
761
762            if in_draft_mode:
763                taxes_map_entry['tax_line'].update(taxes_map_entry['tax_line']._get_fields_onchange_balance(force_computation=True))
764
765    @api.model
766    def _get_base_amount_to_display(self, base_amount, tax_rep_ln, parent_tax_group=None):
767        """ The base amount returned for taxes by compute_all has is the balance
768        of the base line. For inbound operations, positive sign is on credit, so
769        we need to invert the sign of this amount before displaying it.
770        """
771        source_tax = parent_tax_group or tax_rep_ln.invoice_tax_id or tax_rep_ln.refund_tax_id
772        if (tax_rep_ln.invoice_tax_id and source_tax.type_tax_use == 'sale') \
773           or (tax_rep_ln.refund_tax_id and source_tax.type_tax_use == 'purchase'):
774            return -base_amount
775        return base_amount
776
777    def update_lines_tax_exigibility(self):
778        if all(account.user_type_id.type not in {'payable', 'receivable'} for account in self.mapped('line_ids.account_id')):
779            self.line_ids.write({'tax_exigible': True})
780        else:
781            tax_lines_caba = self.line_ids.filtered(lambda x: x.tax_line_id.tax_exigibility == 'on_payment')
782            base_lines_caba = self.line_ids.filtered(lambda x: any(tax.tax_exigibility == 'on_payment'
783                                                                   or (tax.amount_type == 'group'
784                                                                       and 'on_payment' in tax.mapped('children_tax_ids.tax_exigibility'))
785                                                               for tax in x.tax_ids))
786            caba_lines = tax_lines_caba + base_lines_caba
787            caba_lines.write({'tax_exigible': False})
788            (self.line_ids - caba_lines).write({'tax_exigible': True})
789
790    def _recompute_cash_rounding_lines(self):
791        ''' Handle the cash rounding feature on invoices.
792
793        In some countries, the smallest coins do not exist. For example, in Switzerland, there is no coin for 0.01 CHF.
794        For this reason, if invoices are paid in cash, you have to round their total amount to the smallest coin that
795        exists in the currency. For the CHF, the smallest coin is 0.05 CHF.
796
797        There are two strategies for the rounding:
798
799        1) Add a line on the invoice for the rounding: The cash rounding line is added as a new invoice line.
800        2) Add the rounding in the biggest tax amount: The cash rounding line is added as a new tax line on the tax
801        having the biggest balance.
802        '''
803        self.ensure_one()
804        in_draft_mode = self != self._origin
805
806        def _compute_cash_rounding(self, total_amount_currency):
807            ''' Compute the amount differences due to the cash rounding.
808            :param self:                    The current account.move record.
809            :param total_amount_currency:   The invoice's total in invoice's currency.
810            :return:                        The amount differences both in company's currency & invoice's currency.
811            '''
812            difference = self.invoice_cash_rounding_id.compute_difference(self.currency_id, total_amount_currency)
813            if self.currency_id == self.company_id.currency_id:
814                diff_amount_currency = diff_balance = difference
815            else:
816                diff_amount_currency = difference
817                diff_balance = self.currency_id._convert(diff_amount_currency, self.company_id.currency_id, self.company_id, self.date)
818            return diff_balance, diff_amount_currency
819
820        def _apply_cash_rounding(self, diff_balance, diff_amount_currency, cash_rounding_line):
821            ''' Apply the cash rounding.
822            :param self:                    The current account.move record.
823            :param diff_balance:            The computed balance to set on the new rounding line.
824            :param diff_amount_currency:    The computed amount in invoice's currency to set on the new rounding line.
825            :param cash_rounding_line:      The existing cash rounding line.
826            :return:                        The newly created rounding line.
827            '''
828            rounding_line_vals = {
829                'debit': diff_balance > 0.0 and diff_balance or 0.0,
830                'credit': diff_balance < 0.0 and -diff_balance or 0.0,
831                'quantity': 1.0,
832                'amount_currency': diff_amount_currency,
833                'partner_id': self.partner_id.id,
834                'move_id': self.id,
835                'currency_id': self.currency_id.id,
836                'company_id': self.company_id.id,
837                'company_currency_id': self.company_id.currency_id.id,
838                'is_rounding_line': True,
839                'sequence': 9999,
840            }
841
842            if self.invoice_cash_rounding_id.strategy == 'biggest_tax':
843                biggest_tax_line = None
844                for tax_line in self.line_ids.filtered('tax_repartition_line_id'):
845                    if not biggest_tax_line or tax_line.price_subtotal > biggest_tax_line.price_subtotal:
846                        biggest_tax_line = tax_line
847
848                # No tax found.
849                if not biggest_tax_line:
850                    return
851
852                rounding_line_vals.update({
853                    'name': _('%s (rounding)', biggest_tax_line.name),
854                    'account_id': biggest_tax_line.account_id.id,
855                    'tax_repartition_line_id': biggest_tax_line.tax_repartition_line_id.id,
856                    'tax_exigible': biggest_tax_line.tax_exigible,
857                    'exclude_from_invoice_tab': True,
858                })
859
860            elif self.invoice_cash_rounding_id.strategy == 'add_invoice_line':
861                if diff_balance > 0.0 and self.invoice_cash_rounding_id.loss_account_id:
862                    account_id = self.invoice_cash_rounding_id.loss_account_id.id
863                else:
864                    account_id = self.invoice_cash_rounding_id.profit_account_id.id
865                rounding_line_vals.update({
866                    'name': self.invoice_cash_rounding_id.name,
867                    'account_id': account_id,
868                })
869
870            # Create or update the cash rounding line.
871            if cash_rounding_line:
872                cash_rounding_line.update({
873                    'amount_currency': rounding_line_vals['amount_currency'],
874                    'debit': rounding_line_vals['debit'],
875                    'credit': rounding_line_vals['credit'],
876                    'account_id': rounding_line_vals['account_id'],
877                })
878            else:
879                create_method = in_draft_mode and self.env['account.move.line'].new or self.env['account.move.line'].create
880                cash_rounding_line = create_method(rounding_line_vals)
881
882            if in_draft_mode:
883                cash_rounding_line.update(cash_rounding_line._get_fields_onchange_balance(force_computation=True))
884
885        existing_cash_rounding_line = self.line_ids.filtered(lambda line: line.is_rounding_line)
886
887        # The cash rounding has been removed.
888        if not self.invoice_cash_rounding_id:
889            self.line_ids -= existing_cash_rounding_line
890            return
891
892        # The cash rounding strategy has changed.
893        if self.invoice_cash_rounding_id and existing_cash_rounding_line:
894            strategy = self.invoice_cash_rounding_id.strategy
895            old_strategy = 'biggest_tax' if existing_cash_rounding_line.tax_line_id else 'add_invoice_line'
896            if strategy != old_strategy:
897                self.line_ids -= existing_cash_rounding_line
898                existing_cash_rounding_line = self.env['account.move.line']
899
900        others_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type not in ('receivable', 'payable'))
901        others_lines -= existing_cash_rounding_line
902        total_amount_currency = sum(others_lines.mapped('amount_currency'))
903
904        diff_balance, diff_amount_currency = _compute_cash_rounding(self, total_amount_currency)
905
906        # The invoice is already rounded.
907        if self.currency_id.is_zero(diff_balance) and self.currency_id.is_zero(diff_amount_currency):
908            self.line_ids -= existing_cash_rounding_line
909            return
910
911        _apply_cash_rounding(self, diff_balance, diff_amount_currency, existing_cash_rounding_line)
912
913    def _recompute_payment_terms_lines(self):
914        ''' Compute the dynamic payment term lines of the journal entry.'''
915        self.ensure_one()
916        self = self.with_company(self.company_id)
917        in_draft_mode = self != self._origin
918        today = fields.Date.context_today(self)
919        self = self.with_company(self.journal_id.company_id)
920
921        def _get_payment_terms_computation_date(self):
922            ''' Get the date from invoice that will be used to compute the payment terms.
923            :param self:    The current account.move record.
924            :return:        A datetime.date object.
925            '''
926            if self.invoice_payment_term_id:
927                return self.invoice_date or today
928            else:
929                return self.invoice_date_due or self.invoice_date or today
930
931        def _get_payment_terms_account(self, payment_terms_lines):
932            ''' Get the account from invoice that will be set as receivable / payable account.
933            :param self:                    The current account.move record.
934            :param payment_terms_lines:     The current payment terms lines.
935            :return:                        An account.account record.
936            '''
937            if payment_terms_lines:
938                # Retrieve account from previous payment terms lines in order to allow the user to set a custom one.
939                return payment_terms_lines[0].account_id
940            elif self.partner_id:
941                # Retrieve account from partner.
942                if self.is_sale_document(include_receipts=True):
943                    return self.partner_id.property_account_receivable_id
944                else:
945                    return self.partner_id.property_account_payable_id
946            else:
947                # Search new account.
948                domain = [
949                    ('company_id', '=', self.company_id.id),
950                    ('internal_type', '=', 'receivable' if self.move_type in ('out_invoice', 'out_refund', 'out_receipt') else 'payable'),
951                ]
952                return self.env['account.account'].search(domain, limit=1)
953
954        def _compute_payment_terms(self, date, total_balance, total_amount_currency):
955            ''' Compute the payment terms.
956            :param self:                    The current account.move record.
957            :param date:                    The date computed by '_get_payment_terms_computation_date'.
958            :param total_balance:           The invoice's total in company's currency.
959            :param total_amount_currency:   The invoice's total in invoice's currency.
960            :return:                        A list <to_pay_company_currency, to_pay_invoice_currency, due_date>.
961            '''
962            if self.invoice_payment_term_id:
963                to_compute = self.invoice_payment_term_id.compute(total_balance, date_ref=date, currency=self.company_id.currency_id)
964                if self.currency_id == self.company_id.currency_id:
965                    # Single-currency.
966                    return [(b[0], b[1], b[1]) for b in to_compute]
967                else:
968                    # Multi-currencies.
969                    to_compute_currency = self.invoice_payment_term_id.compute(total_amount_currency, date_ref=date, currency=self.currency_id)
970                    return [(b[0], b[1], ac[1]) for b, ac in zip(to_compute, to_compute_currency)]
971            else:
972                return [(fields.Date.to_string(date), total_balance, total_amount_currency)]
973
974        def _compute_diff_payment_terms_lines(self, existing_terms_lines, account, to_compute):
975            ''' Process the result of the '_compute_payment_terms' method and creates/updates corresponding invoice lines.
976            :param self:                    The current account.move record.
977            :param existing_terms_lines:    The current payment terms lines.
978            :param account:                 The account.account record returned by '_get_payment_terms_account'.
979            :param to_compute:              The list returned by '_compute_payment_terms'.
980            '''
981            # As we try to update existing lines, sort them by due date.
982            existing_terms_lines = existing_terms_lines.sorted(lambda line: line.date_maturity or today)
983            existing_terms_lines_index = 0
984
985            # Recompute amls: update existing line or create new one for each payment term.
986            new_terms_lines = self.env['account.move.line']
987            for date_maturity, balance, amount_currency in to_compute:
988                currency = self.journal_id.company_id.currency_id
989                if currency and currency.is_zero(balance) and len(to_compute) > 1:
990                    continue
991
992                if existing_terms_lines_index < len(existing_terms_lines):
993                    # Update existing line.
994                    candidate = existing_terms_lines[existing_terms_lines_index]
995                    existing_terms_lines_index += 1
996                    candidate.update({
997                        'date_maturity': date_maturity,
998                        'amount_currency': -amount_currency,
999                        'debit': balance < 0.0 and -balance or 0.0,
1000                        'credit': balance > 0.0 and balance or 0.0,
1001                    })
1002                else:
1003                    # Create new line.
1004                    create_method = in_draft_mode and self.env['account.move.line'].new or self.env['account.move.line'].create
1005                    candidate = create_method({
1006                        'name': self.payment_reference or '',
1007                        'debit': balance < 0.0 and -balance or 0.0,
1008                        'credit': balance > 0.0 and balance or 0.0,
1009                        'quantity': 1.0,
1010                        'amount_currency': -amount_currency,
1011                        'date_maturity': date_maturity,
1012                        'move_id': self.id,
1013                        'currency_id': self.currency_id.id,
1014                        'account_id': account.id,
1015                        'partner_id': self.commercial_partner_id.id,
1016                        'exclude_from_invoice_tab': True,
1017                    })
1018                new_terms_lines += candidate
1019                if in_draft_mode:
1020                    candidate.update(candidate._get_fields_onchange_balance(force_computation=True))
1021            return new_terms_lines
1022
1023        existing_terms_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
1024        others_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type not in ('receivable', 'payable'))
1025        company_currency_id = (self.company_id or self.env.company).currency_id
1026        total_balance = sum(others_lines.mapped(lambda l: company_currency_id.round(l.balance)))
1027        total_amount_currency = sum(others_lines.mapped('amount_currency'))
1028
1029        if not others_lines:
1030            self.line_ids -= existing_terms_lines
1031            return
1032
1033        computation_date = _get_payment_terms_computation_date(self)
1034        account = _get_payment_terms_account(self, existing_terms_lines)
1035        to_compute = _compute_payment_terms(self, computation_date, total_balance, total_amount_currency)
1036        new_terms_lines = _compute_diff_payment_terms_lines(self, existing_terms_lines, account, to_compute)
1037
1038        # Remove old terms lines that are no longer needed.
1039        self.line_ids -= existing_terms_lines - new_terms_lines
1040
1041        if new_terms_lines:
1042            self.payment_reference = new_terms_lines[-1].name or ''
1043            self.invoice_date_due = new_terms_lines[-1].date_maturity
1044
1045    def _recompute_dynamic_lines(self, recompute_all_taxes=False, recompute_tax_base_amount=False):
1046        ''' Recompute all lines that depend of others.
1047
1048        For example, tax lines depends of base lines (lines having tax_ids set). This is also the case of cash rounding
1049        lines that depend of base lines or tax lines depending the cash rounding strategy. When a payment term is set,
1050        this method will auto-balance the move with payment term lines.
1051
1052        :param recompute_all_taxes: Force the computation of taxes. If set to False, the computation will be done
1053                                    or not depending of the field 'recompute_tax_line' in lines.
1054        '''
1055        for invoice in self:
1056            # Dispatch lines and pre-compute some aggregated values like taxes.
1057            for line in invoice.line_ids:
1058                if line.recompute_tax_line:
1059                    recompute_all_taxes = True
1060                    line.recompute_tax_line = False
1061
1062            # Compute taxes.
1063            if recompute_all_taxes:
1064                invoice._recompute_tax_lines()
1065            if recompute_tax_base_amount:
1066                invoice._recompute_tax_lines(recompute_tax_base_amount=True)
1067
1068            if invoice.is_invoice(include_receipts=True):
1069
1070                # Compute cash rounding.
1071                invoice._recompute_cash_rounding_lines()
1072
1073                # Compute payment terms.
1074                invoice._recompute_payment_terms_lines()
1075
1076                # Only synchronize one2many in onchange.
1077                if invoice != invoice._origin:
1078                    invoice.invoice_line_ids = invoice.line_ids.filtered(lambda line: not line.exclude_from_invoice_tab)
1079
1080    @api.depends('journal_id')
1081    def _compute_company_id(self):
1082        for move in self:
1083            move.company_id = move.journal_id.company_id or move.company_id or self.env.company
1084
1085    def _get_lines_onchange_currency(self):
1086        # Override needed for COGS
1087        return self.line_ids
1088
1089    def onchange(self, values, field_name, field_onchange):
1090        # OVERRIDE
1091        # As the dynamic lines in this model are quite complex, we need to ensure some computations are done exactly
1092        # at the beginning / at the end of the onchange mechanism. So, the onchange recursivity is disabled.
1093        return super(AccountMove, self.with_context(recursive_onchanges=False)).onchange(values, field_name, field_onchange)
1094
1095    # -------------------------------------------------------------------------
1096    # COMPUTE METHODS
1097    # -------------------------------------------------------------------------
1098
1099    @api.depends('company_id', 'invoice_filter_type_domain')
1100    def _compute_suitable_journal_ids(self):
1101        for m in self:
1102            journal_type = m.invoice_filter_type_domain or 'general'
1103            company_id = m.company_id.id or self.env.company.id
1104            domain = [('company_id', '=', company_id), ('type', '=', journal_type)]
1105            m.suitable_journal_ids = self.env['account.journal'].search(domain)
1106
1107    @api.depends('posted_before', 'state', 'journal_id', 'date')
1108    def _compute_name(self):
1109        def journal_key(move):
1110            return (move.journal_id, move.journal_id.refund_sequence and move.move_type)
1111
1112        def date_key(move):
1113            return (move.date.year, move.date.month)
1114
1115        grouped = defaultdict(  # key: journal_id, move_type
1116            lambda: defaultdict(  # key: first adjacent (date.year, date.month)
1117                lambda: {
1118                    'records': self.env['account.move'],
1119                    'format': False,
1120                    'format_values': False,
1121                    'reset': False
1122                }
1123            )
1124        )
1125        self = self.sorted(lambda m: (m.date, m.ref or '', m.id))
1126        highest_name = self[0]._get_last_sequence() if self else False
1127
1128        # Group the moves by journal and month
1129        for move in self:
1130            if not highest_name and move == self[0] and not move.posted_before and move.date:
1131                # In the form view, we need to compute a default sequence so that the user can edit
1132                # it. We only check the first move as an approximation (enough for new in form view)
1133                pass
1134            elif (move.name and move.name != '/') or move.state != 'posted':
1135                try:
1136                    if not move.posted_before:
1137                        move._constrains_date_sequence()
1138                    # Has already a name or is not posted, we don't add to a batch
1139                    continue
1140                except ValidationError:
1141                    # Has never been posted and the name doesn't match the date: recompute it
1142                    pass
1143            group = grouped[journal_key(move)][date_key(move)]
1144            if not group['records']:
1145                # Compute all the values needed to sequence this whole group
1146                move._set_next_sequence()
1147                group['format'], group['format_values'] = move._get_sequence_format_param(move.name)
1148                group['reset'] = move._deduce_sequence_number_reset(move.name)
1149            group['records'] += move
1150
1151        # Fusion the groups depending on the sequence reset and the format used because `seq` is
1152        # the same counter for multiple groups that might be spread in multiple months.
1153        final_batches = []
1154        for journal_group in grouped.values():
1155            journal_group_changed = True
1156            for date_group in journal_group.values():
1157                if (
1158                    journal_group_changed
1159                    or final_batches[-1]['format'] != date_group['format']
1160                    or dict(final_batches[-1]['format_values'], seq=0) != dict(date_group['format_values'], seq=0)
1161                ):
1162                    final_batches += [date_group]
1163                    journal_group_changed = False
1164                elif date_group['reset'] == 'never':
1165                    final_batches[-1]['records'] += date_group['records']
1166                elif (
1167                    date_group['reset'] == 'year'
1168                    and final_batches[-1]['records'][0].date.year == date_group['records'][0].date.year
1169                ):
1170                    final_batches[-1]['records'] += date_group['records']
1171                else:
1172                    final_batches += [date_group]
1173
1174        # Give the name based on previously computed values
1175        for batch in final_batches:
1176            for move in batch['records']:
1177                move.name = batch['format'].format(**batch['format_values'])
1178                batch['format_values']['seq'] += 1
1179            batch['records']._compute_split_sequence()
1180
1181        self.filtered(lambda m: not m.name).name = '/'
1182
1183    @api.depends('journal_id', 'date')
1184    def _compute_highest_name(self):
1185        for record in self:
1186            record.highest_name = record._get_last_sequence()
1187
1188    @api.onchange('name', 'highest_name')
1189    def _onchange_name_warning(self):
1190        if self.name and self.name != '/' and self.name <= (self.highest_name or ''):
1191            self.show_name_warning = True
1192        else:
1193            self.show_name_warning = False
1194
1195        origin_name = self._origin.name
1196        if not origin_name or origin_name == '/':
1197            origin_name = self.highest_name
1198        if self.name and self.name != '/' and origin_name and origin_name != '/':
1199            format, format_values = self._get_sequence_format_param(self.name)
1200            origin_format, origin_format_values = self._get_sequence_format_param(origin_name)
1201
1202            if (
1203                format != origin_format
1204                or dict(format_values, seq=0) != dict(origin_format_values, seq=0)
1205            ):
1206                changed = _(
1207                    "It was previously '%(previous)s' and it is now '%(current)s'.",
1208                    previous=origin_name,
1209                    current=self.name,
1210                )
1211                reset = self._deduce_sequence_number_reset(self.name)
1212                if reset == 'month':
1213                    detected = _(
1214                        "The sequence will restart at 1 at the start of every month.\n"
1215                        "The year detected here is '%(year)s' and the month is '%(month)s'.\n"
1216                        "The incrementing number in this case is '%(formatted_seq)s'."
1217                    )
1218                elif reset == 'year':
1219                    detected = _(
1220                        "The sequence will restart at 1 at the start of every year.\n"
1221                        "The year detected here is '%(year)s'.\n"
1222                        "The incrementing number in this case is '%(formatted_seq)s'."
1223                    )
1224                else:
1225                    detected = _(
1226                        "The sequence will never restart.\n"
1227                        "The incrementing number in this case is '%(formatted_seq)s'."
1228                    )
1229                format_values['formatted_seq'] = "{seq:0{seq_length}d}".format(**format_values)
1230                detected = detected % format_values
1231                return {'warning': {
1232                    'title': _("The sequence format has changed."),
1233                    'message': "%s\n\n%s" % (changed, detected)
1234                }}
1235
1236    def _get_last_sequence_domain(self, relaxed=False):
1237        self.ensure_one()
1238        if not self.date or not self.journal_id:
1239            return "WHERE FALSE", {}
1240        where_string = "WHERE journal_id = %(journal_id)s AND name != '/'"
1241        param = {'journal_id': self.journal_id.id}
1242
1243        if not relaxed:
1244            domain = [('journal_id', '=', self.journal_id.id), ('id', '!=', self.id or self._origin.id), ('name', 'not in', ('/', False))]
1245            if self.journal_id.refund_sequence:
1246                refund_types = ('out_refund', 'in_refund')
1247                domain += [('move_type', 'in' if self.move_type in refund_types else 'not in', refund_types)]
1248            reference_move_name = self.search(domain + [('date', '<=', self.date)], order='date desc', limit=1).name
1249            if not reference_move_name:
1250                reference_move_name = self.search(domain, order='date asc', limit=1).name
1251            sequence_number_reset = self._deduce_sequence_number_reset(reference_move_name)
1252            if sequence_number_reset == 'year':
1253                where_string += " AND date_trunc('year', date::timestamp without time zone) = date_trunc('year', %(date)s) "
1254                param['date'] = self.date
1255                param['anti_regex'] = re.sub(r"\?P<\w+>", "?:", self._sequence_monthly_regex.split('(?P<seq>')[0]) + '$'
1256            elif sequence_number_reset == 'month':
1257                where_string += " AND date_trunc('month', date::timestamp without time zone) = date_trunc('month', %(date)s) "
1258                param['date'] = self.date
1259            else:
1260                param['anti_regex'] = re.sub(r"\?P<\w+>", "?:", self._sequence_yearly_regex.split('(?P<seq>')[0]) + '$'
1261
1262            if param.get('anti_regex') and not self.journal_id.sequence_override_regex:
1263                where_string += " AND sequence_prefix !~ %(anti_regex)s "
1264
1265        if self.journal_id.refund_sequence:
1266            if self.move_type in ('out_refund', 'in_refund'):
1267                where_string += " AND move_type IN ('out_refund', 'in_refund') "
1268            else:
1269                where_string += " AND move_type NOT IN ('out_refund', 'in_refund') "
1270
1271        return where_string, param
1272
1273    def _get_starting_sequence(self):
1274        self.ensure_one()
1275        starting_sequence = "%s/%04d/%02d/0000" % (self.journal_id.code, self.date.year, self.date.month)
1276        if self.journal_id.refund_sequence and self.move_type in ('out_refund', 'in_refund'):
1277            starting_sequence = "R" + starting_sequence
1278        return starting_sequence
1279
1280    @api.depends('move_type')
1281    def _compute_type_name(self):
1282        type_name_mapping = {k: v for k, v in
1283                             self._fields['move_type']._description_selection(self.env)}
1284        replacements = {'out_invoice': _('Invoice'), 'out_refund': _('Credit Note')}
1285
1286        for record in self:
1287            name = type_name_mapping[record.move_type]
1288            record.type_name = replacements.get(record.move_type, name)
1289
1290    @api.depends('move_type')
1291    def _compute_invoice_filter_type_domain(self):
1292        for move in self:
1293            if move.is_sale_document(include_receipts=True):
1294                move.invoice_filter_type_domain = 'sale'
1295            elif move.is_purchase_document(include_receipts=True):
1296                move.invoice_filter_type_domain = 'purchase'
1297            else:
1298                move.invoice_filter_type_domain = False
1299
1300    @api.depends('partner_id')
1301    def _compute_commercial_partner_id(self):
1302        for move in self:
1303            move.commercial_partner_id = move.partner_id.commercial_partner_id
1304
1305    @api.depends('commercial_partner_id')
1306    def _compute_bank_partner_id(self):
1307        for move in self:
1308            if move.is_outbound():
1309                move.bank_partner_id = move.commercial_partner_id
1310            else:
1311                move.bank_partner_id = move.company_id.partner_id
1312
1313    @api.model
1314    def _get_invoice_in_payment_state(self):
1315        ''' Hook to give the state when the invoice becomes fully paid. This is necessary because the users working
1316        with only invoicing don't want to see the 'in_payment' state. Then, this method will be overridden in the
1317        accountant module to enable the 'in_payment' state. '''
1318        return 'paid'
1319
1320    @api.depends(
1321        'line_ids.matched_debit_ids.debit_move_id.move_id.payment_id.is_matched',
1322        'line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual',
1323        'line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual_currency',
1324        'line_ids.matched_credit_ids.credit_move_id.move_id.payment_id.is_matched',
1325        'line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual',
1326        'line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual_currency',
1327        'line_ids.debit',
1328        'line_ids.credit',
1329        'line_ids.currency_id',
1330        'line_ids.amount_currency',
1331        'line_ids.amount_residual',
1332        'line_ids.amount_residual_currency',
1333        'line_ids.payment_id.state',
1334        'line_ids.full_reconcile_id')
1335    def _compute_amount(self):
1336        for move in self:
1337
1338            if move.payment_state == 'invoicing_legacy':
1339                # invoicing_legacy state is set via SQL when setting setting field
1340                # invoicing_switch_threshold (defined in account_accountant).
1341                # The only way of going out of this state is through this setting,
1342                # so we don't recompute it here.
1343                move.payment_state = move.payment_state
1344                continue
1345
1346            total_untaxed = 0.0
1347            total_untaxed_currency = 0.0
1348            total_tax = 0.0
1349            total_tax_currency = 0.0
1350            total_to_pay = 0.0
1351            total_residual = 0.0
1352            total_residual_currency = 0.0
1353            total = 0.0
1354            total_currency = 0.0
1355            currencies = move._get_lines_onchange_currency().currency_id
1356
1357            for line in move.line_ids:
1358                if move.is_invoice(include_receipts=True):
1359                    # === Invoices ===
1360
1361                    if not line.exclude_from_invoice_tab:
1362                        # Untaxed amount.
1363                        total_untaxed += line.balance
1364                        total_untaxed_currency += line.amount_currency
1365                        total += line.balance
1366                        total_currency += line.amount_currency
1367                    elif line.tax_line_id:
1368                        # Tax amount.
1369                        total_tax += line.balance
1370                        total_tax_currency += line.amount_currency
1371                        total += line.balance
1372                        total_currency += line.amount_currency
1373                    elif line.account_id.user_type_id.type in ('receivable', 'payable'):
1374                        # Residual amount.
1375                        total_to_pay += line.balance
1376                        total_residual += line.amount_residual
1377                        total_residual_currency += line.amount_residual_currency
1378                else:
1379                    # === Miscellaneous journal entry ===
1380                    if line.debit:
1381                        total += line.balance
1382                        total_currency += line.amount_currency
1383
1384            if move.move_type == 'entry' or move.is_outbound():
1385                sign = 1
1386            else:
1387                sign = -1
1388            move.amount_untaxed = sign * (total_untaxed_currency if len(currencies) == 1 else total_untaxed)
1389            move.amount_tax = sign * (total_tax_currency if len(currencies) == 1 else total_tax)
1390            move.amount_total = sign * (total_currency if len(currencies) == 1 else total)
1391            move.amount_residual = -sign * (total_residual_currency if len(currencies) == 1 else total_residual)
1392            move.amount_untaxed_signed = -total_untaxed
1393            move.amount_tax_signed = -total_tax
1394            move.amount_total_signed = abs(total) if move.move_type == 'entry' else -total
1395            move.amount_residual_signed = total_residual
1396
1397            currency = len(currencies) == 1 and currencies or move.company_id.currency_id
1398
1399            # Compute 'payment_state'.
1400            new_pmt_state = 'not_paid' if move.move_type != 'entry' else False
1401
1402            if move.is_invoice(include_receipts=True) and move.state == 'posted':
1403
1404                if currency.is_zero(move.amount_residual):
1405                    reconciled_payments = move._get_reconciled_payments()
1406                    if not reconciled_payments or all(payment.is_matched for payment in reconciled_payments):
1407                        new_pmt_state = 'paid'
1408                    else:
1409                        new_pmt_state = move._get_invoice_in_payment_state()
1410                elif currency.compare_amounts(total_to_pay, total_residual) != 0:
1411                    new_pmt_state = 'partial'
1412
1413            if new_pmt_state == 'paid' and move.move_type in ('in_invoice', 'out_invoice', 'entry'):
1414                reverse_type = move.move_type == 'in_invoice' and 'in_refund' or move.move_type == 'out_invoice' and 'out_refund' or 'entry'
1415                reverse_moves = self.env['account.move'].search([('reversed_entry_id', '=', move.id), ('state', '=', 'posted'), ('move_type', '=', reverse_type)])
1416
1417                # We only set 'reversed' state in cas of 1 to 1 full reconciliation with a reverse entry; otherwise, we use the regular 'paid' state
1418                reverse_moves_full_recs = reverse_moves.mapped('line_ids.full_reconcile_id')
1419                if reverse_moves_full_recs.mapped('reconciled_line_ids.move_id').filtered(lambda x: x not in (reverse_moves + reverse_moves_full_recs.mapped('exchange_move_id'))) == move:
1420                    new_pmt_state = 'reversed'
1421
1422            move.payment_state = new_pmt_state
1423
1424    def _inverse_amount_total(self):
1425        for move in self:
1426            if len(move.line_ids) != 2 or move.is_invoice(include_receipts=True):
1427                continue
1428
1429            to_write = []
1430
1431            amount_currency = abs(move.amount_total)
1432            balance = move.currency_id._convert(amount_currency, move.company_currency_id, move.company_id, move.date)
1433
1434            for line in move.line_ids:
1435                if not line.currency_id.is_zero(balance - abs(line.balance)):
1436                    to_write.append((1, line.id, {
1437                        'debit': line.balance > 0.0 and balance or 0.0,
1438                        'credit': line.balance < 0.0 and balance or 0.0,
1439                        'amount_currency': line.balance > 0.0 and amount_currency or -amount_currency,
1440                    }))
1441
1442            move.write({'line_ids': to_write})
1443
1444    def _get_domain_matching_suspense_moves(self):
1445        self.ensure_one()
1446        domain = self.env['account.move.line']._get_suspense_moves_domain()
1447        domain += ['|', ('partner_id', '=?', self.partner_id.id), ('partner_id', '=', False)]
1448        if self.is_inbound():
1449            domain.append(('balance', '=', -self.amount_residual))
1450        else:
1451            domain.append(('balance', '=', self.amount_residual))
1452        return domain
1453
1454    def _compute_has_matching_suspense_amount(self):
1455        for r in self:
1456            res = False
1457            if r.state == 'posted' and r.is_invoice() and r.payment_state == 'not_paid':
1458                domain = r._get_domain_matching_suspense_moves()
1459                #there are more than one but less than 5 suspense moves matching the residual amount
1460                if (0 < self.env['account.move.line'].search_count(domain) < 5):
1461                    domain2 = [
1462                        ('payment_state', '=', 'not_paid'),
1463                        ('state', '=', 'posted'),
1464                        ('amount_residual', '=', r.amount_residual),
1465                        ('move_type', '=', r.move_type)]
1466                    #there are less than 5 other open invoices of the same type with the same residual
1467                    if self.env['account.move'].search_count(domain2) < 5:
1468                        res = True
1469            r.invoice_has_matching_suspense_amount = res
1470
1471    @api.depends('partner_id', 'invoice_source_email', 'partner_id.name')
1472    def _compute_invoice_partner_display_info(self):
1473        for move in self:
1474            vendor_display_name = move.partner_id.display_name
1475            if not vendor_display_name:
1476                if move.invoice_source_email:
1477                    vendor_display_name = _('@From: %(email)s', email=move.invoice_source_email)
1478                else:
1479                    vendor_display_name = _('#Created by: %s', move.sudo().create_uid.name or self.env.user.name)
1480            move.invoice_partner_display_name = vendor_display_name
1481
1482    def _compute_payments_widget_to_reconcile_info(self):
1483        for move in self:
1484            move.invoice_outstanding_credits_debits_widget = json.dumps(False)
1485            move.invoice_has_outstanding = False
1486
1487            if move.state != 'posted' \
1488                    or move.payment_state not in ('not_paid', 'partial') \
1489                    or not move.is_invoice(include_receipts=True):
1490                continue
1491
1492            pay_term_lines = move.line_ids\
1493                .filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
1494
1495            domain = [
1496                ('account_id', 'in', pay_term_lines.account_id.ids),
1497                ('move_id.state', '=', 'posted'),
1498                ('partner_id', '=', move.commercial_partner_id.id),
1499                ('reconciled', '=', False),
1500                '|', ('amount_residual', '!=', 0.0), ('amount_residual_currency', '!=', 0.0),
1501            ]
1502
1503            payments_widget_vals = {'outstanding': True, 'content': [], 'move_id': move.id}
1504
1505            if move.is_inbound():
1506                domain.append(('balance', '<', 0.0))
1507                payments_widget_vals['title'] = _('Outstanding credits')
1508            else:
1509                domain.append(('balance', '>', 0.0))
1510                payments_widget_vals['title'] = _('Outstanding debits')
1511
1512            for line in self.env['account.move.line'].search(domain):
1513
1514                if line.currency_id == move.currency_id:
1515                    # Same foreign currency.
1516                    amount = abs(line.amount_residual_currency)
1517                else:
1518                    # Different foreign currencies.
1519                    amount = move.company_currency_id._convert(
1520                        abs(line.amount_residual),
1521                        move.currency_id,
1522                        move.company_id,
1523                        line.date,
1524                    )
1525
1526                if move.currency_id.is_zero(amount):
1527                    continue
1528
1529                payments_widget_vals['content'].append({
1530                    'journal_name': line.ref or line.move_id.name,
1531                    'amount': amount,
1532                    'currency': move.currency_id.symbol,
1533                    'id': line.id,
1534                    'move_id': line.move_id.id,
1535                    'position': move.currency_id.position,
1536                    'digits': [69, move.currency_id.decimal_places],
1537                    'payment_date': fields.Date.to_string(line.date),
1538                })
1539
1540            if not payments_widget_vals['content']:
1541                continue
1542
1543            move.invoice_outstanding_credits_debits_widget = json.dumps(payments_widget_vals)
1544            move.invoice_has_outstanding = True
1545
1546    def _get_reconciled_info_JSON_values(self):
1547        self.ensure_one()
1548
1549        reconciled_vals = []
1550        for partial, amount, counterpart_line in self._get_reconciled_invoices_partials():
1551            if counterpart_line.move_id.ref:
1552                reconciliation_ref = '%s (%s)' % (counterpart_line.move_id.name, counterpart_line.move_id.ref)
1553            else:
1554                reconciliation_ref = counterpart_line.move_id.name
1555
1556            reconciled_vals.append({
1557                'name': counterpart_line.name,
1558                'journal_name': counterpart_line.journal_id.name,
1559                'amount': amount,
1560                'currency': self.currency_id.symbol,
1561                'digits': [69, self.currency_id.decimal_places],
1562                'position': self.currency_id.position,
1563                'date': counterpart_line.date,
1564                'payment_id': counterpart_line.id,
1565                'partial_id': partial.id,
1566                'account_payment_id': counterpart_line.payment_id.id,
1567                'payment_method_name': counterpart_line.payment_id.payment_method_id.name if counterpart_line.journal_id.type == 'bank' else None,
1568                'move_id': counterpart_line.move_id.id,
1569                'ref': reconciliation_ref,
1570            })
1571        return reconciled_vals
1572
1573    @api.depends('move_type', 'line_ids.amount_residual')
1574    def _compute_payments_widget_reconciled_info(self):
1575        for move in self:
1576            payments_widget_vals = {'title': _('Less Payment'), 'outstanding': False, 'content': []}
1577
1578            if move.state == 'posted' and move.is_invoice(include_receipts=True):
1579                payments_widget_vals['content'] = move._get_reconciled_info_JSON_values()
1580
1581            if payments_widget_vals['content']:
1582                move.invoice_payments_widget = json.dumps(payments_widget_vals, default=date_utils.json_default)
1583            else:
1584                move.invoice_payments_widget = json.dumps(False)
1585
1586    @api.depends('line_ids.price_subtotal', 'line_ids.tax_base_amount', 'line_ids.tax_line_id', 'partner_id', 'currency_id')
1587    def _compute_invoice_taxes_by_group(self):
1588        for move in self:
1589
1590            # Not working on something else than invoices.
1591            if not move.is_invoice(include_receipts=True):
1592                move.amount_by_group = []
1593                continue
1594
1595            lang_env = move.with_context(lang=move.partner_id.lang).env
1596            balance_multiplicator = -1 if move.is_inbound() else 1
1597
1598            tax_lines = move.line_ids.filtered('tax_line_id')
1599            base_lines = move.line_ids.filtered('tax_ids')
1600
1601            tax_group_mapping = defaultdict(lambda: {
1602                'base_lines': set(),
1603                'base_amount': 0.0,
1604                'tax_amount': 0.0,
1605            })
1606
1607            # Compute base amounts.
1608            for base_line in base_lines:
1609                base_amount = balance_multiplicator * (base_line.amount_currency if base_line.currency_id else base_line.balance)
1610
1611                for tax in base_line.tax_ids.flatten_taxes_hierarchy():
1612
1613                    if base_line.tax_line_id.tax_group_id == tax.tax_group_id:
1614                        continue
1615
1616                    tax_group_vals = tax_group_mapping[tax.tax_group_id]
1617                    if base_line not in tax_group_vals['base_lines']:
1618                        tax_group_vals['base_amount'] += base_amount
1619                        tax_group_vals['base_lines'].add(base_line)
1620
1621            # Compute tax amounts.
1622            for tax_line in tax_lines:
1623                tax_amount = balance_multiplicator * (tax_line.amount_currency if tax_line.currency_id else tax_line.balance)
1624                tax_group_vals = tax_group_mapping[tax_line.tax_line_id.tax_group_id]
1625                tax_group_vals['tax_amount'] += tax_amount
1626
1627            tax_groups = sorted(tax_group_mapping.keys(), key=lambda x: x.sequence)
1628            amount_by_group = []
1629            for tax_group in tax_groups:
1630                tax_group_vals = tax_group_mapping[tax_group]
1631                amount_by_group.append((
1632                    tax_group.name,
1633                    tax_group_vals['tax_amount'],
1634                    tax_group_vals['base_amount'],
1635                    formatLang(lang_env, tax_group_vals['tax_amount'], currency_obj=move.currency_id),
1636                    formatLang(lang_env, tax_group_vals['base_amount'], currency_obj=move.currency_id),
1637                    len(tax_group_mapping),
1638                    tax_group.id
1639                ))
1640            move.amount_by_group = amount_by_group
1641
1642    @api.model
1643    def _get_tax_key_for_group_add_base(self, line):
1644        """
1645        Useful for _compute_invoice_taxes_by_group
1646        must be consistent with _get_tax_grouping_key_from_tax_line
1647         @return list
1648        """
1649        # DEPRECATED: TO BE REMOVED IN MASTER
1650        return [line.tax_line_id.id]
1651
1652    @api.depends('date', 'line_ids.debit', 'line_ids.credit', 'line_ids.tax_line_id', 'line_ids.tax_ids', 'line_ids.tax_tag_ids')
1653    def _compute_tax_lock_date_message(self):
1654        for move in self:
1655            if move._affect_tax_report() and move.company_id.tax_lock_date and move.date and move.date <= move.company_id.tax_lock_date:
1656                move.tax_lock_date_message = _(
1657                    "The accounting date is set prior to the tax lock date which is set on %s. "
1658                    "Hence, the accounting date will be changed to the next available date when posting.",
1659                    format_date(self.env, move.company_id.tax_lock_date))
1660            else:
1661                move.tax_lock_date_message = False
1662
1663    @api.depends('restrict_mode_hash_table', 'state')
1664    def _compute_show_reset_to_draft_button(self):
1665        for move in self:
1666            move.show_reset_to_draft_button = not move.restrict_mode_hash_table and move.state in ('posted', 'cancel')
1667
1668    # -------------------------------------------------------------------------
1669    # BUSINESS MODELS SYNCHRONIZATION
1670    # -------------------------------------------------------------------------
1671
1672    def _synchronize_business_models(self, changed_fields):
1673        ''' Ensure the consistency between:
1674        account.payment & account.move
1675        account.bank.statement.line & account.move
1676
1677        The idea is to call the method performing the synchronization of the business
1678        models regarding their related journal entries. To avoid cycling, the
1679        'skip_account_move_synchronization' key is used through the context.
1680
1681        :param changed_fields: A set containing all modified fields on account.move.
1682        '''
1683        if self._context.get('skip_account_move_synchronization'):
1684            return
1685
1686        self_sudo = self.sudo()
1687        self_sudo.payment_id._synchronize_from_moves(changed_fields)
1688        self_sudo.statement_line_id._synchronize_from_moves(changed_fields)
1689
1690    # -------------------------------------------------------------------------
1691    # CONSTRAINT METHODS
1692    # -------------------------------------------------------------------------
1693
1694    @api.constrains('name', 'journal_id', 'state')
1695    def _check_unique_sequence_number(self):
1696        moves = self.filtered(lambda move: move.state == 'posted')
1697        if not moves:
1698            return
1699
1700        self.flush(['name', 'journal_id', 'move_type', 'state'])
1701
1702        # /!\ Computed stored fields are not yet inside the database.
1703        self._cr.execute('''
1704            SELECT move2.id, move2.name
1705            FROM account_move move
1706            INNER JOIN account_move move2 ON
1707                move2.name = move.name
1708                AND move2.journal_id = move.journal_id
1709                AND move2.move_type = move.move_type
1710                AND move2.id != move.id
1711            WHERE move.id IN %s AND move2.state = 'posted'
1712        ''', [tuple(moves.ids)])
1713        res = self._cr.fetchall()
1714        if res:
1715            raise ValidationError(_('Posted journal entry must have an unique sequence number per company.\n'
1716                                    'Problematic numbers: %s\n') % ', '.join(r[1] for r in res))
1717
1718    @api.constrains('ref', 'move_type', 'partner_id', 'journal_id', 'invoice_date')
1719    def _check_duplicate_supplier_reference(self):
1720        moves = self.filtered(lambda move: move.is_purchase_document() and move.ref)
1721        if not moves:
1722            return
1723
1724        self.env["account.move"].flush([
1725            "ref", "move_type", "invoice_date", "journal_id",
1726            "company_id", "partner_id", "commercial_partner_id",
1727        ])
1728        self.env["account.journal"].flush(["company_id"])
1729        self.env["res.partner"].flush(["commercial_partner_id"])
1730
1731        # /!\ Computed stored fields are not yet inside the database.
1732        self._cr.execute('''
1733            SELECT move2.id
1734            FROM account_move move
1735            JOIN account_journal journal ON journal.id = move.journal_id
1736            JOIN res_partner partner ON partner.id = move.partner_id
1737            INNER JOIN account_move move2 ON
1738                move2.ref = move.ref
1739                AND move2.company_id = journal.company_id
1740                AND move2.commercial_partner_id = partner.commercial_partner_id
1741                AND move2.move_type = move.move_type
1742                AND (move.invoice_date is NULL OR move2.invoice_date = move.invoice_date)
1743                AND move2.id != move.id
1744            WHERE move.id IN %s
1745        ''', [tuple(moves.ids)])
1746        duplicated_moves = self.browse([r[0] for r in self._cr.fetchall()])
1747        if duplicated_moves:
1748            raise ValidationError(_('Duplicated vendor reference detected. You probably encoded twice the same vendor bill/credit note:\n%s') % "\n".join(
1749                duplicated_moves.mapped(lambda m: "%(partner)s - %(ref)s - %(date)s" % {
1750                    'ref': m.ref,
1751                    'partner': m.partner_id.display_name,
1752                    'date': format_date(self.env, m.invoice_date),
1753                })
1754            ))
1755
1756    def _check_balanced(self):
1757        ''' Assert the move is fully balanced debit = credit.
1758        An error is raised if it's not the case.
1759        '''
1760        moves = self.filtered(lambda move: move.line_ids)
1761        if not moves:
1762            return
1763
1764        # /!\ As this method is called in create / write, we can't make the assumption the computed stored fields
1765        # are already done. Then, this query MUST NOT depend of computed stored fields (e.g. balance).
1766        # It happens as the ORM makes the create with the 'no_recompute' statement.
1767        self.env['account.move.line'].flush(self.env['account.move.line']._fields)
1768        self.env['account.move'].flush(['journal_id'])
1769        self._cr.execute('''
1770            SELECT line.move_id, ROUND(SUM(line.debit - line.credit), currency.decimal_places)
1771            FROM account_move_line line
1772            JOIN account_move move ON move.id = line.move_id
1773            JOIN account_journal journal ON journal.id = move.journal_id
1774            JOIN res_company company ON company.id = journal.company_id
1775            JOIN res_currency currency ON currency.id = company.currency_id
1776            WHERE line.move_id IN %s
1777            GROUP BY line.move_id, currency.decimal_places
1778            HAVING ROUND(SUM(line.debit - line.credit), currency.decimal_places) != 0.0;
1779        ''', [tuple(self.ids)])
1780
1781        query_res = self._cr.fetchall()
1782        if query_res:
1783            ids = [res[0] for res in query_res]
1784            sums = [res[1] for res in query_res]
1785            raise UserError(_("Cannot create unbalanced journal entry. Ids: %s\nDifferences debit - credit: %s") % (ids, sums))
1786
1787    def _check_fiscalyear_lock_date(self):
1788        for move in self:
1789            lock_date = move.company_id._get_user_fiscal_lock_date()
1790            if move.date <= lock_date:
1791                if self.user_has_groups('account.group_account_manager'):
1792                    message = _("You cannot add/modify entries prior to and inclusive of the lock date %s.", format_date(self.env, lock_date))
1793                else:
1794                    message = _("You cannot add/modify entries prior to and inclusive of the lock date %s. Check the company settings or ask someone with the 'Adviser' role", format_date(self.env, lock_date))
1795                raise UserError(message)
1796        return True
1797
1798    @api.constrains('move_type', 'journal_id')
1799    def _check_journal_type(self):
1800        for record in self:
1801            journal_type = record.journal_id.type
1802
1803            if record.is_sale_document() and journal_type != 'sale' or record.is_purchase_document() and journal_type != 'purchase':
1804                raise ValidationError(_("The chosen journal has a type that is not compatible with your invoice type. Sales operations should go to 'sale' journals, and purchase operations to 'purchase' ones."))
1805
1806    # -------------------------------------------------------------------------
1807    # LOW-LEVEL METHODS
1808    # -------------------------------------------------------------------------
1809
1810    def _move_autocomplete_invoice_lines_values(self):
1811        ''' This method recomputes dynamic lines on the current journal entry that include taxes, cash rounding
1812        and payment terms lines.
1813        '''
1814        self.ensure_one()
1815
1816        for line in self.line_ids:
1817            # Do something only on invoice lines.
1818            if line.exclude_from_invoice_tab:
1819                continue
1820
1821            # Shortcut to load the demo data.
1822            # Doing line.account_id triggers a default_get(['account_id']) that could returns a result.
1823            # A section / note must not have an account_id set.
1824            if not line._cache.get('account_id') and not line.display_type and not line._origin:
1825                line.account_id = line._get_computed_account() or self.journal_id.default_account_id
1826            if line.product_id and not line._cache.get('name'):
1827                line.name = line._get_computed_name()
1828
1829            # Compute the account before the partner_id
1830            # In case account_followup is installed
1831            # Setting the partner will get the account_id in cache
1832            # If the account_id is not in cache, it will trigger the default value
1833            # Which is wrong in some case
1834            # It's better to set the account_id before the partner_id
1835            # Ensure related fields are well copied.
1836            if line.partner_id != self.partner_id.commercial_partner_id:
1837                line.partner_id = self.partner_id.commercial_partner_id
1838            line.date = self.date
1839            line.recompute_tax_line = True
1840            line.currency_id = self.currency_id
1841
1842
1843        self.line_ids._onchange_price_subtotal()
1844        self._recompute_dynamic_lines(recompute_all_taxes=True)
1845
1846        values = self._convert_to_write(self._cache)
1847        values.pop('invoice_line_ids', None)
1848        return values
1849
1850    @api.model
1851    def _move_autocomplete_invoice_lines_create(self, vals_list):
1852        ''' During the create of an account.move with only 'invoice_line_ids' set and not 'line_ids', this method is called
1853        to auto compute accounting lines of the invoice. In that case, accounts will be retrieved and taxes, cash rounding
1854        and payment terms will be computed. At the end, the values will contains all accounting lines in 'line_ids'
1855        and the moves should be balanced.
1856
1857        :param vals_list:   The list of values passed to the 'create' method.
1858        :return:            Modified list of values.
1859        '''
1860        new_vals_list = []
1861        for vals in vals_list:
1862            vals = dict(vals)
1863
1864            if vals.get('invoice_date') and not vals.get('date'):
1865                vals['date'] = vals['invoice_date']
1866
1867            default_move_type = vals.get('move_type') or self._context.get('default_move_type')
1868            ctx_vals = {}
1869            if default_move_type:
1870                ctx_vals['default_move_type'] = default_move_type
1871            if vals.get('journal_id'):
1872                ctx_vals['default_journal_id'] = vals['journal_id']
1873                # reorder the companies in the context so that the company of the journal
1874                # (which will be the company of the move) is the main one, ensuring all
1875                # property fields are read with the correct company
1876                journal_company = self.env['account.journal'].browse(vals['journal_id']).company_id
1877                allowed_companies = self._context.get('allowed_company_ids', journal_company.ids)
1878                reordered_companies = sorted(allowed_companies, key=lambda cid: cid != journal_company.id)
1879                ctx_vals['allowed_company_ids'] = reordered_companies
1880            self_ctx = self.with_context(**ctx_vals)
1881            vals = self_ctx._add_missing_default_values(vals)
1882
1883            is_invoice = vals.get('move_type') in self.get_invoice_types(include_receipts=True)
1884
1885            if 'line_ids' in vals:
1886                vals.pop('invoice_line_ids', None)
1887                new_vals_list.append(vals)
1888                continue
1889
1890            if is_invoice and 'invoice_line_ids' in vals:
1891                vals['line_ids'] = vals['invoice_line_ids']
1892
1893            vals.pop('invoice_line_ids', None)
1894
1895            move = self_ctx.new(vals)
1896            new_vals_list.append(move._move_autocomplete_invoice_lines_values())
1897
1898        return new_vals_list
1899
1900    def _move_autocomplete_invoice_lines_write(self, vals):
1901        ''' During the write of an account.move with only 'invoice_line_ids' set and not 'line_ids', this method is called
1902        to auto compute accounting lines of the invoice. In that case, accounts will be retrieved and taxes, cash rounding
1903        and payment terms will be computed. At the end, the values will contains all accounting lines in 'line_ids'
1904        and the moves should be balanced.
1905
1906        :param vals_list:   A python dict representing the values to write.
1907        :return:            True if the auto-completion did something, False otherwise.
1908        '''
1909        enable_autocomplete = 'invoice_line_ids' in vals and 'line_ids' not in vals and True or False
1910
1911        if not enable_autocomplete:
1912            return False
1913
1914        vals['line_ids'] = vals.pop('invoice_line_ids')
1915        for invoice in self:
1916            invoice_new = invoice.with_context(default_move_type=invoice.move_type, default_journal_id=invoice.journal_id.id).new(origin=invoice)
1917            invoice_new.update(vals)
1918            values = invoice_new._move_autocomplete_invoice_lines_values()
1919            values.pop('invoice_line_ids', None)
1920            invoice.write(values)
1921        return True
1922
1923    @api.returns('self', lambda value: value.id)
1924    def copy(self, default=None):
1925        default = dict(default or {})
1926        if (fields.Date.to_date(default.get('date')) or self.date) <= self.company_id._get_user_fiscal_lock_date():
1927            default['date'] = self.company_id._get_user_fiscal_lock_date() + timedelta(days=1)
1928        if self.move_type == 'entry':
1929            default['partner_id'] = False
1930        return super(AccountMove, self).copy(default)
1931
1932    @api.model_create_multi
1933    def create(self, vals_list):
1934        # OVERRIDE
1935        if any('state' in vals and vals.get('state') == 'posted' for vals in vals_list):
1936            raise UserError(_('You cannot create a move already in the posted state. Please create a draft move and post it after.'))
1937
1938        vals_list = self._move_autocomplete_invoice_lines_create(vals_list)
1939        rslt = super(AccountMove, self).create(vals_list)
1940        for i, vals in enumerate(vals_list):
1941            if 'line_ids' in vals:
1942                rslt[i].update_lines_tax_exigibility()
1943        return rslt
1944
1945    def write(self, vals):
1946        for move in self:
1947            if (move.restrict_mode_hash_table and move.state == "posted" and set(vals).intersection(INTEGRITY_HASH_MOVE_FIELDS)):
1948                raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_MOVE_FIELDS))
1949            if (move.restrict_mode_hash_table and move.inalterable_hash and 'inalterable_hash' in vals) or (move.secure_sequence_number and 'secure_sequence_number' in vals):
1950                raise UserError(_('You cannot overwrite the values ensuring the inalterability of the accounting.'))
1951            if (move.posted_before and 'journal_id' in vals and move.journal_id.id != vals['journal_id']):
1952                raise UserError(_('You cannot edit the journal of an account move if it has been posted once.'))
1953            if (move.name and move.name != '/' and 'journal_id' in vals and move.journal_id.id != vals['journal_id']):
1954                raise UserError(_('You cannot edit the journal of an account move if it already has a sequence number assigned.'))
1955
1956            # You can't change the date of a move being inside a locked period.
1957            if 'date' in vals and move.date != vals['date']:
1958                move._check_fiscalyear_lock_date()
1959                move.line_ids._check_tax_lock_date()
1960
1961            # You can't post subtract a move to a locked period.
1962            if 'state' in vals and move.state == 'posted' and vals['state'] != 'posted':
1963                move._check_fiscalyear_lock_date()
1964                move.line_ids._check_tax_lock_date()
1965
1966            if move.journal_id.sequence_override_regex and vals.get('name') and vals['name'] != '/' and not re.match(move.journal_id.sequence_override_regex, vals['name']):
1967                if not self.env.user.has_group('account.group_account_manager'):
1968                    raise UserError(_('The Journal Entry sequence is not conform to the current format. Only the Advisor can change it.'))
1969                move.journal_id.sequence_override_regex = False
1970
1971        if self._move_autocomplete_invoice_lines_write(vals):
1972            res = True
1973        else:
1974            vals.pop('invoice_line_ids', None)
1975            res = super(AccountMove, self.with_context(check_move_validity=False, skip_account_move_synchronization=True)).write(vals)
1976
1977        # You can't change the date of a not-locked move to a locked period.
1978        # You can't post a new journal entry inside a locked period.
1979        if 'date' in vals or 'state' in vals:
1980            self._check_fiscalyear_lock_date()
1981            self.mapped('line_ids')._check_tax_lock_date()
1982
1983        if ('state' in vals and vals.get('state') == 'posted'):
1984            for move in self.filtered(lambda m: m.restrict_mode_hash_table and not(m.secure_sequence_number or m.inalterable_hash)).sorted(lambda m: (m.date, m.ref or '', m.id)):
1985                new_number = move.journal_id.secure_sequence_id.next_by_id()
1986                vals_hashing = {'secure_sequence_number': new_number,
1987                                'inalterable_hash': move._get_new_hash(new_number)}
1988                res |= super(AccountMove, move).write(vals_hashing)
1989
1990        # Ensure the move is still well balanced.
1991        if 'line_ids' in vals:
1992            if self._context.get('check_move_validity', True):
1993                self._check_balanced()
1994            self.update_lines_tax_exigibility()
1995
1996        self._synchronize_business_models(set(vals.keys()))
1997
1998        return res
1999
2000    def unlink(self):
2001        for move in self:
2002            if move.posted_before and not self._context.get('force_delete'):
2003                raise UserError(_("You cannot delete an entry which has been posted once."))
2004        self.line_ids.unlink()
2005        return super(AccountMove, self).unlink()
2006
2007    @api.depends('name', 'state')
2008    def name_get(self):
2009        result = []
2010        for move in self:
2011            if self._context.get('name_groupby'):
2012                name = '**%s**, %s' % (format_date(self.env, move.date), move._get_move_display_name())
2013                if move.ref:
2014                    name += '     (%s)' % move.ref
2015                if move.partner_id.name:
2016                    name += ' - %s' % move.partner_id.name
2017            else:
2018                name = move._get_move_display_name(show_ref=True)
2019            result.append((move.id, name))
2020        return result
2021
2022    def _creation_subtype(self):
2023        # OVERRIDE
2024        if self.move_type in ('out_invoice', 'out_refund', 'out_receipt'):
2025            return self.env.ref('account.mt_invoice_created')
2026        else:
2027            return super(AccountMove, self)._creation_subtype()
2028
2029    def _track_subtype(self, init_values):
2030        # OVERRIDE to add custom subtype depending of the state.
2031        self.ensure_one()
2032
2033        if not self.is_invoice(include_receipts=True):
2034            return super(AccountMove, self)._track_subtype(init_values)
2035
2036        if 'payment_state' in init_values and self.payment_state == 'paid':
2037            return self.env.ref('account.mt_invoice_paid')
2038        elif 'state' in init_values and self.state == 'posted' and self.is_sale_document(include_receipts=True):
2039            return self.env.ref('account.mt_invoice_validated')
2040        return super(AccountMove, self)._track_subtype(init_values)
2041
2042    def _creation_message(self):
2043        # OVERRIDE
2044        if not self.is_invoice(include_receipts=True):
2045            return super()._creation_message()
2046        return {
2047            'out_invoice': _('Invoice Created'),
2048            'out_refund': _('Credit Note Created'),
2049            'in_invoice': _('Vendor Bill Created'),
2050            'in_refund': _('Refund Created'),
2051            'out_receipt': _('Sales Receipt Created'),
2052            'in_receipt': _('Purchase Receipt Created'),
2053        }[self.move_type]
2054
2055    # -------------------------------------------------------------------------
2056    # RECONCILIATION METHODS
2057    # -------------------------------------------------------------------------
2058
2059    def _collect_tax_cash_basis_values(self):
2060        ''' Collect all information needed to create the tax cash basis journal entries:
2061        - Determine if a tax cash basis journal entry is needed.
2062        - Compute the lines to be processed and the amounts needed to compute a percentage.
2063        :return: A dictionary:
2064            * move:                     The current account.move record passed as parameter.
2065            * to_process_lines:         An account.move.line recordset being not exigible on the tax report.
2066            * currency:                 The currency on which the percentage has been computed.
2067            * total_balance:            sum(payment_term_lines.mapped('balance').
2068            * total_residual:           sum(payment_term_lines.mapped('amount_residual').
2069            * total_amount_currency:    sum(payment_term_lines.mapped('amount_currency').
2070            * total_residual_currency:  sum(payment_term_lines.mapped('amount_residual_currency').
2071            * is_fully_paid:            A flag indicating the current move is now fully paid.
2072        '''
2073        self.ensure_one()
2074
2075        values = {
2076            'move': self,
2077            'to_process_lines': self.env['account.move.line'],
2078            'total_balance': 0.0,
2079            'total_residual': 0.0,
2080            'total_amount_currency': 0.0,
2081            'total_residual_currency': 0.0,
2082        }
2083
2084        currencies = set()
2085        has_term_lines = False
2086        for line in self.line_ids:
2087            if line.account_internal_type in ('receivable', 'payable'):
2088                sign = 1 if line.balance > 0.0 else -1
2089
2090                currencies.add(line.currency_id or line.company_currency_id)
2091                has_term_lines = True
2092                values['total_balance'] += sign * line.balance
2093                values['total_residual'] += sign * line.amount_residual
2094                values['total_amount_currency'] += sign * line.amount_currency
2095                values['total_residual_currency'] += sign * line.amount_residual_currency
2096
2097            elif not line.tax_exigible:
2098
2099                values['to_process_lines'] += line
2100                currencies.add(line.currency_id or line.company_currency_id)
2101
2102        if not values['to_process_lines'] or not has_term_lines:
2103            return None
2104
2105        # Compute the currency on which made the percentage.
2106        if len(currencies) == 1:
2107            values['currency'] = list(currencies)[0]
2108        else:
2109            # Don't support the case where there is multiple involved currencies.
2110            return None
2111
2112        # Determine is the move is now fully paid.
2113        values['is_fully_paid'] = self.company_id.currency_id.is_zero(values['total_residual']) \
2114                                  or values['currency'].is_zero(values['total_residual_currency'])
2115
2116        return values
2117
2118    # -------------------------------------------------------------------------
2119    # BUSINESS METHODS
2120    # -------------------------------------------------------------------------
2121
2122    @api.model
2123    def get_invoice_types(self, include_receipts=False):
2124        return ['out_invoice', 'out_refund', 'in_refund', 'in_invoice'] + (include_receipts and ['out_receipt', 'in_receipt'] or [])
2125
2126    def is_invoice(self, include_receipts=False):
2127        return self.move_type in self.get_invoice_types(include_receipts)
2128
2129    @api.model
2130    def get_sale_types(self, include_receipts=False):
2131        return ['out_invoice', 'out_refund'] + (include_receipts and ['out_receipt'] or [])
2132
2133    def is_sale_document(self, include_receipts=False):
2134        return self.move_type in self.get_sale_types(include_receipts)
2135
2136    @api.model
2137    def get_purchase_types(self, include_receipts=False):
2138        return ['in_invoice', 'in_refund'] + (include_receipts and ['in_receipt'] or [])
2139
2140    def is_purchase_document(self, include_receipts=False):
2141        return self.move_type in self.get_purchase_types(include_receipts)
2142
2143    @api.model
2144    def get_inbound_types(self, include_receipts=True):
2145        return ['out_invoice', 'in_refund'] + (include_receipts and ['out_receipt'] or [])
2146
2147    def is_inbound(self, include_receipts=True):
2148        return self.move_type in self.get_inbound_types(include_receipts)
2149
2150    @api.model
2151    def get_outbound_types(self, include_receipts=True):
2152        return ['in_invoice', 'out_refund'] + (include_receipts and ['in_receipt'] or [])
2153
2154    def is_outbound(self, include_receipts=True):
2155        return self.move_type in self.get_outbound_types(include_receipts)
2156
2157    def _affect_tax_report(self):
2158        return any(line._affect_tax_report() for line in self.line_ids)
2159
2160    def _get_invoice_reference_euro_invoice(self):
2161        """ This computes the reference based on the RF Creditor Reference.
2162            The data of the reference is the database id number of the invoice.
2163            For instance, if an invoice is issued with id 43, the check number
2164            is 07 so the reference will be 'RF07 43'.
2165        """
2166        self.ensure_one()
2167        base = self.id
2168        check_digits = calc_check_digits('{}RF'.format(base))
2169        reference = 'RF{} {}'.format(check_digits, " ".join(["".join(x) for x in zip_longest(*[iter(str(base))]*4, fillvalue="")]))
2170        return reference
2171
2172    def _get_invoice_reference_euro_partner(self):
2173        """ This computes the reference based on the RF Creditor Reference.
2174            The data of the reference is the user defined reference of the
2175            partner or the database id number of the parter.
2176            For instance, if an invoice is issued for the partner with internal
2177            reference 'food buyer 654', the digits will be extracted and used as
2178            the data. This will lead to a check number equal to 00 and the
2179            reference will be 'RF00 654'.
2180            If no reference is set for the partner, its id in the database will
2181            be used.
2182        """
2183        self.ensure_one()
2184        partner_ref = self.partner_id.ref
2185        partner_ref_nr = re.sub('\D', '', partner_ref or '')[-21:] or str(self.partner_id.id)[-21:]
2186        partner_ref_nr = partner_ref_nr[-21:]
2187        check_digits = calc_check_digits('{}RF'.format(partner_ref_nr))
2188        reference = 'RF{} {}'.format(check_digits, " ".join(["".join(x) for x in zip_longest(*[iter(partner_ref_nr)]*4, fillvalue="")]))
2189        return reference
2190
2191    def _get_invoice_reference_odoo_invoice(self):
2192        """ This computes the reference based on the Odoo format.
2193            We simply return the number of the invoice, defined on the journal
2194            sequence.
2195        """
2196        self.ensure_one()
2197        return self.name
2198
2199    def _get_invoice_reference_odoo_partner(self):
2200        """ This computes the reference based on the Odoo format.
2201            The data used is the reference set on the partner or its database
2202            id otherwise. For instance if the reference of the customer is
2203            'dumb customer 97', the reference will be 'CUST/dumb customer 97'.
2204        """
2205        ref = self.partner_id.ref or str(self.partner_id.id)
2206        prefix = _('CUST')
2207        return '%s/%s' % (prefix, ref)
2208
2209    def _get_invoice_computed_reference(self):
2210        self.ensure_one()
2211        if self.journal_id.invoice_reference_type == 'none':
2212            return ''
2213        else:
2214            ref_function = getattr(self, '_get_invoice_reference_{}_{}'.format(self.journal_id.invoice_reference_model, self.journal_id.invoice_reference_type))
2215            if ref_function:
2216                return ref_function()
2217            else:
2218                raise UserError(_('The combination of reference model and reference type on the journal is not implemented'))
2219
2220    def _get_move_display_name(self, show_ref=False):
2221        ''' Helper to get the display name of an invoice depending of its type.
2222        :param show_ref:    A flag indicating of the display name must include or not the journal entry reference.
2223        :return:            A string representing the invoice.
2224        '''
2225        self.ensure_one()
2226        draft_name = ''
2227        if self.state == 'draft':
2228            draft_name += {
2229                'out_invoice': _('Draft Invoice'),
2230                'out_refund': _('Draft Credit Note'),
2231                'in_invoice': _('Draft Bill'),
2232                'in_refund': _('Draft Vendor Credit Note'),
2233                'out_receipt': _('Draft Sales Receipt'),
2234                'in_receipt': _('Draft Purchase Receipt'),
2235                'entry': _('Draft Entry'),
2236            }[self.move_type]
2237            if not self.name or self.name == '/':
2238                draft_name += ' (* %s)' % str(self.id)
2239            else:
2240                draft_name += ' ' + self.name
2241        return (draft_name or self.name) + (show_ref and self.ref and ' (%s%s)' % (self.ref[:50], '...' if len(self.ref) > 50 else '') or '')
2242
2243    def _get_invoice_delivery_partner_id(self):
2244        ''' Hook allowing to retrieve the right delivery address depending of installed modules.
2245        :return: A res.partner record's id representing the delivery address.
2246        '''
2247        self.ensure_one()
2248        return self.partner_id.address_get(['delivery'])['delivery']
2249
2250    def _get_reconciled_payments(self):
2251        """Helper used to retrieve the reconciled payments on this journal entry"""
2252        reconciled_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
2253        reconciled_amls = reconciled_lines.mapped('matched_debit_ids.debit_move_id') + \
2254                          reconciled_lines.mapped('matched_credit_ids.credit_move_id')
2255        return reconciled_amls.move_id.payment_id
2256
2257    def _get_reconciled_statement_lines(self):
2258        """Helper used to retrieve the reconciled payments on this journal entry"""
2259        reconciled_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
2260        reconciled_amls = reconciled_lines.mapped('matched_debit_ids.debit_move_id') + \
2261                          reconciled_lines.mapped('matched_credit_ids.credit_move_id')
2262        return reconciled_amls.move_id.statement_line_id
2263
2264    def _get_reconciled_invoices(self):
2265        """Helper used to retrieve the reconciled payments on this journal entry"""
2266        reconciled_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
2267        reconciled_amls = reconciled_lines.mapped('matched_debit_ids.debit_move_id') + \
2268                          reconciled_lines.mapped('matched_credit_ids.credit_move_id')
2269        return reconciled_amls.move_id.filtered(lambda move: move.is_invoice(include_receipts=True))
2270
2271    def _get_reconciled_invoices_partials(self):
2272        ''' Helper to retrieve the details about reconciled invoices.
2273        :return A list of tuple (partial, amount, invoice_line).
2274        '''
2275        self.ensure_one()
2276        pay_term_lines = self.line_ids\
2277            .filtered(lambda line: line.account_internal_type in ('receivable', 'payable'))
2278        invoice_partials = []
2279
2280        for partial in pay_term_lines.matched_debit_ids:
2281            invoice_partials.append((partial, partial.credit_amount_currency, partial.debit_move_id))
2282        for partial in pay_term_lines.matched_credit_ids:
2283            invoice_partials.append((partial, partial.debit_amount_currency, partial.credit_move_id))
2284        return invoice_partials
2285
2286    def _reverse_move_vals(self, default_values, cancel=True):
2287        ''' Reverse values passed as parameter being the copied values of the original journal entry.
2288        For example, debit / credit must be switched. The tax lines must be edited in case of refunds.
2289
2290        :param default_values:  A copy_date of the original journal entry.
2291        :param cancel:          A flag indicating the reverse is made to cancel the original journal entry.
2292        :return:                The updated default_values.
2293        '''
2294        self.ensure_one()
2295
2296        def compute_tax_repartition_lines_mapping(move_vals):
2297            ''' Computes and returns a mapping between the current repartition lines to the new expected one.
2298            :param move_vals:   The newly created invoice as a python dictionary to be passed to the 'create' method.
2299            :return:            A map invoice_repartition_line => refund_repartition_line.
2300            '''
2301            # invoice_repartition_line => refund_repartition_line
2302            mapping = {}
2303
2304            # Do nothing if the move is not a credit note.
2305            if move_vals['move_type'] not in ('out_refund', 'in_refund'):
2306                return mapping
2307
2308            for line_command in move_vals.get('line_ids', []):
2309                line_vals = line_command[2]  # (0, 0, {...})
2310
2311                if line_vals.get('tax_line_id'):
2312                    # Tax line.
2313                    tax_ids = [line_vals['tax_line_id']]
2314                elif line_vals.get('tax_ids') and line_vals['tax_ids'][0][2]:
2315                    # Base line.
2316                    tax_ids = line_vals['tax_ids'][0][2]
2317                else:
2318                    continue
2319
2320                for tax in self.env['account.tax'].browse(tax_ids).flatten_taxes_hierarchy():
2321                    for inv_rep_line, ref_rep_line in zip(tax.invoice_repartition_line_ids, tax.refund_repartition_line_ids):
2322                        mapping[inv_rep_line] = ref_rep_line
2323            return mapping
2324
2325        move_vals = self.with_context(include_business_fields=True).copy_data(default=default_values)[0]
2326
2327        tax_repartition_lines_mapping = compute_tax_repartition_lines_mapping(move_vals)
2328
2329        for line_command in move_vals.get('line_ids', []):
2330            line_vals = line_command[2]  # (0, 0, {...})
2331
2332            # ==== Inverse debit / credit / amount_currency ====
2333            amount_currency = -line_vals.get('amount_currency', 0.0)
2334            balance = line_vals['credit'] - line_vals['debit']
2335
2336            line_vals.update({
2337                'amount_currency': amount_currency,
2338                'debit': balance > 0.0 and balance or 0.0,
2339                'credit': balance < 0.0 and -balance or 0.0,
2340            })
2341
2342            if move_vals['move_type'] not in ('out_refund', 'in_refund'):
2343                continue
2344
2345            # ==== Map tax repartition lines ====
2346            if line_vals.get('tax_repartition_line_id'):
2347                # Tax line.
2348                invoice_repartition_line = self.env['account.tax.repartition.line'].browse(line_vals['tax_repartition_line_id'])
2349                if invoice_repartition_line not in tax_repartition_lines_mapping:
2350                    raise UserError(_("It seems that the taxes have been modified since the creation of the journal entry. You should create the credit note manually instead."))
2351                refund_repartition_line = tax_repartition_lines_mapping[invoice_repartition_line]
2352
2353                # Find the right account.
2354                account_id = self.env['account.move.line']._get_default_tax_account(refund_repartition_line).id
2355                if not account_id:
2356                    if not invoice_repartition_line.account_id:
2357                        # Keep the current account as the current one comes from the base line.
2358                        account_id = line_vals['account_id']
2359                    else:
2360                        tax = invoice_repartition_line.invoice_tax_id
2361                        base_line = self.line_ids.filtered(lambda line: tax in line.tax_ids.flatten_taxes_hierarchy())[0]
2362                        account_id = base_line.account_id.id
2363
2364                tags = refund_repartition_line.tag_ids
2365                if line_vals.get('tax_ids'):
2366                    subsequent_taxes = self.env['account.tax'].browse(line_vals['tax_ids'][0][2])
2367                    tags += subsequent_taxes.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'base').tag_ids
2368
2369                line_vals.update({
2370                    'tax_repartition_line_id': refund_repartition_line.id,
2371                    'account_id': account_id,
2372                    'tax_tag_ids': [(6, 0, tags.ids)],
2373                })
2374            elif line_vals.get('tax_ids') and line_vals['tax_ids'][0][2]:
2375                # Base line.
2376                taxes = self.env['account.tax'].browse(line_vals['tax_ids'][0][2]).flatten_taxes_hierarchy()
2377                invoice_repartition_lines = taxes\
2378                    .mapped('invoice_repartition_line_ids')\
2379                    .filtered(lambda line: line.repartition_type == 'base')
2380                refund_repartition_lines = invoice_repartition_lines\
2381                    .mapped(lambda line: tax_repartition_lines_mapping[line])
2382
2383                line_vals['tax_tag_ids'] = [(6, 0, refund_repartition_lines.mapped('tag_ids').ids)]
2384        return move_vals
2385
2386    def _reverse_moves(self, default_values_list=None, cancel=False):
2387        ''' Reverse a recordset of account.move.
2388        If cancel parameter is true, the reconcilable or liquidity lines
2389        of each original move will be reconciled with its reverse's.
2390
2391        :param default_values_list: A list of default values to consider per move.
2392                                    ('type' & 'reversed_entry_id' are computed in the method).
2393        :return:                    An account.move recordset, reverse of the current self.
2394        '''
2395        if not default_values_list:
2396            default_values_list = [{} for move in self]
2397
2398        if cancel:
2399            lines = self.mapped('line_ids')
2400            # Avoid maximum recursion depth.
2401            if lines:
2402                lines.remove_move_reconcile()
2403
2404        reverse_type_map = {
2405            'entry': 'entry',
2406            'out_invoice': 'out_refund',
2407            'out_refund': 'entry',
2408            'in_invoice': 'in_refund',
2409            'in_refund': 'entry',
2410            'out_receipt': 'entry',
2411            'in_receipt': 'entry',
2412        }
2413
2414        move_vals_list = []
2415        for move, default_values in zip(self, default_values_list):
2416            default_values.update({
2417                'move_type': reverse_type_map[move.move_type],
2418                'reversed_entry_id': move.id,
2419            })
2420            move_vals_list.append(move.with_context(move_reverse_cancel=cancel)._reverse_move_vals(default_values, cancel=cancel))
2421
2422        reverse_moves = self.env['account.move'].create(move_vals_list)
2423        for move, reverse_move in zip(self, reverse_moves.with_context(check_move_validity=False)):
2424            # Update amount_currency if the date has changed.
2425            if move.date != reverse_move.date:
2426                for line in reverse_move.line_ids:
2427                    if line.currency_id:
2428                        line._onchange_currency()
2429            reverse_move._recompute_dynamic_lines(recompute_all_taxes=False)
2430        reverse_moves._check_balanced()
2431
2432        # Reconcile moves together to cancel the previous one.
2433        if cancel:
2434            reverse_moves.with_context(move_reverse_cancel=cancel)._post(soft=False)
2435            for move, reverse_move in zip(self, reverse_moves):
2436                lines = move.line_ids.filtered(
2437                    lambda x: (x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
2438                              and not x.reconciled
2439                )
2440                for line in lines:
2441                    counterpart_lines = reverse_move.line_ids.filtered(
2442                        lambda x: x.account_id == line.account_id
2443                                  and x.currency_id == line.currency_id
2444                                  and not x.reconciled
2445                    )
2446                    (line + counterpart_lines).with_context(move_reverse_cancel=cancel).reconcile()
2447
2448        return reverse_moves
2449
2450    def open_reconcile_view(self):
2451        return self.line_ids.open_reconcile_view()
2452
2453    @api.model
2454    def message_new(self, msg_dict, custom_values=None):
2455        # OVERRIDE
2456        # Add custom behavior when receiving a new invoice through the mail's gateway.
2457        if (custom_values or {}).get('move_type', 'entry') not in ('out_invoice', 'in_invoice'):
2458            return super().message_new(msg_dict, custom_values=custom_values)
2459
2460        def is_internal_partner(partner):
2461            # Helper to know if the partner is an internal one.
2462            return partner.user_ids and all(user.has_group('base.group_user') for user in partner.user_ids)
2463
2464        # Search for partners in copy.
2465        cc_mail_addresses = email_split(msg_dict.get('cc', ''))
2466        followers = [partner for partner in self._mail_find_partner_from_emails(cc_mail_addresses) if partner]
2467
2468        # Search for partner that sent the mail.
2469        from_mail_addresses = email_split(msg_dict.get('from', ''))
2470        senders = partners = [partner for partner in self._mail_find_partner_from_emails(from_mail_addresses) if partner]
2471
2472        # Search for partners using the user.
2473        if not senders:
2474            senders = partners = list(self._mail_search_on_user(from_mail_addresses))
2475
2476        if partners:
2477            # Check we are not in the case when an internal user forwarded the mail manually.
2478            if is_internal_partner(partners[0]):
2479                # Search for partners in the mail's body.
2480                body_mail_addresses = set(email_re.findall(msg_dict.get('body')))
2481                partners = [partner for partner in self._mail_find_partner_from_emails(body_mail_addresses) if not is_internal_partner(partner)]
2482
2483        # Little hack: Inject the mail's subject in the body.
2484        if msg_dict.get('subject') and msg_dict.get('body'):
2485            msg_dict['body'] = '<div><div><h3>%s</h3></div>%s</div>' % (msg_dict['subject'], msg_dict['body'])
2486
2487        # Create the invoice.
2488        values = {
2489            'name': '/',  # we have to give the name otherwise it will be set to the mail's subject
2490            'invoice_source_email': from_mail_addresses[0],
2491            'partner_id': partners and partners[0].id or False,
2492        }
2493        move_ctx = self.with_context(default_move_type=custom_values['move_type'], default_journal_id=custom_values['journal_id'])
2494        move = super(AccountMove, move_ctx).message_new(msg_dict, custom_values=values)
2495        move._compute_name()  # because the name is given, we need to recompute in case it is the first invoice of the journal
2496
2497        # Assign followers.
2498        all_followers_ids = set(partner.id for partner in followers + senders + partners if is_internal_partner(partner))
2499        move.message_subscribe(list(all_followers_ids))
2500        return move
2501
2502    def post(self):
2503        warnings.warn(
2504            "RedirectWarning method 'post()' is a deprecated alias to 'action_post()' or _post()",
2505            DeprecationWarning,
2506            stacklevel=2
2507        )
2508        return self.action_post()
2509
2510    def _post(self, soft=True):
2511        """Post/Validate the documents.
2512
2513        Posting the documents will give it a number, and check that the document is
2514        complete (some fields might not be required if not posted but are required
2515        otherwise).
2516        If the journal is locked with a hash table, it will be impossible to change
2517        some fields afterwards.
2518
2519        :param soft (bool): if True, future documents are not immediately posted,
2520            but are set to be auto posted automatically at the set accounting date.
2521            Nothing will be performed on those documents before the accounting date.
2522        :return Model<account.move>: the documents that have been posted
2523        """
2524        if soft:
2525            future_moves = self.filtered(lambda move: move.date > fields.Date.context_today(self))
2526            future_moves.auto_post = True
2527            for move in future_moves:
2528                msg = _('This move will be posted at the accounting date: %(date)s', date=format_date(self.env, move.date))
2529                move.message_post(body=msg)
2530            to_post = self - future_moves
2531        else:
2532            to_post = self
2533
2534        # `user_has_group` won't be bypassed by `sudo()` since it doesn't change the user anymore.
2535        if not self.env.su and not self.env.user.has_group('account.group_account_invoice'):
2536            raise AccessError(_("You don't have the access rights to post an invoice."))
2537        for move in to_post:
2538            if move.state == 'posted':
2539                raise UserError(_('The entry %s (id %s) is already posted.') % (move.name, move.id))
2540            if not move.line_ids.filtered(lambda line: not line.display_type):
2541                raise UserError(_('You need to add a line before posting.'))
2542            if move.auto_post and move.date > fields.Date.context_today(self):
2543                date_msg = move.date.strftime(get_lang(self.env).date_format)
2544                raise UserError(_("This move is configured to be auto-posted on %s", date_msg))
2545
2546            if not move.partner_id:
2547                if move.is_sale_document():
2548                    raise UserError(_("The field 'Customer' is required, please complete it to validate the Customer Invoice."))
2549                elif move.is_purchase_document():
2550                    raise UserError(_("The field 'Vendor' is required, please complete it to validate the Vendor Bill."))
2551
2552            if move.is_invoice(include_receipts=True) and float_compare(move.amount_total, 0.0, precision_rounding=move.currency_id.rounding) < 0:
2553                raise UserError(_("You cannot validate an invoice with a negative total amount. You should create a credit note instead. Use the action menu to transform it into a credit note or refund."))
2554
2555            # Handle case when the invoice_date is not set. In that case, the invoice_date is set at today and then,
2556            # lines are recomputed accordingly.
2557            # /!\ 'check_move_validity' must be there since the dynamic lines will be recomputed outside the 'onchange'
2558            # environment.
2559            if not move.invoice_date:
2560                if move.is_sale_document(include_receipts=True):
2561                    move.invoice_date = fields.Date.context_today(self)
2562                    move.with_context(check_move_validity=False)._onchange_invoice_date()
2563                elif move.is_purchase_document(include_receipts=True):
2564                    raise UserError(_("The Bill/Refund date is required to validate this document."))
2565
2566            # When the accounting date is prior to the tax lock date, move it automatically to the next available date.
2567            # /!\ 'check_move_validity' must be there since the dynamic lines will be recomputed outside the 'onchange'
2568            # environment.
2569            if (move.company_id.tax_lock_date and move.date <= move.company_id.tax_lock_date) and (move.line_ids.tax_ids or move.line_ids.tax_tag_ids):
2570                move.date = move._get_accounting_date(move.invoice_date or move.date, True)
2571                move.with_context(check_move_validity=False)._onchange_currency()
2572
2573        # Create the analytic lines in batch is faster as it leads to less cache invalidation.
2574        to_post.mapped('line_ids').create_analytic_lines()
2575        to_post.write({
2576            'state': 'posted',
2577            'posted_before': True,
2578        })
2579
2580        for move in to_post:
2581            move.message_subscribe([p.id for p in [move.partner_id] if p not in move.sudo().message_partner_ids])
2582
2583            # Compute 'ref' for 'out_invoice'.
2584            if move._auto_compute_invoice_reference():
2585                to_write = {
2586                    'payment_reference': move._get_invoice_computed_reference(),
2587                    'line_ids': []
2588                }
2589                for line in move.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')):
2590                    to_write['line_ids'].append((1, line.id, {'name': to_write['payment_reference']}))
2591                move.write(to_write)
2592
2593        for move in to_post:
2594            if move.is_sale_document() \
2595                    and move.journal_id.sale_activity_type_id \
2596                    and (move.journal_id.sale_activity_user_id or move.invoice_user_id).id not in (self.env.ref('base.user_root').id, False):
2597                move.activity_schedule(
2598                    date_deadline=min((date for date in move.line_ids.mapped('date_maturity') if date), default=move.date),
2599                    activity_type_id=move.journal_id.sale_activity_type_id.id,
2600                    summary=move.journal_id.sale_activity_note,
2601                    user_id=move.journal_id.sale_activity_user_id.id or move.invoice_user_id.id,
2602                )
2603
2604        customer_count, supplier_count = defaultdict(int), defaultdict(int)
2605        for move in to_post:
2606            if move.is_sale_document():
2607                customer_count[move.partner_id] += 1
2608            elif move.is_purchase_document():
2609                supplier_count[move.partner_id] += 1
2610        for partner, count in customer_count.items():
2611            (partner | partner.commercial_partner_id)._increase_rank('customer_rank', count)
2612        for partner, count in supplier_count.items():
2613            (partner | partner.commercial_partner_id)._increase_rank('supplier_rank', count)
2614
2615        # Trigger action for paid invoices in amount is zero
2616        to_post.filtered(
2617            lambda m: m.is_invoice(include_receipts=True) and m.currency_id.is_zero(m.amount_total)
2618        ).action_invoice_paid()
2619
2620        # Force balance check since nothing prevents another module to create an incorrect entry.
2621        # This is performed at the very end to avoid flushing fields before the whole processing.
2622        to_post._check_balanced()
2623        return to_post
2624
2625    def _auto_compute_invoice_reference(self):
2626        ''' Hook to be overridden to set custom conditions for auto-computed invoice references.
2627            :return True if the move should get a auto-computed reference else False
2628            :rtype bool
2629        '''
2630        self.ensure_one()
2631        return self.move_type == 'out_invoice' and not self.payment_reference
2632
2633    def action_reverse(self):
2634        action = self.env["ir.actions.actions"]._for_xml_id("account.action_view_account_move_reversal")
2635
2636        if self.is_invoice():
2637            action['name'] = _('Credit Note')
2638
2639        return action
2640
2641    def action_post(self):
2642        self._post(soft=False)
2643        return False
2644
2645    def js_assign_outstanding_line(self, line_id):
2646        ''' Called by the 'payment' widget to reconcile a suggested journal item to the present
2647        invoice.
2648
2649        :param line_id: The id of the line to reconcile with the current invoice.
2650        '''
2651        self.ensure_one()
2652        lines = self.env['account.move.line'].browse(line_id)
2653        lines += self.line_ids.filtered(lambda line: line.account_id == lines[0].account_id and not line.reconciled)
2654        return lines.reconcile()
2655
2656    def js_remove_outstanding_partial(self, partial_id):
2657        ''' Called by the 'payment' widget to remove a reconciled entry to the present invoice.
2658
2659        :param partial_id: The id of an existing partial reconciled with the current invoice.
2660        '''
2661        self.ensure_one()
2662        partial = self.env['account.partial.reconcile'].browse(partial_id)
2663        return partial.unlink()
2664
2665    @api.model
2666    def setting_upload_bill_wizard(self):
2667        """ Called by the 'First Bill' button of the setup bar."""
2668        self.env.company.sudo().set_onboarding_step_done('account_setup_bill_state')
2669
2670        new_wizard = self.env['account.tour.upload.bill'].create({})
2671        view_id = self.env.ref('account.account_tour_upload_bill').id
2672
2673        return {
2674            'type': 'ir.actions.act_window',
2675            'name': _('Import your first bill'),
2676            'view_mode': 'form',
2677            'res_model': 'account.tour.upload.bill',
2678            'target': 'new',
2679            'res_id': new_wizard.id,
2680            'views': [[view_id, 'form']],
2681        }
2682
2683    def button_draft(self):
2684        AccountMoveLine = self.env['account.move.line']
2685        excluded_move_ids = []
2686
2687        if self._context.get('suspense_moves_mode'):
2688            excluded_move_ids = AccountMoveLine.search(AccountMoveLine._get_suspense_moves_domain() + [('move_id', 'in', self.ids)]).mapped('move_id').ids
2689
2690        for move in self:
2691            if move in move.line_ids.mapped('full_reconcile_id.exchange_move_id'):
2692                raise UserError(_('You cannot reset to draft an exchange difference journal entry.'))
2693            if move.tax_cash_basis_rec_id:
2694                raise UserError(_('You cannot reset to draft a tax cash basis journal entry.'))
2695            if move.restrict_mode_hash_table and move.state == 'posted' and move.id not in excluded_move_ids:
2696                raise UserError(_('You cannot modify a posted entry of this journal because it is in strict mode.'))
2697            # We remove all the analytics entries for this journal
2698            move.mapped('line_ids.analytic_line_ids').unlink()
2699
2700        self.mapped('line_ids').remove_move_reconcile()
2701        self.write({'state': 'draft', 'is_move_sent': False})
2702
2703    def button_cancel(self):
2704        self.write({'auto_post': False, 'state': 'cancel'})
2705
2706    def action_invoice_sent(self):
2707        """ Open a window to compose an email, with the edi invoice template
2708            message loaded by default
2709        """
2710        self.ensure_one()
2711        template = self.env.ref('account.email_template_edi_invoice', raise_if_not_found=False)
2712        lang = False
2713        if template:
2714            lang = template._render_lang(self.ids)[self.id]
2715        if not lang:
2716            lang = get_lang(self.env).code
2717        compose_form = self.env.ref('account.account_invoice_send_wizard_form', raise_if_not_found=False)
2718        ctx = dict(
2719            default_model='account.move',
2720            default_res_id=self.id,
2721            # For the sake of consistency we need a default_res_model if
2722            # default_res_id is set. Not renaming default_model as it can
2723            # create many side-effects.
2724            default_res_model='account.move',
2725            default_use_template=bool(template),
2726            default_template_id=template and template.id or False,
2727            default_composition_mode='comment',
2728            mark_invoice_as_sent=True,
2729            custom_layout="mail.mail_notification_paynow",
2730            model_description=self.with_context(lang=lang).type_name,
2731            force_email=True
2732        )
2733        return {
2734            'name': _('Send Invoice'),
2735            'type': 'ir.actions.act_window',
2736            'view_type': 'form',
2737            'view_mode': 'form',
2738            'res_model': 'account.invoice.send',
2739            'views': [(compose_form.id, 'form')],
2740            'view_id': compose_form.id,
2741            'target': 'new',
2742            'context': ctx,
2743        }
2744
2745    def _get_new_hash(self, secure_seq_number):
2746        """ Returns the hash to write on journal entries when they get posted"""
2747        self.ensure_one()
2748        #get the only one exact previous move in the securisation sequence
2749        prev_move = self.search([('state', '=', 'posted'),
2750                                 ('company_id', '=', self.company_id.id),
2751                                 ('journal_id', '=', self.journal_id.id),
2752                                 ('secure_sequence_number', '!=', 0),
2753                                 ('secure_sequence_number', '=', int(secure_seq_number) - 1)])
2754        if prev_move and len(prev_move) != 1:
2755            raise UserError(
2756               _('An error occured when computing the inalterability. Impossible to get the unique previous posted journal entry.'))
2757
2758        #build and return the hash
2759        return self._compute_hash(prev_move.inalterable_hash if prev_move else u'')
2760
2761    def _compute_hash(self, previous_hash):
2762        """ Computes the hash of the browse_record given as self, based on the hash
2763        of the previous record in the company's securisation sequence given as parameter"""
2764        self.ensure_one()
2765        hash_string = sha256((previous_hash + self.string_to_hash).encode('utf-8'))
2766        return hash_string.hexdigest()
2767
2768    def _compute_string_to_hash(self):
2769        def _getattrstring(obj, field_str):
2770            field_value = obj[field_str]
2771            if obj._fields[field_str].type == 'many2one':
2772                field_value = field_value.id
2773            return str(field_value)
2774
2775        for move in self:
2776            values = {}
2777            for field in INTEGRITY_HASH_MOVE_FIELDS:
2778                values[field] = _getattrstring(move, field)
2779
2780            for line in move.line_ids:
2781                for field in INTEGRITY_HASH_LINE_FIELDS:
2782                    k = 'line_%d_%s' % (line.id, field)
2783                    values[k] = _getattrstring(line, field)
2784            #make the json serialization canonical
2785            #  (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
2786            move.string_to_hash = dumps(values, sort_keys=True,
2787                                                ensure_ascii=True, indent=None,
2788                                                separators=(',',':'))
2789
2790    def action_invoice_print(self):
2791        """ Print the invoice and mark it as sent, so that we can see more
2792            easily the next step of the workflow
2793        """
2794        if any(not move.is_invoice(include_receipts=True) for move in self):
2795            raise UserError(_("Only invoices could be printed."))
2796
2797        self.filtered(lambda inv: not inv.is_move_sent).write({'is_move_sent': True})
2798        if self.user_has_groups('account.group_account_invoice'):
2799            return self.env.ref('account.account_invoices').report_action(self)
2800        else:
2801            return self.env.ref('account.account_invoices_without_payment').report_action(self)
2802
2803    def action_invoice_paid(self):
2804        ''' Hook to be overrided called when the invoice moves to the paid state. '''
2805        pass
2806
2807    def action_register_payment(self):
2808        ''' Open the account.payment.register wizard to pay the selected journal entries.
2809        :return: An action opening the account.payment.register wizard.
2810        '''
2811        return {
2812            'name': _('Register Payment'),
2813            'res_model': 'account.payment.register',
2814            'view_mode': 'form',
2815            'context': {
2816                'active_model': 'account.move',
2817                'active_ids': self.ids,
2818            },
2819            'target': 'new',
2820            'type': 'ir.actions.act_window',
2821        }
2822
2823    def action_switch_invoice_into_refund_credit_note(self):
2824        if any(move.move_type not in ('in_invoice', 'out_invoice') for move in self):
2825            raise ValidationError(_("This action isn't available for this document."))
2826
2827        for move in self:
2828            reversed_move = move._reverse_move_vals({}, False)
2829            new_invoice_line_ids = []
2830            for cmd, virtualid, line_vals in reversed_move['line_ids']:
2831                if not line_vals['exclude_from_invoice_tab']:
2832                    new_invoice_line_ids.append((0, 0,line_vals))
2833            if move.amount_total < 0:
2834                # Inverse all invoice_line_ids
2835                for cmd, virtualid, line_vals in new_invoice_line_ids:
2836                    line_vals.update({
2837                        'quantity' : -line_vals['quantity'],
2838                        'amount_currency' : -line_vals['amount_currency'],
2839                        'debit' : line_vals['credit'],
2840                        'credit' : line_vals['debit']
2841                    })
2842            move.write({
2843                'move_type': move.move_type.replace('invoice', 'refund'),
2844                'invoice_line_ids' : [(5, 0, 0)],
2845                'partner_bank_id': False,
2846            })
2847            move.write({'invoice_line_ids' : new_invoice_line_ids})
2848
2849    def _get_report_base_filename(self):
2850        if any(not move.is_invoice() for move in self):
2851            raise UserError(_("Only invoices could be printed."))
2852        return self._get_move_display_name()
2853
2854    def _get_name_invoice_report(self):
2855        """ This method need to be inherit by the localizations if they want to print a custom invoice report instead of
2856        the default one. For example please review the l10n_ar module """
2857        self.ensure_one()
2858        return 'account.report_invoice_document'
2859
2860    def preview_invoice(self):
2861        self.ensure_one()
2862        return {
2863            'type': 'ir.actions.act_url',
2864            'target': 'self',
2865            'url': self.get_portal_url(),
2866        }
2867
2868    def _compute_access_url(self):
2869        super(AccountMove, self)._compute_access_url()
2870        for move in self.filtered(lambda move: move.is_invoice()):
2871            move.access_url = '/my/invoices/%s' % (move.id)
2872
2873    @api.depends('line_ids')
2874    def _compute_has_reconciled_entries(self):
2875        for move in self:
2876            move.has_reconciled_entries = len(move.line_ids._reconciled_lines()) > 1
2877
2878    def action_view_reverse_entry(self):
2879        self.ensure_one()
2880
2881        # Create action.
2882        action = {
2883            'name': _('Reverse Moves'),
2884            'type': 'ir.actions.act_window',
2885            'res_model': 'account.move',
2886        }
2887        reverse_entries = self.env['account.move'].search([('reversed_entry_id', '=', self.id)])
2888        if len(reverse_entries) == 1:
2889            action.update({
2890                'view_mode': 'form',
2891                'res_id': reverse_entries.id,
2892            })
2893        else:
2894            action.update({
2895                'view_mode': 'tree',
2896                'domain': [('id', 'in', reverse_entries.ids)],
2897            })
2898        return action
2899
2900    @api.model
2901    def _autopost_draft_entries(self):
2902        ''' This method is called from a cron job.
2903        It is used to post entries such as those created by the module
2904        account_asset.
2905        '''
2906        records = self.search([
2907            ('state', '=', 'draft'),
2908            ('date', '<=', fields.Date.context_today(self)),
2909            ('auto_post', '=', True),
2910        ])
2911        for ids in self._cr.split_for_in_conditions(records.ids, size=1000):
2912            self.browse(ids)._post()
2913            if not self.env.registry.in_test_mode():
2914                self._cr.commit()
2915
2916    # offer the possibility to duplicate thanks to a button instead of a hidden menu, which is more visible
2917    def action_duplicate(self):
2918        self.ensure_one()
2919        action = self.env["ir.actions.actions"]._for_xml_id("account.action_move_journal_line")
2920        action['context'] = dict(self.env.context)
2921        action['context']['form_view_initial_mode'] = 'edit'
2922        action['context']['view_no_maturity'] = False
2923        action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
2924        action['res_id'] = self.copy().id
2925        return action
2926
2927    @api.model
2928    def _move_dict_to_preview_vals(self, move_vals, currency_id=None):
2929        preview_vals = {
2930            'group_name': "%s, %s" % (format_date(self.env, move_vals['date']) or _('[Not set]'), move_vals['ref']),
2931            'items_vals': move_vals['line_ids'],
2932        }
2933        for line in preview_vals['items_vals']:
2934            if 'partner_id' in line[2]:
2935                # sudo is needed to compute display_name in a multi companies environment
2936                line[2]['partner_id'] = self.env['res.partner'].browse(line[2]['partner_id']).sudo().display_name
2937            line[2]['account_id'] = self.env['account.account'].browse(line[2]['account_id']).display_name or _('Destination Account')
2938            line[2]['debit'] = currency_id and formatLang(self.env, line[2]['debit'], currency_obj=currency_id) or line[2]['debit']
2939            line[2]['credit'] = currency_id and formatLang(self.env, line[2]['credit'], currency_obj=currency_id) or line[2]['debit']
2940        return preview_vals
2941
2942    def generate_qr_code(self):
2943        """ Generates and returns a QR-code generation URL for this invoice,
2944        raising an error message if something is misconfigured.
2945
2946        The chosen QR generation method is the one set in qr_method field if there is one,
2947        or the first eligible one found. If this search had to be performed and
2948        and eligible method was found, qr_method field is set to this method before
2949        returning the URL. If no eligible QR method could be found, we return None.
2950        """
2951        self.ensure_one()
2952
2953        if not self.is_invoice():
2954            raise UserError(_("QR-codes can only be generated for invoice entries."))
2955
2956        qr_code_method = self.qr_code_method
2957        if qr_code_method:
2958            # If the user set a qr code generator manually, we check that we can use it
2959            if not self.partner_bank_id._eligible_for_qr_code(self.qr_code_method, self.partner_id, self.currency_id):
2960                raise UserError(_("The chosen QR-code type is not eligible for this invoice."))
2961        else:
2962            # Else we find one that's eligible and assign it to the invoice
2963            for candidate_method, candidate_name in self.env['res.partner.bank'].get_available_qr_methods_in_sequence():
2964                if self.partner_bank_id._eligible_for_qr_code(candidate_method, self.partner_id, self.currency_id):
2965                    qr_code_method = candidate_method
2966                    break
2967
2968        if not qr_code_method:
2969            # No eligible method could be found; we can't generate the QR-code
2970            return None
2971
2972        unstruct_ref = self.ref if self.ref else self.name
2973        rslt = self.partner_bank_id.build_qr_code_url(self.amount_residual, unstruct_ref, self.payment_reference, self.currency_id, self.partner_id, qr_code_method, silent_errors=False)
2974
2975        # We only set qr_code_method after generating the url; otherwise, it
2976        # could be set even in case of a failure in the QR code generation
2977        # (which would change the field, but not refresh UI, making the displayed data inconsistent with db)
2978        self.qr_code_method = qr_code_method
2979
2980        return rslt
2981
2982    def _message_post_after_hook(self, new_message, message_values):
2983        # OVERRIDE
2984        # When posting a message, check the attachment to see if it's an invoice and update with the imported data.
2985        res = super()._message_post_after_hook(new_message, message_values)
2986
2987        attachments = new_message.attachment_ids
2988        if len(self) != 1 or not attachments or self.env.context.get('no_new_invoice') or not self.is_invoice(include_receipts=True):
2989            return res
2990
2991        odoobot = self.env.ref('base.partner_root')
2992        if attachments and self.state != 'draft':
2993            self.message_post(body=_('The invoice is not a draft, it was not updated from the attachment.'),
2994                              message_type='comment',
2995                              subtype_xmlid='mail.mt_note',
2996                              author_id=odoobot.id)
2997            return res
2998        if attachments and self.line_ids:
2999            self.message_post(body=_('The invoice already contains lines, it was not updated from the attachment.'),
3000                              message_type='comment',
3001                              subtype_xmlid='mail.mt_note',
3002                              author_id=odoobot.id)
3003            return res
3004
3005        decoders = self.env['account.move']._get_update_invoice_from_attachment_decoders(self)
3006        for decoder in sorted(decoders, key=lambda d: d[0]):
3007            # start with message_main_attachment_id, that way if OCR is installed, only that one will be parsed.
3008            # this is based on the fact that the ocr will be the last decoder.
3009            for attachment in attachments.sorted(lambda x: x != self.message_main_attachment_id):
3010                invoice = decoder[1](attachment, self)
3011                if invoice:
3012                    return res
3013
3014        return res
3015
3016    def _get_create_invoice_from_attachment_decoders(self):
3017        """ Returns a list of method that are able to create an invoice from an attachment and a priority.
3018
3019        :returns:   A list of tuples (priority, method) where method takes an attachment as parameter.
3020        """
3021        return []
3022
3023    def _get_update_invoice_from_attachment_decoders(self, invoice):
3024        """ Returns a list of method that are able to create an invoice from an attachment and a priority.
3025
3026        :param invoice: The invoice on which to update the data.
3027        :returns:       A list of tuples (priority, method) where method takes an attachment as parameter.
3028        """
3029        return []
3030
3031class AccountMoveLine(models.Model):
3032    _name = "account.move.line"
3033    _description = "Journal Item"
3034    _order = "date desc, move_name desc, id"
3035    _check_company_auto = True
3036
3037    # ==== Business fields ====
3038    move_id = fields.Many2one('account.move', string='Journal Entry',
3039        index=True, required=True, readonly=True, auto_join=True, ondelete="cascade",
3040        check_company=True,
3041        help="The move of this entry line.")
3042    move_name = fields.Char(string='Number', related='move_id.name', store=True, index=True)
3043    date = fields.Date(related='move_id.date', store=True, readonly=True, index=True, copy=False, group_operator='min')
3044    ref = fields.Char(related='move_id.ref', store=True, copy=False, index=True, readonly=False)
3045    parent_state = fields.Selection(related='move_id.state', store=True, readonly=True)
3046    journal_id = fields.Many2one(related='move_id.journal_id', store=True, index=True, copy=False)
3047    company_id = fields.Many2one(related='move_id.company_id', store=True, readonly=True, default=lambda self: self.env.company)
3048    company_currency_id = fields.Many2one(related='company_id.currency_id', string='Company Currency',
3049        readonly=True, store=True,
3050        help='Utility field to express amount currency')
3051    tax_fiscal_country_id = fields.Many2one(comodel_name='res.country', related='move_id.company_id.account_tax_fiscal_country_id')
3052    account_id = fields.Many2one('account.account', string='Account',
3053        index=True, ondelete="cascade",
3054        domain="[('deprecated', '=', False), ('company_id', '=', 'company_id'),('is_off_balance', '=', False)]",
3055        check_company=True,
3056        tracking=True)
3057    account_internal_type = fields.Selection(related='account_id.user_type_id.type', string="Internal Type", readonly=True)
3058    account_internal_group = fields.Selection(related='account_id.user_type_id.internal_group', string="Internal Group", readonly=True)
3059    account_root_id = fields.Many2one(related='account_id.root_id', string="Account Root", store=True, readonly=True)
3060    sequence = fields.Integer(default=10)
3061    name = fields.Char(string='Label', tracking=True)
3062    quantity = fields.Float(string='Quantity',
3063        default=1.0, digits='Product Unit of Measure',
3064        help="The optional quantity expressed by this line, eg: number of product sold. "
3065             "The quantity is not a legal requirement but is very useful for some reports.")
3066    price_unit = fields.Float(string='Unit Price', digits='Product Price')
3067    discount = fields.Float(string='Discount (%)', digits='Discount', default=0.0)
3068    debit = fields.Monetary(string='Debit', default=0.0, currency_field='company_currency_id')
3069    credit = fields.Monetary(string='Credit', default=0.0, currency_field='company_currency_id')
3070    balance = fields.Monetary(string='Balance', store=True,
3071        currency_field='company_currency_id',
3072        compute='_compute_balance',
3073        help="Technical field holding the debit - credit in order to open meaningful graph views from reports")
3074    cumulated_balance = fields.Monetary(string='Cumulated Balance', store=False,
3075        currency_field='company_currency_id',
3076        compute='_compute_cumulated_balance',
3077        help="Cumulated balance depending on the domain and the order chosen in the view.")
3078    amount_currency = fields.Monetary(string='Amount in Currency', store=True, copy=True,
3079        help="The amount expressed in an optional other currency if it is a multi-currency entry.")
3080    price_subtotal = fields.Monetary(string='Subtotal', store=True, readonly=True,
3081        currency_field='currency_id')
3082    price_total = fields.Monetary(string='Total', store=True, readonly=True,
3083        currency_field='currency_id')
3084    reconciled = fields.Boolean(compute='_compute_amount_residual', store=True)
3085    blocked = fields.Boolean(string='No Follow-up', default=False,
3086        help="You can check this box to mark this journal item as a litigation with the associated partner")
3087    date_maturity = fields.Date(string='Due Date', index=True, tracking=True,
3088        help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.")
3089    currency_id = fields.Many2one('res.currency', string='Currency', required=True)
3090    partner_id = fields.Many2one('res.partner', string='Partner', ondelete='restrict')
3091    product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
3092    product_id = fields.Many2one('product.product', string='Product', ondelete='restrict')
3093    product_uom_category_id = fields.Many2one('uom.category', related='product_id.uom_id.category_id')
3094
3095    # ==== Origin fields ====
3096    reconcile_model_id = fields.Many2one('account.reconcile.model', string="Reconciliation Model", copy=False, readonly=True, check_company=True)
3097    payment_id = fields.Many2one('account.payment', index=True, store=True,
3098        string="Originator Payment",
3099        related='move_id.payment_id',
3100        help="The payment that created this entry")
3101    statement_line_id = fields.Many2one('account.bank.statement.line', index=True, store=True,
3102        string="Originator Statement Line",
3103        related='move_id.statement_line_id',
3104        help="The statement line that created this entry")
3105    statement_id = fields.Many2one(related='statement_line_id.statement_id', store=True, index=True, copy=False,
3106        help="The bank statement used for bank reconciliation")
3107
3108    # ==== Tax fields ====
3109    tax_ids = fields.Many2many(
3110        comodel_name='account.tax',
3111        string="Taxes",
3112        context={'active_test': False},
3113        check_company=True,
3114        help="Taxes that apply on the base amount")
3115    tax_line_id = fields.Many2one('account.tax', string='Originator Tax', ondelete='restrict', store=True,
3116        compute='_compute_tax_line_id', help="Indicates that this journal item is a tax line")
3117    tax_group_id = fields.Many2one(related='tax_line_id.tax_group_id', string='Originator tax group',
3118        readonly=True, store=True,
3119        help='technical field for widget tax-group-custom-field')
3120    tax_base_amount = fields.Monetary(string="Base Amount", store=True, readonly=True,
3121        currency_field='company_currency_id')
3122    tax_exigible = fields.Boolean(string='Appears in VAT report', default=True, readonly=True,
3123        help="Technical field used to mark a tax line as exigible in the vat report or not (only exigible journal items"
3124             " are displayed). By default all new journal items are directly exigible, but with the feature cash_basis"
3125             " on taxes, some will become exigible only when the payment is recorded.")
3126    tax_repartition_line_id = fields.Many2one(comodel_name='account.tax.repartition.line',
3127        string="Originator Tax Distribution Line", ondelete='restrict', readonly=True,
3128        check_company=True,
3129        help="Tax distribution line that caused the creation of this move line, if any")
3130    tax_tag_ids = fields.Many2many(string="Tags", comodel_name='account.account.tag', ondelete='restrict',
3131        help="Tags assigned to this line by the tax creating it, if any. It determines its impact on financial reports.", tracking=True)
3132    tax_audit = fields.Char(string="Tax Audit String", compute="_compute_tax_audit", store=True,
3133        help="Computed field, listing the tax grids impacted by this line, and the amount it applies to each of them.")
3134
3135    # ==== Reconciliation fields ====
3136    amount_residual = fields.Monetary(string='Residual Amount', store=True,
3137        currency_field='company_currency_id',
3138        compute='_compute_amount_residual',
3139        help="The residual amount on a journal item expressed in the company currency.")
3140    amount_residual_currency = fields.Monetary(string='Residual Amount in Currency', store=True,
3141        compute='_compute_amount_residual',
3142        help="The residual amount on a journal item expressed in its currency (possibly not the company currency).")
3143    full_reconcile_id = fields.Many2one('account.full.reconcile', string="Matching", copy=False, index=True, readonly=True)
3144    matched_debit_ids = fields.One2many('account.partial.reconcile', 'credit_move_id', string='Matched Debits',
3145        help='Debit journal items that are matched with this journal item.', readonly=True)
3146    matched_credit_ids = fields.One2many('account.partial.reconcile', 'debit_move_id', string='Matched Credits',
3147        help='Credit journal items that are matched with this journal item.', readonly=True)
3148    matching_number = fields.Char(string="Matching #", compute='_compute_matching_number', store=True, help="Matching number for this line, 'P' if it is only partially reconcile, or the name of the full reconcile if it exists.")
3149
3150    # ==== Analytic fields ====
3151    analytic_line_ids = fields.One2many('account.analytic.line', 'move_id', string='Analytic lines')
3152    analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account',
3153        index=True, compute="_compute_analytic_account", store=True, readonly=False, check_company=True, copy=True)
3154    analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags',
3155        compute="_compute_analytic_account", store=True, readonly=False, check_company=True, copy=True)
3156
3157    # ==== Onchange / display purpose fields ====
3158    recompute_tax_line = fields.Boolean(store=False, readonly=True,
3159        help="Technical field used to know on which lines the taxes must be recomputed.")
3160    display_type = fields.Selection([
3161        ('line_section', 'Section'),
3162        ('line_note', 'Note'),
3163    ], default=False, help="Technical field for UX purpose.")
3164    is_rounding_line = fields.Boolean(help="Technical field used to retrieve the cash rounding line.")
3165    exclude_from_invoice_tab = fields.Boolean(help="Technical field used to exclude some lines from the invoice_line_ids tab in the form view.")
3166
3167    _sql_constraints = [
3168        (
3169            'check_credit_debit',
3170            'CHECK(credit + debit>=0 AND credit * debit=0)',
3171            'Wrong credit or debit value in accounting entry !'
3172        ),
3173        (
3174            'check_accountable_required_fields',
3175             "CHECK(COALESCE(display_type IN ('line_section', 'line_note'), 'f') OR account_id IS NOT NULL)",
3176             "Missing required account on accountable invoice line."
3177        ),
3178        (
3179            'check_non_accountable_fields_null',
3180             "CHECK(display_type NOT IN ('line_section', 'line_note') OR (amount_currency = 0 AND debit = 0 AND credit = 0 AND account_id IS NULL))",
3181             "Forbidden unit price, account and quantity on non-accountable invoice line"
3182        ),
3183        (
3184            'check_amount_currency_balance_sign',
3185            '''CHECK(
3186                (
3187                    (currency_id != company_currency_id)
3188                    AND
3189                    (
3190                        (debit - credit <= 0 AND amount_currency <= 0)
3191                        OR
3192                        (debit - credit >= 0 AND amount_currency >= 0)
3193                    )
3194                )
3195                OR
3196                (
3197                    currency_id = company_currency_id
3198                    AND
3199                    ROUND(debit - credit - amount_currency, 2) = 0
3200                )
3201            )''',
3202            "The amount expressed in the secondary currency must be positive when account is debited and negative when "
3203            "account is credited. If the currency is the same as the one from the company, this amount must strictly "
3204            "be equal to the balance."
3205        ),
3206    ]
3207
3208    # -------------------------------------------------------------------------
3209    # HELPERS
3210    # -------------------------------------------------------------------------
3211
3212    @api.model
3213    def _get_default_line_name(self, document, amount, currency, date, partner=None):
3214        ''' Helper to construct a default label to set on journal items.
3215
3216        E.g. Vendor Reimbursement $ 1,555.00 - Azure Interior - 05/14/2020.
3217
3218        :param document:    A string representing the type of the document.
3219        :param amount:      The document's amount.
3220        :param currency:    The document's currency.
3221        :param date:        The document's date.
3222        :param partner:     The optional partner.
3223        :return:            A string.
3224        '''
3225        values = ['%s %s' % (document, formatLang(self.env, amount, currency_obj=currency))]
3226        if partner:
3227            values.append(partner.display_name)
3228        values.append(format_date(self.env, fields.Date.to_string(date)))
3229        return ' - '.join(values)
3230
3231    @api.model
3232    def _get_default_tax_account(self, repartition_line):
3233        tax = repartition_line.invoice_tax_id or repartition_line.refund_tax_id
3234        if tax.tax_exigibility == 'on_payment':
3235            account = tax.cash_basis_transition_account_id
3236        else:
3237            account = repartition_line.account_id
3238        return account
3239
3240    def _get_computed_name(self):
3241        self.ensure_one()
3242
3243        if not self.product_id:
3244            return ''
3245
3246        if self.partner_id.lang:
3247            product = self.product_id.with_context(lang=self.partner_id.lang)
3248        else:
3249            product = self.product_id
3250
3251        values = []
3252        if product.partner_ref:
3253            values.append(product.partner_ref)
3254        if self.journal_id.type == 'sale':
3255            if product.description_sale:
3256                values.append(product.description_sale)
3257        elif self.journal_id.type == 'purchase':
3258            if product.description_purchase:
3259                values.append(product.description_purchase)
3260        return '\n'.join(values)
3261
3262    def _get_computed_price_unit(self):
3263        ''' Helper to get the default price unit based on the product by taking care of the taxes
3264        set on the product and the fiscal position.
3265        :return: The price unit.
3266        '''
3267        self.ensure_one()
3268
3269        if not self.product_id:
3270            return 0.0
3271
3272        company = self.move_id.company_id
3273        currency = self.move_id.currency_id
3274        company_currency = company.currency_id
3275        product_uom = self.product_id.uom_id
3276        fiscal_position = self.move_id.fiscal_position_id
3277        is_refund_document = self.move_id.move_type in ('out_refund', 'in_refund')
3278        move_date = self.move_id.date or fields.Date.context_today(self)
3279
3280        if self.move_id.is_sale_document(include_receipts=True):
3281            product_price_unit = self.product_id.lst_price
3282            product_taxes = self.product_id.taxes_id
3283        elif self.move_id.is_purchase_document(include_receipts=True):
3284            product_price_unit = self.product_id.standard_price
3285            product_taxes = self.product_id.supplier_taxes_id
3286        else:
3287            return 0.0
3288        product_taxes = product_taxes.filtered(lambda tax: tax.company_id == company)
3289
3290        # Apply unit of measure.
3291        if self.product_uom_id and self.product_uom_id != product_uom:
3292            product_price_unit = product_uom._compute_price(product_price_unit, self.product_uom_id)
3293
3294        # Apply fiscal position.
3295        if product_taxes and fiscal_position:
3296            product_taxes_after_fp = fiscal_position.map_tax(product_taxes, partner=self.partner_id)
3297
3298            if set(product_taxes.ids) != set(product_taxes_after_fp.ids):
3299                flattened_taxes_before_fp = product_taxes._origin.flatten_taxes_hierarchy()
3300                if any(tax.price_include for tax in flattened_taxes_before_fp):
3301                    taxes_res = flattened_taxes_before_fp.compute_all(
3302                        product_price_unit,
3303                        quantity=1.0,
3304                        currency=company_currency,
3305                        product=self.product_id,
3306                        partner=self.partner_id,
3307                        is_refund=is_refund_document,
3308                    )
3309                    product_price_unit = company_currency.round(taxes_res['total_excluded'])
3310
3311                flattened_taxes_after_fp = product_taxes_after_fp._origin.flatten_taxes_hierarchy()
3312                if any(tax.price_include for tax in flattened_taxes_after_fp):
3313                    taxes_res = flattened_taxes_after_fp.compute_all(
3314                        product_price_unit,
3315                        quantity=1.0,
3316                        currency=company_currency,
3317                        product=self.product_id,
3318                        partner=self.partner_id,
3319                        is_refund=is_refund_document,
3320                        handle_price_include=False,
3321                    )
3322                    for tax_res in taxes_res['taxes']:
3323                        tax = self.env['account.tax'].browse(tax_res['id'])
3324                        if tax.price_include:
3325                            product_price_unit += tax_res['amount']
3326
3327        # Apply currency rate.
3328        if currency and currency != company_currency:
3329            product_price_unit = company_currency._convert(product_price_unit, currency, company, move_date)
3330
3331        return product_price_unit
3332
3333    def _get_computed_account(self):
3334        self.ensure_one()
3335        self = self.with_company(self.move_id.journal_id.company_id)
3336
3337        if not self.product_id:
3338            return
3339
3340        fiscal_position = self.move_id.fiscal_position_id
3341        accounts = self.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fiscal_position)
3342        if self.move_id.is_sale_document(include_receipts=True):
3343            # Out invoice.
3344            return accounts['income'] or self.account_id
3345        elif self.move_id.is_purchase_document(include_receipts=True):
3346            # In invoice.
3347            return accounts['expense'] or self.account_id
3348
3349    def _get_computed_taxes(self):
3350        self.ensure_one()
3351
3352        if self.move_id.is_sale_document(include_receipts=True):
3353            # Out invoice.
3354            if self.product_id.taxes_id:
3355                tax_ids = self.product_id.taxes_id.filtered(lambda tax: tax.company_id == self.move_id.company_id)
3356            elif self.account_id.tax_ids:
3357                tax_ids = self.account_id.tax_ids
3358            else:
3359                tax_ids = self.env['account.tax']
3360            if not tax_ids and not self.exclude_from_invoice_tab:
3361                tax_ids = self.move_id.company_id.account_sale_tax_id
3362        elif self.move_id.is_purchase_document(include_receipts=True):
3363            # In invoice.
3364            if self.product_id.supplier_taxes_id:
3365                tax_ids = self.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == self.move_id.company_id)
3366            elif self.account_id.tax_ids:
3367                tax_ids = self.account_id.tax_ids
3368            else:
3369                tax_ids = self.env['account.tax']
3370            if not tax_ids and not self.exclude_from_invoice_tab:
3371                tax_ids = self.move_id.company_id.account_purchase_tax_id
3372        else:
3373            # Miscellaneous operation.
3374            tax_ids = self.account_id.tax_ids
3375
3376        if self.company_id and tax_ids:
3377            tax_ids = tax_ids.filtered(lambda tax: tax.company_id == self.company_id)
3378
3379        return tax_ids
3380
3381    def _get_computed_uom(self):
3382        self.ensure_one()
3383        if self.product_id:
3384            return self.product_id.uom_id
3385        return False
3386
3387    def _set_price_and_tax_after_fpos(self):
3388        self.ensure_one()
3389        # Manage the fiscal position after that and adapt the price_unit.
3390        # E.g. mapping a price-included-tax to a price-excluded-tax must
3391        # remove the tax amount from the price_unit.
3392        # However, mapping a price-included tax to another price-included tax must preserve the balance but
3393        # adapt the price_unit to the new tax.
3394        # E.g. mapping a 10% price-included tax to a 20% price-included tax for a price_unit of 110 should preserve
3395        # 100 as balance but set 120 as price_unit.
3396        if self.tax_ids and self.move_id.fiscal_position_id and self.move_id.fiscal_position_id.tax_ids:
3397            price_subtotal = self._get_price_total_and_subtotal()['price_subtotal']
3398            self.tax_ids = self.move_id.fiscal_position_id.map_tax(
3399                self.tax_ids._origin,
3400                partner=self.move_id.partner_id)
3401            accounting_vals = self._get_fields_onchange_subtotal(
3402                price_subtotal=price_subtotal,
3403                currency=self.move_id.company_currency_id)
3404            amount_currency = accounting_vals['amount_currency']
3405            business_vals = self._get_fields_onchange_balance(amount_currency=amount_currency)
3406            if 'price_unit' in business_vals:
3407                self.price_unit = business_vals['price_unit']
3408
3409    @api.depends('product_id', 'account_id', 'partner_id', 'date')
3410    def _compute_analytic_account(self):
3411        for record in self:
3412            if not record.exclude_from_invoice_tab or not record.move_id.is_invoice(include_receipts=True):
3413                rec = self.env['account.analytic.default'].account_get(
3414                    product_id=record.product_id.id,
3415                    partner_id=record.partner_id.commercial_partner_id.id or record.move_id.partner_id.commercial_partner_id.id,
3416                    account_id=record.account_id.id,
3417                    user_id=record.env.uid,
3418                    date=record.date,
3419                    company_id=record.move_id.company_id.id
3420                )
3421                if rec:
3422                    record.analytic_account_id = rec.analytic_id
3423                    record.analytic_tag_ids = rec.analytic_tag_ids
3424
3425    def _get_price_total_and_subtotal(self, price_unit=None, quantity=None, discount=None, currency=None, product=None, partner=None, taxes=None, move_type=None):
3426        self.ensure_one()
3427        return self._get_price_total_and_subtotal_model(
3428            price_unit=price_unit or self.price_unit,
3429            quantity=quantity or self.quantity,
3430            discount=discount or self.discount,
3431            currency=currency or self.currency_id,
3432            product=product or self.product_id,
3433            partner=partner or self.partner_id,
3434            taxes=taxes or self.tax_ids,
3435            move_type=move_type or self.move_id.move_type,
3436        )
3437
3438    @api.model
3439    def _get_price_total_and_subtotal_model(self, price_unit, quantity, discount, currency, product, partner, taxes, move_type):
3440        ''' This method is used to compute 'price_total' & 'price_subtotal'.
3441
3442        :param price_unit:  The current price unit.
3443        :param quantity:    The current quantity.
3444        :param discount:    The current discount.
3445        :param currency:    The line's currency.
3446        :param product:     The line's product.
3447        :param partner:     The line's partner.
3448        :param taxes:       The applied taxes.
3449        :param move_type:   The type of the move.
3450        :return:            A dictionary containing 'price_subtotal' & 'price_total'.
3451        '''
3452        res = {}
3453
3454        # Compute 'price_subtotal'.
3455        line_discount_price_unit = price_unit * (1 - (discount / 100.0))
3456        subtotal = quantity * line_discount_price_unit
3457
3458        # Compute 'price_total'.
3459        if taxes:
3460            force_sign = -1 if move_type in ('out_invoice', 'in_refund', 'out_receipt') else 1
3461            taxes_res = taxes._origin.with_context(force_sign=force_sign).compute_all(line_discount_price_unit,
3462                quantity=quantity, currency=currency, product=product, partner=partner, is_refund=move_type in ('out_refund', 'in_refund'))
3463            res['price_subtotal'] = taxes_res['total_excluded']
3464            res['price_total'] = taxes_res['total_included']
3465        else:
3466            res['price_total'] = res['price_subtotal'] = subtotal
3467        #In case of multi currency, round before it's use for computing debit credit
3468        if currency:
3469            res = {k: currency.round(v) for k, v in res.items()}
3470        return res
3471
3472    def _get_fields_onchange_subtotal(self, price_subtotal=None, move_type=None, currency=None, company=None, date=None):
3473        self.ensure_one()
3474        return self._get_fields_onchange_subtotal_model(
3475            price_subtotal=price_subtotal or self.price_subtotal,
3476            move_type=move_type or self.move_id.move_type,
3477            currency=currency or self.currency_id,
3478            company=company or self.move_id.company_id,
3479            date=date or self.move_id.date,
3480        )
3481
3482    @api.model
3483    def _get_fields_onchange_subtotal_model(self, price_subtotal, move_type, currency, company, date):
3484        ''' This method is used to recompute the values of 'amount_currency', 'debit', 'credit' due to a change made
3485        in some business fields (affecting the 'price_subtotal' field).
3486
3487        :param price_subtotal:  The untaxed amount.
3488        :param move_type:       The type of the move.
3489        :param currency:        The line's currency.
3490        :param company:         The move's company.
3491        :param date:            The move's date.
3492        :return:                A dictionary containing 'debit', 'credit', 'amount_currency'.
3493        '''
3494        if move_type in self.move_id.get_outbound_types():
3495            sign = 1
3496        elif move_type in self.move_id.get_inbound_types():
3497            sign = -1
3498        else:
3499            sign = 1
3500
3501        amount_currency = price_subtotal * sign
3502        balance = currency._convert(amount_currency, company.currency_id, company, date or fields.Date.context_today(self))
3503        return {
3504            'amount_currency': amount_currency,
3505            'currency_id': currency.id,
3506            'debit': balance > 0.0 and balance or 0.0,
3507            'credit': balance < 0.0 and -balance or 0.0,
3508        }
3509
3510    def _get_fields_onchange_balance(self, quantity=None, discount=None, amount_currency=None, move_type=None, currency=None, taxes=None, price_subtotal=None, force_computation=False):
3511        self.ensure_one()
3512        return self._get_fields_onchange_balance_model(
3513            quantity=quantity or self.quantity,
3514            discount=discount or self.discount,
3515            amount_currency=amount_currency or self.amount_currency,
3516            move_type=move_type or self.move_id.move_type,
3517            currency=currency or self.currency_id or self.move_id.currency_id,
3518            taxes=taxes or self.tax_ids,
3519            price_subtotal=price_subtotal or self.price_subtotal,
3520            force_computation=force_computation,
3521        )
3522
3523    @api.model
3524    def _get_fields_onchange_balance_model(self, quantity, discount, amount_currency, move_type, currency, taxes, price_subtotal, force_computation=False):
3525        ''' This method is used to recompute the values of 'quantity', 'discount', 'price_unit' due to a change made
3526        in some accounting fields such as 'balance'.
3527
3528        This method is a bit complex as we need to handle some special cases.
3529        For example, setting a positive balance with a 100% discount.
3530
3531        :param quantity:        The current quantity.
3532        :param discount:        The current discount.
3533        :param amount_currency: The new balance in line's currency.
3534        :param move_type:       The type of the move.
3535        :param currency:        The currency.
3536        :param taxes:           The applied taxes.
3537        :param price_subtotal:  The price_subtotal.
3538        :return:                A dictionary containing 'quantity', 'discount', 'price_unit'.
3539        '''
3540        if move_type in self.move_id.get_outbound_types():
3541            sign = 1
3542        elif move_type in self.move_id.get_inbound_types():
3543            sign = -1
3544        else:
3545            sign = 1
3546        amount_currency *= sign
3547
3548        # Avoid rounding issue when dealing with price included taxes. For example, when the price_unit is 2300.0 and
3549        # a 5.5% price included tax is applied on it, a balance of 2300.0 / 1.055 = 2180.094 ~ 2180.09 is computed.
3550        # However, when triggering the inverse, 2180.09 + (2180.09 * 0.055) = 2180.09 + 119.90 = 2299.99 is computed.
3551        # To avoid that, set the price_subtotal at the balance if the difference between them looks like a rounding
3552        # issue.
3553        if not force_computation and currency.is_zero(amount_currency - price_subtotal):
3554            return {}
3555
3556        taxes = taxes.flatten_taxes_hierarchy()
3557        if taxes and any(tax.price_include for tax in taxes):
3558            # Inverse taxes. E.g:
3559            #
3560            # Price Unit    | Taxes         | Originator Tax    |Price Subtotal     | Price Total
3561            # -----------------------------------------------------------------------------------
3562            # 110           | 10% incl, 5%  |                   | 100               | 115
3563            # 10            |               | 10% incl          | 10                | 10
3564            # 5             |               | 5%                | 5                 | 5
3565            #
3566            # When setting the balance to -200, the expected result is:
3567            #
3568            # Price Unit    | Taxes         | Originator Tax    |Price Subtotal     | Price Total
3569            # -----------------------------------------------------------------------------------
3570            # 220           | 10% incl, 5%  |                   | 200               | 230
3571            # 20            |               | 10% incl          | 20                | 20
3572            # 10            |               | 5%                | 10                | 10
3573            force_sign = -1 if move_type in ('out_invoice', 'in_refund', 'out_receipt') else 1
3574            taxes_res = taxes._origin.with_context(force_sign=force_sign).compute_all(amount_currency, currency=currency, handle_price_include=False)
3575            for tax_res in taxes_res['taxes']:
3576                tax = self.env['account.tax'].browse(tax_res['id'])
3577                if tax.price_include:
3578                    amount_currency += tax_res['amount']
3579
3580        discount_factor = 1 - (discount / 100.0)
3581        if amount_currency and discount_factor:
3582            # discount != 100%
3583            vals = {
3584                'quantity': quantity or 1.0,
3585                'price_unit': amount_currency / discount_factor / (quantity or 1.0),
3586            }
3587        elif amount_currency and not discount_factor:
3588            # discount == 100%
3589            vals = {
3590                'quantity': quantity or 1.0,
3591                'discount': 0.0,
3592                'price_unit': amount_currency / (quantity or 1.0),
3593            }
3594        elif not discount_factor:
3595            # balance of line is 0, but discount  == 100% so we display the normal unit_price
3596            vals = {}
3597        else:
3598            # balance is 0, so unit price is 0 as well
3599            vals = {'price_unit': 0.0}
3600        return vals
3601
3602    # -------------------------------------------------------------------------
3603    # ONCHANGE METHODS
3604    # -------------------------------------------------------------------------
3605
3606    @api.onchange('amount_currency', 'currency_id', 'debit', 'credit', 'tax_ids', 'account_id', 'price_unit', 'quantity')
3607    def _onchange_mark_recompute_taxes(self):
3608        ''' Recompute the dynamic onchange based on taxes.
3609        If the edited line is a tax line, don't recompute anything as the user must be able to
3610        set a custom value.
3611        '''
3612        for line in self:
3613            if not line.tax_repartition_line_id:
3614                line.recompute_tax_line = True
3615
3616    @api.onchange('analytic_account_id', 'analytic_tag_ids')
3617    def _onchange_mark_recompute_taxes_analytic(self):
3618        ''' Trigger tax recomputation only when some taxes with analytics
3619        '''
3620        for line in self:
3621            if not line.tax_repartition_line_id and any(tax.analytic for tax in line.tax_ids):
3622                line.recompute_tax_line = True
3623
3624    @api.onchange('product_id')
3625    def _onchange_product_id(self):
3626        for line in self:
3627            if not line.product_id or line.display_type in ('line_section', 'line_note'):
3628                continue
3629
3630            line.name = line._get_computed_name()
3631            line.account_id = line._get_computed_account()
3632            taxes = line._get_computed_taxes()
3633            if taxes and line.move_id.fiscal_position_id:
3634                taxes = line.move_id.fiscal_position_id.map_tax(taxes, partner=line.partner_id)
3635            line.tax_ids = taxes
3636            line.product_uom_id = line._get_computed_uom()
3637            line.price_unit = line._get_computed_price_unit()
3638
3639    @api.onchange('product_uom_id')
3640    def _onchange_uom_id(self):
3641        ''' Recompute the 'price_unit' depending of the unit of measure. '''
3642        if self.display_type in ('line_section', 'line_note'):
3643            return
3644        taxes = self._get_computed_taxes()
3645        if taxes and self.move_id.fiscal_position_id:
3646            taxes = self.move_id.fiscal_position_id.map_tax(taxes, partner=self.partner_id)
3647        self.tax_ids = taxes
3648        self.price_unit = self._get_computed_price_unit()
3649
3650    @api.onchange('account_id')
3651    def _onchange_account_id(self):
3652        ''' Recompute 'tax_ids' based on 'account_id'.
3653        /!\ Don't remove existing taxes if there is no explicit taxes set on the account.
3654        '''
3655        if not self.display_type and (self.account_id.tax_ids or not self.tax_ids):
3656            taxes = self._get_computed_taxes()
3657
3658            if taxes and self.move_id.fiscal_position_id:
3659                taxes = self.move_id.fiscal_position_id.map_tax(taxes, partner=self.partner_id)
3660
3661            self.tax_ids = taxes
3662
3663    def _onchange_balance(self):
3664        for line in self:
3665            if line.currency_id == line.move_id.company_id.currency_id:
3666                line.amount_currency = line.balance
3667            else:
3668                continue
3669            if not line.move_id.is_invoice(include_receipts=True):
3670                continue
3671            line.update(line._get_fields_onchange_balance())
3672
3673    @api.onchange('debit')
3674    def _onchange_debit(self):
3675        if self.debit:
3676            self.credit = 0.0
3677        self._onchange_balance()
3678
3679    @api.onchange('credit')
3680    def _onchange_credit(self):
3681        if self.credit:
3682            self.debit = 0.0
3683        self._onchange_balance()
3684
3685    @api.onchange('amount_currency')
3686    def _onchange_amount_currency(self):
3687        for line in self:
3688            company = line.move_id.company_id
3689            balance = line.currency_id._convert(line.amount_currency, company.currency_id, company, line.move_id.date)
3690            line.debit = balance if balance > 0.0 else 0.0
3691            line.credit = -balance if balance < 0.0 else 0.0
3692
3693            if not line.move_id.is_invoice(include_receipts=True):
3694                continue
3695
3696            line.update(line._get_fields_onchange_balance())
3697            line.update(line._get_price_total_and_subtotal())
3698
3699    @api.onchange('quantity', 'discount', 'price_unit', 'tax_ids')
3700    def _onchange_price_subtotal(self):
3701        for line in self:
3702            if not line.move_id.is_invoice(include_receipts=True):
3703                continue
3704
3705            line.update(line._get_price_total_and_subtotal())
3706            line.update(line._get_fields_onchange_subtotal())
3707
3708    @api.onchange('currency_id')
3709    def _onchange_currency(self):
3710        for line in self:
3711            company = line.move_id.company_id
3712
3713            if line.move_id.is_invoice(include_receipts=True):
3714                line._onchange_price_subtotal()
3715            elif not line.move_id.reversed_entry_id:
3716                balance = line.currency_id._convert(line.amount_currency, company.currency_id, company, line.move_id.date or fields.Date.context_today(line))
3717                line.debit = balance if balance > 0.0 else 0.0
3718                line.credit = -balance if balance < 0.0 else 0.0
3719
3720    # -------------------------------------------------------------------------
3721    # COMPUTE METHODS
3722    # -------------------------------------------------------------------------
3723
3724    @api.depends('full_reconcile_id.name', 'matched_debit_ids', 'matched_credit_ids')
3725    def _compute_matching_number(self):
3726        for record in self:
3727            if record.full_reconcile_id:
3728                record.matching_number = record.full_reconcile_id.name
3729            elif record.matched_debit_ids or record.matched_credit_ids:
3730                record.matching_number = 'P'
3731            else:
3732                record.matching_number = None
3733
3734    @api.depends('debit', 'credit')
3735    def _compute_balance(self):
3736        for line in self:
3737            line.balance = line.debit - line.credit
3738
3739    @api.model
3740    def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None):
3741        def to_tuple(t):
3742            return tuple(map(to_tuple, t)) if isinstance(t, (list, tuple)) else t
3743        # Make an explicit order because we will need to reverse it
3744        order = (order or self._order) + ', id'
3745        # Add the domain and order by in order to compute the cumulated balance in _compute_cumulated_balance
3746        return super(AccountMoveLine, self.with_context(domain_cumulated_balance=to_tuple(domain or []), order_cumulated_balance=order)).search_read(domain, fields, offset, limit, order)
3747
3748    @api.depends_context('order_cumulated_balance', 'domain_cumulated_balance')
3749    def _compute_cumulated_balance(self):
3750        if not self.env.context.get('order_cumulated_balance'):
3751            # We do not come from search_read, so we are not in a list view, so it doesn't make any sense to compute the cumulated balance
3752            self.cumulated_balance = 0
3753            return
3754
3755        # get the where clause
3756        query = self._where_calc(list(self.env.context.get('domain_cumulated_balance') or []))
3757        order_string = ", ".join(self._generate_order_by_inner(self._table, self.env.context.get('order_cumulated_balance'), query, reverse_direction=True))
3758        from_clause, where_clause, where_clause_params = query.get_sql()
3759        sql = """
3760            SELECT account_move_line.id, SUM(account_move_line.balance) OVER (
3761                ORDER BY %(order_by)s
3762                ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
3763            )
3764            FROM %(from)s
3765            WHERE %(where)s
3766        """ % {'from': from_clause, 'where': where_clause or 'TRUE', 'order_by': order_string}
3767        self.env.cr.execute(sql, where_clause_params)
3768        result = {r[0]: r[1] for r in self.env.cr.fetchall()}
3769        for record in self:
3770            record.cumulated_balance = result[record.id]
3771
3772    @api.depends('debit', 'credit', 'amount_currency', 'account_id', 'currency_id', 'move_id.state', 'company_id',
3773                 'matched_debit_ids', 'matched_credit_ids')
3774    def _compute_amount_residual(self):
3775        """ Computes the residual amount of a move line from a reconcilable account in the company currency and the line's currency.
3776            This amount will be 0 for fully reconciled lines or lines from a non-reconcilable account, the original line amount
3777            for unreconciled lines, and something in-between for partially reconciled lines.
3778        """
3779        for line in self:
3780            if line.id and (line.account_id.reconcile or line.account_id.internal_type == 'liquidity'):
3781                reconciled_balance = sum(line.matched_credit_ids.mapped('amount')) \
3782                                     - sum(line.matched_debit_ids.mapped('amount'))
3783                reconciled_amount_currency = sum(line.matched_credit_ids.mapped('debit_amount_currency'))\
3784                                             - sum(line.matched_debit_ids.mapped('credit_amount_currency'))
3785
3786                line.amount_residual = line.balance - reconciled_balance
3787
3788                if line.currency_id:
3789                    line.amount_residual_currency = line.amount_currency - reconciled_amount_currency
3790                else:
3791                    line.amount_residual_currency = 0.0
3792
3793                line.reconciled = line.company_currency_id.is_zero(line.amount_residual) \
3794                                  and (not line.currency_id or line.currency_id.is_zero(line.amount_residual_currency))
3795            else:
3796                # Must not have any reconciliation since the line is not eligible for that.
3797                line.amount_residual = 0.0
3798                line.amount_residual_currency = 0.0
3799                line.reconciled = False
3800
3801    @api.depends('tax_repartition_line_id.invoice_tax_id', 'tax_repartition_line_id.refund_tax_id')
3802    def _compute_tax_line_id(self):
3803        """ tax_line_id is computed as the tax linked to the repartition line creating
3804        the move.
3805        """
3806        for record in self:
3807            rep_line = record.tax_repartition_line_id
3808            # A constraint on account.tax.repartition.line ensures both those fields are mutually exclusive
3809            record.tax_line_id = rep_line.invoice_tax_id or rep_line.refund_tax_id
3810
3811    @api.depends('tax_tag_ids', 'debit', 'credit', 'journal_id')
3812    def _compute_tax_audit(self):
3813        separator = '        '
3814
3815        for record in self:
3816            currency = record.company_id.currency_id
3817            audit_str = ''
3818            for tag in record.tax_tag_ids:
3819
3820                if record.move_id.tax_cash_basis_rec_id:
3821                    # Cash basis entries are always treated as misc operations, applying the tag sign directly to the balance
3822                    type_multiplicator = 1
3823                else:
3824                    type_multiplicator = (record.journal_id.type == 'sale' and -1 or 1) * (self._get_refund_tax_audit_condition(record) and -1 or 1)
3825
3826                tag_amount = type_multiplicator * (tag.tax_negate and -1 or 1) * record.balance
3827
3828                if tag.tax_report_line_ids:
3829                    #Then, the tag comes from a report line, and hence has a + or - sign (also in its name)
3830                    for report_line in tag.tax_report_line_ids:
3831                        audit_str += separator if audit_str else ''
3832                        audit_str += report_line.tag_name + ': ' + formatLang(self.env, tag_amount, currency_obj=currency)
3833                else:
3834                    # Then, it's a financial tag (sign is always +, and never shown in tag name)
3835                    audit_str += separator if audit_str else ''
3836                    audit_str += tag.name + ': ' + formatLang(self.env, tag_amount, currency_obj=currency)
3837
3838            record.tax_audit = audit_str
3839
3840    def _get_refund_tax_audit_condition(self, aml):
3841        """ Returns the condition to be used for the provided move line to tell
3842        whether or not it comes from a refund operation.
3843        This is overridden by pos in order to treat returns properly.
3844        """
3845        return aml.move_id.move_type in ('in_refund', 'out_refund')
3846
3847    # -------------------------------------------------------------------------
3848    # CONSTRAINT METHODS
3849    # -------------------------------------------------------------------------
3850
3851    @api.constrains('account_id', 'journal_id')
3852    def _check_constrains_account_id_journal_id(self):
3853        for line in self.filtered(lambda x: x.display_type not in ('line_section', 'line_note')):
3854            account = line.account_id
3855            journal = line.move_id.journal_id
3856
3857            if account.deprecated:
3858                raise UserError(_('The account %s (%s) is deprecated.') % (account.name, account.code))
3859
3860            account_currency = account.currency_id
3861            if account_currency and account_currency != line.company_currency_id and account_currency != line.currency_id:
3862                raise UserError(_('The account selected on your journal entry forces to provide a secondary currency. You should remove the secondary currency on the account.'))
3863
3864            if account.allowed_journal_ids and journal not in account.allowed_journal_ids:
3865                raise UserError(_('You cannot use this account (%s) in this journal, check the field \'Allowed Journals\' on the related account.', account.display_name))
3866
3867            failed_check = False
3868            if (journal.type_control_ids - journal.default_account_id.user_type_id) or journal.account_control_ids:
3869                failed_check = True
3870                if journal.type_control_ids:
3871                    failed_check = account.user_type_id not in (journal.type_control_ids - journal.default_account_id.user_type_id)
3872                if failed_check and journal.account_control_ids:
3873                    failed_check = account not in journal.account_control_ids
3874
3875            if failed_check:
3876                raise UserError(_('You cannot use this account (%s) in this journal, check the section \'Control-Access\' under tab \'Advanced Settings\' on the related journal.', account.display_name))
3877
3878    @api.constrains('account_id', 'tax_ids', 'tax_line_id', 'reconciled')
3879    def _check_off_balance(self):
3880        for line in self:
3881            if line.account_id.internal_group == 'off_balance':
3882                if any(a.internal_group != line.account_id.internal_group for a in line.move_id.line_ids.account_id):
3883                    raise UserError(_('If you want to use "Off-Balance Sheet" accounts, all the accounts of the journal entry must be of this type'))
3884                if line.tax_ids or line.tax_line_id:
3885                    raise UserError(_('You cannot use taxes on lines with an Off-Balance account'))
3886                if line.reconciled:
3887                    raise UserError(_('Lines from "Off-Balance Sheet" accounts cannot be reconciled'))
3888
3889    def _affect_tax_report(self):
3890        self.ensure_one()
3891        return self.tax_ids or self.tax_line_id or self.tax_tag_ids.filtered(lambda x: x.applicability == "taxes")
3892
3893    def _check_tax_lock_date(self):
3894        for line in self.filtered(lambda l: l.move_id.state == 'posted'):
3895            move = line.move_id
3896            if move.company_id.tax_lock_date and move.date <= move.company_id.tax_lock_date and line._affect_tax_report():
3897                raise UserError(_("The operation is refused as it would impact an already issued tax statement. "
3898                                  "Please change the journal entry date or the tax lock date set in the settings (%s) to proceed.")
3899                                % format_date(self.env, move.company_id.tax_lock_date))
3900
3901    def _check_reconciliation(self):
3902        for line in self:
3903            if line.matched_debit_ids or line.matched_credit_ids:
3904                raise UserError(_("You cannot do this modification on a reconciled journal entry. "
3905                                  "You can just change some non legal fields or you must unreconcile first.\n"
3906                                  "Journal Entry (id): %s (%s)") % (line.move_id.name, line.move_id.id))
3907
3908    # -------------------------------------------------------------------------
3909    # LOW-LEVEL METHODS
3910    # -------------------------------------------------------------------------
3911
3912    def init(self):
3913        """ change index on partner_id to a multi-column index on (partner_id, ref), the new index will behave in the
3914            same way when we search on partner_id, with the addition of being optimal when having a query that will
3915            search on partner_id and ref at the same time (which is the case when we open the bank reconciliation widget)
3916        """
3917        cr = self._cr
3918        cr.execute('DROP INDEX IF EXISTS account_move_line_partner_id_index')
3919        cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('account_move_line_partner_id_ref_idx',))
3920        if not cr.fetchone():
3921            cr.execute('CREATE INDEX account_move_line_partner_id_ref_idx ON account_move_line (partner_id, ref)')
3922
3923    @api.model_create_multi
3924    def create(self, vals_list):
3925        # OVERRIDE
3926        ACCOUNTING_FIELDS = ('debit', 'credit', 'amount_currency')
3927        BUSINESS_FIELDS = ('price_unit', 'quantity', 'discount', 'tax_ids')
3928
3929        for vals in vals_list:
3930            move = self.env['account.move'].browse(vals['move_id'])
3931            vals.setdefault('company_currency_id', move.company_id.currency_id.id) # important to bypass the ORM limitation where monetary fields are not rounded; more info in the commit message
3932
3933            # Ensure balance == amount_currency in case of missing currency or same currency as the one from the
3934            # company.
3935            currency_id = vals.get('currency_id') or move.company_id.currency_id.id
3936            if currency_id == move.company_id.currency_id.id:
3937                balance = vals.get('debit', 0.0) - vals.get('credit', 0.0)
3938                vals.update({
3939                    'currency_id': currency_id,
3940                    'amount_currency': balance,
3941                })
3942            else:
3943                vals['amount_currency'] = vals.get('amount_currency', 0.0)
3944
3945            if move.is_invoice(include_receipts=True):
3946                currency = move.currency_id
3947                partner = self.env['res.partner'].browse(vals.get('partner_id'))
3948                taxes = self.new({'tax_ids': vals.get('tax_ids', [])}).tax_ids
3949                tax_ids = set(taxes.ids)
3950                taxes = self.env['account.tax'].browse(tax_ids)
3951
3952                # Ensure consistency between accounting & business fields.
3953                # As we can't express such synchronization as computed fields without cycling, we need to do it both
3954                # in onchange and in create/write. So, if something changed in accounting [resp. business] fields,
3955                # business [resp. accounting] fields are recomputed.
3956                if any(vals.get(field) for field in ACCOUNTING_FIELDS):
3957                    price_subtotal = self._get_price_total_and_subtotal_model(
3958                        vals.get('price_unit', 0.0),
3959                        vals.get('quantity', 0.0),
3960                        vals.get('discount', 0.0),
3961                        currency,
3962                        self.env['product.product'].browse(vals.get('product_id')),
3963                        partner,
3964                        taxes,
3965                        move.move_type,
3966                    ).get('price_subtotal', 0.0)
3967                    vals.update(self._get_fields_onchange_balance_model(
3968                        vals.get('quantity', 0.0),
3969                        vals.get('discount', 0.0),
3970                        vals['amount_currency'],
3971                        move.move_type,
3972                        currency,
3973                        taxes,
3974                        price_subtotal
3975                    ))
3976                    vals.update(self._get_price_total_and_subtotal_model(
3977                        vals.get('price_unit', 0.0),
3978                        vals.get('quantity', 0.0),
3979                        vals.get('discount', 0.0),
3980                        currency,
3981                        self.env['product.product'].browse(vals.get('product_id')),
3982                        partner,
3983                        taxes,
3984                        move.move_type,
3985                    ))
3986                elif any(vals.get(field) for field in BUSINESS_FIELDS):
3987                    vals.update(self._get_price_total_and_subtotal_model(
3988                        vals.get('price_unit', 0.0),
3989                        vals.get('quantity', 0.0),
3990                        vals.get('discount', 0.0),
3991                        currency,
3992                        self.env['product.product'].browse(vals.get('product_id')),
3993                        partner,
3994                        taxes,
3995                        move.move_type,
3996                    ))
3997                    vals.update(self._get_fields_onchange_subtotal_model(
3998                        vals['price_subtotal'],
3999                        move.move_type,
4000                        currency,
4001                        move.company_id,
4002                        move.date,
4003                    ))
4004
4005        lines = super(AccountMoveLine, self).create(vals_list)
4006
4007        moves = lines.mapped('move_id')
4008        if self._context.get('check_move_validity', True):
4009            moves._check_balanced()
4010        moves._check_fiscalyear_lock_date()
4011        lines._check_tax_lock_date()
4012        moves._synchronize_business_models({'line_ids'})
4013
4014        return lines
4015
4016    def write(self, vals):
4017        # OVERRIDE
4018        ACCOUNTING_FIELDS = ('debit', 'credit', 'amount_currency')
4019        BUSINESS_FIELDS = ('price_unit', 'quantity', 'discount', 'tax_ids')
4020        PROTECTED_FIELDS_TAX_LOCK_DATE = ['debit', 'credit', 'tax_line_id', 'tax_ids', 'tax_tag_ids']
4021        PROTECTED_FIELDS_LOCK_DATE = PROTECTED_FIELDS_TAX_LOCK_DATE + ['account_id', 'journal_id', 'amount_currency', 'currency_id', 'partner_id']
4022        PROTECTED_FIELDS_RECONCILIATION = ('account_id', 'date', 'debit', 'credit', 'amount_currency', 'currency_id')
4023
4024        account_to_write = self.env['account.account'].browse(vals['account_id']) if 'account_id' in vals else None
4025
4026        # Check writing a deprecated account.
4027        if account_to_write and account_to_write.deprecated:
4028            raise UserError(_('You cannot use a deprecated account.'))
4029
4030        for line in self:
4031            if line.parent_state == 'posted':
4032                if line.move_id.restrict_mode_hash_table and set(vals).intersection(INTEGRITY_HASH_LINE_FIELDS):
4033                    raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_LINE_FIELDS))
4034                if any(key in vals for key in ('tax_ids', 'tax_line_ids')):
4035                    raise UserError(_('You cannot modify the taxes related to a posted journal item, you should reset the journal entry to draft to do so.'))
4036
4037            # Check the lock date.
4038            if any(self.env['account.move']._field_will_change(line, vals, field_name) for field_name in PROTECTED_FIELDS_LOCK_DATE):
4039                line.move_id._check_fiscalyear_lock_date()
4040
4041            # Check the tax lock date.
4042            if any(self.env['account.move']._field_will_change(line, vals, field_name) for field_name in PROTECTED_FIELDS_TAX_LOCK_DATE):
4043                line._check_tax_lock_date()
4044
4045            # Check the reconciliation.
4046            if any(self.env['account.move']._field_will_change(line, vals, field_name) for field_name in PROTECTED_FIELDS_RECONCILIATION):
4047                line._check_reconciliation()
4048
4049            # Check switching receivable / payable accounts.
4050            if account_to_write:
4051                account_type = line.account_id.user_type_id.type
4052                if line.move_id.is_sale_document(include_receipts=True):
4053                    if (account_type == 'receivable' and account_to_write.user_type_id.type != account_type) \
4054                            or (account_type != 'receivable' and account_to_write.user_type_id.type == 'receivable'):
4055                        raise UserError(_("You can only set an account having the receivable type on payment terms lines for customer invoice."))
4056                if line.move_id.is_purchase_document(include_receipts=True):
4057                    if (account_type == 'payable' and account_to_write.user_type_id.type != account_type) \
4058                            or (account_type != 'payable' and account_to_write.user_type_id.type == 'payable'):
4059                        raise UserError(_("You can only set an account having the payable type on payment terms lines for vendor bill."))
4060
4061        # Tracking stuff can be skipped for perfs using tracking_disable context key
4062        if not self.env.context.get('tracking_disable', False):
4063            # Get all tracked fields (without related fields because these fields must be manage on their own model)
4064            tracking_fields = []
4065            for value in vals:
4066                field = self._fields[value]
4067                if hasattr(field, 'related') and field.related:
4068                    continue # We don't want to track related field.
4069                if hasattr(field, 'tracking') and field.tracking:
4070                    tracking_fields.append(value)
4071            ref_fields = self.env['account.move.line'].fields_get(tracking_fields)
4072
4073            # Get initial values for each line
4074            move_initial_values = {}
4075            for line in self.filtered(lambda l: l.move_id.posted_before): # Only lines with posted once move.
4076                for field in tracking_fields:
4077                    # Group initial values by move_id
4078                    if line.move_id.id not in move_initial_values:
4079                        move_initial_values[line.move_id.id] = {}
4080                    move_initial_values[line.move_id.id].update({field: line[field]})
4081
4082        result = True
4083        for line in self:
4084            cleaned_vals = line.move_id._cleanup_write_orm_values(line, vals)
4085            if not cleaned_vals:
4086                continue
4087
4088            # Auto-fill amount_currency if working in single-currency.
4089            if 'currency_id' not in cleaned_vals \
4090                and line.currency_id == line.company_currency_id \
4091                and any(field_name in cleaned_vals for field_name in ('debit', 'credit')):
4092                cleaned_vals.update({
4093                    'amount_currency': vals.get('debit', 0.0) - vals.get('credit', 0.0),
4094                })
4095
4096            result |= super(AccountMoveLine, line).write(cleaned_vals)
4097
4098            if not line.move_id.is_invoice(include_receipts=True):
4099                continue
4100
4101            # Ensure consistency between accounting & business fields.
4102            # As we can't express such synchronization as computed fields without cycling, we need to do it both
4103            # in onchange and in create/write. So, if something changed in accounting [resp. business] fields,
4104            # business [resp. accounting] fields are recomputed.
4105            if any(field in cleaned_vals for field in ACCOUNTING_FIELDS):
4106                price_subtotal = line._get_price_total_and_subtotal().get('price_subtotal', 0.0)
4107                to_write = line._get_fields_onchange_balance(price_subtotal=price_subtotal)
4108                to_write.update(line._get_price_total_and_subtotal(
4109                    price_unit=to_write.get('price_unit', line.price_unit),
4110                    quantity=to_write.get('quantity', line.quantity),
4111                    discount=to_write.get('discount', line.discount),
4112                ))
4113                result |= super(AccountMoveLine, line).write(to_write)
4114            elif any(field in cleaned_vals for field in BUSINESS_FIELDS):
4115                to_write = line._get_price_total_and_subtotal()
4116                to_write.update(line._get_fields_onchange_subtotal(
4117                    price_subtotal=to_write['price_subtotal'],
4118                ))
4119                result |= super(AccountMoveLine, line).write(to_write)
4120
4121        # Check total_debit == total_credit in the related moves.
4122        if self._context.get('check_move_validity', True):
4123            self.mapped('move_id')._check_balanced()
4124
4125        self.mapped('move_id')._synchronize_business_models({'line_ids'})
4126
4127        if not self.env.context.get('tracking_disable', False):
4128            # Create the dict for the message post
4129            tracking_values = {}  # Tracking values to write in the message post
4130            for move_id, modified_lines in move_initial_values.items():
4131                tmp_move = {move_id: []}
4132                for line in self.filtered(lambda l: l.move_id.id == move_id):
4133                    changes, tracking_value_ids = line._mail_track(ref_fields, modified_lines)  # Return a tuple like (changed field, ORM command)
4134                    tmp = {'line_id': line.id}
4135                    if tracking_value_ids:
4136                        selected_field = tracking_value_ids[0][2]  # Get the last element of the tuple in the list of ORM command. (changed, [(0, 0, THIS)])
4137                        tmp.update({
4138                            **{'field_name': selected_field.get('field_desc')},
4139                            **self._get_formated_values(selected_field)
4140                        })
4141                    elif changes:
4142                        field_name = line._fields[changes.pop()].string  # Get the field name
4143                        tmp.update({
4144                            'error': True,
4145                            'field_error': field_name
4146                        })
4147                    else:
4148                        continue
4149                    tmp_move[move_id].append(tmp)
4150                if len(tmp_move[move_id]) > 0:
4151                    tracking_values.update(tmp_move)
4152
4153            # Write in the chatter.
4154            for move in self.mapped('move_id'):
4155                fields = tracking_values.get(move.id, [])
4156                if len(fields) > 0:
4157                    msg = self._get_tracking_field_string(tracking_values.get(move.id))
4158                    move.message_post(body=msg)  # Write for each concerned move the message in the chatter
4159
4160        return result
4161
4162    def _valid_field_parameter(self, field, name):
4163        # I can't even
4164        return name == 'tracking' or super()._valid_field_parameter(field, name)
4165
4166    def unlink(self):
4167        moves = self.mapped('move_id')
4168
4169        # Prevent deleting lines on posted entries
4170        if not self.env.context.get('force_delete', False) and any(m.state == 'posted' for m in moves):
4171            raise UserError(_('You cannot delete an item linked to a posted entry.'))
4172
4173        # Check the lines are not reconciled (partially or not).
4174        self._check_reconciliation()
4175
4176        # Check the lock date.
4177        moves._check_fiscalyear_lock_date()
4178
4179        # Check the tax lock date.
4180        self._check_tax_lock_date()
4181
4182        res = super(AccountMoveLine, self).unlink()
4183
4184        # Check total_debit == total_credit in the related moves.
4185        if self._context.get('check_move_validity', True):
4186            moves._check_balanced()
4187
4188        return res
4189
4190    @api.model
4191    def default_get(self, default_fields):
4192        # OVERRIDE
4193        values = super(AccountMoveLine, self).default_get(default_fields)
4194
4195        if 'account_id' in default_fields and not values.get('account_id') \
4196            and (self._context.get('journal_id') or self._context.get('default_journal_id')) \
4197            and self._context.get('default_move_type') in ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'):
4198            # Fill missing 'account_id'.
4199            journal = self.env['account.journal'].browse(self._context.get('default_journal_id') or self._context['journal_id'])
4200            values['account_id'] = journal.default_account_id.id
4201        elif self._context.get('line_ids') and any(field_name in default_fields for field_name in ('debit', 'credit', 'account_id', 'partner_id')):
4202            move = self.env['account.move'].new({'line_ids': self._context['line_ids']})
4203
4204            # Suggest default value for debit / credit to balance the journal entry.
4205            balance = sum(line['debit'] - line['credit'] for line in move.line_ids)
4206            # if we are here, line_ids is in context, so journal_id should also be.
4207            journal = self.env['account.journal'].browse(self._context.get('default_journal_id') or self._context['journal_id'])
4208            currency = journal.exists() and journal.company_id.currency_id
4209            if currency:
4210                balance = currency.round(balance)
4211            if balance < 0.0:
4212                values.update({'debit': -balance})
4213            if balance > 0.0:
4214                values.update({'credit': balance})
4215
4216            # Suggest default value for 'partner_id'.
4217            if 'partner_id' in default_fields and not values.get('partner_id'):
4218                if len(move.line_ids[-2:]) == 2 and  move.line_ids[-1].partner_id == move.line_ids[-2].partner_id != False:
4219                    values['partner_id'] = move.line_ids[-2:].mapped('partner_id').id
4220
4221            # Suggest default value for 'account_id'.
4222            if 'account_id' in default_fields and not values.get('account_id'):
4223                if len(move.line_ids[-2:]) == 2 and  move.line_ids[-1].account_id == move.line_ids[-2].account_id != False:
4224                    values['account_id'] = move.line_ids[-2:].mapped('account_id').id
4225        if values.get('display_type') or self.display_type:
4226            values.pop('account_id', None)
4227        return values
4228
4229    @api.depends('ref', 'move_id')
4230    def name_get(self):
4231        result = []
4232        for line in self:
4233            name = line.move_id.name or ''
4234            if line.ref:
4235                name += " (%s)" % line.ref
4236            name += (line.name or line.product_id.display_name) and (' ' + (line.name or line.product_id.display_name)) or ''
4237            result.append((line.id, name))
4238        return result
4239
4240    # -------------------------------------------------------------------------
4241    # TRACKING METHODS
4242    # -------------------------------------------------------------------------
4243
4244    def _get_formated_values(self, tracked_field):
4245        if tracked_field.get('field_type') in ('date', 'datetime'):
4246            return {
4247                'old_value': format_date(self.env, fields.Datetime.from_string(tracked_field.get('old_value_datetime'))),
4248                'new_value': format_date(self.env, fields.Datetime.from_string(tracked_field.get('new_value_datetime'))),
4249            }
4250        elif tracked_field.get('field_type') in ('one2many', 'many2many', 'many2one'):
4251            return {
4252                'old_value': tracked_field.get('old_value_char', ''),
4253                'new_value': tracked_field.get('new_value_char', '')
4254            }
4255        else:
4256            return {
4257                'old_value': [val for key, val in tracked_field.items() if 'old_value' in key][0], # Get the first element because we create a list like ['Elem']
4258                'new_value': [val for key, val in tracked_field.items() if 'new_value' in key][0], # Get the first element because we create a list like ['Elem']
4259            }
4260
4261    def _get_tracking_field_string(self, fields):
4262        ARROW_RIGHT = '<span aria-label="Changed" class="fa fa-long-arrow-alt-right" role="img" title="Changed"></span>'
4263        msg = '<ul>'
4264        for field in fields:
4265            redirect_link = '<a href=# data-oe-model=account.move.line data-oe-id=%d>#%d</a>' % (field['line_id'], field['line_id']) # Account move line link
4266            if field.get('error', False):
4267                msg += '<li>%s: %s</li>' % (
4268                    field['field_error'],
4269                    _('A modification has been operated on the line %s.', redirect_link)
4270                )
4271            else:
4272                msg += '<li>%s: %s %s %s (%s)</li>' % (field['field_name'], field['old_value'], ARROW_RIGHT, field['new_value'], redirect_link)
4273        msg += '</ul>'
4274        return msg
4275
4276    # -------------------------------------------------------------------------
4277    # RECONCILIATION
4278    # -------------------------------------------------------------------------
4279
4280    def _prepare_reconciliation_partials(self):
4281        ''' Prepare the partials on the current journal items to perform the reconciliation.
4282        /!\ The order of records in self is important because the journal items will be reconciled using this order.
4283
4284        :return: A recordset of account.partial.reconcile.
4285        '''
4286        debit_lines = iter(self.filtered(lambda line: line.balance > 0.0 or line.amount_currency > 0.0))
4287        credit_lines = iter(self.filtered(lambda line: line.balance < 0.0 or line.amount_currency < 0.0))
4288        debit_line = None
4289        credit_line = None
4290
4291        debit_amount_residual = 0.0
4292        debit_amount_residual_currency = 0.0
4293        credit_amount_residual = 0.0
4294        credit_amount_residual_currency = 0.0
4295        debit_line_currency = None
4296        credit_line_currency = None
4297
4298        partials_vals_list = []
4299
4300        while True:
4301
4302            # Move to the next available debit line.
4303            if not debit_line:
4304                debit_line = next(debit_lines, None)
4305                if not debit_line:
4306                    break
4307                debit_amount_residual = debit_line.amount_residual
4308
4309                if debit_line.currency_id:
4310                    debit_amount_residual_currency = debit_line.amount_residual_currency
4311                    debit_line_currency = debit_line.currency_id
4312                else:
4313                    debit_amount_residual_currency = debit_amount_residual
4314                    debit_line_currency = debit_line.company_currency_id
4315
4316            # Move to the next available credit line.
4317            if not credit_line:
4318                credit_line = next(credit_lines, None)
4319                if not credit_line:
4320                    break
4321                credit_amount_residual = credit_line.amount_residual
4322
4323                if credit_line.currency_id:
4324                    credit_amount_residual_currency = credit_line.amount_residual_currency
4325                    credit_line_currency = credit_line.currency_id
4326                else:
4327                    credit_amount_residual_currency = credit_amount_residual
4328                    credit_line_currency = credit_line.company_currency_id
4329
4330            min_amount_residual = min(debit_amount_residual, -credit_amount_residual)
4331            has_debit_residual_left = not debit_line.company_currency_id.is_zero(debit_amount_residual) and debit_amount_residual > 0.0
4332            has_credit_residual_left = not credit_line.company_currency_id.is_zero(credit_amount_residual) and credit_amount_residual < 0.0
4333            has_debit_residual_curr_left = not debit_line_currency.is_zero(debit_amount_residual_currency) and debit_amount_residual_currency > 0.0
4334            has_credit_residual_curr_left = not credit_line_currency.is_zero(credit_amount_residual_currency) and credit_amount_residual_currency < 0.0
4335
4336            if debit_line_currency == credit_line_currency:
4337                # Reconcile on the same currency.
4338
4339                # The debit line is now fully reconciled because:
4340                # - either amount_residual & amount_residual_currency are at 0.
4341                # - either the credit_line is not an exchange difference one.
4342                if not has_debit_residual_curr_left and (has_credit_residual_curr_left or not has_debit_residual_left):
4343                    debit_line = None
4344                    continue
4345
4346                # The credit line is now fully reconciled because:
4347                # - either amount_residual & amount_residual_currency are at 0.
4348                # - either the debit is not an exchange difference one.
4349                if not has_credit_residual_curr_left and (has_debit_residual_curr_left or not has_credit_residual_left):
4350                    credit_line = None
4351                    continue
4352
4353                min_amount_residual_currency = min(debit_amount_residual_currency, -credit_amount_residual_currency)
4354                min_debit_amount_residual_currency = min_amount_residual_currency
4355                min_credit_amount_residual_currency = min_amount_residual_currency
4356
4357            else:
4358                # Reconcile on the company's currency.
4359
4360                # The debit line is now fully reconciled since amount_residual is 0.
4361                if not has_debit_residual_left:
4362                    debit_line = None
4363                    continue
4364
4365                # The credit line is now fully reconciled since amount_residual is 0.
4366                if not has_credit_residual_left:
4367                    credit_line = None
4368                    continue
4369
4370                min_debit_amount_residual_currency = credit_line.company_currency_id._convert(
4371                    min_amount_residual,
4372                    debit_line.currency_id,
4373                    credit_line.company_id,
4374                    credit_line.date,
4375                )
4376                min_credit_amount_residual_currency = debit_line.company_currency_id._convert(
4377                    min_amount_residual,
4378                    credit_line.currency_id,
4379                    debit_line.company_id,
4380                    debit_line.date,
4381                )
4382
4383            debit_amount_residual -= min_amount_residual
4384            debit_amount_residual_currency -= min_debit_amount_residual_currency
4385            credit_amount_residual += min_amount_residual
4386            credit_amount_residual_currency += min_credit_amount_residual_currency
4387
4388            partials_vals_list.append({
4389                'amount': min_amount_residual,
4390                'debit_amount_currency': min_debit_amount_residual_currency,
4391                'credit_amount_currency': min_credit_amount_residual_currency,
4392                'debit_move_id': debit_line.id,
4393                'credit_move_id': credit_line.id,
4394            })
4395
4396        return partials_vals_list
4397
4398    def _create_exchange_difference_move(self):
4399        ''' Create the exchange difference journal entry on the current journal items.
4400        :return: An account.move record.
4401        '''
4402
4403        def _add_lines_to_exchange_difference_vals(lines, exchange_diff_move_vals):
4404            ''' Generate the exchange difference values used to create the journal items
4405            in order to fix the residual amounts and add them into 'exchange_diff_move_vals'.
4406
4407            1) When reconciled on the same foreign currency, the journal items are
4408            fully reconciled regarding this currency but it could be not the case
4409            of the balance that is expressed using the company's currency. In that
4410            case, we need to create exchange difference journal items to ensure this
4411            residual amount reaches zero.
4412
4413            2) When reconciled on the company currency but having different foreign
4414            currencies, the journal items are fully reconciled regarding the company
4415            currency but it's not always the case for the foreign currencies. In that
4416            case, the exchange difference journal items are created to ensure this
4417            residual amount in foreign currency reaches zero.
4418
4419            :param lines:                   The account.move.lines to which fix the residual amounts.
4420            :param exchange_diff_move_vals: The current vals of the exchange difference journal entry.
4421            :return:                        A list of pair <line, sequence> to perform the reconciliation
4422                                            at the creation of the exchange difference move where 'line'
4423                                            is the account.move.line to which the 'sequence'-th exchange
4424                                            difference line will be reconciled with.
4425            '''
4426            journal = self.env['account.journal'].browse(exchange_diff_move_vals['journal_id'])
4427            to_reconcile = []
4428
4429            for line in lines:
4430
4431                exchange_diff_move_vals['date'] = max(exchange_diff_move_vals['date'], line.date)
4432
4433                if not line.company_currency_id.is_zero(line.amount_residual):
4434                    # amount_residual_currency == 0 and amount_residual has to be fixed.
4435
4436                    if line.amount_residual > 0.0:
4437                        exchange_line_account = journal.company_id.expense_currency_exchange_account_id
4438                    else:
4439                        exchange_line_account = journal.company_id.income_currency_exchange_account_id
4440
4441                elif line.currency_id and not line.currency_id.is_zero(line.amount_residual_currency):
4442                    # amount_residual == 0 and amount_residual_currency has to be fixed.
4443
4444                    if line.amount_residual_currency > 0.0:
4445                        exchange_line_account = journal.company_id.expense_currency_exchange_account_id
4446                    else:
4447                        exchange_line_account = journal.company_id.income_currency_exchange_account_id
4448                else:
4449                    continue
4450
4451                sequence = len(exchange_diff_move_vals['line_ids'])
4452                exchange_diff_move_vals['line_ids'] += [
4453                    (0, 0, {
4454                        'name': _('Currency exchange rate difference'),
4455                        'debit': -line.amount_residual if line.amount_residual < 0.0 else 0.0,
4456                        'credit': line.amount_residual if line.amount_residual > 0.0 else 0.0,
4457                        'amount_currency': -line.amount_residual_currency,
4458                        'account_id': line.account_id.id,
4459                        'currency_id': line.currency_id.id,
4460                        'partner_id': line.partner_id.id,
4461                        'sequence': sequence,
4462                    }),
4463                    (0, 0, {
4464                        'name': _('Currency exchange rate difference'),
4465                        'debit': line.amount_residual if line.amount_residual > 0.0 else 0.0,
4466                        'credit': -line.amount_residual if line.amount_residual < 0.0 else 0.0,
4467                        'amount_currency': line.amount_residual_currency,
4468                        'account_id': exchange_line_account.id,
4469                        'currency_id': line.currency_id.id,
4470                        'partner_id': line.partner_id.id,
4471                        'sequence': sequence + 1,
4472                    }),
4473                ]
4474
4475                to_reconcile.append((line, sequence))
4476
4477            return to_reconcile
4478
4479        def _add_cash_basis_lines_to_exchange_difference_vals(lines, exchange_diff_move_vals):
4480            ''' Generate the exchange difference values used to create the journal items
4481            in order to fix the cash basis lines using the transfer account in a multi-currencies
4482            environment when this account is not a reconcile one.
4483
4484            When the tax cash basis journal entries are generated and all involved
4485            transfer account set on taxes are all reconcilable, the account balance
4486            will be reset to zero by the exchange difference journal items generated
4487            above. However, this mechanism will not work if there is any transfer
4488            accounts that are not reconcile and we are generating the cash basis
4489            journal items in a foreign currency. In that specific case, we need to
4490            generate extra journal items at the generation of the exchange difference
4491            journal entry to ensure this balance is reset to zero and then, will not
4492            appear on the tax report leading to erroneous tax base amount / tax amount.
4493
4494            :param lines:                   The account.move.lines to which fix the residual amounts.
4495            :param exchange_diff_move_vals: The current vals of the exchange difference journal entry.
4496            '''
4497            for move in lines.move_id:
4498                account_vals_to_fix = {}
4499
4500                move_values = move._collect_tax_cash_basis_values()
4501
4502                # The cash basis doesn't need to be handle for this move because there is another payment term
4503                # line that is not yet fully paid.
4504                if not move_values or not move_values['is_fully_paid']:
4505                    continue
4506
4507                # ==========================================================================
4508                # Add the balance of all tax lines of the current move in order in order
4509                # to compute the residual amount for each of them.
4510                # ==========================================================================
4511
4512                for line in move_values['to_process_lines']:
4513
4514                    vals = {
4515                        'currency_id': line.currency_id.id,
4516                        'partner_id': line.partner_id.id,
4517                        'tax_ids': [(6, 0, line.tax_ids.ids)],
4518                        'tax_tag_ids': [(6, 0, line._convert_tags_for_cash_basis(line.tax_tag_ids).ids)],
4519                        'debit': line.debit,
4520                        'credit': line.credit,
4521                    }
4522
4523                    if line.tax_repartition_line_id:
4524                        # Tax line.
4525                        grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(line)
4526                        if grouping_key in account_vals_to_fix:
4527                            debit = account_vals_to_fix[grouping_key]['debit'] + vals['debit']
4528                            credit = account_vals_to_fix[grouping_key]['credit'] + vals['credit']
4529                            balance = debit - credit
4530
4531                            account_vals_to_fix[grouping_key].update({
4532                                'debit': balance if balance > 0 else 0,
4533                                'credit': -balance if balance < 0 else 0,
4534                                'tax_base_amount': account_vals_to_fix[grouping_key]['tax_base_amount'] + line.tax_base_amount,
4535                            })
4536                        else:
4537                            account_vals_to_fix[grouping_key] = {
4538                                **vals,
4539                                'account_id': line.account_id.id,
4540                                'tax_base_amount': line.tax_base_amount,
4541                                'tax_repartition_line_id': line.tax_repartition_line_id.id,
4542                            }
4543                    elif line.tax_ids:
4544                        # Base line.
4545                        account_to_fix = line.company_id.account_cash_basis_base_account_id
4546                        if not account_to_fix:
4547                            continue
4548
4549                        grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record(line, account=account_to_fix)
4550
4551                        if grouping_key not in account_vals_to_fix:
4552                            account_vals_to_fix[grouping_key] = {
4553                                **vals,
4554                                'account_id': account_to_fix.id,
4555                            }
4556                        else:
4557                            # Multiple base lines could share the same key, if the same
4558                            # cash basis tax is used alone on several lines of the invoices
4559                            account_vals_to_fix[grouping_key]['debit'] += vals['debit']
4560                            account_vals_to_fix[grouping_key]['credit'] += vals['credit']
4561
4562                # ==========================================================================
4563                # Subtract the balance of all previously generated cash basis journal entries
4564                # in order to retrieve the residual balance of each involved transfer account.
4565                # ==========================================================================
4566
4567                cash_basis_moves = self.env['account.move'].search([('tax_cash_basis_move_id', '=', move.id)])
4568                for line in cash_basis_moves.line_ids:
4569                    grouping_key = None
4570                    if line.tax_repartition_line_id:
4571                        # Tax line.
4572                        grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(
4573                            line,
4574                            account=line.tax_line_id.cash_basis_transition_account_id,
4575                        )
4576                    elif line.tax_ids:
4577                        # Base line.
4578                        grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record(
4579                            line,
4580                            account=line.company_id.account_cash_basis_base_account_id,
4581                        )
4582
4583                    if grouping_key not in account_vals_to_fix:
4584                        continue
4585
4586                    account_vals_to_fix[grouping_key]['debit'] -= line.debit
4587                    account_vals_to_fix[grouping_key]['credit'] -= line.credit
4588
4589                # ==========================================================================
4590                # Generate the exchange difference journal items:
4591                # - to reset the balance of all transfer account to zero.
4592                # - fix rounding issues on the tax account/base tax account.
4593                # ==========================================================================
4594
4595                for values in account_vals_to_fix.values():
4596                    balance = values['debit'] - values['credit']
4597
4598                    if move.company_currency_id.is_zero(balance):
4599                        continue
4600
4601                    if values.get('tax_repartition_line_id'):
4602                        # Tax line.
4603                        tax_repartition_line = self.env['account.tax.repartition.line'].browse(values['tax_repartition_line_id'])
4604                        account = tax_repartition_line.account_id or self.env['account.account'].browse(values['account_id'])
4605
4606                        sequence = len(exchange_diff_move_vals['line_ids'])
4607                        exchange_diff_move_vals['line_ids'] += [
4608                            (0, 0, {
4609                                **values,
4610                                'name': _('Currency exchange rate difference (cash basis)'),
4611                                'debit': balance if balance > 0.0 else 0.0,
4612                                'credit': -balance if balance < 0.0 else 0.0,
4613                                'account_id': account.id,
4614                                'sequence': sequence,
4615                            }),
4616                            (0, 0, {
4617                                **values,
4618                                'name': _('Currency exchange rate difference (cash basis)'),
4619                                'debit': -balance if balance < 0.0 else 0.0,
4620                                'credit': balance if balance > 0.0 else 0.0,
4621                                'account_id': values['account_id'],
4622                                'tax_ids': [],
4623                                'tax_tag_ids': [],
4624                                'tax_repartition_line_id': False,
4625                                'sequence': sequence + 1,
4626                            }),
4627                        ]
4628                    else:
4629                        # Base line.
4630                        sequence = len(exchange_diff_move_vals['line_ids'])
4631                        exchange_diff_move_vals['line_ids'] += [
4632                            (0, 0, {
4633                                **values,
4634                                'name': _('Currency exchange rate difference (cash basis)'),
4635                                'debit': balance if balance > 0.0 else 0.0,
4636                                'credit': -balance if balance < 0.0 else 0.0,
4637                                'sequence': sequence,
4638                            }),
4639                            (0, 0, {
4640                                **values,
4641                                'name': _('Currency exchange rate difference (cash basis)'),
4642                                'debit': -balance if balance < 0.0 else 0.0,
4643                                'credit': balance if balance > 0.0 else 0.0,
4644                                'tax_ids': [],
4645                                'tax_tag_ids': [],
4646                                'sequence': sequence + 1,
4647                            }),
4648                        ]
4649
4650        if not self:
4651            return self.env['account.move']
4652
4653        company = self[0].company_id
4654        journal = company.currency_exchange_journal_id
4655
4656        exchange_diff_move_vals = {
4657            'move_type': 'entry',
4658            'date': date.min,
4659            'journal_id': journal.id,
4660            'line_ids': [],
4661        }
4662
4663        # Fix residual amounts.
4664        to_reconcile = _add_lines_to_exchange_difference_vals(self, exchange_diff_move_vals)
4665
4666        # Fix cash basis entries.
4667        is_cash_basis_needed = self[0].account_internal_type in ('receivable', 'payable')
4668        if is_cash_basis_needed:
4669            _add_cash_basis_lines_to_exchange_difference_vals(self, exchange_diff_move_vals)
4670
4671        # ==========================================================================
4672        # Create move and reconcile.
4673        # ==========================================================================
4674
4675        if exchange_diff_move_vals['line_ids']:
4676            # Check the configuration of the exchange difference journal.
4677            if not journal:
4678                raise UserError(_("You should configure the 'Exchange Gain or Loss Journal' in your company settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
4679            if not journal.company_id.expense_currency_exchange_account_id:
4680                raise UserError(_("You should configure the 'Loss Exchange Rate Account' in your company settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
4681            if not journal.company_id.income_currency_exchange_account_id.id:
4682                raise UserError(_("You should configure the 'Gain Exchange Rate Account' in your company settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
4683
4684            exchange_diff_move_vals['date'] = max(exchange_diff_move_vals['date'], company._get_user_fiscal_lock_date())
4685
4686            exchange_move = self.env['account.move'].create(exchange_diff_move_vals)
4687        else:
4688            return None
4689
4690        # Reconcile lines to the newly created exchange difference journal entry by creating more partials.
4691        partials_vals_list = []
4692        for source_line, sequence in to_reconcile:
4693            exchange_diff_line = exchange_move.line_ids[sequence]
4694
4695            if source_line.company_currency_id.is_zero(source_line.amount_residual):
4696                exchange_field = 'amount_residual_currency'
4697            else:
4698                exchange_field = 'amount_residual'
4699
4700            if exchange_diff_line[exchange_field] > 0.0:
4701                debit_line = exchange_diff_line
4702                credit_line = source_line
4703            else:
4704                debit_line = source_line
4705                credit_line = exchange_diff_line
4706
4707            partials_vals_list.append({
4708                'amount': abs(source_line.amount_residual),
4709                'debit_amount_currency': abs(debit_line.amount_residual_currency),
4710                'credit_amount_currency': abs(credit_line.amount_residual_currency),
4711                'debit_move_id': debit_line.id,
4712                'credit_move_id': credit_line.id,
4713            })
4714
4715        self.env['account.partial.reconcile'].create(partials_vals_list)
4716
4717        return exchange_move
4718
4719    def reconcile(self):
4720        ''' Reconcile the current move lines all together.
4721        :return: A dictionary representing a summary of what has been done during the reconciliation:
4722                * partials:             A recorset of all account.partial.reconcile created during the reconciliation.
4723                * full_reconcile:       An account.full.reconcile record created when there is nothing left to reconcile
4724                                        in the involved lines.
4725                * tax_cash_basis_moves: An account.move recordset representing the tax cash basis journal entries.
4726        '''
4727        results = {}
4728
4729        if not self:
4730            return results
4731
4732        # List unpaid invoices
4733        not_paid_invoices = self.move_id.filtered(
4734            lambda move: move.is_invoice(include_receipts=True) and move.payment_state not in ('paid', 'in_payment')
4735        )
4736
4737        # ==== Check the lines can be reconciled together ====
4738        company = None
4739        account = None
4740        for line in self:
4741            if line.reconciled:
4742                raise UserError(_("You are trying to reconcile some entries that are already reconciled."))
4743            if not line.account_id.reconcile and line.account_id.internal_type != 'liquidity':
4744                raise UserError(_("Account %s does not allow reconciliation. First change the configuration of this account to allow it.")
4745                                % line.account_id.display_name)
4746            if line.move_id.state != 'posted':
4747                raise UserError(_('You can only reconcile posted entries.'))
4748            if company is None:
4749                company = line.company_id
4750            elif line.company_id != company:
4751                raise UserError(_("Entries doesn't belong to the same company: %s != %s")
4752                                % (company.display_name, line.company_id.display_name))
4753            if account is None:
4754                account = line.account_id
4755            elif line.account_id != account:
4756                raise UserError(_("Entries are not from the same account: %s != %s")
4757                                % (account.display_name, line.account_id.display_name))
4758
4759        sorted_lines = self.sorted(key=lambda line: (line.date_maturity or line.date, line.currency_id))
4760
4761        # ==== Collect all involved lines through the existing reconciliation ====
4762
4763        involved_lines = sorted_lines
4764        involved_partials = self.env['account.partial.reconcile']
4765        current_lines = involved_lines
4766        current_partials = involved_partials
4767        while current_lines:
4768            current_partials = (current_lines.matched_debit_ids + current_lines.matched_credit_ids) - current_partials
4769            involved_partials += current_partials
4770            current_lines = (current_partials.debit_move_id + current_partials.credit_move_id) - current_lines
4771            involved_lines += current_lines
4772
4773        # ==== Create partials ====
4774
4775        partials = self.env['account.partial.reconcile'].create(sorted_lines._prepare_reconciliation_partials())
4776
4777        # Track newly created partials.
4778        results['partials'] = partials
4779        involved_partials += partials
4780
4781        # ==== Create entries for cash basis taxes ====
4782
4783        is_cash_basis_needed = account.user_type_id.type in ('receivable', 'payable')
4784        if is_cash_basis_needed and not self._context.get('move_reverse_cancel'):
4785            tax_cash_basis_moves = partials._create_tax_cash_basis_moves()
4786            results['tax_cash_basis_moves'] = tax_cash_basis_moves
4787
4788        # ==== Check if a full reconcile is needed ====
4789
4790        if involved_lines[0].currency_id and all(line.currency_id == involved_lines[0].currency_id for line in involved_lines):
4791            is_full_needed = all(line.currency_id.is_zero(line.amount_residual_currency) for line in involved_lines)
4792        else:
4793            is_full_needed = all(line.company_currency_id.is_zero(line.amount_residual) for line in involved_lines)
4794
4795        if is_full_needed:
4796
4797            # ==== Create the exchange difference move ====
4798
4799            if self._context.get('no_exchange_difference'):
4800                exchange_move = None
4801            else:
4802                exchange_move = involved_lines._create_exchange_difference_move()
4803                if exchange_move:
4804                    exchange_move_lines = exchange_move.line_ids.filtered(lambda line: line.account_id == account)
4805
4806                    # Track newly created lines.
4807                    involved_lines += exchange_move_lines
4808
4809                    # Track newly created partials.
4810                    exchange_diff_partials = exchange_move_lines.matched_debit_ids \
4811                                             + exchange_move_lines.matched_credit_ids
4812                    involved_partials += exchange_diff_partials
4813                    results['partials'] += exchange_diff_partials
4814
4815                    exchange_move._post(soft=False)
4816
4817            # ==== Create the full reconcile ====
4818
4819            results['full_reconcile'] = self.env['account.full.reconcile'].create({
4820                'exchange_move_id': exchange_move and exchange_move.id,
4821                'partial_reconcile_ids': [(6, 0, involved_partials.ids)],
4822                'reconciled_line_ids': [(6, 0, involved_lines.ids)],
4823            })
4824
4825        # Trigger action for paid invoices
4826        not_paid_invoices\
4827            .filtered(lambda move: move.payment_state in ('paid', 'in_payment'))\
4828            .action_invoice_paid()
4829
4830        return results
4831
4832    def remove_move_reconcile(self):
4833        """ Undo a reconciliation """
4834        (self.matched_debit_ids + self.matched_credit_ids).unlink()
4835
4836    def _copy_data_extend_business_fields(self, values):
4837        ''' Hook allowing copying business fields under certain conditions.
4838        E.g. The link to the sale order lines must be preserved in case of a refund.
4839        '''
4840        self.ensure_one()
4841
4842    def copy_data(self, default=None):
4843        res = super(AccountMoveLine, self).copy_data(default=default)
4844
4845        for line, values in zip(self, res):
4846            # Don't copy the name of a payment term line.
4847            if line.move_id.is_invoice() and line.account_id.user_type_id.type in ('receivable', 'payable'):
4848                values['name'] = ''
4849            # Don't copy restricted fields of notes
4850            if line.display_type in ('line_section', 'line_note'):
4851                values['amount_currency'] = 0
4852                values['debit'] = 0
4853                values['credit'] = 0
4854                values['account_id'] = False
4855            if self._context.get('include_business_fields'):
4856                line._copy_data_extend_business_fields(values)
4857        return res
4858
4859    # -------------------------------------------------------------------------
4860    # MISC
4861    # -------------------------------------------------------------------------
4862
4863    def _get_analytic_tag_ids(self):
4864        self.ensure_one()
4865        return self.analytic_tag_ids.filtered(lambda r: not r.active_analytic_distribution).ids
4866
4867    def create_analytic_lines(self):
4868        """ Create analytic items upon validation of an account.move.line having an analytic account or an analytic distribution.
4869        """
4870        lines_to_create_analytic_entries = self.env['account.move.line']
4871        analytic_line_vals = []
4872        for obj_line in self:
4873            for tag in obj_line.analytic_tag_ids.filtered('active_analytic_distribution'):
4874                for distribution in tag.analytic_distribution_ids:
4875                    analytic_line_vals.append(obj_line._prepare_analytic_distribution_line(distribution))
4876            if obj_line.analytic_account_id:
4877                lines_to_create_analytic_entries |= obj_line
4878
4879        # create analytic entries in batch
4880        if lines_to_create_analytic_entries:
4881            analytic_line_vals += lines_to_create_analytic_entries._prepare_analytic_line()
4882
4883        self.env['account.analytic.line'].create(analytic_line_vals)
4884
4885    def _prepare_analytic_line(self):
4886        """ Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having
4887            an analytic account. This method is intended to be extended in other modules.
4888            :return list of values to create analytic.line
4889            :rtype list
4890        """
4891        result = []
4892        for move_line in self:
4893            amount = (move_line.credit or 0.0) - (move_line.debit or 0.0)
4894            default_name = move_line.name or (move_line.ref or '/' + ' -- ' + (move_line.partner_id and move_line.partner_id.name or '/'))
4895            result.append({
4896                'name': default_name,
4897                'date': move_line.date,
4898                'account_id': move_line.analytic_account_id.id,
4899                'group_id': move_line.analytic_account_id.group_id.id,
4900                'tag_ids': [(6, 0, move_line._get_analytic_tag_ids())],
4901                'unit_amount': move_line.quantity,
4902                'product_id': move_line.product_id and move_line.product_id.id or False,
4903                'product_uom_id': move_line.product_uom_id and move_line.product_uom_id.id or False,
4904                'amount': amount,
4905                'general_account_id': move_line.account_id.id,
4906                'ref': move_line.ref,
4907                'move_id': move_line.id,
4908                'user_id': move_line.move_id.invoice_user_id.id or self._uid,
4909                'partner_id': move_line.partner_id.id,
4910                'company_id': move_line.analytic_account_id.company_id.id or move_line.move_id.company_id.id,
4911            })
4912        return result
4913
4914    def _prepare_analytic_distribution_line(self, distribution):
4915        """ Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having
4916            analytic tags with analytic distribution.
4917        """
4918        self.ensure_one()
4919        amount = -self.balance * distribution.percentage / 100.0
4920        default_name = self.name or (self.ref or '/' + ' -- ' + (self.partner_id and self.partner_id.name or '/'))
4921        return {
4922            'name': default_name,
4923            'date': self.date,
4924            'account_id': distribution.account_id.id,
4925            'group_id': distribution.account_id.group_id.id,
4926            'partner_id': self.partner_id.id,
4927            'tag_ids': [(6, 0, [distribution.tag_id.id] + self._get_analytic_tag_ids())],
4928            'unit_amount': self.quantity,
4929            'product_id': self.product_id and self.product_id.id or False,
4930            'product_uom_id': self.product_uom_id and self.product_uom_id.id or False,
4931            'amount': amount,
4932            'general_account_id': self.account_id.id,
4933            'ref': self.ref,
4934            'move_id': self.id,
4935            'user_id': self.move_id.invoice_user_id.id or self._uid,
4936            'company_id': distribution.account_id.company_id.id or self.env.company.id,
4937        }
4938
4939    @api.model
4940    def _query_get(self, domain=None):
4941        self.check_access_rights('read')
4942
4943        context = dict(self._context or {})
4944        domain = domain or []
4945        if not isinstance(domain, (list, tuple)):
4946            domain = ast.literal_eval(domain)
4947
4948        date_field = 'date'
4949        if context.get('aged_balance'):
4950            date_field = 'date_maturity'
4951        if context.get('date_to'):
4952            domain += [(date_field, '<=', context['date_to'])]
4953        if context.get('date_from'):
4954            if not context.get('strict_range'):
4955                domain += ['|', (date_field, '>=', context['date_from']), ('account_id.user_type_id.include_initial_balance', '=', True)]
4956            elif context.get('initial_bal'):
4957                domain += [(date_field, '<', context['date_from'])]
4958            else:
4959                domain += [(date_field, '>=', context['date_from'])]
4960
4961        if context.get('journal_ids'):
4962            domain += [('journal_id', 'in', context['journal_ids'])]
4963
4964        state = context.get('state')
4965        if state and state.lower() != 'all':
4966            domain += [('move_id.state', '=', state)]
4967
4968        if context.get('company_id'):
4969            domain += [('company_id', '=', context['company_id'])]
4970        elif context.get('allowed_company_ids'):
4971            domain += [('company_id', 'in', self.env.companies.ids)]
4972        else:
4973            domain += [('company_id', '=', self.env.company.id)]
4974
4975        if context.get('reconcile_date'):
4976            domain += ['|', ('reconciled', '=', False), '|', ('matched_debit_ids.max_date', '>', context['reconcile_date']), ('matched_credit_ids.max_date', '>', context['reconcile_date'])]
4977
4978        if context.get('account_tag_ids'):
4979            domain += [('account_id.tag_ids', 'in', context['account_tag_ids'].ids)]
4980
4981        if context.get('account_ids'):
4982            domain += [('account_id', 'in', context['account_ids'].ids)]
4983
4984        if context.get('analytic_tag_ids'):
4985            domain += [('analytic_tag_ids', 'in', context['analytic_tag_ids'].ids)]
4986
4987        if context.get('analytic_account_ids'):
4988            domain += [('analytic_account_id', 'in', context['analytic_account_ids'].ids)]
4989
4990        if context.get('partner_ids'):
4991            domain += [('partner_id', 'in', context['partner_ids'].ids)]
4992
4993        if context.get('partner_categories'):
4994            domain += [('partner_id.category_id', 'in', context['partner_categories'].ids)]
4995
4996        where_clause = ""
4997        where_clause_params = []
4998        tables = ''
4999        if domain:
5000            domain.append(('display_type', 'not in', ('line_section', 'line_note')))
5001            domain.append(('move_id.state', '!=', 'cancel'))
5002
5003            query = self._where_calc(domain)
5004
5005            # Wrap the query with 'company_id IN (...)' to avoid bypassing company access rights.
5006            self._apply_ir_rules(query)
5007
5008            tables, where_clause, where_clause_params = query.get_sql()
5009        return tables, where_clause, where_clause_params
5010
5011    def _reconciled_lines(self):
5012        ids = []
5013        for aml in self.filtered('account_id.reconcile'):
5014            ids.extend([r.debit_move_id.id for r in aml.matched_debit_ids] if aml.credit > 0 else [r.credit_move_id.id for r in aml.matched_credit_ids])
5015            ids.append(aml.id)
5016        return ids
5017
5018    def open_reconcile_view(self):
5019        action = self.env['ir.actions.act_window']._for_xml_id('account.action_account_moves_all_a')
5020        ids = self._reconciled_lines()
5021        action['domain'] = [('id', 'in', ids)]
5022        return action
5023
5024    def action_automatic_entry(self):
5025        action = self.env['ir.actions.act_window']._for_xml_id('account.account_automatic_entry_wizard_action')
5026        # Force the values of the move line in the context to avoid issues
5027        ctx = dict(self.env.context)
5028        ctx.pop('active_id', None)
5029        ctx['active_ids'] = self.ids
5030        ctx['active_model'] = 'account.move.line'
5031        action['context'] = ctx
5032        return action
5033
5034    @api.model
5035    def _get_suspense_moves_domain(self):
5036        return [
5037            ('move_id.to_check', '=', True),
5038            ('full_reconcile_id', '=', False),
5039            ('statement_line_id', '!=', False),
5040        ]
5041
5042    def _get_attachment_domains(self):
5043        self.ensure_one()
5044        domains = [[('res_model', '=', 'account.move'), ('res_id', '=', self.move_id.id)]]
5045        if self.statement_id:
5046            domains.append([('res_model', '=', 'account.bank.statement'), ('res_id', '=', self.statement_id.id)])
5047        if self.payment_id:
5048            domains.append([('res_model', '=', 'account.payment'), ('res_id', '=', self.payment_id.id)])
5049        return domains
5050
5051    def _convert_tags_for_cash_basis(self, tags):
5052        """ Cash basis entries are managed by the tax report just like misc operations.
5053        So it means that the tax report will not apply any additional multiplicator
5054        to the balance of the cash basis lines.
5055
5056        For invoices move lines whose multiplicator would have been -1 (if their
5057        taxes had not CABA), it will hence cause sign inversion if we directly copy
5058        the tags from those lines. Instead, we need to invert all the signs from these
5059        tags (if they come from tax report lines; tags created in data for financial
5060        reports will stay onchanged).
5061        """
5062        self.ensure_one()
5063        tax_multiplicator = (self.journal_id.type == 'sale' and -1 or 1) * (self.move_id.move_type in ('in_refund', 'out_refund') and -1 or 1)
5064        if tax_multiplicator == -1:
5065            # Take the opposite tags instead
5066            return self._revert_signed_tags(tags)
5067
5068        return tags
5069
5070    @api.model
5071    def _revert_signed_tags(self, tags):
5072        rslt = self.env['account.account.tag']
5073        for tag in tags:
5074            if tag.tax_report_line_ids:
5075                # tag created by an account.tax.report.line
5076                new_tag = tag.tax_report_line_ids[0].tag_ids.filtered(lambda x: x.tax_negate != tag.tax_negate)
5077                rslt += new_tag
5078            else:
5079                # tag created in data for use by an account.financial.html.report.line
5080                rslt += tag
5081
5082        return rslt
5083