This commit is contained in:
csj
2026-09-13 15:32:22 +08:00
parent da6cb24249
commit 4b49b09230
4 changed files with 111 additions and 101 deletions
+49 -84
View File
@@ -17,7 +17,50 @@ def image_for(item):
return Image.open(item.get('previewPath') or item['path']).convert('RGB')
def export_ppt(state, destination, classified):
def export_approved_ppt(records, destination, classified):
import contextlib
import tempfile
import pypdfium2 as pdfium
if not records or len(records) > 100:
raise ValueError('每批请选择 1 至 100 笔已批准申请')
categories = {'交通差旅': '交通', '住宿费用': '住宿'}
with tempfile.TemporaryDirectory(prefix='approved-ppt-pages-', dir=Path(destination).parent) as directory:
matches = []
for record in records:
match = dict(invoices=[], payments=[], category=categories.get(record['type'], record['type']))
match['scope'] = (record.get('groupId', record['team']), record.get('userId', record['name']))
for material in record['materials']:
if material['kind'] not in ('invoice', 'payment'):
continue
path = Path(material['path'])
try:
if path.suffix.lower() == '.pdf':
with contextlib.closing(pdfium.PdfDocument(str(path))) as document:
if not len(document):
raise ValueError('PDF 没有可预览页面')
with contextlib.closing(document[0]) as page:
with contextlib.closing(page.render(scale=140 / 72)) as bitmap:
preview = Path(directory) / f'{len(matches)}-{len(match["invoices"])}-{len(match["payments"])}.png'
bitmap.to_pil().convert('RGB').save(preview)
path = preview
else:
with Image.open(path) as image:
image.verify()
except Exception as error:
raise ValueError(f"申请 #{record['id']} 的“{material['name']}”无法导出:{error}") from error
item = dict(path=str(path), ocr=dict(rawText='', dates=[record['date']]))
match['invoices' if material['kind'] == 'invoice' else 'payments'].append(item)
if not match['invoices'] and not match['payments']:
raise ValueError(f"申请 #{record['id']} 没有发票或付款截图,无法按本地贴票格式导出,请取消选择该申请")
matches.append(match)
export_ppt(dict(matches=matches), destination, classified, preserve_order=True)
def export_ppt(state, destination, classified, preserve_order=False):
if 'approvedRecords' in state:
return export_approved_ppt(state['approvedRecords'], destination, classified)
from pptx import Presentation
from pptx.util import Pt
from pptx.dml.color import RGBColor
@@ -42,7 +85,7 @@ def export_ppt(state, destination, classified):
def payment_page(payments):
page = slide()
height = 660 if 'approvedRecords' in state else 680
height = 680
if len(payments) == 1:
picture(page, payments[0], (300, 20, 680, height))
else:
@@ -84,90 +127,12 @@ def export_ppt(state, destination, classified):
payment_page([match['payments'][0] for match in batch])
photos(sum(len(match['payments']) for match in batch if requires_photo(match)))
if 'approvedRecords' in state:
import contextlib
import tempfile
import pypdfium2 as pdfium
from pptx.enum.text import MSO_AUTO_SIZE
records = state['approvedRecords']
if not records or len(records) > 100:
raise ValueError('每批请选择 1 至 100 笔已批准申请')
with tempfile.TemporaryDirectory(prefix='approved-ppt-pages-', dir=Path(destination).parent) as directory:
page_count = 0
for record in records:
summary = slide()
label(summary, (55, 28, 1170, 64), '已批准报销 · #' + record['id'], 26, True)
details = [record['projectName'] + ' / ' + record['team'],
record['name'] + ' · ' + record['type'],
'提交日期:' + record['date'] + ' 报销金额:¥' + record['amountText']]
for index, detail in enumerate(details):
label(summary, (55, 120 + index * 70, 1170, 60), detail, 22)
note = record.get('note', '').strip()
for offset in range(0, len(note), 240):
note_page = summary if offset == 0 else slide()
label(note_page, (55, 360 if offset == 0 else 100, 1170, 300),
('备注:' if offset == 0 else '备注(续):') + note[offset:offset + 240], 18)
groups = {}
kind_names = {}
for material in record['materials']:
kind_names[material['kind']] = material.get('kindName', '其他材料')
path = Path(material['path'])
images = []
try:
if path.suffix.lower() == '.pdf':
with contextlib.closing(pdfium.PdfDocument(str(path))) as document:
if not 0 < len(document) <= 100:
raise ValueError('单份 PDF 需为 1–100 页,请拆分后导出')
for page_index in range(len(document)):
with contextlib.closing(document[page_index]) as pdf_page:
scale = min(140 / 72, 2600 / max(pdf_page.get_size()))
with contextlib.closing(pdf_page.render(scale=scale)) as bitmap:
preview = Path(directory) / f'{page_count}.png'
bitmap.to_pil().convert('RGB').save(preview)
images.append({'path': str(preview)})
page_count += 1
else:
with Image.open(path) as image:
image.verify()
images.append(material)
page_count += 1
except Exception as error:
raise ValueError(f"申请 #{record['id']} 的“{material['name']}”无法导出:{error}") from error
if page_count > 1000:
raise ValueError('材料展开后超过 1,000 页,请减少选择并分批导出')
groups.setdefault(material['kind'], []).extend(images)
if not groups:
label(summary, (55, 660, 1170, 36), '该申请没有上传附件', 16)
for item in groups.pop('invoice', []):
picture(slide(), item, (40, 25, 1200, 670))
payments = groups.pop('payment', [])
for offset in range(0, len(payments), 4):
payment_page(payments[offset:offset + 4])
for kind, materials in sorted(groups.items()):
for offset in range(0, len(materials), 4):
page = slide()
title = '实拍照片' if kind in ('receipt', 'photo') else kind_names[kind]
label(page, (55, 20, 1170, 50), title, 20, True)
batch = materials[offset:offset + 4]
for index, item in enumerate(batch):
box = (250, 90, 780, 570) if len(batch) == 1 else (55 + index % 2 * 610, 90 + index // 2 * 290, 560, 270)
picture(page, item, box)
start = list(presentation.slides).index(summary)
for page in list(presentation.slides)[start + 1:]:
label(page, (40, 696, 1200, 22),
f"{record['projectName']} / {record['team']} · {record['name']} · {record['date']} · #{record['id']}", 10, True)
for page in presentation.slides:
for shape in page.shapes:
if shape.has_text_frame:
shape.text_frame.word_wrap = True
shape.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
presentation.save(destination)
return
matches = sorted(state['matches'], key=lambda match: (min(material_date(item, True) for item in match['invoices']), match['category'] if classified else ''))
matches = state['matches'] if preserve_order else sorted(state['matches'], key=lambda match: (min(material_date(item, True) for item in match['invoices']), match['category'] if classified else ''))
batch = []
for match in matches:
if batch and batch[-1].get('scope') != match.get('scope'):
simple_batch(batch)
batch = []
if len(match['invoices']) != 1 or len(match['payments']) != 1:
simple_batch(batch)
batch = []