Initial commit: macOS receipt workspace

This commit is contained in:
于晓婷
2026-09-11 12:58:09 +08:00
commit bbacd1d6f3
40 changed files with 2790 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
import argparse
import json
import subprocess
import sys
import shutil
from pathlib import Path
from PIL import Image
def invoke(command, request):
process = subprocess.run(command, input=json.dumps(request, ensure_ascii=False) + '\n', capture_output=True, text=True, timeout=180)
events = [json.loads(line) for line in process.stdout.splitlines() if line.strip()]
errors = [event for event in events if event['event'] == 'error']
if process.returncode or errors:
raise RuntimeError(str(errors) + '\n' + process.stderr)
results = [event['result'] for event in events if event['event'] == 'result']
if len(results) != 1:
raise AssertionError('处理引擎没有返回唯一结果')
return results[0]
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--binary')
parser.add_argument('--output', required=True)
arguments = parser.parse_args()
root = Path(__file__).resolve().parents[3]
output = Path(arguments.output).resolve()
output.mkdir(parents=True, exist_ok=True)
engine = Path(__file__).resolve().parents[1] / 'engine.py'
command = [arguments.binary] if arguments.binary else [sys.executable, str(engine)]
state = invoke(command, dict(operation='scan', path=str(root / 'fapiao/demo-materials'), workspacePath=str(output / 'materials'), folderName='演示报账材料'))
assert len(state['invoices']) == 2
assert len(state['payments']) == 2
assert len(state['photos']) == 1
assert len(state['matches']) == 2
assert {match['category'] for match in state['matches']} == {'餐饮', '住宿'}
assert state['directoryPaymentTotal'] == 596.5
assert sum(match['expenseAmount'] for match in state['matches']) == 596.5
assert not state['warnings']
(output / 'state.json').write_text(json.dumps(state, ensure_ascii=False, indent=2))
for operation, extension in [('ppt', 'pptx'), ('expense', 'xlsx')]:
destination = output / ('演示报账材料.' + extension)
invoke(command, dict(operation=operation, state=state, destination=str(destination), classified=True, purposes={match['id']: match['category'] + '费用' for match in state['matches']}, signatures=['经办人', '财务']))
assert destination.stat().st_size > 1000
pdf_input = output / 'pdf-input'
(pdf_input / '发票').mkdir(parents=True, exist_ok=True)
(pdf_input / '付款截图').mkdir(parents=True, exist_ok=True)
with Image.open(root / 'fapiao/demo-materials/发票/住宿_差旅发票_A001.png') as original:
original.convert('RGB').save(pdf_input / '发票/两页发票.pdf', save_all=True, append_images=[Image.new('RGB', (800, 500), 'white')])
(pdf_input / '发票/损坏文件.pdf').write_bytes(b'not a pdf')
shutil.copyfile(root / 'fapiao/demo-materials/付款截图/酒店支付记录_B009.png', pdf_input / '付款截图/付款.png')
pdf_state = invoke(command, dict(operation='scan', path=str(pdf_input), workspacePath=str(output / 'pdf-materials')))
assert len(pdf_state['invoices']) == 2
assert len(pdf_state['matches']) == 1
assert pdf_state['matches'][0]['expenseAmount'] == 468
assert sum(item['ocr']['status'] == 'failed' for item in pdf_state['invoices']) == 1
invoke(command, dict(operation='ppt', state=pdf_state, destination=str(output / 'PDF首页贴票.pptx')))
print('完整链路通过:5 份材料、2 组自动核对、总额 596.50;PPT、个人报销单、PDF 首页与损坏文件降级均通过。')
if __name__ == '__main__':
main()