This commit is contained in:
gsinghpal
2026-04-04 15:37:16 -04:00
parent c66bdf5089
commit 3cc93b8783
36 changed files with 3278 additions and 548 deletions

View File

@@ -22,6 +22,7 @@
'views/payment_poynt_templates.xml',
'views/poynt_terminal_views.xml',
'views/account_move_views.xml',
'views/account_payment_views.xml',
'views/sale_order_views.xml',
'views/res_config_settings_views.xml',
'views/poynt_settlement_views.xml',

View File

@@ -1,6 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move
from . import account_payment
from . import payment_provider
from . import payment_token
from . import payment_transaction

View File

@@ -0,0 +1,42 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class AccountPayment(models.Model):
_inherit = 'account.payment'
poynt_settlement_line_ids = fields.One2many(
'poynt.settlement.line',
'existing_payment_id',
string="Settlement Lines",
)
poynt_settlement_count = fields.Integer(
string="Settlements",
compute='_compute_poynt_settlement_count',
)
@api.depends('poynt_settlement_line_ids')
def _compute_poynt_settlement_count(self):
for payment in self:
payment.poynt_settlement_count = len(payment.poynt_settlement_line_ids)
def action_view_poynt_settlement(self):
"""Open the settlement batch linked to this payment."""
self.ensure_one()
batch_ids = self.poynt_settlement_line_ids.mapped('batch_id').ids
if len(batch_ids) == 1:
return {
'type': 'ir.actions.act_window',
'name': _("Settlement Batch"),
'res_model': 'poynt.settlement.batch',
'view_mode': 'form',
'res_id': batch_ids[0],
}
return {
'type': 'ir.actions.act_window',
'name': _("Settlement Batches"),
'res_model': 'poynt.settlement.batch',
'view_mode': 'list,form',
'domain': [('id', 'in', batch_ids)],
}

View File

@@ -4,7 +4,7 @@ import logging
from datetime import timedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
@@ -52,10 +52,10 @@ class PoyntSettlementBatch(models.Model):
)
state = fields.Selection([
('draft', "Draft"),
('matched', "Matched"),
('matched', "Matched to Deposit"),
('reconciled', "Reconciled"),
('error', "Error"),
], string="Status", required=True, default='draft', tracking=True)
], string="Status", required=True, default='draft')
currency_id = fields.Many2one(
'res.currency',
@@ -93,10 +93,18 @@ class PoyntSettlementBatch(models.Model):
store=True,
)
matched_count = fields.Integer(
string="Matched to Customers",
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 = [
@@ -113,7 +121,7 @@ class PoyntSettlementBatch(models.Model):
) or '/'
return super().create(vals_list)
@api.depends('line_ids.amount', 'line_ids.action', 'line_ids.partner_id', 'elavon_deposit')
@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(
@@ -127,7 +135,38 @@ class PoyntSettlementBatch(models.Model):
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))
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 === #
@@ -165,7 +204,7 @@ class PoyntSettlementBatch(models.Model):
card = txn.get('fundingSource', {}).get('card', {})
# Convert ISO 8601 timestamp (2025-03-05T19:19:10Z) to Odoo format
# Convert ISO 8601 timestamp to Odoo format
created_at = txn.get('createdAt', '')
if created_at:
created_at = created_at.replace('T', ' ').replace('Z', '')
@@ -198,90 +237,117 @@ class PoyntSettlementBatch(models.Model):
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')
# 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 candidates:
self.notes = f"No unreconciled Elavon deposit found near {self.settlement_date}"
return False
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,
},
}
# 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)
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 diff <= net_amount * 0.05:
if diff < best_diff and (net_amount == 0 or diff <= abs(net_amount) * 0.05):
best_diff = diff
best_match = line
best_match = (row_id, row_date, float(row_amount))
if best_match:
self.write({
'bank_statement_line_id': best_match.id,
'elavon_deposit': best_match.amount,
'settlement_date': best_match.date,
'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.id, best_match.amount, self.fee_amount,
self.name, best_match[0], best_match[2], self.fee_amount,
)
return True
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:
self.notes = (
f"No matching Elavon deposit found. "
f"Poynt net: ${net_amount:.2f}, "
f"closest candidate: ${candidates[0].amount:.2f}"
)
return False
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_customers(self):
"""Attempt to match settlement lines to Odoo customers and invoices."""
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.partner_id and l.action == 'SALE'):
if line._match_to_customer():
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 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
"Poynt batch %s: matched %d/%d lines to existing payments",
self.name, matched, len(self.line_ids.filtered(lambda l: l.action == 'SALE')),
)
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
# 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 all_paid:
if not unmatched and self.state == 'matched':
self.state = 'reconciled'
return True
@@ -318,10 +384,10 @@ class PoyntSettlementBatch(models.Model):
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
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:
@@ -334,7 +400,6 @@ class PoyntSettlementBatch(models.Model):
})
try:
# Fetch all transactions for the date range
transactions = provider._poynt_fetch_settlement_transactions(
txn_date_from, yesterday,
)
@@ -360,7 +425,6 @@ class PoyntSettlementBatch(models.Model):
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', '')
@@ -384,8 +448,8 @@ class PoyntSettlementBatch(models.Model):
# Try to match to bank deposit
batch.action_match_deposit()
# Try to match customers
batch.action_match_customers()
# 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",
@@ -427,23 +491,28 @@ class PoyntSettlementLine(models.Model):
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',
)
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"),
@@ -451,13 +520,13 @@ class PoyntSettlementLine(models.Model):
], string="Action", required=True)
state = fields.Selection([
('fetched', "Fetched"),
('matched', "Matched"),
('paid', "Payment Created"),
('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 a customer (e.g., 'odoo_txn', 'card_token', 'invoice_amount', 'name').",
help="How this line was matched to an existing payment.",
)
notes = fields.Text(string="Notes")
@@ -466,167 +535,131 @@ class PoyntSettlementLine(models.Model):
'This Poynt transaction has already been recorded.'),
]
# === CUSTOMER MATCHING === #
# === MATCH TO EXISTING PAYMENTS === #
def _match_to_customer(self):
"""Attempt to match this settlement line to an Odoo customer/invoice.
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. 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
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.partner_id:
if self.existing_payment_id:
return True
# Strategy 1: Direct Odoo payment transaction
# 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.partner_id:
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,
'invoice_id': odoo_txn.invoice_ids[:1].id if odoo_txn.invoice_ids else False,
'match_method': 'odoo_txn',
'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: Card token match
if self.card_last4:
token = self.env['payment.token'].search([
('payment_details', 'ilike', self.card_last4),
('provider_id.code', '=', 'poynt'),
# 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 token and token.partner_id:
if memo_match:
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',
'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
# 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
# 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 _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
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
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'})
def _find_invoice_for_payment(self, payment):
"""Find the invoice that a payment was applied to."""
if not payment.partner_id:
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)
# 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
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
return False

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_account_payment_form_inherit_poynt_settlement" model="ir.ui.view">
<field name="name">account.payment.form.inherit.poynt.settlement</field>
<field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_view_poynt_settlement" type="object"
class="oe_stat_button" icon="fa-credit-card"
invisible="poynt_settlement_count == 0">
<field name="poynt_settlement_count" string="Settlement" widget="statinfo"/>
</button>
</xpath>
</field>
</record>
</odoo>

View File

@@ -40,19 +40,27 @@
<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"
<button name="action_match_existing_payments" type="object"
string="Match Existing Payments" 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_button_box" name="button_box">
<button name="action_view_payments" type="object"
class="oe_stat_button" icon="fa-money"
invisible="payment_count == 0">
<field name="payment_count" string="Payments" widget="statinfo"/>
</button>
<button name="action_view_invoices" type="object"
class="oe_stat_button" icon="fa-pencil-square-o"
invisible="invoice_count == 0">
<field name="invoice_count" string="Invoices" widget="statinfo"/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
@@ -83,8 +91,8 @@
<page string="Transaction Lines" name="lines">
<field name="line_ids">
<list editable="bottom"
decoration-success="state == 'paid'"
decoration-warning="state == 'matched'"
decoration-success="state == 'matched'"
decoration-muted="state == 'no_match'"
decoration-danger="state == 'error'">
<field name="transaction_date"/>
<field name="action"/>
@@ -93,12 +101,12 @@
<field name="card_last4"/>
<field name="card_holder_name"/>
<field name="partner_id"/>
<field name="invoice_id"/>
<field name="payment_id"/>
<field name="existing_payment_id" string="Staff Payment"/>
<field name="existing_invoice_id" string="Invoice"/>
<field name="match_method"/>
<field name="state" widget="badge"
decoration-success="state == 'paid'"
decoration-warning="state == 'matched'"
decoration-success="state == 'matched'"
decoration-muted="state == 'no_match'"
decoration-danger="state == 'error'"/>
<field name="currency_id" column_invisible="1"/>
</list>
@@ -158,7 +166,7 @@
<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'">
<list decoration-success="state == 'matched'" decoration-muted="state == 'no_match'" decoration-danger="state == 'error'">
<field name="batch_id"/>
<field name="transaction_date"/>
<field name="action"/>
@@ -167,12 +175,12 @@
<field name="card_last4"/>
<field name="card_holder_name"/>
<field name="partner_id"/>
<field name="invoice_id"/>
<field name="payment_id"/>
<field name="existing_payment_id" string="Staff Payment"/>
<field name="existing_invoice_id" string="Invoice"/>
<field name="match_method"/>
<field name="state" widget="badge"
decoration-success="state == 'paid'"
decoration-warning="state == 'matched'"
decoration-success="state == 'matched'"
decoration-muted="state == 'no_match'"
decoration-danger="state == 'error'"/>
</list>
</field>
@@ -188,9 +196,9 @@
<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_unmatched" string="No Payment Found" domain="[('state', '=', 'no_match')]"/>
<filter name="filter_matched" string="Matched to Payment" domain="[('state', '=', 'matched')]"/>
<filter name="filter_fetched" string="Pending Match" domain="[('state', '=', 'fetched')]"/>
<filter name="filter_errors" string="Errors" domain="[('state', '=', 'error')]"/>
<separator/>
<filter name="group_batch" string="Batch" context="{'group_by': 'batch_id'}" domain="[]"/>