akt-cli 0.8.2__tar.gz → 0.9.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.
- {akt_cli-0.8.2 → akt_cli-0.9.0}/PKG-INFO +22 -1
- {akt_cli-0.8.2 → akt_cli-0.9.0}/README.md +21 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/pyproject.toml +1 -1
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/cli.py +9 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/registry.py +2 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/resources.py +91 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/LICENSE +0 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/__init__.py +0 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/client.py +0 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/coa.py +0 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/commands.py +0 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/config.py +0 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/ledger.py +0 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/output.py +0 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/reports.py +0 -0
- {akt_cli-0.8.2 → akt_cli-0.9.0}/src/akt/verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: akt-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: akt — a CLI toolbox to fully drive an Akaunting accounting instance
|
|
5
5
|
Keywords: akaunting,accounting,cli,invoices,bills,api
|
|
6
6
|
Author: AsyncAlchemist
|
|
@@ -265,6 +265,27 @@ agree. `--account` takes a GL code or name; `--category` a mirror-category name
|
|
|
265
265
|
(see `--bank` for the bank/cash account). An explicit `--set de_account_id=` still
|
|
266
266
|
wins.
|
|
267
267
|
|
|
268
|
+
### Split one transaction across several GL accounts
|
|
269
|
+
|
|
270
|
+
`--split` posts **one bank transaction to multiple GL accounts at once** — the way
|
|
271
|
+
an invoice's line items each hit their own account. Repeat it per leg as
|
|
272
|
+
`account=<code|name>,debit=<x>` or `account=<code|name>,credit=<x>`; the legs must
|
|
273
|
+
net to `--amount` (income legs credit-heavy, expense legs debit-heavy):
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# one $13,400 PayPro deposit that books gross revenue, a refund and a fee together
|
|
277
|
+
akt payment create --type income --bank 20 --amount 13400 \
|
|
278
|
+
--split 'account=400,credit=13686' \
|
|
279
|
+
--split 'account=545,debit=280' \
|
|
280
|
+
--split 'account=605,debit=6'
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Needs the [`akt-api` companion module](akt-api/) — it writes the extra DoubleEntry
|
|
284
|
+
item legs (a standalone transaction can't natively carry more than one). The
|
|
285
|
+
transaction keeps a **single `category_id`**, a label just like a multi-item
|
|
286
|
+
invoice's, so Akaunting's native category report shows it under one bucket; the
|
|
287
|
+
**DoubleEntry GL (the item legs) is the source of truth**.
|
|
288
|
+
|
|
268
289
|
**Enforcement (when a `--coa` config is loaded):** akt refuses to create a
|
|
269
290
|
*standalone* income/expense transaction that has no GL account — you must pass
|
|
270
291
|
`--account`, `--category`, or an explicit `--set de_account_id=`. This makes the
|
|
@@ -244,6 +244,27 @@ agree. `--account` takes a GL code or name; `--category` a mirror-category name
|
|
|
244
244
|
(see `--bank` for the bank/cash account). An explicit `--set de_account_id=` still
|
|
245
245
|
wins.
|
|
246
246
|
|
|
247
|
+
### Split one transaction across several GL accounts
|
|
248
|
+
|
|
249
|
+
`--split` posts **one bank transaction to multiple GL accounts at once** — the way
|
|
250
|
+
an invoice's line items each hit their own account. Repeat it per leg as
|
|
251
|
+
`account=<code|name>,debit=<x>` or `account=<code|name>,credit=<x>`; the legs must
|
|
252
|
+
net to `--amount` (income legs credit-heavy, expense legs debit-heavy):
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# one $13,400 PayPro deposit that books gross revenue, a refund and a fee together
|
|
256
|
+
akt payment create --type income --bank 20 --amount 13400 \
|
|
257
|
+
--split 'account=400,credit=13686' \
|
|
258
|
+
--split 'account=545,debit=280' \
|
|
259
|
+
--split 'account=605,debit=6'
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Needs the [`akt-api` companion module](akt-api/) — it writes the extra DoubleEntry
|
|
263
|
+
item legs (a standalone transaction can't natively carry more than one). The
|
|
264
|
+
transaction keeps a **single `category_id`**, a label just like a multi-item
|
|
265
|
+
invoice's, so Akaunting's native category report shows it under one bucket; the
|
|
266
|
+
**DoubleEntry GL (the item legs) is the source of truth**.
|
|
267
|
+
|
|
247
268
|
**Enforcement (when a `--coa` config is loaded):** akt refuses to create a
|
|
248
269
|
*standalone* income/expense transaction that has no GL account — you must pass
|
|
249
270
|
`--account`, `--category`, or an explicit `--set de_account_id=`. This makes the
|
|
@@ -48,6 +48,15 @@ def _add_field_args(p: argparse.ArgumentParser, res: Resource, *, for_update: bo
|
|
|
48
48
|
p.add_argument("--item", action="append", metavar="K=V,...",
|
|
49
49
|
help="ledger line (>= 2, must balance), e.g. "
|
|
50
50
|
"'account_id=10,debit=100' or 'account_id=20,credit=100' (repeatable)")
|
|
51
|
+
elif res.endpoint == "transactions" and not for_update:
|
|
52
|
+
p.add_argument("--split", action="append", metavar="account=CODE,debit|credit=X",
|
|
53
|
+
help="split the GL posting across multiple accounts (repeatable): "
|
|
54
|
+
"one bank transaction that posts N GL item legs, the way an "
|
|
55
|
+
"invoice's line items do. e.g. 'account=400,credit=13686' "
|
|
56
|
+
"'account=545,debit=280'. Legs must net to --amount; needs a "
|
|
57
|
+
"--coa config and the akt-api companion module. category_id "
|
|
58
|
+
"stays a single label — the DoubleEntry GL (item legs) is the "
|
|
59
|
+
"source of truth.")
|
|
51
60
|
if res.supports_attachments:
|
|
52
61
|
p.add_argument("--attachment", action="append", metavar="PATH",
|
|
53
62
|
help="attach a file (pdf/jpg/png, repeatable); switches the "
|
|
@@ -16,6 +16,7 @@ from .resources import (
|
|
|
16
16
|
redate_opening_balance,
|
|
17
17
|
resolve_payment_delete,
|
|
18
18
|
resolve_payment_update,
|
|
19
|
+
split_payment_legs,
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
# Common column sets
|
|
@@ -251,6 +252,7 @@ PAYMENT = Resource(
|
|
|
251
252
|
build_create=build_payment_create,
|
|
252
253
|
delete_resolver=resolve_payment_delete,
|
|
253
254
|
update_resolver=resolve_payment_update,
|
|
255
|
+
post_write=split_payment_legs,
|
|
254
256
|
help="Payments / transactions (income & expense)",
|
|
255
257
|
)
|
|
256
258
|
|
|
@@ -436,6 +436,33 @@ def build_document_update(res: Resource, client: Client, ns: Any, current: dict)
|
|
|
436
436
|
# payment (transaction) body builder
|
|
437
437
|
# --------------------------------------------------------------------------
|
|
438
438
|
|
|
439
|
+
def parse_split_leg(spec: str) -> dict:
|
|
440
|
+
"""Parse one ``--split`` leg: ``account=<code|name>,debit=<x>`` or
|
|
441
|
+
``account=<code|name>,credit=<x>``. Exactly one of debit/credit; ``account`` is a
|
|
442
|
+
GL code or name resolved against the COA (like ``--account``). Returns
|
|
443
|
+
``{'account': str, 'debit'|'credit': float}``."""
|
|
444
|
+
item: dict[str, Any] = {}
|
|
445
|
+
for part in spec.split(","):
|
|
446
|
+
if not part.strip():
|
|
447
|
+
continue
|
|
448
|
+
if "=" not in part:
|
|
449
|
+
raise ValueError(f"--split field must be key=value, got {part!r}")
|
|
450
|
+
k, _, v = part.partition("=")
|
|
451
|
+
item[k.strip()] = v.strip()
|
|
452
|
+
if not item.get("account"):
|
|
453
|
+
raise ValueError(f"--split leg requires an account=<code|name> field: {spec!r}")
|
|
454
|
+
has_d = bool(item.get("debit"))
|
|
455
|
+
has_c = bool(item.get("credit"))
|
|
456
|
+
if has_d == has_c:
|
|
457
|
+
raise ValueError(f"--split leg requires exactly one of debit=/credit=: {spec!r}")
|
|
458
|
+
leg = {"account": item["account"]}
|
|
459
|
+
if has_d:
|
|
460
|
+
leg["debit"] = float(item["debit"])
|
|
461
|
+
else:
|
|
462
|
+
leg["credit"] = float(item["credit"])
|
|
463
|
+
return leg
|
|
464
|
+
|
|
465
|
+
|
|
439
466
|
def build_payment_create(res: Resource, client: Client, ns: Any) -> dict:
|
|
440
467
|
invoice_id = getattr(ns, "invoice", None)
|
|
441
468
|
bill_id = getattr(ns, "bill", None)
|
|
@@ -462,6 +489,33 @@ def build_payment_create(res: Resource, client: Client, ns: Any) -> dict:
|
|
|
462
489
|
if category_id is None: # explicit --category-id wins
|
|
463
490
|
category_id = coa_category_id
|
|
464
491
|
|
|
492
|
+
# --split: one bank transaction posting N GL item legs (like an invoice's line
|
|
493
|
+
# items). Resolve each leg's account to a de_account_id; the placeholder item
|
|
494
|
+
# leg uses the first leg's account, then the split_payment_legs post_write hook
|
|
495
|
+
# fans it out into the N legs via the akt-api endpoint. category_id stays a
|
|
496
|
+
# single representative label (mirrors invoice behavior); the GL is the truth.
|
|
497
|
+
split_specs = getattr(ns, "split", None)
|
|
498
|
+
split_legs = None
|
|
499
|
+
if split_specs:
|
|
500
|
+
if account_ref or category_ref:
|
|
501
|
+
raise ValueError("pass either --split or --account/--category, not both")
|
|
502
|
+
if coa is None:
|
|
503
|
+
raise ValueError("--split requires a COA config (pass --coa FILE or set AKT_COA_FILE)")
|
|
504
|
+
split_legs = []
|
|
505
|
+
for spec in split_specs:
|
|
506
|
+
leg = parse_split_leg(spec)
|
|
507
|
+
leg_de_account_id, leg_category_id = resolve_coding(coa, client, account_ref=leg["account"])
|
|
508
|
+
resolved = {"account_id": leg_de_account_id}
|
|
509
|
+
if "debit" in leg:
|
|
510
|
+
resolved["debit"] = leg["debit"]
|
|
511
|
+
else:
|
|
512
|
+
resolved["credit"] = leg["credit"]
|
|
513
|
+
split_legs.append(resolved)
|
|
514
|
+
if category_id is None: # first leg's category = representative label
|
|
515
|
+
category_id = leg_category_id
|
|
516
|
+
de_account_id = split_legs[0]["account_id"] # placeholder item leg
|
|
517
|
+
ns._split_resolved = split_legs
|
|
518
|
+
|
|
465
519
|
if invoice_id:
|
|
466
520
|
document = client.show("documents", invoice_id, type_scope="invoice")
|
|
467
521
|
ptype = ptype or "income"
|
|
@@ -503,6 +557,17 @@ def build_payment_create(res: Resource, client: Client, ns: Any) -> dict:
|
|
|
503
557
|
if amount is None:
|
|
504
558
|
raise ValueError("--amount is required")
|
|
505
559
|
|
|
560
|
+
# --split balance: the legs must net to the transaction's single item leg, which
|
|
561
|
+
# mirrors --amount and the bank leg. Income posts the item leg as a credit of
|
|
562
|
+
# --amount (net -amount); expense as a debit (net +amount). Fail fast client-side.
|
|
563
|
+
if split_legs is not None:
|
|
564
|
+
expected = float(amount) if ptype == "expense" else -float(amount)
|
|
565
|
+
net = sum(l.get("debit", 0) - l.get("credit", 0) for l in split_legs)
|
|
566
|
+
if abs(net - expected) > 0.005:
|
|
567
|
+
raise ValueError(
|
|
568
|
+
f"--split legs net {net:.2f} but --amount {amount} (type {ptype}) needs "
|
|
569
|
+
f"{expected:.2f} — income legs must be credit-heavy, expense debit-heavy")
|
|
570
|
+
|
|
506
571
|
account_id = getattr(ns, "account_id", None)
|
|
507
572
|
if account_id is None:
|
|
508
573
|
val = client.setting("default.account")
|
|
@@ -576,6 +641,32 @@ def resolve_payment_update(res: Resource, client: Client, ident: str,
|
|
|
576
641
|
return f"transactions/{ident}", None
|
|
577
642
|
|
|
578
643
|
|
|
644
|
+
def split_payment_legs(res: Resource, client: Client, record: dict, ns: Any) -> None:
|
|
645
|
+
"""post_write hook for ``payment create --split``: after the transaction is
|
|
646
|
+
created (with a single placeholder item leg), fan that leg out into the N
|
|
647
|
+
resolved legs via the akt-api ``POST /ledgers/{id}/split`` endpoint. No-op unless
|
|
648
|
+
``--split`` was given. Requires the akt-api companion module."""
|
|
649
|
+
legs = getattr(ns, "_split_resolved", None)
|
|
650
|
+
if not legs:
|
|
651
|
+
return
|
|
652
|
+
if not client.has_ledger_api():
|
|
653
|
+
raise ValueError(
|
|
654
|
+
"--split needs the akt-api companion module (GET /api/akt-api/ledgers is "
|
|
655
|
+
"404). Deploy it (scripts/deploy-akt-api.sh) and retry.")
|
|
656
|
+
txn_id = record.get("id")
|
|
657
|
+
payload = client.get("akt-api/ledgers", params={
|
|
658
|
+
"ledgerable_type": "App\\Models\\Banking\\Transaction",
|
|
659
|
+
"ledgerable_id": txn_id,
|
|
660
|
+
"entry_type": "item",
|
|
661
|
+
})
|
|
662
|
+
rows = payload.get("data", payload) if isinstance(payload, dict) else payload
|
|
663
|
+
if len(rows) != 1:
|
|
664
|
+
raise ValueError(
|
|
665
|
+
f"expected exactly one item leg on transaction {txn_id} to split, found "
|
|
666
|
+
f"{len(rows)}")
|
|
667
|
+
client.post(f"akt-api/ledgers/{rows[0]['id']}/split", {"legs": legs})
|
|
668
|
+
|
|
669
|
+
|
|
579
670
|
def build_transfer_create(res: Resource, client: Client, ns: Any) -> dict:
|
|
580
671
|
body = body_from_fields(res, ns, for_update=False)
|
|
581
672
|
# Transfers validate transferred_at as date-only (Y-m-d), unlike transactions.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|