feat(portal): account_summary controller + 3 unit tests
New /my/account_summary route. Splits posted account.move into Invoices (out_invoice) / Credit Memos (out_refund) / Statements (V1 placeholder). Open Balance helper sums amount_residual across open invoices for the partner's commercial tree. Search filters name OR ref (customer PO). Sort options: date desc/asc, amount desc/asc. Filter pills: open / closed / all. Tests cover the tab partitioning, the open-balance sum, and the search behaviour. Helpers use commercial_partner.env so they work in both HTTP context and unit tests without requiring request.env. Test scaffolding uses fp_from_so_invoice=True context flag and invoice_payment_term_id to satisfy the fusion_plating_jobs and fusion_plating_invoicing create/post gates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -442,6 +442,131 @@ class FpCustomerPortal(CustomerPortal):
|
||||
return '%.0f KB' % (size / 1024)
|
||||
return '%.1f MB' % (size / (1024 * 1024))
|
||||
|
||||
# ==========================================================================
|
||||
# Account Summary (Sub-A IA) — invoices + credits + statements
|
||||
# ==========================================================================
|
||||
_FP_ACCOUNT_SUMMARY_TABS = [
|
||||
('invoices', 'Invoices', 'out_invoice'),
|
||||
('credit_memos', 'Credit Memos', 'out_refund'),
|
||||
('statements', 'Statements', None), # placeholder in V1
|
||||
]
|
||||
_FP_ACCOUNT_SUMMARY_FILTERS = ['open', 'closed', 'all']
|
||||
_FP_ACCOUNT_SUMMARY_SORTS = {
|
||||
'date_desc': 'invoice_date desc, id desc',
|
||||
'date_asc': 'invoice_date asc, id asc',
|
||||
'amount_desc': 'amount_total desc, id desc',
|
||||
'amount_asc': 'amount_total asc, id asc',
|
||||
}
|
||||
_FP_ACCOUNT_SUMMARY_PER_PAGE = 10
|
||||
|
||||
def _fp_account_summary_open_balance(self, commercial_partner):
|
||||
"""Sum of amount_residual across this partner's open invoices.
|
||||
|
||||
Uses commercial_partner.env so this helper works both in HTTP
|
||||
context (where request.env is active) and in unit tests (where
|
||||
the partner record already carries the test env).
|
||||
"""
|
||||
env = commercial_partner.env
|
||||
moves = env['account.move'].sudo().search([
|
||||
('partner_id', 'child_of', commercial_partner.id),
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('amount_residual', '>', 0),
|
||||
])
|
||||
return sum(moves.mapped('amount_residual'))
|
||||
|
||||
def _fp_account_summary_data(self, commercial_partner, tab, filter_state,
|
||||
search, sort, page):
|
||||
"""Return {records, total, pager_offset} for one tab+filter combination.
|
||||
|
||||
tab — 'invoices' | 'credit_memos' | 'statements'
|
||||
filter_state — 'open' | 'closed' | 'all'
|
||||
search — substring matched against name OR ref (case-insensitive)
|
||||
sort — key from _FP_ACCOUNT_SUMMARY_SORTS
|
||||
page — 1-indexed
|
||||
|
||||
Uses commercial_partner.env so this helper works both in HTTP
|
||||
context and in unit tests without requiring request to be active.
|
||||
"""
|
||||
env = commercial_partner.env
|
||||
if tab == 'statements':
|
||||
# V1 placeholder — Statements is a 'coming soon' tab.
|
||||
return {'records': env['account.move'].browse(), 'total': 0,
|
||||
'offset': 0}
|
||||
|
||||
# Resolve move_type from tab key
|
||||
move_type = next(
|
||||
(mt for k, _l, mt in self._FP_ACCOUNT_SUMMARY_TABS if k == tab),
|
||||
'out_invoice',
|
||||
)
|
||||
domain = [
|
||||
('partner_id', 'child_of', commercial_partner.id),
|
||||
('move_type', '=', move_type),
|
||||
('state', '=', 'posted'),
|
||||
]
|
||||
if filter_state == 'open':
|
||||
domain.append(('amount_residual', '>', 0))
|
||||
elif filter_state == 'closed':
|
||||
domain.append(('amount_residual', '=', 0))
|
||||
if search:
|
||||
domain.append('|')
|
||||
domain.append(('name', 'ilike', search))
|
||||
domain.append(('ref', 'ilike', search))
|
||||
|
||||
Move = env['account.move'].sudo()
|
||||
order = self._FP_ACCOUNT_SUMMARY_SORTS.get(sort, 'invoice_date desc')
|
||||
total = Move.search_count(domain)
|
||||
offset = max(0, (page - 1) * self._FP_ACCOUNT_SUMMARY_PER_PAGE)
|
||||
records = Move.search(domain, order=order, limit=self._FP_ACCOUNT_SUMMARY_PER_PAGE, offset=offset)
|
||||
return {'records': records, 'total': total, 'offset': offset}
|
||||
|
||||
@http.route(
|
||||
['/my/account_summary', '/my/account_summary/page/<int:page>'],
|
||||
type='http', auth='user', website=True,
|
||||
)
|
||||
def portal_account_summary(self, page=1, tab='invoices',
|
||||
filter_state='open', search='', sort='date_desc',
|
||||
**kw):
|
||||
partner = request.env.user.partner_id
|
||||
commercial = partner.commercial_partner_id
|
||||
# Sanitize inputs
|
||||
if tab not in [k for k, _l, _t in self._FP_ACCOUNT_SUMMARY_TABS]:
|
||||
tab = 'invoices'
|
||||
if filter_state not in self._FP_ACCOUNT_SUMMARY_FILTERS:
|
||||
filter_state = 'open'
|
||||
if sort not in self._FP_ACCOUNT_SUMMARY_SORTS:
|
||||
sort = 'date_desc'
|
||||
|
||||
data = self._fp_account_summary_data(
|
||||
commercial, tab, filter_state, search, sort, page,
|
||||
)
|
||||
open_balance = self._fp_account_summary_open_balance(commercial)
|
||||
|
||||
pager = portal_pager(
|
||||
url='/my/account_summary',
|
||||
url_args={'tab': tab, 'filter_state': filter_state,
|
||||
'search': search, 'sort': sort},
|
||||
total=data['total'],
|
||||
page=page,
|
||||
step=self._FP_ACCOUNT_SUMMARY_PER_PAGE,
|
||||
)
|
||||
|
||||
values = {
|
||||
'page_name': 'fp_account_summary',
|
||||
'records': data['records'],
|
||||
'tabs': self._FP_ACCOUNT_SUMMARY_TABS,
|
||||
'active_tab': tab,
|
||||
'filter_state': filter_state,
|
||||
'search': search,
|
||||
'sort': sort,
|
||||
'open_balance': open_balance,
|
||||
'currency': commercial.property_account_receivable_id.currency_id
|
||||
if commercial.property_account_receivable_id else request.env.company.currency_id,
|
||||
'pager': pager,
|
||||
'total': data['total'],
|
||||
}
|
||||
return request.render('fusion_plating_portal.portal_my_account_summary', values)
|
||||
|
||||
# ==========================================================================
|
||||
# DASHBOARD
|
||||
# ==========================================================================
|
||||
|
||||
Reference in New Issue
Block a user