|
1 | | -"""Factur-X pipeline integration tests. |
2 | | -
|
3 | | -Covers the Factur-X shortcut in the extraction pipeline: |
4 | | - 1. Factur-X detection skips AI API call |
5 | | - 2. Returns correct wrapper dict |
6 | | - 3. Non-PDF and unavailable cases |
7 | | - 4. Apply and preview modes |
8 | | -""" |
| 1 | +"""Factur-X pipeline integration tests.""" |
9 | 2 |
|
10 | 3 | import base64 |
| 4 | +import json |
11 | 5 | from unittest.mock import MagicMock, patch |
12 | 6 |
|
13 | 7 | from odoo.tests.common import TransactionCase, tagged |
14 | 8 |
|
15 | 9 | _MODULE = 'odoo.addons.account_invoice_digitize_ai' |
16 | 10 |
|
17 | | -SAMPLE_FACTURX_XML = '<xml>facturx</xml>' |
| 11 | +SAMPLE_FACTURX_XML = b'<xml>facturx</xml>' |
| 12 | +SAMPLE_FACTURX_DATA = { |
| 13 | + 'document_type': 'invoice', |
| 14 | + 'is_marked_paid': False, |
| 15 | + 'vendor': {'name': 'Factur-X Vendor', 'vat': 'DE123456789', 'confidence': 1.0}, |
| 16 | + 'buyer': {'name': 'Factor 3', 'vat': 'DE999999999', 'confidence': 1.0}, |
| 17 | + 'invoice': { |
| 18 | + 'reference': 'FX-001', |
| 19 | + 'invoice_date': '2024-01-15', |
| 20 | + 'currency': 'EUR', |
| 21 | + 'confidence': 1.0, |
| 22 | + }, |
| 23 | + 'totals': { |
| 24 | + 'untaxed_amount': 1000.0, |
| 25 | + 'tax_amount': 200.0, |
| 26 | + 'total_amount': 1200.0, |
| 27 | + 'confidence': 1.0, |
| 28 | + }, |
| 29 | + 'tax_lines': [ |
| 30 | + { |
| 31 | + 'tax_label': 'VAT 20%', |
| 32 | + 'tax_rate': 20.0, |
| 33 | + 'base_amount': 1000.0, |
| 34 | + 'tax_amount': 200.0, |
| 35 | + 'confidence': 1.0, |
| 36 | + } |
| 37 | + ], |
| 38 | + 'lines': [ |
| 39 | + { |
| 40 | + 'description': 'Consulting services', |
| 41 | + 'quantity': 1.0, |
| 42 | + 'unit_price': 1000.0, |
| 43 | + 'subtotal_untaxed': 1000.0, |
| 44 | + 'tax_rate': 20.0, |
| 45 | + } |
| 46 | + ], |
| 47 | + 'table_analysis': { |
| 48 | + 'number_format': 'dot_decimal', |
| 49 | + 'complexity': 'simple', |
| 50 | + 'line_count': 1, |
| 51 | + }, |
| 52 | +} |
| 53 | +SAMPLE_FACTURX_PREVIEW_DATA = { |
| 54 | + **SAMPLE_FACTURX_DATA, |
| 55 | + '_source': 'facturx', |
| 56 | + '_require_preview': True, |
| 57 | +} |
18 | 58 |
|
19 | 59 |
|
20 | 60 | @tagged('post_install', '-at_install') |
@@ -91,39 +131,116 @@ def test_facturx_unavailable_skipped(self): |
91 | 131 | @patch(f'{_MODULE}.models.ai_facturx_parser.parse_facturx_xml') |
92 | 132 | def test_facturx_apply_sets_done(self, mock_parse, _is_pdf, _detect): |
93 | 133 | """Successful Factur-X extraction should set status to 'done'.""" |
94 | | - mock_parse.return_value = { |
95 | | - 'vendor': {'name': 'Test Vendor', 'confidence': 1.0}, |
96 | | - 'invoice': {'number': 'FX-001', 'date': '2024-01-15', 'confidence': 1.0}, |
97 | | - 'totals': {'total_amount': 1200, 'confidence': 1.0}, |
98 | | - 'document_type': 'invoice', |
99 | | - } |
| 134 | + mock_parse.return_value = SAMPLE_FACTURX_DATA |
| 135 | + move = self.env['account.move'].create({ |
| 136 | + 'move_type': 'in_invoice', |
| 137 | + 'company_id': self.company.id, |
| 138 | + }) |
100 | 139 | attachment = self.env['ir.attachment'].create({ |
101 | 140 | 'name': 'test.pdf', |
102 | 141 | 'datas': base64.b64encode(b'fake-pdf'), |
103 | 142 | 'res_model': 'account.move', |
104 | | - 'res_id': self.move.id, |
| 143 | + 'res_id': move.id, |
105 | 144 | }) |
106 | | - self.move._ai_trigger_extraction('test-key-123', attachment) |
107 | | - self.assertEqual(self.move.ai_extraction_status, 'done') |
| 145 | + move._ai_trigger_extraction('test-key-123', attachment) |
| 146 | + self.assertEqual(move.ai_extraction_status, 'done') |
108 | 147 |
|
109 | | - # --- 6. Preview returns data without applying --- |
| 148 | + # --- 6. Preview returns normalized data without applying --- |
110 | 149 |
|
111 | 150 | @patch(f'{_MODULE}.models.ai_document.detect_facturx', return_value=SAMPLE_FACTURX_XML) |
112 | 151 | @patch(f'{_MODULE}.models.ai_document.FACTURX_AVAILABLE', True) |
113 | 152 | @patch(f'{_MODULE}.models.ai_document.is_pdf', return_value=True) |
114 | | - def test_facturx_preview_returns_data(self, _is_pdf, _detect): |
115 | | - """In preview mode, Factur-X data should be returned without applying.""" |
| 153 | + @patch(f'{_MODULE}.models.ai_facturx_parser.parse_facturx_xml', return_value=SAMPLE_FACTURX_DATA) |
| 154 | + def test_facturx_preview_returns_normalized_data(self, mock_parse, _is_pdf, _detect): |
| 155 | + """Preview mode should normalize Factur-X XML into JSON-safe extraction data.""" |
| 156 | + move = self.env['account.move'].create({ |
| 157 | + 'move_type': 'in_invoice', |
| 158 | + 'company_id': self.company.id, |
| 159 | + }) |
116 | 160 | attachment = self.env['ir.attachment'].create({ |
117 | 161 | 'name': 'test.pdf', |
118 | 162 | 'datas': base64.b64encode(b'fake-pdf'), |
119 | 163 | 'res_model': 'account.move', |
120 | | - 'res_id': self.move.id, |
| 164 | + 'res_id': move.id, |
121 | 165 | }) |
122 | | - result = self.move._ai_trigger_extraction( |
| 166 | + result = move._ai_trigger_extraction( |
123 | 167 | 'test-key-123', attachment, preview=True, |
124 | 168 | ) |
| 169 | + mock_parse.assert_called_once_with(SAMPLE_FACTURX_XML) |
125 | 170 | self.assertIsNotNone(result) |
126 | | - self.assertTrue(result.get('_facturx')) |
127 | | - self.assertEqual(result['_xml'], SAMPLE_FACTURX_XML) |
| 171 | + self.assertEqual(result, SAMPLE_FACTURX_PREVIEW_DATA) |
128 | 172 | # Status should NOT be 'done' (preview mode) |
129 | | - self.assertNotEqual(self.move.ai_extraction_status, 'done') |
| 173 | + self.assertNotEqual(move.ai_extraction_status, 'done') |
| 174 | + |
| 175 | + # --- 7. Sync button uses normalized Factur-X preview data --- |
| 176 | + |
| 177 | + @patch(f'{_MODULE}.models.ai_document.detect_facturx', return_value=SAMPLE_FACTURX_XML) |
| 178 | + @patch(f'{_MODULE}.models.ai_document.FACTURX_AVAILABLE', True) |
| 179 | + @patch(f'{_MODULE}.models.ai_document.is_pdf', return_value=True) |
| 180 | + @patch(f'{_MODULE}.models.ai_facturx_parser.parse_facturx_xml', return_value=SAMPLE_FACTURX_DATA) |
| 181 | + def test_facturx_sync_button_opens_preview_wizard(self, mock_parse, _is_pdf, _detect): |
| 182 | + """The normal button path must open the preview wizard for Factur-X PDFs.""" |
| 183 | + move = self.env['account.move'].create({ |
| 184 | + 'move_type': 'in_invoice', |
| 185 | + 'company_id': self.company.id, |
| 186 | + }) |
| 187 | + self.env['ir.attachment'].create({ |
| 188 | + 'name': 'test.pdf', |
| 189 | + 'datas': base64.b64encode(b'fake-pdf'), |
| 190 | + 'mimetype': 'application/pdf', |
| 191 | + 'res_model': 'account.move', |
| 192 | + 'res_id': move.id, |
| 193 | + }) |
| 194 | + |
| 195 | + result = move.action_ai_extract() |
| 196 | + |
| 197 | + mock_parse.assert_called_once_with(SAMPLE_FACTURX_XML) |
| 198 | + self.assertEqual(result.get('res_model'), 'ai.preview.wizard') |
| 199 | + self.assertEqual(move.ai_extraction_status, 'pending') |
| 200 | + self.assertEqual(json.loads(move.ai_last_extraction_data), SAMPLE_FACTURX_PREVIEW_DATA) |
| 201 | + |
| 202 | + wizard = self.env['ai.preview.wizard'].browse(result['res_id']) |
| 203 | + self.assertEqual(wizard.invoice_ref, 'FX-001') |
| 204 | + self.assertEqual(wizard.vendor_name, 'Factur-X Vendor') |
| 205 | + |
| 206 | + @patch(f'{_MODULE}.models.ai_document.detect_facturx', return_value=SAMPLE_FACTURX_XML) |
| 207 | + @patch(f'{_MODULE}.models.ai_document.FACTURX_AVAILABLE', True) |
| 208 | + @patch(f'{_MODULE}.models.ai_document.is_pdf', return_value=True) |
| 209 | + @patch(f'{_MODULE}.models.ai_facturx_parser.parse_facturx_xml', return_value=SAMPLE_FACTURX_DATA) |
| 210 | + def test_facturx_sync_button_skips_auto_apply_and_keeps_preview(self, mock_parse, _is_pdf, _detect): |
| 211 | + """Factur-X preview data must not be auto-applied even for reliable vendors.""" |
| 212 | + self.env['ir.config_parameter'].sudo().set_param( |
| 213 | + 'account_invoice_digitize_ai.ai_auto_apply_enabled', |
| 214 | + 'True', |
| 215 | + ) |
| 216 | + partner = self.env['res.partner'].create({ |
| 217 | + 'name': 'Factur-X Vendor', |
| 218 | + 'is_company': True, |
| 219 | + 'vat': 'DE123456789', |
| 220 | + }) |
| 221 | + self.env['ai.vendor.score'].create({ |
| 222 | + 'partner_id': partner.id, |
| 223 | + 'company_id': self.company.id, |
| 224 | + 'total_extractions': 10, |
| 225 | + 'correct_extractions': 9, |
| 226 | + }) |
| 227 | + move = self.env['account.move'].create({ |
| 228 | + 'move_type': 'in_invoice', |
| 229 | + 'company_id': self.company.id, |
| 230 | + }) |
| 231 | + self.env['ir.attachment'].create({ |
| 232 | + 'name': 'test.pdf', |
| 233 | + 'datas': base64.b64encode(b'fake-pdf'), |
| 234 | + 'mimetype': 'application/pdf', |
| 235 | + 'res_model': 'account.move', |
| 236 | + 'res_id': move.id, |
| 237 | + }) |
| 238 | + |
| 239 | + result = move.action_ai_extract() |
| 240 | + |
| 241 | + mock_parse.assert_called_once_with(SAMPLE_FACTURX_XML) |
| 242 | + self.assertEqual(result.get('res_model'), 'ai.preview.wizard') |
| 243 | + self.assertEqual(move.ai_extraction_status, 'pending') |
| 244 | + self.assertFalse(move.ref) |
| 245 | + self.assertFalse(move.partner_id) |
| 246 | + self.assertEqual(json.loads(move.ai_last_extraction_data), SAMPLE_FACTURX_PREVIEW_DATA) |
0 commit comments