193 lines
9.9 KiB
Python
193 lines
9.9 KiB
Python
import json
|
||
import os
|
||
from pathlib import Path
|
||
import subprocess
|
||
import sys
|
||
import unittest
|
||
|
||
from lxml import etree as ET
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||
import test_expense_template as fixtures
|
||
from expense_template import Template, encoded, export_custom_expense, inspect_template, set_cell, tag
|
||
from expense_preview import prepare_preview
|
||
from spreadsheet_layout import formatted, read_layout
|
||
|
||
|
||
class SpreadsheetLayoutTests(unittest.TestCase):
|
||
def setUp(self):
|
||
self.fixture = fixtures.ExpenseTemplateTests()
|
||
self.fixture.setUp()
|
||
self.addCleanup(self.fixture.tearDown)
|
||
|
||
def test_wps_signature_and_accounting_regressions(self):
|
||
source = Path(os.environ.get('RECEIPT_PREVIEW_SOURCE', self.fixture.template))
|
||
original = source.read_bytes()
|
||
mapping = inspect_template(source)['sheets'][0]['mapping']
|
||
output = self.fixture.root / 'preview.xlsx'
|
||
export_custom_expense(dict(matches=[self.fixture.match('changed', '交通', '447')]),
|
||
output, source, mapping, {},
|
||
['部门长', '剧组出纳', '制片主任', '剧组会计', '执行制片人'],
|
||
dict(recipient='测试收款人', bankName='测试银行', accountNumber='001234567890'), {})
|
||
result = prepare_preview(output)
|
||
before = output.read_bytes()
|
||
layout = read_layout(output)
|
||
self.assertEqual(output.read_bytes(), before)
|
||
self.assertEqual(source.read_bytes(), original)
|
||
self.assertEqual(layout, result['previewLayout'])
|
||
self.assertNotIn('templatePreviewLayout', result)
|
||
cells = {c['reference']: c for c in layout['sheets'][0]['cells']}
|
||
for ref, label in [('A30', '部门长'), ('D30', '剧组出纳'), ('A31', '制片主任'), ('D31', '剧组会计')]:
|
||
self.assertEqual(cells[ref]['text'], label + ':')
|
||
self.assertGreater(cells[ref]['overflowRight'], cells[ref]['x'] + cells[ref]['width'])
|
||
self.assertEqual(cells['A30']['overflowRight'], cells['D30']['x'])
|
||
self.assertEqual(cells['H30']['style']['rotation'], 255)
|
||
self.assertEqual(cells['H30']['style']['vertical'], 'bottom')
|
||
self.assertEqual(cells['H30']['style']['borders']['right']['style'], 'medium')
|
||
self.assertEqual(cells['H30']['style']['borders']['bottom']['style'], 'medium')
|
||
for ref in ['F27', 'H29']:
|
||
self.assertTrue(cells[ref]['style']['accounting'])
|
||
self.assertEqual(cells[ref]['text'].strip(), '447.00')
|
||
self.assertTrue(cells[ref]['style']['bold'])
|
||
self.assertNotIn('left', cells['D31']['style']['borders'])
|
||
self.assertEqual(cells['C25']['text'], '001234567890')
|
||
if os.environ.get('RECEIPT_PREVIEW_FIXTURE_DIR'):
|
||
folder = Path(os.environ['RECEIPT_PREVIEW_FIXTURE_DIR'])
|
||
folder.mkdir(parents=True, exist_ok=True)
|
||
(folder / 'layout.json').write_text(json.dumps(layout, ensure_ascii=False), encoding='utf-8')
|
||
(folder / 'preview.xlsx').write_bytes(before)
|
||
|
||
def test_overflow_stops_at_text_and_merged_cells(self):
|
||
output, _ = self.fixture.export()
|
||
template = Template(output)
|
||
_, path, document = template.sheet('个人报销单')
|
||
set_cell(document, 'B30', '阻止覆盖')
|
||
template.parts[path] = encoded(document)
|
||
self.fixture.rewrite(output, template.parts)
|
||
cells = {c['reference']: c for c in read_layout(output)['sheets'][0]['cells']}
|
||
self.assertEqual(cells['A30']['overflowRight'], cells['A30']['x'] + cells['A30']['width'])
|
||
self.assertEqual(cells['D30']['overflowRight'], cells['H30']['x'])
|
||
self.assertEqual(cells['A32']['overflowRight'], cells['A32']['x'] + cells['A32']['width'])
|
||
|
||
def test_visible_sheets_and_pagination(self):
|
||
source, mapping = self.fixture.alternate()
|
||
state = dict(matches=self.fixture.state['matches'] + [self.fixture.match('extra', '住宿', '50')])
|
||
output, _ = self.fixture.export(mapping=mapping, template=source, state=state)
|
||
layout = prepare_preview(output)['previewLayout']
|
||
self.assertEqual([s['name'] for s in layout['sheets']], ['组 B 报销', '保留说明', '组 B 报销-续2'])
|
||
|
||
def test_unrelated_template_style_and_geometry(self):
|
||
from openpyxl import Workbook
|
||
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
||
workbook = Workbook()
|
||
sheet = workbook.active
|
||
sheet.title = '横向费用单'
|
||
sheet.column_dimensions['A'].width = 4
|
||
sheet.column_dimensions['B'].width = 24
|
||
sheet.column_dimensions['C'].width = 12
|
||
sheet.column_dimensions['D'].width = 20
|
||
sheet.column_dimensions['E'].width = 16
|
||
sheet.column_dimensions['F'].width = 10
|
||
sheet.row_dimensions[2].height = 42
|
||
sheet.row_dimensions[6].height = 34
|
||
sheet.merge_cells('B2:F2')
|
||
sheet['B2'] = '另一种费用模板'
|
||
sheet['B2'].font = Font(name='Arial', size=18, bold=True, color='145A32')
|
||
sheet['B2'].fill = PatternFill('solid', fgColor='D5F5E3')
|
||
sheet['B2'].alignment = Alignment(horizontal='center', vertical='center')
|
||
sheet['B2'].border = Border(bottom=Side(style='double', color='145A32'))
|
||
for ref, text in [('B5', '说明'), ('D5', '金额'), ('E5', '比例'), ('F5', '编号')]:
|
||
sheet[ref] = text
|
||
sheet.merge_cells('B6:C6')
|
||
sheet['B6'] = '这里是长一些的费用说明,需要按模板换行'
|
||
sheet['B6'].alignment = Alignment(wrap_text=True, vertical='top')
|
||
sheet['D6'] = 1234.5
|
||
sheet['D6'].number_format = '#,##0.00'
|
||
sheet['D6'].alignment = Alignment(horizontal='right')
|
||
sheet['E6'] = .125
|
||
sheet['E6'].number_format = '0.0%'
|
||
sheet['F6'] = '000012'
|
||
sheet.print_area = 'A1:F8'
|
||
workbook.create_sheet('说明页')['A1'] = '附加工作表'
|
||
output = self.fixture.root / 'different-style.xlsx'
|
||
workbook.save(output)
|
||
before = output.read_bytes()
|
||
layout = read_layout(output)
|
||
self.assertEqual(output.read_bytes(), before)
|
||
self.assertEqual([s['name'] for s in layout['sheets']], ['横向费用单', '说明页'])
|
||
cells = {c['reference']: c for c in layout['sheets'][0]['cells']}
|
||
self.assertNotIn('C2', cells)
|
||
self.assertNotIn('C6', cells)
|
||
self.assertEqual(cells['B2']['height'], 56)
|
||
self.assertEqual(cells['B2']['style']['font'], 'Arial')
|
||
self.assertEqual(cells['B2']['style']['fill'], 'D5F5E3')
|
||
self.assertEqual(cells['B2']['style']['borders']['bottom']['style'], 'double')
|
||
self.assertTrue(cells['B6']['style']['wrap'])
|
||
self.assertEqual(cells['D6']['text'], '1,234.50')
|
||
self.assertEqual(cells['D6']['style']['horizontal'], 'right')
|
||
self.assertEqual(cells['E6']['text'], '12.5%')
|
||
self.assertEqual(cells['F6']['text'], '000012')
|
||
if os.environ.get('RECEIPT_PREVIEW_FIXTURE_DIR'):
|
||
folder = Path(os.environ['RECEIPT_PREVIEW_FIXTURE_DIR'])
|
||
folder.mkdir(parents=True, exist_ok=True)
|
||
(folder / 'alternate-layout.json').write_text(json.dumps(layout, ensure_ascii=False), encoding='utf-8')
|
||
|
||
def test_hidden_dimensions_and_outside_print_area(self):
|
||
output, _ = self.fixture.export()
|
||
template = Template(output)
|
||
_, path, document = template.sheet('个人报销单')
|
||
document.find(tag('cols'))[0].set('hidden', '1')
|
||
document.find(tag('sheetData'))[0].set('hidden', '1')
|
||
set_cell(document, 'J36', '打印范围外')
|
||
template.parts[path] = encoded(document)
|
||
self.fixture.rewrite(output, template.parts)
|
||
cells = {c['reference']: c for c in read_layout(output)['sheets'][0]['cells']}
|
||
self.assertNotIn('A9', cells)
|
||
self.assertEqual(cells['B9']['x'], 0)
|
||
self.assertIn('J36', cells)
|
||
|
||
def test_large_range_rejected(self):
|
||
output, _ = self.fixture.export()
|
||
template = Template(output)
|
||
_, path, document = template.sheet('个人报销单')
|
||
set_cell(document, 'IV2000', 'range')
|
||
template.parts[path] = encoded(document)
|
||
self.fixture.rewrite(output, template.parts)
|
||
with self.assertRaisesRegex(ValueError, '范围过大'):
|
||
read_layout(output)
|
||
|
||
def test_advanced_features_warn(self):
|
||
output, _ = self.fixture.export()
|
||
template = Template(output)
|
||
_, path, document = template.sheet('个人报销单')
|
||
ET.SubElement(document, tag('conditionalFormatting'), sqref='H9')
|
||
template.parts[path] = encoded(document)
|
||
self.fixture.rewrite(output, template.parts)
|
||
self.assertTrue(read_layout(output)['warnings'])
|
||
|
||
def test_numeric_and_date_formatting(self):
|
||
warnings = set()
|
||
for value, code, expected in [('447', '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)', '447.00'),
|
||
('2122.6', '#,##0.00', '2,122.60'), ('-25.3', '0.00;(0.00)', '(25.30)'),
|
||
('12.50', '0.##', '12.5'), ('12', '0.0#', '12.0'),
|
||
('8', '0000', '0008'), ('0.125', '0.0%', '12.5%')]:
|
||
self.assertEqual(formatted(value, 'n', code, False, warnings).strip(), expected)
|
||
self.assertEqual(formatted('1', 'n', 'yyyy/m/d', True, warnings), '1904/1/2')
|
||
self.assertFalse(warnings)
|
||
|
||
def test_sandbox_import(self):
|
||
script = """
|
||
import sys
|
||
def audit(event, args):
|
||
if event == 'open' and str(args[0]).endswith('mime.types'):
|
||
raise PermissionError('System MIME database is unavailable')
|
||
sys.addaudithook(audit)
|
||
import spreadsheet_layout
|
||
"""
|
||
result = subprocess.run([sys.executable, '-c', script], cwd=Path(__file__).resolve().parents[1],
|
||
capture_output=True, text=True)
|
||
self.assertEqual(result.returncode, 0, result.stderr)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
unittest.main()
|