@kirjapro/mcp-server 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/index.js +158 -37
- package/package.json +1 -1
- package/server.json +1 -1
- package/tool-fingerprints.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0] - Unreleased
|
|
4
|
+
|
|
5
|
+
Requires the database migrations `20261001204555`–`20261001204559` (ALV report
|
|
6
|
+
audit batch 1). Version 0.9.1 keeps working against that database: its sales
|
|
7
|
+
and reverse-charge vouchers of a VAT-registered company are refused by the
|
|
8
|
+
database with the same guidance, and its settlement default date stays the
|
|
9
|
+
old one.
|
|
10
|
+
|
|
11
|
+
- `kirjapro_book_sale`, the sale templates (`cash_sale_*`, `bank_sale_*`,
|
|
12
|
+
`invoice_sale_25`) of `kirjapro_create_from_template(_batch)` and
|
|
13
|
+
`kirjapro_book_purchase` with `reverseCharge` are refused for a
|
|
14
|
+
VAT-registered (ALV) company before any write: a journal voucher never
|
|
15
|
+
reaches the ALV report. Sales are created with `kirjapro_create_invoice` and
|
|
16
|
+
delivered with `kirjapro_send_invoice`; reverse-charge purchases are booked
|
|
17
|
+
as received invoices in the web app for now. `kirjapro_list_templates` no
|
|
18
|
+
longer lists the sale templates for such a company. Other companies are
|
|
19
|
+
unchanged.
|
|
20
|
+
- `kirjapro_create_transaction(s_batch)`: the database refuses lines on sales
|
|
21
|
+
accounts 5000-5499 and a net 4210 credit for a VAT-registered company; the
|
|
22
|
+
descriptions say so.
|
|
23
|
+
- `kirjapro_book_vat_settlement` settles the company's VAT period (month,
|
|
24
|
+
quarter or year from `companies.vat_period`) containing `month`. The default
|
|
25
|
+
date is the statutory due date (12th of the second month after the period;
|
|
26
|
+
annual period: end of February), and the fiscal year defaults to the open
|
|
27
|
+
year containing that date, so a December settlement is booked in the next
|
|
28
|
+
year. The database now excludes earlier settlements, closing entries and the
|
|
29
|
+
previous-year import, refuses a settled period, a period whose ledger does
|
|
30
|
+
not match the ALV report and negative balances, and books only non-zero
|
|
31
|
+
lines. The result and the dry run name the period. A company that is not
|
|
32
|
+
VAT-registered is refused.
|
|
33
|
+
- `kirjapro_get_alv_summary` returns `reconciliation`: the 4210/2320 ledger
|
|
34
|
+
against the report (status `match`, `explained` or `mismatch`, the
|
|
35
|
+
differences and the explaining vouchers, invoices, fact mismatches and
|
|
36
|
+
manual rows), and sets `warning` on a mismatch.
|
|
37
|
+
- The server instructions (core and full surface) say that a VAT-registered
|
|
38
|
+
company books sales as invoices, that `kirjapro_create_transaction` cannot
|
|
39
|
+
replace `kirjapro_book_sale` for it, and that its sale templates are hidden.
|
|
40
|
+
- Review the fingerprints of `kirjapro_book_sale`, `kirjapro_book_purchase`,
|
|
41
|
+
`kirjapro_book_vat_settlement`, `kirjapro_create_from_template`,
|
|
42
|
+
`kirjapro_create_from_template_batch`, `kirjapro_list_templates`,
|
|
43
|
+
`kirjapro_create_transaction`, `kirjapro_create_transactions_batch` and
|
|
44
|
+
`kirjapro_get_alv_summary` for their changed descriptions and schemas.
|
|
45
|
+
|
|
3
46
|
## [0.9.1] - 2026-09-29
|
|
4
47
|
|
|
5
48
|
Requires the database migration `20260929120200` (bank reconciliation
|
package/dist/index.js
CHANGED
|
@@ -4148,13 +4148,14 @@ var require_fast_uri = __commonJS({
|
|
|
4148
4148
|
if (!malformedIPLiteral) {
|
|
4149
4149
|
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
|
|
4150
4150
|
}
|
|
4151
|
-
if (
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
parsed.host = reescapeHostDelimiters(host, isIP);
|
|
4156
|
-
}
|
|
4151
|
+
if (uri.indexOf("%") !== -1 && parsed.host !== void 0 && !malformedIPLiteral) {
|
|
4152
|
+
let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
|
|
4153
|
+
if (!isIP) {
|
|
4154
|
+
host = normalizePercentEncoding(host.toLowerCase());
|
|
4157
4155
|
}
|
|
4156
|
+
parsed.host = reescapeHostDelimiters(host, isIP);
|
|
4157
|
+
}
|
|
4158
|
+
if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
|
|
4158
4159
|
if (parsed.path) {
|
|
4159
4160
|
parsed.path = normalizePathEncoding(parsed.path);
|
|
4160
4161
|
}
|
|
@@ -35678,7 +35679,7 @@ var StdioServerTransport = class {
|
|
|
35678
35679
|
import { createClient } from "@supabase/supabase-js";
|
|
35679
35680
|
|
|
35680
35681
|
// src/version.ts
|
|
35681
|
-
var VERSION = "0.
|
|
35682
|
+
var VERSION = "0.10.0";
|
|
35682
35683
|
|
|
35683
35684
|
// src/auth.ts
|
|
35684
35685
|
var SUPABASE_PROJECT_URL = "https://tifewzsimmyoiepblhld.supabase.co";
|
|
@@ -36881,7 +36882,7 @@ function registerTransactionTools(server2) {
|
|
|
36881
36882
|
server2.registerTool(
|
|
36882
36883
|
"kirjapro_create_transaction",
|
|
36883
36884
|
{
|
|
36884
|
-
description: "Create a transaction (tosite) with journal entries. Entries must balance (debits = credits). Accepts account numbers resolved to UUIDs server-side. Prefer kirjapro_create_from_template for standard patterns.",
|
|
36885
|
+
description: "Create a transaction (tosite) with journal entries. Entries must balance (debits = credits). Accepts account numbers resolved to UUIDs server-side. Prefer kirjapro_create_from_template for standard patterns. For a VAT-registered (ALV) company the database refuses lines on sales accounts 5000-5499 and a net 4210 credit: sales are invoices.",
|
|
36885
36886
|
inputSchema: {
|
|
36886
36887
|
fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
|
|
36887
36888
|
transactionDate: ISO_DATE_SCHEMA.describe("Transaction date (YYYY-MM-DD)"),
|
|
@@ -37020,7 +37021,7 @@ function registerTransactionTools(server2) {
|
|
|
37020
37021
|
server2.registerTool(
|
|
37021
37022
|
"kirjapro_create_transactions_batch",
|
|
37022
37023
|
{
|
|
37023
|
-
description: "Preview or atomically create multiple transactions in one call (up to 50). dryRun defaults to true. Commit is always all-or-nothing; partial/best-effort writes are intentionally unavailable.",
|
|
37024
|
+
description: "Preview or atomically create multiple transactions in one call (up to 50). dryRun defaults to true. Commit is always all-or-nothing; partial/best-effort writes are intentionally unavailable. For a VAT-registered (ALV) company the database refuses lines on sales accounts 5000-5499 and a net 4210 credit: sales are invoices.",
|
|
37024
37025
|
inputSchema: {
|
|
37025
37026
|
fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
|
|
37026
37027
|
dryRun: external_exports.boolean().default(true).describe(
|
|
@@ -38598,9 +38599,19 @@ var ALV_SUMMARY_OUTPUT_SHAPE = {
|
|
|
38598
38599
|
reverse_charge_sales_base: external_exports.number().optional(),
|
|
38599
38600
|
eu_service_sales_base: external_exports.number().optional(),
|
|
38600
38601
|
unclassified_legacy_reverse_charge_base: external_exports.number().optional(),
|
|
38602
|
+
// get_alv_summary.reconciliation: the report against the 4210/2320 ledger
|
|
38603
|
+
// (ALV report audit 2026-10-01, T2-1). Absent on an older database.
|
|
38604
|
+
reconciliation: external_exports.object({ status: external_exports.enum(["match", "explained", "mismatch"]) }).passthrough().optional(),
|
|
38601
38605
|
warning: external_exports.string().optional(),
|
|
38602
38606
|
message: external_exports.string().optional()
|
|
38603
38607
|
};
|
|
38608
|
+
function alvReconciliationWarning(reconciliation) {
|
|
38609
|
+
if (!reconciliation || typeof reconciliation !== "object") return void 0;
|
|
38610
|
+
const r = reconciliation;
|
|
38611
|
+
if (r.status !== "mismatch") return void 0;
|
|
38612
|
+
const amount = (value) => (Number(value) || 0).toFixed(2);
|
|
38613
|
+
return `Ledger accounts 4210/2320 differ from the ALV report (output VAT difference ${amount(r.output_difference)} \u20AC, input VAT difference ${amount(r.input_difference)} \u20AC). Review reconciliation before filing.`;
|
|
38614
|
+
}
|
|
38604
38615
|
var BALANCE_SHEET_ROW_SHAPE = external_exports.object({
|
|
38605
38616
|
account_number: external_exports.string(),
|
|
38606
38617
|
name: external_exports.string(),
|
|
@@ -39076,7 +39087,7 @@ function registerReportTools(server2) {
|
|
|
39076
39087
|
server2.registerTool(
|
|
39077
39088
|
"kirjapro_get_alv_summary",
|
|
39078
39089
|
{
|
|
39079
|
-
description: "Get VAT (ALV) summary for a date range. Returns output/input VAT breakdown by rate and OmaVero field codes (301-320). For filing deadlines, read kirjapro://compliance/deadlines.",
|
|
39090
|
+
description: "Get VAT (ALV) summary for a date range. Returns output/input VAT breakdown by rate and OmaVero field codes (301-320), and a reconciliation object comparing the 4210/2320 ledger with the report (status match, explained or mismatch, with the differences and the vouchers, invoices, fact mismatches and manual rows that explain them). A mismatch sets warning. For filing deadlines, read kirjapro://compliance/deadlines.",
|
|
39080
39091
|
inputSchema: {
|
|
39081
39092
|
dateFrom: ISO_DATE_SCHEMA.describe("Period start date (YYYY-MM-DD)"),
|
|
39082
39093
|
dateTo: ISO_DATE_SCHEMA.describe("Period end date (YYYY-MM-DD)")
|
|
@@ -39122,8 +39133,10 @@ function registerReportTools(server2) {
|
|
|
39122
39133
|
field_320: _legacyField320,
|
|
39123
39134
|
...safeResult
|
|
39124
39135
|
} = raw;
|
|
39136
|
+
const warning = alvReconciliationWarning(raw.reconciliation);
|
|
39125
39137
|
return toResult({
|
|
39126
39138
|
...safeResult,
|
|
39139
|
+
...warning && { warning },
|
|
39127
39140
|
period: { from: params.dateFrom, to: params.dateTo },
|
|
39128
39141
|
net_vat_payable_formatted: formatCurrency(result.net_vat_payable || 0),
|
|
39129
39142
|
omavero_fields: {
|
|
@@ -39745,6 +39758,21 @@ var ACCOUNTS = {
|
|
|
39745
39758
|
VAKUUTUKSET: "7970"
|
|
39746
39759
|
};
|
|
39747
39760
|
|
|
39761
|
+
// src/compliance/vat-guard.ts
|
|
39762
|
+
var ALV_REGISTERED_SALE_REFUSAL = "This company is VAT-registered (ALV). A sale booked as a journal voucher never reaches the ALV report (fields 301-303 and 309), so Kirjapro refuses it. Create the sale with kirjapro_create_invoice, deliver it with kirjapro_send_invoice and record the payment with kirjapro_record_payment or bank matching.";
|
|
39763
|
+
var ALV_REGISTERED_REVERSE_CHARGE_REFUSAL = "Reverse-charge purchases must be booked as a received invoice in the Kirjapro web app or as a manual Ostojen ALV row so fields 305/306/313/314 are filled. MCP support follows in a later release.";
|
|
39764
|
+
var NOT_VAT_REGISTERED_SETTLEMENT_REFUSAL = "This company is not VAT-registered (ALV), so it has no VAT settlement to book.";
|
|
39765
|
+
async function readCompanyVat(supabase2, companyId2) {
|
|
39766
|
+
const { data, error: error61 } = await supabase2.from("companies").select("alv_registered, vat_period").eq("id", companyId2).single();
|
|
39767
|
+
if (error61 || !data) {
|
|
39768
|
+
return toErrorContent(
|
|
39769
|
+
error61 ? sanitizeDbError("Failed to verify the company VAT registration", error61) : "Failed to verify the company VAT registration."
|
|
39770
|
+
);
|
|
39771
|
+
}
|
|
39772
|
+
const row = data;
|
|
39773
|
+
return { alvRegistered: row.alv_registered === true, vatPeriod: row.vat_period ?? "monthly" };
|
|
39774
|
+
}
|
|
39775
|
+
|
|
39748
39776
|
// src/tools/templates.ts
|
|
39749
39777
|
function splitVat(gross, vatRate) {
|
|
39750
39778
|
if (vatRate === 0) return { net: roundCents(gross), vat: 0 };
|
|
@@ -39844,24 +39872,28 @@ var TEMPLATES = [
|
|
|
39844
39872
|
// Cash sales
|
|
39845
39873
|
{
|
|
39846
39874
|
id: "cash_sale_25",
|
|
39875
|
+
kind: "sale",
|
|
39847
39876
|
description: "K\xE4teismyynti ALV 25,5%",
|
|
39848
39877
|
vatRate: 25.5,
|
|
39849
39878
|
expand: saleCashTemplate(ACCOUNTS.MYYNTI_25_5, 25.5)
|
|
39850
39879
|
},
|
|
39851
39880
|
{
|
|
39852
39881
|
id: "cash_sale_13_5",
|
|
39882
|
+
kind: "sale",
|
|
39853
39883
|
description: "K\xE4teismyynti ALV 13,5%",
|
|
39854
39884
|
vatRate: 13.5,
|
|
39855
39885
|
expand: saleCashTemplate(ACCOUNTS.MYYNTI_13_5, 13.5)
|
|
39856
39886
|
},
|
|
39857
39887
|
{
|
|
39858
39888
|
id: "cash_sale_10",
|
|
39889
|
+
kind: "sale",
|
|
39859
39890
|
description: "K\xE4teismyynti ALV 10%",
|
|
39860
39891
|
vatRate: 10,
|
|
39861
39892
|
expand: saleCashTemplate(ACCOUNTS.MYYNTI_10, 10)
|
|
39862
39893
|
},
|
|
39863
39894
|
{
|
|
39864
39895
|
id: "cash_sale_0",
|
|
39896
|
+
kind: "sale",
|
|
39865
39897
|
description: "K\xE4teismyynti ALV 0%",
|
|
39866
39898
|
vatRate: 0,
|
|
39867
39899
|
expand: saleCashTemplate(ACCOUNTS.MYYNTI_0, 0)
|
|
@@ -39869,18 +39901,21 @@ var TEMPLATES = [
|
|
|
39869
39901
|
// Bank sales
|
|
39870
39902
|
{
|
|
39871
39903
|
id: "bank_sale_25",
|
|
39904
|
+
kind: "sale",
|
|
39872
39905
|
description: "Pankkimyynti ALV 25,5%",
|
|
39873
39906
|
vatRate: 25.5,
|
|
39874
39907
|
expand: saleBankTemplate(ACCOUNTS.MYYNTI_25_5, 25.5)
|
|
39875
39908
|
},
|
|
39876
39909
|
{
|
|
39877
39910
|
id: "bank_sale_13_5",
|
|
39911
|
+
kind: "sale",
|
|
39878
39912
|
description: "Pankkimyynti ALV 13,5%",
|
|
39879
39913
|
vatRate: 13.5,
|
|
39880
39914
|
expand: saleBankTemplate(ACCOUNTS.MYYNTI_13_5, 13.5)
|
|
39881
39915
|
},
|
|
39882
39916
|
{
|
|
39883
39917
|
id: "bank_sale_10",
|
|
39918
|
+
kind: "sale",
|
|
39884
39919
|
description: "Pankkimyynti ALV 10%",
|
|
39885
39920
|
vatRate: 10,
|
|
39886
39921
|
expand: saleBankTemplate(ACCOUNTS.MYYNTI_10, 10)
|
|
@@ -39888,6 +39923,7 @@ var TEMPLATES = [
|
|
|
39888
39923
|
// Invoice sale
|
|
39889
39924
|
{
|
|
39890
39925
|
id: "invoice_sale_25",
|
|
39926
|
+
kind: "sale",
|
|
39891
39927
|
description: "Laskumyynti ALV 25,5% (myyntisaamiset)",
|
|
39892
39928
|
vatRate: 25.5,
|
|
39893
39929
|
expand: (amount, opts) => {
|
|
@@ -39909,6 +39945,7 @@ var TEMPLATES = [
|
|
|
39909
39945
|
// Purchase expenses
|
|
39910
39946
|
{
|
|
39911
39947
|
id: "purchase_expense",
|
|
39948
|
+
kind: "purchase",
|
|
39912
39949
|
description: "Osto ALV 25,5% (pankkitililt\xE4)",
|
|
39913
39950
|
vatRate: 25.5,
|
|
39914
39951
|
expand: (amount, opts) => {
|
|
@@ -39929,6 +39966,7 @@ var TEMPLATES = [
|
|
|
39929
39966
|
},
|
|
39930
39967
|
{
|
|
39931
39968
|
id: "service_expense",
|
|
39969
|
+
kind: "purchase",
|
|
39932
39970
|
description: "Ulkopuolinen palvelu ALV 25,5%",
|
|
39933
39971
|
vatRate: 25.5,
|
|
39934
39972
|
expand: (amount, opts) => {
|
|
@@ -39950,6 +39988,7 @@ var TEMPLATES = [
|
|
|
39950
39988
|
// Rent (no VAT)
|
|
39951
39989
|
{
|
|
39952
39990
|
id: "rent_payment",
|
|
39991
|
+
kind: "other",
|
|
39953
39992
|
description: "Vuokranmaksu (pankkitililt\xE4)",
|
|
39954
39993
|
vatRate: null,
|
|
39955
39994
|
expand: (amount, opts) => applyOverrides(
|
|
@@ -39971,6 +40010,7 @@ var TEMPLATES = [
|
|
|
39971
40010
|
// Bank transfer
|
|
39972
40011
|
{
|
|
39973
40012
|
id: "bank_transfer",
|
|
40013
|
+
kind: "other",
|
|
39974
40014
|
description: "Tilisiirto kassasta pankkitilille",
|
|
39975
40015
|
vatRate: null,
|
|
39976
40016
|
expand: (amount, opts) => applyOverrides(
|
|
@@ -39991,6 +40031,12 @@ var TEMPLATES = [
|
|
|
39991
40031
|
}
|
|
39992
40032
|
];
|
|
39993
40033
|
var TEMPLATE_MAP = new Map(TEMPLATES.map((t) => [t.id, t]));
|
|
40034
|
+
function isSaleTemplate(templateId) {
|
|
40035
|
+
return TEMPLATE_MAP.get(templateId)?.kind === "sale";
|
|
40036
|
+
}
|
|
40037
|
+
function availableTemplates(alvRegistered) {
|
|
40038
|
+
return alvRegistered ? TEMPLATES.filter((t) => t.kind !== "sale") : TEMPLATES;
|
|
40039
|
+
}
|
|
39994
40040
|
function expandTemplate(templateId, amount, opts) {
|
|
39995
40041
|
const template = TEMPLATE_MAP.get(templateId);
|
|
39996
40042
|
if (!template) {
|
|
@@ -40105,7 +40151,7 @@ function registerTemplateTools(server2) {
|
|
|
40105
40151
|
server2.registerTool(
|
|
40106
40152
|
"kirjapro_list_templates",
|
|
40107
40153
|
{
|
|
40108
|
-
description: "List available transaction templates. Templates handle double-entry balancing and VAT splitting automatically.",
|
|
40154
|
+
description: "List available transaction templates. Templates handle double-entry balancing and VAT splitting automatically. Sale templates are not listed for a VAT-registered (ALV) company: its sales are invoices.",
|
|
40109
40155
|
inputSchema: {},
|
|
40110
40156
|
outputSchema: {
|
|
40111
40157
|
templates: external_exports.array(external_exports.object({
|
|
@@ -40117,7 +40163,9 @@ function registerTemplateTools(server2) {
|
|
|
40117
40163
|
annotations: READ_ONLY_ANNOTATIONS
|
|
40118
40164
|
},
|
|
40119
40165
|
async () => {
|
|
40120
|
-
const
|
|
40166
|
+
const companyVat = await readCompanyVat(getSupabase(), getCompanyId());
|
|
40167
|
+
if ("isError" in companyVat) return companyVat;
|
|
40168
|
+
const list = availableTemplates(companyVat.alvRegistered).map((t) => ({
|
|
40121
40169
|
id: t.id,
|
|
40122
40170
|
description: t.description,
|
|
40123
40171
|
vatRate: t.vatRate
|
|
@@ -40128,7 +40176,7 @@ function registerTemplateTools(server2) {
|
|
|
40128
40176
|
server2.registerTool(
|
|
40129
40177
|
"kirjapro_create_from_template",
|
|
40130
40178
|
{
|
|
40131
|
-
description: 'Create a transaction from a template. Use kirjapro_list_templates for available templates. accountOverrides swaps default accounts (e.g. {"2320": "2330"}).',
|
|
40179
|
+
description: 'Create a transaction from a template. Use kirjapro_list_templates for available templates. accountOverrides swaps default accounts (e.g. {"2320": "2330"}). Sale templates are refused for a VAT-registered (ALV) company: create an invoice instead.',
|
|
40132
40180
|
inputSchema: {
|
|
40133
40181
|
template: external_exports.string().describe('Template ID (e.g. "cash_sale_25", "purchase_expense")'),
|
|
40134
40182
|
fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
|
|
@@ -40144,6 +40192,11 @@ function registerTemplateTools(server2) {
|
|
|
40144
40192
|
async (params) => {
|
|
40145
40193
|
const supabase2 = getSupabase();
|
|
40146
40194
|
const companyId2 = getCompanyId();
|
|
40195
|
+
if (isSaleTemplate(params.template)) {
|
|
40196
|
+
const companyVat = await readCompanyVat(supabase2, companyId2);
|
|
40197
|
+
if ("isError" in companyVat) return companyVat;
|
|
40198
|
+
if (companyVat.alvRegistered) return toErrorContent(ALV_REGISTERED_SALE_REFUSAL);
|
|
40199
|
+
}
|
|
40147
40200
|
return withIdempotency(
|
|
40148
40201
|
supabase2,
|
|
40149
40202
|
companyId2,
|
|
@@ -40184,7 +40237,7 @@ function registerTemplateTools(server2) {
|
|
|
40184
40237
|
server2.registerTool(
|
|
40185
40238
|
"kirjapro_create_from_template_batch",
|
|
40186
40239
|
{
|
|
40187
|
-
description: "Preview or atomically create multiple template transactions in one call (up to 50). dryRun defaults to true. Commit is always all-or-nothing; partial/best-effort writes are intentionally unavailable.",
|
|
40240
|
+
description: "Preview or atomically create multiple template transactions in one call (up to 50). dryRun defaults to true. Commit is always all-or-nothing; partial/best-effort writes are intentionally unavailable. A batch with a sale template is refused for a VAT-registered (ALV) company before any write.",
|
|
40188
40241
|
inputSchema: {
|
|
40189
40242
|
fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
|
|
40190
40243
|
dryRun: external_exports.boolean().default(true).describe(
|
|
@@ -40207,6 +40260,16 @@ function registerTemplateTools(server2) {
|
|
|
40207
40260
|
async (params) => {
|
|
40208
40261
|
const supabase2 = getSupabase();
|
|
40209
40262
|
const companyId2 = getCompanyId();
|
|
40263
|
+
const saleIndexes = params.transactions.map((tx, index) => isSaleTemplate(tx.template) ? index : -1).filter((index) => index >= 0);
|
|
40264
|
+
if (saleIndexes.length > 0) {
|
|
40265
|
+
const companyVat = await readCompanyVat(supabase2, companyId2);
|
|
40266
|
+
if ("isError" in companyVat) return companyVat;
|
|
40267
|
+
if (companyVat.alvRegistered) {
|
|
40268
|
+
return toErrorContent(
|
|
40269
|
+
`${ALV_REGISTERED_SALE_REFUSAL} Sale templates at index ${saleIndexes.join(", ")}; nothing was written.`
|
|
40270
|
+
);
|
|
40271
|
+
}
|
|
40272
|
+
}
|
|
40210
40273
|
return withIdempotency(
|
|
40211
40274
|
supabase2,
|
|
40212
40275
|
companyId2,
|
|
@@ -40430,6 +40493,35 @@ function freshnessMeta(rates = FINNISH_RATES_2026, today = /* @__PURE__ */ new D
|
|
|
40430
40493
|
};
|
|
40431
40494
|
}
|
|
40432
40495
|
|
|
40496
|
+
// src/compliance/vat-settlement-date.ts
|
|
40497
|
+
var pad = (value) => String(value).padStart(2, "0");
|
|
40498
|
+
function vatPeriodRange(month, vatPeriod) {
|
|
40499
|
+
const [yearText, monthText] = month.split("-");
|
|
40500
|
+
const year = Number(yearText);
|
|
40501
|
+
const monthNumber = Number(monthText);
|
|
40502
|
+
const period = vatPeriod ?? "monthly";
|
|
40503
|
+
const startMonth = period === "yearly" ? 1 : period === "quarterly" ? Math.floor((monthNumber - 1) / 3) * 3 + 1 : monthNumber;
|
|
40504
|
+
const endMonth = period === "yearly" ? 12 : period === "quarterly" ? startMonth + 2 : monthNumber;
|
|
40505
|
+
const lastDay = new Date(Date.UTC(year, endMonth, 0)).getUTCDate();
|
|
40506
|
+
const label = period === "yearly" ? String(year) : period === "quarterly" ? `${year}-Q${(startMonth + 2) / 3}` : month;
|
|
40507
|
+
return {
|
|
40508
|
+
start: `${year}-${pad(startMonth)}-01`,
|
|
40509
|
+
end: `${year}-${pad(endMonth)}-${pad(lastDay)}`,
|
|
40510
|
+
label
|
|
40511
|
+
};
|
|
40512
|
+
}
|
|
40513
|
+
function defaultSettlementDate(month, vatPeriod) {
|
|
40514
|
+
const { end } = vatPeriodRange(month, vatPeriod);
|
|
40515
|
+
const [endYear, endMonth] = end.split("-").map(Number);
|
|
40516
|
+
if ((vatPeriod ?? "monthly") === "yearly") {
|
|
40517
|
+
const februaryDays = new Date(Date.UTC(endYear + 1, 2, 0)).getUTCDate();
|
|
40518
|
+
return `${endYear + 1}-02-${pad(februaryDays)}`;
|
|
40519
|
+
}
|
|
40520
|
+
const dueMonthIndex = endMonth - 1 + 2;
|
|
40521
|
+
const dueYear = endYear + Math.floor(dueMonthIndex / 12);
|
|
40522
|
+
return `${dueYear}-${pad(dueMonthIndex % 12 + 1)}-12`;
|
|
40523
|
+
}
|
|
40524
|
+
|
|
40433
40525
|
// src/tools/bookings.ts
|
|
40434
40526
|
function expenseAccountWarning(account) {
|
|
40435
40527
|
const first = account.trim().charAt(0);
|
|
@@ -40456,7 +40548,7 @@ function registerBookingTools(server2) {
|
|
|
40456
40548
|
server2.registerTool(
|
|
40457
40549
|
"kirjapro_book_purchase",
|
|
40458
40550
|
{
|
|
40459
|
-
description: "Book a purchase (osto) with automatic VAT splitting. Supports reverse charge (AVL 9\xA7) and entertainment (edustus, AVL 114\xA7). Rate rules: food/restaurants 13.5%, insurance 0% (exempt), alcohol 25.5%.",
|
|
40551
|
+
description: "Book a purchase (osto) with automatic VAT splitting. Supports reverse charge (AVL 9\xA7) for companies that are not VAT-registered and entertainment (edustus, AVL 114\xA7). For a VAT-registered (ALV) company reverseCharge is refused: book it as a received invoice in the web app. Rate rules: food/restaurants 13.5%, insurance 0% (exempt), alcohol 25.5%.",
|
|
40460
40552
|
inputSchema: {
|
|
40461
40553
|
amount: external_exports.number().finite().positive().max(1e9).describe(
|
|
40462
40554
|
"EUR amount. Standard purchase: GROSS (VAT-inclusive) \u2014 VAT is split out of this. Reverse charge (reverseCharge=true): NET foreign-invoice amount \u2014 the foreign supplier does not charge Finnish VAT, so the parameter must be net. VAT is self-assessed on top per AVL 9\xA7 producing equal debit to ALV-saamiset and credit to ALV-velka. Entertainment (edustus=true): GROSS (full amount to expense, no VAT deduction per AVL 114\xA7)."
|
|
@@ -40479,6 +40571,11 @@ function registerBookingTools(server2) {
|
|
|
40479
40571
|
const rate = Number.parseFloat(params.vatRate);
|
|
40480
40572
|
const gross = roundCents(params.amount);
|
|
40481
40573
|
let entries;
|
|
40574
|
+
if (params.reverseCharge && !params.edustus) {
|
|
40575
|
+
const vat = await readCompanyVat(getSupabase(), getCompanyId());
|
|
40576
|
+
if ("isError" in vat) return vat;
|
|
40577
|
+
if (vat.alvRegistered) return toErrorContent(ALV_REGISTERED_REVERSE_CHARGE_REFUSAL);
|
|
40578
|
+
}
|
|
40482
40579
|
if (params.edustus) {
|
|
40483
40580
|
entries = [
|
|
40484
40581
|
{ accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
|
|
@@ -40547,11 +40644,11 @@ function registerBookingTools(server2) {
|
|
|
40547
40644
|
server2.registerTool(
|
|
40548
40645
|
"kirjapro_book_vat_settlement",
|
|
40549
40646
|
{
|
|
40550
|
-
description: `Book ALV-tilitys (VAT settlement) for
|
|
40647
|
+
description: `Book ALV-tilitys (VAT settlement) for the company's VAT period that contains the given month. Settles the period's net from ALV accounts (${ACCOUNTS.ALV_SAAMISET}/${ACCOUNTS.ALV_VELKA}) by voucher date, excluding earlier settlements, closing entries and the previous-year import. Default date: the statutory due date (12th of the second month after the period; annual period: end of February), not moved to a business day. The period follows the company's VAT period. Refuses a period already settled, a period whose ledger does not match the ALV report, and negative balances.`,
|
|
40551
40648
|
inputSchema: {
|
|
40552
|
-
month: external_exports.string().regex(/^\d{4}
|
|
40553
|
-
fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (
|
|
40554
|
-
settlementDate: ISO_DATE_SCHEMA.optional().describe("Settlement date (default: 12th of month
|
|
40649
|
+
month: external_exports.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/).describe("A month of the VAT period to settle (YYYY-MM); a quarterly or annual company settles the whole period containing it"),
|
|
40650
|
+
fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID of the settlement date (default: the open fiscal year containing the settlement date)"),
|
|
40651
|
+
settlementDate: ISO_DATE_SCHEMA.optional().describe("Settlement date (default: the statutory due date, 12th of the second month after the period; annual period: end of February)"),
|
|
40555
40652
|
paymentAccount: external_exports.string().default(ACCOUNTS.PANKKITILI).describe(`Bank account (default "${ACCOUNTS.PANKKITILI}" Pankkitili)`),
|
|
40556
40653
|
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
|
|
40557
40654
|
dryRun: DRY_RUN_SCHEMA
|
|
@@ -40561,16 +40658,25 @@ function registerBookingTools(server2) {
|
|
|
40561
40658
|
async (params) => {
|
|
40562
40659
|
const supabase2 = getSupabase();
|
|
40563
40660
|
const companyId2 = getCompanyId();
|
|
40661
|
+
const companyVat = await readCompanyVat(supabase2, companyId2);
|
|
40662
|
+
if ("isError" in companyVat) return companyVat;
|
|
40663
|
+
if (!companyVat.alvRegistered) return toErrorContent(NOT_VAT_REGISTERED_SETTLEMENT_REFUSAL);
|
|
40664
|
+
const period = vatPeriodRange(params.month, companyVat.vatPeriod);
|
|
40665
|
+
const dueDate = defaultSettlementDate(params.month, companyVat.vatPeriod);
|
|
40666
|
+
const settlementDate = params.settlementDate ?? dueDate;
|
|
40564
40667
|
let fiscalYearId = params.fiscalYearId;
|
|
40565
40668
|
if (!fiscalYearId) {
|
|
40566
|
-
const
|
|
40567
|
-
if (
|
|
40568
|
-
|
|
40669
|
+
const { data: years, error: yearsError } = await supabase2.from("fiscal_years").select("id").eq("company_id", companyId2).eq("is_closed", false).lte("start_date", settlementDate).gte("end_date", settlementDate).order("start_date", { ascending: false }).limit(1);
|
|
40670
|
+
if (yearsError) {
|
|
40671
|
+
return toErrorContent(sanitizeDbError("Failed to query fiscal years", yearsError));
|
|
40672
|
+
}
|
|
40673
|
+
if (!years || years.length === 0) {
|
|
40674
|
+
return toErrorContent(
|
|
40675
|
+
`No open fiscal year contains the settlement date ${settlementDate} (statutory due date for ${period.label}: ${dueDate}). Create that fiscal year or pass settlementDate inside an open fiscal year.`
|
|
40676
|
+
);
|
|
40677
|
+
}
|
|
40678
|
+
fiscalYearId = years[0].id;
|
|
40569
40679
|
}
|
|
40570
|
-
const [year, monthStr] = params.month.split("-");
|
|
40571
|
-
const monthNum = Number.parseInt(monthStr, 10);
|
|
40572
|
-
const nextMonth = monthNum === 12 ? `${Number.parseInt(year, 10) + 1}-01` : `${year}-${String(monthNum + 1).padStart(2, "0")}`;
|
|
40573
|
-
const settlementDate = params.settlementDate ?? `${nextMonth}-12`;
|
|
40574
40680
|
const { data: fy } = await supabase2.from("fiscal_years").select("start_date, end_date, is_closed").eq("id", fiscalYearId).eq("company_id", companyId2).single();
|
|
40575
40681
|
if (!fy) {
|
|
40576
40682
|
return toErrorContent(
|
|
@@ -40584,7 +40690,7 @@ function registerBookingTools(server2) {
|
|
|
40584
40690
|
}
|
|
40585
40691
|
if (settlementDate > fy.end_date) {
|
|
40586
40692
|
return toErrorContent(
|
|
40587
|
-
`Settlement date ${settlementDate} is after fiscal year end ${fy.end_date}. Pass an explicit settlementDate within ${fy.start_date}..${fy.end_date}
|
|
40693
|
+
`Settlement date ${settlementDate} is after fiscal year end ${fy.end_date}. Pass an explicit settlementDate within ${fy.start_date}..${fy.end_date} or the fiscal year that contains ${settlementDate} \u2014 silent clamping would shift the ALV-kausi.`
|
|
40588
40694
|
);
|
|
40589
40695
|
}
|
|
40590
40696
|
if (settlementDate < fy.start_date) {
|
|
@@ -40596,9 +40702,14 @@ function registerBookingTools(server2) {
|
|
|
40596
40702
|
return toDryRunResponse("kirjapro_book_vat_settlement", {
|
|
40597
40703
|
action: "book_vat_settlement",
|
|
40598
40704
|
month: params.month,
|
|
40705
|
+
vat_period: companyVat.vatPeriod,
|
|
40706
|
+
period_start: period.start,
|
|
40707
|
+
period_end: period.end,
|
|
40599
40708
|
settlement_date: settlementDate,
|
|
40709
|
+
statutory_due_date: dueDate,
|
|
40710
|
+
fiscal_year_id: fiscalYearId,
|
|
40600
40711
|
payment_account: params.paymentAccount,
|
|
40601
|
-
hint:
|
|
40712
|
+
hint: `Call kirjapro_get_alv_summary for ${period.start}..${period.end} to preview the net VAT and its reconciliation before committing.`
|
|
40602
40713
|
});
|
|
40603
40714
|
}
|
|
40604
40715
|
return withIdempotency(
|
|
@@ -40621,20 +40732,27 @@ function registerBookingTools(server2) {
|
|
|
40621
40732
|
);
|
|
40622
40733
|
}
|
|
40623
40734
|
const result = data;
|
|
40735
|
+
const periodStart = result.period_start ?? period.start;
|
|
40736
|
+
const periodEnd = result.period_end ?? period.end;
|
|
40624
40737
|
if (result.direction === "none") {
|
|
40625
40738
|
return toResult({
|
|
40626
|
-
message: result.message ?? `No VAT entries found for ${
|
|
40739
|
+
message: result.message ?? `No VAT entries found for ${period.label}. Nothing to settle.`,
|
|
40627
40740
|
alv_saamiset: 0,
|
|
40628
|
-
alv_velka: 0
|
|
40741
|
+
alv_velka: 0,
|
|
40742
|
+
period_start: periodStart,
|
|
40743
|
+
period_end: periodEnd
|
|
40629
40744
|
});
|
|
40630
40745
|
}
|
|
40631
40746
|
return toResult({
|
|
40632
|
-
message: `VAT settlement booked for ${
|
|
40747
|
+
message: `VAT settlement booked for ${period.label}`,
|
|
40633
40748
|
transaction_id: result.transaction_id,
|
|
40634
40749
|
alv_saamiset: result.alv_saamiset,
|
|
40635
40750
|
alv_velka: result.alv_velka,
|
|
40636
40751
|
net_vat: result.net_vat,
|
|
40637
|
-
direction: result.direction
|
|
40752
|
+
direction: result.direction,
|
|
40753
|
+
period_start: periodStart,
|
|
40754
|
+
period_end: periodEnd,
|
|
40755
|
+
settlement_date: settlementDate
|
|
40638
40756
|
});
|
|
40639
40757
|
}
|
|
40640
40758
|
);
|
|
@@ -40643,7 +40761,7 @@ function registerBookingTools(server2) {
|
|
|
40643
40761
|
server2.registerTool(
|
|
40644
40762
|
"kirjapro_book_sale",
|
|
40645
40763
|
{
|
|
40646
|
-
description: "Book a sale (myynti) with automatic VAT splitting. Payment types: invoice (default, D Myyntisaamiset), cash (D bank), prepaid.",
|
|
40764
|
+
description: "Book a sale (myynti) with automatic VAT splitting for a company that is not VAT-registered. Payment types: invoice (default, D Myyntisaamiset), cash (D bank), prepaid. A VAT-registered (ALV) company is refused: its sales reach the ALV report only as invoices (kirjapro_create_invoice + kirjapro_send_invoice).",
|
|
40647
40765
|
inputSchema: {
|
|
40648
40766
|
amount: external_exports.number().finite().positive().max(1e9).describe("Gross EUR amount (VAT-inclusive)"),
|
|
40649
40767
|
description: external_exports.string().min(1).max(500).describe('Transaction description (e.g. "Konsultointi Acme Oy, 40h \xD7 95 \u20AC")'),
|
|
@@ -40660,6 +40778,9 @@ function registerBookingTools(server2) {
|
|
|
40660
40778
|
annotations: WRITE_ANNOTATIONS
|
|
40661
40779
|
},
|
|
40662
40780
|
async (params) => {
|
|
40781
|
+
const companyVat = await readCompanyVat(getSupabase(), getCompanyId());
|
|
40782
|
+
if ("isError" in companyVat) return companyVat;
|
|
40783
|
+
if (companyVat.alvRegistered) return toErrorContent(ALV_REGISTERED_SALE_REFUSAL);
|
|
40663
40784
|
const rate = Number.parseFloat(params.vatRate);
|
|
40664
40785
|
const gross = roundCents(params.amount);
|
|
40665
40786
|
let entries;
|
|
@@ -42647,7 +42768,7 @@ var INSTRUCTIONS_CORE_ADDENDUM = `
|
|
|
42647
42768
|
The following workflows are NOT exposed in the current (core) surface. When a user asks for them, redirect to the web UI rather than reaching for the closest available tool:
|
|
42648
42769
|
|
|
42649
42770
|
- **Recurring invoice templates** \u2014 created and managed in the web app; cron generates the issued invoices
|
|
42650
|
-
- **Booking shortcuts** (book_purchase / book_sale / book_vat_settlement) \u2014 for these, you CAN compose the journal entries manually via \`kirjapro_create_transaction\` with explicit debit/credit lines if the user prefers; otherwise redirect
|
|
42771
|
+
- **Booking shortcuts** (book_purchase / book_sale / book_vat_settlement) \u2014 for these, you CAN compose the journal entries manually via \`kirjapro_create_transaction\` with explicit debit/credit lines if the user prefers; otherwise redirect. Exception: a VAT-registered (ALV) company books sales only as invoices (\`kirjapro_create_invoice\` + \`kirjapro_send_invoice\`) and reverse-charge purchases in the web app; the database refuses its vouchers with sales accounts 5000-5499 or a net 4210 credit
|
|
42651
42772
|
- **Payroll (palkanlaskenta)** \u2014 Kirjapro does NOT calculate or book payroll. Direct the user to Palkkaus.fi or Palkka.fi for the calculation, then bring the resulting payslip back and book it via \`kirjapro_create_transaction\` with explicit debit/credit lines
|
|
42652
42773
|
- **Transaction templates** \u2014 manual journal entries via \`kirjapro_create_transaction\`
|
|
42653
42774
|
- **Manual offer status changes** \u2014 the customer's email-link click sets status server-side; just wait for the user to confirm acceptance
|
|
@@ -42666,10 +42787,10 @@ var INSTRUCTIONS_FULL_ADDENDUM = `
|
|
|
42666
42787
|
|
|
42667
42788
|
### Booking Shortcuts
|
|
42668
42789
|
Faster than composing journal entries via \`kirjapro_create_transaction\`:
|
|
42669
|
-
- \`kirjapro_book_sale\` / \`kirjapro_book_purchase\` \u2014 one-liner with auto VAT splitting
|
|
42670
|
-
- \`kirjapro_book_vat_settlement\` \u2014
|
|
42790
|
+
- \`kirjapro_book_sale\` / \`kirjapro_book_purchase\` \u2014 one-liner with auto VAT splitting. A VAT-registered (ALV) company books sales as invoices (\`kirjapro_create_invoice\` + \`kirjapro_send_invoice\`): \`kirjapro_book_sale\` and \`kirjapro_book_purchase\` with \`reverseCharge\` are refused for it
|
|
42791
|
+
- \`kirjapro_book_vat_settlement\` \u2014 settles the company's VAT period from ALV-saamiset/ALV-velka. Call \`kirjapro_get_alv_summary\` for the period first to preview the net and its reconciliation.
|
|
42671
42792
|
- **Payroll is intentionally NOT exposed.** Direct users to Palkkaus.fi / Palkka.fi for salary calculation; the payslip can then be booked manually via \`kirjapro_create_transaction\`.
|
|
42672
|
-
- \`kirjapro_list_templates\` \u2192 \`kirjapro_create_from_template\` for common patterns (
|
|
42793
|
+
- \`kirjapro_list_templates\` \u2192 \`kirjapro_create_from_template\` for common patterns (built-in templates; the sale templates are hidden and refused for a VAT-registered company)
|
|
42673
42794
|
|
|
42674
42795
|
### Manual Offer Status
|
|
42675
42796
|
- \`kirjapro_update_offer_status\` \u2014 manual override; usually the customer's email-link click handles accept/reject server-side
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirjapro/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "MCP server for Finnish accounting — exposes Kirjapro bookkeeping, invoicing, and reporting tools to Claude, ChatGPT, Mistral, and other MCP clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@kirjapro/mcp-server",
|
|
3
3
|
"displayName": "Kirjapro - Finnish Accounting",
|
|
4
4
|
"description": "Finnish double-entry bookkeeping, invoicing, and financial reporting for growing businesses. Create transactions, generate invoices with VAT calculation, produce income statements and balance sheets, and validate Finnish business identifiers.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.10.0",
|
|
6
6
|
"homepage": "https://kirjapro.fi",
|
|
7
7
|
"repository": "https://github.com/Jaollo/kirja-pro",
|
|
8
8
|
"license": "MIT",
|
package/tool-fingerprints.json
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
"fingerprints": [
|
|
3
3
|
{
|
|
4
4
|
"name": "kirjapro_book_purchase",
|
|
5
|
-
"hash": "
|
|
5
|
+
"hash": "ec096990916f5112"
|
|
6
6
|
},
|
|
7
7
|
{
|
|
8
8
|
"name": "kirjapro_book_sale",
|
|
9
|
-
"hash": "
|
|
9
|
+
"hash": "6868bdf46073f442"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"name": "kirjapro_book_vat_settlement",
|
|
13
|
-
"hash": "
|
|
13
|
+
"hash": "39f807857fb81e5d"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"name": "kirjapro_convert_offer_to_invoice",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"name": "kirjapro_create_from_template",
|
|
25
|
-
"hash": "
|
|
25
|
+
"hash": "9e774f4f25c6ce93"
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
"name": "kirjapro_create_from_template_batch",
|
|
29
|
-
"hash": "
|
|
29
|
+
"hash": "415d1d219c81467f"
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"name": "kirjapro_create_invoice",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
"name": "kirjapro_create_transaction",
|
|
45
|
-
"hash": "
|
|
45
|
+
"hash": "bea9ccf179928813"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"name": "kirjapro_create_transactions_batch",
|
|
49
|
-
"hash": "
|
|
49
|
+
"hash": "160de9628d56e6be"
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"name": "kirjapro_delete_invoice",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
68
|
"name": "kirjapro_get_alv_summary",
|
|
69
|
-
"hash": "
|
|
69
|
+
"hash": "1c225c5d61c0839f"
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
"name": "kirjapro_get_ar_aging",
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
},
|
|
163
163
|
{
|
|
164
164
|
"name": "kirjapro_list_templates",
|
|
165
|
-
"hash": "
|
|
165
|
+
"hash": "7601b5d1da0d7283"
|
|
166
166
|
},
|
|
167
167
|
{
|
|
168
168
|
"name": "kirjapro_list_transactions",
|