This commit is contained in:
csj
2026-09-16 21:02:44 +08:00
parent 7fea2063aa
commit 45b6f693d6
11 changed files with 320 additions and 86 deletions
+70 -72
View File
@@ -54,7 +54,7 @@ def export_approved_ppt(records, destination, classified):
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)
return export_ppt(dict(matches=matches), destination, classified, preserve_order=True)
def export_ppt(state, destination, classified, preserve_order=False):
@@ -64,92 +64,90 @@ def export_ppt(state, destination, classified, preserve_order=False):
from pptx import Presentation
from pptx.util import Pt
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import PP_ALIGN
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
presentation = Presentation()
presentation.slide_width, presentation.slide_height = Pt(1280), Pt(720)
presentation.slide_width, presentation.slide_height = Pt(720), Pt(960)
def slide():
return presentation.slides.add_slide(presentation.slide_layouts[6])
page = presentation.slides.add_slide(presentation.slide_layouts[6])
page.background.fill.solid()
page.background.fill.fore_color.rgb = RGBColor(255, 255, 255)
return page
def picture(page, item, box):
image = image_for(item)
def picture(page, item, box, kind):
left, top, width, height = box
scale = min(width / image.width, height / image.height)
actual_width, actual_height = int(image.width * scale), int(image.height * scale)
content = io.BytesIO()
image.save(content, format='PNG')
content.seek(0)
page.shapes.add_picture(content, Pt(left + (width - actual_width) // 2), Pt(top + (height - actual_height) // 2), Pt(actual_width), Pt(actual_height))
with image_for(item) as image:
scale = min(width / image.width, height / image.height)
actual_width, actual_height = image.width * scale, image.height * scale
with io.BytesIO() as content:
image.save(content, format='PNG')
content.seek(0)
shape = page.shapes.add_picture(
content, Pt(left + (width - actual_width) / 2), Pt(top + (height - actual_height) / 2),
Pt(actual_width), Pt(actual_height)
)
shape.name = f'{kind}-{len(page.shapes)}'
def payment_page(payments):
page = slide()
height = 680
if len(payments) == 1:
picture(page, payments[0], (300, 20, 680, height))
else:
width = (1200 - 24 * (len(payments) - 1)) // len(payments)
for index, item in enumerate(payments):
picture(page, item, (40 + index * (width + 24), 20, width, height))
def label(page, box, value, size, title=False):
shape = page.shapes.add_shape(MSO_SHAPE.RECTANGLE, *(Pt(value) for value in box))
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(245, 248, 247) if title else RGBColor(255, 255, 255)
shape.line.color.rgb = RGBColor(216, 226, 222) if title else RGBColor(170, 188, 182)
shape.line.width = Pt(1 if title else 1.5)
shape.text = value
for paragraph in shape.text_frame.paragraphs:
def photo_hint(page, box):
shape = page.shapes.add_textbox(*(Pt(value) for value in box))
shape.name = 'manual-photo-area'
frame = shape.text_frame
frame.clear()
frame.vertical_anchor = MSO_ANCHOR.MIDDLE
frame.word_wrap = True
frame.text = '实物照片粘贴区\n请手动粘贴,可删除此提示'
for paragraph in frame.paragraphs:
paragraph.alignment = PP_ALIGN.CENTER
for run in paragraph.runs:
run.font.name = 'PingFang SC'
run.font.size = Pt(size)
run.font.color.rgb = RGBColor(39, 91, 78) if title else RGBColor(112, 132, 126)
run.font.size = Pt(14)
run.font.color.rgb = RGBColor(155, 155, 155)
def photos(count):
for offset in range(0, count, 4):
slots = min(4, count - offset)
def combined_page(invoice, payments, inline_photo=False):
page = slide()
picture(page, invoice, (48, 44, 624, 360), 'invoice')
for index, item in enumerate(payments):
picture(page, item, (48 + index * 324, 444, 300, 468), 'payment')
if inline_photo:
photo_hint(page, (372, 444, 300, 468))
def payment_pages(payments):
for offset in range(0, len(payments), 6):
page = slide()
label(page, (55, 28, 1170, 52), f'实物照片粘贴区(请手动粘贴) {offset + 1}-{min(count, offset + 4)} / {count}', 18, True)
for index in range(slots):
box = (250, 105, 780, 555) if slots == 1 else (55 + index * 610, 105, 560, 555) if slots == 2 else (55 + index % 2 * 610, 105 + index // 2 * 285, 560, 255)
label(page, box, f'实物照片 {offset + index + 1}\n请在此处粘贴', 16)
for index, item in enumerate(payments[offset:offset + 6]):
box = (48 + index % 3 * 216, 48 + index // 3 * 444, 192, 420)
picture(page, item, box, 'payment')
def requires_photo(match):
return match['category'].strip() not in ['交通', '住宿']
def simple_batch(batch):
if not batch:
return
for match in batch:
picture(slide(), match['invoices'][0], (40, 25, 1200, 670))
payment_page([match['payments'][0] for match in batch])
photos(sum(len(match['payments']) for match in batch if requires_photo(match)))
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 = []
matches = state['matches'] if preserve_order else sorted(
state['matches'],
key=lambda match: (
min((material_date(item, True) for item in match['invoices']), default='9999-12-31'),
match['category'] if classified else ''
)
)
if not matches:
raise ValueError('请先选择要导出的贴票材料')
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 = []
for item in sorted(match['invoices'], key=material_date):
picture(slide(), item, (40, 25, 1200, 670))
payments = sorted(match['payments'], key=material_date)
for index in range(0, len(payments), 4):
payment_page(payments[index:index + 4])
if requires_photo(match):
photos(len(payments))
else:
batch.append(match)
if len(batch) == 2:
simple_batch(batch)
batch = []
simple_batch(batch)
invoices = sorted(match.get('invoices', []), key=material_date)
payments = sorted(match.get('payments', []), key=material_date)
if not invoices and not payments:
raise ValueError('所选组没有发票或付款截图,无法导出')
if len(invoices) == 1 and len(payments) == 1:
combined_page(invoices[0], payments, inline_photo=True)
continue
paired_count = len(invoices) - len(invoices) % 2
for offset in range(0, paired_count, 2):
page = slide()
picture(page, invoices[offset], (48, 44, 624, 408), 'invoice')
picture(page, invoices[offset + 1], (48, 508, 624, 408), 'invoice')
if len(invoices) % 2:
combined_page(invoices[-1], payments[:2])
payments = payments[2:]
payment_pages(payments)
photo_hint(slide(), (48, 48, 624, 864))
presentation.save(destination)
return dict(pptLayout='portrait-receipts-v1', pptSlideCount=len(presentation.slides))
def export_travel(state, destination):