quints 0.1.1__tar.gz → 0.3.0__tar.gz
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.
- {quints-0.1.1 → quints-0.3.0}/.gitignore +1 -0
- {quints-0.1.1 → quints-0.3.0}/PKG-INFO +3 -3
- {quints-0.1.1 → quints-0.3.0}/README.md +1 -1
- {quints-0.1.1 → quints-0.3.0}/examples/.gitignore +3 -0
- {quints-0.1.1 → quints-0.3.0}/examples/AGENTS.md +21 -10
- quints-0.3.0/examples/accounts.bean +43 -0
- {quints-0.1.1 → quints-0.3.0}/examples/answers.toml +1 -0
- quints-0.3.0/examples/books/2026.bean +28 -0
- quints-0.3.0/examples/commodities.bean +9 -0
- quints-0.3.0/examples/invoicing/acme-2026-07.yaml +15 -0
- quints-0.3.0/examples/invoicing/customers.yaml +15 -0
- quints-0.3.0/examples/invoicing/globex-2026-08.yaml +13 -0
- quints-0.3.0/examples/invoicing/issuer.yaml +16 -0
- quints-0.3.0/examples/main.bean +23 -0
- quints-0.3.0/examples/pyproject.toml +14 -0
- {quints-0.1.1 → quints-0.3.0}/examples/quints.toml +18 -3
- {quints-0.1.1 → quints-0.3.0}/pyproject.toml +2 -2
- {quints-0.1.1 → quints-0.3.0}/src/quints/__init__.py +1 -1
- {quints-0.1.1 → quints-0.3.0}/src/quints/cli.py +54 -3
- {quints-0.1.1 → quints-0.3.0}/src/quints/config.py +12 -0
- quints-0.3.0/src/quints/init.py +795 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/kmu.py +38 -9
- {quints-0.1.1 → quints-0.3.0}/src/quints/plugins/kmu.py +5 -3
- {quints-0.1.1 → quints-0.3.0}/src/quints/prices.py +50 -3
- {quints-0.1.1 → quints-0.3.0}/src/quints/report_pdf.py +10 -5
- quints-0.3.0/tests/test_docs.py +112 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_init.py +104 -5
- {quints-0.1.1 → quints-0.3.0}/tests/test_prices_sync.py +21 -1
- quints-0.1.1/examples/invoicing/acme-2026-07.yaml +0 -19
- quints-0.1.1/examples/invoicing/issuer.yaml +0 -12
- quints-0.1.1/examples/main.bean +0 -78
- quints-0.1.1/src/quints/init.py +0 -462
- quints-0.1.1/tests/test_docs.py +0 -48
- {quints-0.1.1 → quints-0.3.0}/LICENSE +0 -0
- {quints-0.1.1 → quints-0.3.0}/examples/documents/.gitkeep +0 -0
- {quints-0.1.1 → quints-0.3.0}/examples/inbox/.gitkeep +0 -0
- {quints-0.1.1 → quints-0.3.0}/examples/prices.bean +0 -0
- {quints-0.1.1 → quints-0.3.0}/examples/staging/.gitkeep +0 -0
- {quints-0.1.1 → quints-0.3.0}/examples/statements/ubs-2026.mt940 +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/fava/__init__.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/fava/templates/QuintDashboard.html +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/fx.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/importing.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/inbox.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/invoice/__init__.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/invoice/draft.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/invoice/labels.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/invoice/model.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/invoice/qr.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/invoice/render.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/invoice/template.typ +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/invoice/vatid.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/invoice/verify.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/ledger.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/match.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/mwst.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/plugins/__init__.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/receivables.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/report.typ +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/settlement.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/ui.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/src/quints/vat.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/fixtures/transactions.mt940 +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_cli_json.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_config.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_fava_ext.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_fx.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_importing.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_inbox_match.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_invoice.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_kmu_plugin.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_kmu_report.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_ledger.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_mwst.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_receivables.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_report_pdf.py +0 -0
- {quints-0.1.1 → quints-0.3.0}/tests/test_settlement.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quints
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Swiss VAT & accounting toolkit for plain-text (beancount) books: MWST reports and quarterly settlement, Bezugsteuer helpers, BAZG daily FX rates, QR-bill invoicing, KMU statutory statements, and statement importers (UBS MT940, Wise, Stripe) that draft into a staging area
|
|
5
5
|
License: GPL-2.0-only
|
|
6
6
|
License-File: LICENSE
|
|
@@ -11,7 +11,7 @@ Requires-Dist: beancount>=3.2.0
|
|
|
11
11
|
Requires-Dist: beangulp-mt940>=0.1.0
|
|
12
12
|
Requires-Dist: beangulp-stripe>=0.1.0
|
|
13
13
|
Requires-Dist: beangulp-wise[sca]>=0.1.0
|
|
14
|
-
Requires-Dist: beanprice-bazg>=0.
|
|
14
|
+
Requires-Dist: beanprice-bazg>=0.2.0
|
|
15
15
|
Requires-Dist: fava>=1.30
|
|
16
16
|
Requires-Dist: pydantic>=2.7
|
|
17
17
|
Requires-Dist: python-stdnum>=1.20
|
|
@@ -27,7 +27,7 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
|
|
28
28
|
Swiss VAT & accounting toolkit for plain-text ([beancount](https://github.com/beancount/beancount)) books.
|
|
29
29
|
|
|
30
|
-
Everything a Swiss micro-company (GmbH
|
|
30
|
+
Everything a Swiss micro-company (GmbH, AG, or Einzelfirma) needs on top of beancount + Fava:
|
|
31
31
|
|
|
32
32
|
- **MWST**: quarterly VAT report mapped to the ESTV form Ziffern, settlement
|
|
33
33
|
transactions, VAT status, and Bezugsteuer (reverse-charge, Art. 45 ff. MWSTG)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Swiss VAT & accounting toolkit for plain-text ([beancount](https://github.com/beancount/beancount)) books.
|
|
4
4
|
|
|
5
|
-
Everything a Swiss micro-company (GmbH
|
|
5
|
+
Everything a Swiss micro-company (GmbH, AG, or Einzelfirma) needs on top of beancount + Fava:
|
|
6
6
|
|
|
7
7
|
- **MWST**: quarterly VAT report mapped to the ESTV form Ziffern, settlement
|
|
8
8
|
transactions, VAT status, and Bezugsteuer (reverse-charge, Art. 45 ff. MWSTG)
|
|
@@ -5,20 +5,31 @@ with [`quints`](https://github.com/sealambda/quints). **`quints` is a
|
|
|
5
5
|
deterministic tool — you drive it, it never calls a model.** Your job is to
|
|
6
6
|
extend and maintain the ledger; `quints` validates and reports on it.
|
|
7
7
|
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
`uv sync` once — it installs quints, which brings beancount and fava along.
|
|
11
|
+
Then `uv run quints check` (or activate the venv and call `quints` and the
|
|
12
|
+
standard beancount tools directly).
|
|
13
|
+
|
|
8
14
|
## Layout
|
|
9
15
|
|
|
10
|
-
- `main.bean` — the
|
|
11
|
-
"quints.plugins.kmu"` is enabled: every `*:CH:GmbH:*` account
|
|
12
|
-
opened with a four-digit `kmu:` code (Swiss KMU Kontenrahmen).
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
15
|
-
|
|
16
|
-
- `
|
|
16
|
+
- `main.bean` — options, plugins, includes; the entry point every tool loads.
|
|
17
|
+
`plugin "quints.plugins.kmu"` is enabled: every `*:CH:GmbH:*` account
|
|
18
|
+
**must** be opened with a four-digit `kmu:` code (Swiss KMU Kontenrahmen).
|
|
19
|
+
- `accounts.bean` — the chart of accounts (all `open` directives).
|
|
20
|
+
- `books/2026.bean` — transactions, one file per fiscal year. `main.bean`
|
|
21
|
+
includes `books/*.bean`, so a new year just needs a new file.
|
|
22
|
+
- `commodities.bean` — currencies; `prices.bean` — FX rates, refresh with
|
|
23
|
+
`quints prices sync`.
|
|
24
|
+
- `quints.toml` — entity config (name, legal form, VAT, importer rules). VAT
|
|
25
|
+
*rates* are law and live in code, not here.
|
|
26
|
+
- `staging/` — importer drafts land here; `inbox/` — incoming source
|
|
27
|
+
documents; `documents/` — filed documents, mirroring the account tree.
|
|
17
28
|
|
|
18
29
|
## Extending the chart of accounts (the part that needs judgement)
|
|
19
30
|
|
|
20
|
-
Add income/expense sub-trees for this business as `open` directives
|
|
21
|
-
the KMU code it rolls up to, e.g.:
|
|
31
|
+
Add income/expense sub-trees for this business as `open` directives in
|
|
32
|
+
`accounts.bean`, each with the KMU code it rolls up to, e.g.:
|
|
22
33
|
|
|
23
34
|
```beancount
|
|
24
35
|
2026-01-01 open Expenses:CH:GmbH:Marketing:Ads CHF
|
|
@@ -33,7 +44,7 @@ KMU Kontenrahmen; `quints check` fails on a `:CH:GmbH:` account with no valid
|
|
|
33
44
|
|
|
34
45
|
1. Draft bank/PSP activity: `quints import ubs <file>` → `staging/`.
|
|
35
46
|
2. Review, add the VAT decision (InputVAT / Bezugsteuer / none) and a linked
|
|
36
|
-
document, then move drafts into `
|
|
47
|
+
document, then move drafts into `books/2026.bean`.
|
|
37
48
|
3. **Always** `quints check` before you consider the books consistent.
|
|
38
49
|
|
|
39
50
|
## Machine-readable surfaces (prefer these over scraping text)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
; Chart of accounts — Swiss KMU Kontenrahmen. Every *:CH:GmbH:* account
|
|
2
|
+
; must carry the four-digit kmu: code it rolls up to (enforced by
|
|
3
|
+
; quints.plugins.kmu). Extend the chart by adding more open directives;
|
|
4
|
+
; see AGENTS.md and `quints report konten`.
|
|
5
|
+
|
|
6
|
+
2026-01-01 open Assets:CH:GmbH:Current:UBS:CHF CHF
|
|
7
|
+
kmu: "1020" ; Bank deposits
|
|
8
|
+
2026-01-01 open Assets:CH:GmbH:Receivable:Trade
|
|
9
|
+
kmu: "1100" ; Trade receivables
|
|
10
|
+
2026-01-01 open Assets:CH:GmbH:Tax:InputVAT CHF
|
|
11
|
+
kmu: "1170" ; Input VAT
|
|
12
|
+
2026-01-01 open Liabilities:CH:GmbH:Tax:OutputVAT CHF
|
|
13
|
+
kmu: "2200" ; VAT due
|
|
14
|
+
2026-01-01 open Liabilities:CH:GmbH:Tax:Bezugsteuer CHF
|
|
15
|
+
kmu: "2200" ; VAT due
|
|
16
|
+
2026-01-01 open Liabilities:CH:GmbH:Tax:PayableVAT CHF
|
|
17
|
+
kmu: "2200" ; VAT due
|
|
18
|
+
2026-01-01 open Equity:CH:GmbH:Capital:Share CHF
|
|
19
|
+
kmu: "2800" ; Share capital
|
|
20
|
+
2026-01-01 open Income:CH:GmbH:Consulting:External:Domestic
|
|
21
|
+
kmu: "3400" ; Revenue from services
|
|
22
|
+
2026-01-01 open Income:CH:GmbH:Consulting:External:Export
|
|
23
|
+
kmu: "3400" ; Revenue from services
|
|
24
|
+
2026-01-01 open Expenses:CH:GmbH:IT:Hosting
|
|
25
|
+
kmu: "6570" ; IT expenses
|
|
26
|
+
2026-01-01 open Expenses:CH:GmbH:FX:CurrencyLoss CHF
|
|
27
|
+
kmu: "6900" ; Financial expenses
|
|
28
|
+
2026-01-01 open Income:CH:GmbH:Rounding CHF
|
|
29
|
+
kmu: "6950" ; Financial income
|
|
30
|
+
2026-01-01 open Income:CH:GmbH:FX:CurrencyGain CHF
|
|
31
|
+
kmu: "6950" ; Financial income
|
|
32
|
+
2026-01-01 open Assets:CH:GmbH:Current:Wise:CHF CHF
|
|
33
|
+
kmu: "1020" ; Bank deposits
|
|
34
|
+
2026-01-01 open Assets:CH:GmbH:Current:Wise:EUR EUR
|
|
35
|
+
kmu: "1020" ; Bank deposits
|
|
36
|
+
2026-01-01 open Assets:CH:GmbH:Current:Wise:USD USD
|
|
37
|
+
kmu: "1020" ; Bank deposits
|
|
38
|
+
2026-01-01 open Expenses:CH:GmbH:BankFees:Wise
|
|
39
|
+
kmu: "6940" ; Bank charges
|
|
40
|
+
2026-01-01 open Assets:CH:GmbH:Current:Stripe:EUR EUR
|
|
41
|
+
kmu: "1020" ; Bank deposits
|
|
42
|
+
2026-01-01 open Expenses:CH:GmbH:BankFees:Stripe
|
|
43
|
+
kmu: "6940" ; Bank charges
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
; 2026 — transactions for the fiscal year. Reviewed importer drafts
|
|
2
|
+
; move here from staging/. main.bean includes books/*.bean, so a new
|
|
3
|
+
; year just needs a new books/<year>.bean file.
|
|
4
|
+
|
|
5
|
+
; ── sample activity (remove once you book your own) ───────────────────
|
|
6
|
+
|
|
7
|
+
2026-01-02 * "Founders" "Share capital paid in"
|
|
8
|
+
Assets:CH:GmbH:Current:UBS:CHF 20000.00 CHF
|
|
9
|
+
Equity:CH:GmbH:Capital:Share -20000.00 CHF
|
|
10
|
+
|
|
11
|
+
2026-07-02 * "Acme AG" "Consulting — July" ^INV2026014
|
|
12
|
+
Assets:CH:GmbH:Receivable:Trade 1081.00 CHF
|
|
13
|
+
Income:CH:GmbH:Consulting:External:Domestic -1000.00 CHF
|
|
14
|
+
Liabilities:CH:GmbH:Tax:OutputVAT -81.00 CHF
|
|
15
|
+
|
|
16
|
+
2026-07-20 * "Acme AG" "Payment INV2026014" ^INV2026014
|
|
17
|
+
Assets:CH:GmbH:Current:UBS:CHF 1081.00 CHF
|
|
18
|
+
Assets:CH:GmbH:Receivable:Trade -1081.00 CHF
|
|
19
|
+
|
|
20
|
+
2026-08-05 * "Globex Ltd" "Export consulting" ^INV2026015
|
|
21
|
+
Assets:CH:GmbH:Receivable:Trade 500.00 EUR
|
|
22
|
+
Income:CH:GmbH:Consulting:External:Export -500.00 EUR
|
|
23
|
+
|
|
24
|
+
2026-08-12 * "Foreign SaaS" "Cloud hosting (reverse charge)"
|
|
25
|
+
Expenses:CH:GmbH:IT:Hosting 100.00 EUR
|
|
26
|
+
Assets:CH:GmbH:Tax:InputVAT 7.53 CHF @@ 8.10 EUR
|
|
27
|
+
Liabilities:CH:GmbH:Tax:Bezugsteuer -7.53 CHF @@ 8.10 EUR
|
|
28
|
+
Assets:CH:GmbH:Current:Wise:EUR -100.00 EUR
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
; Currencies in use. Non-CHF commodities carry the beanprice source
|
|
2
|
+
; (official BAZG/EZV daily CHF rates) that `quints prices sync` and
|
|
3
|
+
; `bean-price` read.
|
|
4
|
+
|
|
5
|
+
2026-01-01 commodity CHF
|
|
6
|
+
2026-01-01 commodity EUR
|
|
7
|
+
price: "CHF:beanprice_bazg/EUR"
|
|
8
|
+
2026-01-01 commodity USD
|
|
9
|
+
price: "CHF:beanprice_bazg/USD"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Sample domestic invoice — a Swiss QR-bill. It ties to the ^INV2026014
|
|
2
|
+
# booking in books/2026.bean (net 1'000.00 + 8.1% VAT = 1'081.00), so
|
|
3
|
+
# `quints invoice` cross-checks it clean against the ledger.
|
|
4
|
+
number: INV2026014
|
|
5
|
+
kind: domestic
|
|
6
|
+
currency: CHF
|
|
7
|
+
issue_date: 2026-07-02
|
|
8
|
+
supply: Juli 2026
|
|
9
|
+
customer: acme
|
|
10
|
+
items:
|
|
11
|
+
- description: Consulting — July
|
|
12
|
+
quantity: 1
|
|
13
|
+
unit_price: 1000.00
|
|
14
|
+
unit: Pauschal
|
|
15
|
+
locale: de_CH
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Customer registry — invoices reference these entries by key. A customer
|
|
2
|
+
# is a flat entry, or a dated `versions` history for address changes.
|
|
3
|
+
acme:
|
|
4
|
+
name: Acme AG
|
|
5
|
+
address:
|
|
6
|
+
- Bahnhofstrasse 1
|
|
7
|
+
- 8001 Zürich
|
|
8
|
+
globex:
|
|
9
|
+
name: Globex Ltd
|
|
10
|
+
country: IE
|
|
11
|
+
# Reverse-charge exports must carry the customer's VAT number.
|
|
12
|
+
vat_id: IE1234567T
|
|
13
|
+
address:
|
|
14
|
+
- 1 Liffey Street
|
|
15
|
+
- Dublin 1
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Sample export invoice — foreign currency, reverse charge, no QR part.
|
|
2
|
+
# Ties to the ^INV2026015 booking in books/2026.bean (500.00 EUR).
|
|
3
|
+
number: INV2026015
|
|
4
|
+
kind: export
|
|
5
|
+
currency: EUR
|
|
6
|
+
issue_date: 2026-08-05
|
|
7
|
+
supply: August 2026
|
|
8
|
+
customer: globex
|
|
9
|
+
items:
|
|
10
|
+
- description: Export consulting
|
|
11
|
+
quantity: 1
|
|
12
|
+
unit_price: 500.00
|
|
13
|
+
locale: en
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Issuer identity for `quints invoice` — name, address, VAT ID, and one
|
|
2
|
+
# bank account per invoicing currency. Sample data: replace the VAT ID
|
|
3
|
+
# and IBANs with your own before issuing a real invoice.
|
|
4
|
+
name: Example GmbH
|
|
5
|
+
address:
|
|
6
|
+
- Beispielstrasse 1
|
|
7
|
+
- 8000 Zürich
|
|
8
|
+
vat_id: CHE-267.359.056 MWST
|
|
9
|
+
email: billing@example.ch
|
|
10
|
+
bank:
|
|
11
|
+
CHF:
|
|
12
|
+
# QR-IBAN (QR-IID variant) — a Swiss QR-bill with a QRR reference.
|
|
13
|
+
qr_iban: CH44 3199 9123 0008 8901 2
|
|
14
|
+
EUR:
|
|
15
|
+
# Regular IBAN — foreign transfers can't use the QR-bill scheme.
|
|
16
|
+
iban: CH93 0076 2011 6238 5295 7
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
; Example GmbH — books managed with quints (https://github.com/sealambda/quints)
|
|
2
|
+
;
|
|
3
|
+
; Generated by `quints init`. Layout:
|
|
4
|
+
; accounts.bean chart of accounts (KMU Kontenrahmen)
|
|
5
|
+
; commodities.bean currencies and their price sources
|
|
6
|
+
; prices.bean FX rates — refresh with `quints prices sync`
|
|
7
|
+
; books/2026.bean transactions, one file per fiscal year
|
|
8
|
+
; Validate with `quints check`; after `uv sync`, plain
|
|
9
|
+
; `bean-check main.bean` and `fava main.bean` work too.
|
|
10
|
+
|
|
11
|
+
plugin "quints.plugins.kmu" ":CH:GmbH:"
|
|
12
|
+
plugin "fava.plugins.link_documents"
|
|
13
|
+
|
|
14
|
+
2026-01-01 custom "fava-extension" "quints.fava"
|
|
15
|
+
|
|
16
|
+
include "accounts.bean"
|
|
17
|
+
include "commodities.bean"
|
|
18
|
+
include "prices.bean"
|
|
19
|
+
include "books/*.bean"
|
|
20
|
+
|
|
21
|
+
option "title" "Example GmbH"
|
|
22
|
+
option "operating_currency" "CHF"
|
|
23
|
+
option "documents" "documents/"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# `uv sync` installs quints — which brings beancount, fava, the statement
|
|
2
|
+
# importers, and the BAZG price source along as its own dependencies — so
|
|
3
|
+
# the standard beancount toolchain (`bean-check main.bean`, `fava
|
|
4
|
+
# main.bean`, `bean-query`) works in this repo, not only the quints CLI.
|
|
5
|
+
[project]
|
|
6
|
+
name = "example-gmbh"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"quints",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[tool.uv]
|
|
14
|
+
package = false
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# quints.toml — everything entity-specific lives here, next to main.bean.
|
|
2
|
-
# Account names
|
|
3
|
-
# uncomment and edit in quints.toml + main.bean together to change them.
|
|
2
|
+
# Account names mirror accounts.bean; change them in both files together.
|
|
4
3
|
|
|
5
4
|
[entity]
|
|
6
5
|
name = "Example GmbH"
|
|
6
|
+
legal_form = "gmbh" # gmbh | ag | einzelfirma
|
|
7
7
|
vat_method = "effective" # "saldo" is not supported yet
|
|
8
8
|
vat_registered_since = 2026-01-01 # earlier periods are pre-liability
|
|
9
9
|
operating_currency = "CHF"
|
|
@@ -12,6 +12,21 @@ operating_currency = "CHF"
|
|
|
12
12
|
main = "main.bean"
|
|
13
13
|
prices = "prices.bean"
|
|
14
14
|
|
|
15
|
+
[accounts]
|
|
16
|
+
entity_marker = ":CH:GmbH:" # scopes KMU statements and the plugin
|
|
17
|
+
input_vat = "Assets:CH:GmbH:Tax:InputVAT"
|
|
18
|
+
output_vat = "Liabilities:CH:GmbH:Tax:OutputVAT"
|
|
19
|
+
bezugsteuer = "Liabilities:CH:GmbH:Tax:Bezugsteuer"
|
|
20
|
+
payable_vat = "Liabilities:CH:GmbH:Tax:PayableVAT"
|
|
21
|
+
receivable = "Assets:CH:GmbH:Receivable:Trade"
|
|
22
|
+
income_prefix = "Income:CH:GmbH"
|
|
23
|
+
export_marker = ":Export" # income sub-account marker → Ziffer 221
|
|
24
|
+
income_domestic = "Income:CH:GmbH:Consulting:External:Domestic"
|
|
25
|
+
income_export = "Income:CH:GmbH:Consulting:External:Export"
|
|
26
|
+
fx_gain = "Income:CH:GmbH:FX:CurrencyGain"
|
|
27
|
+
fx_loss = "Expenses:CH:GmbH:FX:CurrencyLoss"
|
|
28
|
+
rounding_income = "Income:CH:GmbH:Rounding"
|
|
29
|
+
|
|
15
30
|
[report]
|
|
16
31
|
language = "en" # or "de"; --lang overrides
|
|
17
32
|
|
|
@@ -42,7 +57,7 @@ USD = "Assets:CH:GmbH:Current:Wise:USD"
|
|
|
42
57
|
[import.stripe]
|
|
43
58
|
account_id = "acct_XXXXXXXXXXXX" # guard: refuse a key for another account
|
|
44
59
|
fees_account = "Expenses:CH:GmbH:BankFees:Stripe"
|
|
45
|
-
tax_account = "Assets:CH:GmbH:Tax:InputVAT" # VAT inside Stripe's
|
|
60
|
+
tax_account = "Assets:CH:GmbH:Tax:InputVAT" # VAT inside Stripe's own fees
|
|
46
61
|
rules = [
|
|
47
62
|
['\bpayout\b', "Assets:CH:GmbH:Current:Wise:EUR", "*"],
|
|
48
63
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "quints"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Swiss VAT & accounting toolkit for plain-text (beancount) books: MWST reports and quarterly settlement, Bezugsteuer helpers, BAZG daily FX rates, QR-bill invoicing, KMU statutory statements, and statement importers (UBS MT940, Wise, Stripe) that draft into a staging area"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -15,7 +15,7 @@ dependencies = [
|
|
|
15
15
|
"beangulp-mt940>=0.1.0",
|
|
16
16
|
"beangulp-stripe>=0.1.0",
|
|
17
17
|
"beangulp-wise[sca]>=0.1.0",
|
|
18
|
-
"beanprice-bazg>=0.
|
|
18
|
+
"beanprice-bazg>=0.2.0",
|
|
19
19
|
"fava>=1.30",
|
|
20
20
|
"pydantic>=2.7",
|
|
21
21
|
"python-stdnum>=1.20",
|
|
@@ -330,8 +330,37 @@ def prices_sync(
|
|
|
330
330
|
Without --from: extend each currency forward to today (fast, daily use).
|
|
331
331
|
With --from DATE: re-scan the whole range and fill any missing days.
|
|
332
332
|
"""
|
|
333
|
+
from rich.progress import (
|
|
334
|
+
BarColumn,
|
|
335
|
+
MofNCompleteColumn,
|
|
336
|
+
Progress,
|
|
337
|
+
TaskID,
|
|
338
|
+
TextColumn,
|
|
339
|
+
TimeRemainingColumn,
|
|
340
|
+
)
|
|
341
|
+
|
|
333
342
|
repair = _parse_date(from_) if from_ else None
|
|
334
|
-
|
|
343
|
+
# BAZG has no bulk endpoint — the fetch is one request per calendar day per
|
|
344
|
+
# currency, so a backfill takes a while: show a live per-currency bar
|
|
345
|
+
# (stderr, transient) unless the caller asked for machine-readable output.
|
|
346
|
+
with Progress(
|
|
347
|
+
TextColumn("[progress.description]{task.description}"),
|
|
348
|
+
BarColumn(),
|
|
349
|
+
MofNCompleteColumn(),
|
|
350
|
+
TextColumn("days"),
|
|
351
|
+
TimeRemainingColumn(),
|
|
352
|
+
console=ui.err_console,
|
|
353
|
+
transient=True,
|
|
354
|
+
disable=as_json,
|
|
355
|
+
) as bars:
|
|
356
|
+
tasks: dict[str, TaskID] = {}
|
|
357
|
+
|
|
358
|
+
def on_progress(ccy: str, done: int, total: int) -> None:
|
|
359
|
+
if ccy not in tasks:
|
|
360
|
+
tasks[ccy] = bars.add_task(ccy, total=total)
|
|
361
|
+
bars.update(tasks[ccy], completed=done)
|
|
362
|
+
|
|
363
|
+
result = prices_mod.sync(out, repair_from=repair, progress=on_progress)
|
|
335
364
|
if as_json:
|
|
336
365
|
_json_out(
|
|
337
366
|
{
|
|
@@ -801,6 +830,11 @@ def init(
|
|
|
801
830
|
Path("."), help="Target project directory (created if missing)."
|
|
802
831
|
),
|
|
803
832
|
name: str | None = typer.Option(None, "--name", help="Entity name, e.g. 'Acme GmbH'."),
|
|
833
|
+
legal_form: str | None = typer.Option(
|
|
834
|
+
None,
|
|
835
|
+
"--legal-form",
|
|
836
|
+
help="Legal form: gmbh, ag, or einzelfirma (sole proprietorship / freelancer).",
|
|
837
|
+
),
|
|
804
838
|
lang: str | None = typer.Option(None, "--lang", "-l", help="Report language: en or de."),
|
|
805
839
|
importers: str | None = typer.Option(
|
|
806
840
|
None, "--importers", help="Comma-separated: ubs, wise, stripe (default: none)."
|
|
@@ -815,7 +849,8 @@ def init(
|
|
|
815
849
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip prompts; accept defaults."),
|
|
816
850
|
as_json: bool = typer.Option(False, "--json", help="Machine-readable output."),
|
|
817
851
|
):
|
|
818
|
-
"""Scaffold a new quints project (chart of accounts,
|
|
852
|
+
"""Scaffold a new quints project (chart of accounts, per-year books,
|
|
853
|
+
quints.toml, pyproject.toml, AGENTS.md).
|
|
819
854
|
|
|
820
855
|
Deterministic: the same answers always produce the same files. Run
|
|
821
856
|
interactively, or feed a TOML answer-file with --answers for CI/repeatable
|
|
@@ -829,6 +864,22 @@ def init(
|
|
|
829
864
|
answers = init_mod.Answers()
|
|
830
865
|
|
|
831
866
|
interactive = answers_file is None and not yes
|
|
867
|
+
if legal_form is not None:
|
|
868
|
+
answers = replace(answers, legal_form=legal_form.strip().lower())
|
|
869
|
+
elif interactive:
|
|
870
|
+
answers = replace(
|
|
871
|
+
answers,
|
|
872
|
+
legal_form=typer.prompt("Legal form (gmbh/ag/einzelfirma)", default=answers.legal_form)
|
|
873
|
+
.strip()
|
|
874
|
+
.lower(),
|
|
875
|
+
)
|
|
876
|
+
# An answer-file's entity name is authoritative; otherwise suggest one
|
|
877
|
+
# that matches the chosen legal form instead of the GmbH default.
|
|
878
|
+
example_names = {"gmbh": "Example GmbH", "ag": "Example AG", "einzelfirma": "Jane Doe"}
|
|
879
|
+
if answers_file is None and answers.entity_name == init_mod.Answers.entity_name:
|
|
880
|
+
answers = replace(
|
|
881
|
+
answers, entity_name=example_names.get(answers.legal_form, answers.entity_name)
|
|
882
|
+
)
|
|
832
883
|
if name is not None:
|
|
833
884
|
answers = replace(answers, entity_name=name)
|
|
834
885
|
elif interactive:
|
|
@@ -875,7 +926,7 @@ def init(
|
|
|
875
926
|
if result.written and not result.skipped:
|
|
876
927
|
ui.console.print(
|
|
877
928
|
f"\nScaffolded [b]{answers.entity_name}[/] in {directory}. "
|
|
878
|
-
"Next: [b]quints check[/]
|
|
929
|
+
"Next: [b]uv sync[/], then [b]quints check[/] and [b]quints mwst -q 2026-Q3[/]."
|
|
879
930
|
)
|
|
880
931
|
|
|
881
932
|
|
|
@@ -28,6 +28,16 @@ else: # pragma: no cover
|
|
|
28
28
|
|
|
29
29
|
DEFAULT_PATH = Path("quints.toml")
|
|
30
30
|
|
|
31
|
+
# Legal form → account-name component (Assets:CH:<component>:…). The keys are
|
|
32
|
+
# the entity families the official KMU Kontenrahmen (veb.ch) prints Klasse-28
|
|
33
|
+
# variants for: juristische Personen (gmbh, ag) and Einzelunternehmen;
|
|
34
|
+
# Personengesellschaft (per-partner capital blocks) is not supported yet.
|
|
35
|
+
LEGAL_FORMS: dict[str, str] = {
|
|
36
|
+
"gmbh": "GmbH",
|
|
37
|
+
"ag": "AG",
|
|
38
|
+
"einzelfirma": "Einzelfirma",
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
|
|
32
42
|
@dataclass(frozen=True)
|
|
33
43
|
class UbsImport:
|
|
@@ -76,6 +86,7 @@ class StripeImport:
|
|
|
76
86
|
class Config:
|
|
77
87
|
# [entity]
|
|
78
88
|
entity_name: str = "Example GmbH"
|
|
89
|
+
legal_form: str = "gmbh" # key into LEGAL_FORMS; picks the Klasse-28 equity variant
|
|
79
90
|
vat_method: str = "effective" # "saldo" would need a different Form-310 mapping
|
|
80
91
|
vat_registered_since: Date | None = None
|
|
81
92
|
operating_currency: str = "CHF"
|
|
@@ -159,6 +170,7 @@ def _from_mapping(raw: dict) -> Config:
|
|
|
159
170
|
updates[field] = cast(value) if cast else value
|
|
160
171
|
|
|
161
172
|
take(entity, "name", "entity_name")
|
|
173
|
+
take(entity, "legal_form", "legal_form")
|
|
162
174
|
take(entity, "vat_method", "vat_method")
|
|
163
175
|
take(entity, "vat_registered_since", "vat_registered_since")
|
|
164
176
|
take(entity, "operating_currency", "operating_currency")
|