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