1
This commit is contained in:
+70
-72
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user