mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-23 03:02:06 +02:00
19.0 vanilla
This commit is contained in:
parent
ba20ce7443
commit
768b70e05e
2357 changed files with 1057103 additions and 712486 deletions
|
|
@ -1,8 +1,13 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import format_date, str2bool
|
||||
from odoo.tools.translate import _
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
from odoo.tools.image import image_data_uri
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
|
|
@ -14,7 +19,11 @@ class AccountMove(models.Model):
|
|||
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)
|
||||
compute='_compute_authorized_transaction_ids', readonly=True, copy=False,
|
||||
compute_sudo=True)
|
||||
transaction_count = fields.Integer(
|
||||
string="Transaction Count", compute='_compute_transaction_count'
|
||||
)
|
||||
amount_paid = fields.Monetary(
|
||||
string="Amount paid",
|
||||
compute='_compute_amount_paid'
|
||||
|
|
@ -27,6 +36,11 @@ class AccountMove(models.Model):
|
|||
lambda tx: tx.state == 'authorized'
|
||||
)
|
||||
|
||||
@api.depends('transaction_ids')
|
||||
def _compute_transaction_count(self):
|
||||
for invoice in self:
|
||||
invoice.transaction_count = len(invoice.transaction_ids)
|
||||
|
||||
@api.depends('transaction_ids')
|
||||
def _compute_amount_paid(self):
|
||||
""" Sum all the transaction amount for which state is in 'authorized' or 'done'
|
||||
|
|
@ -41,34 +55,72 @@ class AccountMove(models.Model):
|
|||
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
|
||||
pending_transactions = transactions.filtered(
|
||||
lambda tx: tx.state in {'pending', 'authorized'}
|
||||
and tx.provider_code not in {'none', 'custom'})
|
||||
enabled_feature = str2bool(
|
||||
self.env['ir.config_parameter'].sudo().get_param(
|
||||
'account_payment.enable_portal_payment'
|
||||
)
|
||||
)
|
||||
return enabled_feature and bool(
|
||||
(self.amount_residual or not transactions)
|
||||
and self.state == 'posted'
|
||||
and self.payment_state in ('not_paid', 'partial')
|
||||
and self.payment_state in ('not_paid', 'in_payment', 'partial')
|
||||
and not self.currency_id.is_zero(self.amount_residual)
|
||||
and self.amount_total
|
||||
and self.move_type == 'out_invoice'
|
||||
and (pending_manual_txs or not transactions or self.amount_paid < self.amount_total)
|
||||
and not pending_transactions
|
||||
)
|
||||
|
||||
def _get_online_payment_error(self):
|
||||
"""
|
||||
Returns the appropriate error message to be displayed if _has_to_be_paid() method returns False.
|
||||
"""
|
||||
self.ensure_one()
|
||||
transactions = self.transaction_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))
|
||||
pending_transactions = transactions.filtered(
|
||||
lambda tx: tx.state in {'pending', 'authorized'}
|
||||
and tx.provider_code not in {'none', 'custom'})
|
||||
enabled_feature = str2bool(
|
||||
self.env['ir.config_parameter'].sudo().get_param(
|
||||
'account_payment.enable_portal_payment'
|
||||
)
|
||||
)
|
||||
errors = []
|
||||
if not enabled_feature:
|
||||
errors.append(_("This invoice cannot be paid online."))
|
||||
if transactions or self.currency_id.is_zero(self.amount_residual):
|
||||
errors.append(_("There is no amount to be paid."))
|
||||
if self.state != 'posted':
|
||||
errors.append(_("This invoice isn't posted."))
|
||||
if self.currency_id.is_zero(self.amount_residual):
|
||||
errors.append(_("This invoice has already been paid."))
|
||||
if self.move_type != 'out_invoice':
|
||||
errors.append(_("This is not an outgoing invoice."))
|
||||
if pending_transactions:
|
||||
errors.append(_("There are pending transactions for this invoice."))
|
||||
return '\n'.join(errors)
|
||||
|
||||
@api.private
|
||||
def get_portal_last_transaction(self):
|
||||
self.ensure_one()
|
||||
return self.with_context(active_test=False).transaction_ids._get_last()
|
||||
return self.with_context(active_test=False).sudo().transaction_ids._get_last()
|
||||
|
||||
def payment_action_capture(self):
|
||||
""" Capture all transactions linked to this invoice. """
|
||||
self.ensure_one()
|
||||
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()
|
||||
|
||||
# In sudo mode to bypass the checks on the rights on the transactions.
|
||||
return self.sudo().transaction_ids.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()
|
||||
|
||||
# In sudo mode to bypass the checks on the rights on the transactions.
|
||||
self.sudo().authorized_transaction_ids.action_void()
|
||||
|
||||
def action_view_payment_transactions(self):
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('payment.action_payment_transaction')
|
||||
|
|
@ -83,11 +135,48 @@ class AccountMove(models.Model):
|
|||
return action
|
||||
|
||||
def _get_default_payment_link_values(self):
|
||||
self.ensure_one()
|
||||
next_payment_values = self._get_invoice_next_payment_values()
|
||||
amount_max = next_payment_values.get('amount_due')
|
||||
additional_info = {}
|
||||
open_installments = []
|
||||
installment_state = next_payment_values.get('installment_state')
|
||||
next_amount_to_pay = next_payment_values.get('next_amount_to_pay')
|
||||
if installment_state in ('next', 'overdue'):
|
||||
open_installments = []
|
||||
for installment in next_payment_values.get('not_reconciled_installments'):
|
||||
data = {
|
||||
'type': installment['type'],
|
||||
'number': installment['number'],
|
||||
'amount': installment['amount_residual_currency_unsigned'],
|
||||
'date_maturity': format_date(self.env, installment['date_maturity']),
|
||||
}
|
||||
open_installments.append(data)
|
||||
|
||||
elif installment_state == 'epd':
|
||||
amount_max = next_amount_to_pay # with epd, next_amount_to_pay is the invoice amount residual
|
||||
additional_info.update({
|
||||
'has_eligible_epd': True,
|
||||
'discount_date': next_payment_values.get('discount_date')
|
||||
})
|
||||
|
||||
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,
|
||||
'open_installments': open_installments,
|
||||
'amount': next_amount_to_pay,
|
||||
'amount_max': amount_max,
|
||||
**additional_info
|
||||
}
|
||||
|
||||
def _generate_portal_payment_qr(self):
|
||||
self.ensure_one()
|
||||
portal_url = self._get_portal_payment_link()
|
||||
barcode = self.env['ir.actions.report'].barcode(barcode_type="QR", value=portal_url, width=128, height=128, quiet=False)
|
||||
return image_data_uri(base64.b64encode(barcode))
|
||||
|
||||
def _get_portal_payment_link(self):
|
||||
self.ensure_one()
|
||||
payment_link_wizard = self.env['payment.link.wizard'].with_context(
|
||||
active_id=self.id, active_model=self._name
|
||||
).create({})
|
||||
return payment_link_wizard.link
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class AccountPayment(models.Model):
|
|||
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
|
||||
bypass_search_access=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="""[
|
||||
|
|
@ -49,7 +49,16 @@ class AccountPayment(models.Model):
|
|||
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':
|
||||
payment_method = (
|
||||
tx_sudo.payment_method_id.primary_payment_method_id
|
||||
or tx_sudo.payment_method_id
|
||||
)
|
||||
if (
|
||||
tx_sudo # The payment was created by a transaction.
|
||||
and tx_sudo.provider_id.support_refund != 'none'
|
||||
and payment_method.support_refund != 'none'
|
||||
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
|
||||
|
|
@ -63,17 +72,11 @@ class AccountPayment(models.Model):
|
|||
@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),
|
||||
*self.env['payment.token']._check_company_domain(payment.company_id),
|
||||
('provider_id.capture_manually', '=', False),
|
||||
('partner_id', 'in', related_partner_ids.ids),
|
||||
('partner_id', '=', payment.partner_id.id),
|
||||
('provider_id', '=', payment.payment_method_line_id.payment_provider_id.id),
|
||||
])
|
||||
else:
|
||||
|
|
@ -93,10 +96,10 @@ class AccountPayment(models.Model):
|
|||
('source_payment_id', 'in', self.ids),
|
||||
('payment_transaction_id.operation', '=', 'refund')
|
||||
],
|
||||
fields=['source_payment_id'],
|
||||
groupby=['source_payment_id']
|
||||
groupby=['source_payment_id'],
|
||||
aggregates=['__count']
|
||||
)
|
||||
data = {x['source_payment_id'][0]: x['source_payment_id_count'] for x in rg_data}
|
||||
data = {source_payment.id: count for source_payment, count in rg_data}
|
||||
for payment in self:
|
||||
payment.refunds_count = data.get(payment.id, 0)
|
||||
|
||||
|
|
@ -109,18 +112,12 @@ class AccountPayment(models.Model):
|
|||
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),
|
||||
*self.env['payment.token']._check_company_domain(self.company_id),
|
||||
('partner_id', '=', self.partner_id.id),
|
||||
('provider_id.capture_manually', '=', False),
|
||||
('provider_id', '=', self.payment_method_line_id.payment_provider_id.id),
|
||||
], limit=1)
|
||||
], limit=1) # In sudo mode to read the provider fields.
|
||||
|
||||
#=== ACTION METHODS ===#
|
||||
|
||||
|
|
@ -138,10 +135,10 @@ class AccountPayment(models.Model):
|
|||
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()
|
||||
tx._charge_with_token()
|
||||
|
||||
# Post payments for issued transactions
|
||||
transactions._finalize_post_processing()
|
||||
transactions._post_process()
|
||||
payments_tx_done = payments_need_tx.filtered(
|
||||
lambda p: p.payment_transaction_id.state == 'done'
|
||||
)
|
||||
|
|
@ -177,7 +174,7 @@ class AccountPayment(models.Model):
|
|||
action['res_id'] = refund_tx.id
|
||||
action['view_mode'] = 'form'
|
||||
else:
|
||||
action['view_mode'] = 'tree,form'
|
||||
action['view_mode'] = 'list,form'
|
||||
action['domain'] = [('source_payment_id', '=', self.id)]
|
||||
return action
|
||||
|
||||
|
|
@ -203,10 +200,17 @@ class AccountPayment(models.Model):
|
|||
|
||||
def _prepare_payment_transaction_vals(self, **extra_create_values):
|
||||
self.ensure_one()
|
||||
if self.env.context.get('active_model', '') == 'account.move':
|
||||
invoice_ids = self.env.context.get('active_ids', [])
|
||||
elif self.env.context.get('active_model', '') == 'account.move.line':
|
||||
invoice_ids = self.env['account.move.line'].browse(self.env.context.get('active_ids')).move_id.ids
|
||||
else:
|
||||
invoice_ids = []
|
||||
return {
|
||||
'provider_id': self.payment_token_id.provider_id.id,
|
||||
'payment_method_id': self.payment_token_id.payment_method_id.id,
|
||||
'reference': self.env['payment.transaction']._compute_reference(
|
||||
self.payment_token_id.provider_id.code, prefix=self.ref
|
||||
self.payment_token_id.provider_id.code, prefix=self.memo
|
||||
),
|
||||
'amount': self.amount,
|
||||
'currency_id': self.currency_id.id,
|
||||
|
|
@ -214,10 +218,7 @@ class AccountPayment(models.Model):
|
|||
'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,
|
||||
'invoice_ids': [Command.set(invoice_ids)],
|
||||
}
|
||||
|
||||
def _get_payment_refund_wizard_values(self):
|
||||
|
|
|
|||
|
|
@ -15,6 +15,6 @@ class AccountPaymentMethod(models.Model):
|
|||
continue
|
||||
res[code] = {
|
||||
'mode': 'electronic',
|
||||
'domain': [('type', '=', 'bank')],
|
||||
'type': ('bank',),
|
||||
}
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
# -*- 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):
|
||||
|
|
@ -14,6 +12,7 @@ class AccountPaymentMethodLine(models.Model):
|
|||
compute='_compute_payment_provider_id',
|
||||
store=True,
|
||||
readonly=False,
|
||||
domain="[('code', '=', code)]",
|
||||
)
|
||||
payment_provider_state = fields.Selection(
|
||||
related='payment_provider_id.state'
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class Paymentprovider(models.Model):
|
||||
|
||||
class PaymentProvider(models.Model):
|
||||
_inherit = 'payment.provider'
|
||||
|
||||
journal_id = fields.Many2one(
|
||||
|
|
@ -12,7 +13,8 @@ class Paymentprovider(models.Model):
|
|||
comodel_name='account.journal',
|
||||
compute='_compute_journal_id',
|
||||
inverse='_inverse_journal_id',
|
||||
domain='[("type", "=", "bank"), ("company_id", "=", company_id)]',
|
||||
check_company=True,
|
||||
domain='[("type", "=", "bank")]',
|
||||
copy=False,
|
||||
)
|
||||
|
||||
|
|
@ -23,10 +25,14 @@ class Paymentprovider(models.Model):
|
|||
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,
|
||||
)
|
||||
default_payment_method = self._get_provider_payment_method(self._get_code())
|
||||
if not default_payment_method:
|
||||
return
|
||||
|
||||
pay_method_line = self.env['account.payment.method.line'].search([
|
||||
('payment_provider_id', '=', self.id),
|
||||
('journal_id', '!=', False),
|
||||
], limit=1)
|
||||
|
||||
if not self.journal_id:
|
||||
if pay_method_line:
|
||||
|
|
@ -36,9 +42,10 @@ class Paymentprovider(models.Model):
|
|||
if not pay_method_line:
|
||||
pay_method_line = self.env['account.payment.method.line'].search(
|
||||
[
|
||||
('company_id', '=', self.company_id.id),
|
||||
('code', '=', self.code),
|
||||
*self.env['account.payment.method.line']._check_company_domain(self.company_id),
|
||||
('code', '=', self._get_code()),
|
||||
('payment_provider_id', '=', False),
|
||||
('journal_id', '!=', False),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
|
|
@ -47,34 +54,44 @@ class Paymentprovider(models.Model):
|
|||
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,
|
||||
'payment_method_id': default_payment_method.id,
|
||||
'journal_id': self.journal_id.id,
|
||||
'payment_provider_id': self.id,
|
||||
'payment_account_id': self._get_payment_method_outstanding_account_id(default_payment_method)
|
||||
}
|
||||
pay_method_line_same_code = self.env['account.payment.method.line'].search(
|
||||
[
|
||||
('company_id', '=', self.company_id.id),
|
||||
('code', '=', self.code),
|
||||
*self.env['account.payment.method.line']._check_company_domain(self.company_id),
|
||||
('code', '=', self._get_code()),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
if pay_method_line_same_code:
|
||||
create_values['payment_account_id'] = pay_method_line_same_code.payment_account_id.id
|
||||
if self._get_code() == 'sepa_direct_debit':
|
||||
create_values['name'] = "Online SEPA"
|
||||
self.env['account.payment.method.line'].create(create_values)
|
||||
|
||||
def _get_payment_method_outstanding_account_id(self, payment_method_id):
|
||||
if self.code == 'custom':
|
||||
return False
|
||||
account_ref = 'account_journal_payment_debit_account_id' if payment_method_id.payment_type == 'inbound' else 'account_journal_payment_credit_account_id'
|
||||
chart_template = self.with_context(allowed_company_ids=self.company_id.root_id.ids).env['account.chart.template']
|
||||
outstanding_account_id = (
|
||||
chart_template.ref(account_ref, raise_if_not_found=False)
|
||||
or self.company_id.transfer_account_id
|
||||
).id
|
||||
return outstanding_account_id
|
||||
|
||||
@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,
|
||||
)
|
||||
pay_method_line = self.env['account.payment.method.line'].search([
|
||||
('payment_provider_id', '=', provider._origin.id),
|
||||
('journal_id', '!=', False),
|
||||
], limit=1)
|
||||
|
||||
if pay_method_line:
|
||||
provider.journal_id = pay_method_line.journal_id
|
||||
|
|
@ -93,13 +110,6 @@ class Paymentprovider(models.Model):
|
|||
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)
|
||||
|
|
@ -107,9 +117,9 @@ class Paymentprovider(models.Model):
|
|||
#=== BUSINESS METHODS ===#
|
||||
|
||||
@api.model
|
||||
def _setup_provider(self, code):
|
||||
def _setup_provider(self, code, **kwargs):
|
||||
""" Override of `payment` to create the payment method of the provider. """
|
||||
super()._setup_provider(code)
|
||||
super()._setup_provider(code, **kwargs)
|
||||
self._setup_payment_method(code)
|
||||
|
||||
@api.model
|
||||
|
|
@ -122,17 +132,16 @@ class Paymentprovider(models.Model):
|
|||
'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)
|
||||
def _check_existing_payment(self, payment_method):
|
||||
existing_payment_count = self.env['account.payment'].search_count([('payment_method_id', '=', payment_method.id)], limit=1)
|
||||
return bool(existing_payment_count)
|
||||
|
||||
@api.model
|
||||
def _remove_provider(self, code):
|
||||
def _remove_provider(self, code, **kwargs):
|
||||
""" 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)
|
||||
# If the payment method is used by any payments, we block the uninstallation of the module.
|
||||
if self._check_existing_payment(payment_method):
|
||||
raise UserError(_("You cannot uninstall this module as payments using this payment method already exist."))
|
||||
super()._remove_provider(code, **kwargs)
|
||||
payment_method.unlink()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, SUPERUSER_ID, _
|
||||
from odoo import SUPERUSER_ID, _, api, fields, models
|
||||
|
||||
|
||||
class PaymentTransaction(models.Model):
|
||||
|
|
@ -59,14 +59,14 @@ class PaymentTransaction(models.Model):
|
|||
action['view_mode'] = 'form'
|
||||
action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
|
||||
else:
|
||||
action['view_mode'] = 'tree,form'
|
||||
action['view_mode'] = 'list,form'
|
||||
action['domain'] = [('id', 'in', invoice_ids)]
|
||||
return action
|
||||
|
||||
#=== BUSINESS METHODS - PAYMENT FLOW ===#
|
||||
|
||||
@api.model
|
||||
def _compute_reference_prefix(self, provider_code, separator, **values):
|
||||
def _compute_reference_prefix(self, 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
|
||||
|
|
@ -75,7 +75,6 @@ class PaymentTransaction(models.Model):
|
|||
|
||||
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), ...], ...}.
|
||||
|
|
@ -88,40 +87,48 @@ class PaymentTransaction(models.Model):
|
|||
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
|
||||
prefix = separator.join(invoices.filtered(lambda inv: inv.name).mapped('name'))
|
||||
if name := values.get('name_next_installment'):
|
||||
prefix = name
|
||||
return prefix
|
||||
return super()._compute_reference_prefix(separator, **values)
|
||||
|
||||
#=== BUSINESS METHODS - POST-PROCESSING ===#
|
||||
|
||||
def _reconcile_after_done(self):
|
||||
""" Post relevant fiscal documents and create missing payments.
|
||||
def _post_process(self):
|
||||
""" Override of `payment` to add account-specific logic to the post-processing.
|
||||
|
||||
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
|
||||
In particular, for confirmed transactions we write a message in the chatter with the payment
|
||||
and transaction references, post relevant fiscal documents, and create missing payments. For
|
||||
cancelled transactions, we cancel the payment.
|
||||
"""
|
||||
super()._reconcile_after_done()
|
||||
super()._post_process()
|
||||
for tx in self.filtered(lambda t: t.state == 'done'):
|
||||
# Validate invoices automatically once the transaction is confirmed.
|
||||
self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()
|
||||
|
||||
# Validate invoices automatically once the transaction is confirmed
|
||||
self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()
|
||||
# Create and post missing payments.
|
||||
# As there is nothing to reconcile for validation transactions, no payment is created
|
||||
# for them. This is also true for validations with or without a validity check (transfer
|
||||
# of a small amount with immediate refund) because validation amounts are not included
|
||||
# in payouts. As the reconciliation is done in the child transactions for partial voids
|
||||
# and captures, no payment is created for their source transactions either.
|
||||
if (
|
||||
tx.operation != 'validation'
|
||||
and not tx.payment_id
|
||||
and not any(child.state in ['done', 'cancel'] for child in tx.child_transaction_ids)
|
||||
):
|
||||
tx.with_company(tx.company_id)._create_payment()
|
||||
|
||||
# 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()
|
||||
if tx.payment_id:
|
||||
message = _(
|
||||
"The payment related to transaction %(ref)s has been posted: %(link)s",
|
||||
ref=tx._get_html_link(),
|
||||
link=tx.payment_id._get_html_link(),
|
||||
)
|
||||
tx._log_message_on_linked_documents(message)
|
||||
for tx in self.filtered(lambda t: t.state == 'cancel'):
|
||||
tx.payment_id.action_cancel()
|
||||
|
||||
def _create_payment(self, **extra_create_values):
|
||||
"""Create an `account.payment` record for the current transaction.
|
||||
|
|
@ -136,6 +143,8 @@ class PaymentTransaction(models.Model):
|
|||
"""
|
||||
self.ensure_one()
|
||||
|
||||
reference = f'{self.reference} - {self.provider_reference or ""}'
|
||||
|
||||
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 = {
|
||||
|
|
@ -149,19 +158,52 @@ class PaymentTransaction(models.Model):
|
|||
'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 ""}',
|
||||
'memo': reference,
|
||||
'write_off_line_vals': [],
|
||||
'invoice_ids': self.invoice_ids,
|
||||
**extra_create_values,
|
||||
}
|
||||
|
||||
for invoice in self.invoice_ids:
|
||||
if invoice.state != 'posted':
|
||||
continue
|
||||
next_payment_values = invoice._get_invoice_next_payment_values()
|
||||
if next_payment_values['installment_state'] == 'epd' and self.amount == next_payment_values['amount_due']:
|
||||
aml = next_payment_values['epd_line']
|
||||
epd_aml_values_list = [({
|
||||
'aml': aml,
|
||||
'amount_currency': -aml.amount_residual_currency,
|
||||
'balance': -aml.balance,
|
||||
})]
|
||||
open_balance = next_payment_values['epd_discount_amount']
|
||||
early_payment_values = self.env['account.move']._get_invoice_counterpart_amls_for_early_payment_discount(epd_aml_values_list, open_balance)
|
||||
for aml_values_list in early_payment_values.values():
|
||||
if (aml_values_list):
|
||||
aml_vl = aml_values_list[0]
|
||||
aml_vl['partner_id'] = invoice.partner_id.id
|
||||
payment_values['write_off_line_vals'] += [aml_vl]
|
||||
break
|
||||
|
||||
payment_term_lines = self.invoice_ids.line_ids.filtered(lambda line: line.display_type == 'payment_term')
|
||||
if payment_term_lines:
|
||||
payment_values['destination_account_id'] = payment_term_lines[0].account_id.id
|
||||
|
||||
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()
|
||||
# Reconcile the payment with the source transaction's invoices in case of a partial capture.
|
||||
if self.operation == self.source_transaction_id.operation:
|
||||
invoices = self.source_transaction_id.invoice_ids
|
||||
else:
|
||||
invoices = self.invoice_ids
|
||||
invoices = invoices.filtered(lambda inv: inv.state != 'cancel')
|
||||
if invoices:
|
||||
invoices.filtered(lambda inv: inv.state == 'draft').action_post()
|
||||
|
||||
(payment.line_ids + self.invoice_ids.line_ids).filtered(
|
||||
(payment.move_id.line_ids + invoices.line_ids).filtered(
|
||||
lambda line: line.account_id == payment.destination_account_id
|
||||
and not line.reconciled
|
||||
).reconcile()
|
||||
|
|
@ -182,26 +224,19 @@ class PaymentTransaction(models.Model):
|
|||
: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)
|
||||
if self.env.uid == SUPERUSER_ID or self.env.context.get('payment_backend_action'):
|
||||
author = self.env.user.partner_id
|
||||
else:
|
||||
author = self.partner_id
|
||||
if self.source_transaction_id:
|
||||
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)
|
||||
invoice.message_post(body=message, author_id=author.id)
|
||||
payment_id = self.source_transaction_id.payment_id
|
||||
if payment_id:
|
||||
payment_id.message_post(body=message, author_id=author.id)
|
||||
for invoice in self._get_invoices_to_notify():
|
||||
invoice.message_post(body=message, author_id=author.id)
|
||||
|
||||
#=== 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)
|
||||
def _get_invoices_to_notify(self):
|
||||
""" Return the invoices on which to log payment-related messages. """
|
||||
return self.invoice_ids
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue