1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/sale_subscription/models/payment_transaction.py
2024-12-10 09:04:09 +07:00

220 lines
11 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, time
from odoo import api, fields, models
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
# used to control the renewal flow based on the transaction state
renewal_state = fields.Selection([('draft', 'Draft'),
('pending', 'Pending'),
('authorized', 'Authorized'),
('cancel', 'Refused')], compute='_compute_renewal_state')
subscription_action = fields.Selection([
('automatic_send_mail', 'Send Mail (automatic payment)'),
('manual_send_mail', 'Send Mail (manual payment)'),
('assign_token', 'Assign Token'),
])
@api.depends('state')
def _compute_renewal_state(self):
for tx in self:
if tx.state in ['draft', 'pending']:
renewal_state = tx.state
elif tx.state in ('done', 'authorized'):
renewal_state = 'authorized'
else:
# tx state in cancel or error
renewal_state = 'cancel'
tx.renewal_state = renewal_state
####################
# Business Methods #
####################
def _get_mandate_values(self):
""" Override of `payment` to inject subscription-specific data into the mandate values.
Note: `self.ensure_one()`
:return: The dict of module-specific mandate values.
:rtype: dict
"""
mandate_values = super()._get_mandate_values()
if len(self.sale_order_ids) != 1 or not self.sale_order_ids.is_subscription:
return mandate_values
# Convert start and end dates into datetime by setting the time to midnight.
start_datetime = self.sale_order_ids.start_date \
and datetime.combine(self.sale_order_ids.start_date, time())
end_datetime = self.sale_order_ids.end_date \
and datetime.combine(self.sale_order_ids.end_date, time())
mandate_values.update({
# The maximum amount that can be charged with the mandated.
'amount': self.sale_order_ids.amount_total,
'MRR': self.sale_order_ids.recurring_monthly,
'start_datetime': start_datetime,
'end_datetime': end_datetime,
'recurrence_unit': self.sale_order_ids.plan_id.billing_period_unit,
'recurrence_duration': self.sale_order_ids.plan_id.billing_period_value,
})
return mandate_values
def _create_or_link_to_invoice(self):
tx_to_invoice = self.env['payment.transaction']
for tx in self:
if len(tx.sale_order_ids) > 1 or tx.invoice_ids or not tx.sale_order_ids.is_subscription:
continue
elif tx.renewal_state in ['draft', 'pending', 'cancel']:
# tx should be in an authorized renewal_state otherwise _reconcile_after_done will not be called
# but this is a safety to prevent issue when the code is called manually
continue
tx_to_invoice += tx
tx._cancel_draft_invoices()
tx_to_invoice._invoice_sale_orders()
tx_to_invoice.invoice_ids.with_company(self.company_id)._post()
tx_to_invoice.filtered(lambda t: not t.subscription_action).invoice_ids.transaction_ids._send_invoice()
def _reconcile_after_done(self):
# override to force invoice creation if the transaction is done for a subscription
# We don't take care of the sale.automatic_invoice parameter in that case.
res = super()._reconcile_after_done()
self.filtered(lambda tx: tx.operation != 'validation').with_context(forced_invoice=True)._create_or_link_to_invoice()
self._post_subscription_action()
return res
def _get_invoiced_subscription_transaction(self):
# create the invoices for the transactions that are not yet linked to invoice
# `_do_payment` do link an invoice to the payment transaction
# calling `super()._invoice_sale_orders()` would create a second invoice for the next period
# instead of the current period and would reconcile the payment with the new invoice
def _filter_invoiced_subscription(self):
self.ensure_one()
# we look for tx with one invoice
if len(self.invoice_ids) != 1:
return False
return any(self.invoice_ids.mapped('invoice_line_ids.sale_line_ids.order_id.is_subscription'))
return self.filtered(_filter_invoiced_subscription)
def _get_partial_payment_subscription_transaction(self):
# filter transaction which are only a partial payment of subscription and that don't fulfill a payment that
# is already existing
tx_with_partial_payments = self.env["payment.transaction"]
for tx in self:
order = tx.sale_order_ids.filtered(lambda so: so.state == 'sale')
if not any(order.mapped('is_subscription')):
# not subscription related
continue
elif len(order) > 1:
# we don't support multiple order per tx. Accounting should invoice manually
tx_with_partial_payments |= tx
elif order.currency_id.compare_amounts(
sum(order.transaction_ids.filtered(lambda tx: tx.renewal_state == 'authorized' and not tx.invoice_ids).mapped('amount')),
order.amount_total
) != 0:
# The payment amount and other unused transactions will confirm and pay the invoice
tx_with_partial_payments |= tx
return tx_with_partial_payments
def _invoice_sale_orders(self):
""" Override of payment to increase next_invoice_date when needed. """
transaction_to_invoice = self - self._get_invoiced_subscription_transaction()
transaction_to_invoice -= self._get_partial_payment_subscription_transaction()
# Update the next_invoice_date of SOL when the payment_mode is 'success_payment'
# We have to do it here because when a client confirms and pay a SO from the portal with success_payment
# The next_invoice_date won't be updated by the reconcile_pending_transaction callback (do_payment is not called)
# Create invoice
res = super(PaymentTransaction, transaction_to_invoice)._invoice_sale_orders()
return res
def _finalize_post_processing(self):
""" Override of `payment` to handle reconcilation for subscription's validation transaction.
references.
`super()._finalize_post_processing` never call `_reconcile_after_done` on validation tx.
We explicitely calls it here to make sure the token is assigned.
:return: None
"""
# Avoid post processing tx whose SO is still being processed by the invoice cron
process_tx = self.filtered(lambda tx: not any(tx.sale_order_ids.mapped('is_invoice_cron')))
process_tx.filtered(lambda tx: tx.operation == 'validation' and tx.sale_order_ids.is_subscription)._reconcile_after_done()
super(PaymentTransaction, process_tx)._finalize_post_processing()
def _post_subscription_action(self):
"""
Execute the subscription action once the transaction is in an acceptable state
This will also reopen the order and remove the payment pending state.
Partial payment should not have a subscription_action defined and therefore should not reopen the order.
"""
for tx in self:
orders = tx.sale_order_ids
# quotation subscription paid on portal have pending transactions
orders.pending_transaction = False
if not tx.subscription_action or tx.renewal_state != 'authorized':
# We don't assign failing tokens, and we don't send emails
continue
if tx.subscription_action == 'assign_token':
orders._assign_token(tx)
if tx.operation == 'validation':
# validation transaction have the `assign_token` `subscription_action`
# Once the token is assigned, we are done because we don't send emails in that case.
continue
orders.set_open()
orders._send_success_mail(tx.invoice_ids, tx)
if tx.subscription_action in ['manual_send_mail', 'automatic_send_mail']:
automatic = tx.subscription_action == 'automatic_send_mail'
for order in orders:
order._subscription_post_success_payment(tx, tx.invoice_ids, automatic=automatic)
def _send_invoice(self):
subscription_action_txs = self.filtered(lambda tx: (
tx.operation != 'validation'
and tx.subscription_action
and tx.renewal_state == 'authorized'
and not tx.is_post_processed
))
# we're going to send subscription invoices with a specific
# email, so skip those from here to not send them a second time
return super(PaymentTransaction, self - subscription_action_txs)._send_invoice()
def _set_done(self, **kwargs):
self.sale_order_ids.filtered('is_subscription').payment_exception = False
return super()._set_done(**kwargs)
def _set_pending(self, **kwargs):
self.sale_order_ids.filtered('is_subscription').payment_exception = False
return super()._set_pending(**kwargs)
def _set_authorize(self, **kwargs):
self.sale_order_ids.filtered('is_subscription').payment_exception = False
return super()._set_authorize(**kwargs)
def _set_canceled(self, state_message=None, **kwargs):
self._handle_unsuccessful_transaction()
return super()._set_canceled(state_message, **kwargs)
def _set_error(self, state_message):
self._handle_unsuccessful_transaction()
return super()._set_error(state_message)
def _handle_unsuccessful_transaction(self):
""" Unset pending transactions for subscriptions and cancel their draft invoices. """
for transaction in self:
subscriptions = transaction.sale_order_ids.filtered('is_subscription')
if subscriptions:
subscriptions.pending_transaction = False
transaction._cancel_draft_invoices()
def _cancel_draft_invoices(self):
""" Cancel draft invoices attached to subscriptions. """
self.ensure_one()
subscriptions = self.sale_order_ids.filtered('is_subscription')
draft_invoices = subscriptions.order_line.invoice_lines.move_id.filtered(lambda am: am.state == 'draft')
if draft_invoices:
draft_invoices.state = 'cancel'