1# -*- coding: utf-8 -*- 2from odoo import api, fields, models, _ 3from odoo.osv import expression 4from odoo.exceptions import UserError, ValidationError 5from odoo.addons.base.models.res_bank import sanitize_account_number 6from odoo.tools import remove_accents 7import logging 8import re 9 10_logger = logging.getLogger(__name__) 11 12 13class AccountJournalGroup(models.Model): 14 _name = 'account.journal.group' 15 _description = "Account Journal Group" 16 _check_company_auto = True 17 18 name = fields.Char("Journal Group", required=True, translate=True) 19 company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company) 20 excluded_journal_ids = fields.Many2many('account.journal', string="Excluded Journals", domain="[('company_id', '=', company_id)]", 21 check_company=True) 22 sequence = fields.Integer(default=10) 23 24 25class AccountJournal(models.Model): 26 _name = "account.journal" 27 _description = "Journal" 28 _order = 'sequence, type, code' 29 _inherit = ['mail.thread', 'mail.activity.mixin'] 30 _check_company_auto = True 31 32 def _default_inbound_payment_methods(self): 33 return self.env.ref('account.account_payment_method_manual_in') 34 35 def _default_outbound_payment_methods(self): 36 return self.env.ref('account.account_payment_method_manual_out') 37 38 def __get_bank_statements_available_sources(self): 39 return [('undefined', _('Undefined Yet'))] 40 41 def _get_bank_statements_available_sources(self): 42 return self.__get_bank_statements_available_sources() 43 44 def _default_alias_domain(self): 45 return self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain") 46 47 def _default_invoice_reference_model(self): 48 """Get the invoice reference model according to the company's country.""" 49 country_code = self.env.company.country_id.code 50 country_code = country_code and country_code.lower() 51 if country_code: 52 for model in self._fields['invoice_reference_model'].get_values(self.env): 53 if model.startswith(country_code): 54 return model 55 return 'odoo' 56 57 name = fields.Char(string='Journal Name', required=True) 58 code = fields.Char(string='Short Code', size=5, required=True, help="Shorter name used for display. The journal entries of this journal will also be named using this prefix by default.") 59 active = fields.Boolean(default=True, help="Set active to false to hide the Journal without removing it.") 60 type = fields.Selection([ 61 ('sale', 'Sales'), 62 ('purchase', 'Purchase'), 63 ('cash', 'Cash'), 64 ('bank', 'Bank'), 65 ('general', 'Miscellaneous'), 66 ], required=True, 67 help="Select 'Sale' for customer invoices journals.\n"\ 68 "Select 'Purchase' for vendor bills journals.\n"\ 69 "Select 'Cash' or 'Bank' for journals that are used in customer or vendor payments.\n"\ 70 "Select 'General' for miscellaneous operations journals.") 71 type_control_ids = fields.Many2many('account.account.type', 'journal_account_type_control_rel', 'journal_id', 'type_id', string='Allowed account types') 72 account_control_ids = fields.Many2many('account.account', 'journal_account_control_rel', 'journal_id', 'account_id', string='Allowed accounts', 73 check_company=True, 74 domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('is_off_balance', '=', False)]") 75 default_account_type = fields.Many2one('account.account.type', compute="_compute_default_account_type") 76 default_account_id = fields.Many2one( 77 comodel_name='account.account', check_company=True, copy=False, ondelete='restrict', 78 string='Default Account', 79 domain="[('deprecated', '=', False), ('company_id', '=', company_id)," 80 "'|', ('user_type_id', '=', default_account_type), ('user_type_id', 'in', type_control_ids)," 81 "('user_type_id.type', 'not in', ('receivable', 'payable'))]") 82 payment_debit_account_id = fields.Many2one( 83 comodel_name='account.account', check_company=True, copy=False, ondelete='restrict', 84 help="Incoming payments entries triggered by invoices/refunds will be posted on the Outstanding Receipts Account " 85 "and displayed as blue lines in the bank reconciliation widget. During the reconciliation process, concerned " 86 "transactions will be reconciled with entries on the Outstanding Receipts Account instead of the " 87 "receivable account.", string='Outstanding Receipts Account', 88 domain=lambda self: "[('deprecated', '=', False), ('company_id', '=', company_id), \ 89 ('user_type_id.type', 'not in', ('receivable', 'payable')), \ 90 '|', ('user_type_id', '=', %s), ('id', '=', default_account_id)]" % self.env.ref('account.data_account_type_current_assets').id) 91 payment_credit_account_id = fields.Many2one( 92 comodel_name='account.account', check_company=True, copy=False, ondelete='restrict', 93 help="Outgoing payments entries triggered by bills/credit notes will be posted on the Outstanding Payments Account " 94 "and displayed as blue lines in the bank reconciliation widget. During the reconciliation process, concerned " 95 "transactions will be reconciled with entries on the Outstanding Payments Account instead of the " 96 "payable account.", string='Outstanding Payments Account', 97 domain=lambda self: "[('deprecated', '=', False), ('company_id', '=', company_id), \ 98 ('user_type_id.type', 'not in', ('receivable', 'payable')), \ 99 '|', ('user_type_id', '=', %s), ('id', '=', default_account_id)]" % self.env.ref('account.data_account_type_current_assets').id) 100 suspense_account_id = fields.Many2one( 101 comodel_name='account.account', check_company=True, ondelete='restrict', readonly=False, store=True, 102 compute='_compute_suspense_account_id', 103 help="Bank statements transactions will be posted on the suspense account until the final reconciliation " 104 "allowing finding the right account.", string='Suspense Account', 105 domain=lambda self: "[('deprecated', '=', False), ('company_id', '=', company_id), \ 106 ('user_type_id.type', 'not in', ('receivable', 'payable')), \ 107 ('user_type_id', '=', %s)]" % self.env.ref('account.data_account_type_current_liabilities').id) 108 restrict_mode_hash_table = fields.Boolean(string="Lock Posted Entries with Hash", 109 help="If ticked, the accounting entry or invoice receives a hash as soon as it is posted and cannot be modified anymore.") 110 sequence = fields.Integer(help='Used to order Journals in the dashboard view', default=10) 111 112 invoice_reference_type = fields.Selection(string='Communication Type', required=True, selection=[('none', 'Free'), ('partner', 'Based on Customer'), ('invoice', 'Based on Invoice')], default='invoice', help='You can set here the default communication that will appear on customer invoices, once validated, to help the customer to refer to that particular invoice when making the payment.') 113 invoice_reference_model = fields.Selection(string='Communication Standard', required=True, selection=[('odoo', 'Odoo'),('euro', 'European')], default=_default_invoice_reference_model, help="You can choose different models for each type of reference. The default one is the Odoo reference.") 114 115 #groups_id = fields.Many2many('res.groups', 'account_journal_group_rel', 'journal_id', 'group_id', string='Groups') 116 currency_id = fields.Many2one('res.currency', help='The currency used to enter statement', string="Currency") 117 company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, index=True, default=lambda self: self.env.company, 118 help="Company related to this journal") 119 country_code = fields.Char(related='company_id.country_id.code', readonly=True) 120 121 refund_sequence = fields.Boolean(string='Dedicated Credit Note Sequence', help="Check this box if you don't want to share the same sequence for invoices and credit notes made from this journal", default=False) 122 sequence_override_regex = fields.Text(help="Technical field used to enforce complex sequence composition that the system would normally misunderstand.\n"\ 123 "This is a regex that can include all the following capture groups: prefix1, year, prefix2, month, prefix3, seq, suffix.\n"\ 124 "The prefix* groups are the separators between the year, month and the actual increasing sequence number (seq).\n"\ 125 126 "e.g: ^(?P<prefix1>.*?)(?P<year>\d{4})(?P<prefix2>\D*?)(?P<month>\d{2})(?P<prefix3>\D+?)(?P<seq>\d+)(?P<suffix>\D*?)$") 127 128 inbound_payment_method_ids = fields.Many2many( 129 comodel_name='account.payment.method', 130 relation='account_journal_inbound_payment_method_rel', 131 column1='journal_id', 132 column2='inbound_payment_method', 133 domain=[('payment_type', '=', 'inbound')], 134 string='Inbound Payment Methods', 135 compute='_compute_inbound_payment_method_ids', 136 store=True, 137 readonly=False, 138 help="Manual: Get paid by cash, check or any other method outside of Odoo.\n" 139 "Electronic: Get paid automatically through a payment acquirer by requesting a transaction" 140 " on a card saved by the customer when buying or subscribing online (payment token).\n" 141 "Batch Deposit: Encase several customer checks at once by generating a batch deposit to" 142 " submit to your bank. When encoding the bank statement in Odoo,you are suggested to" 143 " reconcile the transaction with the batch deposit. Enable this option from the settings." 144 ) 145 outbound_payment_method_ids = fields.Many2many( 146 comodel_name='account.payment.method', 147 relation='account_journal_outbound_payment_method_rel', 148 column1='journal_id', 149 column2='outbound_payment_method', 150 domain=[('payment_type', '=', 'outbound')], 151 string='Outbound Payment Methods', 152 compute='_compute_outbound_payment_method_ids', 153 store=True, 154 readonly=False, 155 help="Manual:Pay bill by cash or any other method outside of Odoo.\n" 156 "Check:Pay bill by check and print it from Odoo.\n" 157 "SEPA Credit Transfer: Pay bill from a SEPA Credit Transfer file you submit to your" 158 " bank. Enable this option from the settings." 159 ) 160 at_least_one_inbound = fields.Boolean(compute='_methods_compute', store=True) 161 at_least_one_outbound = fields.Boolean(compute='_methods_compute', store=True) 162 profit_account_id = fields.Many2one( 163 comodel_name='account.account', check_company=True, 164 help="Used to register a profit when the ending balance of a cash register differs from what the system computes", 165 string='Profit Account', 166 domain=lambda self: "[('deprecated', '=', False), ('company_id', '=', company_id), \ 167 ('user_type_id.type', 'not in', ('receivable', 'payable')), \ 168 ('user_type_id', 'in', %s)]" % [self.env.ref('account.data_account_type_revenue').id, 169 self.env.ref('account.data_account_type_other_income').id]) 170 loss_account_id = fields.Many2one( 171 comodel_name='account.account', check_company=True, 172 help="Used to register a loss when the ending balance of a cash register differs from what the system computes", 173 string='Loss Account', 174 domain=lambda self: "[('deprecated', '=', False), ('company_id', '=', company_id), \ 175 ('user_type_id.type', 'not in', ('receivable', 'payable')), \ 176 ('user_type_id', '=', %s)]" % self.env.ref('account.data_account_type_expenses').id) 177 178 # Bank journals fields 179 company_partner_id = fields.Many2one('res.partner', related='company_id.partner_id', string='Account Holder', readonly=True, store=False) 180 bank_account_id = fields.Many2one('res.partner.bank', 181 string="Bank Account", 182 ondelete='restrict', copy=False, 183 check_company=True, 184 domain="[('partner_id','=', company_partner_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]") 185 bank_statements_source = fields.Selection(selection=_get_bank_statements_available_sources, string='Bank Feeds', default='undefined', help="Defines how the bank statements will be registered") 186 bank_acc_number = fields.Char(related='bank_account_id.acc_number', readonly=False) 187 bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id', readonly=False) 188 189 # Sale journals fields 190 sale_activity_type_id = fields.Many2one('mail.activity.type', string='Schedule Activity', default=False, help="Activity will be automatically scheduled on payment due date, improving collection process.") 191 sale_activity_user_id = fields.Many2one('res.users', string="Activity User", help="Leave empty to assign the Salesperson of the invoice.") 192 sale_activity_note = fields.Text('Activity Summary') 193 194 # alias configuration for journals 195 alias_id = fields.Many2one('mail.alias', string='Email Alias', help="Send one separate email for each invoice.\n\n" 196 "Any file extension will be accepted.\n\n" 197 "Only PDF and XML files will be interpreted by Odoo", copy=False) 198 alias_domain = fields.Char('Alias domain', compute='_compute_alias_domain', default=_default_alias_domain, compute_sudo=True) 199 alias_name = fields.Char('Alias Name', copy=False, related='alias_id.alias_name', help="It creates draft invoices and bills by sending an email.", readonly=False) 200 201 journal_group_ids = fields.Many2many('account.journal.group', 202 domain="[('company_id', '=', company_id)]", 203 check_company=True, 204 string="Journal Groups") 205 206 secure_sequence_id = fields.Many2one('ir.sequence', 207 help='Sequence to use to ensure the securisation of data', 208 check_company=True, 209 readonly=True, copy=False) 210 211 _sql_constraints = [ 212 ('code_company_uniq', 'unique (code, name, company_id)', 'The code and name of the journal must be unique per company !'), 213 ] 214 215 @api.depends('type') 216 def _compute_default_account_type(self): 217 default_account_id_types = { 218 'bank': 'account.data_account_type_liquidity', 219 'cash': 'account.data_account_type_liquidity', 220 'sale': 'account.data_account_type_revenue', 221 'purchase': 'account.data_account_type_expenses' 222 } 223 224 for journal in self: 225 if journal.type in default_account_id_types: 226 journal.default_account_type = self.env.ref(default_account_id_types[journal.type]).id 227 else: 228 journal.default_account_type = False 229 230 @api.depends('type') 231 def _compute_outbound_payment_method_ids(self): 232 for journal in self: 233 if journal.type in ('bank', 'cash'): 234 journal.outbound_payment_method_ids = self._default_outbound_payment_methods() 235 else: 236 journal.outbound_payment_method_ids = False 237 238 @api.depends('type') 239 def _compute_inbound_payment_method_ids(self): 240 for journal in self: 241 if journal.type in ('bank', 'cash'): 242 journal.inbound_payment_method_ids = self._default_inbound_payment_methods() 243 else: 244 journal.inbound_payment_method_ids = False 245 246 @api.depends('company_id', 'type') 247 def _compute_suspense_account_id(self): 248 for journal in self: 249 if journal.type not in ('bank', 'cash'): 250 journal.suspense_account_id = False 251 elif journal.suspense_account_id: 252 journal.suspense_account_id = journal.suspense_account_id 253 elif journal.company_id.account_journal_suspense_account_id: 254 journal.suspense_account_id = journal.company_id.account_journal_suspense_account_id 255 else: 256 journal.suspense_account_id = False 257 258 def _compute_alias_domain(self): 259 alias_domain = self._default_alias_domain() 260 for record in self: 261 record.alias_domain = alias_domain 262 263 @api.constrains('type_control_ids') 264 def _constrains_type_control_ids(self): 265 self.env['account.move.line'].flush(['account_id', 'journal_id']) 266 self.flush(['type_control_ids']) 267 self._cr.execute(""" 268 SELECT aml.id 269 FROM account_move_line aml 270 WHERE aml.journal_id in (%s) 271 AND EXISTS (SELECT 1 FROM journal_account_type_control_rel rel WHERE rel.journal_id = aml.journal_id) 272 AND NOT EXISTS (SELECT 1 FROM account_account acc 273 JOIN journal_account_type_control_rel rel ON acc.user_type_id = rel.type_id 274 WHERE acc.id = aml.account_id AND rel.journal_id = aml.journal_id) 275 """, tuple(self.ids)) 276 if self._cr.fetchone(): 277 raise ValidationError(_('Some journal items already exist in this journal but with accounts from different types than the allowed ones.')) 278 279 @api.constrains('account_control_ids') 280 def _constrains_account_control_ids(self): 281 self.env['account.move.line'].flush(['account_id', 'journal_id']) 282 self.flush(['account_control_ids']) 283 self._cr.execute(""" 284 SELECT aml.id 285 FROM account_move_line aml 286 WHERE aml.journal_id in (%s) 287 AND EXISTS (SELECT 1 FROM journal_account_control_rel rel WHERE rel.journal_id = aml.journal_id) 288 AND NOT EXISTS (SELECT 1 FROM journal_account_control_rel rel WHERE rel.account_id = aml.account_id AND rel.journal_id = aml.journal_id) 289 """, tuple(self.ids)) 290 if self._cr.fetchone(): 291 raise ValidationError(_('Some journal items already exist in this journal but with other accounts than the allowed ones.')) 292 293 @api.constrains('type', 'bank_account_id') 294 def _check_bank_account(self): 295 for journal in self: 296 if journal.type == 'bank' and journal.bank_account_id: 297 if journal.bank_account_id.company_id and journal.bank_account_id.company_id != journal.company_id: 298 raise ValidationError(_('The bank account of a bank journal must belong to the same company (%s).', journal.company_id.name)) 299 # A bank account can belong to a customer/supplier, in which case their partner_id is the customer/supplier. 300 # Or they are part of a bank journal and their partner_id must be the company's partner_id. 301 if journal.bank_account_id.partner_id != journal.company_id.partner_id: 302 raise ValidationError(_('The holder of a journal\'s bank account must be the company (%s).', journal.company_id.name)) 303 304 @api.constrains('company_id') 305 def _check_company_consistency(self): 306 if not self: 307 return 308 309 self.flush(['company_id']) 310 self._cr.execute(''' 311 SELECT move.id 312 FROM account_move move 313 JOIN account_journal journal ON journal.id = move.journal_id 314 WHERE move.journal_id IN %s 315 AND move.company_id != journal.company_id 316 ''', [tuple(self.ids)]) 317 if self._cr.fetchone(): 318 raise UserError(_("You can't change the company of your journal since there are some journal entries linked to it.")) 319 320 @api.constrains('type', 'default_account_id') 321 def _check_type_default_account_id_type(self): 322 for journal in self: 323 if journal.type in ('sale', 'purchase') and journal.default_account_id.user_type_id.type in ('receivable', 'payable'): 324 raise ValidationError(_("The type of the journal's default credit/debit account shouldn't be 'receivable' or 'payable'.")) 325 326 @api.constrains('active') 327 def _check_auto_post_draft_entries(self): 328 # constraint should be tested just after archiving a journal, but shouldn't be raised when unarchiving a journal containing draft entries 329 for journal in self.filtered(lambda j: not j.active): 330 pending_moves = self.env['account.move'].search([ 331 ('journal_id', '=', journal.id), 332 ('state', '=', 'draft') 333 ], limit=1) 334 335 if pending_moves: 336 raise ValidationError(_("You can not archive a journal containing draft journal entries.\n\n" 337 "To proceed:\n" 338 "1/ click on the top-right button 'Journal Entries' from this journal form\n" 339 "2/ then filter on 'Draft' entries\n" 340 "3/ select them all and post or delete them through the action menu")) 341 342 @api.onchange('type') 343 def _onchange_type(self): 344 self.refund_sequence = self.type in ('sale', 'purchase') 345 346 def _get_alias_values(self, type, alias_name=None): 347 if not alias_name: 348 alias_name = self.name 349 if self.company_id != self.env.ref('base.main_company'): 350 alias_name += '-' + str(self.company_id.name) 351 try: 352 remove_accents(alias_name).encode('ascii') 353 except UnicodeEncodeError: 354 try: 355 remove_accents(self.code).encode('ascii') 356 safe_alias_name = self.code 357 except UnicodeEncodeError: 358 safe_alias_name = self.type 359 _logger.warning("Cannot use '%s' as email alias, fallback to '%s'", 360 alias_name, safe_alias_name) 361 alias_name = safe_alias_name 362 return { 363 'alias_defaults': {'move_type': type == 'purchase' and 'in_invoice' or 'out_invoice', 'company_id': self.company_id.id, 'journal_id': self.id}, 364 'alias_parent_thread_id': self.id, 365 'alias_name': alias_name, 366 } 367 368 def unlink(self): 369 bank_accounts = self.env['res.partner.bank'].browse() 370 for bank_account in self.mapped('bank_account_id'): 371 accounts = self.search([('bank_account_id', '=', bank_account.id)]) 372 if accounts <= self: 373 bank_accounts += bank_account 374 self.mapped('alias_id').sudo().unlink() 375 ret = super(AccountJournal, self).unlink() 376 bank_accounts.unlink() 377 return ret 378 379 @api.returns('self', lambda value: value.id) 380 def copy(self, default=None): 381 default = dict(default or {}) 382 default.update( 383 code=_("%s (copy)") % (self.code or ''), 384 name=_("%s (copy)") % (self.name or '')) 385 return super(AccountJournal, self).copy(default) 386 387 def _update_mail_alias(self, vals): 388 self.ensure_one() 389 alias_values = self._get_alias_values(type=vals.get('type') or self.type, alias_name=vals.get('alias_name')) 390 if self.alias_id: 391 self.alias_id.sudo().write(alias_values) 392 else: 393 alias_values['alias_model_id'] = self.env['ir.model']._get('account.move').id 394 alias_values['alias_parent_model_id'] = self.env['ir.model']._get('account.journal').id 395 self.alias_id = self.env['mail.alias'].sudo().create(alias_values) 396 397 if vals.get('alias_name'): 398 # remove alias_name to avoid useless write on alias 399 del(vals['alias_name']) 400 401 def write(self, vals): 402 for journal in self: 403 company = journal.company_id 404 if ('company_id' in vals and journal.company_id.id != vals['company_id']): 405 if self.env['account.move'].search([('journal_id', '=', journal.id)], limit=1): 406 raise UserError(_('This journal already contains items, therefore you cannot modify its company.')) 407 company = self.env['res.company'].browse(vals['company_id']) 408 if journal.bank_account_id.company_id and journal.bank_account_id.company_id != company: 409 journal.bank_account_id.write({ 410 'company_id': company.id, 411 'partner_id': company.partner_id.id, 412 }) 413 if 'currency_id' in vals: 414 if journal.bank_account_id: 415 journal.bank_account_id.currency_id = vals['currency_id'] 416 if 'bank_account_id' in vals: 417 if not vals.get('bank_account_id'): 418 raise UserError(_('You cannot remove the bank account from the journal once set.')) 419 else: 420 bank_account = self.env['res.partner.bank'].browse(vals['bank_account_id']) 421 if bank_account.partner_id != company.partner_id: 422 raise UserError(_("The partners of the journal's company and the related bank account mismatch.")) 423 if 'alias_name' in vals: 424 journal._update_mail_alias(vals) 425 if 'restrict_mode_hash_table' in vals and not vals.get('restrict_mode_hash_table'): 426 journal_entry = self.env['account.move'].search([('journal_id', '=', self.id), ('state', '=', 'posted'), ('secure_sequence_number', '!=', 0)], limit=1) 427 if len(journal_entry) > 0: 428 field_string = self._fields['restrict_mode_hash_table'].get_description(self.env)['string'] 429 raise UserError(_("You cannot modify the field %s of a journal that already has accounting entries.", field_string)) 430 result = super(AccountJournal, self).write(vals) 431 432 # Ensure the liquidity accounts are sharing the same foreign currency. 433 if 'currency_id' in vals: 434 for journal in self.filtered(lambda journal: journal.type in ('bank', 'cash')): 435 journal.default_account_id.currency_id = journal.currency_id 436 437 # Create the bank_account_id if necessary 438 if 'bank_acc_number' in vals: 439 for journal in self.filtered(lambda r: r.type == 'bank' and not r.bank_account_id): 440 journal.set_bank_account(vals.get('bank_acc_number'), vals.get('bank_id')) 441 for record in self: 442 if record.restrict_mode_hash_table and not record.secure_sequence_id: 443 record._create_secure_sequence(['secure_sequence_id']) 444 445 return result 446 447 @api.model 448 def get_next_bank_cash_default_code(self, journal_type, company): 449 journal_code_base = (journal_type == 'cash' and 'CSH' or 'BNK') 450 journals = self.env['account.journal'].search([('code', 'like', journal_code_base + '%'), ('company_id', '=', company.id)]) 451 for num in range(1, 100): 452 # journal_code has a maximal size of 5, hence we can enforce the boundary num < 100 453 journal_code = journal_code_base + str(num) 454 if journal_code not in journals.mapped('code'): 455 return journal_code 456 457 @api.model 458 def _prepare_liquidity_account_vals(self, company, code, vals): 459 return { 460 'name': vals.get('name'), 461 'code': code, 462 'user_type_id': self.env.ref('account.data_account_type_liquidity').id, 463 'currency_id': vals.get('currency_id'), 464 'company_id': company.id, 465 } 466 467 @api.model 468 def _fill_missing_values(self, vals): 469 journal_type = vals.get('type') 470 471 # 'type' field is required. 472 if not journal_type: 473 return 474 475 # === Fill missing company === 476 company = self.env['res.company'].browse(vals['company_id']) if vals.get('company_id') else self.env.company 477 vals['company_id'] = company.id 478 479 # Don't get the digits on 'chart_template_id' since the chart template could be a custom one. 480 random_account = self.env['account.account'].search([('company_id', '=', company.id)], limit=1) 481 digits = len(random_account.code) if random_account else 6 482 483 liquidity_type = self.env.ref('account.data_account_type_liquidity') 484 current_assets_type = self.env.ref('account.data_account_type_current_assets') 485 486 if journal_type in ('bank', 'cash'): 487 has_liquidity_accounts = vals.get('default_account_id') 488 has_payment_accounts = vals.get('payment_debit_account_id') or vals.get('payment_credit_account_id') 489 has_profit_account = vals.get('profit_account_id') 490 has_loss_account = vals.get('loss_account_id') 491 492 if journal_type == 'bank': 493 liquidity_account_prefix = company.bank_account_code_prefix or '' 494 else: 495 liquidity_account_prefix = company.cash_account_code_prefix or company.bank_account_code_prefix or '' 496 497 # === Fill missing name === 498 vals['name'] = vals.get('name') or vals.get('bank_acc_number') 499 500 # === Fill missing code === 501 if 'code' not in vals: 502 vals['code'] = self.get_next_bank_cash_default_code(journal_type, company) 503 if not vals['code']: 504 raise UserError(_("Cannot generate an unused journal code. Please fill the 'Shortcode' field.")) 505 506 # === Fill missing accounts === 507 if not has_liquidity_accounts: 508 default_account_code = self.env['account.account']._search_new_account_code(company, digits, liquidity_account_prefix) 509 default_account_vals = self._prepare_liquidity_account_vals(company, default_account_code, vals) 510 vals['default_account_id'] = self.env['account.account'].create(default_account_vals).id 511 if not has_payment_accounts: 512 vals['payment_debit_account_id'] = self.env['account.account'].create({ 513 'name': _("Outstanding Receipts"), 514 'code': self.env['account.account']._search_new_account_code(company, digits, liquidity_account_prefix), 515 'reconcile': True, 516 'user_type_id': current_assets_type.id, 517 'company_id': company.id, 518 }).id 519 vals['payment_credit_account_id'] = self.env['account.account'].create({ 520 'name': _("Outstanding Payments"), 521 'code': self.env['account.account']._search_new_account_code(company, digits, liquidity_account_prefix), 522 'reconcile': True, 523 'user_type_id': current_assets_type.id, 524 'company_id': company.id, 525 }).id 526 if journal_type == 'cash' and not has_profit_account: 527 vals['profit_account_id'] = company.default_cash_difference_income_account_id.id 528 if journal_type == 'cash' and not has_loss_account: 529 vals['loss_account_id'] = company.default_cash_difference_expense_account_id.id 530 531 # === Fill missing refund_sequence === 532 if 'refund_sequence' not in vals: 533 vals['refund_sequence'] = vals['type'] in ('sale', 'purchase') 534 535 @api.model 536 def create(self, vals): 537 # OVERRIDE 538 self._fill_missing_values(vals) 539 540 journal = super(AccountJournal, self.with_context(mail_create_nolog=True)).create(vals) 541 542 if 'alias_name' in vals: 543 journal._update_mail_alias(vals) 544 545 # Create the bank_account_id if necessary 546 if journal.type == 'bank' and not journal.bank_account_id and vals.get('bank_acc_number'): 547 journal.set_bank_account(vals.get('bank_acc_number'), vals.get('bank_id')) 548 549 return journal 550 551 def set_bank_account(self, acc_number, bank_id=None): 552 """ Create a res.partner.bank (if not exists) and set it as value of the field bank_account_id """ 553 self.ensure_one() 554 res_partner_bank = self.env['res.partner.bank'].search([('sanitized_acc_number', '=', sanitize_account_number(acc_number)), 555 ('company_id', '=', self.company_id.id)], limit=1) 556 if res_partner_bank: 557 self.bank_account_id = res_partner_bank.id 558 else: 559 self.bank_account_id = self.env['res.partner.bank'].create({ 560 'acc_number': acc_number, 561 'bank_id': bank_id, 562 'company_id': self.company_id.id, 563 'currency_id': self.currency_id.id, 564 'partner_id': self.company_id.partner_id.id, 565 }).id 566 567 def name_get(self): 568 res = [] 569 for journal in self: 570 name = journal.name 571 if journal.currency_id and journal.currency_id != journal.company_id.currency_id: 572 name = "%s (%s)" % (name, journal.currency_id.name) 573 res += [(journal.id, name)] 574 return res 575 576 @api.model 577 def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): 578 args = args or [] 579 580 if operator == 'ilike' and not (name or '').strip(): 581 domain = [] 582 else: 583 connector = '&' if operator in expression.NEGATIVE_TERM_OPERATORS else '|' 584 domain = [connector, ('code', operator, name), ('name', operator, name)] 585 return self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid) 586 587 @api.depends('inbound_payment_method_ids', 'outbound_payment_method_ids') 588 def _methods_compute(self): 589 for journal in self: 590 journal.at_least_one_inbound = bool(len(journal.inbound_payment_method_ids)) 591 journal.at_least_one_outbound = bool(len(journal.outbound_payment_method_ids)) 592 593 def action_configure_bank_journal(self): 594 """ This function is called by the "configure" button of bank journals, 595 visible on dashboard if no bank statement source has been defined yet 596 """ 597 # We simply call the setup bar function. 598 return self.env['res.company'].setting_init_bank_account_action() 599 600 def create_invoice_from_attachment(self, attachment_ids=[]): 601 ''' Create the invoices from files. 602 :return: A action redirecting to account.move tree/form view. 603 ''' 604 attachments = self.env['ir.attachment'].browse(attachment_ids) 605 if not attachments: 606 raise UserError(_("No attachment was provided")) 607 608 invoices = self.env['account.move'] 609 for attachment in attachments: 610 attachment.write({'res_model': 'mail.compose.message'}) 611 decoders = self.env['account.move']._get_create_invoice_from_attachment_decoders() 612 invoice = False 613 for decoder in sorted(decoders, key=lambda d: d[0]): 614 invoice = decoder[1](attachment) 615 if invoice: 616 break 617 if not invoice: 618 invoice = self.env['account.move'].create({}) 619 invoice.with_context(no_new_invoice=True).message_post(attachment_ids=[attachment.id]) 620 invoices += invoice 621 622 action_vals = { 623 'name': _('Generated Documents'), 624 'domain': [('id', 'in', invoices.ids)], 625 'res_model': 'account.move', 626 'views': [[False, "tree"], [False, "form"]], 627 'type': 'ir.actions.act_window', 628 'context': self._context 629 } 630 if len(invoices) == 1: 631 action_vals.update({'res_id': invoices[0].id, 'view_mode': 'form'}) 632 else: 633 action_vals['view_mode'] = 'tree,form' 634 return action_vals 635 636 def _create_invoice_from_single_attachment(self, attachment): 637 """ Creates an invoice and post the attachment. If the related modules 638 are installed, it will trigger OCR or the import from the EDI. 639 DEPRECATED : use create_invoice_from_attachment instead 640 641 :returns: the created invoice. 642 """ 643 invoice_action = self.create_invoice_from_attachment(attachment.ids) 644 return self.env['account.move'].browse(invoice_action['res_id']) 645 646 def _create_secure_sequence(self, sequence_fields): 647 """This function creates a no_gap sequence on each journal in self that will ensure 648 a unique number is given to all posted account.move in such a way that we can always 649 find the previous move of a journal entry on a specific journal. 650 """ 651 for journal in self: 652 vals_write = {} 653 for seq_field in sequence_fields: 654 if not journal[seq_field]: 655 vals = { 656 'name': _('Securisation of %s - %s') % (seq_field, journal.name), 657 'code': 'SECUR%s-%s' % (journal.id, seq_field), 658 'implementation': 'no_gap', 659 'prefix': '', 660 'suffix': '', 661 'padding': 0, 662 'company_id': journal.company_id.id} 663 seq = self.env['ir.sequence'].create(vals) 664 vals_write[seq_field] = seq.id 665 if vals_write: 666 journal.write(vals_write) 667 668 # ------------------------------------------------------------------------- 669 # REPORTING METHODS 670 # ------------------------------------------------------------------------- 671 672 def _get_journal_bank_account_balance(self, domain=None): 673 ''' Get the bank balance of the current journal by filtering the journal items using the journal's accounts. 674 675 /!\ The current journal is not part of the applied domain. This is the expected behavior since we only want 676 a logic based on accounts. 677 678 :param domain: An additional domain to be applied on the account.move.line model. 679 :return: Tuple having balance expressed in journal's currency 680 along with the total number of move lines having the same account as of the journal's default account. 681 ''' 682 self.ensure_one() 683 self.env['account.move.line'].check_access_rights('read') 684 685 if not self.default_account_id: 686 return 0.0, 0 687 688 domain = (domain or []) + [ 689 ('account_id', 'in', tuple(self.default_account_id.ids)), 690 ('display_type', 'not in', ('line_section', 'line_note')), 691 ('move_id.state', '!=', 'cancel'), 692 ] 693 query = self.env['account.move.line']._where_calc(domain) 694 tables, where_clause, where_params = query.get_sql() 695 696 query = ''' 697 SELECT 698 COUNT(account_move_line.id) AS nb_lines, 699 COALESCE(SUM(account_move_line.balance), 0.0), 700 COALESCE(SUM(account_move_line.amount_currency), 0.0) 701 FROM ''' + tables + ''' 702 WHERE ''' + where_clause + ''' 703 ''' 704 705 company_currency = self.company_id.currency_id 706 journal_currency = self.currency_id if self.currency_id and self.currency_id != company_currency else False 707 708 self._cr.execute(query, where_params) 709 nb_lines, balance, amount_currency = self._cr.fetchone() 710 return amount_currency if journal_currency else balance, nb_lines 711 712 def _get_journal_outstanding_payments_account_balance(self, domain=None, date=None): 713 ''' Get the outstanding payments balance of the current journal by filtering the journal items using the 714 journal's accounts. 715 716 :param domain: An additional domain to be applied on the account.move.line model. 717 :param date: The date to be used when performing the currency conversions. 718 :return: The balance expressed in the journal's currency. 719 ''' 720 self.ensure_one() 721 self.env['account.move.line'].check_access_rights('read') 722 conversion_date = date or fields.Date.context_today(self) 723 724 accounts = self.payment_debit_account_id + self.payment_credit_account_id 725 if not accounts: 726 return 0.0, 0 727 728 # Allow user managing payments without any statement lines. 729 # In that case, the user manages transactions only using the register payment wizard. 730 if self.default_account_id in accounts: 731 return 0.0, 0 732 733 domain = (domain or []) + [ 734 ('account_id', 'in', tuple(accounts.ids)), 735 ('display_type', 'not in', ('line_section', 'line_note')), 736 ('move_id.state', '!=', 'cancel'), 737 ('reconciled', '=', False), 738 ('journal_id', '=', self.id), 739 ] 740 query = self.env['account.move.line']._where_calc(domain) 741 tables, where_clause, where_params = query.get_sql() 742 743 self._cr.execute(''' 744 SELECT 745 COUNT(account_move_line.id) AS nb_lines, 746 account_move_line.currency_id, 747 account.reconcile AS is_account_reconcile, 748 SUM(account_move_line.amount_residual) AS amount_residual, 749 SUM(account_move_line.balance) AS balance, 750 SUM(account_move_line.amount_residual_currency) AS amount_residual_currency, 751 SUM(account_move_line.amount_currency) AS amount_currency 752 FROM ''' + tables + ''' 753 JOIN account_account account ON account.id = account_move_line.account_id 754 WHERE ''' + where_clause + ''' 755 GROUP BY account_move_line.currency_id, account.reconcile 756 ''', where_params) 757 758 company_currency = self.company_id.currency_id 759 journal_currency = self.currency_id if self.currency_id and self.currency_id != company_currency else False 760 balance_currency = journal_currency or company_currency 761 762 total_balance = 0.0 763 nb_lines = 0 764 for res in self._cr.dictfetchall(): 765 nb_lines += res['nb_lines'] 766 767 amount_currency = res['amount_residual_currency'] if res['is_account_reconcile'] else res['amount_currency'] 768 balance = res['amount_residual'] if res['is_account_reconcile'] else res['balance'] 769 770 if res['currency_id'] and journal_currency and res['currency_id'] == journal_currency.id: 771 total_balance += amount_currency 772 elif journal_currency: 773 total_balance += company_currency._convert(balance, balance_currency, self.company_id, conversion_date) 774 else: 775 total_balance += balance 776 return total_balance, nb_lines 777 778 def _get_last_bank_statement(self, domain=None): 779 ''' Retrieve the last bank statement created using this journal. 780 :param domain: An additional domain to be applied on the account.bank.statement model. 781 :return: An account.bank.statement record or an empty recordset. 782 ''' 783 self.ensure_one() 784 last_statement_domain = (domain or []) + [('journal_id', '=', self.id)] 785 last_st_line = self.env['account.bank.statement.line'].search(last_statement_domain, order='date desc, id desc', limit=1) 786 return last_st_line.statement_id 787