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-quicklook-v1') 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-quicklook-v1') 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()