Files
reimburse/native-engine/tests/test_expense_preview.py
T
2026-09-18 10:35:38 +08:00

175 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, export_custom_expense, 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-v4')
values.close()
formulas.close()
def test_original_preview_preserves_signature_borders_fonts_and_geometry(self):
signatures = ['部门长', '剧组出纳', '制片主任', '剧组会计', '执行制片人']
payee = dict(recipient='测试收款人', bankName='测试银行', accountNumber='001234567890')
source = self.fixture.root / 'signature-template.xlsx'
output = self.fixture.root / 'signature-preview.xlsx'
export_custom_expense(self.fixture.state, source, self.fixture.template, self.fixture.mapping,
{}, signatures, payee, {})
prepare_preview(source)
changed = dict(matches=[self.fixture.match('preview-changed', '交通', '447')])
export_custom_expense(changed, output, source, self.fixture.mapping,
{}, signatures, payee, {})
result = prepare_preview(output)
original, preview = Template(source), Template(output)
self.assertEqual(result['previewLayout']['version'], 4)
self.assertEqual(original.parts['xl/styles.xml'], preview.parts['xl/styles.xml'])
_, _, before = original.sheet('个人报销单')
_, _, after = preview.sheet('个人报销单')
for name in ['cols', 'mergeCells', 'sheetFormatPr', 'pageMargins', 'pageSetup']:
self.assertEqual(encoded(before.find(tag(name))), encoded(after.find(tag(name))), name)
for reference, label in zip(['A30', 'D30', 'A31', 'D31', 'A32'], signatures):
old = before.find('.//' + tag('c') + f'[@r="{reference}"]')
new = after.find('.//' + tag('c') + f'[@r="{reference}"]')
self.assertEqual(encoded(old), encoded(new), reference)
self.assertEqual(''.join(node.text or '' for node in new.iter(tag('t'))), label + '')
old_cells = {cell.get('r'): cell.get('s') for cell in before.iter(tag('c'))}
new_cells = {cell.get('r'): cell.get('s') for cell in after.iter(tag('c'))}
self.assertEqual(old_cells, new_cells)
for row in [30, 31, 32]:
old = before.find(tag('sheetData') + '/' + tag('row') + f'[@r="{row}"]')
new = after.find(tag('sheetData') + '/' + tag('row') + f'[@r="{row}"]')
self.assertEqual(old.attrib, new.attrib)
folder = os.environ.get('RECEIPT_PREVIEW_FIXTURE_DIR')
if folder:
folder = Path(folder)
folder.mkdir(parents=True, exist_ok=True)
(folder / 'template.xlsx').write_bytes(source.read_bytes())
(folder / 'preview.xlsx').write_bytes(output.read_bytes())
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-v4')
self.assertTrue(result['previewLayout']['sheets'])
self.assertNotIn('templatePreviewLayout', result)
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()
def test_engine_process_template_preview_is_read_only_without_expense_selection(self):
from spreadsheet_layout import read_layout
alternate, _ = self.fixture.alternate()
binary = os.environ.get('RECEIPT_ENGINE_BINARY')
command = [binary] if binary else [sys.executable, str(Path(__file__).resolve().parents[1] / 'engine.py')]
for source in [self.fixture.template, alternate]:
before = source.read_bytes()
request = dict(operation='preview-expense-template', templatePath=str(source))
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, read_layout(source))
self.assertEqual(source.read_bytes(), before)
if __name__ == '__main__':
unittest.main()