Compare commits
3 Commits
25f033d0c8
...
ffc029a875
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffc029a875 | ||
|
|
6048df0645 | ||
|
|
b6aedc9bbe |
@@ -2,3 +2,4 @@ from . import models
|
|||||||
from . import controllers
|
from . import controllers
|
||||||
from . import services
|
from . import services
|
||||||
from . import wizards
|
from . import wizards
|
||||||
|
from . import reports
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting — Bank Reconciliation',
|
'name': 'Fusion Accounting — Bank Reconciliation',
|
||||||
'version': '19.0.1.0.20',
|
'version': '19.0.1.0.23',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'sequence': 28,
|
'sequence': 28,
|
||||||
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',
|
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',
|
||||||
@@ -24,7 +24,7 @@ Built by Nexa Systems Inc.
|
|||||||
'author': 'Nexa Systems Inc.',
|
'author': 'Nexa Systems Inc.',
|
||||||
'website': 'https://nexasystems.ca',
|
'website': 'https://nexasystems.ca',
|
||||||
'maintainer': 'Nexa Systems Inc.',
|
'maintainer': 'Nexa Systems Inc.',
|
||||||
'depends': ['fusion_accounting_core'],
|
'depends': ['fusion_accounting_core', 'fusion_accounting_migration'],
|
||||||
'external_dependencies': {
|
'external_dependencies': {
|
||||||
'python': ['hypothesis'],
|
'python': ['hypothesis'],
|
||||||
},
|
},
|
||||||
@@ -33,6 +33,8 @@ Built by Nexa Systems Inc.
|
|||||||
'data/cron.xml',
|
'data/cron.xml',
|
||||||
'wizards/auto_reconcile_wizard_views.xml',
|
'wizards/auto_reconcile_wizard_views.xml',
|
||||||
'wizards/bulk_reconcile_wizard_views.xml',
|
'wizards/bulk_reconcile_wizard_views.xml',
|
||||||
|
'reports/migration_audit_report_views.xml',
|
||||||
|
'reports/migration_audit_report_action.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
'assets': {
|
||||||
'web.assets_backend': [
|
'web.assets_backend': [
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ from . import account_reconcile_model
|
|||||||
from . import fusion_reconcile_engine
|
from . import fusion_reconcile_engine
|
||||||
from . import fusion_unreconciled_bank_line_mv
|
from . import fusion_unreconciled_bank_line_mv
|
||||||
from . import fusion_bank_rec_cron
|
from . import fusion_bank_rec_cron
|
||||||
|
from . import fusion_migration_wizard
|
||||||
|
|||||||
97
fusion_accounting_bank_rec/models/fusion_migration_wizard.py
Normal file
97
fusion_accounting_bank_rec/models/fusion_migration_wizard.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"""Bank-rec specific migration step.
|
||||||
|
|
||||||
|
Hooks into fusion.migration.wizard (defined by fusion_accounting_migration)
|
||||||
|
to bootstrap fusion.reconcile.precedent from existing
|
||||||
|
account.partial.reconcile rows. This gives the AI immediate "memory" from
|
||||||
|
past Enterprise reconciles so suggestions can be ranked by precedent
|
||||||
|
similarity from day one.
|
||||||
|
|
||||||
|
The bootstrap step is exposed as a public method (_bank_rec_bootstrap_step)
|
||||||
|
so tests and the audit report can invoke it directly. action_run_migration
|
||||||
|
is overridden to call super() then run the bootstrap.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import _, models
|
||||||
|
|
||||||
|
from ..services.precedent_backfill import backfill_precedents
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FusionMigrationWizard(models.TransientModel):
|
||||||
|
_inherit = "fusion.migration.wizard"
|
||||||
|
|
||||||
|
def _bank_rec_bootstrap_step(self):
|
||||||
|
"""Migration step: backfill precedents + refresh patterns + refresh MV.
|
||||||
|
|
||||||
|
Returns a dict describing what happened, suitable for surfacing to
|
||||||
|
the user via notification or PDF audit report.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
_logger.info(
|
||||||
|
"fusion_accounting_bank_rec migration step: bootstrap starting")
|
||||||
|
|
||||||
|
company_id = None
|
||||||
|
if 'company_id' in self._fields and self.company_id:
|
||||||
|
company_id = self.company_id.id
|
||||||
|
|
||||||
|
precedent_result = backfill_precedents(
|
||||||
|
self.env, company_id=company_id, limit=10000)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.env['fusion.bank.rec.cron']._cron_refresh_patterns()
|
||||||
|
patterns_ok = True
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
_logger.warning(
|
||||||
|
"Pattern refresh during migration failed: %s", e)
|
||||||
|
patterns_ok = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.env['fusion.unreconciled.bank.line.mv']._refresh(
|
||||||
|
concurrently=False)
|
||||||
|
mv_ok = True
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
_logger.warning("MV refresh during migration failed: %s", e)
|
||||||
|
mv_ok = False
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'step': 'bank_rec_bootstrap',
|
||||||
|
'precedents_created': precedent_result['created'],
|
||||||
|
'precedents_skipped': precedent_result['skipped'],
|
||||||
|
'patterns_refreshed': patterns_ok,
|
||||||
|
'mv_refreshed': mv_ok,
|
||||||
|
}
|
||||||
|
_logger.info(
|
||||||
|
"fusion_accounting_bank_rec bootstrap complete: %s", result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def action_run_migration(self):
|
||||||
|
"""Override the migration entry-point to add the bank-rec step.
|
||||||
|
|
||||||
|
Calls super() (which currently returns a notification stub from
|
||||||
|
Phase 0) and then runs the bank-rec bootstrap. Returns a
|
||||||
|
notification summarizing both.
|
||||||
|
"""
|
||||||
|
_ = super().action_run_migration()
|
||||||
|
result = self._bank_rec_bootstrap_step()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'type': 'success',
|
||||||
|
'title': _("Bank-Rec Migration Complete"),
|
||||||
|
'message': _(
|
||||||
|
"Backfilled %(created)d precedents "
|
||||||
|
"(skipped %(skipped)d). "
|
||||||
|
"Patterns refreshed: %(p)s. MV refreshed: %(m)s."
|
||||||
|
) % {
|
||||||
|
'created': result['precedents_created'],
|
||||||
|
'skipped': result['precedents_skipped'],
|
||||||
|
'p': 'yes' if result['patterns_refreshed'] else 'no',
|
||||||
|
'm': 'yes' if result['mv_refreshed'] else 'no',
|
||||||
|
},
|
||||||
|
'sticky': False,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ class FusionReconcilePrecedent(models.Model):
|
|||||||
reconciled_at = fields.Datetime()
|
reconciled_at = fields.Datetime()
|
||||||
source = fields.Selection([
|
source = fields.Selection([
|
||||||
('historical_bootstrap', 'Imported from history'),
|
('historical_bootstrap', 'Imported from history'),
|
||||||
|
('backfill', 'Backfilled from account.partial.reconcile (migration)'),
|
||||||
('manual', 'Manual reconcile via fusion'),
|
('manual', 'Manual reconcile via fusion'),
|
||||||
('ai_accepted', 'AI suggestion accepted'),
|
('ai_accepted', 'AI suggestion accepted'),
|
||||||
('auto_rule', 'account.reconcile.model auto-fired'),
|
('auto_rule', 'account.reconcile.model auto-fired'),
|
||||||
|
|||||||
1
fusion_accounting_bank_rec/reports/__init__.py
Normal file
1
fusion_accounting_bank_rec/reports/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import migration_audit_report
|
||||||
51
fusion_accounting_bank_rec/reports/migration_audit_report.py
Normal file
51
fusion_accounting_bank_rec/reports/migration_audit_report.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""QWeb PDF report: summary of bank-rec migration outcomes.
|
||||||
|
|
||||||
|
Triggered from the migration wizard's "Print" menu after the wizard
|
||||||
|
completes. For each company on the system, reports:
|
||||||
|
- Backfilled precedents (source='backfill')
|
||||||
|
- Fusion reconcile patterns
|
||||||
|
- Bank statement lines still unreconciled
|
||||||
|
|
||||||
|
Lets the operator confirm Phase 1 migration successfully bootstrapped
|
||||||
|
the AI's reconcile memory from past Enterprise reconciles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
|
||||||
|
class FusionMigrationAuditReport(models.AbstractModel):
|
||||||
|
_name = "report.fusion_accounting_bank_rec.migration_audit_template"
|
||||||
|
_description = "Bank-Rec Migration Audit Report"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_report_values(self, docids, data=None):
|
||||||
|
Wizard = self.env['fusion.migration.wizard']
|
||||||
|
wizards = Wizard.browse(docids) if docids else Wizard
|
||||||
|
|
||||||
|
Precedent = self.env['fusion.reconcile.precedent']
|
||||||
|
Pattern = self.env['fusion.reconcile.pattern']
|
||||||
|
Line = self.env['account.bank.statement.line']
|
||||||
|
|
||||||
|
company_stats = []
|
||||||
|
for company in self.env['res.company'].search([]):
|
||||||
|
company_stats.append({
|
||||||
|
'company': company,
|
||||||
|
'precedents_count': Precedent.search_count([
|
||||||
|
('company_id', '=', company.id),
|
||||||
|
('source', '=', 'backfill'),
|
||||||
|
]),
|
||||||
|
'patterns_count': Pattern.search_count([
|
||||||
|
('company_id', '=', company.id),
|
||||||
|
]),
|
||||||
|
'unreconciled_count': Line.search_count([
|
||||||
|
('company_id', '=', company.id),
|
||||||
|
('is_reconciled', '=', False),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'doc_ids': docids,
|
||||||
|
'doc_model': 'fusion.migration.wizard',
|
||||||
|
'docs': wizards,
|
||||||
|
'company_stats': company_stats,
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="action_report_migration_audit" model="ir.actions.report">
|
||||||
|
<field name="name">Bank-Rec Migration Audit</field>
|
||||||
|
<field name="model">fusion.migration.wizard</field>
|
||||||
|
<field name="report_type">qweb-pdf</field>
|
||||||
|
<field name="report_name">fusion_accounting_bank_rec.migration_audit_template</field>
|
||||||
|
<field name="report_file">fusion_accounting_bank_rec.migration_audit_template</field>
|
||||||
|
<field name="binding_model_id" ref="fusion_accounting_migration.model_fusion_migration_wizard"/>
|
||||||
|
<field name="binding_type">report</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="migration_audit_template">
|
||||||
|
<t t-call="web.html_container">
|
||||||
|
<t t-call="web.external_layout">
|
||||||
|
<div class="page">
|
||||||
|
<h2>Bank-Rec Migration Audit</h2>
|
||||||
|
<p>
|
||||||
|
Generated
|
||||||
|
<span t-esc="context_timestamp(datetime.datetime.now()).strftime('%Y-%m-%d %H:%M')"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Per-Company Summary</h3>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Company</th>
|
||||||
|
<th class="text-end">Backfilled Precedents</th>
|
||||||
|
<th class="text-end">Patterns</th>
|
||||||
|
<th class="text-end">Still Unreconciled</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr t-foreach="company_stats" t-as="cs">
|
||||||
|
<td><span t-esc="cs['company'].name"/></td>
|
||||||
|
<td class="text-end"><span t-esc="cs['precedents_count']"/></td>
|
||||||
|
<td class="text-end"><span t-esc="cs['patterns_count']"/></td>
|
||||||
|
<td class="text-end"><span t-esc="cs['unreconciled_count']"/></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
This report verifies that Phase 1 migration successfully
|
||||||
|
bootstrapped the AI's reconcile memory from past Enterprise
|
||||||
|
reconciles.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
@@ -4,3 +4,4 @@ from . import matching_strategies
|
|||||||
from . import precedent_lookup
|
from . import precedent_lookup
|
||||||
from . import pattern_extractor
|
from . import pattern_extractor
|
||||||
from . import confidence_scoring
|
from . import confidence_scoring
|
||||||
|
from . import precedent_backfill
|
||||||
|
|||||||
116
fusion_accounting_bank_rec/services/precedent_backfill.py
Normal file
116
fusion_accounting_bank_rec/services/precedent_backfill.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""Pure-Python helpers for backfilling fusion.reconcile.precedent
|
||||||
|
from existing account.partial.reconcile rows during migration.
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
- Each account.partial.reconcile that involves at least one
|
||||||
|
account.bank.statement.line's reconcile-account line is a candidate.
|
||||||
|
- One precedent per qualifying partial. The (statement_line.id, account_id,
|
||||||
|
amount) triple is encoded into matched_account_ids so a second run can
|
||||||
|
detect and skip already-backfilled rows (idempotency).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .memo_tokenizer import tokenize_memo
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _identify_bank_side(partial):
|
||||||
|
"""Return (bank_move_line, counterpart_move_line, statement_line_id)
|
||||||
|
or (None, None, None) if neither side is a bank statement line."""
|
||||||
|
debit_line = partial.debit_move_id
|
||||||
|
credit_line = partial.credit_move_id
|
||||||
|
|
||||||
|
if debit_line.move_id.statement_line_id:
|
||||||
|
return debit_line, credit_line, debit_line.move_id.statement_line_id.id
|
||||||
|
if credit_line.move_id.statement_line_id:
|
||||||
|
return credit_line, debit_line, credit_line.move_id.statement_line_id.id
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_precedents(env, *, company_id=None, batch_size=500, limit=10000):
|
||||||
|
"""Walk account.partial.reconcile and create fusion.reconcile.precedent
|
||||||
|
rows for any reconcile that involves a bank statement line.
|
||||||
|
|
||||||
|
Idempotent: skips partials whose (statement_line, account, amount)
|
||||||
|
signature is already present in fusion.reconcile.precedent (encoded
|
||||||
|
via matched_account_ids).
|
||||||
|
|
||||||
|
Returns dict with `created` and `skipped` counts.
|
||||||
|
"""
|
||||||
|
Precedent = env['fusion.reconcile.precedent'].sudo()
|
||||||
|
Partial = env['account.partial.reconcile'].sudo()
|
||||||
|
Line = env['account.bank.statement.line'].sudo()
|
||||||
|
|
||||||
|
in_test_mode = env.cr.__class__.__name__ == 'TestCursor'
|
||||||
|
|
||||||
|
# Pre-filter to partials that touch a bank statement line on either side.
|
||||||
|
# In a real DB we typically have 10x more invoice<->payment partials than
|
||||||
|
# bank-rec partials; filtering here keeps the loop bounded and makes the
|
||||||
|
# default limit reflect "real" candidates rather than every partial ever.
|
||||||
|
domain = [
|
||||||
|
'|',
|
||||||
|
('debit_move_id.move_id.statement_line_id', '!=', False),
|
||||||
|
('credit_move_id.move_id.statement_line_id', '!=', False),
|
||||||
|
]
|
||||||
|
if company_id:
|
||||||
|
domain.append(('company_id', '=', company_id))
|
||||||
|
partials = Partial.search(domain, limit=limit, order='id asc')
|
||||||
|
|
||||||
|
created = 0
|
||||||
|
skipped = 0
|
||||||
|
for partial in partials:
|
||||||
|
bank_line, counterpart, bsl_id = _identify_bank_side(partial)
|
||||||
|
if not bsl_id:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
signature_account = str(counterpart.account_id.id)
|
||||||
|
|
||||||
|
existing = Precedent.search([
|
||||||
|
('partner_id', '=',
|
||||||
|
counterpart.partner_id.id if counterpart.partner_id else False),
|
||||||
|
('amount', '=', abs(partial.amount)),
|
||||||
|
('matched_account_ids', '=ilike', f'%{signature_account}%'),
|
||||||
|
('source', '=', 'backfill'),
|
||||||
|
], limit=1)
|
||||||
|
if existing:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
statement_line = Line.browse(bsl_id)
|
||||||
|
try:
|
||||||
|
currency = (partial.debit_currency_id
|
||||||
|
or partial.company_id.currency_id)
|
||||||
|
Precedent.create({
|
||||||
|
'company_id': partial.company_id.id,
|
||||||
|
'partner_id': (counterpart.partner_id.id
|
||||||
|
if counterpart.partner_id else False),
|
||||||
|
'amount': abs(partial.amount),
|
||||||
|
'currency_id': currency.id,
|
||||||
|
'date': statement_line.date or partial.create_date.date(),
|
||||||
|
'memo_tokens': ','.join(
|
||||||
|
tokenize_memo(statement_line.payment_ref or '')),
|
||||||
|
'journal_id': statement_line.journal_id.id,
|
||||||
|
'matched_move_line_count': 1,
|
||||||
|
'matched_account_ids': signature_account,
|
||||||
|
'reconciler_user_id': partial.create_uid.id,
|
||||||
|
'reconciled_at': partial.create_date,
|
||||||
|
'source': 'backfill',
|
||||||
|
})
|
||||||
|
created += 1
|
||||||
|
if created % batch_size == 0:
|
||||||
|
if not in_test_mode:
|
||||||
|
env.cr.commit()
|
||||||
|
_logger.info(
|
||||||
|
"Backfill progress: %d created, %d skipped",
|
||||||
|
created, skipped)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
_logger.warning("Backfill skip partial %s: %s", partial.id, e)
|
||||||
|
skipped += 1
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
"precedent_backfill complete: %d created, %d skipped",
|
||||||
|
created, skipped)
|
||||||
|
return {'created': created, 'skipped': skipped}
|
||||||
@@ -18,3 +18,4 @@ from . import test_cron_methods
|
|||||||
from . import test_controller
|
from . import test_controller
|
||||||
from . import test_auto_reconcile_wizard
|
from . import test_auto_reconcile_wizard
|
||||||
from . import test_bulk_reconcile_wizard
|
from . import test_bulk_reconcile_wizard
|
||||||
|
from . import test_migration_round_trip
|
||||||
|
|||||||
115
fusion_accounting_bank_rec/tests/test_migration_round_trip.py
Normal file
115
fusion_accounting_bank_rec/tests/test_migration_round_trip.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"""Migration round-trip: bootstrap step backfills precedents from
|
||||||
|
existing account.partial.reconcile rows.
|
||||||
|
|
||||||
|
Exercises Task 39's _bank_rec_bootstrap_step end-to-end:
|
||||||
|
1. Set up a bank-line / invoice reconciliation via the engine. This
|
||||||
|
creates an account.partial.reconcile row.
|
||||||
|
2. Wipe the auto-recorded fusion.reconcile.precedent rows so the
|
||||||
|
backfill has work to do.
|
||||||
|
3. Run wizard._bank_rec_bootstrap_step().
|
||||||
|
4. Assert at least one precedent was created with source='backfill',
|
||||||
|
the wizard reports successful pattern + MV refresh, and that a
|
||||||
|
second run is a no-op (idempotent).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase, tagged
|
||||||
|
|
||||||
|
from . import _factories as f
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestMigrationRoundTrip(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.partner = self.env['res.partner'].create({
|
||||||
|
'name': 'Migration Round-Trip Partner',
|
||||||
|
})
|
||||||
|
self.journal = f.make_bank_journal(
|
||||||
|
self.env, name='Migration Bank', code='MIGBK')
|
||||||
|
self.statement = f.make_bank_statement(
|
||||||
|
self.env, journal=self.journal, name='Migration Statement')
|
||||||
|
|
||||||
|
def _seed_partial_reconciles(self, amounts):
|
||||||
|
"""Create one reconciled bank-line/invoice pair per amount, reusing
|
||||||
|
a single bank journal so we don't violate the
|
||||||
|
account_journal_code_company_uniq constraint.
|
||||||
|
|
||||||
|
Each call here produces one account.partial.reconcile row.
|
||||||
|
Returns the partial recordset.
|
||||||
|
"""
|
||||||
|
Engine = self.env['fusion.reconcile.engine']
|
||||||
|
partials = self.env['account.partial.reconcile']
|
||||||
|
for amount in amounts:
|
||||||
|
invoice = f.make_invoice(
|
||||||
|
self.env, partner=self.partner, amount=amount)
|
||||||
|
recv_lines = invoice.line_ids.filtered(
|
||||||
|
lambda l: l.account_id.account_type == 'asset_receivable')
|
||||||
|
bank_line = f.make_bank_line(
|
||||||
|
self.env, statement=self.statement, amount=amount,
|
||||||
|
partner=self.partner)
|
||||||
|
result = Engine.reconcile_one(
|
||||||
|
bank_line, against_lines=recv_lines)
|
||||||
|
partials |= self.env['account.partial.reconcile'].browse(
|
||||||
|
result['partial_ids'])
|
||||||
|
return partials
|
||||||
|
|
||||||
|
def _wipe_precedents(self):
|
||||||
|
self.env['fusion.reconcile.precedent'].search([
|
||||||
|
('partner_id', '=', self.partner.id),
|
||||||
|
]).unlink()
|
||||||
|
|
||||||
|
def test_bootstrap_creates_precedents_from_existing_reconciles(self):
|
||||||
|
partials = self._seed_partial_reconciles([125.00, 275.00])
|
||||||
|
self.assertTrue(partials,
|
||||||
|
"Test setup should produce account.partial.reconcile rows")
|
||||||
|
|
||||||
|
self._wipe_precedents()
|
||||||
|
before_backfill = self.env['fusion.reconcile.precedent'].search_count([
|
||||||
|
('partner_id', '=', self.partner.id),
|
||||||
|
('source', '=', 'backfill'),
|
||||||
|
])
|
||||||
|
self.assertEqual(before_backfill, 0,
|
||||||
|
"Precondition: no backfill precedents should exist before bootstrap")
|
||||||
|
|
||||||
|
wizard = self.env['fusion.migration.wizard'].create({})
|
||||||
|
result = wizard._bank_rec_bootstrap_step()
|
||||||
|
|
||||||
|
self.assertEqual(result['step'], 'bank_rec_bootstrap')
|
||||||
|
self.assertGreaterEqual(result['precedents_created'], 1,
|
||||||
|
"Bootstrap should backfill at least one precedent from the "
|
||||||
|
"partial.reconcile rows produced in setUp")
|
||||||
|
self.assertTrue(result['mv_refreshed'],
|
||||||
|
"Bootstrap should report successful MV refresh")
|
||||||
|
|
||||||
|
after_backfill = self.env['fusion.reconcile.precedent'].search_count([
|
||||||
|
('partner_id', '=', self.partner.id),
|
||||||
|
('source', '=', 'backfill'),
|
||||||
|
])
|
||||||
|
self.assertGreaterEqual(after_backfill, 1,
|
||||||
|
"At least one source='backfill' precedent should exist post-bootstrap")
|
||||||
|
|
||||||
|
def test_bootstrap_step_idempotent(self):
|
||||||
|
self._seed_partial_reconciles([411.00])
|
||||||
|
self._wipe_precedents()
|
||||||
|
|
||||||
|
wizard = self.env['fusion.migration.wizard'].create({})
|
||||||
|
result1 = wizard._bank_rec_bootstrap_step()
|
||||||
|
created_first_run = result1['precedents_created']
|
||||||
|
self.assertGreaterEqual(created_first_run, 1)
|
||||||
|
|
||||||
|
result2 = wizard._bank_rec_bootstrap_step()
|
||||||
|
self.assertEqual(result2['precedents_created'], 0,
|
||||||
|
"Second bootstrap should create zero precedents (idempotent)")
|
||||||
|
self.assertGreaterEqual(result2['precedents_skipped'], created_first_run,
|
||||||
|
"Second bootstrap should skip at least what the first one created")
|
||||||
|
|
||||||
|
def test_bootstrap_refreshes_mv_without_error(self):
|
||||||
|
"""The bootstrap call must not raise even when there's nothing to do."""
|
||||||
|
wizard = self.env['fusion.migration.wizard'].create({})
|
||||||
|
try:
|
||||||
|
result = wizard._bank_rec_bootstrap_step()
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
self.fail(f"Bootstrap raised: {e}")
|
||||||
|
self.assertIn('mv_refreshed', result)
|
||||||
|
self.assertIn('patterns_refreshed', result)
|
||||||
Reference in New Issue
Block a user