Files
hub/tools/make-office-templates.py
T

169 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Makes the ready-made Office templates in public/templates. Needs: pip install python-docx openpyxl python-pptx. Run: python3 tools/make-office-templates.py public/templates
import sys, os
from docx import Document
from docx.shared import Pt, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from pptx import Presentation
from pptx.util import Inches as PI, Pt as PP
from pptx.dml.color import RGBColor as PC
OUT = sys.argv[1]; os.makedirs(OUT, exist_ok=True)
BLUE = '3D6BF0'
# ---------------- Word
def base(doc, size=11):
st = doc.styles['Normal']; st.font.name = 'Calibri'; st.font.size = Pt(size)
for s in doc.sections: s.left_margin = s.right_margin = Inches(1); s.top_margin = s.bottom_margin = Inches(0.9)
def para(doc, text='', bold=False, size=None, color=None, align=None, after=6, italic=False):
p = doc.add_paragraph(); r = p.add_run(text); r.bold = bold; r.italic = italic
if size: r.font.size = Pt(size)
if color: r.font.color.rgb = RGBColor.from_string(color)
if align: p.alignment = align
p.paragraph_format.space_after = Pt(after); return p
def heading(doc, text, size=13):
p = para(doc, text.upper(), bold=True, size=size - 2, color=BLUE, after=3); p.paragraph_format.space_before = Pt(10)
return p
d = Document(); base(d, 11)
para(d, '[Your name]', bold=True, size=14, after=0); para(d, '[Street address]', after=0); para(d, '[City, State ZIP]', after=0); para(d, '[Phone] · [Email]', after=14)
para(d, '[Date]', after=14); para(d, '[Recipient name]', after=0); para(d, '[Title or company]', after=0); para(d, '[Street address]', after=0); para(d, '[City, State ZIP]', after=14)
para(d, 'Dear [Name],', after=10)
para(d, 'Start with the reason you are writing, in a sentence or two. Keep it friendly and direct.', after=10)
para(d, 'Use the middle paragraph for the details: what you need, what happened, or what you are offering. Short paragraphs are easier to read.', after=10)
para(d, 'Close by saying what happens next, and thank them for their time.', after=14)
para(d, 'Sincerely,', after=30); para(d, '[Your name]', after=0)
d.save(f'{OUT}/letter.docx')
d = Document(); base(d, 10.5)
para(d, '[Your Name]', bold=True, size=24, after=0); para(d, '[City, State] · [Phone] · [Email]', color='666666', after=8)
heading(d, 'Summary'); para(d, 'Two or three sentences about who you are, what you do well, and what you are looking for.')
heading(d, 'Experience')
for i in range(2):
para(d, '[Job title], [Employer]', bold=True, after=0); para(d, '[Month Year] – [Month Year]', italic=True, color='666666', after=3)
for t in ['What you did and the result it had (use numbers when you can)', 'Something you improved, built or led', 'A skill you used every day']:
p = d.add_paragraph(t, style='List Bullet'); p.paragraph_format.space_after = Pt(1)
para(d, '', after=4)
heading(d, 'Education'); para(d, '[School], [Degree or program]', bold=True, after=0); para(d, '[Year]', italic=True, color='666666')
heading(d, 'Skills'); para(d, '[Skill] · [Skill] · [Skill] · [Skill] · [Skill]')
d.save(f'{OUT}/resume.docx')
d = Document(); base(d, 11)
para(d, 'Meeting notes', bold=True, size=22, color=BLUE, after=2); para(d, '[Meeting name]', size=13, after=10)
t = d.add_table(rows=4, cols=2); t.style = 'Light Grid Accent 1'
for i, (a, b) in enumerate([('Date', '[Date and time]'), ('Where', '[Place or link]'), ('Who was there', '[Names]'), ('Taking notes', '[Name]')]): t.cell(i, 0).text = a; t.cell(i, 1).text = b
para(d, '', after=4); heading(d, 'Agenda')
for x in ['[First topic]', '[Second topic]', '[Third topic]']: d.add_paragraph(x, style='List Number')
heading(d, 'Notes'); para(d, 'What was said and what was decided.', italic=True, color='666666'); para(d, '', after=30)
heading(d, 'Action items')
t = d.add_table(rows=4, cols=3); t.style = 'Light Grid Accent 1'
for i, h in enumerate(['What needs doing', 'Who', 'By when']): t.cell(0, i).text = h
d.save(f'{OUT}/meeting-notes.docx')
d = Document(); base(d, 11)
para(d, '[Family Name] News', bold=True, size=28, color=BLUE, align=WD_ALIGN_PARAGRAPH.CENTER, after=0); para(d, '[Month Year]', align=WD_ALIGN_PARAGRAPH.CENTER, color='666666', after=14)
heading(d, 'The big news'); para(d, 'Share the headline: a trip, a birthday, a new job, a new pet. A few friendly sentences is plenty.')
heading(d, 'What everyone has been up to')
for n in ['[Name]', '[Name]', '[Name]']: para(d, n, bold=True, after=0); para(d, 'A line or two about school, work, hobbies or something funny.', after=8)
heading(d, 'Coming up'); t = d.add_table(rows=4, cols=2); t.style = 'Light List Accent 1'
t.cell(0, 0).text = 'Date'; t.cell(0, 1).text = 'What'
heading(d, 'A favorite moment'); para(d, 'Add a photo here (Insert → Image) and a sentence about it.', italic=True, color='666666')
d.save(f'{OUT}/family-newsletter.docx')
d = Document(); base(d, 11)
para(d, '[Recipe name]', bold=True, size=24, color=BLUE, after=2); para(d, 'Serves [4] · Prep [15 min] · Cook [30 min]', color='666666', after=10)
heading(d, 'Ingredients')
for x in ['[2 cups flour]', '[1 tsp salt]', '[1 cup milk]', '[2 eggs]']: d.add_paragraph(x, style='List Bullet')
heading(d, 'Steps')
for x in ['[First step]', '[Second step]', '[Third step]']: d.add_paragraph(x, style='List Number')
heading(d, 'Notes'); para(d, 'Where it came from, what to change next time, what goes well with it.', italic=True, color='666666')
d.save(f'{OUT}/recipe-card.docx')
# ---------------- Excel
thin = Side(style='thin', color='D0D5DD'); BOX = Border(left=thin, right=thin, top=thin, bottom=thin)
HEAD = PatternFill('solid', fgColor=BLUE); SOFT = PatternFill('solid', fgColor='EEF2FF')
def hdr(ws, row, values, widths=None):
for i, v in enumerate(values, 1):
c = ws.cell(row=row, column=i, value=v); c.font = Font(bold=True, color='FFFFFF'); c.fill = HEAD; c.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True); c.border = BOX
if widths:
for i, w in enumerate(widths, 1): ws.column_dimensions[get_column_letter(i)].width = w
def title(ws, text, sub=None):
ws['A1'] = text; ws['A1'].font = Font(bold=True, size=18, color=BLUE)
if sub: ws['A2'] = sub; ws['A2'].font = Font(italic=True, color='666666')
wb = Workbook(); ws = wb.active; ws.title = 'Budget'; title(ws, 'Monthly budget', 'Fill in the planned and actual amounts. The last column does the math.')
ws['A4'] = 'Income'; ws['A4'].font = Font(bold=True, size=12)
hdr(ws, 5, ['Item', 'Planned', 'Actual', 'Difference'], [30, 14, 14, 14])
inc = ['Paycheck', 'Second income', 'Other']
for i, n in enumerate(inc):
r = 6 + i; ws.cell(r, 1, n); ws.cell(r, 2, 0); ws.cell(r, 3, 0); ws.cell(r, 4, f'=C{r}-B{r}')
r = 6 + len(inc); ws.cell(r, 1, 'Total income').font = Font(bold=True); ws.cell(r, 2, f'=SUM(B6:B{r-1})'); ws.cell(r, 3, f'=SUM(C6:C{r-1})'); ws.cell(r, 4, f'=C{r}-B{r}'); inc_total = r
for c in range(1, 5): ws.cell(r, c).fill = SOFT; ws.cell(r, c).font = Font(bold=True)
start = r + 2; ws.cell(start, 1, 'Expenses').font = Font(bold=True, size=12); hdr(ws, start + 1, ['Item', 'Planned', 'Actual', 'Difference'])
exp = ['Rent or mortgage', 'Utilities', 'Groceries', 'Transportation', 'Insurance', 'Phone and internet', 'Eating out', 'Kids and school', 'Health', 'Giving', 'Savings', 'Fun', 'Other']
first = start + 2
for i, n in enumerate(exp):
r = first + i; ws.cell(r, 1, n); ws.cell(r, 2, 0); ws.cell(r, 3, 0); ws.cell(r, 4, f'=B{r}-C{r}')
last = first + len(exp) - 1; r = last + 1; ws.cell(r, 1, 'Total expenses'); ws.cell(r, 2, f'=SUM(B{first}:B{last})'); ws.cell(r, 3, f'=SUM(C{first}:C{last})'); ws.cell(r, 4, f'=B{r}-C{r}'); exp_total = r
for c in range(1, 5): ws.cell(r, c).fill = SOFT; ws.cell(r, c).font = Font(bold=True)
r += 2; ws.cell(r, 1, 'Left over (income − expenses)').font = Font(bold=True, size=12); ws.cell(r, 2, f'=B{inc_total}-B{exp_total}'); ws.cell(r, 3, f'=C{inc_total}-C{exp_total}')
for row in ws.iter_rows(min_row=6, max_row=r):
for c in row[1:4]: c.number_format = '$#,##0.00;[Red]-$#,##0.00'
for c in row[:4]:
if c.row not in (start, start + 1, r) and c.value is not None: c.border = BOX
ws.freeze_panes = 'A6'; ws.sheet_view.showGridLines = False; wb.save(f'{OUT}/monthly-budget.xlsx')
wb = Workbook(); ws = wb.active; ws.title = 'Chores'; title(ws, 'Weekly chore chart', 'Put a name in each box, or a check mark when it is done.')
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; hdr(ws, 4, ['Chore'] + days + ['Points'], [26] + [10] * 7 + [10])
chores = ['Make bed', 'Set the table', 'Clear the table', 'Dishes / dishwasher', 'Feed the pets', 'Take out trash', 'Tidy bedroom', 'Laundry', 'Vacuum', 'Water plants']
for i, n in enumerate(chores):
r = 5 + i; ws.cell(r, 1, n)
for c in range(1, 10): ws.cell(r, c).border = BOX; ws.cell(r, c).alignment = Alignment(horizontal='center' if c > 1 else 'left')
ws.cell(r, 9, f'=COUNTA(B{r}:H{r})')
r = 5 + len(chores); ws.cell(r, 1, 'Total').font = Font(bold=True); ws.cell(r, 9, f'=SUM(I5:I{r-1})'); ws.cell(r, 9).font = Font(bold=True)
for c in range(1, 10): ws.cell(r, c).fill = SOFT; ws.cell(r, c).border = BOX
ws.freeze_panes = 'B5'; ws.sheet_view.showGridLines = False; wb.save(f'{OUT}/chore-chart.xlsx')
wb = Workbook(); ws = wb.active; ws.title = 'Meals'; title(ws, 'Weekly meal planner', 'Plan the week, then build the shopping list from it.')
hdr(ws, 4, ['', 'Breakfast', 'Lunch', 'Dinner', 'Snacks'], [14, 24, 24, 28, 22])
for i, d_ in enumerate(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']):
r = 5 + i; ws.cell(r, 1, d_).font = Font(bold=True); ws.cell(r, 1).fill = SOFT
for c in range(1, 6): ws.cell(r, c).border = BOX; ws.cell(r, c).alignment = Alignment(vertical='top', wrap_text=True)
ws.row_dimensions[r].height = 34
ws.cell(13, 1, 'Shopping list').font = Font(bold=True, size=12); ws.cell(14, 1, 'Write what you need to buy under here.').font = Font(italic=True, color='666666')
ws.sheet_view.showGridLines = False; wb.save(f'{OUT}/meal-planner.xlsx')
wb = Workbook(); ws = wb.active; ws.title = 'Expenses'; title(ws, 'Expense tracker', 'One line per purchase. The totals below add themselves up.')
hdr(ws, 4, ['Date', 'What', 'Category', 'Paid with', 'Amount'], [14, 32, 18, 16, 14])
for r in range(5, 45):
for c in range(1, 6): ws.cell(r, c).border = BOX
ws.cell(r, 1).number_format = 'mmm d, yyyy'; ws.cell(r, 5).number_format = '$#,##0.00'
ws['D46'] = 'Total'; ws['D46'].font = Font(bold=True); ws['E46'] = '=SUM(E5:E44)'; ws['E46'].number_format = '$#,##0.00'; ws['E46'].font = Font(bold=True)
ws['G4'] = 'By category'; ws['G4'].font = Font(bold=True, color='FFFFFF'); ws['G4'].fill = HEAD; ws['H4'] = 'Total'; ws['H4'].font = Font(bold=True, color='FFFFFF'); ws['H4'].fill = HEAD; ws.column_dimensions['G'].width = 18; ws.column_dimensions['H'].width = 14
for i, n in enumerate(['Groceries', 'Gas', 'Eating out', 'Home', 'Kids', 'Health', 'Fun', 'Other']):
r = 5 + i; ws.cell(r, 7, n); ws.cell(r, 8, f'=SUMIF($C$5:$C$44,G{r},$E$5:$E$44)'); ws.cell(r, 8).number_format = '$#,##0.00'
ws.freeze_panes = 'A5'; ws.sheet_view.showGridLines = False; wb.save(f'{OUT}/expense-tracker.xlsx')
# ---------------- PowerPoint
prs = Presentation(); prs.slide_width = PI(13.333); prs.slide_height = PI(7.5)
def slide(title_text, lines=None, layout=1, sub=None):
s = prs.slides.add_slide(prs.slide_layouts[layout]); s.shapes.title.text = title_text
for p in s.shapes.title.text_frame.paragraphs:
for r in p.runs: r.font.color.rgb = PC.from_string(BLUE); r.font.bold = True; r.font.name = 'Calibri'
if layout == 0 and sub is not None: s.placeholders[1].text = sub
elif lines is not None:
tf = s.placeholders[1].text_frame; tf.clear()
for i, t in enumerate(lines):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph(); p.text = t; p.font.size = PP(26); p.font.name = 'Calibri'
return s
slide('[Presentation title]', layout=0, sub='[Your name] · [Date]')
slide('Agenda', ['[First topic]', '[Second topic]', '[Third topic]', 'Questions'])
slide('[A key point]', ['Say it in one clear sentence', 'Back it up with two or three short points', 'Add a picture or a chart on the right (Insert → Image)'])
slide('[Another key point]', ['Keep to a few words per line', 'Let the pictures do the talking', 'Practice saying the rest out loud'])
slide('Summary', ['What we covered', 'What happens next', 'Who does what'])
slide('Thank you', layout=0, sub='[Contact or question prompt]')
prs.save(f'{OUT}/simple-presentation.pptx')
print(sorted(os.listdir(OUT)))