akt-cli 0.4.0__tar.gz → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akt-cli
3
- Version: 0.4.0
3
+ Version: 0.5.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
@@ -171,6 +171,15 @@ akt tax create --name "Sales Tax" --rate 8.25
171
171
  akt bank create --name "Business Checking" --number 1001 --currency-code USD
172
172
  akt bank list
173
173
 
174
+ # Open a book with a prior-period opening balance. The Double-Entry module
175
+ # auto-posts an opening-balance journal entry dated to the account's creation
176
+ # date; --opening-balance-date re-dates it to the period boundary so it lands in
177
+ # the right financial year. Needs a positive --opening-balance.
178
+ akt bank create --name "Business Checking" --number 1001 --currency-code USD \
179
+ --opening-balance 5000 --opening-balance-date 2024-12-31
180
+ # Re-date the opening entry of an existing account:
181
+ akt bank update 3 --opening-balance-date 2024-12-31
182
+
174
183
  # Invoice with line items (totals computed server-side; number auto-generated)
175
184
  akt invoice create --contact 12 \
176
185
  --item 'name=Consulting,price=150,quantity=10,item_id=2' \
@@ -150,6 +150,15 @@ akt tax create --name "Sales Tax" --rate 8.25
150
150
  akt bank create --name "Business Checking" --number 1001 --currency-code USD
151
151
  akt bank list
152
152
 
153
+ # Open a book with a prior-period opening balance. The Double-Entry module
154
+ # auto-posts an opening-balance journal entry dated to the account's creation
155
+ # date; --opening-balance-date re-dates it to the period boundary so it lands in
156
+ # the right financial year. Needs a positive --opening-balance.
157
+ akt bank create --name "Business Checking" --number 1001 --currency-code USD \
158
+ --opening-balance 5000 --opening-balance-date 2024-12-31
159
+ # Re-date the opening entry of an existing account:
160
+ akt bank update 3 --opening-balance-date 2024-12-31
161
+
153
162
  # Invoice with line items (totals computed server-side; number auto-generated)
154
163
  akt invoice create --contact 12 \
155
164
  --item 'name=Consulting,price=150,quantity=10,item_id=2' \
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "akt-cli"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "akt — a CLI toolbox to fully drive an Akaunting accounting instance"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -74,7 +74,11 @@ def parse_coa(text: str) -> CoaConfig:
74
74
  code = int(row["code"])
75
75
  name = str(row["name"])
76
76
  type_id = int(row["type_id"])
77
- mirror = bool(row.get("mirror", True))
77
+ mirror = row.get("mirror", True)
78
+ if not isinstance(mirror, bool):
79
+ raise ValueError(
80
+ f"account '{name}': 'mirror' must be a boolean (true/false), "
81
+ f"got {mirror!r} — did you quote it?")
78
82
  category = str(row.get("category", name))
79
83
  if code in seen_codes:
80
84
  raise ValueError(f"duplicate code {code}")
@@ -56,6 +56,8 @@ def cmd_create(res: Resource, client: Client, ns: Any) -> int:
56
56
  else:
57
57
  payload = client.post(endpoint, body, type_scope=type_scope)
58
58
  data = payload.get("data", payload) if isinstance(payload, dict) else payload
59
+ if res.post_write:
60
+ res.post_write(res, client, data, ns)
59
61
  emit(data, as_json=True)
60
62
  return 0
61
63
 
@@ -104,6 +106,8 @@ def cmd_update(res: Resource, client: Client, ns: Any) -> int:
104
106
  body["remove_attachment"] = 1
105
107
  payload = client.put(path, body, type_scope=type_scope)
106
108
  data = payload.get("data", payload) if isinstance(payload, dict) else payload
109
+ if res.post_write:
110
+ res.post_write(res, client, data, ns)
107
111
  emit(data, as_json=True)
108
112
  return 0
109
113
 
@@ -13,6 +13,7 @@ from .resources import (
13
13
  build_journal_update,
14
14
  build_payment_create,
15
15
  build_transfer_create,
16
+ redate_opening_balance,
16
17
  resolve_payment_delete,
17
18
  resolve_payment_update,
18
19
  )
@@ -101,6 +102,10 @@ BANK = Resource(
101
102
  f("type", "Account type", default="bank"),
102
103
  f("currency-code", "Currency code", default="USD"),
103
104
  f("opening-balance", "Opening balance", default=0),
105
+ f("opening-balance-date",
106
+ "Date for the opening-balance journal entry (YYYY-MM-DD). Re-dates the "
107
+ "entry the Double-Entry module auto-posts; needs a positive opening balance.",
108
+ dest="opening_balance_date", local=True),
104
109
  f("bank-name", "Bank name"),
105
110
  f("bank-phone", "Bank phone"),
106
111
  f("bank-address", "Bank address"),
@@ -112,6 +117,7 @@ BANK = Resource(
112
117
  ("Enabled", "enabled"),
113
118
  ],
114
119
  help="Bank / cash accounts",
120
+ post_write=redate_opening_balance,
115
121
  )
116
122
 
117
123
  CATEGORY = Resource(
@@ -12,6 +12,7 @@ import datetime as _dt
12
12
  import json
13
13
  import mimetypes
14
14
  import os
15
+ import sys
15
16
  from dataclasses import dataclass, field
16
17
  from typing import Any, Callable
17
18
 
@@ -32,6 +33,7 @@ class Field:
32
33
  default: Any = None # default applied on create when omitted
33
34
  is_flag: bool = False # store_true boolean flag
34
35
  choices: list[str] | None = None
36
+ local: bool = False # CLI flag only; never sent in the request body
35
37
 
36
38
 
37
39
  def f(name: str, help: str = "", **kw) -> Field:
@@ -69,6 +71,10 @@ class Resource:
69
71
  # returns (path, type_scope) for update; lets document-linked payments use
70
72
  # the nested route (the flat /transactions route 400s on any document_id)
71
73
  update_resolver: Callable[["Resource", Client, str, dict], "tuple[str, str | None]"] | None = None
74
+ # runs after a successful create/update on the /api path, with the returned
75
+ # record + the argparse namespace; used by bank to re-date the auto-posted
76
+ # opening-balance journal entry.
77
+ post_write: Callable[["Resource", Client, dict, Any], None] | None = None
72
78
 
73
79
  def contact_scope(self) -> str:
74
80
  """ACL scope of the contact tied to a document resource."""
@@ -205,6 +211,8 @@ def body_from_fields(res: Resource, ns: Any, *, for_update: bool,
205
211
  """
206
212
  body: dict[str, Any] = {}
207
213
  for fld in res.fields:
214
+ if fld.local:
215
+ continue
208
216
  val = getattr(ns, fld.dest, None)
209
217
  if fld.is_flag:
210
218
  # tri-state: only include if explicitly toggled
@@ -733,6 +741,76 @@ def build_journal_update(res: Resource, client: Client, ns: Any, current: dict)
733
741
  return body
734
742
 
735
743
 
744
+ # --------------------------------------------------------------------------
745
+ # opening-balance journal entry re-dating (bank post_write hook)
746
+ # --------------------------------------------------------------------------
747
+
748
+ def _journal_reput_body(current: dict, *, paid_at: str, journal_number: str) -> dict:
749
+ """Full journal-entry PUT body that re-dates an existing entry, preserving
750
+ its ledgers (by id) and amounts. ``journal_number`` is supplied by the caller
751
+ (the server-created opening-balance entry carries none, but the API requires
752
+ one)."""
753
+ body: dict[str, Any] = {
754
+ "paid_at": paid_at,
755
+ "journal_number": journal_number,
756
+ "description": current.get("description"),
757
+ "basis": current.get("basis") or "accrual",
758
+ "currency_code": current.get("currency_code") or "USD",
759
+ "currency_rate": current.get("currency_rate", 1) or 1,
760
+ "amount": 0,
761
+ "items": _journal_items_from_current(current),
762
+ }
763
+ if current.get("reference"):
764
+ body["reference"] = current["reference"]
765
+ return body
766
+
767
+
768
+ def _find_opening_balance_je(client: Client, record: dict) -> dict | None:
769
+ """Locate the opening-balance journal entry the Double-Entry module auto-posts
770
+ for a bank/cash account. It carries ``reference = "opening-balance:<coa_id>"``
771
+ and ``description`` ending ``";<account name>"``. The chart-of-accounts API
772
+ doesn't expose the bank<->coa link, so match on those two fields and, if an
773
+ account name is duplicated, take the newest by ``created_at``."""
774
+ name = str(record.get("name") or "")
775
+ suffix = ";" + name
776
+ matches = [
777
+ e for e in client.list("journal-entry", all_pages=True)
778
+ if str(e.get("reference") or "").startswith("opening-balance:")
779
+ and str(e.get("description") or "").endswith(suffix)
780
+ ]
781
+ if not matches:
782
+ return None
783
+ matches.sort(key=lambda e: str(e.get("created_at") or ""), reverse=True)
784
+ return matches[0]
785
+
786
+
787
+ def redate_opening_balance(res: "Resource", client: Client, record: dict, ns: Any) -> None:
788
+ """post_write hook for bank create/update: re-date the auto-posted
789
+ opening-balance journal entry to ``--opening-balance-date``.
790
+
791
+ The Double-Entry module stamps that entry with the account's created_at and
792
+ exposes no date field, so we set the balance normally (keeping the Banking
793
+ register correct) and rewrite the entry's date here. Re-dating via the
794
+ journal-entry route also rewrites each ledger's issued_at (what reports
795
+ filter on), and the module's account-update path only touches ledger
796
+ amounts, so the date sticks across later edits."""
797
+ date = getattr(ns, "opening_balance_date", None)
798
+ if not date:
799
+ return
800
+ je = _find_opening_balance_je(client, record)
801
+ if je is None:
802
+ print(f"note: no opening-balance journal entry found for {res.noun} "
803
+ f"{record.get('id')}; --opening-balance-date had no effect "
804
+ f"(opening balance is 0, or the Double-Entry module / owners-"
805
+ f"contribution account is not configured)", file=sys.stderr)
806
+ return
807
+ number = je.get("journal_number") or _next_journal_number(client)
808
+ body = _journal_reput_body(je, paid_at=_normalize_date(date), journal_number=number)
809
+ client.put(f"journal-entry/{je['id']}", body)
810
+ print(f"note: re-dated opening-balance journal entry {je['id']} to {date}",
811
+ file=sys.stderr)
812
+
813
+
736
814
  # --------------------------------------------------------------------------
737
815
  # chart-of-accounts (double-entry) — web-surface CRUD body builder
738
816
  # --------------------------------------------------------------------------
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes