This commit is contained in:
gsinghpal
2026-04-03 15:45:18 -04:00
parent 4cd7357aa0
commit c66bdf5089
71 changed files with 6721 additions and 118 deletions

View File

@@ -24,10 +24,12 @@
'views/account_move_views.xml',
'views/sale_order_views.xml',
'views/res_config_settings_views.xml',
'views/poynt_settlement_views.xml',
'wizard/poynt_payment_wizard_views.xml',
'wizard/poynt_refund_wizard_views.xml',
'data/payment_provider_data.xml',
'data/poynt_settlement_data.xml',
'data/poynt_receipt_email_template.xml',
],
'post_init_hook': 'post_init_hook',

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Sequence for settlement batch references -->
<record id="seq_poynt_settlement_batch" model="ir.sequence">
<field name="name">Poynt Settlement Batch</field>
<field name="code">poynt.settlement.batch</field>
<field name="prefix">SETTLE/%(year)s/%(month)s/</field>
<field name="padding">3</field>
<field name="company_id" eval="False"/>
</record>
<!-- Daily settlement sync cron -->
<record id="ir_cron_poynt_settlement_sync" model="ir.cron">
<field name="name">Poynt: Daily Settlement Sync</field>
<field name="model_id" ref="model_poynt_settlement_batch"/>
<field name="state">code</field>
<field name="code">model._cron_daily_settlement_sync()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active">True</field>
</record>
</odoo>

View File

@@ -4,6 +4,7 @@ from . import account_move
from . import payment_provider
from . import payment_token
from . import payment_transaction
from . import poynt_settlement
from . import poynt_terminal
from . import res_config_settings
from . import sale_order

View File

@@ -539,6 +539,61 @@ class PaymentProvider(models.Model):
)
return None
# === BUSINESS METHODS - SETTLEMENT === #
def _poynt_fetch_settlement_transactions(self, date_from, date_to):
"""Fetch all transactions from Poynt API for a date range.
Paginates through results using startOffset. Filters for settled
SALE and REFUND transactions.
:param date date_from: Start date (inclusive).
:param date date_to: End date (inclusive).
:return: List of transaction dicts from the Poynt API.
:rtype: list[dict]
"""
self.ensure_one()
# Convert dates to ISO8601 timestamps (start of day / end of day)
start_at = f"{date_from}T00:00:00Z"
end_at = f"{date_to}T23:59:59Z"
all_transactions = []
offset = 0
limit = 100
while True:
params = {
'startAt': start_at,
'endAt': end_at,
'limit': limit,
'startOffset': offset,
}
result = self._poynt_make_request(
'GET', 'transactions', params=params,
)
transactions = result.get('transactions', [])
if not transactions:
# Also check if result itself is a list (API version variance)
if isinstance(result, list):
transactions = result
else:
break
all_transactions.extend(transactions)
# Check if there are more pages
if len(transactions) < limit:
break
offset += limit
_logger.info(
"Poynt: fetched %d transactions for %s to %s",
len(all_transactions), date_from, date_to,
)
return all_transactions
def _poynt_notification(self, message, notification_type='info'):
"""Return a display_notification action.

View File

@@ -0,0 +1,632 @@
# 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, ValidationError
_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"),
('reconciled', "Reconciled"),
('error', "Error"),
], string="Status", required=True, default='draft', tracking=True)
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 Customers",
compute='_compute_totals',
store=True,
)
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.partner_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.partner_id))
# === 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 (2025-03-05T19:19:10Z) 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."))
# Look for Elavon deposit on the settlement date (or ±1 day for timing)
StmtLine = self.env['account.bank.statement.line']
domain = [
('journal_id.name', 'ilike', 'Scotia'),
('date', '>=', self.settlement_date - timedelta(days=1)),
('date', '<=', self.settlement_date + timedelta(days=1)),
('amount', '>', 0),
('payment_ref', 'ilike', 'ELAVON'),
('is_reconciled', '=', False),
]
candidates = StmtLine.search(domain, order='date asc')
if not candidates:
self.notes = f"No unreconciled Elavon deposit found near {self.settlement_date}"
return False
# Try to find the closest match by amount
net_amount = self.poynt_total
best_match = None
best_diff = float('inf')
for line in candidates:
diff = abs(line.amount - net_amount)
# Allow up to 5% tolerance for processing fees
if diff < best_diff and diff <= net_amount * 0.05:
best_diff = diff
best_match = line
if best_match:
self.write({
'bank_statement_line_id': best_match.id,
'elavon_deposit': best_match.amount,
'settlement_date': best_match.date,
'state': 'matched',
})
_logger.info(
"Poynt batch %s matched to bank line %s (deposit $%.2f, fees $%.2f)",
self.name, best_match.id, best_match.amount, self.fee_amount,
)
return True
else:
self.notes = (
f"No matching Elavon deposit found. "
f"Poynt net: ${net_amount:.2f}, "
f"closest candidate: ${candidates[0].amount:.2f}"
)
return False
def action_match_customers(self):
"""Attempt to match settlement lines to Odoo customers and invoices."""
self.ensure_one()
matched = 0
for line in self.line_ids.filtered(lambda l: not l.partner_id and l.action == 'SALE'):
if line._match_to_customer():
matched += 1
_logger.info(
"Poynt batch %s: matched %d/%d lines to customers",
self.name, matched, len(self.line_ids),
)
return True
def action_create_payments(self):
"""Create account.payment records for matched settlement lines."""
self.ensure_one()
if self.state == 'reconciled':
raise UserError(_("This batch is already reconciled."))
payable_lines = self.line_ids.filtered(
lambda l: l.partner_id and l.action == 'SALE' and l.state in ('fetched', 'matched') and not l.payment_id
)
if not payable_lines:
raise UserError(_("No matched lines available for payment creation."))
for line in payable_lines:
line._create_customer_payment()
# Check if all lines are processed
all_paid = all(
l.state in ('paid', 'error') or l.action == 'REFUND'
for l in self.line_ids
)
if all_paid:
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() # 0=Monday, 6=Sunday
if weekday == 6: # Sunday → fetch Fri-Sun, deposit Monday
txn_date_from = yesterday - timedelta(days=2) # Friday
elif weekday == 5: # Saturday → skip, will be batched with Sunday
_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:
# Fetch all transactions for the date range
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', {})
# 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': 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 customers
batch.action_match_customers()
_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")
partner_id = fields.Many2one(
'res.partner',
string="Customer",
ondelete='set null',
)
invoice_id = fields.Many2one(
'account.move',
string="Matched Invoice",
domain="[('move_type', '=', 'out_invoice')]",
ondelete='set null',
)
payment_id = fields.Many2one(
'account.payment',
string="Payment",
readonly=True,
ondelete='set null',
)
action = fields.Selection([
('SALE', "Sale"),
('REFUND', "Refund"),
('VOID', "Void"),
], string="Action", required=True)
state = fields.Selection([
('fetched', "Fetched"),
('matched', "Matched"),
('paid', "Payment Created"),
('error', "Error"),
], string="Status", required=True, default='fetched')
match_method = fields.Char(
string="Match Method",
help="How this line was matched to a customer (e.g., 'odoo_txn', 'card_token', 'invoice_amount', 'name').",
)
notes = fields.Text(string="Notes")
_sql_constraints = [
('unique_poynt_txn', 'unique(poynt_transaction_id)',
'This Poynt transaction has already been recorded.'),
]
# === CUSTOMER MATCHING === #
def _match_to_customer(self):
"""Attempt to match this settlement line to an Odoo customer/invoice.
Matching strategy (in priority order):
1. Check poynt_transaction_id in payment.transaction (direct Odoo payment)
2. Match by card_last4 against payment.token records
3. Match by amount against open invoices within ±2 days
4. Match by card_holder_name fuzzy search against res.partner
:return: True if matched, False otherwise.
"""
self.ensure_one()
if self.partner_id:
return True
# Strategy 1: Direct Odoo payment transaction
PaymentTxn = self.env['payment.transaction']
odoo_txn = PaymentTxn.search([
('poynt_transaction_id', '=', self.poynt_transaction_id),
], limit=1)
if odoo_txn and odoo_txn.partner_id:
self.write({
'partner_id': odoo_txn.partner_id.id,
'invoice_id': odoo_txn.invoice_ids[:1].id if odoo_txn.invoice_ids else False,
'match_method': 'odoo_txn',
'state': 'matched',
})
return True
# Strategy 2: Card token match
if self.card_last4:
token = self.env['payment.token'].search([
('payment_details', 'ilike', self.card_last4),
('provider_id.code', '=', 'poynt'),
], limit=1)
if token and token.partner_id:
self.write({
'partner_id': token.partner_id.id,
'match_method': 'card_token',
'state': 'matched',
})
# Try to find matching invoice
self._match_invoice()
return True
# Strategy 3: Amount match against open invoices
if self.amount and self.transaction_date:
date = self.transaction_date.date() if self.transaction_date else fields.Date.today()
invoices = self.env['account.move'].search([
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('payment_state', 'in', ('not_paid', 'partial')),
('amount_residual', '=', self.amount),
('invoice_date', '>=', date - timedelta(days=7)),
('invoice_date', '<=', date + timedelta(days=2)),
], limit=1)
if invoices:
self.write({
'partner_id': invoices.partner_id.id,
'invoice_id': invoices.id,
'match_method': 'invoice_amount',
'state': 'matched',
})
return True
# Strategy 4: Cardholder name fuzzy match
if self.card_holder_name:
name = self.card_holder_name.strip()
if len(name) >= 3:
partners = self.env['res.partner'].search([
'|',
('name', 'ilike', name),
('name', 'ilike', name.split()[-1] if ' ' in name else name),
], limit=5)
if len(partners) == 1:
self.write({
'partner_id': partners.id,
'match_method': 'name',
'state': 'matched',
})
self._match_invoice()
return True
return False
def _match_invoice(self):
"""Try to find a matching open invoice for this line's partner and amount."""
self.ensure_one()
if self.invoice_id or not self.partner_id:
return
invoices = self.env['account.move'].search([
('partner_id', '=', self.partner_id.id),
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('payment_state', 'in', ('not_paid', 'partial')),
('amount_residual', '=', self.amount),
], limit=1, order='invoice_date desc')
if invoices:
self.invoice_id = invoices.id
# === PAYMENT CREATION === #
def _create_customer_payment(self):
"""Create an account.payment for this matched settlement line."""
self.ensure_one()
if not self.partner_id:
self.write({'state': 'error', 'notes': 'No customer matched'})
return False
if self.payment_id:
return True
try:
# Use the provider's journal (Poynt payment journal)
journal = self.batch_id.provider_id.journal_id
if not journal:
# Fall back to first bank journal
journal = self.env['account.journal'].search([
('type', '=', 'bank'),
('company_id', '=', self.env.company.id),
], limit=1)
payment_vals = {
'partner_id': self.partner_id.id,
'amount': self.amount,
'currency_id': self.currency_id.id,
'journal_id': journal.id,
'payment_type': 'inbound',
'partner_type': 'customer',
'payment_method_line_id': journal.inbound_payment_method_line_ids[:1].id,
'memo': f"Poynt {self.card_brand or 'Card'} ****{self.card_last4 or '????'} - {self.batch_id.name}",
}
payment = self.env['account.payment'].create(payment_vals)
payment.action_post()
self.write({
'payment_id': payment.id,
'state': 'paid',
})
# Reconcile with invoice if matched
if self.invoice_id and self.invoice_id.payment_state in ('not_paid', 'partial'):
try:
(payment.move_id.line_ids + self.invoice_id.line_ids).filtered(
lambda l: l.account_id.account_type == 'asset_receivable' and not l.reconciled
).reconcile()
except Exception as e:
_logger.warning(
"Could not auto-reconcile payment %s with invoice %s: %s",
payment.name, self.invoice_id.name, e,
)
return True
except Exception as e:
self.write({'state': 'error', 'notes': str(e)})
_logger.error(
"Failed to create payment for settlement line %s: %s",
self.poynt_transaction_id, e,
)
return False

View File

@@ -8,3 +8,7 @@ access_poynt_refund_wizard_admin,poynt.refund.wizard.admin,model_poynt_refund_wi
access_payment_provider_poynt_user,payment.provider.poynt.user,payment.model_payment_provider,group_fusion_poynt_user,1,0,0,0
access_payment_transaction_poynt_user,payment.transaction.poynt.user,payment.model_payment_transaction,group_fusion_poynt_user,1,1,1,0
access_payment_method_poynt_user,payment.method.poynt.user,payment.model_payment_method,group_fusion_poynt_user,1,0,0,0
access_poynt_settlement_batch_user,poynt.settlement.batch.user,model_poynt_settlement_batch,group_fusion_poynt_user,1,0,0,0
access_poynt_settlement_batch_admin,poynt.settlement.batch.admin,model_poynt_settlement_batch,group_fusion_poynt_admin,1,1,1,1
access_poynt_settlement_line_user,poynt.settlement.line.user,model_poynt_settlement_line,group_fusion_poynt_user,1,0,0,0
access_poynt_settlement_line_admin,poynt.settlement.line.admin,model_poynt_settlement_line,group_fusion_poynt_admin,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 access_payment_provider_poynt_user payment.provider.poynt.user payment.model_payment_provider group_fusion_poynt_user 1 0 0 0
9 access_payment_transaction_poynt_user payment.transaction.poynt.user payment.model_payment_transaction group_fusion_poynt_user 1 1 1 0
10 access_payment_method_poynt_user payment.method.poynt.user payment.model_payment_method group_fusion_poynt_user 1 0 0 0
11 access_poynt_settlement_batch_user poynt.settlement.batch.user model_poynt_settlement_batch group_fusion_poynt_user 1 0 0 0
12 access_poynt_settlement_batch_admin poynt.settlement.batch.admin model_poynt_settlement_batch group_fusion_poynt_admin 1 1 1 1
13 access_poynt_settlement_line_user poynt.settlement.line.user model_poynt_settlement_line group_fusion_poynt_user 1 0 0 0
14 access_poynt_settlement_line_admin poynt.settlement.line.admin model_poynt_settlement_line group_fusion_poynt_admin 1 1 1 1

View File

@@ -0,0 +1,231 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ==================== SETTLEMENT BATCH ==================== -->
<!-- Tree View -->
<record id="poynt_settlement_batch_view_list" model="ir.ui.view">
<field name="name">poynt.settlement.batch.list</field>
<field name="model">poynt.settlement.batch</field>
<field name="arch" type="xml">
<list decoration-info="state == 'draft'" decoration-success="state == 'reconciled'" decoration-warning="state == 'matched'" decoration-danger="state == 'error'">
<field name="name"/>
<field name="transaction_date"/>
<field name="settlement_date"/>
<field name="sale_count"/>
<field name="refund_count"/>
<field name="poynt_total" sum="Total"/>
<field name="elavon_deposit" sum="Total"/>
<field name="fee_amount" sum="Total"/>
<field name="matched_count"/>
<field name="state" widget="badge"
decoration-info="state == 'draft'"
decoration-success="state == 'reconciled'"
decoration-warning="state == 'matched'"
decoration-danger="state == 'error'"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="poynt_settlement_batch_view_form" model="ir.ui.view">
<field name="name">poynt.settlement.batch.form</field>
<field name="model">poynt.settlement.batch</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_fetch_transactions" type="object"
string="Fetch Transactions" class="btn-primary"
invisible="line_ids or state != 'draft'"/>
<button name="action_match_deposit" type="object"
string="Match Bank Deposit" class="btn-primary"
invisible="state != 'draft' or not line_ids"/>
<button name="action_match_customers" type="object"
string="Match Customers" class="btn-secondary"
invisible="state not in ('draft', 'matched')"/>
<button name="action_create_payments" type="object"
string="Create Payments" class="btn-primary"
invisible="state not in ('matched',)"
confirm="This will create customer payment records for all matched lines. Continue?"/>
<button name="action_reset_to_draft" type="object"
string="Reset to Draft" class="btn-secondary"
invisible="state in ('draft', 'reconciled')"/>
<field name="state" widget="statusbar" statusbar_visible="draft,matched,reconciled"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<group>
<group>
<field name="transaction_date"/>
<field name="settlement_date"/>
<field name="provider_id"/>
</group>
<group>
<field name="poynt_total"/>
<field name="elavon_deposit"/>
<field name="fee_amount"/>
<field name="currency_id" invisible="1"/>
<field name="bank_statement_line_id"/>
</group>
</group>
<group>
<group>
<field name="sale_count"/>
<field name="refund_count"/>
<field name="matched_count"/>
</group>
</group>
<notebook>
<page string="Transaction Lines" name="lines">
<field name="line_ids">
<list editable="bottom"
decoration-success="state == 'paid'"
decoration-warning="state == 'matched'"
decoration-danger="state == 'error'">
<field name="transaction_date"/>
<field name="action"/>
<field name="amount" sum="Total"/>
<field name="card_brand"/>
<field name="card_last4"/>
<field name="card_holder_name"/>
<field name="partner_id"/>
<field name="invoice_id"/>
<field name="payment_id"/>
<field name="match_method"/>
<field name="state" widget="badge"
decoration-success="state == 'paid'"
decoration-warning="state == 'matched'"
decoration-danger="state == 'error'"/>
<field name="currency_id" column_invisible="1"/>
</list>
</field>
</page>
<page string="Notes" name="notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="poynt_settlement_batch_view_search" model="ir.ui.view">
<field name="name">poynt.settlement.batch.search</field>
<field name="model">poynt.settlement.batch</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="transaction_date"/>
<field name="settlement_date"/>
<filter name="filter_draft" string="Draft" domain="[('state', '=', 'draft')]"/>
<filter name="filter_matched" string="Matched" domain="[('state', '=', 'matched')]"/>
<filter name="filter_reconciled" string="Reconciled" domain="[('state', '=', 'reconciled')]"/>
<filter name="filter_error" string="Errors" domain="[('state', '=', 'error')]"/>
<separator/>
<filter name="group_state" string="Status" context="{'group_by': 'state'}" domain="[]"/>
<filter name="group_settlement_date" string="Settlement Date" context="{'group_by': 'settlement_date:month'}" domain="[]"/>
</search>
</field>
</record>
<!-- Action -->
<record id="action_poynt_settlement_batch" model="ir.actions.act_window">
<field name="name">Settlement Batches</field>
<field name="res_model">poynt.settlement.batch</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="poynt_settlement_batch_view_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No settlement batches yet
</p>
<p>
Settlement batches are created automatically by the daily cron job,
or you can create one manually to fetch Poynt terminal transactions
for a specific date.
</p>
</field>
</record>
<!-- ==================== SETTLEMENT LINE ==================== -->
<!-- Tree View (standalone) -->
<record id="poynt_settlement_line_view_list" model="ir.ui.view">
<field name="name">poynt.settlement.line.list</field>
<field name="model">poynt.settlement.line</field>
<field name="arch" type="xml">
<list decoration-success="state == 'paid'" decoration-warning="state == 'matched'" decoration-danger="state == 'error'">
<field name="batch_id"/>
<field name="transaction_date"/>
<field name="action"/>
<field name="amount" sum="Total"/>
<field name="card_brand"/>
<field name="card_last4"/>
<field name="card_holder_name"/>
<field name="partner_id"/>
<field name="invoice_id"/>
<field name="payment_id"/>
<field name="match_method"/>
<field name="state" widget="badge"
decoration-success="state == 'paid'"
decoration-warning="state == 'matched'"
decoration-danger="state == 'error'"/>
</list>
</field>
</record>
<!-- Search View -->
<record id="poynt_settlement_line_view_search" model="ir.ui.view">
<field name="name">poynt.settlement.line.search</field>
<field name="model">poynt.settlement.line</field>
<field name="arch" type="xml">
<search>
<field name="card_holder_name"/>
<field name="card_last4"/>
<field name="partner_id"/>
<field name="poynt_transaction_id"/>
<filter name="filter_unmatched" string="Unmatched" domain="[('partner_id', '=', False), ('action', '=', 'SALE')]"/>
<filter name="filter_matched" string="Matched" domain="[('state', '=', 'matched')]"/>
<filter name="filter_paid" string="Paid" domain="[('state', '=', 'paid')]"/>
<filter name="filter_errors" string="Errors" domain="[('state', '=', 'error')]"/>
<separator/>
<filter name="group_batch" string="Batch" context="{'group_by': 'batch_id'}" domain="[]"/>
<filter name="group_partner" string="Customer" context="{'group_by': 'partner_id'}" domain="[]"/>
<filter name="group_card_brand" string="Card Brand" context="{'group_by': 'card_brand'}" domain="[]"/>
<filter name="group_state" string="Status" context="{'group_by': 'state'}" domain="[]"/>
</search>
</field>
</record>
<!-- Action -->
<record id="action_poynt_settlement_line" model="ir.actions.act_window">
<field name="name">Settlement Transactions</field>
<field name="res_model">poynt.settlement.line</field>
<field name="view_mode">list</field>
<field name="search_view_id" ref="poynt_settlement_line_view_search"/>
</record>
<!-- ==================== MENUS ==================== -->
<menuitem id="menu_poynt_settlement_root"
name="Settlements"
parent="account.root_payment_menu"
sequence="30"/>
<menuitem id="menu_poynt_settlement_batches"
name="Settlement Batches"
parent="menu_poynt_settlement_root"
action="action_poynt_settlement_batch"
sequence="10"/>
<menuitem id="menu_poynt_settlement_lines"
name="All Transactions"
parent="menu_poynt_settlement_root"
action="action_poynt_settlement_line"
sequence="20"/>
</odoo>