diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/account/wizard/account_payment_register.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/account/wizard/account_payment_register.py')
| -rw-r--r-- | addons/account/wizard/account_payment_register.py | 535 |
1 files changed, 535 insertions, 0 deletions
diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py new file mode 100644 index 00000000..98bed601 --- /dev/null +++ b/addons/account/wizard/account_payment_register.py @@ -0,0 +1,535 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + + +class AccountPaymentRegister(models.TransientModel): + _name = 'account.payment.register' + _description = 'Register Payment' + + # == Business fields == + payment_date = fields.Date(string="Payment Date", required=True, + default=fields.Date.context_today) + amount = fields.Monetary(currency_field='currency_id', store=True, readonly=False, + compute='_compute_amount') + communication = fields.Char(string="Memo", store=True, readonly=False, + compute='_compute_communication') + group_payment = fields.Boolean(string="Group Payments", store=True, readonly=False, + compute='_compute_group_payment', + help="Only one payment will be created by partner (bank)/ currency.") + currency_id = fields.Many2one('res.currency', string='Currency', store=True, readonly=False, + compute='_compute_currency_id', + help="The payment's currency.") + journal_id = fields.Many2one('account.journal', store=True, readonly=False, + compute='_compute_journal_id', + domain="[('company_id', '=', company_id), ('type', 'in', ('bank', 'cash'))]") + partner_bank_id = fields.Many2one('res.partner.bank', string="Recipient Bank Account", + readonly=False, store=True, + compute='_compute_partner_bank_id', + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id), ('partner_id', '=', partner_id)]") + company_currency_id = fields.Many2one('res.currency', string="Company Currency", + related='company_id.currency_id') + + # == Fields given through the context == + line_ids = fields.Many2many('account.move.line', 'account_payment_register_move_line_rel', 'wizard_id', 'line_id', + string="Journal items", readonly=True, copy=False,) + payment_type = fields.Selection([ + ('outbound', 'Send Money'), + ('inbound', 'Receive Money'), + ], string='Payment Type', store=True, copy=False, + compute='_compute_from_lines') + partner_type = fields.Selection([ + ('customer', 'Customer'), + ('supplier', 'Vendor'), + ], store=True, copy=False, + compute='_compute_from_lines') + source_amount = fields.Monetary( + string="Amount to Pay (company currency)", store=True, copy=False, + currency_field='company_currency_id', + compute='_compute_from_lines') + source_amount_currency = fields.Monetary( + string="Amount to Pay (foreign currency)", store=True, copy=False, + currency_field='source_currency_id', + compute='_compute_from_lines') + source_currency_id = fields.Many2one('res.currency', + string='Source Currency', store=True, copy=False, + compute='_compute_from_lines', + help="The payment's currency.") + can_edit_wizard = fields.Boolean(store=True, copy=False, + compute='_compute_from_lines', + help="Technical field used to indicate the user can edit the wizard content such as the amount.") + can_group_payments = fields.Boolean(store=True, copy=False, + compute='_compute_from_lines', + help="Technical field used to indicate the user can see the 'group_payments' box.") + company_id = fields.Many2one('res.company', store=True, copy=False, + compute='_compute_from_lines') + partner_id = fields.Many2one('res.partner', + string="Customer/Vendor", store=True, copy=False, ondelete='restrict', + compute='_compute_from_lines') + + # == Payment methods fields == + payment_method_id = fields.Many2one('account.payment.method', string='Payment Method', + readonly=False, store=True, + compute='_compute_payment_method_id', + domain="[('id', 'in', available_payment_method_ids)]", + help="Manual: Get paid by cash, check or any other method outside of Odoo.\n"\ + "Electronic: Get paid automatically through a payment acquirer by requesting a transaction on a card saved by the customer when buying or subscribing online (payment token).\n"\ + "Check: Pay bill by check and print it from Odoo.\n"\ + "Batch Deposit: Encase several customer checks at once by generating a batch deposit to submit to your bank. When encoding the bank statement in Odoo, you are suggested to reconcile the transaction with the batch deposit.To enable batch deposit, module account_batch_payment must be installed.\n"\ + "SEPA Credit Transfer: Pay bill from a SEPA Credit Transfer file you submit to your bank. To enable sepa credit transfer, module account_sepa must be installed ") + available_payment_method_ids = fields.Many2many('account.payment.method', + compute='_compute_payment_method_fields') + hide_payment_method = fields.Boolean( + compute='_compute_payment_method_fields', + help="Technical field used to hide the payment method if the selected journal has only one available which is 'manual'") + + # == Payment difference fields == + payment_difference = fields.Monetary( + compute='_compute_payment_difference') + payment_difference_handling = fields.Selection([ + ('open', 'Keep open'), + ('reconcile', 'Mark as fully paid'), + ], default='open', string="Payment Difference Handling") + writeoff_account_id = fields.Many2one('account.account', string="Difference Account", copy=False, + domain="[('deprecated', '=', False), ('company_id', '=', company_id)]") + writeoff_label = fields.Char(string='Journal Item Label', default='Write-Off', + help='Change label of the counterpart that will hold the payment difference') + + # == Display purpose fields == + show_partner_bank_account = fields.Boolean( + compute='_compute_show_require_partner_bank', + help="Technical field used to know whether the field `partner_bank_id` needs to be displayed or not in the payments form views") + require_partner_bank_account = fields.Boolean( + compute='_compute_show_require_partner_bank', + help="Technical field used to know whether the field `partner_bank_id` needs to be required or not in the payments form views") + country_code = fields.Char(related='company_id.country_id.code', readonly=True) + + # ------------------------------------------------------------------------- + # HELPERS + # ------------------------------------------------------------------------- + + @api.model + def _get_batch_communication(self, batch_result): + ''' Helper to compute the communication based on the batch. + :param batch_result: A batch returned by '_get_batches'. + :return: A string representing a communication to be set on payment. + ''' + labels = set(line.name or line.move_id.ref or line.move_id.name for line in batch_result['lines']) + return ' '.join(sorted(labels)) + + @api.model + def _get_line_batch_key(self, line): + ''' Turn the line passed as parameter to a dictionary defining on which way the lines + will be grouped together. + :return: A python dictionary. + ''' + return { + 'partner_id': line.partner_id.id, + 'account_id': line.account_id.id, + 'currency_id': (line.currency_id or line.company_currency_id).id, + 'partner_bank_id': (line.move_id.partner_bank_id or line.partner_id.commercial_partner_id.bank_ids[:1]).id, + 'partner_type': 'customer' if line.account_internal_type == 'receivable' else 'supplier', + 'payment_type': 'inbound' if line.balance > 0.0 else 'outbound', + } + + def _get_batches(self): + ''' Group the account.move.line linked to the wizard together. + :return: A list of batches, each one containing: + * key_values: The key as a dictionary used to group the journal items together. + * moves: An account.move recordset. + ''' + self.ensure_one() + + lines = self.line_ids._origin + + if len(lines.company_id) > 1: + raise UserError(_("You can't create payments for entries belonging to different companies.")) + if not lines: + raise UserError(_("You can't open the register payment wizard without at least one receivable/payable line.")) + + batches = {} + for line in lines: + batch_key = self._get_line_batch_key(line) + + serialized_key = '-'.join(str(v) for v in batch_key.values()) + batches.setdefault(serialized_key, { + 'key_values': batch_key, + 'lines': self.env['account.move.line'], + }) + batches[serialized_key]['lines'] += line + return list(batches.values()) + + @api.model + def _get_wizard_values_from_batch(self, batch_result): + ''' Extract values from the batch passed as parameter (see '_get_batches') + to be mounted in the wizard view. + :param batch_result: A batch returned by '_get_batches'. + :return: A dictionary containing valid fields + ''' + key_values = batch_result['key_values'] + lines = batch_result['lines'] + company = lines[0].company_id + + source_amount = abs(sum(lines.mapped('amount_residual'))) + if key_values['currency_id'] == company.currency_id.id: + source_amount_currency = source_amount + else: + source_amount_currency = abs(sum(lines.mapped('amount_residual_currency'))) + + return { + 'company_id': company.id, + 'partner_id': key_values['partner_id'], + 'partner_type': key_values['partner_type'], + 'payment_type': key_values['payment_type'], + 'source_currency_id': key_values['currency_id'], + 'source_amount': source_amount, + 'source_amount_currency': source_amount_currency, + } + + # ------------------------------------------------------------------------- + # COMPUTE METHODS + # ------------------------------------------------------------------------- + + @api.depends('line_ids') + def _compute_from_lines(self): + ''' Load initial values from the account.moves passed through the context. ''' + for wizard in self: + batches = wizard._get_batches() + batch_result = batches[0] + wizard_values_from_batch = wizard._get_wizard_values_from_batch(batch_result) + + if len(batches) == 1: + # == Single batch to be mounted on the view == + wizard.update(wizard_values_from_batch) + + wizard.can_edit_wizard = True + wizard.can_group_payments = len(batch_result['lines']) != 1 + else: + # == Multiple batches: The wizard is not editable == + wizard.update({ + 'company_id': batches[0]['lines'][0].company_id.id, + 'partner_id': False, + 'partner_type': False, + 'payment_type': wizard_values_from_batch['payment_type'], + 'source_currency_id': False, + 'source_amount': False, + 'source_amount_currency': False, + }) + + wizard.can_edit_wizard = False + wizard.can_group_payments = any(len(batch_result['lines']) != 1 for batch_result in batches) + + @api.depends('can_edit_wizard') + def _compute_communication(self): + # The communication can't be computed in '_compute_from_lines' because + # it's a compute editable field and then, should be computed in a separated method. + for wizard in self: + if wizard.can_edit_wizard: + batches = self._get_batches() + wizard.communication = wizard._get_batch_communication(batches[0]) + else: + wizard.communication = False + + @api.depends('can_edit_wizard') + def _compute_group_payment(self): + for wizard in self: + if wizard.can_edit_wizard: + batches = wizard._get_batches() + wizard.group_payment = len(batches[0]['lines'].move_id) == 1 + else: + wizard.group_payment = False + + @api.depends('company_id', 'source_currency_id') + def _compute_journal_id(self): + for wizard in self: + domain = [ + ('type', 'in', ('bank', 'cash')), + ('company_id', '=', wizard.company_id.id), + ] + journal = None + if wizard.source_currency_id: + journal = self.env['account.journal'].search(domain + [('currency_id', '=', wizard.source_currency_id.id)], limit=1) + if not journal: + journal = self.env['account.journal'].search(domain, limit=1) + wizard.journal_id = journal + + @api.depends('journal_id') + def _compute_currency_id(self): + for wizard in self: + wizard.currency_id = wizard.journal_id.currency_id or wizard.source_currency_id or wizard.company_id.currency_id + + @api.depends('partner_id') + def _compute_partner_bank_id(self): + ''' The default partner_bank_id will be the first available on the partner. ''' + for wizard in self: + available_partner_bank_accounts = wizard.partner_id.bank_ids.filtered(lambda x: x.company_id in (False, wizard.company_id)) + if available_partner_bank_accounts: + wizard.partner_bank_id = available_partner_bank_accounts[0]._origin + else: + wizard.partner_bank_id = False + + @api.depends('payment_type', + 'journal_id.inbound_payment_method_ids', + 'journal_id.outbound_payment_method_ids') + def _compute_payment_method_fields(self): + for wizard in self: + if wizard.payment_type == 'inbound': + wizard.available_payment_method_ids = wizard.journal_id.inbound_payment_method_ids + else: + wizard.available_payment_method_ids = wizard.journal_id.outbound_payment_method_ids + + wizard.hide_payment_method = len(wizard.available_payment_method_ids) == 1 and wizard.available_payment_method_ids.code == 'manual' + + @api.depends('payment_type', + 'journal_id.inbound_payment_method_ids', + 'journal_id.outbound_payment_method_ids') + def _compute_payment_method_id(self): + for wizard in self: + if wizard.payment_type == 'inbound': + available_payment_methods = wizard.journal_id.inbound_payment_method_ids + else: + available_payment_methods = wizard.journal_id.outbound_payment_method_ids + + # Select the first available one by default. + if available_payment_methods: + wizard.payment_method_id = available_payment_methods[0]._origin + else: + wizard.payment_method_id = False + + @api.depends('payment_method_id') + def _compute_show_require_partner_bank(self): + """ Computes if the destination bank account must be displayed in the payment form view. By default, it + won't be displayed but some modules might change that, depending on the payment type.""" + for wizard in self: + wizard.show_partner_bank_account = wizard.payment_method_id.code in self.env['account.payment']._get_method_codes_using_bank_account() + wizard.require_partner_bank_account = wizard.payment_method_id.code in self.env['account.payment']._get_method_codes_needing_bank_account() + + @api.depends('source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date') + def _compute_amount(self): + for wizard in self: + if wizard.source_currency_id == wizard.currency_id: + # Same currency. + wizard.amount = wizard.source_amount_currency + elif wizard.currency_id == wizard.company_id.currency_id: + # Payment expressed on the company's currency. + wizard.amount = wizard.source_amount + else: + # Foreign currency on payment different than the one set on the journal entries. + amount_payment_currency = wizard.company_id.currency_id._convert(wizard.source_amount, wizard.currency_id, wizard.company_id, wizard.payment_date) + wizard.amount = amount_payment_currency + + @api.depends('amount') + def _compute_payment_difference(self): + for wizard in self: + if wizard.source_currency_id == wizard.currency_id: + # Same currency. + wizard.payment_difference = wizard.source_amount_currency - wizard.amount + elif wizard.currency_id == wizard.company_id.currency_id: + # Payment expressed on the company's currency. + wizard.payment_difference = wizard.source_amount - wizard.amount + else: + # Foreign currency on payment different than the one set on the journal entries. + amount_payment_currency = wizard.company_id.currency_id._convert(wizard.source_amount, wizard.currency_id, wizard.company_id, wizard.payment_date) + wizard.payment_difference = amount_payment_currency - wizard.amount + + # ------------------------------------------------------------------------- + # LOW-LEVEL METHODS + # ------------------------------------------------------------------------- + + @api.model + def default_get(self, fields_list): + # OVERRIDE + res = super().default_get(fields_list) + + if 'line_ids' in fields_list and 'line_ids' not in res: + + # Retrieve moves to pay from the context. + + if self._context.get('active_model') == 'account.move': + lines = self.env['account.move'].browse(self._context.get('active_ids', [])).line_ids + elif self._context.get('active_model') == 'account.move.line': + lines = self.env['account.move.line'].browse(self._context.get('active_ids', [])) + else: + raise UserError(_( + "The register payment wizard should only be called on account.move or account.move.line records." + )) + + # Keep lines having a residual amount to pay. + available_lines = self.env['account.move.line'] + for line in lines: + if line.move_id.state != 'posted': + raise UserError(_("You can only register payment for posted journal entries.")) + + if line.account_internal_type not in ('receivable', 'payable'): + continue + if line.currency_id: + if line.currency_id.is_zero(line.amount_residual_currency): + continue + else: + if line.company_currency_id.is_zero(line.amount_residual): + continue + available_lines |= line + + # Check. + if not available_lines: + raise UserError(_("You can't register a payment because there is nothing left to pay on the selected journal items.")) + if len(lines.company_id) > 1: + raise UserError(_("You can't create payments for entries belonging to different companies.")) + if len(set(available_lines.mapped('account_internal_type'))) > 1: + raise UserError(_("You can't register payments for journal items being either all inbound, either all outbound.")) + + res['line_ids'] = [(6, 0, available_lines.ids)] + + return res + + # ------------------------------------------------------------------------- + # BUSINESS METHODS + # ------------------------------------------------------------------------- + + def _create_payment_vals_from_wizard(self): + payment_vals = { + 'date': self.payment_date, + 'amount': self.amount, + 'payment_type': self.payment_type, + 'partner_type': self.partner_type, + 'ref': self.communication, + 'journal_id': self.journal_id.id, + 'currency_id': self.currency_id.id, + 'partner_id': self.partner_id.id, + 'partner_bank_id': self.partner_bank_id.id, + 'payment_method_id': self.payment_method_id.id, + 'destination_account_id': self.line_ids[0].account_id.id + } + + if not self.currency_id.is_zero(self.payment_difference) and self.payment_difference_handling == 'reconcile': + payment_vals['write_off_line_vals'] = { + 'name': self.writeoff_label, + 'amount': self.payment_difference, + 'account_id': self.writeoff_account_id.id, + } + return payment_vals + + def _create_payment_vals_from_batch(self, batch_result): + batch_values = self._get_wizard_values_from_batch(batch_result) + return { + 'date': self.payment_date, + 'amount': batch_values['source_amount_currency'], + 'payment_type': batch_values['payment_type'], + 'partner_type': batch_values['partner_type'], + 'ref': self._get_batch_communication(batch_result), + 'journal_id': self.journal_id.id, + 'currency_id': batch_values['source_currency_id'], + 'partner_id': batch_values['partner_id'], + 'partner_bank_id': batch_result['key_values']['partner_bank_id'], + 'payment_method_id': self.payment_method_id.id, + 'destination_account_id': batch_result['lines'][0].account_id.id + } + + def _create_payments(self): + self.ensure_one() + batches = self._get_batches() + edit_mode = self.can_edit_wizard and (len(batches[0]['lines']) == 1 or self.group_payment) + + to_reconcile = [] + if edit_mode: + payment_vals = self._create_payment_vals_from_wizard() + payment_vals_list = [payment_vals] + to_reconcile.append(batches[0]['lines']) + else: + # Don't group payments: Create one batch per move. + if not self.group_payment: + new_batches = [] + for batch_result in batches: + for line in batch_result['lines']: + new_batches.append({ + **batch_result, + 'lines': line, + }) + batches = new_batches + + payment_vals_list = [] + for batch_result in batches: + payment_vals_list.append(self._create_payment_vals_from_batch(batch_result)) + to_reconcile.append(batch_result['lines']) + + payments = self.env['account.payment'].create(payment_vals_list) + + # If payments are made using a currency different than the source one, ensure the balance match exactly in + # order to fully paid the source journal items. + # For example, suppose a new currency B having a rate 100:1 regarding the company currency A. + # If you try to pay 12.15A using 0.12B, the computed balance will be 12.00A for the payment instead of 12.15A. + if edit_mode: + for payment, lines in zip(payments, to_reconcile): + # Batches are made using the same currency so making 'lines.currency_id' is ok. + if payment.currency_id != lines.currency_id: + liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines() + source_balance = abs(sum(lines.mapped('amount_residual'))) + payment_rate = liquidity_lines[0].amount_currency / liquidity_lines[0].balance + source_balance_converted = abs(source_balance) * payment_rate + + # Translate the balance into the payment currency is order to be able to compare them. + # In case in both have the same value (12.15 * 0.01 ~= 0.12 in our example), it means the user + # attempt to fully paid the source lines and then, we need to manually fix them to get a perfect + # match. + payment_balance = abs(sum(counterpart_lines.mapped('balance'))) + payment_amount_currency = abs(sum(counterpart_lines.mapped('amount_currency'))) + if not payment.currency_id.is_zero(source_balance_converted - payment_amount_currency): + continue + + delta_balance = source_balance - payment_balance + + # Balance are already the same. + if self.company_currency_id.is_zero(delta_balance): + continue + + # Fix the balance but make sure to peek the liquidity and counterpart lines first. + debit_lines = (liquidity_lines + counterpart_lines).filtered('debit') + credit_lines = (liquidity_lines + counterpart_lines).filtered('credit') + + payment.move_id.write({'line_ids': [ + (1, debit_lines[0].id, {'debit': debit_lines[0].debit + delta_balance}), + (1, credit_lines[0].id, {'credit': credit_lines[0].credit + delta_balance}), + ]}) + + payments.action_post() + + domain = [('account_internal_type', 'in', ('receivable', 'payable')), ('reconciled', '=', False)] + for payment, lines in zip(payments, to_reconcile): + + # When using the payment tokens, the payment could not be posted at this point (e.g. the transaction failed) + # and then, we can't perform the reconciliation. + if payment.state != 'posted': + continue + + payment_lines = payment.line_ids.filtered_domain(domain) + for account in payment_lines.account_id: + (payment_lines + lines)\ + .filtered_domain([('account_id', '=', account.id), ('reconciled', '=', False)])\ + .reconcile() + + return payments + + def action_create_payments(self): + payments = self._create_payments() + + if self._context.get('dont_redirect_to_payments'): + return True + + action = { + 'name': _('Payments'), + 'type': 'ir.actions.act_window', + 'res_model': 'account.payment', + 'context': {'create': False}, + } + if len(payments) == 1: + action.update({ + 'view_mode': 'form', + 'res_id': payments.id, + }) + else: + action.update({ + 'view_mode': 'tree,form', + 'domain': [('id', 'in', payments.ids)], + }) + return action |
