mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-23 12:42:01 +02:00
Initial commit: Accounting packages
This commit is contained in:
commit
4ef34c2317
2661 changed files with 1709616 additions and 0 deletions
|
|
@ -0,0 +1,9 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import account_journal
|
||||
from . import account_move
|
||||
from . import account_payment
|
||||
from . import account_payment_method
|
||||
from . import account_payment_method_line
|
||||
from . import payment_provider
|
||||
from . import payment_transaction
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = "account.journal"
|
||||
|
||||
def _get_available_payment_method_lines(self, payment_type):
|
||||
lines = super()._get_available_payment_method_lines(payment_type)
|
||||
|
||||
return lines.filtered(lambda l: l.payment_provider_state != 'disabled')
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_linked_to_payment_provider(self):
|
||||
linked_providers = self.env['payment.provider'].sudo().search([]).filtered(
|
||||
lambda p: p.journal_id.id in self.ids and p.state != 'disabled'
|
||||
)
|
||||
if linked_providers:
|
||||
raise UserError(_(
|
||||
"You must first deactivate a payment provider before deleting its journal.\n"
|
||||
"Linked providers: %s", ', '.join(p.display_name for p in linked_providers)
|
||||
))
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
transaction_ids = fields.Many2many(
|
||||
string="Transactions", comodel_name='payment.transaction',
|
||||
relation='account_invoice_transaction_rel', column1='invoice_id', column2='transaction_id',
|
||||
readonly=True, copy=False)
|
||||
authorized_transaction_ids = fields.Many2many(
|
||||
string="Authorized Transactions", comodel_name='payment.transaction',
|
||||
compute='_compute_authorized_transaction_ids', readonly=True, copy=False)
|
||||
amount_paid = fields.Monetary(
|
||||
string="Amount paid",
|
||||
compute='_compute_amount_paid'
|
||||
)
|
||||
|
||||
@api.depends('transaction_ids')
|
||||
def _compute_authorized_transaction_ids(self):
|
||||
for invoice in self:
|
||||
invoice.authorized_transaction_ids = invoice.transaction_ids.filtered(
|
||||
lambda tx: tx.state == 'authorized'
|
||||
)
|
||||
|
||||
@api.depends('transaction_ids')
|
||||
def _compute_amount_paid(self):
|
||||
""" Sum all the transaction amount for which state is in 'authorized' or 'done'
|
||||
"""
|
||||
for invoice in self:
|
||||
invoice.amount_paid = sum(
|
||||
invoice.transaction_ids.filtered(
|
||||
lambda tx: tx.state in ('authorized', 'done')
|
||||
).mapped('amount')
|
||||
)
|
||||
|
||||
def _has_to_be_paid(self):
|
||||
self.ensure_one()
|
||||
transactions = self.transaction_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))
|
||||
pending_manual_txs = transactions.filtered(lambda tx: tx.state == 'pending' and tx.provider_code in ('none', 'custom'))
|
||||
return bool(
|
||||
(
|
||||
self.amount_residual
|
||||
or not transactions
|
||||
)
|
||||
and self.state == 'posted'
|
||||
and self.payment_state in ('not_paid', 'partial')
|
||||
and self.amount_total
|
||||
and self.move_type == 'out_invoice'
|
||||
and (pending_manual_txs or not transactions or self.amount_paid < self.amount_total)
|
||||
)
|
||||
|
||||
def get_portal_last_transaction(self):
|
||||
self.ensure_one()
|
||||
return self.with_context(active_test=False).transaction_ids._get_last()
|
||||
|
||||
def payment_action_capture(self):
|
||||
""" Capture all transactions linked to this invoice. """
|
||||
payment_utils.check_rights_on_recordset(self)
|
||||
# In sudo mode because we need to be able to read on provider fields.
|
||||
self.authorized_transaction_ids.sudo().action_capture()
|
||||
|
||||
def payment_action_void(self):
|
||||
""" Void all transactions linked to this invoice. """
|
||||
payment_utils.check_rights_on_recordset(self)
|
||||
# In sudo mode because we need to be able to read on provider fields.
|
||||
self.authorized_transaction_ids.sudo().action_void()
|
||||
|
||||
def action_view_payment_transactions(self):
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('payment.action_payment_transaction')
|
||||
|
||||
if len(self.transaction_ids) == 1:
|
||||
action['view_mode'] = 'form'
|
||||
action['res_id'] = self.transaction_ids.id
|
||||
action['views'] = []
|
||||
else:
|
||||
action['domain'] = [('id', 'in', self.transaction_ids.ids)]
|
||||
|
||||
return action
|
||||
|
||||
def _get_default_payment_link_values(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'description': self.payment_reference,
|
||||
'amount': self.amount_residual,
|
||||
'currency_id': self.currency_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'amount_max': self.amount_residual,
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, Command, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = 'account.payment'
|
||||
|
||||
# == Business fields ==
|
||||
payment_transaction_id = fields.Many2one(
|
||||
string="Payment Transaction",
|
||||
comodel_name='payment.transaction',
|
||||
readonly=True,
|
||||
auto_join=True, # No access rule bypass since access to payments means access to txs too
|
||||
)
|
||||
payment_token_id = fields.Many2one(
|
||||
string="Saved Payment Token", comodel_name='payment.token', domain="""[
|
||||
('id', 'in', suitable_payment_token_ids),
|
||||
]""",
|
||||
help="Note that only tokens from providers allowing to capture the amount are available.")
|
||||
amount_available_for_refund = fields.Monetary(compute='_compute_amount_available_for_refund')
|
||||
|
||||
# == Display purpose fields ==
|
||||
suitable_payment_token_ids = fields.Many2many(
|
||||
comodel_name='payment.token',
|
||||
compute='_compute_suitable_payment_token_ids',
|
||||
compute_sudo=True,
|
||||
)
|
||||
# Technical field used to hide or show the payment_token_id if needed
|
||||
use_electronic_payment_method = fields.Boolean(
|
||||
compute='_compute_use_electronic_payment_method',
|
||||
)
|
||||
|
||||
# == Fields used for traceability ==
|
||||
source_payment_id = fields.Many2one(
|
||||
string="Source Payment",
|
||||
comodel_name='account.payment',
|
||||
help="The source payment of related refund payments",
|
||||
related='payment_transaction_id.source_transaction_id.payment_id',
|
||||
readonly=True,
|
||||
store=True, # Stored for the group by in `_compute_refunds_count`
|
||||
index='btree_not_null',
|
||||
)
|
||||
refunds_count = fields.Integer(string="Refunds Count", compute='_compute_refunds_count')
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
def _compute_amount_available_for_refund(self):
|
||||
for payment in self:
|
||||
tx_sudo = payment.payment_transaction_id.sudo()
|
||||
if tx_sudo.provider_id.support_refund and tx_sudo.operation != 'refund':
|
||||
# Only consider refund transactions that are confirmed by summing the amounts of
|
||||
# payments linked to such refund transactions. Indeed, should a refund transaction
|
||||
# be stuck forever in a transient state (due to webhook failure, for example), the
|
||||
# user would never be allowed to refund the source transaction again.
|
||||
refund_payments = self.search([('source_payment_id', '=', payment.id)])
|
||||
refunded_amount = abs(sum(refund_payments.mapped('amount')))
|
||||
payment.amount_available_for_refund = payment.amount - refunded_amount
|
||||
else:
|
||||
payment.amount_available_for_refund = 0
|
||||
|
||||
@api.depends('payment_method_line_id')
|
||||
def _compute_suitable_payment_token_ids(self):
|
||||
for payment in self:
|
||||
related_partner_ids = (
|
||||
payment.partner_id
|
||||
| payment.partner_id.commercial_partner_id
|
||||
| payment.partner_id.commercial_partner_id.child_ids
|
||||
)._origin
|
||||
|
||||
if payment.use_electronic_payment_method:
|
||||
payment.suitable_payment_token_ids = self.env['payment.token'].sudo().search([
|
||||
('company_id', '=', payment.company_id.id),
|
||||
('provider_id.capture_manually', '=', False),
|
||||
('partner_id', 'in', related_partner_ids.ids),
|
||||
('provider_id', '=', payment.payment_method_line_id.payment_provider_id.id),
|
||||
])
|
||||
else:
|
||||
payment.suitable_payment_token_ids = [Command.clear()]
|
||||
|
||||
@api.depends('payment_method_line_id')
|
||||
def _compute_use_electronic_payment_method(self):
|
||||
for payment in self:
|
||||
# Get a list of all electronic payment method codes.
|
||||
# These codes are comprised of 'electronic' and the providers of each payment provider.
|
||||
codes = [key for key in dict(self.env['payment.provider']._fields['code']._description_selection(self.env))]
|
||||
payment.use_electronic_payment_method = payment.payment_method_code in codes
|
||||
|
||||
def _compute_refunds_count(self):
|
||||
rg_data = self.env['account.payment']._read_group(
|
||||
domain=[
|
||||
('source_payment_id', 'in', self.ids),
|
||||
('payment_transaction_id.operation', '=', 'refund')
|
||||
],
|
||||
fields=['source_payment_id'],
|
||||
groupby=['source_payment_id']
|
||||
)
|
||||
data = {x['source_payment_id'][0]: x['source_payment_id_count'] for x in rg_data}
|
||||
for payment in self:
|
||||
payment.refunds_count = data.get(payment.id, 0)
|
||||
|
||||
#=== ONCHANGE METHODS ===#
|
||||
|
||||
@api.onchange('partner_id', 'payment_method_line_id', 'journal_id')
|
||||
def _onchange_set_payment_token_id(self):
|
||||
codes = [key for key in dict(self.env['payment.provider']._fields['code']._description_selection(self.env))]
|
||||
if not (self.payment_method_code in codes and self.partner_id and self.journal_id):
|
||||
self.payment_token_id = False
|
||||
return
|
||||
|
||||
related_partner_ids = (
|
||||
self.partner_id
|
||||
| self.partner_id.commercial_partner_id
|
||||
| self.partner_id.commercial_partner_id.child_ids
|
||||
)._origin
|
||||
|
||||
self.payment_token_id = self.env['payment.token'].sudo().search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('partner_id', 'in', related_partner_ids.ids),
|
||||
('provider_id.capture_manually', '=', False),
|
||||
('provider_id', '=', self.payment_method_line_id.payment_provider_id.id),
|
||||
], limit=1)
|
||||
|
||||
#=== ACTION METHODS ===#
|
||||
|
||||
def action_post(self):
|
||||
# Post the payments "normally" if no transactions are needed.
|
||||
# If not, let the provider update the state.
|
||||
|
||||
payments_need_tx = self.filtered(
|
||||
lambda p: p.payment_token_id and not p.payment_transaction_id
|
||||
)
|
||||
# creating the transaction require to access data on payment providers, not always accessible to users
|
||||
# able to create payments
|
||||
transactions = payments_need_tx.sudo()._create_payment_transaction()
|
||||
|
||||
res = super(AccountPayment, self - payments_need_tx).action_post()
|
||||
|
||||
for tx in transactions: # Process the transactions with a payment by token
|
||||
tx._send_payment_request()
|
||||
|
||||
# Post payments for issued transactions
|
||||
transactions._finalize_post_processing()
|
||||
payments_tx_done = payments_need_tx.filtered(
|
||||
lambda p: p.payment_transaction_id.state == 'done'
|
||||
)
|
||||
super(AccountPayment, payments_tx_done).action_post()
|
||||
payments_tx_not_done = payments_need_tx.filtered(
|
||||
lambda p: p.payment_transaction_id.state != 'done'
|
||||
)
|
||||
payments_tx_not_done.action_cancel()
|
||||
|
||||
return res
|
||||
|
||||
def action_refund_wizard(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _("Refund"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'payment.refund.wizard',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_view_refunds(self):
|
||||
self.ensure_one()
|
||||
action = {
|
||||
'name': _("Refund"),
|
||||
'res_model': 'account.payment',
|
||||
'type': 'ir.actions.act_window',
|
||||
}
|
||||
if self.refunds_count == 1:
|
||||
refund_tx = self.env['account.payment'].search([
|
||||
('source_payment_id', '=', self.id)
|
||||
], limit=1)
|
||||
action['res_id'] = refund_tx.id
|
||||
action['view_mode'] = 'form'
|
||||
else:
|
||||
action['view_mode'] = 'tree,form'
|
||||
action['domain'] = [('source_payment_id', '=', self.id)]
|
||||
return action
|
||||
|
||||
#=== BUSINESS METHODS - PAYMENT FLOW ===#
|
||||
|
||||
def _create_payment_transaction(self, **extra_create_values):
|
||||
for payment in self:
|
||||
if payment.payment_transaction_id:
|
||||
raise ValidationError(_(
|
||||
"A payment transaction with reference %s already exists.",
|
||||
payment.payment_transaction_id.reference
|
||||
))
|
||||
elif not payment.payment_token_id:
|
||||
raise ValidationError(_("A token is required to create a new payment transaction."))
|
||||
|
||||
transactions = self.env['payment.transaction']
|
||||
for payment in self:
|
||||
transaction_vals = payment._prepare_payment_transaction_vals(**extra_create_values)
|
||||
transaction = self.env['payment.transaction'].create(transaction_vals)
|
||||
transactions += transaction
|
||||
payment.payment_transaction_id = transaction # Link the transaction to the payment
|
||||
return transactions
|
||||
|
||||
def _prepare_payment_transaction_vals(self, **extra_create_values):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'provider_id': self.payment_token_id.provider_id.id,
|
||||
'reference': self.env['payment.transaction']._compute_reference(
|
||||
self.payment_token_id.provider_id.code, prefix=self.ref
|
||||
),
|
||||
'amount': self.amount,
|
||||
'currency_id': self.currency_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'token_id': self.payment_token_id.id,
|
||||
'operation': 'offline',
|
||||
'payment_id': self.id,
|
||||
**({'invoice_ids': [Command.set(self._context.get('active_ids', []))]}
|
||||
if self._context.get('active_model') == 'account.move'
|
||||
else {}),
|
||||
**extra_create_values,
|
||||
}
|
||||
|
||||
def _get_payment_refund_wizard_values(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'transaction_id': self.payment_transaction_id.id,
|
||||
'payment_amount': self.amount,
|
||||
'amount_available_for_refund': self.amount_available_for_refund,
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class AccountPaymentMethod(models.Model):
|
||||
_inherit = 'account.payment.method'
|
||||
|
||||
@api.model
|
||||
def _get_payment_method_information(self):
|
||||
res = super()._get_payment_method_information()
|
||||
for code, _desc in self.env['payment.provider']._fields['code'].selection:
|
||||
if code in ('none', 'custom'):
|
||||
continue
|
||||
res[code] = {
|
||||
'mode': 'electronic',
|
||||
'domain': [('type', '=', 'bank')],
|
||||
}
|
||||
return res
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class AccountPaymentMethodLine(models.Model):
|
||||
_inherit = "account.payment.method.line"
|
||||
|
||||
payment_provider_id = fields.Many2one(
|
||||
comodel_name='payment.provider',
|
||||
compute='_compute_payment_provider_id',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
payment_provider_state = fields.Selection(
|
||||
related='payment_provider_id.state'
|
||||
)
|
||||
|
||||
@api.depends('payment_provider_id.name')
|
||||
def _compute_name(self):
|
||||
super()._compute_name()
|
||||
for line in self:
|
||||
if line.payment_provider_id and not line.name:
|
||||
line.name = line.payment_provider_id.name
|
||||
|
||||
@api.depends('payment_method_id')
|
||||
def _compute_payment_provider_id(self):
|
||||
results = self.journal_id._get_journals_payment_method_information()
|
||||
manage_providers = results['manage_providers']
|
||||
method_information_mapping = results['method_information_mapping']
|
||||
providers_per_code = results['providers_per_code']
|
||||
|
||||
for line in self:
|
||||
journal = line.journal_id
|
||||
company = journal.company_id
|
||||
if (
|
||||
company
|
||||
and line.payment_method_id
|
||||
and not line.payment_provider_id
|
||||
and manage_providers
|
||||
and method_information_mapping.get(line.payment_method_id.id, {}).get('mode') == 'electronic'
|
||||
):
|
||||
provider_ids = providers_per_code.get(company.id, {}).get(line.code, set())
|
||||
|
||||
# Exclude the 'unique' / 'electronic' values that are already set on the journal.
|
||||
protected_provider_ids = set()
|
||||
for payment_type in ('inbound', 'outbound'):
|
||||
lines = journal[f'{payment_type}_payment_method_line_ids']
|
||||
for journal_line in lines:
|
||||
if journal_line.payment_method_id:
|
||||
if (
|
||||
manage_providers
|
||||
and method_information_mapping.get(journal_line.payment_method_id.id, {}).get('mode') == 'electronic'
|
||||
):
|
||||
protected_provider_ids.add(journal_line.payment_provider_id.id)
|
||||
|
||||
candidates_provider_ids = provider_ids - protected_provider_ids
|
||||
if candidates_provider_ids:
|
||||
line.payment_provider_id = next(iter(candidates_provider_ids))
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_active_provider(self):
|
||||
""" Ensure we don't remove an account.payment.method.line that is linked to a provider
|
||||
in the test or enabled state.
|
||||
"""
|
||||
active_provider = self.payment_provider_id.filtered(lambda provider: provider.state in ['enabled', 'test'])
|
||||
if active_provider:
|
||||
raise UserError(_(
|
||||
"You can't delete a payment method that is linked to a provider in the enabled "
|
||||
"or test state.\n""Linked providers(s): %s",
|
||||
', '.join(a.display_name for a in active_provider),
|
||||
))
|
||||
|
||||
def action_open_provider_form(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Provider'),
|
||||
'view_mode': 'form',
|
||||
'res_model': 'payment.provider',
|
||||
'target': 'current',
|
||||
'res_id': self.payment_provider_id.id
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class Paymentprovider(models.Model):
|
||||
_inherit = 'payment.provider'
|
||||
|
||||
journal_id = fields.Many2one(
|
||||
string="Payment Journal",
|
||||
help="The journal in which the successful transactions are posted.",
|
||||
comodel_name='account.journal',
|
||||
compute='_compute_journal_id',
|
||||
inverse='_inverse_journal_id',
|
||||
domain='[("type", "=", "bank"), ("company_id", "=", company_id)]',
|
||||
copy=False,
|
||||
)
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
def _ensure_payment_method_line(self, allow_create=True):
|
||||
self.ensure_one()
|
||||
if not self.id:
|
||||
return
|
||||
|
||||
pay_method_line = self.env['account.payment.method.line'].search(
|
||||
[('code', '=', self.code), ('payment_provider_id', '=', self.id)],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if not self.journal_id:
|
||||
if pay_method_line:
|
||||
pay_method_line.unlink()
|
||||
return
|
||||
|
||||
if not pay_method_line:
|
||||
pay_method_line = self.env['account.payment.method.line'].search(
|
||||
[
|
||||
('company_id', '=', self.company_id.id),
|
||||
('code', '=', self.code),
|
||||
('payment_provider_id', '=', False),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
if pay_method_line:
|
||||
pay_method_line.payment_provider_id = self
|
||||
pay_method_line.journal_id = self.journal_id
|
||||
pay_method_line.name = self.name
|
||||
elif allow_create:
|
||||
default_payment_method_id = self._get_default_payment_method_id(self.code)
|
||||
if not default_payment_method_id:
|
||||
return
|
||||
|
||||
create_values = {
|
||||
'name': self.name,
|
||||
'payment_method_id': default_payment_method_id,
|
||||
'journal_id': self.journal_id.id,
|
||||
'payment_provider_id': self.id,
|
||||
}
|
||||
pay_method_line_same_code = self.env['account.payment.method.line'].search(
|
||||
[
|
||||
('company_id', '=', self.company_id.id),
|
||||
('code', '=', self.code),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
if pay_method_line_same_code:
|
||||
create_values['payment_account_id'] = pay_method_line_same_code.payment_account_id.id
|
||||
self.env['account.payment.method.line'].create(create_values)
|
||||
|
||||
@api.depends('code', 'state', 'company_id')
|
||||
def _compute_journal_id(self):
|
||||
for provider in self:
|
||||
pay_method_line = self.env['account.payment.method.line'].search(
|
||||
[('code', '=', provider.code), ('payment_provider_id', '=', provider._origin.id)],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if pay_method_line:
|
||||
provider.journal_id = pay_method_line.journal_id
|
||||
elif provider.state in ('enabled', 'test'):
|
||||
provider.journal_id = self.env['account.journal'].search(
|
||||
[
|
||||
('company_id', '=', provider.company_id.id),
|
||||
('type', '=', 'bank'),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
if provider.id:
|
||||
provider._ensure_payment_method_line()
|
||||
|
||||
def _inverse_journal_id(self):
|
||||
for provider in self:
|
||||
provider._ensure_payment_method_line()
|
||||
|
||||
@api.model
|
||||
def _get_default_payment_method_id(self, code):
|
||||
provider_payment_method = self._get_provider_payment_method(code)
|
||||
if provider_payment_method:
|
||||
return provider_payment_method.id
|
||||
return None
|
||||
|
||||
@api.model
|
||||
def _get_provider_payment_method(self, code):
|
||||
return self.env['account.payment.method'].search([('code', '=', code)], limit=1)
|
||||
|
||||
#=== BUSINESS METHODS ===#
|
||||
|
||||
@api.model
|
||||
def _setup_provider(self, code):
|
||||
""" Override of `payment` to create the payment method of the provider. """
|
||||
super()._setup_provider(code)
|
||||
self._setup_payment_method(code)
|
||||
|
||||
@api.model
|
||||
def _setup_payment_method(self, code):
|
||||
if code not in ('none', 'custom') and not self._get_provider_payment_method(code):
|
||||
providers_description = dict(self._fields['code']._description_selection(self.env))
|
||||
self.env['account.payment.method'].sudo().create({
|
||||
'name': providers_description[code],
|
||||
'code': code,
|
||||
'payment_type': 'inbound',
|
||||
})
|
||||
|
||||
def _check_existing_payment_method_lines(self, payment_method):
|
||||
existing_payment_method_lines_count = \
|
||||
self.env['account.payment.method.line'].search_count([('payment_method_id', '=', \
|
||||
payment_method.id)], limit=1)
|
||||
return bool(existing_payment_method_lines_count)
|
||||
|
||||
@api.model
|
||||
def _remove_provider(self, code):
|
||||
""" Override of `payment` to delete the payment method of the provider. """
|
||||
payment_method = self._get_provider_payment_method(code)
|
||||
if self._check_existing_payment_method_lines(payment_method):
|
||||
raise UserError(_("To uninstall this module, please remove first the corresponding payment method line in the incoming payments tab defined on the bank journal."))
|
||||
super()._remove_provider(code)
|
||||
payment_method.unlink()
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, SUPERUSER_ID, _
|
||||
|
||||
|
||||
class PaymentTransaction(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
payment_id = fields.Many2one(
|
||||
string="Payment", comodel_name='account.payment', readonly=True)
|
||||
|
||||
invoice_ids = fields.Many2many(
|
||||
string="Invoices", comodel_name='account.move', relation='account_invoice_transaction_rel',
|
||||
column1='transaction_id', column2='invoice_id', readonly=True, copy=False,
|
||||
domain=[('move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))])
|
||||
invoices_count = fields.Integer(string="Invoices Count", compute='_compute_invoices_count')
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
@api.depends('invoice_ids')
|
||||
def _compute_invoices_count(self):
|
||||
tx_data = {}
|
||||
if self.ids:
|
||||
self.env.cr.execute(
|
||||
'''
|
||||
SELECT transaction_id, count(invoice_id)
|
||||
FROM account_invoice_transaction_rel
|
||||
WHERE transaction_id IN %s
|
||||
GROUP BY transaction_id
|
||||
''',
|
||||
[tuple(self.ids)]
|
||||
)
|
||||
tx_data = dict(self.env.cr.fetchall()) # {id: count}
|
||||
for tx in self:
|
||||
tx.invoices_count = tx_data.get(tx.id, 0)
|
||||
|
||||
#=== ACTION METHODS ===#
|
||||
|
||||
def action_view_invoices(self):
|
||||
""" Return the action for the views of the invoices linked to the transaction.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:return: The action
|
||||
:rtype: dict
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
action = {
|
||||
'name': _("Invoices"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.move',
|
||||
'target': 'current',
|
||||
}
|
||||
invoice_ids = self.invoice_ids.ids
|
||||
if len(invoice_ids) == 1:
|
||||
invoice = invoice_ids[0]
|
||||
action['res_id'] = invoice
|
||||
action['view_mode'] = 'form'
|
||||
action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
|
||||
else:
|
||||
action['view_mode'] = 'tree,form'
|
||||
action['domain'] = [('id', 'in', invoice_ids)]
|
||||
return action
|
||||
|
||||
#=== BUSINESS METHODS - PAYMENT FLOW ===#
|
||||
|
||||
@api.model
|
||||
def _compute_reference_prefix(self, provider_code, separator, **values):
|
||||
""" Compute the reference prefix from the transaction values.
|
||||
|
||||
If the `values` parameter has an entry with 'invoice_ids' as key and a list of (4, id, O) or
|
||||
(6, 0, ids) X2M command as value, the prefix is computed based on the invoice name(s).
|
||||
Otherwise, an empty string is returned.
|
||||
|
||||
Note: This method should be called in sudo mode to give access to documents (INV, SO, ...).
|
||||
|
||||
:param str provider_code: The code of the provider handling the transaction
|
||||
:param str separator: The custom separator used to separate data references
|
||||
:param dict values: The transaction values used to compute the reference prefix. It should
|
||||
have the structure {'invoice_ids': [(X2M command), ...], ...}.
|
||||
:return: The computed reference prefix if invoice ids are found, an empty string otherwise
|
||||
:rtype: str
|
||||
"""
|
||||
command_list = values.get('invoice_ids')
|
||||
if command_list:
|
||||
# Extract invoice id(s) from the X2M commands
|
||||
invoice_ids = self._fields['invoice_ids'].convert_to_cache(command_list, self)
|
||||
invoices = self.env['account.move'].browse(invoice_ids).exists()
|
||||
if len(invoices) == len(invoice_ids): # All ids are valid
|
||||
return separator.join(invoices.mapped('name'))
|
||||
return super()._compute_reference_prefix(provider_code, separator, **values)
|
||||
|
||||
def _set_canceled(self, state_message=None):
|
||||
""" Update the transactions' state to 'cancel'.
|
||||
|
||||
:param str state_message: The reason for which the transaction is set in 'cancel' state
|
||||
:return: updated transactions
|
||||
:rtype: `payment.transaction` recordset
|
||||
"""
|
||||
processed_txs = super()._set_canceled(state_message)
|
||||
# Cancel the existing payments
|
||||
processed_txs.payment_id.action_cancel()
|
||||
return processed_txs
|
||||
|
||||
#=== BUSINESS METHODS - POST-PROCESSING ===#
|
||||
|
||||
def _reconcile_after_done(self):
|
||||
""" Post relevant fiscal documents and create missing payments.
|
||||
|
||||
As there is nothing to reconcile for validation transactions, no payment is created for
|
||||
them. This is also true for validations with a validity check (transfer of a small amount
|
||||
with immediate refund) because validation amounts are not included in payouts.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
super()._reconcile_after_done()
|
||||
|
||||
# Validate invoices automatically once the transaction is confirmed
|
||||
self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()
|
||||
|
||||
# Create and post missing payments for transactions requiring reconciliation
|
||||
for tx in self.filtered(lambda t: t.operation != 'validation' and not t.payment_id):
|
||||
tx._create_payment()
|
||||
|
||||
def _create_payment(self, **extra_create_values):
|
||||
"""Create an `account.payment` record for the current transaction.
|
||||
|
||||
If the transaction is linked to some invoices, their reconciliation is done automatically.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param dict extra_create_values: Optional extra create values
|
||||
:return: The created payment
|
||||
:rtype: recordset of `account.payment`
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
payment_method_line = self.provider_id.journal_id.inbound_payment_method_line_ids\
|
||||
.filtered(lambda l: l.payment_provider_id == self.provider_id)
|
||||
payment_values = {
|
||||
'amount': abs(self.amount), # A tx may have a negative amount, but a payment must >= 0
|
||||
'payment_type': 'inbound' if self.amount > 0 else 'outbound',
|
||||
'currency_id': self.currency_id.id,
|
||||
'partner_id': self.partner_id.commercial_partner_id.id,
|
||||
'partner_type': 'customer',
|
||||
'journal_id': self.provider_id.journal_id.id,
|
||||
'company_id': self.provider_id.company_id.id,
|
||||
'payment_method_line_id': payment_method_line.id,
|
||||
'payment_token_id': self.token_id.id,
|
||||
'payment_transaction_id': self.id,
|
||||
'ref': f'{self.reference} - {self.partner_id.name} - {self.provider_reference or ""}',
|
||||
**extra_create_values,
|
||||
}
|
||||
payment = self.env['account.payment'].create(payment_values)
|
||||
payment.action_post()
|
||||
|
||||
# Track the payment to make a one2one.
|
||||
self.payment_id = payment
|
||||
|
||||
if self.invoice_ids:
|
||||
self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()
|
||||
|
||||
(payment.line_ids + self.invoice_ids.line_ids).filtered(
|
||||
lambda line: line.account_id == payment.destination_account_id
|
||||
and not line.reconciled
|
||||
).reconcile()
|
||||
|
||||
return payment
|
||||
|
||||
#=== BUSINESS METHODS - LOGGING ===#
|
||||
|
||||
def _log_message_on_linked_documents(self, message):
|
||||
""" Log a message on the payment and the invoices linked to the transaction.
|
||||
|
||||
For a module to implement payments and link documents to a transaction, it must override
|
||||
this method and call super, then log the message on documents linked to the transaction.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param str message: The message to be logged
|
||||
:return: None
|
||||
"""
|
||||
self.ensure_one()
|
||||
self = self.with_user(SUPERUSER_ID) # Log messages as 'OdooBot'
|
||||
if self.source_transaction_id.payment_id:
|
||||
self.source_transaction_id.payment_id.message_post(body=message)
|
||||
for invoice in self.source_transaction_id.invoice_ids:
|
||||
invoice.message_post(body=message)
|
||||
for invoice in self.invoice_ids:
|
||||
invoice.message_post(body=message)
|
||||
|
||||
#=== BUSINESS METHODS - POST-PROCESSING ===#
|
||||
|
||||
def _finalize_post_processing(self):
|
||||
""" Override of `payment` to write a message in the chatter with the payment and transaction
|
||||
references.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
super()._finalize_post_processing()
|
||||
for tx in self.filtered('payment_id'):
|
||||
message = _(
|
||||
"The payment related to the transaction with reference %(ref)s has been posted: "
|
||||
"%(link)s", ref=tx.reference, link=tx.payment_id._get_html_link()
|
||||
)
|
||||
tx._log_message_on_linked_documents(message)
|
||||
Loading…
Add table
Add a link
Reference in a new issue