Skip to content

Commit c880923

Browse files
mborchukclaude
andauthored
New modules (#40)
* new modules * fix: resolve CodeQL findings on PR #40 - Drop unused json import and unused locals (paid/overdue/tpl) in the invoice-email and recurring-invoices tests. - Remove duplicated ModuleManager import in conftest.py (merge artifact). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 87d3c89 commit c880923

18 files changed

Lines changed: 1824 additions & 2 deletions

modules/invoice_email/__init__.py

Whitespace-only changes.

modules/invoice_email/index.py

Lines changed: 377 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{# Invoice view panel — send form + history. Rendered by get_invoice_view_panels. #}
2+
<div style="margin-top: 20px; border: 1px solid var(--color-border); border-radius: 6px; padding: 15px;">
3+
<h3 style="margin-top: 0;">📧 Email this invoice</h3>
4+
5+
{% if not configured %}
6+
<p style="font-size: 13px; color: var(--color-text-muted);">
7+
SMTP is not configured yet — set it up in
8+
<a href="{{ url_for('settings') }}#invoice_email">Settings → Email</a>.
9+
</p>
10+
{% else %}
11+
<form method="POST" action="{{ url_for('invoice_email.email_send', invoice_id=invoice.id) }}">
12+
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 12px;">
13+
<div class="form-group">
14+
<label>Recipient</label>
15+
<input type="email" name="recipient" required value="{{ recipient }}"
16+
placeholder="customer@example.com">
17+
</div>
18+
<div class="form-group">
19+
<label>Subject</label>
20+
<input type="text" name="subject" value="Invoice {invoice_number}">
21+
</div>
22+
</div>
23+
<div class="form-group">
24+
<label>Body <small style="color: var(--color-text-muted);">placeholders: {invoice_number} {client_name} {amount} {due_date}</small></label>
25+
<textarea name="body" rows="4">Dear {client_name},
26+
27+
Please find attached invoice {invoice_number} for {amount}, due {due_date}.
28+
29+
Best regards</textarea>
30+
</div>
31+
<button type="submit" class="btn btn-success">Send email</button>
32+
</form>
33+
{% endif %}
34+
35+
{% if history %}
36+
<h4 style="margin-bottom: 6px;">Send history</h4>
37+
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
38+
<thead>
39+
<tr>
40+
<th style="text-align:left; padding: 4px; border-bottom: 1px solid var(--color-border);">When</th>
41+
<th style="text-align:left; padding: 4px; border-bottom: 1px solid var(--color-border);">To</th>
42+
<th style="text-align:left; padding: 4px; border-bottom: 1px solid var(--color-border);">Kind</th>
43+
<th style="text-align:left; padding: 4px; border-bottom: 1px solid var(--color-border);">Status</th>
44+
</tr>
45+
</thead>
46+
<tbody>
47+
{% for log in history %}
48+
<tr>
49+
<td style="padding: 4px;">{{ log.created_at.strftime('%d/%m/%Y %H:%M') }}</td>
50+
<td style="padding: 4px;">{{ log.recipient }}</td>
51+
<td style="padding: 4px;">{{ log.kind }}</td>
52+
<td style="padding: 4px;">
53+
{% if log.status == 'sent' %}
54+
<span style="color: var(--color-success);">sent</span>
55+
{% else %}
56+
<span style="color: var(--color-danger, red);" title="{{ log.error }}">failed</span>
57+
{% endif %}
58+
</td>
59+
</tr>
60+
{% endfor %}
61+
</tbody>
62+
</table>
63+
{% endif %}
64+
</div>

modules/recurring_invoices/__init__.py

Whitespace-only changes.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Cadence math for recurring invoices — pure functions, no Flask, no DB.
4+
5+
CAVEMAN NOTE: keep dumb so tests feed dates and check dates. Monthly and
6+
quarterly only (per PM scope, closed-ended picker).
7+
"""
8+
9+
from datetime import date
10+
11+
CADENCES = ('monthly', 'quarterly')
12+
13+
# Safety cap: never materialize more than this many missed periods in one run
14+
# (protects against a template with next_run_date years in the past).
15+
MAX_CATCHUP_PERIODS = 12
16+
17+
18+
def add_cadence(d, cadence):
19+
"""Return `d` advanced by one cadence period, clamping the day-of-month.
20+
21+
Jan 31 + monthly -> Feb 28/29 (clamped), like most billing systems do.
22+
"""
23+
if cadence not in CADENCES:
24+
raise ValueError(f'Unknown cadence: {cadence}')
25+
months = 1 if cadence == 'monthly' else 3
26+
month_index = d.month - 1 + months
27+
year = d.year + month_index // 12
28+
month = month_index % 12 + 1
29+
day = min(d.day, _days_in_month(year, month))
30+
return date(year, month, day)
31+
32+
33+
def _days_in_month(year, month):
34+
if month == 12:
35+
nxt = date(year + 1, 1, 1)
36+
else:
37+
nxt = date(year, month + 1, 1)
38+
return (nxt - date(year, month, 1)).days
39+
40+
41+
def due_dates(next_run_date, today, cadence):
42+
"""All period dates due on or before `today`, capped at MAX_CATCHUP_PERIODS.
43+
44+
Returns (list_of_due_dates, new_next_run_date). Idempotent by construction:
45+
caller persists new_next_run_date, so a rerun the same day yields [].
46+
"""
47+
due = []
48+
cursor = next_run_date
49+
while cursor <= today and len(due) < MAX_CATCHUP_PERIODS:
50+
due.append(cursor)
51+
cursor = add_cadence(cursor, cadence)
52+
return due, cursor

0 commit comments

Comments
 (0)