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