1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
|
# 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'] = '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):
""" 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
prefix = separator.join(invoices.mapped('name'))
if name := values.get('name_next_installment'):
prefix = name
return prefix
return super()._compute_reference_prefix(provider_code, separator, **values)
#=== BUSINESS METHODS - POST-PROCESSING ===#
def _post_process(self):
""" Override of `payment` to add account-specific logic to the post-processing.
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()._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()
# 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()
if tx.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)
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.
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()
reference = (f'{self.reference} - '
f'{self.partner_id.display_name or ""} - '
f'{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 = {
'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,
'memo': reference,
'write_off_line_vals': [],
'invoice_ids': self.invoice_ids,
**extra_create_values,
}
if self.invoice_ids:
next_payment_values = self.invoice_ids._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'] = self.partner_id.id
payment_values['write_off_line_vals'] += [aml_vl]
payment = self.env['account.payment'].create(payment_values)
payment.action_post()
# Track the payment to make a one2one.
self.payment_id = payment
# 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
if invoices:
invoices.filtered(lambda inv: inv.state == 'draft').action_post()
(payment.move_id.line_ids + invoices.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()
author = self.env.user.partner_id if self.env.uid == SUPERUSER_ID else self.partner_id
if self.source_transaction_id:
for invoice in self.source_transaction_id.invoice_ids:
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.invoice_ids:
invoice.message_post(body=message, author_id=author.id)
|