709 lines
26 KiB
Python
709 lines
26 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import base64
|
|
import json
|
|
import logging
|
|
|
|
from werkzeug.urls import url_encode
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.tools.urls import urljoin as url_join
|
|
|
|
from odoo.addons.fusion_clover import const
|
|
from odoo.addons.fusion_clover import utils as clover_utils
|
|
from odoo.addons.fusion_clover.controllers.main import CloverController
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PaymentTransaction(models.Model):
|
|
_inherit = 'payment.transaction'
|
|
|
|
clover_charge_id = fields.Char(
|
|
string="Clover Charge ID",
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
clover_refund_id = fields.Char(
|
|
string="Clover Refund ID",
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
clover_receipt_data = fields.Text(
|
|
string="Clover Receipt Data",
|
|
readonly=True,
|
|
copy=False,
|
|
help="JSON blob with receipt-relevant fields captured at payment time.",
|
|
)
|
|
clover_order_id = fields.Char(
|
|
string="Clover Order ID",
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
clover_voided = fields.Boolean(
|
|
string="Voided",
|
|
default=False,
|
|
copy=False,
|
|
)
|
|
clover_void_date = fields.Datetime(
|
|
string="Void Date",
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
|
|
def _get_provider_sudo(self):
|
|
return self.provider_id.sudo()
|
|
|
|
# === BUSINESS METHODS - PAYMENT FLOW === #
|
|
|
|
def _get_specific_processing_values(self, processing_values):
|
|
"""Override of payment to return Clover-specific processing values."""
|
|
if self.provider_code != 'clover':
|
|
return super()._get_specific_processing_values(processing_values)
|
|
|
|
if self.operation == 'online_token':
|
|
return {}
|
|
|
|
provider = self._get_provider_sudo()
|
|
base_url = provider.get_base_url()
|
|
return_url = url_join(
|
|
base_url,
|
|
f'{CloverController._return_url}?{url_encode({"reference": self.reference})}',
|
|
)
|
|
|
|
return {
|
|
'return_url': return_url,
|
|
'merchant_id': provider.clover_merchant_id,
|
|
'is_test': provider.state == 'test',
|
|
}
|
|
|
|
def _send_payment_request(self):
|
|
"""Override of `payment` to send a payment request to Clover."""
|
|
if self.provider_code != 'clover':
|
|
return super()._send_payment_request()
|
|
|
|
if self.operation in ('online_token', 'offline'):
|
|
return self._clover_process_token_payment()
|
|
|
|
@staticmethod
|
|
def _detect_card_brand_from_details(payment_details):
|
|
"""Detect card brand from the payment_details string on a token."""
|
|
details = (payment_details or '').upper()
|
|
if 'AMEX' in details or 'AMERICAN_EXPRESS' in details:
|
|
return 'amex'
|
|
if 'VISA' in details:
|
|
return 'visa'
|
|
if 'MASTER' in details:
|
|
return 'mastercard'
|
|
return 'other'
|
|
|
|
def _apply_token_surcharge(self):
|
|
"""Apply surcharge to the linked invoice for token-based payments."""
|
|
ICP = self.env['ir.config_parameter'].sudo()
|
|
if ICP.get_param('fusion_clover.surcharge_enabled', 'False') != 'True':
|
|
return
|
|
|
|
if not self.token_id or not self.invoice_ids:
|
|
return
|
|
|
|
for inv in self.invoice_ids:
|
|
sale_orders = inv.mapped('line_ids.sale_line_ids.order_id')
|
|
for so in sale_orders:
|
|
if getattr(so, 'is_rental_order', False):
|
|
if not getattr(so, 'rental_apply_cc_fee', True):
|
|
return
|
|
|
|
card_type = self._detect_card_brand_from_details(
|
|
self.token_id.payment_details,
|
|
)
|
|
rate_key = {
|
|
'visa': 'fusion_clover.surcharge_visa_rate',
|
|
'mastercard': 'fusion_clover.surcharge_mastercard_rate',
|
|
'amex': 'fusion_clover.surcharge_amex_rate',
|
|
'debit': 'fusion_clover.surcharge_debit_rate',
|
|
}.get(card_type, 'fusion_clover.surcharge_other_rate')
|
|
rate = float(ICP.get_param(rate_key, '0') or 0)
|
|
if rate <= 0:
|
|
return
|
|
|
|
product_id = int(ICP.get_param('fusion_clover.surcharge_product_id', '0') or 0)
|
|
product = self.env['product.product'].sudo().browse(product_id).exists()
|
|
if not product:
|
|
product = self.env.ref(
|
|
'fusion_clover.product_cc_processing_fee', raise_if_not_found=False,
|
|
)
|
|
if not product:
|
|
_logger.warning("Surcharge product not configured; skipping token surcharge")
|
|
return
|
|
|
|
total_fee = 0.0
|
|
for invoice in self.invoice_ids.sudo():
|
|
already_has = invoice.invoice_line_ids.filtered(
|
|
lambda l: l.product_id.id == product.id
|
|
)
|
|
if already_has:
|
|
continue
|
|
|
|
fee_amount = round(invoice.amount_residual * rate / 100.0, 2)
|
|
if fee_amount <= 0:
|
|
continue
|
|
|
|
was_posted = invoice.state == 'posted'
|
|
if was_posted:
|
|
invoice.button_draft()
|
|
|
|
description = "Credit Card Processing Fee (%.2f%% surcharge)" % rate
|
|
invoice.write({
|
|
'invoice_line_ids': [(0, 0, {
|
|
'product_id': product.id,
|
|
'name': description,
|
|
'quantity': 1,
|
|
'price_unit': fee_amount,
|
|
'tax_ids': [(5, 0, 0)],
|
|
})],
|
|
})
|
|
|
|
if was_posted:
|
|
invoice.action_post()
|
|
|
|
total_fee += fee_amount
|
|
|
|
if total_fee > 0:
|
|
self.amount += total_fee
|
|
|
|
def _clover_process_token_payment(self):
|
|
"""Process a payment using a stored token (card on file)."""
|
|
try:
|
|
self._apply_token_surcharge()
|
|
|
|
provider = self._get_provider_sudo()
|
|
capture = not provider.capture_manually
|
|
clover_token = self.token_id.clover_source_token
|
|
|
|
if not clover_token:
|
|
self._set_error(_("No Clover token found for this saved card."))
|
|
return
|
|
|
|
result = provider._clover_create_charge(
|
|
source_token=clover_token,
|
|
amount=self.amount,
|
|
currency=self.currency_id,
|
|
capture=capture,
|
|
description=self.reference,
|
|
ecomind='moto',
|
|
metadata={'odoo_reference': self.reference},
|
|
)
|
|
|
|
charge_id = result.get('id', '')
|
|
status = result.get('status', '')
|
|
|
|
self.clover_charge_id = charge_id
|
|
self.provider_reference = charge_id
|
|
|
|
payment_data = {
|
|
'reference': self.reference,
|
|
'clover_charge_id': charge_id,
|
|
'clover_status': status,
|
|
'source': result.get('source', {}),
|
|
}
|
|
|
|
if status == 'failed':
|
|
outcome = result.get('outcome', {})
|
|
decline_msg = outcome.get('type', status)
|
|
self._set_error(
|
|
_("Payment %(status)s: %(reason)s",
|
|
status=status, reason=decline_msg)
|
|
)
|
|
return
|
|
|
|
self._process('clover', payment_data)
|
|
except ValidationError as e:
|
|
self._set_error(str(e))
|
|
|
|
def _send_refund_request(self):
|
|
"""Override of `payment` to send a refund request to Clover."""
|
|
if self.provider_code != 'clover':
|
|
return super()._send_refund_request()
|
|
|
|
source_tx = self.source_transaction_id
|
|
charge_id = source_tx.clover_charge_id or source_tx.provider_reference
|
|
refund_amount = abs(self.amount)
|
|
|
|
try:
|
|
result = self._get_provider_sudo()._clover_create_refund(
|
|
charge_id=charge_id,
|
|
amount=refund_amount,
|
|
currency=self.currency_id,
|
|
reason=f'Refund for {source_tx.reference}',
|
|
)
|
|
|
|
refund_id = result.get('id', '')
|
|
self.provider_reference = refund_id
|
|
self.clover_refund_id = refund_id
|
|
|
|
payment_data = {
|
|
'reference': self.reference,
|
|
'clover_charge_id': charge_id,
|
|
'clover_refund_id': refund_id,
|
|
'clover_status': result.get('status', 'succeeded'),
|
|
}
|
|
self._process('clover', payment_data)
|
|
except ValidationError as e:
|
|
self._set_error(str(e))
|
|
|
|
def _send_capture_request(self):
|
|
"""Override of `payment` to send a capture request to Clover."""
|
|
if self.provider_code != 'clover':
|
|
return super()._send_capture_request()
|
|
|
|
source_tx = self.source_transaction_id
|
|
charge_id = source_tx.clover_charge_id or source_tx.provider_reference
|
|
|
|
try:
|
|
result = self._get_provider_sudo()._clover_capture_charge(
|
|
charge_id=charge_id,
|
|
amount=self.amount,
|
|
currency=self.currency_id,
|
|
)
|
|
|
|
payment_data = {
|
|
'reference': self.reference,
|
|
'clover_charge_id': result.get('id', charge_id),
|
|
'clover_status': result.get('status', 'succeeded'),
|
|
}
|
|
self._process('clover', payment_data)
|
|
except ValidationError as e:
|
|
self._set_error(str(e))
|
|
|
|
def _send_void_request(self):
|
|
"""Override of `payment` to send a void (refund full) request to Clover.
|
|
|
|
Clover doesn't have a dedicated void endpoint -- a full refund before
|
|
settlement acts as a void.
|
|
"""
|
|
if self.provider_code != 'clover':
|
|
return super()._send_void_request()
|
|
|
|
source_tx = self.source_transaction_id
|
|
charge_id = source_tx.clover_charge_id or source_tx.provider_reference
|
|
|
|
try:
|
|
result = self._get_provider_sudo()._clover_create_refund(
|
|
charge_id=charge_id,
|
|
reason=f'Void for {source_tx.reference}',
|
|
)
|
|
|
|
payment_data = {
|
|
'reference': self.reference,
|
|
'clover_charge_id': charge_id,
|
|
'clover_refund_id': result.get('id', ''),
|
|
'clover_status': result.get('status', 'succeeded'),
|
|
}
|
|
self._process('clover', payment_data)
|
|
except ValidationError as e:
|
|
self._set_error(str(e))
|
|
|
|
# === ACTION METHODS - VOID === #
|
|
|
|
def action_clover_void(self):
|
|
"""Void a confirmed Clover transaction (same-day, before settlement).
|
|
|
|
Clover's Ecommerce API treats a full refund on an unsettled charge as a
|
|
void. We issue ``POST /v1/refunds`` for the full amount; if the charge
|
|
has already settled, the processor will decline the void (the user
|
|
should create a credit note and use the refund wizard instead).
|
|
"""
|
|
self.ensure_one()
|
|
if self.provider_code != 'clover':
|
|
raise ValidationError(_("This action is only available for Clover transactions."))
|
|
if self.state != 'done':
|
|
raise ValidationError(_("Only confirmed transactions can be voided."))
|
|
|
|
charge_id = self.clover_charge_id or self.provider_reference
|
|
if not charge_id:
|
|
raise ValidationError(_("No Clover charge ID found."))
|
|
|
|
# Guard against double reversal
|
|
existing_refund = self.env['payment.transaction'].sudo().search([
|
|
('source_transaction_id', '=', self.id),
|
|
('operation', '=', 'refund'),
|
|
('state', '=', 'done'),
|
|
], limit=1)
|
|
if existing_refund:
|
|
raise ValidationError(_(
|
|
"This transaction has already been refunded "
|
|
"(%(ref)s). Voiding would result in a double reversal.",
|
|
ref=existing_refund.reference,
|
|
))
|
|
|
|
provider = self._get_provider_sudo()
|
|
|
|
# Verify on Clover the charge hasn't already been refunded
|
|
try:
|
|
charge_data = provider._clover_make_ecom_request(
|
|
'GET', f'v1/charges/{charge_id}',
|
|
)
|
|
charge_status = charge_data.get('status', '')
|
|
if charge_status == 'refunded':
|
|
raise ValidationError(_(
|
|
"This charge has already been refunded on Clover. "
|
|
"It cannot be voided again."
|
|
))
|
|
except ValidationError:
|
|
raise
|
|
except Exception:
|
|
_logger.debug("Could not verify charge %s before void", charge_id)
|
|
|
|
# Issue full refund (acts as void before settlement)
|
|
try:
|
|
result = provider._clover_create_refund(
|
|
charge_id=charge_id,
|
|
reason=f'Void for {self.reference}',
|
|
)
|
|
except ValidationError as e:
|
|
error_msg = str(e)
|
|
if '400' in error_msg or 'declined' in error_msg.lower():
|
|
raise ValidationError(_(
|
|
"Void declined by the payment processor. This usually "
|
|
"means the batch has already settled. Settled transactions "
|
|
"cannot be voided.\n\n"
|
|
"To reverse this payment, create a Credit Note on the "
|
|
"invoice and process a refund through the Clover refund "
|
|
"wizard."
|
|
))
|
|
raise
|
|
|
|
_logger.info(
|
|
"Clover void response: id=%s, status=%s",
|
|
result.get('id', ''), result.get('status', ''),
|
|
)
|
|
|
|
# Cancel the Odoo payment
|
|
if self.payment_id:
|
|
self.payment_id.sudo().action_cancel()
|
|
|
|
self.sudo().write({
|
|
'state': 'cancel',
|
|
'clover_voided': True,
|
|
'clover_void_date': fields.Datetime.now(),
|
|
})
|
|
|
|
invoice = self.invoice_ids[:1]
|
|
if invoice:
|
|
invoice.sudo().message_post(
|
|
body=_(
|
|
"Payment voided: transaction %(ref)s was voided on Clover "
|
|
"(Clover Refund ID: %(refund_id)s).",
|
|
ref=self.reference,
|
|
refund_id=result.get('id', ''),
|
|
),
|
|
)
|
|
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'type': 'success',
|
|
'message': _("Transaction voided successfully on Clover."),
|
|
'next': {'type': 'ir.actions.client', 'tag': 'soft_reload'},
|
|
},
|
|
}
|
|
|
|
# === BUSINESS METHODS - NOTIFICATION PROCESSING === #
|
|
|
|
@api.model
|
|
def _search_by_reference(self, provider_code, payment_data):
|
|
"""Override of payment to find the transaction based on Clover data."""
|
|
if provider_code != 'clover':
|
|
return super()._search_by_reference(provider_code, payment_data)
|
|
|
|
reference = payment_data.get('reference')
|
|
if reference:
|
|
tx = self.search([
|
|
('reference', '=', reference),
|
|
('provider_code', '=', 'clover'),
|
|
])
|
|
else:
|
|
charge_id = payment_data.get('clover_charge_id')
|
|
if charge_id:
|
|
tx = self.search([
|
|
('clover_charge_id', '=', charge_id),
|
|
('provider_code', '=', 'clover'),
|
|
])
|
|
else:
|
|
_logger.warning("Received Clover data with no reference or charge ID")
|
|
tx = self
|
|
|
|
if not tx:
|
|
_logger.warning(
|
|
"No transaction found matching Clover reference %s", reference,
|
|
)
|
|
|
|
return tx
|
|
|
|
def _extract_amount_data(self, payment_data):
|
|
"""Override of `payment` for the Odoo 19 amount-tamper check.
|
|
|
|
Odoo's ``_validate_amount`` calls this and KErrors if the dict
|
|
doesn't contain ``amount`` + ``currency_code``. Other providers
|
|
always include these in their ``payment_data``; we historically
|
|
didn't. Extract from the Clover charge response shape:
|
|
|
|
- source token charges (Ecommerce):
|
|
``{'amount': <cents>, 'currency': 'usd', ...}`` (raw Clover
|
|
response usually shoved into ``payment_data['raw']``)
|
|
- terminal payments: ``payment_data['amount']`` (we set this
|
|
explicitly when we build the dict)
|
|
|
|
Returns ``None`` to opt out of the amount check when no usable
|
|
amount field is present — the alternative (hard-failing the
|
|
transaction) is worse than skipping validation, and our charge
|
|
amounts are server-controlled (we send them; Clover doesn't
|
|
invent new amounts).
|
|
"""
|
|
if self.provider_code != 'clover':
|
|
return super()._extract_amount_data(payment_data)
|
|
|
|
amount_minor = payment_data.get('amount')
|
|
currency_code = payment_data.get('currency')
|
|
# Some code paths put the raw Clover response under 'raw' or pass
|
|
# the whole charge dict — try those fallbacks before opting out.
|
|
if amount_minor is None and isinstance(payment_data.get('raw'), dict):
|
|
amount_minor = payment_data['raw'].get('amount')
|
|
currency_code = payment_data['raw'].get('currency') or currency_code
|
|
if amount_minor is None or not currency_code:
|
|
return None # opt out of amount validation (server-controlled)
|
|
|
|
# Clover amounts are in minor units (cents for USD/CAD, no
|
|
# decimals for JPY/KRW). Convert back to major units using the
|
|
# transaction's currency to know the divisor.
|
|
from odoo.addons.fusion_clover import utils as clover_utils
|
|
amount_major = clover_utils.parse_clover_amount(
|
|
int(amount_minor), self.currency_id,
|
|
)
|
|
return {
|
|
'amount': float(amount_major),
|
|
'currency_code': str(currency_code).upper(),
|
|
}
|
|
|
|
def _apply_updates(self, payment_data):
|
|
"""Override of `payment` to update the transaction based on Clover data."""
|
|
if self.provider_code != 'clover':
|
|
return super()._apply_updates(payment_data)
|
|
|
|
charge_id = payment_data.get('clover_charge_id')
|
|
if charge_id:
|
|
self.provider_reference = charge_id
|
|
self.clover_charge_id = charge_id
|
|
|
|
refund_id = payment_data.get('clover_refund_id')
|
|
if refund_id:
|
|
self.clover_refund_id = refund_id
|
|
|
|
source = payment_data.get('source', {})
|
|
if source:
|
|
card_details = clover_utils.extract_card_details(source)
|
|
if card_details.get('brand'):
|
|
payment_method = self.env['payment.method']._get_from_code(
|
|
card_details['brand'],
|
|
mapping=const.CARD_BRAND_MAPPING,
|
|
)
|
|
if payment_method:
|
|
self.payment_method_id = payment_method
|
|
|
|
status = payment_data.get('clover_status', '')
|
|
if not status:
|
|
self._set_error(_("Received data with missing transaction status."))
|
|
return
|
|
|
|
odoo_state = clover_utils.get_clover_status(status)
|
|
|
|
if odoo_state == 'authorized':
|
|
self._set_authorized()
|
|
elif odoo_state == 'done':
|
|
self._set_done()
|
|
self._post_process()
|
|
self._clover_generate_receipt(payment_data)
|
|
elif odoo_state == 'cancel':
|
|
self._set_canceled()
|
|
elif odoo_state == 'refund':
|
|
self._set_done()
|
|
self._post_process()
|
|
self._clover_generate_receipt(payment_data)
|
|
elif odoo_state == 'error':
|
|
error_msg = payment_data.get('error_message', _("Payment was declined by Clover."))
|
|
self._set_error(error_msg)
|
|
else:
|
|
_logger.warning(
|
|
"Received unknown Clover status (%s) for transaction %s.",
|
|
status, self.reference,
|
|
)
|
|
self._set_error(
|
|
_("Received data with unrecognized status: %s.", status)
|
|
)
|
|
|
|
def _create_payment(self, **extra_create_values):
|
|
"""Override to route Clover payments directly to the bank account."""
|
|
if self.provider_code != 'clover':
|
|
return super()._create_payment(**extra_create_values)
|
|
|
|
self.ensure_one()
|
|
provider = self._get_provider_sudo()
|
|
reference = f'{self.reference} - {self.provider_reference or ""}'
|
|
payment_method_line = provider.journal_id.inbound_payment_method_line_ids\
|
|
.filtered(lambda l: l.payment_provider_id == provider)
|
|
payment_values = {
|
|
'amount': abs(self.amount),
|
|
'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': provider.journal_id.id,
|
|
'company_id': provider.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,
|
|
}
|
|
|
|
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)
|
|
|
|
bank_account = provider.journal_id.default_account_id
|
|
if bank_account and bank_account.account_type == 'asset_cash':
|
|
payment.outstanding_account_id = bank_account
|
|
|
|
payment.action_post()
|
|
self.payment_id = payment
|
|
|
|
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.move_id.line_ids + invoices.line_ids).filtered(
|
|
lambda line: line.account_id == payment.destination_account_id
|
|
and not line.reconciled
|
|
).reconcile()
|
|
|
|
return payment
|
|
|
|
def _extract_token_values(self, payment_data):
|
|
"""Override of `payment` to return token data based on Clover data."""
|
|
if self.provider_code != 'clover':
|
|
return super()._extract_token_values(payment_data)
|
|
|
|
source = payment_data.get('source', {})
|
|
card_details = clover_utils.extract_card_details(source)
|
|
|
|
if not card_details:
|
|
_logger.warning(
|
|
"Tokenization requested but no card data in payment response."
|
|
)
|
|
return {}
|
|
|
|
return {
|
|
'payment_details': card_details.get('last4', ''),
|
|
'clover_source_token': source.get('id', ''),
|
|
}
|
|
|
|
# === RECEIPT GENERATION === #
|
|
|
|
def _clover_generate_receipt(self, payment_data=None):
|
|
"""Store receipt data and generate a PDF receipt."""
|
|
self.ensure_one()
|
|
if self.provider_code != 'clover' or not self.clover_charge_id:
|
|
return
|
|
|
|
try:
|
|
self._clover_store_receipt_data(payment_data)
|
|
self._clover_attach_receipt_pdf()
|
|
except Exception:
|
|
_logger.exception(
|
|
"Receipt generation failed for transaction %s", self.reference,
|
|
)
|
|
|
|
def _clover_store_receipt_data(self, payment_data=None):
|
|
"""Persist receipt-relevant fields as a JSON blob."""
|
|
source = payment_data.get('source', {}) if payment_data else {}
|
|
|
|
receipt = {
|
|
'charge_id': self.clover_charge_id or '',
|
|
'reference': self.reference,
|
|
'status': payment_data.get('clover_status', '') if payment_data else '',
|
|
'card_brand': source.get('brand', ''),
|
|
'card_last4': str(source.get('last4', '')),
|
|
'card_first6': str(source.get('first6', '')),
|
|
'exp_month': source.get('exp_month', ''),
|
|
'exp_year': source.get('exp_year', ''),
|
|
'transaction_amount': float(self.amount),
|
|
'currency': self.currency_id.name,
|
|
}
|
|
|
|
self.clover_receipt_data = json.dumps(receipt)
|
|
|
|
def _clover_attach_receipt_pdf(self):
|
|
"""Render the QWeb receipt report and attach the PDF to the invoice."""
|
|
invoice = self.invoice_ids[:1]
|
|
if not invoice:
|
|
return
|
|
|
|
try:
|
|
report = self.env.ref('fusion_clover.action_report_clover_receipt')
|
|
pdf_content, _report_type = report._render_qweb_pdf(report.report_name, [self.id])
|
|
except Exception:
|
|
_logger.debug("Could not render Clover receipt PDF for %s", self.reference)
|
|
return
|
|
|
|
filename = f"Payment_Receipt_{self.reference}.pdf"
|
|
attachment = self.env['ir.attachment'].sudo().create({
|
|
'name': filename,
|
|
'type': 'binary',
|
|
'datas': base64.b64encode(pdf_content),
|
|
'res_model': 'account.move',
|
|
'res_id': invoice.id,
|
|
'mimetype': 'application/pdf',
|
|
})
|
|
|
|
invoice.sudo().message_post(
|
|
body=_(
|
|
"Payment receipt generated for transaction %(ref)s.",
|
|
ref=self.reference,
|
|
),
|
|
attachment_ids=[attachment.id],
|
|
)
|
|
|
|
def _get_clover_receipt_values(self):
|
|
"""Parse the stored receipt JSON for use in QWeb templates."""
|
|
self.ensure_one()
|
|
data = self.clover_receipt_data
|
|
if not data and self.source_transaction_id:
|
|
data = self.source_transaction_id.clover_receipt_data
|
|
if not data:
|
|
return {}
|
|
try:
|
|
return json.loads(data)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return {}
|
|
|
|
def _get_source_receipt_values(self):
|
|
"""Return receipt values from the original sale transaction."""
|
|
self.ensure_one()
|
|
if self.source_transaction_id and self.source_transaction_id.clover_receipt_data:
|
|
try:
|
|
return json.loads(self.source_transaction_id.clover_receipt_data)
|
|
except (json.JSONDecodeError, TypeError):
|
|
pass
|
|
return {}
|