666 lines
24 KiB
Python
666 lines
24 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
from datetime import timedelta
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PoyntSettlementBatch(models.Model):
|
|
_name = 'poynt.settlement.batch'
|
|
_description = 'Poynt Settlement Batch'
|
|
_order = 'settlement_date desc, id desc'
|
|
_rec_name = 'name'
|
|
|
|
name = fields.Char(
|
|
string="Batch Reference",
|
|
required=True,
|
|
readonly=True,
|
|
default='/',
|
|
copy=False,
|
|
)
|
|
settlement_date = fields.Date(
|
|
string="Settlement Date",
|
|
required=True,
|
|
help="The date Elavon deposits into the bank (T+1 business day from transactions).",
|
|
)
|
|
transaction_date = fields.Date(
|
|
string="Transaction Date",
|
|
required=True,
|
|
help="The date card transactions were processed at the terminal.",
|
|
)
|
|
provider_id = fields.Many2one(
|
|
'payment.provider',
|
|
string="Payment Provider",
|
|
required=True,
|
|
domain="[('code', '=', 'poynt')]",
|
|
ondelete='restrict',
|
|
)
|
|
bank_statement_line_id = fields.Many2one(
|
|
'account.bank.statement.line',
|
|
string="Bank Statement Line",
|
|
help="The Elavon deposit line on the bank statement.",
|
|
ondelete='set null',
|
|
)
|
|
line_ids = fields.One2many(
|
|
'poynt.settlement.line',
|
|
'batch_id',
|
|
string="Settlement Lines",
|
|
)
|
|
state = fields.Selection([
|
|
('draft', "Draft"),
|
|
('matched', "Matched to Deposit"),
|
|
('reconciled', "Reconciled"),
|
|
('error', "Error"),
|
|
], string="Status", required=True, default='draft')
|
|
|
|
currency_id = fields.Many2one(
|
|
'res.currency',
|
|
string="Currency",
|
|
required=True,
|
|
default=lambda self: self.env.company.currency_id,
|
|
)
|
|
poynt_total = fields.Monetary(
|
|
string="Poynt Total",
|
|
currency_field='currency_id',
|
|
compute='_compute_totals',
|
|
store=True,
|
|
help="Sum of all Poynt transactions (sales - refunds) for this batch.",
|
|
)
|
|
elavon_deposit = fields.Monetary(
|
|
string="Elavon Deposit",
|
|
currency_field='currency_id',
|
|
help="The amount Elavon deposited into the bank account.",
|
|
)
|
|
fee_amount = fields.Monetary(
|
|
string="Processing Fees",
|
|
currency_field='currency_id',
|
|
compute='_compute_totals',
|
|
store=True,
|
|
help="Difference between Poynt total and Elavon deposit (Elavon processing fees).",
|
|
)
|
|
sale_count = fields.Integer(
|
|
string="Sales",
|
|
compute='_compute_totals',
|
|
store=True,
|
|
)
|
|
refund_count = fields.Integer(
|
|
string="Refunds",
|
|
compute='_compute_totals',
|
|
store=True,
|
|
)
|
|
matched_count = fields.Integer(
|
|
string="Matched to Existing Payments",
|
|
compute='_compute_totals',
|
|
store=True,
|
|
)
|
|
payment_count = fields.Integer(
|
|
string="Payments",
|
|
compute='_compute_smart_buttons',
|
|
)
|
|
invoice_count = fields.Integer(
|
|
string="Invoices",
|
|
compute='_compute_smart_buttons',
|
|
)
|
|
notes = fields.Text(string="Notes")
|
|
|
|
_sql_constraints = [
|
|
('unique_provider_txn_date', 'unique(provider_id, transaction_date)',
|
|
'A settlement batch already exists for this provider and transaction date.'),
|
|
]
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for vals in vals_list:
|
|
if vals.get('name', '/') == '/':
|
|
vals['name'] = self.env['ir.sequence'].next_by_code(
|
|
'poynt.settlement.batch'
|
|
) or '/'
|
|
return super().create(vals_list)
|
|
|
|
@api.depends('line_ids.amount', 'line_ids.action', 'line_ids.existing_payment_id', 'elavon_deposit')
|
|
def _compute_totals(self):
|
|
for batch in self:
|
|
sales = sum(
|
|
line.amount for line in batch.line_ids if line.action == 'SALE'
|
|
)
|
|
refunds = sum(
|
|
line.amount for line in batch.line_ids if line.action == 'REFUND'
|
|
)
|
|
net = sales - refunds
|
|
batch.poynt_total = net
|
|
batch.fee_amount = net - batch.elavon_deposit if batch.elavon_deposit else 0.0
|
|
batch.sale_count = len(batch.line_ids.filtered(lambda l: l.action == 'SALE'))
|
|
batch.refund_count = len(batch.line_ids.filtered(lambda l: l.action == 'REFUND'))
|
|
batch.matched_count = len(batch.line_ids.filtered(lambda l: l.existing_payment_id))
|
|
|
|
def _compute_smart_buttons(self):
|
|
for batch in self:
|
|
payments = batch.line_ids.mapped('existing_payment_id')
|
|
invoices = batch.line_ids.mapped('existing_invoice_id')
|
|
batch.payment_count = len(payments)
|
|
batch.invoice_count = len(invoices)
|
|
|
|
def action_view_payments(self):
|
|
"""Open linked payments in a list view."""
|
|
self.ensure_one()
|
|
payment_ids = self.line_ids.mapped('existing_payment_id').ids
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _("Payments - %s", self.name),
|
|
'res_model': 'account.payment',
|
|
'view_mode': 'list,form',
|
|
'domain': [('id', 'in', payment_ids)],
|
|
}
|
|
|
|
def action_view_invoices(self):
|
|
"""Open linked invoices in a list view."""
|
|
self.ensure_one()
|
|
invoice_ids = self.line_ids.mapped('existing_invoice_id').ids
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _("Invoices - %s", self.name),
|
|
'res_model': 'account.move',
|
|
'view_mode': 'list,form',
|
|
'domain': [('id', 'in', invoice_ids)],
|
|
}
|
|
|
|
# === BUSINESS METHODS === #
|
|
|
|
def action_fetch_transactions(self):
|
|
"""Fetch Poynt transactions for this batch's transaction date."""
|
|
self.ensure_one()
|
|
if self.line_ids:
|
|
raise UserError(_("This batch already has transaction lines. Clear them first."))
|
|
|
|
provider = self.provider_id
|
|
transactions = provider._poynt_fetch_settlement_transactions(
|
|
self.transaction_date, self.transaction_date,
|
|
)
|
|
|
|
lines_vals = []
|
|
existing_txn_ids = set()
|
|
for txn in transactions:
|
|
txn_id = txn.get('id', '')
|
|
if txn_id in existing_txn_ids:
|
|
continue
|
|
existing_txn_ids.add(txn_id)
|
|
|
|
action = txn.get('action', '')
|
|
if action not in ('SALE', 'REFUND'):
|
|
continue
|
|
|
|
status = txn.get('processorResponse', {}).get('status', '')
|
|
settlement = txn.get('settlementStatus', '')
|
|
if status != 'Approved' and settlement != 'SETTLED':
|
|
continue
|
|
|
|
amounts = txn.get('amounts', {})
|
|
amount_cents = amounts.get('transactionAmount', 0)
|
|
amount = amount_cents / 100.0
|
|
|
|
card = txn.get('fundingSource', {}).get('card', {})
|
|
|
|
# Convert ISO 8601 timestamp to Odoo format
|
|
created_at = txn.get('createdAt', '')
|
|
if created_at:
|
|
created_at = created_at.replace('T', ' ').replace('Z', '')
|
|
|
|
lines_vals.append({
|
|
'batch_id': self.id,
|
|
'poynt_transaction_id': txn_id,
|
|
'poynt_order_id': txn.get('context', {}).get('orderId', ''),
|
|
'transaction_date': created_at,
|
|
'amount': amount,
|
|
'card_brand': card.get('type', ''),
|
|
'card_last4': card.get('numberLast4', ''),
|
|
'card_holder_name': card.get('cardHolderFullName', ''),
|
|
'action': action,
|
|
'state': 'fetched',
|
|
})
|
|
|
|
if lines_vals:
|
|
self.env['poynt.settlement.line'].create(lines_vals)
|
|
|
|
_logger.info(
|
|
"Poynt settlement batch %s: fetched %d transactions for %s",
|
|
self.name, len(lines_vals), self.transaction_date,
|
|
)
|
|
return True
|
|
|
|
def action_match_deposit(self):
|
|
"""Match this batch to an Elavon bank statement line."""
|
|
self.ensure_one()
|
|
if not self.line_ids:
|
|
raise UserError(_("No transaction lines to match. Fetch transactions first."))
|
|
|
|
# Search for Elavon deposits near the settlement date
|
|
# Use journal_id = 50 (Scotia Current) and SQL for the date
|
|
# since date is a related field from account.move
|
|
self.env.cr.execute("""
|
|
SELECT absl.id, am.date, absl.amount
|
|
FROM account_bank_statement_line absl
|
|
JOIN account_move am ON am.id = absl.move_id
|
|
WHERE absl.journal_id = 50
|
|
AND am.date >= %s
|
|
AND am.date <= %s
|
|
AND absl.amount > 0
|
|
AND absl.payment_ref ILIKE '%%ELAVON%%'
|
|
ORDER BY am.date
|
|
""", [
|
|
self.settlement_date - timedelta(days=1),
|
|
self.settlement_date + timedelta(days=1),
|
|
])
|
|
rows = self.env.cr.fetchall()
|
|
|
|
if not rows:
|
|
self.write({
|
|
'notes': f"No Elavon deposit found near {self.settlement_date}",
|
|
})
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'message': _("No Elavon deposit found near %s", self.settlement_date),
|
|
'type': 'warning',
|
|
'sticky': False,
|
|
},
|
|
}
|
|
|
|
net_amount = self.poynt_total
|
|
best_match = None
|
|
best_diff = float('inf')
|
|
|
|
for row_id, row_date, row_amount in rows:
|
|
diff = abs(float(row_amount) - net_amount)
|
|
# Allow up to 5% tolerance for processing fees
|
|
if diff < best_diff and (net_amount == 0 or diff <= abs(net_amount) * 0.05):
|
|
best_diff = diff
|
|
best_match = (row_id, row_date, float(row_amount))
|
|
|
|
if best_match:
|
|
self.write({
|
|
'bank_statement_line_id': best_match[0],
|
|
'elavon_deposit': best_match[2],
|
|
'settlement_date': best_match[1],
|
|
'state': 'matched',
|
|
})
|
|
_logger.info(
|
|
"Poynt batch %s matched to bank line %s (deposit $%.2f, fees $%.2f)",
|
|
self.name, best_match[0], best_match[2], self.fee_amount,
|
|
)
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'message': _("Matched to Elavon deposit of $%(amount).2f (fees: $%(fees).2f)",
|
|
amount=best_match[2], fees=self.fee_amount),
|
|
'type': 'success',
|
|
'sticky': False,
|
|
},
|
|
}
|
|
else:
|
|
closest = min(rows, key=lambda r: abs(float(r[2]) - net_amount))
|
|
self.write({
|
|
'notes': (
|
|
f"No matching deposit. "
|
|
f"Poynt net: ${net_amount:.2f}, "
|
|
f"closest: ${float(closest[2]):.2f} on {closest[1]}"
|
|
),
|
|
})
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'message': _(
|
|
"No matching deposit found. Poynt net: $%(net).2f, "
|
|
"closest deposit: $%(closest).2f",
|
|
net=net_amount, closest=float(closest[2]),
|
|
),
|
|
'type': 'warning',
|
|
'sticky': False,
|
|
},
|
|
}
|
|
|
|
def action_match_existing_payments(self):
|
|
"""Match settlement lines to EXISTING payments already recorded by staff.
|
|
|
|
This does NOT create new payments. Staff already record payments when
|
|
customers pay at the terminal. This method links the Poynt transaction
|
|
to that existing payment for audit/reconciliation purposes.
|
|
"""
|
|
self.ensure_one()
|
|
matched = 0
|
|
for line in self.line_ids.filtered(lambda l: not l.existing_payment_id and l.action == 'SALE'):
|
|
if line._match_to_existing_payment():
|
|
matched += 1
|
|
|
|
_logger.info(
|
|
"Poynt batch %s: matched %d/%d lines to existing payments",
|
|
self.name, matched, len(self.line_ids.filtered(lambda l: l.action == 'SALE')),
|
|
)
|
|
|
|
# Check if all SALE lines are matched
|
|
unmatched = self.line_ids.filtered(
|
|
lambda l: l.action == 'SALE' and not l.existing_payment_id and l.state != 'no_match'
|
|
)
|
|
if not unmatched and self.state == 'matched':
|
|
self.state = 'reconciled'
|
|
|
|
return True
|
|
|
|
def action_reset_to_draft(self):
|
|
"""Reset batch to draft state."""
|
|
self.ensure_one()
|
|
self.write({'state': 'draft'})
|
|
return True
|
|
|
|
# === CRON === #
|
|
|
|
@api.model
|
|
def _cron_daily_settlement_sync(self):
|
|
"""Daily cron: fetch yesterday's transactions, match to today's deposit."""
|
|
provider = self.env['payment.provider'].search([
|
|
('code', '=', 'poynt'),
|
|
('state', '=', 'enabled'),
|
|
], limit=1)
|
|
if not provider:
|
|
_logger.info("Poynt settlement cron: no active Poynt provider found.")
|
|
return
|
|
|
|
yesterday = fields.Date.today() - timedelta(days=1)
|
|
today = fields.Date.today()
|
|
|
|
# Check if batch already exists
|
|
existing = self.search([
|
|
('provider_id', '=', provider.id),
|
|
('transaction_date', '=', yesterday),
|
|
])
|
|
if existing:
|
|
_logger.info("Poynt settlement cron: batch for %s already exists.", yesterday)
|
|
return
|
|
|
|
# Handle weekend: if today is Monday, fetch Fri+Sat+Sun
|
|
weekday = yesterday.weekday()
|
|
if weekday == 6: # Sunday → fetch Fri-Sun
|
|
txn_date_from = yesterday - timedelta(days=2)
|
|
elif weekday == 5: # Saturday → skip
|
|
_logger.info("Poynt settlement cron: Saturday — will batch with Sunday/Monday.")
|
|
return
|
|
else:
|
|
txn_date_from = yesterday
|
|
|
|
batch = self.create({
|
|
'provider_id': provider.id,
|
|
'transaction_date': txn_date_from,
|
|
'settlement_date': today,
|
|
})
|
|
|
|
try:
|
|
transactions = provider._poynt_fetch_settlement_transactions(
|
|
txn_date_from, yesterday,
|
|
)
|
|
|
|
lines_vals = []
|
|
seen = set()
|
|
for txn in transactions:
|
|
txn_id = txn.get('id', '')
|
|
if txn_id in seen:
|
|
continue
|
|
seen.add(txn_id)
|
|
|
|
action = txn.get('action', '')
|
|
if action not in ('SALE', 'REFUND'):
|
|
continue
|
|
|
|
status = txn.get('processorResponse', {}).get('status', '')
|
|
settlement = txn.get('settlementStatus', '')
|
|
if status != 'Approved' and settlement != 'SETTLED':
|
|
continue
|
|
|
|
amounts = txn.get('amounts', {})
|
|
amount = amounts.get('transactionAmount', 0) / 100.0
|
|
card = txn.get('fundingSource', {}).get('card', {})
|
|
|
|
created_at = txn.get('createdAt', '')
|
|
if created_at:
|
|
created_at = created_at.replace('T', ' ').replace('Z', '')
|
|
|
|
lines_vals.append({
|
|
'batch_id': batch.id,
|
|
'poynt_transaction_id': txn_id,
|
|
'poynt_order_id': txn.get('context', {}).get('orderId', ''),
|
|
'transaction_date': created_at,
|
|
'amount': amount,
|
|
'card_brand': card.get('type', ''),
|
|
'card_last4': card.get('numberLast4', ''),
|
|
'card_holder_name': card.get('cardHolderFullName', ''),
|
|
'action': action,
|
|
'state': 'fetched',
|
|
})
|
|
|
|
if lines_vals:
|
|
self.env['poynt.settlement.line'].create(lines_vals)
|
|
|
|
# Try to match to bank deposit
|
|
batch.action_match_deposit()
|
|
|
|
# Try to match to existing payments (NOT create new ones)
|
|
batch.action_match_existing_payments()
|
|
|
|
_logger.info(
|
|
"Poynt settlement cron: created batch %s with %d lines for %s→%s",
|
|
batch.name, len(lines_vals), txn_date_from, yesterday,
|
|
)
|
|
except Exception as e:
|
|
batch.write({'state': 'error', 'notes': str(e)})
|
|
_logger.error("Poynt settlement cron failed: %s", e)
|
|
|
|
|
|
class PoyntSettlementLine(models.Model):
|
|
_name = 'poynt.settlement.line'
|
|
_description = 'Poynt Settlement Line'
|
|
_order = 'transaction_date desc, id desc'
|
|
|
|
batch_id = fields.Many2one(
|
|
'poynt.settlement.batch',
|
|
string="Settlement Batch",
|
|
required=True,
|
|
ondelete='cascade',
|
|
index=True,
|
|
)
|
|
poynt_transaction_id = fields.Char(
|
|
string="Poynt Transaction ID",
|
|
required=True,
|
|
index=True,
|
|
)
|
|
poynt_order_id = fields.Char(string="Poynt Order ID")
|
|
transaction_date = fields.Datetime(string="Transaction Date")
|
|
amount = fields.Monetary(
|
|
string="Amount",
|
|
currency_field='currency_id',
|
|
required=True,
|
|
)
|
|
currency_id = fields.Many2one(
|
|
related='batch_id.currency_id',
|
|
store=True,
|
|
)
|
|
card_brand = fields.Char(string="Card Brand")
|
|
card_last4 = fields.Char(string="Card Last 4", size=4)
|
|
card_holder_name = fields.Char(string="Cardholder Name")
|
|
|
|
# Links to EXISTING records (staff-created, not settlement-created)
|
|
existing_payment_id = fields.Many2one(
|
|
'account.payment',
|
|
string="Existing Payment",
|
|
readonly=True,
|
|
ondelete='set null',
|
|
help="The payment already recorded by staff for this transaction.",
|
|
)
|
|
existing_invoice_id = fields.Many2one(
|
|
'account.move',
|
|
string="Linked Invoice",
|
|
domain="[('move_type', '=', 'out_invoice')]",
|
|
ondelete='set null',
|
|
help="The invoice this payment was applied to.",
|
|
)
|
|
partner_id = fields.Many2one(
|
|
'res.partner',
|
|
string="Customer",
|
|
ondelete='set null',
|
|
)
|
|
|
|
action = fields.Selection([
|
|
('SALE', "Sale"),
|
|
('REFUND', "Refund"),
|
|
('VOID', "Void"),
|
|
], string="Action", required=True)
|
|
state = fields.Selection([
|
|
('fetched', "Fetched"),
|
|
('matched', "Matched to Payment"),
|
|
('no_match', "No Existing Payment"),
|
|
('error', "Error"),
|
|
], string="Status", required=True, default='fetched')
|
|
match_method = fields.Char(
|
|
string="Match Method",
|
|
help="How this line was matched to an existing payment.",
|
|
)
|
|
notes = fields.Text(string="Notes")
|
|
|
|
_sql_constraints = [
|
|
('unique_poynt_txn', 'unique(poynt_transaction_id)',
|
|
'This Poynt transaction has already been recorded.'),
|
|
]
|
|
|
|
# === MATCH TO EXISTING PAYMENTS === #
|
|
|
|
def _match_to_existing_payment(self):
|
|
"""Match this Poynt transaction to an existing payment already in Odoo.
|
|
|
|
Staff record payments when customers pay at the terminal. This method
|
|
finds that existing payment — it does NOT create a new one.
|
|
|
|
Matching strategy (in priority order):
|
|
1. Poynt transaction ID in payment.transaction (direct Odoo integration)
|
|
2. Poynt transaction UUID found in payment memo field
|
|
3. Exact amount + cardholder name match on same date (±2 days)
|
|
4. Exact amount match on same date (±2 days)
|
|
|
|
:return: True if matched, False otherwise.
|
|
"""
|
|
self.ensure_one()
|
|
if self.existing_payment_id:
|
|
return True
|
|
|
|
# Strategy 1: Direct Odoo payment transaction (Poynt-integrated payments)
|
|
PaymentTxn = self.env['payment.transaction']
|
|
odoo_txn = PaymentTxn.search([
|
|
('poynt_transaction_id', '=', self.poynt_transaction_id),
|
|
], limit=1)
|
|
if odoo_txn and odoo_txn.payment_id:
|
|
self.write({
|
|
'existing_payment_id': odoo_txn.payment_id.id,
|
|
'partner_id': odoo_txn.partner_id.id,
|
|
'existing_invoice_id': odoo_txn.invoice_ids[:1].id if odoo_txn.invoice_ids else False,
|
|
'match_method': 'poynt_txn',
|
|
'state': 'matched',
|
|
})
|
|
return True
|
|
|
|
# Strategy 2: Poynt transaction UUID in payment memo field
|
|
# Staff sometimes record the UUID when entering payments manually
|
|
if self.poynt_transaction_id:
|
|
memo_match = self.env['account.payment'].search([
|
|
('memo', 'ilike', self.poynt_transaction_id),
|
|
('payment_type', '=', 'inbound'),
|
|
('state', 'in', ('posted', 'in_process')),
|
|
], limit=1)
|
|
if memo_match:
|
|
self.write({
|
|
'existing_payment_id': memo_match.id,
|
|
'partner_id': memo_match.partner_id.id if memo_match.partner_id else False,
|
|
'existing_invoice_id': self._find_invoice_for_payment(memo_match),
|
|
'match_method': 'memo_uuid',
|
|
'state': 'matched',
|
|
})
|
|
return True
|
|
|
|
# Determine the date range for searching
|
|
if self.transaction_date:
|
|
txn_date = self.transaction_date.date()
|
|
else:
|
|
txn_date = self.batch_id.transaction_date
|
|
date_from = txn_date - timedelta(days=2)
|
|
date_to = txn_date + timedelta(days=2)
|
|
|
|
# Strategy 3: Exact amount + same date range on account.payment
|
|
# These are payments staff manually recorded
|
|
payments = self.env['account.payment'].search([
|
|
('amount', '=', self.amount),
|
|
('payment_type', '=', 'inbound'),
|
|
('date', '>=', date_from),
|
|
('date', '<=', date_to),
|
|
('state', 'in', ('posted', 'in_process')),
|
|
# Exclude payments already matched to other settlement lines
|
|
('id', 'not in', self._get_already_matched_payment_ids()),
|
|
], order='date asc')
|
|
|
|
if payments:
|
|
# Prefer one with a partner that matches cardholder name
|
|
if self.card_holder_name:
|
|
name = self.card_holder_name.strip()
|
|
for pay in payments:
|
|
if pay.partner_id and name.lower() in (pay.partner_id.name or '').lower():
|
|
self.write({
|
|
'existing_payment_id': pay.id,
|
|
'partner_id': pay.partner_id.id,
|
|
'existing_invoice_id': self._find_invoice_for_payment(pay),
|
|
'match_method': 'amount_name',
|
|
'state': 'matched',
|
|
})
|
|
return True
|
|
|
|
# Fall back to first matching payment
|
|
pay = payments[0]
|
|
self.write({
|
|
'existing_payment_id': pay.id,
|
|
'partner_id': pay.partner_id.id if pay.partner_id else False,
|
|
'existing_invoice_id': self._find_invoice_for_payment(pay),
|
|
'match_method': 'amount_date',
|
|
'state': 'matched',
|
|
})
|
|
return True
|
|
|
|
# No existing payment found — mark for review
|
|
self.write({
|
|
'state': 'no_match',
|
|
'notes': f"No existing payment found for ${self.amount:.2f} near {txn_date}",
|
|
})
|
|
return False
|
|
|
|
def _get_already_matched_payment_ids(self):
|
|
"""Get payment IDs already matched to other lines in this batch."""
|
|
return self.batch_id.line_ids.filtered(
|
|
lambda l: l.existing_payment_id and l.id != self.id
|
|
).mapped('existing_payment_id').ids
|
|
|
|
def _find_invoice_for_payment(self, payment):
|
|
"""Find the invoice that a payment was applied to."""
|
|
if not payment.partner_id:
|
|
return False
|
|
|
|
# Check reconciled invoices via the payment's move lines
|
|
receivable_lines = payment.move_id.line_ids.filtered(
|
|
lambda l: l.account_id.account_type == 'asset_receivable' and l.reconciled
|
|
)
|
|
for line in receivable_lines:
|
|
for partial in (line.matched_debit_ids | line.matched_credit_ids):
|
|
counterpart = partial.debit_move_id if partial.credit_move_id == line else partial.credit_move_id
|
|
if counterpart.move_id.move_type == 'out_invoice':
|
|
return counterpart.move_id.id
|
|
|
|
return False
|