from odoo import fields, models, api, Command, _ from odoo.exceptions import UserError, ValidationError from odoo.tools.misc import format_date class AccountPayment(models.Model): _inherit = 'account.payment' l10n_latam_new_check_ids = fields.One2many('l10n_latam.check', 'payment_id', string='Checks') l10n_latam_move_check_ids = fields.Many2many( comodel_name='l10n_latam.check', relation='l10n_latam_check_account_payment_rel', column1="payment_id", column2="check_id", required=True, copy=False, string="Checks Operations" ) # Warning message in case of unlogical third party check operations l10n_latam_check_warning_msg = fields.Text(compute='_compute_l10n_latam_check_warning_msg') amount = fields.Monetary(compute="_compute_amount", readonly=False, store=True) @api.constrains('state', 'move_id') def _check_move_id(self): for payment in self: if ( not payment.move_id and payment.payment_method_code in ('own_checks', 'new_third_party_checks', 'in_third_party_checks', 'out_third_party_checks', 'return_third_party_checks') and not payment.outstanding_account_id ): raise ValidationError(_("A payment with any Third Party Check or Own Check payment methods needs an outstanding account")) @api.depends('l10n_latam_move_check_ids.amount', 'l10n_latam_new_check_ids.amount', 'payment_method_code') def _compute_amount(self): for rec in self: checks = rec.l10n_latam_new_check_ids if rec._is_latam_check_payment(check_subtype='new_check') else rec.l10n_latam_move_check_ids if checks: rec.amount = sum(checks.mapped('amount')) def _is_latam_check_payment(self, check_subtype=False): if check_subtype == 'move_check': codes = ['in_third_party_checks', 'out_third_party_checks', 'return_third_party_checks'] elif check_subtype == 'new_check': codes = ['new_third_party_checks', 'own_checks'] else: codes = ['in_third_party_checks', 'out_third_party_checks', 'return_third_party_checks', 'new_third_party_checks', 'own_checks'] return self.payment_method_code in codes def action_post(self): # unlink checks if payment method code is not for checks. We do it on post and not when changing payment # method so that the user don't loose checks data in case of changing payment method and coming back again # also, changing partner recompute payment method so all checks would be cleaned for payment in self.filtered(lambda x: x.l10n_latam_new_check_ids and not x._is_latam_check_payment(check_subtype='new_check')): payment.l10n_latam_new_check_ids.unlink() if not self.env.context.get('l10n_ar_skip_remove_check'): for payment in self.filtered(lambda x: x.l10n_latam_move_check_ids and not x._is_latam_check_payment(check_subtype='move_check')): payment.l10n_latam_move_check_ids = False msgs = self._get_blocking_l10n_latam_warning_msg() if msgs: raise ValidationError('* %s' % '\n* '.join(msgs)) super().action_post() self._l10n_latam_check_split_move() def _get_latam_checks(self): self.ensure_one() if self._is_latam_check_payment(check_subtype='new_check'): return self.l10n_latam_new_check_ids elif self._is_latam_check_payment(check_subtype='move_check'): return self.l10n_latam_move_check_ids else: return self.env['l10n_latam.check'] def _get_blocking_l10n_latam_warning_msg(self): msgs = [] for rec in self.filtered(lambda x: x.state == 'draft' and x._is_latam_check_payment()): if any(rec.currency_id != check.currency_id for check in rec._get_latam_checks()): msgs.append(_('The currency of the payment and the currency of the check must be the same.')) if not rec.currency_id.is_zero(sum(rec._get_latam_checks().mapped('amount')) - rec.amount): msgs.append( _('The amount of the payment does not match the amount of the selected check. ' 'Please try to deselect and select the check again.') ) # checks being moved if rec._is_latam_check_payment(check_subtype='move_check'): if any(check.payment_id.state == 'draft' for check in rec.l10n_latam_move_check_ids): msgs.append( _('Selected checks "%s" are not posted', rec.l10n_latam_move_check_ids.filtered(lambda x: x.payment_id.state == 'draft').mapped('display_name')) ) elif rec.payment_type == 'outbound' and any(check.current_journal_id != rec.journal_id for check in rec.l10n_latam_move_check_ids): # check outbound payment and transfer or inbound transfer msgs.append(_( 'Some checks are not anymore in journal, it seems it has been moved by another payment.') ) elif rec.payment_type == 'inbound' and not rec._is_latam_check_transfer() and any(rec.l10n_latam_move_check_ids.mapped('current_journal_id')): msgs.append( _("Some checks are already in hand and can't be received again. Checks: %s", ', '.join(rec.l10n_latam_move_check_ids.mapped('display_name'))) ) for check in rec.l10n_latam_move_check_ids: date = rec.date or fields.Datetime.now() last_operation = check._get_last_operation() if last_operation and last_operation[0].date > date: msgs.append( _( "It seems you're trying to move a check with a date (%(date)s) prior to last " "operation done with the check (%(last_operation)s). This may be wrong, please " "double check it. By continue, the last operation on " "the check will remain being %(last_operation)s", date=format_date(self.env, date), last_operation=last_operation.display_name ) ) return msgs def _get_reconciled_checks_error(self): checks_reconciled = self.l10n_latam_new_check_ids.filtered(lambda x: x.issue_state in ['debited', 'voided']) if checks_reconciled: raise UserError( _("You can't cancel or re-open a payment with checks if some check has been debited or been voided. " "Checks:\n%s", ('\n'.join(['* %s (%s)' % (x.name, x.issue_state) for x in checks_reconciled]))) ) def action_cancel(self): self._get_reconciled_checks_error() super().action_cancel() def action_draft(self): self._get_reconciled_checks_error() super().action_draft() def _l10n_latam_check_split_move(self): for payment in self.filtered(lambda x: x.payment_method_code == 'own_checks' and x.payment_type == 'outbound'): if len(payment.l10n_latam_new_check_ids) == 1: liquidity_line = payment._seek_for_lines()[0] payment.l10n_latam_new_check_ids.outstanding_line_id = liquidity_line.id continue vals = { 'journal_id': payment.journal_id.id, 'move_type': 'entry', 'line_ids': [], } payment_liquidity_line = payment._seek_for_lines()[0] # One line per check checks_total = sum(payment.l10n_latam_new_check_ids.mapped('amount')) liquidity_balance_total = 0.0 liquidity_balance = 0.0 for check in payment.l10n_latam_new_check_ids: liquidity_amount_currency = -check.amount if check == payment.l10n_latam_new_check_ids[-1]: liquidity_balance = payment.currency_id.round(payment_liquidity_line.balance - liquidity_balance) else: liquidity_balance = payment.currency_id.round(payment_liquidity_line.balance * check.amount / checks_total) liquidity_balance_total += liquidity_balance vals['line_ids'].append( Command.create({ 'name': _( 'Check %(check_number)s - %(suffix)s', check_number=check.name, suffix=''.join([item[1] for item in payment._get_aml_default_display_name_list()])), 'date_maturity': check.payment_date, 'amount_currency': liquidity_amount_currency, 'currency_id': check.currency_id.id, 'debit': max(0.0, liquidity_balance), 'credit': -min(liquidity_balance, 0.0), 'partner_id': payment_liquidity_line.partner_id.id, 'account_id': payment_liquidity_line.account_id.id, 'l10n_latam_check_ids': [Command.link(check.id)] }), ) # Cancel payment line vals['line_ids'].append( Command.create({ 'name': payment_liquidity_line.name, 'date_maturity': payment_liquidity_line.date_maturity, 'amount_currency': -payment_liquidity_line.amount_currency, 'currency_id': payment_liquidity_line.currency_id.id, 'debit': -payment_liquidity_line.debit, 'credit': -payment_liquidity_line.credit, 'partner_id': payment_liquidity_line.partner_id.id, 'account_id': payment_liquidity_line.account_id.id, }), ) move_id = self.env['account.move'].create(vals) move_id.action_post() split_move_counterpart_line = move_id.line_ids.filtered(lambda x: x.amount_currency == -payment_liquidity_line.amount_currency) (split_move_counterpart_line + payment_liquidity_line).reconcile() def _l10n_latam_check_unlink_split_move(self): self.ensure_one() for check in self.l10n_latam_new_check_ids: if self.move_id == check.outstanding_line_id.move_id: check.outstanding_line_id = False continue check.outstanding_line_id.move_id.button_draft() check.outstanding_line_id.move_id.unlink() @api.depends( 'payment_method_line_id', 'state', 'date', 'amount', 'currency_id', 'company_id', 'l10n_latam_move_check_ids.issuer_vat', 'l10n_latam_move_check_ids.bank_id', 'l10n_latam_move_check_ids.payment_id.date', 'l10n_latam_new_check_ids.amount', 'l10n_latam_new_check_ids.name', ) def _compute_l10n_latam_check_warning_msg(self): """ Compute warning message for latam checks checks We use l10n_latam_check_number as de dependency because on the interface this is the field the user is using. Another approach could be to add an onchange on _inverse_l10n_latam_check_number method """ self.l10n_latam_check_warning_msg = False for rec in self.filtered(lambda x: x._is_latam_check_payment()): msgs = rec._get_blocking_l10n_latam_warning_msg() # new third party check uniqueness warning (on own checks it's done by a sql constraint) if rec.payment_method_code == 'new_third_party_checks': same_checks = self.env['l10n_latam.check'] for check in rec.l10n_latam_new_check_ids.filtered( lambda x: x.name and x.payment_method_line_id.code == 'new_third_party_checks' and x.bank_id and x.issuer_vat): same_checks += same_checks.search([ ('company_id', '=', rec.company_id.id), ('bank_id', '=', check.bank_id.id), ('issuer_vat', '=', check.issuer_vat), ('name', '=', check.name), ('payment_id.state', '!=', 'draft'), ('id', '!=', check._origin.id)], limit=1) if same_checks: msgs.append( _("Other checks were found with same number, issuer and bank. Please double check you are not " "encoding the same check more than once. List of other payments/checks: %s", ", ".join(same_checks.mapped('display_name'))) ) rec.l10n_latam_check_warning_msg = msgs and '* %s' % '\n* '.join(msgs) or False @api.model def _get_trigger_fields_to_synchronize(self): res = super()._get_trigger_fields_to_synchronize() return res + ('l10n_latam_new_check_ids',) def _prepare_move_line_default_vals(self, write_off_line_vals=None, force_balance=None): """ Add check name and operation on liquidity line """ res = super()._prepare_move_line_default_vals(write_off_line_vals=write_off_line_vals, force_balance=force_balance) # if only one check we don't create the split line, we add same data on liquidity line if self.payment_method_code == 'own_checks' and self.payment_type == 'outbound' and len(self.l10n_latam_new_check_ids) == 1: res[0].update({ 'name': _( 'Check %(check_number)s - %(suffix)s', check_number=self.l10n_latam_new_check_ids.name, suffix=''.join([item[1] for item in self._get_aml_default_display_name_list()])), 'date_maturity': self.l10n_latam_new_check_ids.payment_date, }) # we dont check the payment method code because when deposited on bank/cash journals pay method is manual but we still change the label # we dont want this names on the own checks because it doesn't add value, already each split/check line will have it name elif (self.l10n_latam_new_check_ids or self.l10n_latam_move_check_ids) and self.payment_method_code != 'own_checks': check_name = [check_name for check_name in (self.l10n_latam_new_check_ids | self.l10n_latam_move_check_ids).mapped('name') if check_name] document_name = ( _('Checks %s received') if self.payment_type == 'inbound' else _('Checks %s delivered')) % ( ', '.join(check_name) ) res[0].update({ 'name': document_name + ' - ' + ''.join([item[1] for item in self._get_aml_default_display_name_list()]), }) return res @api.depends('l10n_latam_move_check_ids') def _compute_destination_account_id(self): # EXTENDS 'account' super()._compute_destination_account_id() for payment in self: if payment.l10n_latam_move_check_ids and (not payment.partner_id or payment.partner_id == payment.company_id.partner_id): payment.destination_account_id = payment.company_id.transfer_account_id.id def _is_latam_check_transfer(self): self.ensure_one() return not self.partner_id and self.destination_account_id == self.company_id.transfer_account_id