feat(invoices): quarterly, half-yearly and yearly recurring invoice schedules (#1438)
* fix(mcp): offer the link tool in the uncategorized-transactions VAT blocker The gnubok_vat_close_check blocker hint only named categorize/auto-match, both of which create new bookkeeping. For a transaction whose affarshandelse is already booked on an existing verifikat, following the hint would double-book, so agents dead-ended the case into "contact support" (2026-08-06 support mail from Orto Engineering). The hint now also names gnubok_link_transaction_to_journal_entry, is extracted as an exported constant pinned by a test, and the tool joins the categorize_month recommended loadout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): quarterly, half-yearly and yearly recurring schedules User request: recurring invoice schedules only supported monthly cadence. Adds interval_months (SMALLINT 1-12, default 1) to recurring_invoice_schedules; the UI offers manadsvis/kvartalsvis/ halvarsvis/arsvis presets while API and MCP accept any 1-12. The cron advances next_run_date by whole intervals from the due date, and the new rollNextRunDateForward() helper rolls missed or edited interval schedules on their own month grid so a quarterly Jan/Apr/Jul/Oct schedule missed in an outage rolls Jan 15 to Apr 15, never Feb 15. Monthly (interval 1) keeps its existing today-anchored recompute semantics unchanged. Changing the interval alone never touches next_run_date: the new cadence applies from the next run, so an edit can never pull a send earlier. Existing rows default to 1 and behave byte-identically. The MCP slice of this feature (interval_months on the three recurring-schedule tools in server.ts) was committed in d2600907f alongside the VAT-blocker hint fix by a parallel session sharing this worktree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): address PR #1438 review findings CodeRabbit round 1, all three findings: - MCP descriptions now state the full accepted interval range (any integer 1-12) instead of enumerating only the 1/3/6/12 presets, and qualify that changing ONLY interval_months leaves next_run_date untouched. - assertValidCadence rejects fractional day_of_month. - rollNextRunDateForward rejects calendar-invalid anchors that pass the shape regex (2026-13-05, 2026-02-31), with regression tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d41ef2a909
commit
a0ca692fed
@@ -112,6 +112,7 @@ function NewRecurringScheduleForm({
|
||||
customer_id: z.string().uuid(t('validation_customer_required')),
|
||||
name: z.string().min(1, t('validation_name_required')),
|
||||
day_of_month: z.number().int().min(1).max(31),
|
||||
interval_months: z.number().int().min(1).max(12),
|
||||
send_hour: z.number().int().min(0).max(23),
|
||||
payment_terms_days: z.number().int().min(0).max(90),
|
||||
currency: z.enum(['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK']),
|
||||
@@ -139,6 +140,7 @@ function NewRecurringScheduleForm({
|
||||
customer_id: schedule.customer_id,
|
||||
name: schedule.name,
|
||||
day_of_month: schedule.day_of_month,
|
||||
interval_months: schedule.interval_months ?? 1,
|
||||
send_hour: schedule.send_hour ?? 8,
|
||||
payment_terms_days: schedule.payment_terms_days,
|
||||
currency: schedule.currency,
|
||||
@@ -163,6 +165,7 @@ function NewRecurringScheduleForm({
|
||||
customer_id: '',
|
||||
name: '',
|
||||
day_of_month: 15,
|
||||
interval_months: 1,
|
||||
send_hour: 8,
|
||||
payment_terms_days: 30,
|
||||
currency: 'SEK',
|
||||
@@ -296,6 +299,37 @@ function NewRecurringScheduleForm({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="interval_months">{t('interval_label')}</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="interval_months"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={String(field.value)}
|
||||
onValueChange={(v) => field.onChange(Number(v))}
|
||||
>
|
||||
<SelectTrigger id="interval_months">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">{t('interval_monthly')}</SelectItem>
|
||||
<SelectItem value="3">{t('interval_quarterly')}</SelectItem>
|
||||
<SelectItem value="6">{t('interval_semiannual')}</SelectItem>
|
||||
<SelectItem value="12">{t('interval_yearly')}</SelectItem>
|
||||
{/* A non-preset interval (set via API/MCP, e.g. every 2
|
||||
months) must stay selectable so an edit doesn't
|
||||
silently coerce it to a preset. */}
|
||||
{![1, 3, 6, 12].includes(field.value) && (
|
||||
<SelectItem value={String(field.value)}>
|
||||
{t('interval_every_n', { n: field.value })}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="day_of_month">{t('day_label')}</Label>
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user