Files
reimburse/native-engine/tests/test_expense_preview.py
T
2026-09-17 15:43:40 +08:00

120 lines
6.3 KiB
Python

import datetime
import json
import os
import subprocess
import sys
import unittest
from decimal import Decimal
from pathlib import Path
from openpyxl import load_workbook
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import test_expense_template as fixtures
from expense_preview import PreviewCalculator, UnsupportedFormula, builtin_rmb, prepare_preview
from expense_template import Template, encoded, set_cell, tag
class ExpensePreviewTests(unittest.TestCase):
def setUp(self):
self.fixture = fixtures.ExpenseTemplateTests()
self.fixture.setUp()
self.addCleanup(self.fixture.tearDown)
def test_filled_workbook_previews_current_totals_and_preserves_formulas(self):
output, _ = self.fixture.export()
result = prepare_preview(output)
values = load_workbook(output, data_only=True)
formulas = load_workbook(output)
sheet = values.active
self.assertEqual(sheet['C9'].value, '交通')
self.assertEqual(sheet['H9'].value, 100.3)
self.assertEqual(sheet['H10'].value, 444)
self.assertEqual(sheet['F27'].value, 544.3)
self.assertEqual(sheet['H29'].value, 544.3)
self.assertEqual(formulas.active['F27'].value, '=SUM(H9:H21)')
self.assertEqual(formulas.active['H29'].value, '=IF(F27>C27,F27-C27,0)')
self.assertEqual(result['previewUncalculatedCells'], [])
self.assertEqual(sheet['D29'].value, '伍佰肆拾肆元叁角整')
self.assertEqual(result['expensePreview'], 'xlsx-layout-v2')
values.close()
formulas.close()
def test_builtin_uppercase_amount_formula(self):
for amount, expected in [('3491.86', '叁仟肆佰玖拾壹元捌角陆分'), ('10001.01', '壹万零壹元零壹分'),
('100000001', '壹亿零壹元整'), ('100010000', '壹亿零壹万元整'),
('0.01', '壹分'), ('0.1', '壹角整'), ('-50.02', '负伍拾元零贰分'),
('0', ''), ('1.999', '贰元整'), ('999999999999.99', '玖仟玖佰玖拾玖亿玖仟玖佰玖拾玖万玖仟玖佰玖拾玖元玖角玖分')]:
with self.subTest(amount=amount):
self.assertEqual(builtin_rmb(Decimal(amount)), expected)
def test_caches_each_custom_page_not_whole_report(self):
source, mapping = self.fixture.alternate()
state = dict(matches=self.fixture.state['matches'] + [self.fixture.match('third', '住宿', '50')])
output, _ = self.fixture.export(mapping=mapping, template=source, state=state)
prepare_preview(output)
workbook = load_workbook(output, data_only=True)
self.assertEqual(workbook['组 B 报销']['F8'].value, 544.3)
self.assertEqual(workbook['组 B 报销-续2']['F8'].value, 50)
self.assertIn('保留说明', workbook.sheetnames)
workbook.close()
def test_clears_unknown_cache_and_does_not_execute_formula(self):
output, _ = self.fixture.export()
template = Template(output)
_, path, document = template.sheet('个人报销单')
set_cell(document, 'I35', '__import__("os").system("touch /tmp/should-not-run")', formula=True)
from lxml import etree as ET
ET.SubElement(document.find('.//' + tag('c') + '[@r="I35"]'), tag('v')).text = '9999'
template.parts[path] = encoded(document)
self.fixture.rewrite(output, template.parts)
result = prepare_preview(output)
self.assertIn('个人报销单!I35', result['previewUncalculatedCells'])
workbook = load_workbook(output, data_only=True)
self.assertIsNone(workbook.active['I35'].value)
workbook.close()
def test_calculator_cycles_text_dates_and_conditionals(self):
template = Template(self.fixture.template)
_, _, sheet = template.sheet('个人报销单')
set_cell(sheet, 'J1', 10)
set_cell(sheet, 'J2', '文字')
set_cell(sheet, 'J3', '-J1*2+5', formula=True)
set_cell(sheet, 'J4', 'J5', formula=True)
set_cell(sheet, 'J5', 'J4', formula=True)
calculator = PreviewCalculator(sheet)
self.assertEqual(calculator.cell('J3'), -15)
self.assertEqual(calculator.expression('SUM(J1:J3)'), -5)
self.assertEqual(calculator.expression('IF(J1>0,J1/2,1/0)'), 5)
self.assertEqual(calculator.expression('TODAY()'), (datetime.date.today() - datetime.date(1899, 12, 30)).days)
self.assertEqual(PreviewCalculator(sheet, True).expression('TODAY()'),
(datetime.date.today() - datetime.date(1904, 1, 1)).days)
with self.assertRaises(UnsupportedFormula):
calculator.cell('J4')
with self.assertRaises(UnsupportedFormula):
calculator.expression('2**10000000')
def test_engine_process_preview_builtin_and_imported_templates(self):
for custom in [False, True]:
with self.subTest(custom=custom):
output = self.fixture.root / ('custom-preview.xlsx' if custom else 'builtin-preview.xlsx')
request = dict(operation='expense', state=self.fixture.state, destination=str(output), preparePreview=True)
if custom:
request.update(templatePath=str(self.fixture.template), templateMapping=self.fixture.mapping)
binary = os.environ.get('RECEIPT_ENGINE_BINARY')
command = [binary] if binary else [sys.executable, str(Path(__file__).resolve().parents[1] / 'engine.py')]
process = subprocess.run(command, input=json.dumps(request) + '\n', text=True, capture_output=True, timeout=60)
self.assertEqual(process.returncode, 0, process.stderr + process.stdout)
result = next(json.loads(line)['result'] for line in process.stdout.splitlines() if json.loads(line).get('event') == 'result')
self.assertEqual(result['expensePreview'], 'xlsx-layout-v2')
self.assertTrue(result['previewLayout']['sheets'])
self.assertEqual(result['expenseRowCount'], 2)
workbook = load_workbook(output, data_only=True)
self.assertEqual(workbook.active['F27'].value, 544.3)
self.assertEqual(workbook.active['H29'].value, 544.3)
self.assertIn(workbook.active['C23'].value, ('', None))
workbook.close()
if __name__ == '__main__':
unittest.main()