fix: Windows reads of lock files, update paths cfg(unix), merge main again (P11c)
spec_vault swept every file under a book, lock files included; on Windows a held LockFileEx lock is mandatory and the read failed. The locks are already dedicated files (never data), so the test skips *.lock. Merged main (P13 step 6, the macOS server.log assert, P11a's AppImage identity): host.rs keeps main's five-field identity on Unix and size/last-write on Windows; the update poll and set_program_version are Unix-only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
commit
7749afc557
234 files changed
+13028
-18411
No files matched your search
@@ -3,8 +3,11 @@
|
|||||||
# program linked on a newer one does not start on an older one), attach
|
# program linked on a newer one does not start on an older one), attach
|
||||||
# them to the GitHub release, and add them to the apt and dnf repositories
|
# them to the GitHub release, and add them to the apt and dnf repositories
|
||||||
# on the gh-pages branch (GitHub Pages serves them). Unsigned: step 5 of
|
# on the gh-pages branch (GitHub Pages serves them). Unsigned: step 5 of
|
||||||
# the unit (the release key, Q-4) signs the manifest and the indexes, and
|
# the unit (the release key, Q-4) signs the manifest and the indexes. The
|
||||||
# step 6 adds the macOS and Windows jobs. The release holds no key today.
|
# macOS job builds the .dmg UNSIGNED (no Developer ID, no notarization:
|
||||||
|
# step 5); the Windows job is unit P11c. The release holds no key today.
|
||||||
|
# To see the artifacts without a tag: Actions > release > Run workflow,
|
||||||
|
# pick the branch; each job uploads its files as a run artifact.
|
||||||
name: release
|
name: release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -75,3 +78,46 @@ jobs:
|
|||||||
git add -A
|
git add -A
|
||||||
git commit -m "wanderbooks $v"
|
git commit -m "wanderbooks $v"
|
||||||
git push origin HEAD:gh-pages
|
git push origin HEAD:gh-pages
|
||||||
|
- name: Keep them with the run (a dispatch has no release)
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-packages
|
||||||
|
path: target/packages/${{ steps.version.outputs.version }}/
|
||||||
|
|
||||||
|
macos:
|
||||||
|
# UNSIGNED: a .dmg that Gatekeeper refuses until step 5 signs and
|
||||||
|
# notarizes it. Apple silicon (macos-latest); an Intel build is not
|
||||||
|
# made yet.
|
||||||
|
runs-on: macos-latest
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: "0"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: jdx/mise-action@v2
|
||||||
|
with:
|
||||||
|
install_args: "rust just cargo:cargo-packager"
|
||||||
|
- name: The tag is the program's version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
own=$(sed -n 's/^version = "\(.*\)"/\1/p' crates/ledger-cli/Cargo.toml | head -n1)
|
||||||
|
tag="${GITHUB_REF_NAME#v}"
|
||||||
|
if [ "${GITHUB_REF_TYPE}" = tag ] && [ "$tag" != "$own" ]; then
|
||||||
|
echo "tag $GITHUB_REF_NAME is not version $own" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "version=$own" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Build the unsigned .dmg
|
||||||
|
run: just package-macos ${{ steps.version.outputs.version }}
|
||||||
|
- name: Attach it to the release
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
v=${{ steps.version.outputs.version }}
|
||||||
|
gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes "wanderbooks $v"
|
||||||
|
gh release upload "$GITHUB_REF_NAME" --clobber target/packages/$v/*.dmg
|
||||||
|
- name: Keep it with the run (a dispatch has no release)
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-dmg-unsigned
|
||||||
|
path: target/packages/${{ steps.version.outputs.version }}/*.dmg
|
||||||
@@ -7,4 +7,3 @@ site/dist/
|
|||||||
/mutants.out*/
|
/mutants.out*/
|
||||||
/mutants-*/
|
/mutants-*/
|
||||||
# bookspec's own workspace target
|
# bookspec's own workspace target
|
||||||
/bookspec/target/
|
|
||||||
@@ -199,7 +199,7 @@ clone, with status. Rules:
|
|||||||
| `docs/receipt-store.md` | Blob store + `ledger-store` SQLite persistence spec |
|
| `docs/receipt-store.md` | Blob store + `ledger-store` SQLite persistence spec |
|
||||||
| `docs/testing.md` | Every test layer, each unit's test record, and what P12 must mutate |
|
| `docs/testing.md` | Every test layer, each unit's test record, and what P12 must mutate |
|
||||||
| `docs/parties-and-attachments.md` | The party directory, roles, the TIN vault, attachments to any subject |
|
| `docs/parties-and-attachments.md` | The party directory, roles, the TIN vault, attachments to any subject |
|
||||||
| `bookspec/README.md` | The formal accounting specification's examples, checker and runner (P13) |
|
| https://git.swaits.com/swaits/bookspec | The formal accounting specification's own repository: README, examples, checker, runner (P13); pinned in `crates/ledger-core/Cargo.toml` |
|
||||||
| `deploy/README.md` | Running the books on a machine of your own (P9a) |
|
| `deploy/README.md` | Running the books on a machine of your own (P9a) |
|
||||||
| `docs/glossary.md` | Debit/credit and friends, for agents new to accounting |
|
| `docs/glossary.md` | Debit/credit and friends, for agents new to accounting |
|
||||||
| `plan/STATUS.md` | Where the project stands right now |
|
| `plan/STATUS.md` | Where the project stands right now |
|
||||||
|
|||||||
Generated
+23
@@ -763,8 +763,10 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "bookspec-checker"
|
name = "bookspec-checker"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://git.swaits.com/swaits/bookspec?rev=74f2853d5f0db0066147c17fed8033cfe3d83a24#74f2853d5f0db0066147c17fed8033cfe3d83a24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"include_dir",
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -774,6 +776,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "bookspec-runner"
|
name = "bookspec-runner"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://git.swaits.com/swaits/bookspec?rev=74f2853d5f0db0066147c17fed8033cfe3d83a24#74f2853d5f0db0066147c17fed8033cfe3d83a24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bookspec-checker",
|
"bookspec-checker",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3451,6 +3454,25 @@ version = "0.14.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c"
|
checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
|
||||||
|
dependencies = [
|
||||||
|
"include_dir_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir_macros"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.2"
|
version = "2.14.2"
|
||||||
@@ -4039,6 +4061,7 @@ dependencies = [
|
|||||||
name = "ledger-package"
|
name = "ledger-package"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bookspec-checker",
|
||||||
"chrono",
|
"chrono",
|
||||||
"ledger-config",
|
"ledger-config",
|
||||||
"ledger-core",
|
"ledger-core",
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ crates/ledger-package/ # the shared read-only view: the CPA package, the invoice
|
|||||||
crates/ledger-vault/ # the TIN vault (XChaCha20-Poly1305 under the book's key)
|
crates/ledger-vault/ # the TIN vault (XChaCha20-Poly1305 under the book's key)
|
||||||
crates/ledger-gui/ # the viewer: read-only, live (egui)
|
crates/ledger-gui/ # the viewer: read-only, live (egui)
|
||||||
verify/lean/ # the accounting laws as Lean theorems (`just prove`)
|
verify/lean/ # the accounting laws as Lean theorems (`just prove`)
|
||||||
bookspec/ # the formal accounting specification: checker, runner, examples (P13)
|
# the formal accounting specification lives in its own repository, https://git.swaits.com/swaits/bookspec (pinned dev-dependency, P13)
|
||||||
fuzz/, oracle/ # fuzz targets; the GnuCash differential (`just oracle`)
|
fuzz/, oracle/ # fuzz targets; the GnuCash differential (`just oracle`)
|
||||||
deploy/ # the self-host recipe and system unit (P9a)
|
deploy/ # the self-host recipe and system unit (P9a)
|
||||||
site/ # wanderbooks.ai and install.sh
|
site/ # wanderbooks.ai and install.sh
|
||||||
|
|||||||
Generated
-714
@@ -1,714 +0,0 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 4
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "android_system_properties"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arrayvec"
|
|
||||||
version = "0.7.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bookspec-checker"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"rust_decimal",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"toml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bookspec-gnucash"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"bookspec-checker",
|
|
||||||
"bookspec-runner",
|
|
||||||
"rust_decimal",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bookspec-runner"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"bookspec-checker",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "borsh"
|
|
||||||
version = "1.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a"
|
|
||||||
dependencies = [
|
|
||||||
"borsh-derive",
|
|
||||||
"bytes",
|
|
||||||
"cfg_aliases",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "borsh-derive"
|
|
||||||
version = "1.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"proc-macro-crate",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 3.0.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bumpalo"
|
|
||||||
version = "3.20.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytes"
|
|
||||||
version = "1.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cc"
|
|
||||||
version = "1.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
|
|
||||||
dependencies = [
|
|
||||||
"find-msvc-tools",
|
|
||||||
"shlex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "1.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg_aliases"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chrono"
|
|
||||||
version = "0.4.45"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
|
||||||
dependencies = [
|
|
||||||
"iana-time-zone",
|
|
||||||
"js-sys",
|
|
||||||
"num-traits",
|
|
||||||
"serde",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "core-foundation-sys"
|
|
||||||
version = "0.8.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "equivalent"
|
|
||||||
version = "1.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "find-msvc-tools"
|
|
||||||
version = "0.1.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-core"
|
|
||||||
version = "0.3.34"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-task"
|
|
||||||
version = "0.3.34"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-util"
|
|
||||||
version = "0.3.34"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-task",
|
|
||||||
"pin-project-lite",
|
|
||||||
"slab",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "getrandom"
|
|
||||||
version = "0.2.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"wasi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "getrandom"
|
|
||||||
version = "0.3.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"r-efi",
|
|
||||||
"wasip2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.17.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iana-time-zone"
|
|
||||||
version = "0.1.65"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
|
||||||
dependencies = [
|
|
||||||
"android_system_properties",
|
|
||||||
"core-foundation-sys",
|
|
||||||
"iana-time-zone-haiku",
|
|
||||||
"js-sys",
|
|
||||||
"log",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"windows-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iana-time-zone-haiku"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indexmap"
|
|
||||||
version = "2.14.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
|
||||||
dependencies = [
|
|
||||||
"equivalent",
|
|
||||||
"hashbrown",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itoa"
|
|
||||||
version = "1.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "js-sys"
|
|
||||||
version = "0.3.106"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"futures-util",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libc"
|
|
||||||
version = "0.2.191"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "log"
|
|
||||||
version = "0.4.34"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "memchr"
|
|
||||||
version = "2.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.2.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell"
|
|
||||||
version = "1.21.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-project-lite"
|
|
||||||
version = "0.2.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ppv-lite86"
|
|
||||||
version = "0.2.21"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
|
||||||
dependencies = [
|
|
||||||
"zerocopy",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-crate"
|
|
||||||
version = "3.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
|
||||||
dependencies = [
|
|
||||||
"toml_edit",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.107"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.47"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "r-efi"
|
|
||||||
version = "5.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.8.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"rand_chacha 0.3.1",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.9.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
|
||||||
dependencies = [
|
|
||||||
"rand_chacha 0.9.0",
|
|
||||||
"rand_core 0.9.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_chacha"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
|
||||||
dependencies = [
|
|
||||||
"ppv-lite86",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_chacha"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
|
||||||
dependencies = [
|
|
||||||
"ppv-lite86",
|
|
||||||
"rand_core 0.9.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.6.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.17",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.9.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.3.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust_decimal"
|
|
||||||
version = "1.43.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7653272e75dcac41dc199fbea6f5797633994fafd339943c06c9af16bf29cd3a"
|
|
||||||
dependencies = [
|
|
||||||
"arrayvec",
|
|
||||||
"borsh",
|
|
||||||
"bytes",
|
|
||||||
"num-traits",
|
|
||||||
"rand 0.8.8",
|
|
||||||
"rand 0.9.5",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustversion"
|
|
||||||
version = "1.0.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.229"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
|
||||||
dependencies = [
|
|
||||||
"serde_core",
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_core"
|
|
||||||
version = "1.0.229"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
|
||||||
dependencies = [
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive"
|
|
||||||
version = "1.0.229"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 3.0.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_json"
|
|
||||||
version = "1.0.154"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
|
||||||
dependencies = [
|
|
||||||
"itoa",
|
|
||||||
"memchr",
|
|
||||||
"serde",
|
|
||||||
"serde_core",
|
|
||||||
"zmij",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_spanned"
|
|
||||||
version = "1.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
|
|
||||||
dependencies = [
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "shlex"
|
|
||||||
version = "2.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "slab"
|
|
||||||
version = "0.4.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "2.0.119"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "3.0.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml"
|
|
||||||
version = "1.1.8+spec-1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap",
|
|
||||||
"serde_core",
|
|
||||||
"serde_spanned",
|
|
||||||
"toml_datetime",
|
|
||||||
"toml_parser",
|
|
||||||
"toml_writer",
|
|
||||||
"winnow",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_datetime"
|
|
||||||
version = "1.1.2+spec-1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
|
|
||||||
dependencies = [
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_edit"
|
|
||||||
version = "0.25.17+spec-1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap",
|
|
||||||
"toml_datetime",
|
|
||||||
"toml_parser",
|
|
||||||
"winnow",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_parser"
|
|
||||||
version = "1.1.5+spec-1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
|
|
||||||
dependencies = [
|
|
||||||
"winnow",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_writer"
|
|
||||||
version = "1.1.3+spec-1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.27"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasi"
|
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasip2"
|
|
||||||
version = "1.0.4+wasi-0.2.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
|
||||||
dependencies = [
|
|
||||||
"wit-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen"
|
|
||||||
version = "0.2.129"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"once_cell",
|
|
||||||
"rustversion",
|
|
||||||
"serde",
|
|
||||||
"wasm-bindgen-macro",
|
|
||||||
"wasm-bindgen-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-macro"
|
|
||||||
version = "0.2.129"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"wasm-bindgen-macro-support",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-macro-support"
|
|
||||||
version = "0.2.129"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
|
||||||
dependencies = [
|
|
||||||
"bumpalo",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 3.0.9",
|
|
||||||
"wasm-bindgen-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-shared"
|
|
||||||
version = "0.2.129"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-core"
|
|
||||||
version = "0.62.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
|
||||||
dependencies = [
|
|
||||||
"windows-implement",
|
|
||||||
"windows-interface",
|
|
||||||
"windows-link",
|
|
||||||
"windows-result",
|
|
||||||
"windows-strings",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-implement"
|
|
||||||
version = "0.60.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.119",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-interface"
|
|
||||||
version = "0.59.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.119",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-link"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-result"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-strings"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winnow"
|
|
||||||
version = "1.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen"
|
|
||||||
version = "0.57.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerocopy"
|
|
||||||
version = "0.8.63"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
|
|
||||||
dependencies = [
|
|
||||||
"zerocopy-derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerocopy-derive"
|
|
||||||
version = "0.8.63"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.119",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zmij"
|
|
||||||
version = "1.0.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# bookspec's own workspace, inside the wanderbooks repository until the
|
|
||||||
# specification and its harness are stable enough to move to their own
|
|
||||||
# repository (P13 step 6, D-34).
|
|
||||||
[workspace]
|
|
||||||
resolver = "2"
|
|
||||||
members = ["checker", "runner", "gnucash"]
|
|
||||||
|
|
||||||
[workspace.package]
|
|
||||||
edition = "2024"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
repository = "https://git.swaits.com/swaits/wanderbooks"
|
|
||||||
|
|
||||||
[workspace.lints.rust]
|
|
||||||
missing_docs = "warn"
|
|
||||||
@@ -1,534 +0,0 @@
|
|||||||
# bookspec: examples (seed)
|
|
||||||
|
|
||||||
bookspec is the formal accounting specification planned in
|
|
||||||
`plan/served.md` (P13, D-34): every rule stated normatively, sourced to its
|
|
||||||
authority, with a symbolic form and concrete examples, independent of any
|
|
||||||
engine. Its own repository comes later. Until then this directory holds its
|
|
||||||
first examples: P3's regression tests, each written here as it is built, so
|
|
||||||
the spec starts from cases a real engine got wrong.
|
|
||||||
|
|
||||||
Every example is checked against wanderbooks on every `cargo test`
|
|
||||||
(`crates/ledger-core/tests/bookspec.rs`, and for the formats appendix
|
|
||||||
`crates/ledger-package/tests/package.rs`): an example the engine disagrees
|
|
||||||
with fails the build, so the examples can't rot. An expected value is worked
|
|
||||||
by hand from the cited source, never copied from an engine's output.
|
|
||||||
|
|
||||||
The schema, the parser (`Example::from_toml`, every structure with
|
|
||||||
`deny_unknown_fields`) and the `Engine` trait an accounting engine
|
|
||||||
implements live in `bookspec/checker`; `bookspec/runner` runs any engine
|
|
||||||
through the examples and reports per rule: pass, fail or not claimed. The
|
|
||||||
wanderbooks adapter is `crates/ledger-core/tests/bookspec.rs`. The rule
|
|
||||||
identifiers are `BS-<area>-<n>`, tabled in `bookspec/spec/rules-index.md`
|
|
||||||
(P13 steps 1 to 3).
|
|
||||||
|
|
||||||
## The shape of an example
|
|
||||||
|
|
||||||
One TOML file per example in `examples/`, named for its `id`.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
id = "cash-flow.reversed-property-contribution" # stable; never reused
|
|
||||||
title = "A property contribution reversed the same day"
|
|
||||||
rule = "One normative sentence: what MUST (or MUST NOT) hold."
|
|
||||||
status = "AUTHORITY" # AUTHORITY | CONVENTION | CHOICE
|
|
||||||
sources = ["ASC 230-10-45-24", "ASC 230-10-50-3"] # exact citations
|
|
||||||
tags = ["accrual", "book", "us"] # applicability
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD" # ISO 4217
|
|
||||||
fiscal_year_start = "01-01" # MM-DD
|
|
||||||
basis = "accrual" # accrual | cash
|
|
||||||
rounding = "half-even" # half-even | half-away-from-zero |
|
|
||||||
# largest-remainder: how a figure is
|
|
||||||
# rounded to the currency's minor unit,
|
|
||||||
# where a rule rounds (none here)
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset" # asset | liability | equity | revenue | expense
|
|
||||||
|
|
||||||
[[events]] # in order, counting from 1
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "Property contributed"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "2340.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "2340.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "Reverse entry 1"
|
|
||||||
reverses = 1 # the event (1-based) whose entry this
|
|
||||||
# one reverses: its lines, sides flipped;
|
|
||||||
# no `lines` here
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "cash_flow" # what is asked of the books (see below)
|
|
||||||
from = "2026-01-01" # inclusive
|
|
||||||
to = "2026-12-31" # inclusive (a query as of a day: as_of)
|
|
||||||
|
|
||||||
[expect.values] # every value an exact decimal string
|
|
||||||
operating = "0.00"
|
|
||||||
```
|
|
||||||
|
|
||||||
Events count from 1: the first `[[events]]` is event 1. An event is an
|
|
||||||
entry (`lines`), a reversal (`reverses`), a close (`close`, below), a
|
|
||||||
bill or an invoice (`bill`, `invoice`, below), each posting one entry;
|
|
||||||
or one of the books' commands that post what is due (`run_depreciation`, `dispose_asset`,
|
|
||||||
`run_schedules`, below), which post as many entries as are due, none
|
|
||||||
included; or a schedule's registration (`register_schedule`), which
|
|
||||||
posts none: exactly one of them. A key that names an event (`reverses =
|
|
||||||
N`, `entry = N`, a cash flow's `<N>`) names the entry event N posted, one
|
|
||||||
before it: an event that posted none, or several, can't be named that
|
|
||||||
way, and `reverses = 0` names no event and is refused. While every event
|
|
||||||
posts one entry, event N's is entry #N. A change that posts no entry (a
|
|
||||||
rename, a policy) is a `[[changes]]` entry instead (below). A key the
|
|
||||||
shape doesn't have is refused too, never ignored.
|
|
||||||
|
|
||||||
An entry, a reversal, a close, a bill, an invoice and a disposal are
|
|
||||||
dated (`date`); the other commands date their own entries (a run its
|
|
||||||
month's end) and take no date. An entry, a reversal, a close, a bill and
|
|
||||||
an invoice are described (`description`); a command describes its own
|
|
||||||
entries and takes none.
|
|
||||||
|
|
||||||
The books are the engine's own (`ledger_store::Store`, in a directory of
|
|
||||||
their own): each event is the command the books take it by, journaled as
|
|
||||||
they journal it, and after every expectation is checked the books must
|
|
||||||
replay to what they kept (`Store::verify`).
|
|
||||||
|
|
||||||
`rounding` names the rule the example's figures are rounded by where one
|
|
||||||
rounds: `half-even` or `half-away-from-zero` to the currency's minor unit,
|
|
||||||
or `largest-remainder` for an amount split among parts (each part rounded
|
|
||||||
down, the units left over to the largest remainders, ties to the part
|
|
||||||
listed first; rules.md B-13). An example that closes to the members
|
|
||||||
declares `largest-remainder`, the rule a members close splits by.
|
|
||||||
|
|
||||||
`status` says what the expected values rest on: `AUTHORITY`, the cited
|
|
||||||
authorities alone (a standard, a regulation, a statute, a court, the
|
|
||||||
mechanics of double entry); `CONVENTION`, common practice where no
|
|
||||||
authority governs (rules.md's CONVENTION); `CHOICE`, an owner's or the
|
|
||||||
house's decision (a plan D-number, or a CHOICE rules.md records) that
|
|
||||||
some expected value depends on, even when authorities carry the rest. An
|
|
||||||
example whose figures follow a decision cites it in `sources`.
|
|
||||||
|
|
||||||
Required everywhere: the top-level keys (`title`, `rule`, `tags` and
|
|
||||||
`sources` non-empty), `[setup]` with its four keys and its accounts, the
|
|
||||||
events (`events = []`, before `[setup]`, when nothing is posted), and at
|
|
||||||
least one `[[expect]]`. A value from a closed list (a type, a kind, a
|
|
||||||
role, a form, a basis) is spelled exactly as written here. A figure
|
|
||||||
query's values are
|
|
||||||
exact decimal strings (`"2340.00"`, `"-12.50"`; never a float), a format
|
|
||||||
query's are text (below). Amounts on lines are positive;
|
|
||||||
the side gives the direction.
|
|
||||||
|
|
||||||
### Optional keys, for the rules that need them
|
|
||||||
|
|
||||||
- `appendix = "formats"` (top level): the example shows an interchange
|
|
||||||
format, not an accounting rule (P13's appendix of formats; the IIF
|
|
||||||
file today). It loads like any other; only it may ask a format query.
|
|
||||||
- `parent` on an account: its parent account's code. A parent is declared
|
|
||||||
before its subaccounts.
|
|
||||||
- `header = true` on an account: a header, which groups other accounts
|
|
||||||
and takes no postings (it needs no tax category: rules.md B-32).
|
|
||||||
- `tax_category` on an account: its tax category, a key of the books'
|
|
||||||
catalog (`gross_receipts`, `office_expense`, ...; `ledger_core::tax`),
|
|
||||||
its link to a line of the return (B-32).
|
|
||||||
- `today` (in `[setup]`): the day the books are asked on, `YYYY-MM-DD`,
|
|
||||||
in the company's time zone: what an omitted date means (B-30). A query
|
|
||||||
that omits a date needs it.
|
|
||||||
- `cash_accounts`, `financing_accounts`, `investing_accounts` (in
|
|
||||||
`[setup]`): arrays of account codes, the statement of cash flows'
|
|
||||||
account roles (ASC 230-10-45-10 to 45-15). The cash and financing
|
|
||||||
accounts are also what a payee is paid from, for `vendor_1099_paid` and
|
|
||||||
`vendor_1099` (a payment card is neither: its charges go on Form
|
|
||||||
1099-K; rules.md B-19); with the cards, what party money is, for
|
|
||||||
`party_part` and `party_totals` (B-35 item 5: no `cash_accounts`, every
|
|
||||||
asset account but receivables); the cash accounts are what the K-1
|
|
||||||
worksheet reads as money (`k1`, `k1_tax_basis`, `k1_reconciliation`)
|
|
||||||
and an IIF file's bank accounts (B-34).
|
|
||||||
- `[[setup.fixed_assets]]` `{ cost_account, accumulated_account,
|
|
||||||
expense_account }`: accounts that hold property and equipment (their
|
|
||||||
cost and accumulated depreciation are investing; ASC 230-10-45-12(c),
|
|
||||||
45-13(c)), for an example no asset's own figures matter to. An asset
|
|
||||||
with its figures is a `[[setup.assets]]` entry (below), whose accounts
|
|
||||||
hold property and equipment too.
|
|
||||||
- `[[setup.assets]]`: fixed assets registered before the first event,
|
|
||||||
each named by its `id` in the events and queries that ask about it
|
|
||||||
(rules.md B-15):
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "X" # its name in this example
|
|
||||||
description = "Asset X"
|
|
||||||
cost = "12000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01" # depreciation starts here
|
|
||||||
method = "straight_line" # the one method the books have
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 2 # optional: the event whose entry put it
|
|
||||||
# on the books (B-15 rule 7)
|
|
||||||
basis = { rule = "personal_use_conversion",
|
|
||||||
contributor_adjusted_basis = "1688.40",
|
|
||||||
fmv_at_contribution = "1440.00",
|
|
||||||
sec704c_method = "traditional" } # optional (B-25)
|
|
||||||
```
|
|
||||||
|
|
||||||
Registering an asset posts nothing: the entry that buys it is an event
|
|
||||||
of its own. `acquired_by = N` links that entry, event N's, once the
|
|
||||||
event is applied: its date is the asset's acquired date, which puts it
|
|
||||||
on the register (before it, the in-service date does). `basis` records
|
|
||||||
how the company got it and the facts its tax figures follow from, as
|
|
||||||
the asset is registered: `rule` `"purchase"`, `"carryover_723"` or
|
|
||||||
`"personal_use_conversion"`; a contribution's
|
|
||||||
`contributor_adjusted_basis` and `fmv_at_contribution`; the §704(c)
|
|
||||||
method if chosen (`"traditional"`, `"traditional_with_curative"`,
|
|
||||||
`"remedial"`).
|
|
||||||
- `[[setup.schedules]]`, and `register_schedule = { ... }` as an event: a
|
|
||||||
prepaid expense or deferred revenue recognized over its months (rules.md
|
|
||||||
B-16), named by its `id`:
|
|
||||||
`{ id, kind, description, total, start, months, convention,
|
|
||||||
balance_account, pl_account }`, `kind` `"prepaid_expense"` or
|
|
||||||
`"deferred_revenue"`, `convention` `"daily"` (by the day, the last month
|
|
||||||
taking what the others leave) or `"monthly_even"` (equal months from
|
|
||||||
the 1st). Registering one posts nothing.
|
|
||||||
- `[[setup.parties]]` `{ name, roles, creditor_of?, tin_last4?,
|
|
||||||
w9_tin_last4?, tin_purged? }`: the company's directory (B-26). `roles`
|
|
||||||
are role keys (`member`, `vendor`, `customer`, `contractor`,
|
|
||||||
`landlord`, `employee`, `financial_institution`, `payout_platform`,
|
|
||||||
`tax_agency`, `government_agency`); `vendor` or `customer` (one at
|
|
||||||
most) gives the party its record. `creditor_of`: the liabilities, by
|
|
||||||
code, the company owes the party itself (a card's issuer, a lender,
|
|
||||||
each member for what the members advanced: B-35 item 6). `tin_last4`:
|
|
||||||
its TIN facts' last four (an SSN's); `w9_tin_last4`: its W-9's (a 1099
|
|
||||||
role's); `tin_purged = true`: the vault's copy purged, the facts kept (needs
|
|
||||||
`tin_last4`).
|
|
||||||
Never a full TIN. Each name once.
|
|
||||||
- `[[setup.members]]` `{ name, capital_account, share?,
|
|
||||||
distributions_account? }`: the members of a multi-member LLC. `share`
|
|
||||||
is the member's share of income, an exact decimal fraction above zero (`"0.5"`,
|
|
||||||
`"0.3333"`); every member has one or none does, and the shares sum to
|
|
||||||
1. A close to the members, `members_equity`, `k1`, `k1_tax_basis` and
|
|
||||||
`k1_reconciliation` need them. `distributions_account`: the member's
|
|
||||||
distributions account (B-13). A
|
|
||||||
member whose `name` is a party's in `setup.parties` with the `member`
|
|
||||||
role is that party, as the company's settings name them: an entry's
|
|
||||||
parties' shares resolve with those members (B-35), and so does the
|
|
||||||
1099 worksheet.
|
|
||||||
- `[[setup.payees]]` `{ name, form }`: a payee marked for an information
|
|
||||||
return from the beginning, `form` `"1099-NEC"` or `"1099-MISC"`; each
|
|
||||||
name once. A payee in `setup.parties` with the `vendor` role is that
|
|
||||||
party's vendor record; any other is a vendor record of its own.
|
|
||||||
- `[[setup.status_changes]]` `{ payee, effective, decision, reason_kind?,
|
|
||||||
reason? }`: a payee's 1099 decision from `effective` (`YYYY-MM-DD`) on
|
|
||||||
(rules.md B-19, D-40: an LLC's corporate election, say): `decision`
|
|
||||||
`"1099-NEC"`, `"1099-MISC"`, `"undetermined"` or `"not_reportable"`,
|
|
||||||
the last with `reason_kind` (`corporation`, `tax_exempt`,
|
|
||||||
`united_states`, `state`, `foreign_government`, `foreign_person`,
|
|
||||||
`merchandise`, `employee`, `other`) and the facts in words (`reason`).
|
|
||||||
Each payment takes the decision in force on its date.
|
|
||||||
- `[[setup.instruments]]` `{ label, kind, account, last4?, owner? }`: a
|
|
||||||
payment method (B-19): `kind` `bank`, `business_card`, `member_card`,
|
|
||||||
`member_cash_or_check` or `platform`; `account` the code it pays
|
|
||||||
through; `owner` a party's name in `setup.parties` (informational); each label
|
|
||||||
once. A business card's
|
|
||||||
liability is a card account (B-35 item 5): while none is recorded, a
|
|
||||||
liability whose name says "card", not "gift card", is.
|
|
||||||
- `[[setup.projects]]` `{ name, kind?, status?, party? }`: a project
|
|
||||||
(rules.md B-37), added with its facts, each name once: `kind`
|
|
||||||
`internal`, `client`, `on_spec` or `other`; `status` `active` (the
|
|
||||||
default), `paused`, `finished` or `abandoned`; `party` a
|
|
||||||
`setup.parties` name.
|
|
||||||
- `project = "<name>"` on a line of `lines`: the line is tagged with
|
|
||||||
that project (a `setup.projects` name), as the books' command tags it,
|
|
||||||
after the line is added. Not on a reversal, which carries its
|
|
||||||
original's tags.
|
|
||||||
- `payee` on an event: the payee (by name, one of `setup.payees`) the
|
|
||||||
entry paid. Only on an entry (`lines`): a reversal takes its
|
|
||||||
original's, a bill names its vendor, an invoice its customer, a close
|
|
||||||
and a command have none.
|
|
||||||
- `parties = [{ party, amount? }, { party, percent? }, ...]` on an event:
|
|
||||||
the parties (by name, in `setup.parties`) the entry is with, each with
|
|
||||||
its share: an `amount` (an exact decimal string), a `percent`, or
|
|
||||||
neither (derived from the entry: all of it for one party, the members'
|
|
||||||
capital lines or income shares for several: B-35). An entry naming a
|
|
||||||
party with a vendor record is tagged with that payee too; with
|
|
||||||
`payee` as well, the payee's party is one of the parties.
|
|
||||||
- `instrument` on an event: the payment method (a `setup.instruments`
|
|
||||||
label) the entry was paid with. Neither `parties` nor `instrument` on a
|
|
||||||
reversal, which takes its original's, nor on a command.
|
|
||||||
- `older = true` on an entry (`lines`): the entry as an earlier version
|
|
||||||
could write it, which this version's commands refuse: a line by hand
|
|
||||||
on a receivables or payables account, where only an invoice, a bill or
|
|
||||||
their payments post now (rules.md B-38, the control guard). Books that
|
|
||||||
hold one must still read right, so the books take it as replay does.
|
|
||||||
The same line written today is a `refused = "untagged_control_line"`
|
|
||||||
event instead. An event cannot be both.
|
|
||||||
- `close = { start, to, rounding? }` as an event: closes the period from
|
|
||||||
`start` through the event's `date` (rules.md B-11, B-13), `to`
|
|
||||||
`"members"` (split by share, in the order listed) or
|
|
||||||
`"retained_earnings"`. A close to the members splits cumulatively, each
|
|
||||||
close giving each member their split of everything the members' closes
|
|
||||||
have closed less what earlier closes gave them; `rounding = "per-close"` (members only)
|
|
||||||
splits that close's net on its own instead (books from an engine that
|
|
||||||
rounded each close by itself). A close must have something to close: it
|
|
||||||
posts one entry like any other event.
|
|
||||||
- `bill = { vendor, amount }` or `bill = { vendor, lines = [{ amount,
|
|
||||||
description?, project? }, ...] }` as an event: a bill from `vendor` (a
|
|
||||||
name; a payee's record if one has it) for `amount` (one line, described
|
|
||||||
as the event) or its `lines` (exactly one of them; a line's description
|
|
||||||
is the event's when left out), dated and due on the event's date. It
|
|
||||||
posts one entry: General Expenses (6999) debited and Accounts Payable
|
|
||||||
(2000) credited, each account created with that fixed name when first
|
|
||||||
needed, as the books create a control role's account (B-32). A line's
|
|
||||||
`project` (a `setup.projects` name) puts its cost on that project: the
|
|
||||||
entry's expense side is one line per project named, the untagged lines'
|
|
||||||
as one, the payable line never tagged (B-37 item 8).
|
|
||||||
- `invoice = { customer, amount }` or `invoice = { customer, lines = [...]
|
|
||||||
}` as an event: an invoice to `customer` (a name, its record added when
|
|
||||||
first named), as a bill is (the same `lines`), dated and due on the
|
|
||||||
event's date. It posts one entry: Accounts Receivable (1200) debited and
|
|
||||||
Revenue (4000) credited, each created with that fixed name when first
|
|
||||||
needed; a line's `project` puts its revenue on that project, the
|
|
||||||
receivable line never tagged (B-37 item 8).
|
|
||||||
- `run_depreciation = { asset, period }` as an event: the asset's run for
|
|
||||||
`period` (`"YYYY-MM"`), which posts what the schedule says is due
|
|
||||||
through the month's end and the books don't carry, dated then: a
|
|
||||||
skipped month caught up, nothing when nothing is due (rules.md B-15).
|
|
||||||
- `dispose_asset = { asset, proceeds, proceeds_account, gain_loss_account
|
|
||||||
}` as an event, dated: the asset's disposal for `proceeds` (an exact
|
|
||||||
decimal, zero allowed), received into `proceeds_account`, its gain or
|
|
||||||
loss to `gain_loss_account` (B-15). It posts the depreciation still due
|
|
||||||
through its day in an entry of its own when the month has no run, then
|
|
||||||
the disposal entry.
|
|
||||||
- `run_schedules = { through }` as an event: every schedule's months due
|
|
||||||
through `through` posted (B-16).
|
|
||||||
- `write_off = { invoice, amount?, reason }` as an event, dated: writes
|
|
||||||
off the invoice event `invoice` (1-based; an `invoice` event before it)
|
|
||||||
posted, as a bad debt (rules.md B-41): Bad Debt Expense (6990) is
|
|
||||||
debited and Accounts Receivable credited, the receivable line tagged
|
|
||||||
with the invoice, and the evidence of worthlessness filed in the same
|
|
||||||
call. `amount` (an exact decimal above zero; left out, all the invoice
|
|
||||||
owes on the day) makes it partial. Refused on cash-basis books and
|
|
||||||
above what is owed.
|
|
||||||
- `refused = "<reason>"` on a reversal, an entry or a command: the books
|
|
||||||
refuse it, for that reason (below); it changes nothing, posts nothing,
|
|
||||||
and the example goes on without it. An entry posts whole or not at all,
|
|
||||||
so a refused one leaves no draft behind.
|
|
||||||
- `[[changes]]` `{ after, rename?, require_tax_category?, correct_asset?
|
|
||||||
}`: a change that posts no entry, exactly one of `rename = { code, name
|
|
||||||
}` (rename the account `code`, B-31), `require_tax_category = "warn"`
|
|
||||||
or `"refuse"` (the policy, B-32; the books start at `warn`) and
|
|
||||||
`correct_asset = { asset, cost?, salvage?, life_months?, in_service?,
|
|
||||||
reason }` (a registration mistake corrected: each figure given replaces
|
|
||||||
the asset's, the rest stay; refused while the books carry depreciation
|
|
||||||
for it). It is made
|
|
||||||
right after event `after` (1 to the number of events), once the
|
|
||||||
expectations asked after that event are checked, in the order listed.
|
|
||||||
- `after = N` on an expectation: asked of the books after event N (before
|
|
||||||
the changes made after it), not after the last event and every change
|
|
||||||
(1 to the number of events; not on a format query).
|
|
||||||
|
|
||||||
Party totals and the 1099 worksheet answer different questions (B-35
|
|
||||||
item 9): a member paying a contractor through the members' financing
|
|
||||||
account is, in party totals, paid to the contractor (money, the
|
|
||||||
members' financing being money) and nothing to the member until the
|
|
||||||
company reimburses them; on the 1099 worksheet it is the company's
|
|
||||||
payment, counted toward the contractor's 1099 unless it was made by card
|
|
||||||
(B-19), and undetermined until its payment method is recorded.
|
|
||||||
|
|
||||||
### Queries and their values
|
|
||||||
|
|
||||||
A query over a period takes `from` and `to`; a query as of a day takes
|
|
||||||
`as_of`; the other is refused. A date left out is resolved by the one
|
|
||||||
rule (rules.md B-30) on `setup.today`: an omitted `to` is today, an
|
|
||||||
omitted `from` the first day of the fiscal year that holds `to`, an
|
|
||||||
omitted `as_of` today (`vendor_1099_paid`, `vendor_1099`,
|
|
||||||
`party_totals` and the format queries give theirs). A query no crate
|
|
||||||
answers is refused, never skipped.
|
|
||||||
|
|
||||||
| query | dates | values | checked by |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `cash_flow` | `from`, `to` | figures (below) | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `trial_balance` | `as_of` | each account's balance by code, debits less credits (a credit balance negative), every account of `setup.accounts` listed (not those the books create) | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `profit_and_loss` | `from`, `to` | each revenue and expense account's activity by code, on its normal side, closing entries left out, every one of `setup.accounts` listed; `net_income`, revenue less expenses | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `entry` | `entry = N`, no dates | the lines of event N's entry, `"<code>.debit"` and `"<code>.credit"` each the sum of that account's lines on that side, every line of the entry named | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `balances` | `as_of` | each account named by code, its balance on its normal side (a member's capital after a loss is negative); the example names the accounts it shows | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `members_equity` | `from`, `to` | the statement of changes in members' equity (rules.md B-13): `"<member>.beginning"`, `.contributions`, `.net_income`, `.distributions`, `.other`, `.ending`; the example names the figures it shows | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `k1` | `from`, `to` | the K-1 worksheet (B-13; Schedule K-1 Item L): `"<member>.beginning"`, `.contributions_cash`, `.contributions_property`, `.net_income`, `.distributions`, `.other`, `.ending`; the example names the figures it shows (`cash_accounts` required) | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `k1_tax_basis` | `from`, `to` | the K-1 worksheet on the tax basis (B-36; Schedule K-1 Item L, tax-basis method), beside `k1`'s book figures: `"<member>.beginning"`, `.contributions_cash`, `.contributions_property`, `.net_income`, `.other`, `.distributions`, `.ending`; a line the books can't know is `"open"`, with what they know so far as `"<member>.known_<line>"`; the example names the figures it shows (`cash_accounts` required) | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `k1_reconciliation` | `from`, `to` | each member's book capital less tax-basis capital at `to`, by cause (B-36): `"<member>.beginning"`, `.built_in_gain_704c`, `.property_without_basis`, `.property_distributed`, `.total`; the example names the members it shows, each with every cause (`cash_accounts` required) | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `vendor_1099_paid` | `from` (January 1 of `to`'s year), `to` | each payee's payments that count toward its 1099, by name: out of the cash and financing accounts (`cash_accounts` required), a card's or payment network's left out (B-19), every payee listed | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `vendor_1099` | `payee`; `from` (January 1 of `to`'s year), `to` | one payee's line of the 1099 worksheet (B-19): `status` (`reportable`, `under_threshold`, `not_reportable` or `undetermined`), `threshold`, `paid` (counts toward its 1099), `excluded_card` (a card's or payment network's: Form 1099-K), `undetermined` (through a financing account, its payment method not recorded), `not_reportable` (paid while decided not reportable), every one given | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `party_part` | `entry = N`, `party`, no dates | entry N's part for the party (B-35): `resolved`, signed as a line on the party's own account would be (paid or given positive, received negative), the entry's `direction` (`paid`, `received` or `neither`), and what of it moved as money (`cash`) and as property (`in_kind`), every one given | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `party_totals` | `party`, `basis` (`cash` or `all`), `from`, `to` | what was `paid` to the party and `received` from it over the period (B-35 item 4: basis `cash` money, `all` money and property in kind; each entry on its first original's side), both given | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `party_tin_last4` | `party`, no dates | `tin_last4`: the party's TIN facts' last four, else its W-9's, `""` for none (B-26) | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `period` | `from`, `to`, either or both left out | the dates a period report asked with those dates covers, by the one rule (B-30): `from`, `to` (`YYYY-MM-DD`); or `refused` | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `day` | `as_of`, or left out | the day a report as of a day is made as of: `as_of` | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `calendar_year` | none | the year a calendar-year count (1099s, party totals) asked without one counts: `year` | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `account_names` | none | each account's name by code, as the chart holds it and every report prints it, past periods included (B-31); every account listed, those the books created included | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `uncategorized_accounts` | none | a list: every account that needs a tax category (revenue or expense, not a header) and has none, with activity or not, `"<code> <name>"`, by code (B-32) | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `run` | `event = N`, no dates | what the `run_depreciation` event N posted: `amount` (zero when nothing was due, or the month had its run already), and `caught_up`, what of it is beyond the month's own depreciation (B-15); both listed | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `disposal` | `asset`, no dates | what the asset's disposal took (B-15), every figure listed: `final_depreciation` (posted in its own entry), `catch_up_depreciation` and `excess_depreciation` (charged, and credited back, in the disposal entry), `accumulated` (cleared), `book_value`, `gain_loss` (a gain positive) | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `depreciation_schedule` | `asset`, `as_of` | the asset's schedule through the day (B-15): each month named (`"YYYY-MM"`) its depreciation, `"0.00"` for a month it doesn't reach; `accumulated` and `book_value` through the day; the example names the figures it shows | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `register_row` | `asset`, `as_of` | the asset's row of the fixed asset register (B-15 rules 7 and 8, B-24): `acquired`, `in_service` (`YYYY-MM-DD`), `cost`, `accumulated_scheduled` (the schedule's), `accumulated_posted` (what the books carry), `unposted` (the one less the other), `book_value` (cost less posted), each `"0.00"` once disposed; the example names the figures it shows; an asset acquired after the day is refused | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `register_totals` | `as_of` | the register's totals over the assets held (B-24, B-25): `total_cost`, `total_accumulated_scheduled`, `total_accumulated_posted`, `total_book_value`, `total_depreciable_basis`, `total_tax_basis_723`, `missing_basis` (a count), and how it ties to the books: `unregistered_cost`, `unposted_depreciation`, `unattributed_accumulated`; the example names the figures it shows | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `asset_basis` | `asset`, no dates | the asset's basis facts and the tax figures they give (B-25): `rule`, `contributor_adjusted_basis`, `fmv_at_contribution`, `tax_basis_723`, `depreciable_basis`, `built_in_gain_704c` (a loss negative), each `"none"` where it has none, and `sec704c_1c` (`"true"` for a built-in loss, which counts only for the contributor); the example names the figures it shows | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `project` | `project`, `from`, `to` | the project's figures (B-37), every one given: `revenue`, `expenses` and `net` for the period (the revenue and expense lines tagged to it, closes left out), `to_date.revenue`, `to_date.expenses` and `to_date.net` (every tagged line through `to`), and `unrecovered_cost` (expenses less revenue to date, never below zero; `"none"` unless it is on spec) | `ledger-core` `tests/bookspec.rs`; `ledger-cli` `tests/spec_projects.rs` `project_figures_match_profit_and_loss_by_class` |
|
|
||||||
| `schedule_plan` | `schedule`, no dates | the schedule's plan (B-16): what it recognizes at each month end named (`"YYYY-MM-DD"`), `"0.00"` at one it doesn't post; `total`, all it recognizes; the example names the figures it shows | `ledger-core` `tests/bookspec.rs` |
|
|
||||||
| `iif_lines` | `as_of` | formats appendix: the file's lines in order, each its fields joined by tabs, without the CR LF that ends each | `ledger-package` `the_bookspec_iif_example_matches_the_engine` |
|
|
||||||
| `iif_changes` | `as_of` | formats appendix: each text the file writes differently from the books, in file order, as `[what, books' text, file's text]` (the example's header gives `what`) | `ledger-package` `the_bookspec_iif_example_matches_the_engine` |
|
|
||||||
|
|
||||||
A figure query is a table (`[expect.values]`, or `values = { ... }`); a
|
|
||||||
format query, and `uncategorized_accounts`, an array (`values = [ ...
|
|
||||||
]`). `period`, `day` and `calendar_year` give dates and a year,
|
|
||||||
`account_names` names, `party_part`'s `direction`, `vendor_1099`'s
|
|
||||||
`status` and `party_tin_last4` words, not figures; `register_row` gives
|
|
||||||
dates beside its figures, `project` gives `"none"` as the unrecovered cost of a
|
|
||||||
project that isn't on spec, `asset_basis` a rule's name, `"none"` and
|
|
||||||
`"true"` or `"false"`, and a `k1_tax_basis` line `"open"`, beside them.
|
|
||||||
|
|
||||||
A command is tried on a copy of the books, as the books' own command
|
|
||||||
does it, and changes nothing checked after it. It shows no values: it is
|
|
||||||
`accepted = true`, or `refused = "<reason>"`.
|
|
||||||
|
|
||||||
| command | keys | |
|
|
||||||
|---|---|---|
|
|
||||||
| `rename_account` | `code`, `name` | rename the account (B-31); accepted, it must move no amount and keep the name trimmed |
|
|
||||||
| `add_account` | `account = { code, name, type, parent?, header?, tax_category? }` | add the account, under the policy in force (B-31, B-32) |
|
|
||||||
| `set_tax_category` | `code`, `category` (left out: clear it) | set or clear the account's tax category, under the policy in force (B-32) |
|
|
||||||
|
|
||||||
A refusal (a command's, or a `period`'s) is named for what was refused,
|
|
||||||
never by an engine's message: `end_before_start` (both dates given, the
|
|
||||||
start after the end), `start_after_today` (only a start, after today, the
|
|
||||||
end an omitted end means), `blank`, `unchanged` (the name the account has,
|
|
||||||
trimmed), `sibling_has_it` (another account under the same parent has the
|
|
||||||
name, ignoring capitals and surrounding space), `holds_a_tin` (a run of
|
|
||||||
nine digits), `not_one_line` (a line break, tab or other control
|
|
||||||
character, or past 200 characters), `code_taken`, and
|
|
||||||
`tax_category_required` (an account that needs a category left without
|
|
||||||
one under `refuse`). A refused event's: `depreciation_past_disposal` (a
|
|
||||||
disposal dated before depreciation a run carries: a later month's run
|
|
||||||
stands, or a run's change dated after the disposal's month isn't undone),
|
|
||||||
`entry_irreversible` (an asset's entry that can't be reversed: its
|
|
||||||
disposal, a disposed asset's depreciation, a run re-instated twice or past
|
|
||||||
its schedule, or a reversal dated before its run), `already_disposed`,
|
|
||||||
`invalid_depreciation_period` (a month that ends before the asset is in
|
|
||||||
service), `invalid_schedule` (an equal-months schedule not starting on
|
|
||||||
the 1st, among others) and `untagged_control_line` (a hand line on a
|
|
||||||
receivables or payables control account, which only a document may post
|
|
||||||
to: rules.md B-38's control guard).
|
|
||||||
|
|
||||||
The refusals of the ledger's own laws (BS-entry, BS-ledger, BS-period)
|
|
||||||
are spelled `unbalanced` (debits differ from credits), `too_few_lines`,
|
|
||||||
`negative_amount` and `zero_amount` (BS-entry-2), `finer_than_minor_unit`
|
|
||||||
(an amount finer than the currency's smallest unit, BS-ledger-1),
|
|
||||||
`parent_type_mismatch` (a child account of another type than its parent,
|
|
||||||
BS-ledger-2), `already_reversed` (BS-ledger-6) and `period_closed` (an
|
|
||||||
entry dated inside a closed period, BS-period-2). A refused entry may
|
|
||||||
carry the amount the books refuse (zero or negative, in the amount's
|
|
||||||
exact decimal spelling); an accepted one carries a positive amount.
|
|
||||||
|
|
||||||
`cash_flow` (rules.md B-14): the statement of cash flows for `[from, to]`.
|
|
||||||
|
|
||||||
| key | the value |
|
|
||||||
|---|---|
|
|
||||||
| `operating`, `investing`, `financing` | each section's net cash, + in |
|
|
||||||
| `net_change`, `cash_beginning`, `cash_ending` | the cash accounts' change and balances |
|
|
||||||
| `difference` | sections less the change in cash: always `"0.00"` |
|
|
||||||
| `net_income` | revenue less expenses, closing entries excluded |
|
|
||||||
| `<section>.<N>` | event N's entry's cash in that section (`financing.4`) |
|
|
||||||
| `<section>.<N>.on_behalf` | of it, what a member or a lender paid or received for the company, no cash account moving (constructive receipt and disbursement) |
|
|
||||||
| `<section>.count` | how many entries the section lists |
|
|
||||||
| `class.<a..g>` | an operating class's total, by ASC 230-10-45-25's letter: (a) from customers, (b) interest and dividends received, (c) other receipts, (d) paid to suppliers and employees, (e) interest paid, (f) income taxes paid, (g) other payments; given one, every class the statement has must be given |
|
|
||||||
| `noncash.<N>` | event N's entry's noncash investing and financing disclosure |
|
|
||||||
| `noncash.count` | how many entries are disclosed |
|
|
||||||
| `reconciliation.depreciation` | depreciation added back |
|
|
||||||
| `reconciliation.investing_gain_loss` | gains and losses whose cash is investing, taken out |
|
|
||||||
| `reconciliation.noncash_through.<code>` | operating movements no cash settled, through that account |
|
|
||||||
| `reconciliation.other_noncash` | other operating movements no cash settled |
|
|
||||||
| `reconciliation.balance_change.<code>` | minus the change in an operating asset or liability |
|
|
||||||
|
|
||||||
An entry named in a `<section>.<N>` (or `.on_behalf`) or `noncash.<N>`
|
|
||||||
key must be the only such entries when `<section>.count` or
|
|
||||||
`noncash.count` is given too; a
|
|
||||||
reconciliation key not given must be zero or absent.
|
|
||||||
|
|
||||||
Every example, in any appendix, is loaded by
|
|
||||||
`crates/ledger-core/tests/bookspec.rs` (`every_bookspec_example_holds`):
|
|
||||||
its shape (a key the shape doesn't have is refused), its setup, and every
|
|
||||||
event taken or refused as it says, on the books' own store. Each query is
|
|
||||||
then checked by the crate that answers it (the table above), so the shape
|
|
||||||
never rots either.
|
|
||||||
|
|
||||||
## The specification, the laws, the checks (P13 steps 4 and 5)
|
|
||||||
|
|
||||||
- `spec/` is the specification text: `00-model.md` (definitions, the
|
|
||||||
model, the five laws, how an example is checked) and one file per area
|
|
||||||
(`entry.md`, `ledger.md`, ... `format.md`), each rule a section with its
|
|
||||||
status, its MUST/MUST NOT/MAY statement, its sources (a vendored source
|
|
||||||
with a verbatim pin, or an exact external reference), its symbolic form,
|
|
||||||
its examples by id and its applicability. `spec/rules-index.md` is the
|
|
||||||
table from `docs/spec/rules.md`'s B-1 to B-45. The checker's
|
|
||||||
`tests/spec.rs` fails the build when a rule lacks its section, a
|
|
||||||
statement lacks MUST/SHOULD/MAY, a pin is not verbatim in its vendored
|
|
||||||
source, a rule's examples differ from the catalogue's, or the edition
|
|
||||||
passes 60 pages.
|
|
||||||
- `lean/` is the five laws as Lean theorems (`Bookspec.law_*`), ported
|
|
||||||
from `verify/lean`; `lake build` in that directory builds them and
|
|
||||||
`lake env lean Audit.lean` prints their axioms, which the checker's
|
|
||||||
`tests/lean.rs` requires to be only `propext`, `Quot.sound` and
|
|
||||||
`Classical.choice`.
|
|
||||||
- The checker derives expected figures itself (`checker/src/derive.rs`):
|
|
||||||
an engine-free reference model with exact decimals replays an example's
|
|
||||||
entries, reversals, close, bills and invoices and computes its
|
|
||||||
`trial_balance`, `balances`, `profit_and_loss` and `entry` figures from
|
|
||||||
the rules' symbolic forms, and checks that each refusal the example
|
|
||||||
expects is justified by its rule. An example whose figure disagrees
|
|
||||||
fails `checker/tests/derive.rs`, before any engine is consulted. What
|
|
||||||
the model does not cover is reported as skipped, never passed.
|
|
||||||
- `gnucash/` is the second engine: GnuCash 5.10 in the oracle container
|
|
||||||
(`just oracle` builds it), driven through `oracle/gnucash/books.py`.
|
|
||||||
`cargo test -p bookspec-gnucash -- --ignored` runs it (it needs
|
|
||||||
Docker). It claims the rules whose examples ask only for balances and
|
|
||||||
passes `BS-entry-3`, `BS-ledger-3`, `BS-statement-1` and
|
|
||||||
`BS-statement-2`.
|
|
||||||
|
|
||||||
### GnuCash, the second engine: where it differs
|
|
||||||
|
|
||||||
GnuCash is a cross-check, never the authority (`docs/oracle.md`, "Who is
|
|
||||||
right"). It differs from three examples, each settled from the sources
|
|
||||||
and pinned by `gnucash/tests/gnucash.rs`:
|
|
||||||
|
|
||||||
- **`entry-an-unbalanced-entry-is-refused`.** GnuCash accepts debits of
|
|
||||||
100.00 against credits of 90.00 and parks the ten dollars in an
|
|
||||||
`Imbalance-USD` account of its own making; the accounts it lists no
|
|
||||||
longer net to zero. BS-entry-1 (Pacioli ch. 36; IRS Pub. 583: "the
|
|
||||||
total debits must equal the total credits") says such an entry is
|
|
||||||
refused. The example stands; GnuCash records a difference, flagged for
|
|
||||||
the user to repair, instead of refusing it.
|
|
||||||
- **`entry-a-line-amount-is-positive`.** GnuCash takes a credit of
|
|
||||||
minus 100.00 as a debit of 100.00, both lines debits, and parks 200.00
|
|
||||||
in the Imbalance account. BS-entry-2 is a CONVENTION (no authority
|
|
||||||
forbids a negative amount); the example stands as the convention the
|
|
||||||
specification chose, and GnuCash's reading is the very ambiguity the
|
|
||||||
convention removes.
|
|
||||||
- **`ledger-a-fraction-of-a-cent-is-refused`.** GnuCash rounds 10.005 to
|
|
||||||
10.01 silently. BS-ledger-1 is a CHOICE (the owner, 2026-10-13, found
|
|
||||||
by the first GnuCash cross-check): the books never round silently. An
|
|
||||||
engine's pick is its own; GnuCash's is the other option, recorded.
|
|
||||||
|
|
||||||
The example `ledger-a-mistake-is-reversed-never-erased` and the others
|
|
||||||
that need an engine's own reversal link or entry query are not claimed by
|
|
||||||
the GnuCash adapter: GnuCash has no reversal link, so it cannot answer
|
|
||||||
`already_reversed`.
|
|
||||||
|
|
||||||
## Licence and sources
|
|
||||||
|
|
||||||
The examples state rules in their own words with the paragraph cited
|
|
||||||
(FASB's Codification is copyrighted: cited, never copied; see
|
|
||||||
`docs/spec/sources/fasb-asc.md`). The examples are MIT OR Apache-2.0, as
|
|
||||||
the rest of this repository, until bookspec moves to its own repository
|
|
||||||
(planned: the spec under CC BY 4.0).
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "bookspec-checker"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
description = "bookspec's example schema, its rule identifiers and the Engine trait an accounting engine implements."
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
serde_json = "1"
|
|
||||||
toml = "1"
|
|
||||||
rust_decimal = "1"
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1,680 +0,0 @@
|
|||||||
//! The specification's own checker (`plan/served.md` P13 step 5):
|
|
||||||
//! every example's expected figures derived from its rule's symbolic
|
|
||||||
//! form, by an engine-free reference model with exact decimals.
|
|
||||||
//!
|
|
||||||
//! The runner asks an *engine* whether it agrees with an example. This
|
|
||||||
//! module asks whether the *example* agrees with the specification: it
|
|
||||||
//! replays the example's events on the smallest ledger the rules of
|
|
||||||
//! `bookspec/spec/*.md` describe (entries, reversals, a close, a bill, an
|
|
||||||
//! invoice) and computes each expectation it can from the symbolic forms
|
|
||||||
//! (`sum` of signed values, normal balances, the accounting equation,
|
|
||||||
//! net income). An expectation whose figure disagrees with the
|
|
||||||
//! derivation, or an event whose refusal the rules do not justify, fails
|
|
||||||
//! the specification's build, before any engine is consulted.
|
|
||||||
//!
|
|
||||||
//! What the model does not cover (depreciation, schedules, members,
|
|
||||||
//! 1099s, formats, the cash-flow sections) is not derived here: an
|
|
||||||
//! expectation is [`Derived::Skipped`] with the reason, never silently
|
|
||||||
//! passed, and the example is still checked by the engines.
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
|
||||||
use rust_decimal::Decimal;
|
|
||||||
|
|
||||||
use crate::engine::RuleId;
|
|
||||||
use crate::rules::Area;
|
|
||||||
use crate::schema::{Event, Example, Expect, Values};
|
|
||||||
|
|
||||||
/// What the derivation found of one expectation or event.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Derived {
|
|
||||||
/// The specification derives the example's figures, and they agree.
|
|
||||||
Agrees {
|
|
||||||
/// The rule whose symbolic form derived them.
|
|
||||||
rule: RuleId,
|
|
||||||
},
|
|
||||||
/// The specification derives different figures, or does not justify
|
|
||||||
/// a refusal: the example is wrong.
|
|
||||||
Disagrees {
|
|
||||||
/// The rule whose symbolic form was applied.
|
|
||||||
rule: RuleId,
|
|
||||||
/// What differs, in words.
|
|
||||||
why: String,
|
|
||||||
},
|
|
||||||
/// The reference model does not cover it.
|
|
||||||
Skipped {
|
|
||||||
/// Why not.
|
|
||||||
why: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The derivation of one example: one result per event that was refused
|
|
||||||
/// and per expectation.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Derivation {
|
|
||||||
/// The example's id.
|
|
||||||
pub example: String,
|
|
||||||
/// What was derived, in the order met.
|
|
||||||
pub results: Vec<(String, Derived)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Derivation {
|
|
||||||
/// The disagreements, each as a sentence.
|
|
||||||
pub fn disagreements(&self) -> Vec<String> {
|
|
||||||
self.results
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(what, d)| match d {
|
|
||||||
Derived::Disagrees { rule, why } => {
|
|
||||||
Some(format!("{}: {what}: {why} ({rule})", self.example))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How many results agree.
|
|
||||||
pub fn agreed(&self) -> usize {
|
|
||||||
self.results
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, d)| matches!(d, Derived::Agrees { .. }))
|
|
||||||
.count()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
enum Side {
|
|
||||||
Debit,
|
|
||||||
Credit,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct Line {
|
|
||||||
account: String,
|
|
||||||
side: Side,
|
|
||||||
amount: Decimal,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct Posted {
|
|
||||||
date: NaiveDate,
|
|
||||||
lines: Vec<Line>,
|
|
||||||
closing: bool,
|
|
||||||
reversed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The reference model: posted entries, the closed periods, the types.
|
|
||||||
struct Model {
|
|
||||||
types: BTreeMap<String, String>,
|
|
||||||
entries: Vec<Posted>,
|
|
||||||
/// Event number (from 1) to the entry it posted.
|
|
||||||
event_entry: BTreeMap<usize, usize>,
|
|
||||||
closed_through: Vec<NaiveDate>,
|
|
||||||
minor_digits: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign(side: Side) -> Decimal {
|
|
||||||
match side {
|
|
||||||
Side::Debit => Decimal::ONE,
|
|
||||||
Side::Credit => -Decimal::ONE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dec(s: &str) -> Option<Decimal> {
|
|
||||||
Decimal::from_str(s).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn date(s: &str) -> Option<NaiveDate> {
|
|
||||||
NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rule(area: Area, n: u32) -> RuleId {
|
|
||||||
RuleId::new(area, n)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The accounts the books create for a document or a close, with their
|
|
||||||
/// types (rules.md B-23's default roles).
|
|
||||||
fn role_type(code: &str) -> Option<&'static str> {
|
|
||||||
match code {
|
|
||||||
"1200" => Some("asset"),
|
|
||||||
"2000" => Some("liability"),
|
|
||||||
"3999" => Some("equity"),
|
|
||||||
"4000" => Some("revenue"),
|
|
||||||
"6999" => Some("expense"),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Model {
|
|
||||||
fn new(ex: &Example) -> Self {
|
|
||||||
let mut types: BTreeMap<String, String> = ex
|
|
||||||
.setup
|
|
||||||
.accounts
|
|
||||||
.iter()
|
|
||||||
.map(|a| (a.code.clone(), a.kind.clone()))
|
|
||||||
.collect();
|
|
||||||
for code in ["1200", "2000", "3999", "4000", "6999"] {
|
|
||||||
types
|
|
||||||
.entry(code.to_string())
|
|
||||||
.or_insert_with(|| role_type(code).unwrap().to_string());
|
|
||||||
}
|
|
||||||
// ISO 4217: the currency's minor-unit digits (two unless listed).
|
|
||||||
let minor_digits = match ex.setup.currency.as_str() {
|
|
||||||
"JPY" | "KRW" | "VND" | "CLP" | "ISK" => 0,
|
|
||||||
"BHD" | "KWD" | "OMR" | "JOD" => 3,
|
|
||||||
_ => 2,
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
types,
|
|
||||||
entries: Vec::new(),
|
|
||||||
event_entry: BTreeMap::new(),
|
|
||||||
closed_through: Vec::new(),
|
|
||||||
minor_digits,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signed sum of an account's lines dated on or before `through`
|
|
||||||
/// (debit positive), counting entries that stand as written: the
|
|
||||||
/// reversal is its own entry, so the original is always counted.
|
|
||||||
fn signed(
|
|
||||||
&self,
|
|
||||||
code: &str,
|
|
||||||
from: Option<NaiveDate>,
|
|
||||||
through: NaiveDate,
|
|
||||||
closings: bool,
|
|
||||||
) -> Decimal {
|
|
||||||
self.entries
|
|
||||||
.iter()
|
|
||||||
.filter(|e| {
|
|
||||||
e.date <= through && from.is_none_or(|f| e.date >= f) && (closings || !e.closing)
|
|
||||||
})
|
|
||||||
.flat_map(|e| e.lines.iter())
|
|
||||||
.filter(|l| l.account == code)
|
|
||||||
.map(|l| sign(l.side) * l.amount)
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_debit_normal(&self, code: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
self.types.get(code).map(String::as_str),
|
|
||||||
Some("asset") | Some("expense")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_normal_side(&self, code: &str, signed: Decimal) -> Decimal {
|
|
||||||
if self.is_debit_normal(code) {
|
|
||||||
signed
|
|
||||||
} else {
|
|
||||||
-signed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Why the rules refuse this entry, if they do: (reason, rule).
|
|
||||||
fn refusal_justified(&self, e: &Event, reason: &str) -> Result<RuleId, String> {
|
|
||||||
let on = |side: &str| -> Vec<Decimal> {
|
|
||||||
e.lines
|
|
||||||
.iter()
|
|
||||||
.filter(|l| l.side == side)
|
|
||||||
.filter_map(|l| dec(&l.amount))
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
let d: Decimal = on("debit").into_iter().sum();
|
|
||||||
let c: Decimal = on("credit").into_iter().sum();
|
|
||||||
let amounts: Vec<Decimal> = e.lines.iter().filter_map(|l| dec(&l.amount)).collect();
|
|
||||||
match reason {
|
|
||||||
"unbalanced" if d != c => Ok(rule(Area::Entry, 1)),
|
|
||||||
"unbalanced" => Err(format!("debits {d} equal credits {c}")),
|
|
||||||
"too_few_lines" if e.lines.len() < 2 => Ok(rule(Area::Entry, 1)),
|
|
||||||
"negative_amount" if amounts.iter().any(|a| *a < Decimal::ZERO) => {
|
|
||||||
Ok(rule(Area::Entry, 2))
|
|
||||||
}
|
|
||||||
"zero_amount" if amounts.iter().any(|a| a.is_zero()) => Ok(rule(Area::Entry, 2)),
|
|
||||||
"finer_than_minor_unit"
|
|
||||||
if amounts
|
|
||||||
.iter()
|
|
||||||
.any(|a| a.normalize().scale() > self.minor_digits) =>
|
|
||||||
{
|
|
||||||
Ok(rule(Area::Ledger, 1))
|
|
||||||
}
|
|
||||||
"period_closed" => {
|
|
||||||
let day = e.date.as_deref().and_then(date).ok_or("no date")?;
|
|
||||||
if self.closed_through.iter().any(|end| day <= *end) {
|
|
||||||
Ok(rule(Area::Period, 2))
|
|
||||||
} else {
|
|
||||||
Err(format!("{day} is in no closed period"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"already_reversed" => {
|
|
||||||
let of = e.reverses.ok_or("no reversal named")?;
|
|
||||||
let idx = self.event_entry.get(&of).ok_or("names no entry")?;
|
|
||||||
if self.entries[*idx].reversed {
|
|
||||||
Ok(rule(Area::Ledger, 6))
|
|
||||||
} else {
|
|
||||||
Err(format!("event {of} was not yet reversed"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
other => Err(format!("{other:?} is not derived by the reference model")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Post an accepted event; `Err` says what the model cannot post.
|
|
||||||
fn post(&mut self, n: usize, e: &Event) -> Result<(), String> {
|
|
||||||
let day = e.date.as_deref().and_then(date);
|
|
||||||
let lines = |ls: &[crate::schema::LineSpec]| -> Option<Vec<Line>> {
|
|
||||||
ls.iter()
|
|
||||||
.map(|l| {
|
|
||||||
Some(Line {
|
|
||||||
account: l.account.clone(),
|
|
||||||
side: if l.side == "debit" {
|
|
||||||
Side::Debit
|
|
||||||
} else {
|
|
||||||
Side::Credit
|
|
||||||
},
|
|
||||||
amount: dec(&l.amount)?,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
let posted = if !e.lines.is_empty() {
|
|
||||||
Posted {
|
|
||||||
date: day.ok_or("undated")?,
|
|
||||||
lines: lines(&e.lines).ok_or("an unreadable amount")?,
|
|
||||||
closing: false,
|
|
||||||
reversed: false,
|
|
||||||
}
|
|
||||||
} else if let Some(of) = e.reverses {
|
|
||||||
let idx = *self
|
|
||||||
.event_entry
|
|
||||||
.get(&of)
|
|
||||||
.ok_or("reverses an event that posted nothing")?;
|
|
||||||
let original = self.entries[idx].clone();
|
|
||||||
self.entries[idx].reversed = true;
|
|
||||||
Posted {
|
|
||||||
date: day.ok_or("undated")?,
|
|
||||||
lines: original
|
|
||||||
.lines
|
|
||||||
.iter()
|
|
||||||
.map(|l| Line {
|
|
||||||
account: l.account.clone(),
|
|
||||||
side: match l.side {
|
|
||||||
Side::Debit => Side::Credit,
|
|
||||||
Side::Credit => Side::Debit,
|
|
||||||
},
|
|
||||||
amount: l.amount,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
closing: false,
|
|
||||||
reversed: false,
|
|
||||||
}
|
|
||||||
} else if let Some(b) = &e.bill {
|
|
||||||
let total = document_total(
|
|
||||||
b.amount.as_deref(),
|
|
||||||
&b.lines.iter().map(|l| l.amount.clone()).collect::<Vec<_>>(),
|
|
||||||
)?;
|
|
||||||
if b.lines.iter().any(|l| l.project.is_some()) {
|
|
||||||
return Err("project tags are not modelled".into());
|
|
||||||
}
|
|
||||||
Posted {
|
|
||||||
date: day.ok_or("undated")?,
|
|
||||||
lines: vec![
|
|
||||||
Line {
|
|
||||||
account: "6999".into(),
|
|
||||||
side: Side::Debit,
|
|
||||||
amount: total,
|
|
||||||
},
|
|
||||||
Line {
|
|
||||||
account: "2000".into(),
|
|
||||||
side: Side::Credit,
|
|
||||||
amount: total,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
closing: false,
|
|
||||||
reversed: false,
|
|
||||||
}
|
|
||||||
} else if let Some(i) = &e.invoice {
|
|
||||||
let total = document_total(
|
|
||||||
i.amount.as_deref(),
|
|
||||||
&i.lines.iter().map(|l| l.amount.clone()).collect::<Vec<_>>(),
|
|
||||||
)?;
|
|
||||||
if i.lines.iter().any(|l| l.project.is_some()) {
|
|
||||||
return Err("project tags are not modelled".into());
|
|
||||||
}
|
|
||||||
Posted {
|
|
||||||
date: day.ok_or("undated")?,
|
|
||||||
lines: vec![
|
|
||||||
Line {
|
|
||||||
account: "1200".into(),
|
|
||||||
side: Side::Debit,
|
|
||||||
amount: total,
|
|
||||||
},
|
|
||||||
Line {
|
|
||||||
account: "4000".into(),
|
|
||||||
side: Side::Credit,
|
|
||||||
amount: total,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
closing: false,
|
|
||||||
reversed: false,
|
|
||||||
}
|
|
||||||
} else if let Some(c) = &e.close {
|
|
||||||
if c.to != "retained_earnings" {
|
|
||||||
return Err("a close to the members is not modelled".into());
|
|
||||||
}
|
|
||||||
let end = day.ok_or("undated")?;
|
|
||||||
let mut out = Vec::new();
|
|
||||||
let mut net = Decimal::ZERO;
|
|
||||||
let codes: Vec<String> = self.types.keys().cloned().collect();
|
|
||||||
for code in codes {
|
|
||||||
let ty = self.types[&code].as_str();
|
|
||||||
if ty != "revenue" && ty != "expense" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let s = self.signed(&code, None, end, true);
|
|
||||||
if s.is_zero() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
net -= s; // net income is credit-normal: revenue - expense
|
|
||||||
out.push(Line {
|
|
||||||
account: code,
|
|
||||||
side: if s > Decimal::ZERO {
|
|
||||||
Side::Credit
|
|
||||||
} else {
|
|
||||||
Side::Debit
|
|
||||||
},
|
|
||||||
amount: s.abs(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if !net.is_zero() {
|
|
||||||
out.push(Line {
|
|
||||||
account: "3999".into(),
|
|
||||||
side: if net > Decimal::ZERO {
|
|
||||||
Side::Credit
|
|
||||||
} else {
|
|
||||||
Side::Debit
|
|
||||||
},
|
|
||||||
amount: net.abs(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
self.closed_through.push(end);
|
|
||||||
Posted {
|
|
||||||
date: end,
|
|
||||||
lines: out,
|
|
||||||
closing: true,
|
|
||||||
reversed: false,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err("an event the reference model does not post".into());
|
|
||||||
};
|
|
||||||
// The symbolic form of BS-entry-1: what is posted balances.
|
|
||||||
let (d, c): (Decimal, Decimal) =
|
|
||||||
posted
|
|
||||||
.lines
|
|
||||||
.iter()
|
|
||||||
.fold((Decimal::ZERO, Decimal::ZERO), |(d, c), l| match l.side {
|
|
||||||
Side::Debit => (d + l.amount, c),
|
|
||||||
Side::Credit => (d, c + l.amount),
|
|
||||||
});
|
|
||||||
if d != c {
|
|
||||||
return Err(format!(
|
|
||||||
"the example posts an unbalanced entry (debits {d}, credits {c})"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
self.entries.push(posted);
|
|
||||||
self.event_entry.insert(n, self.entries.len() - 1);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn document_total(amount: Option<&str>, lines: &[String]) -> Result<Decimal, String> {
|
|
||||||
match amount {
|
|
||||||
Some(a) => dec(a).ok_or_else(|| "an unreadable amount".into()),
|
|
||||||
None => lines
|
|
||||||
.iter()
|
|
||||||
.map(|l| dec(l).ok_or_else(|| "an unreadable amount".to_string()))
|
|
||||||
.sum(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derive what the specification says about `ex`.
|
|
||||||
pub fn derive(ex: &Example) -> Derivation {
|
|
||||||
let mut results = Vec::new();
|
|
||||||
let mut model = Model::new(ex);
|
|
||||||
// The expectations asked after event N are checked right after it; the
|
|
||||||
// rest at the end. The model keeps one state per point, so replay.
|
|
||||||
let mut usable = true;
|
|
||||||
let mut why_not = String::new();
|
|
||||||
let unsupported_setup = !ex.setup.assets.is_empty()
|
|
||||||
|| !ex.setup.schedules.is_empty()
|
|
||||||
|| !ex.setup.fixed_assets.is_empty()
|
|
||||||
|| !ex.setup.members.is_empty();
|
|
||||||
if unsupported_setup {
|
|
||||||
usable = false;
|
|
||||||
why_not = "the setup holds assets, schedules or members, which the reference model does not carry".into();
|
|
||||||
}
|
|
||||||
for (i, e) in ex.events.iter().enumerate() {
|
|
||||||
let n = i + 1;
|
|
||||||
if usable {
|
|
||||||
match &e.refused {
|
|
||||||
Some(reason) => match model.refusal_justified(e, reason) {
|
|
||||||
Ok(rule) => results.push((
|
|
||||||
format!("event {n} refused {reason}"),
|
|
||||||
Derived::Agrees { rule },
|
|
||||||
)),
|
|
||||||
Err(why) => results.push((
|
|
||||||
format!("event {n} refused {reason}"),
|
|
||||||
if why.contains("not derived") {
|
|
||||||
Derived::Skipped { why }
|
|
||||||
} else {
|
|
||||||
Derived::Disagrees {
|
|
||||||
rule: rule(Area::Entry, 1),
|
|
||||||
why,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
if let Err(why) = model.post(n, e) {
|
|
||||||
usable = false;
|
|
||||||
why_not = format!("event {n}: {why}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for x in ex.expect.iter().filter(|x| x.after == Some(n)) {
|
|
||||||
results.push(expectation(&model, x, usable, &why_not));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for x in ex.expect.iter().filter(|x| x.after.is_none()) {
|
|
||||||
results.push(expectation(&model, x, usable, &why_not));
|
|
||||||
}
|
|
||||||
Derivation {
|
|
||||||
example: ex.id.clone(),
|
|
||||||
results,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expectation(model: &Model, x: &Expect, usable: bool, why_not: &str) -> (String, Derived) {
|
|
||||||
let what = format!(
|
|
||||||
"{} {}",
|
|
||||||
x.query,
|
|
||||||
x.as_of.as_deref().or(x.to.as_deref()).unwrap_or("")
|
|
||||||
);
|
|
||||||
if !usable {
|
|
||||||
return (
|
|
||||||
what,
|
|
||||||
Derived::Skipped {
|
|
||||||
why: why_not.to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let Some(Values::Figures(want)) = &x.values else {
|
|
||||||
return (
|
|
||||||
what,
|
|
||||||
Derived::Skipped {
|
|
||||||
why: "no figures to derive".into(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
let derived = match x.query.as_str() {
|
|
||||||
"trial_balance" => x.as_of.as_deref().and_then(date).map(|d| {
|
|
||||||
(
|
|
||||||
rule(Area::Statement, 1),
|
|
||||||
want.keys()
|
|
||||||
.map(|k| (k.clone(), model.signed(k, None, d, true)))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
"balances" => x.as_of.as_deref().and_then(date).map(|d| {
|
|
||||||
(
|
|
||||||
rule(Area::Ledger, 3),
|
|
||||||
want.keys()
|
|
||||||
.map(|k| {
|
|
||||||
(
|
|
||||||
k.clone(),
|
|
||||||
model.on_normal_side(k, model.signed(k, None, d, true)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
"profit_and_loss" => {
|
|
||||||
let from = x.from.as_deref().and_then(date);
|
|
||||||
let to = x.to.as_deref().and_then(date);
|
|
||||||
from.zip(to).map(|(f, t)| {
|
|
||||||
let mut figs = Vec::new();
|
|
||||||
for k in want.keys() {
|
|
||||||
if k == "net_income" {
|
|
||||||
let mut net = Decimal::ZERO;
|
|
||||||
for (code, ty) in &model.types {
|
|
||||||
if ty == "revenue" || ty == "expense" {
|
|
||||||
net += model
|
|
||||||
.on_normal_side(code, model.signed(code, Some(f), t, false))
|
|
||||||
* if ty == "revenue" {
|
|
||||||
Decimal::ONE
|
|
||||||
} else {
|
|
||||||
-Decimal::ONE
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
figs.push((k.clone(), net));
|
|
||||||
} else {
|
|
||||||
figs.push((
|
|
||||||
k.clone(),
|
|
||||||
model.on_normal_side(k, model.signed(k, Some(f), t, false)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(rule(Area::Statement, 3), figs)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"entry" => x.entry.and_then(|n| model.event_entry.get(&n)).map(|idx| {
|
|
||||||
let e = &model.entries[*idx];
|
|
||||||
let figs = want
|
|
||||||
.keys()
|
|
||||||
.map(|k| {
|
|
||||||
let (code, side) = k.split_once('.').unwrap_or((k, ""));
|
|
||||||
let v: Decimal = e
|
|
||||||
.lines
|
|
||||||
.iter()
|
|
||||||
.filter(|l| {
|
|
||||||
l.account == code
|
|
||||||
&& matches!(
|
|
||||||
(side, l.side),
|
|
||||||
("debit", Side::Debit) | ("credit", Side::Credit)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(|l| l.amount)
|
|
||||||
.sum();
|
|
||||||
(k.clone(), v)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
(rule(Area::Ledger, 4), figs)
|
|
||||||
}),
|
|
||||||
other => {
|
|
||||||
return (
|
|
||||||
what,
|
|
||||||
Derived::Skipped {
|
|
||||||
why: format!("{other} is not derived by the reference model"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let Some((rule, figs)) = derived else {
|
|
||||||
return (
|
|
||||||
what,
|
|
||||||
Derived::Skipped {
|
|
||||||
why: "an undated or unresolved query".into(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
for (k, got) in figs {
|
|
||||||
match want.get(&k).and_then(|w| dec(w)) {
|
|
||||||
Some(w) if w == got => {}
|
|
||||||
Some(w) => {
|
|
||||||
return (
|
|
||||||
what,
|
|
||||||
Derived::Disagrees {
|
|
||||||
rule,
|
|
||||||
why: format!("{k}: the example says {w}, the rule derives {got}"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
return (
|
|
||||||
what,
|
|
||||||
Derived::Disagrees {
|
|
||||||
rule,
|
|
||||||
why: format!("{k}: no exact figure"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The laws on the whole book, at the day asked: BS-statement-2.
|
|
||||||
if let (true, Some(d)) = (
|
|
||||||
matches!(x.query.as_str(), "trial_balance" | "balances"),
|
|
||||||
x.as_of.as_deref().and_then(date),
|
|
||||||
) {
|
|
||||||
let (mut a, mut l, mut e, mut r, mut xp) = (
|
|
||||||
Decimal::ZERO,
|
|
||||||
Decimal::ZERO,
|
|
||||||
Decimal::ZERO,
|
|
||||||
Decimal::ZERO,
|
|
||||||
Decimal::ZERO,
|
|
||||||
);
|
|
||||||
let mut net = Decimal::ZERO;
|
|
||||||
for (code, ty) in &model.types {
|
|
||||||
let s = model.signed(code, None, d, true);
|
|
||||||
net += s;
|
|
||||||
let n = model.on_normal_side(code, s);
|
|
||||||
match ty.as_str() {
|
|
||||||
"asset" => a += n,
|
|
||||||
"liability" => l += n,
|
|
||||||
"equity" => e += n,
|
|
||||||
"revenue" => r += n,
|
|
||||||
"expense" => xp += n,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !net.is_zero() {
|
|
||||||
return (
|
|
||||||
what,
|
|
||||||
Derived::Disagrees {
|
|
||||||
rule: self::rule(Area::Statement, 1),
|
|
||||||
why: format!("the books do not net to zero ({net})"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if a != l + e + (r - xp) {
|
|
||||||
return (
|
|
||||||
what,
|
|
||||||
Derived::Disagrees {
|
|
||||||
rule: self::rule(Area::Statement, 2),
|
|
||||||
why: format!("A {a} != L {l} + E {e} + (R {r} - X {xp})"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(what, Derived::Agrees { rule })
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
//! The interface an accounting engine implements to be checked against
|
|
||||||
//! bookspec (`plan/served.md` P13, D-34).
|
|
||||||
//!
|
|
||||||
//! bookspec is a specification first: its examples are the concrete cases
|
|
||||||
//! (`bookspec/examples/*.toml`), its rule identifiers the normative rules
|
|
||||||
//! (`BS-<area>-<n>`), and this trait the only thing an engine has to
|
|
||||||
//! provide for the runner to check it. The engine's answers are compared
|
|
||||||
//! with the examples' expected values by the runner; an engine is never
|
|
||||||
//! asked whether it agrees with itself.
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::rules::{Area, Rule, rule_for_id, rule_for_refusal};
|
|
||||||
use crate::schema::{ChangeSpec, Event, Query, Setup};
|
|
||||||
|
|
||||||
/// A bookspec rule identifier: `BS-<area>-<n>` (e.g. `BS-entry-1`).
|
|
||||||
///
|
|
||||||
/// The areas are the specification's subjects (`Area`), and `n` counts
|
|
||||||
/// from 1 within an area. The catalogue in [`mod@crate::rules`] is the list of
|
|
||||||
/// every identifier the specification states; an identifier that is not in
|
|
||||||
/// it is a typo, not a rule.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct RuleId(String);
|
|
||||||
|
|
||||||
/// Why a string is not a bookspec rule identifier.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct RuleIdError {
|
|
||||||
what: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for RuleIdError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.what)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for RuleIdError {}
|
|
||||||
|
|
||||||
impl RuleId {
|
|
||||||
/// The identifier of area `area`, rule number `n` (counting from 1).
|
|
||||||
pub fn new(area: Area, n: u32) -> Self {
|
|
||||||
assert!(n >= 1, "bookspec rule numbers count from 1");
|
|
||||||
Self(format!("BS-{}-{n}", area.key()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse `BS-<area>-<n>`, refusing an unknown area, a missing or
|
|
||||||
/// non-numeric number, and a number that is zero or written with a
|
|
||||||
/// leading zero (one spelling per rule).
|
|
||||||
pub fn parse(s: &str) -> Result<Self, RuleIdError> {
|
|
||||||
let rest = s.strip_prefix("BS-").ok_or_else(|| RuleIdError {
|
|
||||||
what: format!("{s:?} is no bookspec rule id (BS-<area>-<n>)"),
|
|
||||||
})?;
|
|
||||||
let (area, n) = rest.rsplit_once('-').ok_or_else(|| RuleIdError {
|
|
||||||
what: format!("{s:?} is no bookspec rule id (BS-<area>-<n>)"),
|
|
||||||
})?;
|
|
||||||
let area = Area::parse(area).ok_or_else(|| RuleIdError {
|
|
||||||
what: format!("{s:?}: {:?} is no bookspec area", area),
|
|
||||||
})?;
|
|
||||||
if n.is_empty() || !n.bytes().all(|b| b.is_ascii_digit()) {
|
|
||||||
return Err(RuleIdError {
|
|
||||||
what: format!("{s:?}: {n:?} is no rule number"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if n.starts_with('0') {
|
|
||||||
return Err(RuleIdError {
|
|
||||||
what: format!("{s:?}: rule numbers count from 1, without a leading zero"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let n: u32 = n.parse().map_err(|_| RuleIdError {
|
|
||||||
what: format!("{s:?}: {n:?} is no rule number"),
|
|
||||||
})?;
|
|
||||||
if n == 0 {
|
|
||||||
return Err(RuleIdError {
|
|
||||||
what: format!("{s:?}: rule numbers count from 1"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(Self::new(area, n))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The identifier as the specification writes it.
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The area it belongs to.
|
|
||||||
pub fn area(&self) -> Area {
|
|
||||||
// Constructed only by `new`/`parse`, both of which check the area.
|
|
||||||
Area::parse(self.0.split('-').nth(1).expect("a checked rule id"))
|
|
||||||
.expect("a checked rule id's area")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The rule's entry in the catalogue, if the specification states it.
|
|
||||||
pub fn rule(&self) -> Option<&'static Rule> {
|
|
||||||
rule_for_id(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for RuleId {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.write_str(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for RuleId {
|
|
||||||
type Err = RuleIdError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Self::parse(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An engine's refusal: the rule that refuses, and why in the example's
|
|
||||||
/// words (bookspec/README.md's refusal vocabulary, never an engine's own
|
|
||||||
/// message).
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Refusal {
|
|
||||||
/// The bookspec rule the refusal belongs to.
|
|
||||||
pub rule: RuleId,
|
|
||||||
/// What was refused, in the specification's words.
|
|
||||||
pub reason: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Refusal {
|
|
||||||
/// A refusal of `reason` under `rule`.
|
|
||||||
pub fn new(rule: RuleId, reason: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
rule,
|
|
||||||
reason: reason.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A refusal of `reason`, under the rule the specification assigns
|
|
||||||
/// that refusal (see [`rule_for_refusal`]); an unknown reason is a
|
|
||||||
/// specification defect, and the first rule of the ledger area is
|
|
||||||
/// named so the report still has an identifier to show.
|
|
||||||
pub fn of_reason(reason: impl Into<String>) -> Self {
|
|
||||||
let reason = reason.into();
|
|
||||||
let rule = rule_for_refusal(&reason)
|
|
||||||
.map(|r| r.id.clone())
|
|
||||||
.unwrap_or_else(|| RuleId::new(Area::Ledger, 1));
|
|
||||||
Self { rule, reason }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What an engine declares about itself: the rules it takes the
|
|
||||||
/// conformance test on, and the option it picks for each of the
|
|
||||||
/// specification's choices (`CHOICE`: the specification names the options,
|
|
||||||
/// an engine declares its pick; `plan/served.md` P13 step 3).
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
||||||
pub struct Claims {
|
|
||||||
/// The rule identifiers the engine claims.
|
|
||||||
pub rules: BTreeSet<RuleId>,
|
|
||||||
/// The choice rules whose option the engine picks, and the option.
|
|
||||||
pub choices: BTreeMap<RuleId, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Claims {
|
|
||||||
/// The claims of an engine that claims `rules`, with no choices.
|
|
||||||
pub fn new(rules: impl IntoIterator<Item = RuleId>) -> Self {
|
|
||||||
Self {
|
|
||||||
rules: rules.into_iter().collect(),
|
|
||||||
choices: BTreeMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// True when `rule` is claimed.
|
|
||||||
pub fn claims(&self, rule: &RuleId) -> bool {
|
|
||||||
self.rules.contains(rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Declare the option this engine picks for `rule`.
|
|
||||||
pub fn with_choice(mut self, rule: RuleId, option: impl Into<String>) -> Self {
|
|
||||||
self.choices.insert(rule, option.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The option this engine picks for `rule`, if it declares one.
|
|
||||||
pub fn choice(&self, rule: &RuleId) -> Option<&str> {
|
|
||||||
self.choices.get(rule).map(String::as_str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An accounting engine, as bookspec checks it.
|
|
||||||
///
|
|
||||||
/// The four methods are the plan's (`plan/served.md` 8.2 P13 step 2); the
|
|
||||||
/// fifth, [`Engine::change`], is the one departure: an example's
|
|
||||||
/// `[[changes]]` (a rename, the tax-category policy, an asset correction)
|
|
||||||
/// posts no entry, so it is neither a setup nor an event, and the plan's
|
|
||||||
/// four methods have no path for it. It is required, never defaulted, so
|
|
||||||
/// an engine cannot silently skip the examples that use one.
|
|
||||||
pub trait Engine {
|
|
||||||
/// Set the books up: the chart, the fixed assets, the schedules, the
|
|
||||||
/// parties, the members, the payees and their dated decisions, the
|
|
||||||
/// payment methods and the projects.
|
|
||||||
fn load(&mut self, setup: &Setup) -> Result<(), Refusal>;
|
|
||||||
|
|
||||||
/// Apply one event: an entry, a reversal, a close, a bill, an invoice,
|
|
||||||
/// a depreciation run, a disposal, a schedules run or a schedule's
|
|
||||||
/// registration. An event the engine refuses leaves the books as they
|
|
||||||
/// were.
|
|
||||||
fn apply(&mut self, event: &Event) -> Result<(), Refusal>;
|
|
||||||
|
|
||||||
/// Answer one query with the engine's own figures. A figure is an
|
|
||||||
/// exact decimal string (never a JSON number: no engine may round a
|
|
||||||
/// cent through a float), a date is `YYYY-MM-DD`, and a format
|
|
||||||
/// query's value is its lines or rows. A query the engine refuses
|
|
||||||
/// (a period the rule refuses, a command it refuses) is an
|
|
||||||
/// [`Err`].
|
|
||||||
fn query(&self, q: &Query) -> Result<serde_json::Value, Refusal>;
|
|
||||||
|
|
||||||
/// The rules and choices this engine declares.
|
|
||||||
fn claims(&self) -> Claims;
|
|
||||||
|
|
||||||
/// Apply one change that posts no entry (a `[[changes]]` entry).
|
|
||||||
fn change(&mut self, change: &ChangeSpec) -> Result<(), Refusal>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The refusal an engine reports for an error it has no example-word
|
|
||||||
/// reason for: the reason as given, under `fallback`.
|
|
||||||
pub fn fallback_refusal(fallback: RuleId, reason: impl Into<String>) -> Refusal {
|
|
||||||
Refusal::new(fallback, reason)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
//! bookspec's checker: the example schema, the rule identifiers, and the
|
|
||||||
//! [`Engine`] trait an accounting engine implements (`plan/served.md` P13,
|
|
||||||
//! D-34).
|
|
||||||
//!
|
|
||||||
//! bookspec is a specification first: every rule is stated normatively,
|
|
||||||
//! sourced, and demonstrated by concrete examples (`bookspec/examples/`).
|
|
||||||
//! This crate is the machine-readable part: [`Example`] is the schema and
|
|
||||||
//! its parser, [`mod@rules`] the catalogue (`BS-<area>-<n>`), and [`Engine`]
|
|
||||||
//! the only interface a conformance runner needs. The runner lives in
|
|
||||||
//! `bookspec-runner`; wanderbooks' adapter is
|
|
||||||
//! `crates/ledger-core/tests/bookspec.rs`.
|
|
||||||
//!
|
|
||||||
//! The crate knows nothing of any engine: no money type, no ledger, no
|
|
||||||
//! store. That is what lets a second engine (the GnuCash oracle, later) be
|
|
||||||
//! checked by the same examples.
|
|
||||||
|
|
||||||
pub mod derive;
|
|
||||||
pub mod engine;
|
|
||||||
pub mod rules;
|
|
||||||
pub mod schema;
|
|
||||||
|
|
||||||
pub use derive::{Derivation, Derived, derive};
|
|
||||||
pub use engine::{Claims, Engine, Refusal, RuleId, RuleIdError, fallback_refusal};
|
|
||||||
pub use rules::{Area, Rule, Status, covered_rules, rule, rule_for_id, rule_for_refusal, rules};
|
|
||||||
pub use schema::*;
|
|
||||||
@@ -1,594 +0,0 @@
|
|||||||
//! bookspec's rule identifiers and their catalogue.
|
|
||||||
//!
|
|
||||||
//! Every rule of `docs/spec/rules.md` (B-1 to B-45) has one bookspec
|
|
||||||
//! identifier, `BS-<area>-<n>`, allocated by [`Area`] and numbered in
|
|
||||||
//! B-rule order within its area. `bookspec/spec/rules-index.md` is the
|
|
||||||
//! table, and a test keeps the two in step. The rules from B-39 on are
|
|
||||||
//! allocated in `plan/served.md` 8.0.3 but not yet stated; they carry an
|
|
||||||
//! identifier and an area and no status, so a later unit fills them in
|
|
||||||
//! without moving anyone else's number.
|
|
||||||
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use crate::engine::RuleId;
|
|
||||||
|
|
||||||
/// One of bookspec's areas: the subject a rule belongs to.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub enum Area {
|
|
||||||
/// The mechanics of an entry: lines, sides, amounts, dates.
|
|
||||||
Entry,
|
|
||||||
/// The ledger: the chart, accounts, the journal, the format.
|
|
||||||
Ledger,
|
|
||||||
/// Reports: the trial balance, the accounting equation, statements.
|
|
||||||
Statement,
|
|
||||||
/// Periods: closes, closed periods, what an omitted date means.
|
|
||||||
Period,
|
|
||||||
/// Partners and parties: shares, members' capital, the directory.
|
|
||||||
Partner,
|
|
||||||
/// Fixed assets and capitalization.
|
|
||||||
Asset,
|
|
||||||
/// Prepaid expenses and deferred revenue.
|
|
||||||
Prepaid,
|
|
||||||
/// Receivables and payables: documents, aging, settlements.
|
|
||||||
Arap,
|
|
||||||
/// Revenue: when it is recognized and how it is presented.
|
|
||||||
Revenue,
|
|
||||||
/// Information returns: 1099s and their thresholds.
|
|
||||||
Tax1099,
|
|
||||||
/// Interchange formats: the journal format gate and the IIF.
|
|
||||||
Format,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Area {
|
|
||||||
/// Every area, in the order the specification lists them.
|
|
||||||
pub const ALL: [Area; 11] = [
|
|
||||||
Area::Entry,
|
|
||||||
Area::Ledger,
|
|
||||||
Area::Statement,
|
|
||||||
Area::Period,
|
|
||||||
Area::Partner,
|
|
||||||
Area::Asset,
|
|
||||||
Area::Prepaid,
|
|
||||||
Area::Arap,
|
|
||||||
Area::Revenue,
|
|
||||||
Area::Tax1099,
|
|
||||||
Area::Format,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// The area's key in a rule identifier.
|
|
||||||
pub fn key(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Area::Entry => "entry",
|
|
||||||
Area::Ledger => "ledger",
|
|
||||||
Area::Statement => "statement",
|
|
||||||
Area::Period => "period",
|
|
||||||
Area::Partner => "partner",
|
|
||||||
Area::Asset => "asset",
|
|
||||||
Area::Prepaid => "prepaid",
|
|
||||||
Area::Arap => "arap",
|
|
||||||
Area::Revenue => "revenue",
|
|
||||||
Area::Tax1099 => "1099",
|
|
||||||
Area::Format => "format",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The area `key` names.
|
|
||||||
pub fn parse(key: &str) -> Option<Self> {
|
|
||||||
Self::ALL.into_iter().find(|a| a.key() == key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The status of a rule, in bookspec's vocabulary: what its statement
|
|
||||||
/// rests on. `docs/spec/rules.md`'s "DECISION" is bookspec's [`Status::Choice`]
|
|
||||||
/// (`plan/served.md` P13 step 3): the specification names the options, and
|
|
||||||
/// an engine declares its pick through [`crate::Claims`].
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub enum Status {
|
|
||||||
/// A cited authority states the rule.
|
|
||||||
Authority,
|
|
||||||
/// Common practice, where no authority states the rule.
|
|
||||||
Convention,
|
|
||||||
/// A human's or the house's choice among options the specification
|
|
||||||
/// names.
|
|
||||||
Choice,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Status {
|
|
||||||
/// The status as the specification writes it.
|
|
||||||
pub fn key(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Status::Authority => "AUTHORITY",
|
|
||||||
Status::Convention => "CONVENTION",
|
|
||||||
Status::Choice => "CHOICE",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The status `key` names.
|
|
||||||
pub fn parse(key: &str) -> Option<Self> {
|
|
||||||
[Status::Authority, Status::Convention, Status::Choice]
|
|
||||||
.into_iter()
|
|
||||||
.find(|s| s.key() == key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One rule of the specification.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Rule {
|
|
||||||
/// Its number in `docs/spec/rules.md`, 1 to 45.
|
|
||||||
pub b: u8,
|
|
||||||
/// Its bookspec identifier.
|
|
||||||
pub id: RuleId,
|
|
||||||
/// Its area.
|
|
||||||
pub area: Area,
|
|
||||||
/// Its normative title, as `docs/spec/rules.md` states it.
|
|
||||||
pub title: &'static str,
|
|
||||||
/// What its statement rests on; `None` for a rule allocated but not
|
|
||||||
/// yet stated (B-39 to B-45, `plan/served.md` 8.0.3).
|
|
||||||
pub status: Option<Status>,
|
|
||||||
/// The example ids that demonstrate it, by `Example::id`.
|
|
||||||
pub examples: &'static [&'static str],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rule {
|
|
||||||
/// True when the rule has at least one example to check it.
|
|
||||||
pub fn has_examples(&self) -> bool {
|
|
||||||
!self.examples.is_empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The catalogue: every rule, in B-rule order.
|
|
||||||
pub fn rules() -> &'static [Rule] {
|
|
||||||
static RULES: LazyLock<Vec<Rule>> = LazyLock::new(build);
|
|
||||||
&RULES
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The rule `b` (1 to 45).
|
|
||||||
pub fn rule(b: u8) -> Option<&'static Rule> {
|
|
||||||
rules().iter().find(|r| r.b == b)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The rule `id` names, if the specification states it.
|
|
||||||
pub fn rule_for_id(id: &RuleId) -> Option<&'static Rule> {
|
|
||||||
rules().iter().find(|r| &r.id == id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The rule a refusal belongs to, by its reason in the examples' words
|
|
||||||
/// (bookspec/README.md).
|
|
||||||
pub fn rule_for_refusal(reason: &str) -> Option<&'static Rule> {
|
|
||||||
let b = match reason {
|
|
||||||
"unbalanced" | "too_few_lines" => 1,
|
|
||||||
"negative_amount" | "zero_amount" => 2,
|
|
||||||
"finer_than_minor_unit" => 3,
|
|
||||||
"parent_type_mismatch" => 4,
|
|
||||||
"already_reversed" => 10,
|
|
||||||
"period_closed" => 12,
|
|
||||||
"end_before_start" | "start_after_today" => 30,
|
|
||||||
"blank" | "unchanged" | "sibling_has_it" | "holds_a_tin" | "not_one_line"
|
|
||||||
| "code_taken" => 31,
|
|
||||||
"tax_category_required" => 32,
|
|
||||||
"depreciation_past_disposal"
|
|
||||||
| "entry_irreversible"
|
|
||||||
| "already_disposed"
|
|
||||||
| "invalid_depreciation_period" => 15,
|
|
||||||
"invalid_schedule" => 16,
|
|
||||||
"untagged_control_line" => 38,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
rule(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The rules an engine can claim today: those with at least one example.
|
|
||||||
/// A rule with no example has no evidence, so an engine cannot claim it
|
|
||||||
/// (`plan/served.md` P13 step 4 fills the rest in).
|
|
||||||
pub fn covered_rules() -> Vec<&'static Rule> {
|
|
||||||
rules().iter().filter(|r| r.has_examples()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build() -> Vec<Rule> {
|
|
||||||
// The areas' ordinals are assigned here in B-rule order, one line per
|
|
||||||
// rule: an area's n is its place in `Area::ALL`'s set of rules.
|
|
||||||
let mut next = std::collections::BTreeMap::new();
|
|
||||||
let mut out = Vec::with_capacity(45);
|
|
||||||
for (b, area, title, status, examples) in SPEC {
|
|
||||||
let n: u32 = *next.entry(*area).and_modify(|n| *n += 1).or_insert(1);
|
|
||||||
out.push(Rule {
|
|
||||||
b: *b,
|
|
||||||
id: RuleId::new(*area, n),
|
|
||||||
area: *area,
|
|
||||||
title,
|
|
||||||
status: *status,
|
|
||||||
examples,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
type Spec = &'static [(
|
|
||||||
u8,
|
|
||||||
Area,
|
|
||||||
&'static str,
|
|
||||||
Option<Status>,
|
|
||||||
&'static [&'static str],
|
|
||||||
)];
|
|
||||||
|
|
||||||
const SPEC: Spec = &[
|
|
||||||
(
|
|
||||||
1,
|
|
||||||
Area::Entry,
|
|
||||||
"Every entry is a double entry: debits equal credits",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&[
|
|
||||||
"entry-an-unbalanced-entry-is-refused",
|
|
||||||
"statement-the-books-balance-and-the-equation-holds",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
2,
|
|
||||||
Area::Entry,
|
|
||||||
"A line's amount is positive; its side gives the direction",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&["entry-a-line-amount-is-positive"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
3,
|
|
||||||
Area::Ledger,
|
|
||||||
"One money through the ledger; foreign amounts at the transaction-date rate",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["ledger-a-fraction-of-a-cent-is-refused"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
4,
|
|
||||||
Area::Ledger,
|
|
||||||
"Five elements, closed list",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["ledger-a-child-account-has-its-parents-type"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
5,
|
|
||||||
Area::Ledger,
|
|
||||||
"Normal balances",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&["statement-the-books-balance-and-the-equation-holds"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
6,
|
|
||||||
Area::Statement,
|
|
||||||
"The trial balance balances",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["statement-the-books-balance-and-the-equation-holds"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
7,
|
|
||||||
Area::Statement,
|
|
||||||
"The accounting equation",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["statement-the-books-balance-and-the-equation-holds"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
8,
|
|
||||||
Area::Ledger,
|
|
||||||
"Posted is forever: the book is written only forward",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["ledger-a-mistake-is-reversed-never-erased"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
9,
|
|
||||||
Area::Ledger,
|
|
||||||
"A mistake is corrected by an opposite entry, never erased",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["ledger-a-mistake-is-reversed-never-erased"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
10,
|
|
||||||
Area::Ledger,
|
|
||||||
"An entry is reversed at most once; a closing entry never",
|
|
||||||
Some(Status::Choice),
|
|
||||||
&["ledger-a-mistake-is-reversed-never-erased"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
11,
|
|
||||||
Area::Period,
|
|
||||||
"Revenue and expenses close into profit and loss, and that into capital",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["period-a-close-sweeps-the-year-into-capital"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
12,
|
|
||||||
Area::Period,
|
|
||||||
"A closed period takes no new postings; periods close in order",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&["period-a-close-sweeps-the-year-into-capital"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
13,
|
|
||||||
Area::Partner,
|
|
||||||
"Partners share income by the partnership agreement; capital accounts carry it",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&[
|
|
||||||
"members-close.odd-cent-across-closes",
|
|
||||||
"members-close.statement-before-and-after-a-close",
|
|
||||||
"members-close.thirds-over-twelve-months",
|
|
||||||
"members-close.true-up-of-a-per-close-split",
|
|
||||||
"p3-party-members-equity-and-k1",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
14,
|
|
||||||
Area::Statement,
|
|
||||||
"Statements: P&L for a period; cash flows by the direct method",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&[
|
|
||||||
"cash-flow.debt-to-equity",
|
|
||||||
"cash-flow.government-fees",
|
|
||||||
"cash-flow.paid-on-the-companys-behalf",
|
|
||||||
"cash-flow.part-cash-purchase",
|
|
||||||
"cash-flow.reversal-of-a-reversal",
|
|
||||||
"cash-flow.reversed-property-contribution",
|
|
||||||
"cash-flow.two-member-llc-first-year",
|
|
||||||
"period-a-close-sweeps-the-year-into-capital",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
15,
|
|
||||||
Area::Asset,
|
|
||||||
"Depreciation: straight line, salvage never depreciated, partial months by days",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&[
|
|
||||||
"assets-acquired-before-in-service",
|
|
||||||
"assets-a-reversed-run-is-not-posted",
|
|
||||||
"assets-book-value-is-what-the-books-carry",
|
|
||||||
"depreciation-catch-up-month",
|
|
||||||
"depreciation-disposal-after-a-late-change-undone",
|
|
||||||
"depreciation-disposal-before-a-later-run",
|
|
||||||
"depreciation-disposal-in-final-month",
|
|
||||||
"depreciation-disposal-inside-a-posted-month",
|
|
||||||
"depreciation-disposal-mid-month",
|
|
||||||
"depreciation-final-month-by-days",
|
|
||||||
"depreciation-final-month-drift",
|
|
||||||
"depreciation-reversal-before-its-run-is-refused",
|
|
||||||
"depreciation-run-reversed-after-its-month",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
16,
|
|
||||||
Area::Prepaid,
|
|
||||||
"Prepaid expenses and deferred revenue are recognized over the period benefited or earned",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&[
|
|
||||||
"schedule-daily-deferred-revenue",
|
|
||||||
"schedule-daily-small-first-and-last-month",
|
|
||||||
"schedule-equal-months-start-on-the-first",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
17,
|
|
||||||
Area::Revenue,
|
|
||||||
"Platform payouts are recorded gross when the business is the principal",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
18,
|
|
||||||
Area::Arap,
|
|
||||||
"Aging buckets: Current, 1-30, 31-60, 61-90, 90+ days past due",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
19,
|
|
||||||
Area::Tax1099,
|
|
||||||
"1099 reporting thresholds, payment methods, and decisions",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&[
|
|
||||||
"1099-a-dated-status-applies-by-payment-date",
|
|
||||||
"1099-a-members-check-through-financing-counts",
|
|
||||||
"1099-and-party-totals-for-a-members-payment",
|
|
||||||
"1099-card-payments-are-excluded",
|
|
||||||
"1099-money-between-own-accounts-pays-no-one",
|
|
||||||
"1099-money-from-a-payee-is-netted-only-as-a-refund",
|
|
||||||
"1099-the-threshold-is-the-years",
|
|
||||||
"1099-worksheet-through-as-of",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
20,
|
|
||||||
Area::Ledger,
|
|
||||||
"Every transaction has supporting documents; records are kept",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
21,
|
|
||||||
Area::Entry,
|
|
||||||
"Exact amounts in the books; rounding only on returns",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["entry-cents-add-exactly"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
22,
|
|
||||||
Area::Asset,
|
|
||||||
"Expense or capitalize (book policy, mirrors tax)",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
23,
|
|
||||||
Area::Ledger,
|
|
||||||
"Control roles: one account each, of the right kind, moved only when empty",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&["arap-hand-control-posting-refused"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
24,
|
|
||||||
Area::Asset,
|
|
||||||
"Asset tags: one label per asset, never reused",
|
|
||||||
Some(Status::Choice),
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
25,
|
|
||||||
Area::Asset,
|
|
||||||
"Tax figures beside book value: the section 723 basis, the depreciable basis, the section 704(c) built-in gain",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&[
|
|
||||||
"assets-723-basis-apart-from-depreciable-basis",
|
|
||||||
"assets-built-in-gain-fixed-at-contribution",
|
|
||||||
"assets-four-contributed-devices",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
26,
|
|
||||||
Area::Partner,
|
|
||||||
"Parties: one directory, no full TIN in the books",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["p3-party-tin-last4"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
27,
|
|
||||||
Area::Format,
|
|
||||||
"The journal format gate: newer is not corrupt",
|
|
||||||
Some(Status::Choice),
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
28,
|
|
||||||
Area::Entry,
|
|
||||||
"What new commands take: four-digit years, amounts a line can carry",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
29,
|
|
||||||
Area::Ledger,
|
|
||||||
"The books keep how they are kept",
|
|
||||||
Some(Status::Choice),
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
30,
|
|
||||||
Area::Period,
|
|
||||||
"A report asked for without dates covers this fiscal year so far",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["default-period-fiscal-year-to-date"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
31,
|
|
||||||
Area::Ledger,
|
|
||||||
"Renaming an account changes its label, nothing else",
|
|
||||||
Some(Status::Choice),
|
|
||||||
&["rename-changes-no-amount"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
32,
|
|
||||||
Area::Ledger,
|
|
||||||
"Every revenue and expense account maps to a line of the return",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["tax-category-required"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
33,
|
|
||||||
Area::Statement,
|
|
||||||
"A package as of a day shows the books through that day",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&["as-of-package-stops-at-as-of"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
34,
|
|
||||||
Area::Format,
|
|
||||||
"The QuickBooks Desktop file (IIF): plain ASCII, every entry through as_of, closes left out, classes carried",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&["iif-quickbooks-journal-file"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
35,
|
|
||||||
Area::Partner,
|
|
||||||
"A party's part of an entry, and a party's totals",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&[
|
|
||||||
"p3-party-accrual-counts-once",
|
|
||||||
"p3-party-distribution-still-owed",
|
|
||||||
"p3-party-gift-card-is-a-promise",
|
|
||||||
"p3-party-members-equity-and-k1",
|
|
||||||
"p3-party-own-liability-never-pays",
|
|
||||||
"p3-party-reversed-card-payment",
|
|
||||||
"p3-party-signs-and-reversals",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
36,
|
|
||||||
Area::Partner,
|
|
||||||
"The K-1 worksheet reports tax-basis capital beside book capital",
|
|
||||||
Some(Status::Authority),
|
|
||||||
&[
|
|
||||||
"partnership-k1-tax-basis-capital",
|
|
||||||
"partnership-k1-tax-basis-odd-cents",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
37,
|
|
||||||
Area::Statement,
|
|
||||||
"A project is a class with its facts; its figures are its tagged lines",
|
|
||||||
Some(Status::Choice),
|
|
||||||
&[
|
|
||||||
"projects-invoice-and-bill-lines-count-by-project",
|
|
||||||
"projects-net-is-its-tagged-lines",
|
|
||||||
"projects-on-spec-costs-are-expenses",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
38,
|
|
||||||
Area::Arap,
|
|
||||||
"A document's figures are its ledger lines that stand; a control account is its documents",
|
|
||||||
Some(Status::Choice),
|
|
||||||
&["arap-hand-control-posting-refused"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
39,
|
|
||||||
Area::Arap,
|
|
||||||
"Terms: when a document is due (allocated, not yet stated)",
|
|
||||||
None,
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
40,
|
|
||||||
Area::Arap,
|
|
||||||
"Numbers and the issued copy (allocated, not yet stated)",
|
|
||||||
None,
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
41,
|
|
||||||
Area::Arap,
|
|
||||||
"A debt that will not be collected is written off, and a recovery is income",
|
|
||||||
Some(Status::Choice),
|
|
||||||
&["bad-debt-direct-write-off"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
42,
|
|
||||||
Area::Arap,
|
|
||||||
"A settlement is exact, one party, and names its lines; the gross discount and the gross fee",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
43,
|
|
||||||
Area::Entry,
|
|
||||||
"Drafts and evidence (allocated, not yet stated)",
|
|
||||||
None,
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
44,
|
|
||||||
Area::Ledger,
|
|
||||||
"Evidence is read from the bytes, never trusted over them; identical files are kept once",
|
|
||||||
Some(Status::Convention),
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
45,
|
|
||||||
Area::Ledger,
|
|
||||||
"Audits and attestations (allocated, not yet stated)",
|
|
||||||
None,
|
|
||||||
&[],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,107 +0,0 @@
|
|||||||
//! The specification's own checker: every example's expected figures are
|
|
||||||
//! derived from its rules' symbolic forms, engine-free (P13 step 5).
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use bookspec_checker::{Derived, Example, derive};
|
|
||||||
|
|
||||||
fn examples() -> Vec<Example> {
|
|
||||||
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples");
|
|
||||||
let mut files: Vec<PathBuf> = std::fs::read_dir(dir)
|
|
||||||
.unwrap()
|
|
||||||
.map(|e| e.unwrap().path())
|
|
||||||
.filter(|p| p.extension().is_some_and(|x| x == "toml"))
|
|
||||||
.collect();
|
|
||||||
files.sort();
|
|
||||||
files
|
|
||||||
.iter()
|
|
||||||
.map(|f| Example::from_toml(&std::fs::read_to_string(f).unwrap()).unwrap())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No example's figure disagrees with what its rule derives.
|
|
||||||
#[test]
|
|
||||||
fn no_example_disagrees_with_its_derivation() {
|
|
||||||
let mut bad = Vec::new();
|
|
||||||
let mut agreed = 0;
|
|
||||||
for ex in examples() {
|
|
||||||
let d = derive(&ex);
|
|
||||||
bad.extend(d.disagreements());
|
|
||||||
agreed += d.agreed();
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
bad.is_empty(),
|
|
||||||
"the examples and the rules disagree:\n{}",
|
|
||||||
bad.join("\n")
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
agreed >= 30,
|
|
||||||
"the reference model derived only {agreed} results"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The nine examples written for the ledger's own laws are derived in
|
|
||||||
/// full: not one of their results is skipped.
|
|
||||||
#[test]
|
|
||||||
fn the_law_examples_are_derived_in_full() {
|
|
||||||
for id in [
|
|
||||||
"entry-an-unbalanced-entry-is-refused",
|
|
||||||
"entry-a-line-amount-is-positive",
|
|
||||||
"ledger-a-fraction-of-a-cent-is-refused",
|
|
||||||
"statement-the-books-balance-and-the-equation-holds",
|
|
||||||
"ledger-a-mistake-is-reversed-never-erased",
|
|
||||||
"period-a-close-sweeps-the-year-into-capital",
|
|
||||||
"entry-cents-add-exactly",
|
|
||||||
] {
|
|
||||||
let ex = examples().into_iter().find(|e| e.id == id).unwrap();
|
|
||||||
let d = derive(&ex);
|
|
||||||
assert!(!d.results.is_empty(), "{id}");
|
|
||||||
for (what, r) in &d.results {
|
|
||||||
assert!(matches!(r, Derived::Agrees { .. }), "{id}: {what}: {r:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An example whose expectation is wrong fails the specification's build:
|
|
||||||
/// the same example with one cent moved is caught, and so is a refusal the
|
|
||||||
/// rules do not justify.
|
|
||||||
#[test]
|
|
||||||
fn a_wrong_expectation_is_caught() {
|
|
||||||
let ex = examples()
|
|
||||||
.into_iter()
|
|
||||||
.find(|e| e.id == "statement-the-books-balance-and-the-equation-holds")
|
|
||||||
.unwrap();
|
|
||||||
let text = std::fs::read_to_string(
|
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.join("../examples/statement-the-books-balance-and-the-equation-holds.toml"),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(derive(&ex).disagreements().is_empty());
|
|
||||||
let wrong = text.replacen("\"1000\" = \"6550.25\"", "\"1000\" = \"6550.26\"", 1);
|
|
||||||
let d = derive(&Example::from_toml(&wrong).unwrap());
|
|
||||||
assert!(
|
|
||||||
d.disagreements().iter().any(|m| m.contains("6550.26")),
|
|
||||||
"{:?}",
|
|
||||||
d.disagreements()
|
|
||||||
);
|
|
||||||
// A refusal the rules do not justify: the entry balances.
|
|
||||||
let ex = examples()
|
|
||||||
.into_iter()
|
|
||||||
.find(|e| e.id == "entry-an-unbalanced-entry-is-refused")
|
|
||||||
.unwrap();
|
|
||||||
let text = std::fs::read_to_string(
|
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.join("../examples/entry-an-unbalanced-entry-is-refused.toml"),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(derive(&ex).disagreements().is_empty());
|
|
||||||
let wrong = text.replacen("amount = \"90.00\"", "amount = \"100.00\"", 1);
|
|
||||||
let d = derive(&Example::from_toml(&wrong).unwrap());
|
|
||||||
assert!(
|
|
||||||
d.disagreements()
|
|
||||||
.iter()
|
|
||||||
.any(|m| m.contains("equal credits")),
|
|
||||||
"{:?}",
|
|
||||||
d.disagreements()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
//! The documentation bookspec's identifiers live in: `docs/spec/rules.md`
|
|
||||||
//! carries each stated rule's id and speaks bookspec's `CHOICE`.
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use bookspec_checker::rules;
|
|
||||||
|
|
||||||
/// `docs/spec/rules.md` carries every stated rule's id under its heading,
|
|
||||||
/// and no longer calls a choice a decision (P13 step 3).
|
|
||||||
#[test]
|
|
||||||
fn rules_md_carries_every_id() {
|
|
||||||
let text = std::fs::read_to_string(
|
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/rules.md"),
|
|
||||||
)
|
|
||||||
.expect("docs/spec/rules.md");
|
|
||||||
for rule in rules().iter().filter(|r| r.status.is_some()) {
|
|
||||||
assert!(
|
|
||||||
text.contains(&format!("**bookspec:** `{}`", rule.id)),
|
|
||||||
"B-{} carries {}",
|
|
||||||
rule.b,
|
|
||||||
rule.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
!text.contains("DECISION"),
|
|
||||||
"rules.md speaks bookspec's CHOICE"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
//! The five laws are Lean theorems in `bookspec/lean` (P13 step 4), with
|
|
||||||
//! no assumption beyond Lean's standard axioms.
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
const LAWS: [&str; 5] = [
|
|
||||||
"law_debits_equal_credits",
|
|
||||||
"law_accounting_equation",
|
|
||||||
"law_trial_balance",
|
|
||||||
"law_net_income_articulates",
|
|
||||||
"law_cash_flow_is_change_in_cash",
|
|
||||||
];
|
|
||||||
|
|
||||||
fn lean_dir() -> PathBuf {
|
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../lean")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every law is stated in `Laws.lean`, audited in `Audit.lean`, and no
|
|
||||||
/// file of the library says `sorry`.
|
|
||||||
#[test]
|
|
||||||
fn the_five_laws_are_stated_and_audited() {
|
|
||||||
let laws = std::fs::read_to_string(lean_dir().join("Bookspec/Laws.lean")).unwrap();
|
|
||||||
let audit = std::fs::read_to_string(lean_dir().join("Audit.lean")).unwrap();
|
|
||||||
for law in LAWS {
|
|
||||||
assert!(laws.contains(&format!("theorem {law}")), "{law} is stated");
|
|
||||||
assert!(
|
|
||||||
audit.contains(&format!("#print axioms {law}")),
|
|
||||||
"{law} is audited"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for entry in std::fs::read_dir(lean_dir().join("Bookspec")).unwrap() {
|
|
||||||
let path = entry.unwrap().path();
|
|
||||||
let text = std::fs::read_to_string(&path).unwrap();
|
|
||||||
for (i, line) in text.lines().enumerate() {
|
|
||||||
let code = line.split("--").next().unwrap();
|
|
||||||
assert!(
|
|
||||||
!code
|
|
||||||
.split(|c: char| !c.is_alphanumeric() && c != '_')
|
|
||||||
.any(|w| w == "sorry"),
|
|
||||||
"{}:{}: sorry",
|
|
||||||
path.display(),
|
|
||||||
i + 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// With Lean installed, the library builds and every law depends only on
|
|
||||||
/// `propext`, `Quot.sound` and `Classical.choice`. Without it the test
|
|
||||||
/// says so and passes: `mise install` provides Lean (`just prove` is
|
|
||||||
/// where the machine is known to have it).
|
|
||||||
#[test]
|
|
||||||
fn the_laws_use_standard_axioms_only() {
|
|
||||||
let built = Command::new("lake")
|
|
||||||
.arg("build")
|
|
||||||
.current_dir(lean_dir())
|
|
||||||
.output();
|
|
||||||
let Ok(built) = built else {
|
|
||||||
eprintln!("lake is not installed: the Lean build is not checked here");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
assert!(
|
|
||||||
built.status.success(),
|
|
||||||
"lake build:\n{}",
|
|
||||||
String::from_utf8_lossy(&built.stdout)
|
|
||||||
);
|
|
||||||
let out = Command::new("lake")
|
|
||||||
.args(["env", "lean", "Audit.lean"])
|
|
||||||
.current_dir(lean_dir())
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
let text = String::from_utf8_lossy(&out.stdout);
|
|
||||||
assert!(out.status.success(), "audit:\n{text}");
|
|
||||||
for law in LAWS {
|
|
||||||
let line = text
|
|
||||||
.lines()
|
|
||||||
.find(|l| l.contains(&format!("Bookspec.{law}'")))
|
|
||||||
.unwrap_or_else(|| panic!("{law} is not audited:\n{text}"));
|
|
||||||
for axiom in line
|
|
||||||
.split('[')
|
|
||||||
.nth(1)
|
|
||||||
.unwrap()
|
|
||||||
.trim_end_matches(']')
|
|
||||||
.split(", ")
|
|
||||||
{
|
|
||||||
assert!(
|
|
||||||
["propext", "Quot.sound", "Classical.choice"].contains(&axiom.trim()),
|
|
||||||
"{law}: axiom {axiom}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
//! The rule catalogue: identifiers, the index table, and the examples
|
|
||||||
//! behind each rule.
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use bookspec_checker::{Area, Claims, RuleId, Status, covered_rules, rules};
|
|
||||||
|
|
||||||
fn repo_path(rel: &str) -> PathBuf {
|
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR")).join(rel)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every rule B-1 to B-45 has exactly one identifier, its area's number
|
|
||||||
/// counts from one without a gap, and the identifier spells the area.
|
|
||||||
#[test]
|
|
||||||
fn every_b_rule_has_one_id() {
|
|
||||||
let mut seen = BTreeSet::new();
|
|
||||||
let mut by_area: BTreeMap<Area, Vec<u32>> = BTreeMap::new();
|
|
||||||
for b in 1..=45 {
|
|
||||||
let rule = bookspec_checker::rule(b).unwrap_or_else(|| panic!("no rule B-{b}"));
|
|
||||||
assert_eq!(rule.id.area(), rule.area, "B-{b}: the id's area");
|
|
||||||
assert_eq!(
|
|
||||||
rule.id.as_str().split('-').next(),
|
|
||||||
Some("BS"),
|
|
||||||
"B-{b}: the identifier's prefix"
|
|
||||||
);
|
|
||||||
assert!(seen.insert(rule.id.clone()), "B-{b}: the id is used twice");
|
|
||||||
assert_eq!(RuleId::parse(rule.id.as_str()).unwrap(), rule.id);
|
|
||||||
let n: u32 = rule
|
|
||||||
.id
|
|
||||||
.as_str()
|
|
||||||
.rsplit_once('-')
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
by_area.entry(rule.area).or_default().push(n);
|
|
||||||
}
|
|
||||||
assert_eq!(seen.len(), 45);
|
|
||||||
for (area, numbers) in by_area {
|
|
||||||
let mut want: Vec<u32> = (1..=numbers.len() as u32).collect();
|
|
||||||
let mut got = numbers;
|
|
||||||
got.sort_unstable();
|
|
||||||
want.sort_unstable();
|
|
||||||
assert_eq!(got, want, "{}: numbers count from one", area.key());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The index table is the catalogue: one row per rule, the same id, area
|
|
||||||
/// and status.
|
|
||||||
#[test]
|
|
||||||
fn the_index_matches_the_catalogue() {
|
|
||||||
let text = std::fs::read_to_string(repo_path("../spec/rules-index.md"))
|
|
||||||
.expect("bookspec/spec/rules-index.md");
|
|
||||||
let mut rows = BTreeMap::new();
|
|
||||||
for line in text.lines() {
|
|
||||||
let Some(rest) = line.strip_prefix("| B-") else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let fields: Vec<&str> = line.split('|').map(str::trim).collect();
|
|
||||||
// "", "B-n", "`BS-...`", area, status, rule, examples, ""
|
|
||||||
assert_eq!(fields.len(), 8, "the table has six columns: {line}");
|
|
||||||
let b: u8 = rest
|
|
||||||
.split_once(' ')
|
|
||||||
.unwrap()
|
|
||||||
.0
|
|
||||||
.parse()
|
|
||||||
.unwrap_or_else(|_| panic!("{line}"));
|
|
||||||
let id = fields[2].trim_matches('`');
|
|
||||||
let area = fields[3];
|
|
||||||
let status = fields[4];
|
|
||||||
assert!(
|
|
||||||
rows.insert(b, (id.to_string(), area.to_string(), status.to_string()))
|
|
||||||
.is_none()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for rule in rules() {
|
|
||||||
let (id, area, status) = rows
|
|
||||||
.get(&rule.b)
|
|
||||||
.unwrap_or_else(|| panic!("the index lists B-{}", rule.b));
|
|
||||||
assert_eq!(id, rule.id.as_str(), "B-{}", rule.b);
|
|
||||||
assert_eq!(area, rule.area.key(), "B-{}", rule.b);
|
|
||||||
let want = rule.status.map_or("—", |s| s.key());
|
|
||||||
assert_eq!(status, want, "B-{}", rule.b);
|
|
||||||
}
|
|
||||||
assert_eq!(rows.len(), 45, "the index lists every rule once");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every rule either shows an example or is allocated and not yet stated;
|
|
||||||
/// no example id is invented, and every example file is listed.
|
|
||||||
#[test]
|
|
||||||
fn every_example_is_listed_under_a_rule() {
|
|
||||||
let mut files: Vec<String> = std::fs::read_dir(repo_path("../examples"))
|
|
||||||
.expect("bookspec/examples")
|
|
||||||
.map(|e| e.unwrap().path())
|
|
||||||
.filter(|p| p.extension().is_some_and(|x| x == "toml"))
|
|
||||||
.map(|p| p.file_stem().unwrap().to_string_lossy().into_owned())
|
|
||||||
.collect();
|
|
||||||
files.sort();
|
|
||||||
let listed: BTreeSet<&str> = rules()
|
|
||||||
.iter()
|
|
||||||
.flat_map(|r| r.examples.iter().copied())
|
|
||||||
.collect();
|
|
||||||
for f in &files {
|
|
||||||
assert!(listed.contains(f.as_str()), "{f} is listed under no rule");
|
|
||||||
}
|
|
||||||
for id in &listed {
|
|
||||||
assert!(
|
|
||||||
files.iter().any(|f| f == id),
|
|
||||||
"the catalogue lists {id}, which is no example"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for rule in rules() {
|
|
||||||
if rule.status.is_none() {
|
|
||||||
assert!(
|
|
||||||
!rule.has_examples(),
|
|
||||||
"B-{} is not yet stated, so it has no example",
|
|
||||||
rule.b
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A rule with evidence is claimed by an engine; one without is not.
|
|
||||||
#[test]
|
|
||||||
fn a_claimed_rule_has_an_example() {
|
|
||||||
let covered = covered_rules();
|
|
||||||
assert!(!covered.is_empty());
|
|
||||||
for rule in covered {
|
|
||||||
assert!(rule.has_examples(), "{}", rule.id);
|
|
||||||
assert!(
|
|
||||||
matches!(
|
|
||||||
rule.status,
|
|
||||||
Some(Status::Authority | Status::Convention | Status::Choice)
|
|
||||||
),
|
|
||||||
"{}",
|
|
||||||
rule.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An identifier has one spelling: `BS-<area>-<n>`, no leading zero.
|
|
||||||
#[test]
|
|
||||||
fn an_id_has_one_spelling() {
|
|
||||||
assert_eq!(RuleId::parse("BS-entry-1").unwrap().as_str(), "BS-entry-1");
|
|
||||||
for bad in [
|
|
||||||
"entry-1",
|
|
||||||
"BS-nothing-1",
|
|
||||||
"BS-entry",
|
|
||||||
"BS-entry-0",
|
|
||||||
"BS-entry-01",
|
|
||||||
"BS-entry-x",
|
|
||||||
"BS-entry-1-2",
|
|
||||||
] {
|
|
||||||
assert!(RuleId::parse(bad).is_err(), "{bad:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A choice rule's option is declared by the engine, not by the spec.
|
|
||||||
#[test]
|
|
||||||
fn a_choice_rule_declares_its_option() {
|
|
||||||
let rule = bookspec_checker::rule(13).unwrap();
|
|
||||||
assert_eq!(rule.status, Some(Status::Authority));
|
|
||||||
let choice = bookspec_checker::rule(31).unwrap();
|
|
||||||
assert_eq!(choice.status, Some(Status::Choice));
|
|
||||||
let claims = Claims::new([choice.id.clone()]).with_choice(choice.id.clone(), "refuse_blank");
|
|
||||||
assert_eq!(claims.choice(&choice.id), Some("refuse_blank"));
|
|
||||||
assert_eq!(claims.choice(&rule.id), None);
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
//! The example schema: what a well-formed bookspec example is, and what
|
|
||||||
//! the parser refuses.
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use bookspec_checker::{
|
|
||||||
Example, Query, Refusal, TEXT_VALUED, Values, refusal_reasons, rule_for_refusal,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn examples_dir() -> PathBuf {
|
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A minimal well-formed example, and the text with one line replaced.
|
|
||||||
const BASE: &str = r#"
|
|
||||||
id = "schema.test"
|
|
||||||
title = "A test"
|
|
||||||
rule = "Every entry MUST balance."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = ["bookspec/README.md"]
|
|
||||||
tags = ["schema"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-02"
|
|
||||||
description = "Contributed"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-01-02"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "100.00"
|
|
||||||
"3000" = "-100.00"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
fn base() -> Example {
|
|
||||||
Example::from_toml(BASE).expect("the base example is well formed")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every example of the seed parses and follows the schema: its closed
|
|
||||||
/// lists, its dates, its amounts and its references.
|
|
||||||
#[test]
|
|
||||||
fn every_seed_example_parses_and_validates() {
|
|
||||||
let mut files: Vec<PathBuf> = std::fs::read_dir(examples_dir())
|
|
||||||
.expect("bookspec/examples")
|
|
||||||
.map(|e| e.unwrap().path())
|
|
||||||
.filter(|p| p.extension().is_some_and(|x| x == "toml"))
|
|
||||||
.collect();
|
|
||||||
files.sort();
|
|
||||||
assert!(files.len() >= 56, "the seed has at least 56 examples");
|
|
||||||
let mut ids = BTreeSet::new();
|
|
||||||
for f in &files {
|
|
||||||
let text = std::fs::read_to_string(f).unwrap();
|
|
||||||
let ex = Example::from_toml(&text).unwrap_or_else(|e| panic!("{}: {e}", f.display()));
|
|
||||||
let stem = f.file_stem().unwrap().to_string_lossy();
|
|
||||||
assert_eq!(ex.id, stem, "{}: the file is named for its id", f.display());
|
|
||||||
assert!(ids.insert(ex.id.clone()), "{}: id used twice", ex.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A key the schema does not have is refused, never ignored.
|
|
||||||
#[test]
|
|
||||||
fn an_unknown_key_is_refused() {
|
|
||||||
let text = BASE.replace("cash_accounts = ", "not_a_key = ");
|
|
||||||
let text = format!("{text}\ncash_account = []\n");
|
|
||||||
assert!(Example::from_toml(&text).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The status is one of the three the specification uses.
|
|
||||||
#[test]
|
|
||||||
fn the_status_is_authority_convention_or_choice() {
|
|
||||||
let text = BASE.replace(r#"status = "AUTHORITY""#, r#"status = "DECISION""#);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("status"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A line's amount is exact and positive.
|
|
||||||
#[test]
|
|
||||||
fn a_line_amount_is_exact_and_positive() {
|
|
||||||
let text = BASE.replace(r#"amount = "100.00" }"#, r#"amount = "-100.00" }"#);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("amounts are positive"), "{e}");
|
|
||||||
let text = BASE.replace(r#"amount = "100.00" }"#, r#"amount = "1e2" }"#);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("exact decimal"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An event is exactly one of the nine kinds.
|
|
||||||
#[test]
|
|
||||||
fn an_event_is_exactly_one_kind() {
|
|
||||||
let text = BASE.replace(
|
|
||||||
r#"lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "100.00" },
|
|
||||||
]"#,
|
|
||||||
r#"lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
reverses = 1"#,
|
|
||||||
);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("an event is one of"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `reverses` counts events from 1 and never names itself or a later one.
|
|
||||||
#[test]
|
|
||||||
fn reverses_counts_events_from_one() {
|
|
||||||
let text = format!(
|
|
||||||
"{BASE}\n[[events]]\ndate = \"2026-01-03\"\ndescription = \"Reverse it\"\nreverses = 0\n"
|
|
||||||
);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("events count from 1"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A members close needs the shares, and they sum to 1.
|
|
||||||
#[test]
|
|
||||||
fn the_members_shares_sum_to_one() {
|
|
||||||
let text = BASE.replace(
|
|
||||||
r#"[[setup.accounts]]
|
|
||||||
code = "3000""#,
|
|
||||||
r#"[[setup.members]]
|
|
||||||
name = "A"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "0.6"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "B"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000""#,
|
|
||||||
);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("shares sum to 1"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A member's share is an exact decimal fraction above zero.
|
|
||||||
#[test]
|
|
||||||
fn a_share_is_a_fraction_above_zero() {
|
|
||||||
let text = BASE.replace(
|
|
||||||
r#"[[setup.accounts]]
|
|
||||||
code = "3000""#,
|
|
||||||
r#"[[setup.members]]
|
|
||||||
name = "A"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "1.5"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000""#,
|
|
||||||
);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("share"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every refusal reason the specification names belongs to exactly one
|
|
||||||
/// rule, and a refusal names that rule.
|
|
||||||
#[test]
|
|
||||||
fn every_refusal_reason_names_a_rule() {
|
|
||||||
for reason in refusal_reasons() {
|
|
||||||
let rule = rule_for_refusal(reason).unwrap_or_else(|| panic!("no rule for {reason:?}"));
|
|
||||||
assert!(
|
|
||||||
!rule.title.contains("allocated"),
|
|
||||||
"{reason:?} belongs to a stated rule"
|
|
||||||
);
|
|
||||||
assert_eq!(Refusal::of_reason(reason).rule, rule.id);
|
|
||||||
assert_eq!(Refusal::of_reason(reason).reason, reason);
|
|
||||||
}
|
|
||||||
assert!(rule_for_refusal("not_a_reason").is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A query no crate answers is refused.
|
|
||||||
#[test]
|
|
||||||
fn an_unknown_query_is_refused() {
|
|
||||||
let text = BASE.replace(r#"query = "trial_balance""#, r#"query = "trial_balances""#);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("no query answers"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A period may be refused instead of answering; another query may not.
|
|
||||||
#[test]
|
|
||||||
fn a_period_may_be_refused() {
|
|
||||||
let text = BASE.replace(
|
|
||||||
r#"query = "trial_balance"
|
|
||||||
as_of = "2026-01-02"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "100.00"
|
|
||||||
"3000" = "-100.00""#,
|
|
||||||
r#"query = "trial_balance"
|
|
||||||
as_of = "2026-01-02"
|
|
||||||
refused = "end_before_start""#,
|
|
||||||
);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("refused is for"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A command is accepted or refused, never both, and shows no values.
|
|
||||||
#[test]
|
|
||||||
fn a_command_shows_no_values() {
|
|
||||||
let text = BASE.replace(
|
|
||||||
r#"query = "trial_balance"
|
|
||||||
as_of = "2026-01-02"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "100.00"
|
|
||||||
"3000" = "-100.00""#,
|
|
||||||
r#"query = "rename_account"
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash on hand"
|
|
||||||
accepted = true
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "100.00""#,
|
|
||||||
);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("a command is accepted"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A text-valued figure query takes words; a figure query takes exact
|
|
||||||
/// decimal strings.
|
|
||||||
#[test]
|
|
||||||
fn a_figure_query_takes_exact_decimals() {
|
|
||||||
assert!(TEXT_VALUED.contains(&"vendor_1099"));
|
|
||||||
let text = BASE.replace(r#""1000" = "100.00""#, r#""1000" = "100.0.0""#);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("exact decimal"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `Query` carries the question and not the answer, and knows which
|
|
||||||
/// queries list everything they cover.
|
|
||||||
#[test]
|
|
||||||
fn a_query_does_not_carry_its_expected_values() {
|
|
||||||
let ex = base();
|
|
||||||
let x = &ex.expect[0];
|
|
||||||
let q: Query = x.query();
|
|
||||||
assert_eq!(q.name, "trial_balance");
|
|
||||||
assert!(q.is_complete());
|
|
||||||
assert!(!q.is_command());
|
|
||||||
assert!(!q.is_format());
|
|
||||||
assert_eq!(
|
|
||||||
x.values("schema.test"),
|
|
||||||
&Values::Figures(BTreeMap::from([
|
|
||||||
("1000".to_string(), "100.00".to_string()),
|
|
||||||
("3000".to_string(), "-100.00".to_string()),
|
|
||||||
]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A format query belongs to the formats appendix.
|
|
||||||
#[test]
|
|
||||||
fn a_format_query_is_of_the_formats_appendix() {
|
|
||||||
let text = BASE.replace(
|
|
||||||
r#"query = "trial_balance"
|
|
||||||
as_of = "2026-01-02""#,
|
|
||||||
r#"query = "iif_lines"
|
|
||||||
as_of = "2026-01-02""#,
|
|
||||||
);
|
|
||||||
let e = Example::from_toml(&text).unwrap_err();
|
|
||||||
assert!(e.message.contains("iif_lines"), "{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The seed's index is the table the catalogue is: B-1 to B-45, one
|
|
||||||
/// identifier each. The catalogue test reads it too; this checks the ids
|
|
||||||
/// themselves.
|
|
||||||
#[test]
|
|
||||||
fn the_rule_index_lists_every_stated_rule() {
|
|
||||||
let index = std::fs::read_to_string(
|
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../spec/rules-index.md"),
|
|
||||||
)
|
|
||||||
.expect("bookspec/spec/rules-index.md");
|
|
||||||
for b in 1..=45 {
|
|
||||||
assert!(
|
|
||||||
index.contains(&format!("| B-{b} |")),
|
|
||||||
"the index lists B-{b}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
//! The specification text (`bookspec/spec/*.md`, P13 step 4): every rule
|
|
||||||
//! has its section, its statement, its pinned sources, its symbolic form
|
|
||||||
//! and its examples, and every citation resolves.
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use bookspec_checker::{Rule, rules};
|
|
||||||
|
|
||||||
fn spec_dir() -> PathBuf {
|
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../spec")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sources_dir() -> PathBuf {
|
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/sources")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A rule's section as the text gives it.
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
struct Section {
|
|
||||||
file: String,
|
|
||||||
heading: String,
|
|
||||||
fields: BTreeMap<String, Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every `### BS-...` section of the area files, by its identifier.
|
|
||||||
fn sections() -> BTreeMap<String, Section> {
|
|
||||||
let mut out = BTreeMap::new();
|
|
||||||
let mut files: Vec<PathBuf> = std::fs::read_dir(spec_dir())
|
|
||||||
.unwrap()
|
|
||||||
.map(|e| e.unwrap().path())
|
|
||||||
.filter(|p| p.extension().is_some_and(|x| x == "md"))
|
|
||||||
.collect();
|
|
||||||
files.sort();
|
|
||||||
for f in files {
|
|
||||||
let name = f.file_name().unwrap().to_string_lossy().into_owned();
|
|
||||||
let text = std::fs::read_to_string(&f).unwrap();
|
|
||||||
let mut current: Option<(String, Section)> = None;
|
|
||||||
let mut field = String::new();
|
|
||||||
for line in text.lines() {
|
|
||||||
if let Some(h) = line.strip_prefix("### BS-") {
|
|
||||||
if let Some((id, s)) = current.take() {
|
|
||||||
assert!(out.insert(id.clone(), s).is_none(), "{id} twice");
|
|
||||||
}
|
|
||||||
let id = format!("BS-{}", h.split(':').next().unwrap());
|
|
||||||
current = Some((
|
|
||||||
id,
|
|
||||||
Section {
|
|
||||||
file: name.clone(),
|
|
||||||
heading: h.to_string(),
|
|
||||||
..Section::default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some((_, s)) = current.as_mut() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if let Some(rest) = line.strip_prefix("- **") {
|
|
||||||
let (key, value) = rest.split_once(":**").expect("a field: - **Name:** ...");
|
|
||||||
field = key.to_string();
|
|
||||||
let e = s.fields.entry(field.clone()).or_default();
|
|
||||||
if !value.trim().is_empty() {
|
|
||||||
e.push(value.trim().to_string());
|
|
||||||
}
|
|
||||||
} else if let Some(item) = line.strip_prefix(" - ") {
|
|
||||||
s.fields
|
|
||||||
.entry(field.clone())
|
|
||||||
.or_default()
|
|
||||||
.push(item.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some((id, s)) = current.take() {
|
|
||||||
assert!(out.insert(id.clone(), s).is_none(), "{id} twice");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn one<'a>(s: &'a Section, id: &str, field: &str) -> &'a str {
|
|
||||||
let v = s
|
|
||||||
.fields
|
|
||||||
.get(field)
|
|
||||||
.unwrap_or_else(|| panic!("{id}: no {field}"));
|
|
||||||
assert_eq!(v.len(), 1, "{id}: {field} is one line");
|
|
||||||
&v[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whitespace and typographic quotes made plain, so a pin copied from a
|
|
||||||
/// wrapped or curly-quoted source still matches.
|
|
||||||
fn squash(s: &str) -> String {
|
|
||||||
let plain = s
|
|
||||||
.replace(['\u{2019}', '\u{2018}'], "'")
|
|
||||||
.replace(['\u{201c}', '\u{201d}'], "\"");
|
|
||||||
plain.split_whitespace().collect::<Vec<_>>().join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn backticked(s: &str) -> Vec<&str> {
|
|
||||||
s.split('`').skip(1).step_by(2).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every one of the 45 rules has one section, in the file of its area,
|
|
||||||
/// headed with its identifier and B-number.
|
|
||||||
#[test]
|
|
||||||
fn every_rule_has_its_section() {
|
|
||||||
let sections = sections();
|
|
||||||
assert_eq!(sections.len(), 45, "one section per rule");
|
|
||||||
for rule in rules() {
|
|
||||||
let s = sections
|
|
||||||
.get(rule.id.as_str())
|
|
||||||
.unwrap_or_else(|| panic!("{}: no section", rule.id));
|
|
||||||
assert_eq!(s.file, format!("{}.md", rule.area.key()), "{}", rule.id);
|
|
||||||
assert!(
|
|
||||||
s.heading.ends_with(&format!("(B-{})", rule.b)),
|
|
||||||
"{}: heading {:?} names B-{}",
|
|
||||||
rule.id,
|
|
||||||
s.heading,
|
|
||||||
rule.b
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A stated rule has a status the catalogue agrees with, a normative
|
|
||||||
/// statement, a symbolic form and applicability; a reserved one says so.
|
|
||||||
#[test]
|
|
||||||
fn every_stated_rule_is_complete() {
|
|
||||||
let sections = sections();
|
|
||||||
for rule in rules() {
|
|
||||||
let s = §ions[rule.id.as_str()];
|
|
||||||
let id = rule.id.as_str();
|
|
||||||
let status = one(s, id, "Status");
|
|
||||||
match rule.status {
|
|
||||||
Some(st) => {
|
|
||||||
assert!(status.starts_with(st.key()), "{id}: status {status:?}");
|
|
||||||
let statement = one(s, id, "Statement");
|
|
||||||
assert!(
|
|
||||||
["MUST", "SHOULD", "MAY"]
|
|
||||||
.iter()
|
|
||||||
.any(|w| statement.contains(w)),
|
|
||||||
"{id}: a statement says MUST, SHOULD or MAY"
|
|
||||||
);
|
|
||||||
for field in ["Symbolic form", "Applicability"] {
|
|
||||||
assert!(
|
|
||||||
one(s, id, field).len() > 10 && !one(s, id, field).starts_with("none"),
|
|
||||||
"{id}: {field}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => assert!(status.starts_with("reserved"), "{id}: reserved"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A section's examples are the catalogue's, exactly; "none yet" says why.
|
|
||||||
#[test]
|
|
||||||
fn the_examples_are_the_catalogues() {
|
|
||||||
let sections = sections();
|
|
||||||
for rule in rules() {
|
|
||||||
let s = §ions[rule.id.as_str()];
|
|
||||||
let id = rule.id.as_str();
|
|
||||||
let line = one(s, id, "Examples");
|
|
||||||
let listed: BTreeSet<&str> = backticked(line)
|
|
||||||
.into_iter()
|
|
||||||
.filter(|b| *b != "none yet")
|
|
||||||
.collect();
|
|
||||||
let want: BTreeSet<&str> = rule.examples.iter().copied().collect();
|
|
||||||
if rule.examples.is_empty() {
|
|
||||||
assert!(line.starts_with("none yet"), "{id}: {line}");
|
|
||||||
if rule.status.is_some() {
|
|
||||||
assert!(
|
|
||||||
line.len() > "none yet. ".len() + 20,
|
|
||||||
"{id}: a stated rule with no example says why"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assert_eq!(listed, want, "{id}: examples");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every citation resolves: a vendored source exists, and a verbatim
|
|
||||||
/// pin appears in it; an external reference names its paragraph.
|
|
||||||
#[test]
|
|
||||||
fn every_citation_resolves() {
|
|
||||||
let sections = sections();
|
|
||||||
let mut vendored = 0;
|
|
||||||
let mut external = 0;
|
|
||||||
let mut bad: Vec<String> = Vec::new();
|
|
||||||
for rule in rules() {
|
|
||||||
let s = §ions[rule.id.as_str()];
|
|
||||||
let id = rule.id.as_str();
|
|
||||||
let sources = s
|
|
||||||
.fields
|
|
||||||
.get("Sources")
|
|
||||||
.unwrap_or_else(|| panic!("{id}: no Sources"));
|
|
||||||
if rule.status.is_none() {
|
|
||||||
assert_eq!(sources, &vec!["none yet".to_string()], "{id}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
assert!(!sources.is_empty(), "{id}: sourced");
|
|
||||||
for item in sources {
|
|
||||||
if let Some(rest) = item.strip_prefix("vendored `") {
|
|
||||||
let (file, rest) = rest
|
|
||||||
.split_once("`:")
|
|
||||||
.unwrap_or_else(|| panic!("{id}: {item}"));
|
|
||||||
let path = sources_dir().join(file.strip_prefix("sources/").unwrap_or(file));
|
|
||||||
let text = std::fs::read_to_string(&path)
|
|
||||||
.unwrap_or_else(|_| panic!("{id}: {file} is not vendored"));
|
|
||||||
let hay = squash(&text);
|
|
||||||
// Every quoted pin is verbatim in the file.
|
|
||||||
let quoted: Vec<&str> = rest.split('"').skip(1).step_by(2).collect();
|
|
||||||
for q in "ed {
|
|
||||||
if !hay.contains(&squash(q)) {
|
|
||||||
bad.push(format!("{id}: {q:?} is not in {file}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if quoted.is_empty() {
|
|
||||||
bad.push(format!("{id}: {item} has no verbatim pin"));
|
|
||||||
}
|
|
||||||
vendored += 1;
|
|
||||||
} else if let Some(rest) = item.strip_prefix("external:") {
|
|
||||||
assert!(
|
|
||||||
rest.trim().len() > 20,
|
|
||||||
"{id}: an external reference is exact: {item}"
|
|
||||||
);
|
|
||||||
external += 1;
|
|
||||||
} else {
|
|
||||||
panic!("{id}: a source is `vendored` or `external`: {item}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(bad.is_empty(), "pins not found:\n{}", bad.join("\n"));
|
|
||||||
assert!(vendored >= 30, "vendored citations: {vendored}");
|
|
||||||
assert!(external >= 15, "external citations: {external}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The first edition's page budget: 60 pages of 55 lines.
|
|
||||||
#[test]
|
|
||||||
fn the_edition_fits_its_page_budget() {
|
|
||||||
let mut lines = 0;
|
|
||||||
for e in std::fs::read_dir(spec_dir()).unwrap() {
|
|
||||||
let p = e.unwrap().path();
|
|
||||||
if p.extension().is_some_and(|x| x == "md") {
|
|
||||||
lines += std::fs::read_to_string(p).unwrap().lines().count();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(lines <= 60 * 55, "{lines} lines is over 60 pages");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A stated rule with no example is exactly one an engine cannot claim.
|
|
||||||
#[test]
|
|
||||||
fn rules_without_examples_are_not_claimable() {
|
|
||||||
let uncovered: Vec<&Rule> = rules().iter().filter(|r| !r.has_examples()).collect();
|
|
||||||
let claimable: BTreeSet<_> = bookspec_checker::covered_rules()
|
|
||||||
.into_iter()
|
|
||||||
.map(|r| r.b)
|
|
||||||
.collect();
|
|
||||||
for r in uncovered {
|
|
||||||
assert!(!claimable.contains(&r.b), "B-{}", r.b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
id = "1099-a-dated-status-applies-by-payment-date"
|
|
||||||
title = "A payee's 1099 status applies to each payment as of the payment's date"
|
|
||||||
rule = """
|
|
||||||
Whether a payee is exempt (a corporation) is judged as of each payment: the
|
|
||||||
payer may treat a payee as a corporation when the condition is met before
|
|
||||||
the payment is made. An LLC that elects to be taxed as a corporation keeps
|
|
||||||
its EIN and becomes a corporation on the election's effective date (the
|
|
||||||
date its Form 8832 specifies). Payments before that date count toward its
|
|
||||||
1099-NEC; payments from it on are payments to a corporation, for which no
|
|
||||||
return is required. (A payee that incorporates is a new legal person: a new
|
|
||||||
payee, with its own W-9.)
|
|
||||||
"""
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"Treas. Reg. §1.6041-3(p)(1) (docs/spec/sources/irs-1099-payment-method.md)",
|
|
||||||
"Treas. Reg. §1.6049-4(c)(1)(ii)(A) (\"before a payment is made\")",
|
|
||||||
"Treas. Reg. §301.7701-2(b)(2), §301.7701-3(c)(1)(iii)",
|
|
||||||
"DECISION D-40 (wanderbooks owner, 2026-10-20): dated statuses",
|
|
||||||
]
|
|
||||||
tags = ["1099", "dated-status", "corporation", "election"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "cash"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Contract Labor"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Harbor Design LLC"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
[[setup.status_changes]]
|
|
||||||
payee = "Harbor Design LLC"
|
|
||||||
effective = "2026-07-01"
|
|
||||||
decision = "not_reportable"
|
|
||||||
reason_kind = "corporation"
|
|
||||||
reason = "elected to be taxed as a corporation (Form 8832), effective July 1"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-01"
|
|
||||||
description = "Design work"
|
|
||||||
payee = "Harbor Design LLC"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1500.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-30"
|
|
||||||
description = "Design work, the day before the election"
|
|
||||||
payee = "Harbor Design LLC"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-07-01"
|
|
||||||
description = "Design work, the election's day"
|
|
||||||
payee = "Harbor Design LLC"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "800.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "800.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-09-15"
|
|
||||||
description = "Design work"
|
|
||||||
payee = "Harbor Design LLC"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: before July 1, 1,500.00 + 1,000.00 = 2,500.00 counts (at least
|
|
||||||
# 2,000.00: reportable); from July 1 on, 800.00 + 1,200.00 = 2,000.00 paid
|
|
||||||
# to a corporation, not reportable.
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099"
|
|
||||||
payee = "Harbor Design LLC"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { status = "reportable", threshold = "2000.00", paid = "2500.00", excluded_card = "0.00", undetermined = "0.00", not_reportable = "2000.00" }
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
id = "1099-a-members-check-through-financing-counts"
|
|
||||||
title = "A member's own check for the company counts toward the payee's 1099; their card doesn't; unknown stays unknown"
|
|
||||||
rule = """
|
|
||||||
The payer is the person who makes the payment in the course of its trade or
|
|
||||||
business. A person paying on another's behalf is the payer only if it
|
|
||||||
performs management or oversight functions or has a significant economic
|
|
||||||
interest; writing checks at another's direction is neither. A member who
|
|
||||||
pays a company bill from their own funds at the company's direction, the
|
|
||||||
company bearing the cost through its liability to the member, leaves the
|
|
||||||
company the payer: the member's check counts toward the payee's 1099. The
|
|
||||||
member's personal card is a payment card: excluded (Form 1099-K). A
|
|
||||||
member's payment whose method isn't recorded is undetermined, never
|
|
||||||
guessed; the payee is reportable only if what certainly counts reaches the
|
|
||||||
threshold.
|
|
||||||
"""
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Treas. Reg. §1.6041-1(a)(1)(i), (e)(1) (docs/spec/sources/irs-1099-payment-method.md)",
|
|
||||||
"Treas. Reg. §1.6041-1(a)(1)(iv)",
|
|
||||||
"26 U.S.C. §6041(a), as amended by Pub. L. 119-21 §70433 (docs/spec/sources/irs-1099-thresholds.md)",
|
|
||||||
"DECISION (owner, plan/served.md P3 defect 12; D-40, 2026-10-20): a payment through financing with no payment method recorded is undetermined (wanderbooks rules.md B-19)",
|
|
||||||
]
|
|
||||||
tags = ["1099", "financing", "member-paid", "instruments"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "cash"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to Members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Contract Labor"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.instruments]]
|
|
||||||
label = "Riley's checks"
|
|
||||||
kind = "member_cash_or_check"
|
|
||||||
account = "2100"
|
|
||||||
|
|
||||||
[[setup.instruments]]
|
|
||||||
label = "Riley's Visa"
|
|
||||||
kind = "member_card"
|
|
||||||
account = "2100"
|
|
||||||
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Dana Studio"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-05-01"
|
|
||||||
description = "Logo, paid by a member's personal check"
|
|
||||||
payee = "Dana Studio"
|
|
||||||
instrument = "Riley's checks"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "2500.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "2500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-01"
|
|
||||||
description = "Icons, charged to a member's personal card"
|
|
||||||
payee = "Dana Studio"
|
|
||||||
instrument = "Riley's Visa"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "800.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "800.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-07-01"
|
|
||||||
description = "Revisions, paid by a member (how not recorded)"
|
|
||||||
payee = "Dana Studio"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "300.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: counts 2,500.00 (the member's check); excluded 800.00 (their
|
|
||||||
# card); undetermined 300.00. What certainly counts, 2,500.00, is at least
|
|
||||||
# 2,000.00 however the 300.00 turns out: reportable, on 2,500.00 (or
|
|
||||||
# 2,800.00 once the 300.00 is known to be a check).
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099"
|
|
||||||
payee = "Dana Studio"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { status = "reportable", threshold = "2000.00", paid = "2500.00", excluded_card = "800.00", undetermined = "300.00", not_reportable = "0.00" }
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
# How a payee's party totals and its 1099 amount relate (wanderbooks
|
|
||||||
# rules.md B-35 item 9, B-19): a member paying a contractor for the
|
|
||||||
# company through the members' financing account, by check, by their own
|
|
||||||
# card and with no method recorded; a company card charge; a company
|
|
||||||
# check; then the member reimbursed. Party totals count every payment as
|
|
||||||
# paid to the contractor, in money; the 1099 worksheet counts the
|
|
||||||
# company's payments that aren't by card, and holds the unrecorded one
|
|
||||||
# undetermined. The shape: bookspec/README.md.
|
|
||||||
|
|
||||||
id = "1099-and-party-totals-for-a-members-payment"
|
|
||||||
title = "A payee's party total is its 1099 amount plus its card payments plus its payments of unknown method"
|
|
||||||
rule = """
|
|
||||||
A member who pays a payee for the company from their own funds, the
|
|
||||||
company owing them for it through its financing account, leaves the
|
|
||||||
company the payer: on the 1099 worksheet the member's own cash or check
|
|
||||||
counts toward the payee's 1099, the member's own card is excluded (Form
|
|
||||||
1099-K), and a payment whose method isn't recorded is undetermined. Party
|
|
||||||
totals report what moved with each party, whoever handled the money: every
|
|
||||||
one of those payments is paid to the payee, in money, on its date, and the
|
|
||||||
member is paid when the company reimburses them. So the payee's party
|
|
||||||
total paid is its 1099 amount plus what was paid by card or payment
|
|
||||||
network plus what is undetermined; the reimbursement is paid to the
|
|
||||||
member and pays no payee.
|
|
||||||
"""
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Treas. Reg. §1.6041-1(e)(1): a person paying on another's behalf who only writes the check at its direction isn't the payor (wanderbooks docs/spec/sources/irs-1099-payment-method.md)",
|
|
||||||
"Treas. Reg. §1.6041-1(a)(1)(iv): a payment card transaction is reported under §6050W, not §6041 (docs/spec/sources/irs-1099-payment-method.md)",
|
|
||||||
"DECISION (owner, 2026-10-20): money is cash, cards, the members' financing and loans (wanderbooks rules.md B-35 item 5)",
|
|
||||||
"DECISION D-40 (owner, 2026-10-20): payment methods; an untagged payment through financing is undetermined (rules.md B-19)",
|
|
||||||
]
|
|
||||||
tags = ["1099", "parties", "member-paid", "instruments", "cards"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "cash"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2050"
|
|
||||||
name = "Company Visa"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to Members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital — Tobin Harlow"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital — Morgan Ellis"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Contract Labor"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
roles = ["member"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
roles = ["member"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Dana Studio"
|
|
||||||
roles = ["vendor", "contractor"]
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
capital_account = "3001"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
# The contractor's vendor record, marked for a 1099-NEC.
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Dana Studio"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
[[setup.instruments]]
|
|
||||||
label = "Company Visa"
|
|
||||||
kind = "business_card"
|
|
||||||
account = "2050"
|
|
||||||
|
|
||||||
[[setup.instruments]]
|
|
||||||
label = "Tobin's checks"
|
|
||||||
kind = "member_cash_or_check"
|
|
||||||
account = "2100"
|
|
||||||
owner = "Tobin Harlow"
|
|
||||||
|
|
||||||
[[setup.instruments]]
|
|
||||||
label = "Tobin's Visa"
|
|
||||||
kind = "member_card"
|
|
||||||
account = "2100"
|
|
||||||
owner = "Tobin Harlow"
|
|
||||||
|
|
||||||
# 1. Tobin pays the contractor by personal check.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-02"
|
|
||||||
description = "Design work, paid by Tobin's check"
|
|
||||||
parties = [{ party = "Dana Studio" }]
|
|
||||||
instrument = "Tobin's checks"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1500.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "1500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 2. Tobin pays on his own card.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-04-01"
|
|
||||||
description = "Icons, charged to Tobin's own card"
|
|
||||||
parties = [{ party = "Dana Studio" }]
|
|
||||||
instrument = "Tobin's Visa"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "400.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "400.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 3. Tobin pays; how is not recorded.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-05-01"
|
|
||||||
description = "Revisions, paid by Tobin"
|
|
||||||
parties = [{ party = "Dana Studio" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "300.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 4. Charged to the company card.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-01"
|
|
||||||
description = "Banner, on the company card"
|
|
||||||
parties = [{ party = "Dana Studio" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "200.00" },
|
|
||||||
{ account = "2050", side = "credit", amount = "200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 5. The company's own check.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-15"
|
|
||||||
description = "Brochure, company check"
|
|
||||||
parties = [{ party = "Dana Studio" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "600.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "600.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 6. The company reimburses Tobin for events 1 to 3.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-07-01"
|
|
||||||
description = "Tobin reimbursed"
|
|
||||||
parties = [{ party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2100", side = "debit", amount = "2200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "2200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 2
|
|
||||||
party = "Dana Studio"
|
|
||||||
values = { resolved = "400.00", direction = "paid", cash = "400.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
# By hand: 1,500.00 + 400.00 + 300.00 + 200.00 + 600.00 = 3,000.00, every
|
|
||||||
# payment paid to the contractor, in money.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Dana Studio"
|
|
||||||
basis = "cash"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "3000.00", received = "0.00" }
|
|
||||||
|
|
||||||
# By hand: counts 1,500.00 (Tobin's check) + 600.00 (the company's check)
|
|
||||||
# = 2,100.00; excluded 400.00 (Tobin's card) + 200.00 (the company card)
|
|
||||||
# = 600.00; undetermined 300.00. 2,100.00 + 600.00 + 300.00 = 3,000.00,
|
|
||||||
# the party total. What certainly counts, 2,100.00, is at least 2,000.00:
|
|
||||||
# reportable.
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099"
|
|
||||||
payee = "Dana Studio"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { status = "reportable", threshold = "2000.00", paid = "2100.00", excluded_card = "600.00", undetermined = "300.00", not_reportable = "0.00" }
|
|
||||||
|
|
||||||
# The reimbursement is paid to Tobin, and pays no payee.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
basis = "cash"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "2200.00", received = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099_paid"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Dana Studio" = "2100.00" }
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
id = "1099-card-payments-are-excluded"
|
|
||||||
title = "Payments by card are left off Form 1099-NEC, and listed"
|
|
||||||
rule = """
|
|
||||||
A payment made by payment card (credit, debit or stored-value) or through a
|
|
||||||
third party payment network that section 6041 would otherwise cover is
|
|
||||||
reported by the payment settlement entity on Form 1099-K under section 6050W,
|
|
||||||
not by the payer on Form 1099-NEC or 1099-MISC, whoever's card it was. A
|
|
||||||
payment by check, ACH or transfer counts toward the payee's 1099-NEC. A
|
|
||||||
bank payment with no payment method recorded is presumed a check, ACH or
|
|
||||||
transfer; a debit-card payment from the bank is excluded once tagged with
|
|
||||||
the card.
|
|
||||||
"""
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"Treas. Reg. §1.6041-1(a)(1)(iv) (docs/spec/sources/irs-1099-payment-method.md)",
|
|
||||||
"Treas. Reg. §1.6050W-1(b)(1), (b)(3) (payment card; accepted as payment)",
|
|
||||||
"IRS, Understanding your Form 1099-K (credit, debit or stored value cards)",
|
|
||||||
"Instructions for Forms 1099-MISC and 1099-NEC (Rev. 12/2026), Form 1099-K",
|
|
||||||
"DECISION D-40 (wanderbooks owner, 2026-10-20): an untagged bank payment counts",
|
|
||||||
]
|
|
||||||
tags = ["1099", "payment-card", "instruments"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "cash"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2050"
|
|
||||||
name = "Company card"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to Members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Contract Labor"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.instruments]]
|
|
||||||
label = "Harbor Visa"
|
|
||||||
kind = "business_card"
|
|
||||||
account = "2050"
|
|
||||||
|
|
||||||
[[setup.instruments]]
|
|
||||||
label = "Harbor debit card"
|
|
||||||
kind = "business_card"
|
|
||||||
account = "1000"
|
|
||||||
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Harbor Design LLC"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-10"
|
|
||||||
description = "Design work, charged to the company card"
|
|
||||||
payee = "Harbor Design LLC"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "2500.00" },
|
|
||||||
{ account = "2050", side = "credit", amount = "2500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-10"
|
|
||||||
description = "Design work, paid with the debit card"
|
|
||||||
payee = "Harbor Design LLC"
|
|
||||||
instrument = "Harbor debit card"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-04-10"
|
|
||||||
description = "Design work, paid by check"
|
|
||||||
payee = "Harbor Design LLC"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "900.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "900.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: counts 900.00 (the check); excluded 2,500.00 + 1,200.00 =
|
|
||||||
# 3,700.00 (the card's account, and the bank payment tagged with the debit
|
|
||||||
# card); 900.00 < 2,000.00: no 1099 is due.
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099"
|
|
||||||
payee = "Harbor Design LLC"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { status = "under_threshold", threshold = "2000.00", paid = "900.00", excluded_card = "3700.00", undetermined = "0.00", not_reportable = "0.00" }
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
id = "1099-money-between-own-accounts-pays-no-one"
|
|
||||||
title = "Paying the card bill, reimbursing a member, or paying down a loan pays no payee"
|
|
||||||
rule = """
|
|
||||||
A 1099 reports payments made to the payee. Money moved between the payer's
|
|
||||||
own payment sources (its bank paying its card's balance, reimbursing a
|
|
||||||
member who paid for it, paying down a loan's principal) pays no payee, even
|
|
||||||
when the entry is tagged with the card issuer, the member's payee record or
|
|
||||||
the lender: the movement out of one source is offset by the movement into
|
|
||||||
the other.
|
|
||||||
"""
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = [
|
|
||||||
"26 U.S.C. §6041(a) (payments made to the payee: docs/spec/sources/irs-1099-thresholds.md)",
|
|
||||||
"wanderbooks P3 review R1 (2026-10-20): netting an entry's payment buckets",
|
|
||||||
]
|
|
||||||
tags = ["1099", "netting"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "cash"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100", "2500"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2050"
|
|
||||||
name = "Company card"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to Members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2500"
|
|
||||||
name = "Equipment loan"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.instruments]]
|
|
||||||
label = "Harbor Visa"
|
|
||||||
kind = "business_card"
|
|
||||||
account = "2050"
|
|
||||||
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Harbor Card Services"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Harbor Equipment Finance"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-05-25"
|
|
||||||
description = "Card bill paid from the bank"
|
|
||||||
payee = "Harbor Card Services"
|
|
||||||
lines = [
|
|
||||||
{ account = "2050", side = "debit", amount = "1200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-01"
|
|
||||||
description = "Loan principal"
|
|
||||||
payee = "Harbor Equipment Finance"
|
|
||||||
lines = [
|
|
||||||
{ account = "2500", side = "debit", amount = "3000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "3000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: the bill: +1,200.00 out of the bank, −1,200.00 into the card's
|
|
||||||
# account: nets to 0.00 in every bucket. The loan: +3,000.00 out of the
|
|
||||||
# bank, −3,000.00 into the loan (a financing account): 0.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099"
|
|
||||||
payee = "Harbor Card Services"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { status = "under_threshold", threshold = "2000.00", paid = "0.00", excluded_card = "0.00", undetermined = "0.00", not_reportable = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099"
|
|
||||||
payee = "Harbor Equipment Finance"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { status = "under_threshold", threshold = "2000.00", paid = "0.00", excluded_card = "0.00", undetermined = "0.00", not_reportable = "0.00" }
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
id = "1099-money-from-a-payee-is-netted-only-as-a-refund"
|
|
||||||
title = "Money received from a payee lowers its 1099 only when it refunds what was paid"
|
|
||||||
rule = """
|
|
||||||
A 1099 reports the payments the payer made to the payee in the calendar
|
|
||||||
year. Money the payee gives back of a payment (a refund crediting the
|
|
||||||
expense, or a deposit returned) reduces what was paid. Money received from
|
|
||||||
the payee for anything else (its payment of the payer's invoice, a sale to
|
|
||||||
it, an asset disposed of to it, a loan from it) is not a refund of a
|
|
||||||
payment and is not netted against the payments; it is listed beside them.
|
|
||||||
"""
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = [
|
|
||||||
"26 U.S.C. §6041(a) (payments made in the course of the trade or business: docs/spec/sources/irs-1099-thresholds.md)",
|
|
||||||
"wanderbooks P3 review R6 (2026-10-20): a sale to a payee doesn't lower its 1099",
|
|
||||||
]
|
|
||||||
tags = ["1099", "netting", "refund"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "cash"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1200"
|
|
||||||
name = "Accounts Receivable"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1400"
|
|
||||||
name = "Deposits"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2600"
|
|
||||||
name = "Loan payable"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Contract Labor"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Dana Studio"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-01"
|
|
||||||
description = "Design work"
|
|
||||||
payee = "Dana Studio"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "3000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "3000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-01"
|
|
||||||
description = "Dana Studio pays the company's invoice"
|
|
||||||
older = true
|
|
||||||
payee = "Dana Studio"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "450.00" },
|
|
||||||
{ account = "1200", side = "credit", amount = "450.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-04-01"
|
|
||||||
description = "Dana Studio lends the company money"
|
|
||||||
payee = "Dana Studio"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "2600", side = "credit", amount = "1000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-05-01"
|
|
||||||
description = "Deposit for the next project"
|
|
||||||
payee = "Dana Studio"
|
|
||||||
lines = [
|
|
||||||
{ account = "1400", side = "debit", amount = "500.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-01"
|
|
||||||
description = "Deposit returned: the project was cancelled"
|
|
||||||
payee = "Dana Studio"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "500.00" },
|
|
||||||
{ account = "1400", side = "credit", amount = "500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: paid 3,000.00 + 500.00 (the deposit, paid when made) − 500.00
|
|
||||||
# (its return, a refund) = 3,000.00: reportable. The invoice payment
|
|
||||||
# (450.00) and the loan (1,000.00) are listed, not netted (netting them
|
|
||||||
# would leave 1,550.00, under the threshold).
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099"
|
|
||||||
payee = "Dana Studio"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { status = "reportable", threshold = "2000.00", paid = "3000.00", excluded_card = "0.00", undetermined = "0.00", not_reportable = "0.00" }
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
id = "1099-the-threshold-is-the-years"
|
|
||||||
title = "The 1099-NEC threshold is the one in force for the year the payments were made"
|
|
||||||
rule = """
|
|
||||||
A payer reports payments to a payee aggregating the threshold or more in a
|
|
||||||
calendar year: $600 for payments made through December 31, 2025, and
|
|
||||||
$2,000 ("of $2,000 or more") for payments made after it (indexed for
|
|
||||||
inflation for calendar years after 2026, not yet known: never guessed). The
|
|
||||||
threshold is met at exactly the amount.
|
|
||||||
"""
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"26 U.S.C. §6041(a), (h), §6041A(a)(2) (docs/spec/sources/irs-1099-thresholds.md)",
|
|
||||||
"Pub. L. 119-21 §70433(a), (f): \"shall apply with respect to payments made after December 31, 2025\" (docs/spec/sources/irs-1099-payment-method.md)",
|
|
||||||
"Instructions for Forms 1099-MISC and 1099-NEC (Rev. April 2025): $600",
|
|
||||||
]
|
|
||||||
tags = ["1099", "threshold"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "cash"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Contract Labor"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Riley Chen"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Cedar Studio"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2025-11-01"
|
|
||||||
description = "Illustration"
|
|
||||||
payee = "Riley Chen"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "600.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "600.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-01"
|
|
||||||
description = "Illustration"
|
|
||||||
payee = "Riley Chen"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1999.99" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1999.99" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-01"
|
|
||||||
description = "Photography"
|
|
||||||
payee = "Cedar Studio"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "2000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "2000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: 2025, Riley 600.00 against $600: reportable (exactly the
|
|
||||||
# threshold). 2026, Riley 1,999.99 against $2,000: under; Cedar 2,000.00:
|
|
||||||
# reportable.
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099"
|
|
||||||
payee = "Riley Chen"
|
|
||||||
from = "2025-01-01"
|
|
||||||
to = "2025-12-31"
|
|
||||||
values = { status = "reportable", threshold = "600.00", paid = "600.00", excluded_card = "0.00", undetermined = "0.00", not_reportable = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099"
|
|
||||||
payee = "Riley Chen"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { status = "under_threshold", threshold = "2000.00", paid = "1999.99", excluded_card = "0.00", undetermined = "0.00", not_reportable = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099"
|
|
||||||
payee = "Cedar Studio"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { status = "reportable", threshold = "2000.00", paid = "2000.00", excluded_card = "0.00", undetermined = "0.00", not_reportable = "0.00" }
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
# What a 1099 worksheet as of a day counts toward a payee: the payments
|
|
||||||
# of that day's calendar year made from January 1 through the day.
|
|
||||||
#
|
|
||||||
# wanderbooks: P3 defect 21 and decision D-36 (the CPA package's 1099
|
|
||||||
# worksheet counted the whole calendar year in a package as of June 30);
|
|
||||||
# rules.md B-19, B-33; regression test `the_1099_worksheet_stops_at_as_of`
|
|
||||||
# (ledger-package tests/package.rs).
|
|
||||||
#
|
|
||||||
# Setup beyond the accounts (bookspec/README.md): `cash_accounts` are the
|
|
||||||
# accounts payments are made from; each `[[setup.payees]]` is a payee
|
|
||||||
# marked for a form; an event's `payee` tags the payment to them. Event N
|
|
||||||
# is entry N. Expected values: vendor_1099_paid gives each marked payee's
|
|
||||||
# payments counted from `from`, January 1, through `to`, every payee
|
|
||||||
# listed. The 2026 threshold is $2,000 (B-19), so the payee is not yet
|
|
||||||
# reportable as of June 30 (1400.00) and is for the year (3050.00).
|
|
||||||
|
|
||||||
id = "1099-worksheet-through-as-of"
|
|
||||||
title = "A 1099 worksheet as of a day counts the year's payments through that day"
|
|
||||||
rule = "A payee's 1099 amount as of a day D MUST be the payments to the payee made from January 1 of D's calendar year through D, excluding payments made with a payment card; as of December 31 it is the whole calendar year's."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"26 U.S.C. §6041(a), as amended by Pub. L. 119-21 §70433: payments \"of $2,000 or more in any calendar year\" (docs/spec/sources/irs-1099-thresholds.md)",
|
|
||||||
"26 U.S.C. §6041A(a)(2): \"the aggregate of such remuneration paid to such person during such calendar year\" (docs/spec/sources/irs-1099-thresholds.md)",
|
|
||||||
"IRS, Instructions for Forms 1099-MISC and 1099-NEC (Rev. December 2026), Form 1099-K: \"Payments made with a credit card or payment card and certain other types of payments, including third-party network transactions, must be reported on Form 1099-K by the payment settlement entity under section 6050W and are not subject to reporting on Form 1099-MISC or Form 1099-NEC.\" (docs/spec/sources/irs-1099-thresholds.md)",
|
|
||||||
]
|
|
||||||
tags = ["1099", "as-of", "calendar-year", "information-return", "period-boundary"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "cash"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2200"
|
|
||||||
name = "Credit card"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Owner capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6100"
|
|
||||||
name = "Contract labor"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Contractor A"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
# Entry 1: the year before.
|
|
||||||
[[events]]
|
|
||||||
date = "2025-12-31"
|
|
||||||
description = "Contract work, December"
|
|
||||||
payee = "Contractor A"
|
|
||||||
lines = [
|
|
||||||
{ account = "6100", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 2.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-02"
|
|
||||||
description = "Owner contribution"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "10000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "10000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 3.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-15"
|
|
||||||
description = "Contract work, March"
|
|
||||||
payee = "Contractor A"
|
|
||||||
lines = [
|
|
||||||
{ account = "6100", side = "debit", amount = "900.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "900.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 4: by card, so reported on Form 1099-K, never counted here.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-10"
|
|
||||||
description = "Contract work, June, by card"
|
|
||||||
payee = "Contractor A"
|
|
||||||
lines = [
|
|
||||||
{ account = "6100", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "2200", side = "credit", amount = "300.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 5: made on the day itself, so counted.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-30"
|
|
||||||
description = "Contract work, June"
|
|
||||||
payee = "Contractor A"
|
|
||||||
lines = [
|
|
||||||
{ account = "6100", side = "debit", amount = "500.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 6: the day after.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-07-01"
|
|
||||||
description = "Contract work, July"
|
|
||||||
payee = "Contractor A"
|
|
||||||
lines = [
|
|
||||||
{ account = "6100", side = "debit", amount = "700.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "700.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 7.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-11-20"
|
|
||||||
description = "Contract work, November"
|
|
||||||
payee = "Contractor A"
|
|
||||||
lines = [
|
|
||||||
{ account = "6100", side = "debit", amount = "950.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "950.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099_paid"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-06-30"
|
|
||||||
values = { "Contractor A" = "1400.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099_paid"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Contractor A" = "3050.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099_paid"
|
|
||||||
from = "2025-01-01"
|
|
||||||
to = "2025-12-31"
|
|
||||||
values = { "Contractor A" = "100.00" }
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
id = "arap-hand-control-posting-refused"
|
|
||||||
title = "A hand line on receivables is refused; one an earlier version wrote still reads right"
|
|
||||||
rule = "A line on a receivables or payables control account MUST carry its document, so a hand entry MUST be refused; an entry an earlier version wrote is replayed as it stands."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"plan/served.md 8.3.1 A-6 (the owner: hand entries to 1200 and 2000 are refused)",
|
|
||||||
"plan/served.md D-45",
|
|
||||||
"ASC 310-10 (receivables)",
|
|
||||||
"ASC 405-20-40-1 (a liability is removed only when paid or released)",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "receivables", "legacy"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1200"
|
|
||||||
name = "Accounts Receivable"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Revenue"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
# The invoice makes 1200 a control account: its balance is now its
|
|
||||||
# documents'. The work was billed and the income counted.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "Survey work"
|
|
||||||
invoice = { customer = "Ridgeway Co", amount = "100.00" }
|
|
||||||
|
|
||||||
# A line an earlier version wrote by hand on 1200: this version's commands
|
|
||||||
# refuse one, but books that hold it must still read right, so it is
|
|
||||||
# posted as replay posts it.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-15"
|
|
||||||
description = "Receivable entered by hand"
|
|
||||||
older = true
|
|
||||||
lines = [
|
|
||||||
{ account = "1200", side = "debit", amount = "50.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "50.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# The same line written today is refused: no document carries it.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-20"
|
|
||||||
description = "Another hand line"
|
|
||||||
refused = "untagged_control_line"
|
|
||||||
lines = [
|
|
||||||
{ account = "1200", side = "debit", amount = "25.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "25.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "0.00"
|
|
||||||
"1200" = "150.00"
|
|
||||||
"4000" = "150.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "0.00"
|
|
||||||
"1200" = "150.00"
|
|
||||||
"4000" = "-150.00"
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# A report as of a day shows the books through that day, and nothing
|
|
||||||
# dated after it, however long after the day it is made.
|
|
||||||
#
|
|
||||||
# wanderbooks: P3 defect 21 (the CPA package's QuickBooks file held entries
|
|
||||||
# dated after the package's as_of); rules.md B-33; regression tests
|
|
||||||
# `every_file_in_an_as_of_package_stops_at_as_of` and
|
|
||||||
# `as_of_package_iif_stops_at_as_of` (ledger-package tests/package.rs).
|
|
||||||
#
|
|
||||||
# Expected values (bookspec/README.md): trial_balance gives each
|
|
||||||
# account's balance as of the day, debit positive and credit negative,
|
|
||||||
# every account listed; profit_and_loss gives each revenue and expense
|
|
||||||
# account's activity for the period on its normal side, every one
|
|
||||||
# listed, and net_income (revenue less expenses).
|
|
||||||
|
|
||||||
id = "as-of-package-stops-at-as-of"
|
|
||||||
title = "A report as of a day leaves out what is dated after it"
|
|
||||||
rule = "A balance or report as of a day D MUST include every posted entry dated on or before D and MUST NOT include any entry dated after D, whenever it is produced; a period report through D covers the entries dated from the period's first day through D."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = [
|
|
||||||
"IRS Publication 538 (Rev. January 2022), Tax year: \"A tax year is an annual accounting period for keeping records and reporting income and expenses.\" (docs/spec/sources/irs-periods-and-methods.md)",
|
|
||||||
"Pacioli, Summa de arithmetica (1494), ch. 36, the trial balance: the total of the debits equals the total of the credits (Geijsbeek translation, 1914; docs/spec/sources/pacioli-1494.md)",
|
|
||||||
]
|
|
||||||
tags = ["as-of", "trial-balance", "profit-and-loss", "reversal", "period-boundary"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Owner capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Rent"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# Entry 1.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-02"
|
|
||||||
description = "Owner contribution"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "10000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "10000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 2.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-01"
|
|
||||||
description = "Sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "4000.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "4000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 3: dated the day itself, so in.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-30"
|
|
||||||
description = "June rent"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 4: the day after, so out of anything as of 2026-06-30.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-07-01"
|
|
||||||
description = "July rent"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 5: a reversal dated after the day leaves its original in.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-07-15"
|
|
||||||
description = "Reversal of the February sale"
|
|
||||||
reverses = 2
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-06-30"
|
|
||||||
values = { "1000" = "12800.00", "3000" = "-10000.00", "4000" = "-4000.00", "6000" = "1200.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "profit_and_loss"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-06-30"
|
|
||||||
values = { "4000" = "4000.00", "6000" = "1200.00", net_income = "2800.00" }
|
|
||||||
|
|
||||||
# The same books a month later: the July rent and the reversal are in.
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-07-31"
|
|
||||||
values = { "1000" = "7600.00", "3000" = "-10000.00", "4000" = "0.00", "6000" = "2400.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "profit_and_loss"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-07-31"
|
|
||||||
values = { "4000" = "0.00", "6000" = "2400.00", net_income = "-2400.00" }
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# P3 defect 13, named regression 4
|
|
||||||
# (personal_use_contribution_keeps_its_723_basis_apart_from_depreciable_basis).
|
|
||||||
id = "assets-723-basis-apart-from-depreciable-basis"
|
|
||||||
title = "Property contributed from personal use: the §723 basis apart from the depreciable basis"
|
|
||||||
rule = "The partnership's basis in property a partner contributes is the partner's adjusted basis (IRC §723). Its depreciable basis, for property converted from personal use, is the lesser of fair market value and that adjusted basis at conversion (CHOICE recorded by the owner as D-41, the CPA to confirm). The §704(c) built-in gain or loss is fair market value less the §723 basis at contribution; a built-in loss counts only for the contributing partner (IRC §704(c)(1)(C))."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"26 U.S.C. §723",
|
|
||||||
"Treas. Reg. §1.168(i)-4(b)(1): 'the lesser of its fair market value or its adjusted depreciable basis'",
|
|
||||||
"IRS Pub. 946 (2025), Property changed from personal use",
|
|
||||||
"Treas. Reg. §1.704-3(a)(3)(ii)",
|
|
||||||
"26 U.S.C. §704(c)(1)(C)",
|
|
||||||
"DECISION D-41 (owner, 2026-10-20): the depreciable basis of contributed personal-use property is the lesser of its fair market value and the member's adjusted basis, the CPA to confirm (wanderbooks plan/served.md; rules.md B-25)",
|
|
||||||
]
|
|
||||||
tags = ["fixed-assets", "tax-basis", "partnerships", "704c"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# A laptop a member contributes from personal use: worth 1,440.00, held by
|
|
||||||
# the member at an adjusted basis of 1,688.40. (Nothing rounds here.)
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "laptop"
|
|
||||||
description = "Laptop"
|
|
||||||
cost = "1440.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 36
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 1
|
|
||||||
basis = { rule = "personal_use_conversion", contributor_adjusted_basis = "1688.40", fmv_at_contribution = "1440.00" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Laptop contributed at its value"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1440.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1440.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Hand check: §723 basis 1,688.40 (the member's adjusted basis).
|
|
||||||
# Depreciable: the lesser of 1,440.00 and 1,688.40 = 1,440.00. Built-in
|
|
||||||
# gain: 1,440.00 - 1,688.40 = -248.40, a loss (§704(c)(1)(C)).
|
|
||||||
[[expect]]
|
|
||||||
query = "asset_basis"
|
|
||||||
asset = "laptop"
|
|
||||||
values = { tax_basis_723 = "1688.40", depreciable_basis = "1440.00", built_in_gain_704c = "-248.40", sec704c_1c = "true" }
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# P3 defect 13, named regression 2 (a_reversed_depreciation_run_is_not_posted), its
|
|
||||||
# posted figure; a reversal of the reversal reinstates the run.
|
|
||||||
id = "assets-a-reversed-run-is-not-posted"
|
|
||||||
title = "A reversed depreciation run is no longer posted; reversing the reversal reinstates it"
|
|
||||||
rule = "The accumulated depreciation the books carry for an asset counts each run while its entry stands: a reversal takes the run back from the reversal's date, and a reversal of that reversal puts it back from its own date."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"wanderbooks docs/spec/rules.md B-8, B-9 (a correction is an opposite entry; reversing the reversal reinstates the original)",
|
|
||||||
"wanderbooks docs/spec/rules.md B-15 rules 6 and 8",
|
|
||||||
]
|
|
||||||
tags = ["fixed-assets", "depreciation", "reversal"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# 1,200.00, no salvage, 12 months from 2026-01-01: 100.00 a month.
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "printer"
|
|
||||||
description = "Printer"
|
|
||||||
cost = "1200.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 1
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Printer bought"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# January's run: 100.00, dated Jan 31.
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "printer", period = "2026-01" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-01"
|
|
||||||
description = "Reversal of the January run"
|
|
||||||
reverses = 2
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-02"
|
|
||||||
description = "Reversal of the reversal"
|
|
||||||
reverses = 3
|
|
||||||
|
|
||||||
# Hand check: on Jan 31 the books carry January's run, 100.00; on Feb 1 its
|
|
||||||
# reversal takes it back, 0.00, while the schedule through Feb 1 is
|
|
||||||
# 100.00 + 100.00 x 1/28 = 103.57 (all of it unposted); on Feb 2 the
|
|
||||||
# reversal of the reversal puts the run back, 100.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "register_row"
|
|
||||||
asset = "printer"
|
|
||||||
as_of = "2026-01-31"
|
|
||||||
values = { accumulated_posted = "100.00", book_value = "1100.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "register_row"
|
|
||||||
asset = "printer"
|
|
||||||
as_of = "2026-02-01"
|
|
||||||
values = { accumulated_posted = "0.00", accumulated_scheduled = "103.57", unposted = "103.57", book_value = "1200.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "register_row"
|
|
||||||
asset = "printer"
|
|
||||||
as_of = "2026-02-02"
|
|
||||||
values = { accumulated_posted = "100.00", book_value = "1100.00" }
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# P3 defect 13, named regression 3
|
|
||||||
# (an_asset_bought_before_it_is_in_service_is_on_the_register_from_acquisition).
|
|
||||||
id = "assets-acquired-before-in-service"
|
|
||||||
title = "An asset is on the register from acquisition and depreciates from its in-service date"
|
|
||||||
rule = "Depreciation starts on the day the asset is placed in service; the asset is on the register (and the books carry its cost) from the day the company acquired it, the date of the entry that put it on the books."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"ASC 360-10-35-4 (cost allocated over the periods the asset serves)",
|
|
||||||
"IRS Pub. 946 (2025), Placed in Service: 'ready and available for a specific use' (for tax depreciation, the CPA's)",
|
|
||||||
"wanderbooks docs/spec/rules.md B-15, B-24",
|
|
||||||
"DECISION (owner, 2026-10-20; wanderbooks plan/served.md P3, \"Settled in design\"): an asset's acquired date comes from the entry it is linked to (rules.md B-15 rule 7)",
|
|
||||||
]
|
|
||||||
tags = ["fixed-assets", "dates"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# A kiln bought 2026-03-10 for 3,000.00, placed in service 2026-04-01, 36
|
|
||||||
# months, no salvage.
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "kiln"
|
|
||||||
description = "Kiln"
|
|
||||||
cost = "3000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 36
|
|
||||||
in_service = "2026-04-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 1
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-10"
|
|
||||||
description = "Kiln bought"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "3000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "3000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Hand check: on the register from Mar 10 (its entry's date), nothing
|
|
||||||
# accrued before Apr 1 (a March charge from the purchase date would have
|
|
||||||
# been 3,000.00 / 36 x 22/31 = 59.14: none is due), and the register
|
|
||||||
# carries the cost the books do.
|
|
||||||
[[expect]]
|
|
||||||
query = "register_row"
|
|
||||||
asset = "kiln"
|
|
||||||
as_of = "2026-03-31"
|
|
||||||
values = { acquired = "2026-03-10", in_service = "2026-04-01", accumulated_scheduled = "0.00", accumulated_posted = "0.00", book_value = "3000.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "register_totals"
|
|
||||||
as_of = "2026-03-31"
|
|
||||||
values = { total_cost = "3000.00", unregistered_cost = "0.00" }
|
|
||||||
|
|
||||||
# April: 3,000.00 / 36 = 83.333... -> 83.33.
|
|
||||||
[[expect]]
|
|
||||||
query = "depreciation_schedule"
|
|
||||||
asset = "kiln"
|
|
||||||
as_of = "2026-04-30"
|
|
||||||
values = { "2026-03" = "0.00", "2026-04" = "83.33" }
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# P3 defect 13, named regression 1 (list_assets_book_value_is_what_the_books_carry).
|
|
||||||
id = "assets-book-value-is-what-the-books-carry"
|
|
||||||
title = "Book value is cost less the depreciation posted, never the schedule's"
|
|
||||||
rule = "An asset's carrying amount is its cost less the accumulated depreciation the books have recorded for it; what the depreciation schedule says is due through a date is a second figure, reported beside it, and their difference is depreciation not yet posted."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"ASC 360-10-35-4 (cost less salvage allocated over the periods the asset serves)",
|
|
||||||
"wanderbooks docs/spec/rules.md B-24 (the register reconciles to the general ledger)",
|
|
||||||
]
|
|
||||||
tags = ["fixed-assets", "depreciation", "posted-vs-scheduled"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# A laptop contributed 2026-05-15 at 1,440.00, in service that day, 36
|
|
||||||
# months, no salvage: 40.00 a month.
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "laptop"
|
|
||||||
description = "Laptop"
|
|
||||||
cost = "1440.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 36
|
|
||||||
in_service = "2026-05-15"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 1
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-05-15"
|
|
||||||
description = "Laptop contributed"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1440.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1440.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# May's run, dated the month's last day: 40.00 x 17/31 = 21.9355 -> 21.94.
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "laptop", period = "2026-05" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "run"
|
|
||||||
event = 2
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
amount = "21.94"
|
|
||||||
caught_up = "0.00"
|
|
||||||
|
|
||||||
# Hand check: through May 20, 6 days of May, 40.00 x 6/31 = 7.7419 ->
|
|
||||||
# 7.74 is due; the run is dated May 31, so nothing is posted yet, and the
|
|
||||||
# book value is the cost (before P3 the tool called 1,432.26 book value).
|
|
||||||
[[expect]]
|
|
||||||
query = "register_row"
|
|
||||||
asset = "laptop"
|
|
||||||
as_of = "2026-05-20"
|
|
||||||
values = { accumulated_posted = "0.00", accumulated_scheduled = "7.74", unposted = "7.74", book_value = "1440.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "register_row"
|
|
||||||
asset = "laptop"
|
|
||||||
as_of = "2026-05-31"
|
|
||||||
values = { accumulated_posted = "21.94", accumulated_scheduled = "21.94", unposted = "0.00", book_value = "1418.06" }
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# P3 defect 13, named regression 5 (built_in_gain_is_fixed_at_contribution_not_book_cost).
|
|
||||||
id = "assets-built-in-gain-fixed-at-contribution"
|
|
||||||
title = "The §704(c) built-in gain is fixed at contribution, whatever the book cost becomes"
|
|
||||||
rule = "The built-in gain on contributed property is the excess of its book value (its fair market value) over the contributing partner's adjusted tax basis upon contribution; correcting the book cost later never changes the figure at contribution."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"Treas. Reg. §1.704-3(a)(3)(i)-(ii)",
|
|
||||||
]
|
|
||||||
tags = ["fixed-assets", "tax-basis", "704c"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# A press contributed from business use: worth 1,000.00, held by the member
|
|
||||||
# at an adjusted basis of 400.00. (Nothing rounds here.)
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "press"
|
|
||||||
description = "Press"
|
|
||||||
cost = "1000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 36
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 1
|
|
||||||
basis = { rule = "carryover_723", contributor_adjusted_basis = "400.00", fmv_at_contribution = "1000.00" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Press contributed at its value"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Hand check: §723 basis 400.00, depreciated from (carryover); built-in
|
|
||||||
# gain 1,000.00 - 400.00 = 600.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "asset_basis"
|
|
||||||
asset = "press"
|
|
||||||
after = 1
|
|
||||||
values = { tax_basis_723 = "400.00", depreciable_basis = "400.00", built_in_gain_704c = "600.00" }
|
|
||||||
|
|
||||||
# The book cost corrected to 1,100.00 (costs to bring it to use): the gain
|
|
||||||
# at contribution stays 1,000.00 - 400.00 = 600.00, not 700.00.
|
|
||||||
[[changes]]
|
|
||||||
after = 1
|
|
||||||
correct_asset = { asset = "press", cost = "1100.00", reason = "costs to bring it to use" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "asset_basis"
|
|
||||||
asset = "press"
|
|
||||||
values = { tax_basis_723 = "400.00", fmv_at_contribution = "1000.00", built_in_gain_704c = "600.00" }
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# P3 defect 13: four devices two members contribute from personal use, each at the
|
|
||||||
# lower of its value and its basis.
|
|
||||||
id = "assets-four-contributed-devices"
|
|
||||||
title = "Each contributed device takes the lower of its value and its basis; gains and losses are per property"
|
|
||||||
rule = "Each item of contributed property is its own: depreciable basis is the lesser of its fair market value and the contributor's adjusted basis; its built-in gain or loss is its own, and gains and losses of different properties are never netted."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Treas. Reg. §1.168(i)-4(b)(1)",
|
|
||||||
"Treas. Reg. §1.704-3(a)(2): section 704(c) applies 'on a property-by-property basis'",
|
|
||||||
"Treas. Reg. §1.704-3(a)(3)(ii)",
|
|
||||||
"26 U.S.C. §704(c)(1)(C)",
|
|
||||||
"DECISION D-41 (owner, 2026-10-20): the depreciable basis of contributed personal-use property is the lesser of its fair market value and the member's adjusted basis, the CPA to confirm (wanderbooks plan/served.md; rules.md B-25)",
|
|
||||||
]
|
|
||||||
tags = ["fixed-assets", "tax-basis", "partnerships", "704c"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital, Tobin Harlow"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital, Morgan Ellis"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
capital_account = "3001"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
# Four devices the two members contribute jointly from personal use, each
|
|
||||||
# at its value. (Nothing rounds here.)
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "laptop"
|
|
||||||
description = "Laptop"
|
|
||||||
cost = "1400.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 36
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 1
|
|
||||||
basis = { rule = "personal_use_conversion", contributor_adjusted_basis = "1750.40", fmv_at_contribution = "1400.00", sec704c_method = "traditional" }
|
|
||||||
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "phone"
|
|
||||||
description = "Phone"
|
|
||||||
cost = "900.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 36
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 1
|
|
||||||
basis = { rule = "personal_use_conversion", contributor_adjusted_basis = "1129.99", fmv_at_contribution = "900.00", sec704c_method = "traditional" }
|
|
||||||
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "camera"
|
|
||||||
description = "Camera"
|
|
||||||
cost = "1300.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 36
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 1
|
|
||||||
basis = { rule = "personal_use_conversion", contributor_adjusted_basis = "1080.00", fmv_at_contribution = "1300.00", sec704c_method = "traditional" }
|
|
||||||
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "monitor"
|
|
||||||
description = "Monitor"
|
|
||||||
cost = "500.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 36
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 1
|
|
||||||
basis = { rule = "personal_use_conversion", contributor_adjusted_basis = "500.00", fmv_at_contribution = "500.00", sec704c_method = "traditional" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Four devices contributed jointly"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1400.00" },
|
|
||||||
{ account = "1500", side = "debit", amount = "900.00" },
|
|
||||||
{ account = "1500", side = "debit", amount = "1300.00" },
|
|
||||||
{ account = "1500", side = "debit", amount = "500.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "2050.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "2050.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Hand check, each device its own: the lesser of value and basis, and the
|
|
||||||
# value less the basis.
|
|
||||||
[[expect]]
|
|
||||||
query = "asset_basis"
|
|
||||||
asset = "laptop"
|
|
||||||
values = { tax_basis_723 = "1750.40", depreciable_basis = "1400.00", built_in_gain_704c = "-350.40", sec704c_1c = "true" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "asset_basis"
|
|
||||||
asset = "phone"
|
|
||||||
values = { tax_basis_723 = "1129.99", depreciable_basis = "900.00", built_in_gain_704c = "-229.99", sec704c_1c = "true" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "asset_basis"
|
|
||||||
asset = "camera"
|
|
||||||
values = { tax_basis_723 = "1080.00", depreciable_basis = "1080.00", built_in_gain_704c = "220.00", sec704c_1c = "false" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "asset_basis"
|
|
||||||
asset = "monitor"
|
|
||||||
values = { tax_basis_723 = "500.00", depreciable_basis = "500.00", built_in_gain_704c = "0.00", sec704c_1c = "false" }
|
|
||||||
|
|
||||||
# Totals: cost 1,400.00 + 900.00 + 1,300.00 + 500.00 = 4,100.00; §723
|
|
||||||
# 1,750.40 + 1,129.99 + 1,080.00 + 500.00 = 4,460.39; depreciable
|
|
||||||
# 1,400.00 + 900.00 + 1,080.00 + 500.00 = 3,880.00. The gains and losses
|
|
||||||
# are each property's own: the register totals none of them.
|
|
||||||
[[expect]]
|
|
||||||
query = "register_totals"
|
|
||||||
as_of = "2026-06-30"
|
|
||||||
values = { total_cost = "4100.00", total_tax_basis_723 = "4460.39", total_depreciable_basis = "3880.00", missing_basis = "0" }
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
id = "bad-debt-direct-write-off"
|
|
||||||
title = "An invoice that will not be collected is written off, in parts"
|
|
||||||
rule = "A debt that becomes worthless MUST be written off in the year it becomes worthless, a partial write-off MUST be deductible to the extent charged off, and no write-off may exceed what is owed."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"IRC §166(a)",
|
|
||||||
"IRC §111",
|
|
||||||
"Treas. Reg. §1.166-1(e)",
|
|
||||||
"Treas. Reg. §1.166-2(a)",
|
|
||||||
"Treas. Reg. §1.166-3(a)",
|
|
||||||
"plan/served.md 8.3.1 A-5: the direct write-off method",
|
|
||||||
"ASC 326-20 (noted: an allowance is the GAAP preference)",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "receivables"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1200"
|
|
||||||
name = "Accounts Receivable"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Revenue"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6990"
|
|
||||||
name = "Bad Debt Expense"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "bad_debts"
|
|
||||||
|
|
||||||
# The work was done and billed: the invoice is income already counted.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "Survey work"
|
|
||||||
invoice = { customer = "Ridgeway Co", amount = "100.00" }
|
|
||||||
|
|
||||||
# Part of it is charged off when the collection efforts end; the rest
|
|
||||||
# stays owed and aged.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-15"
|
|
||||||
write_off = { invoice = 1, amount = "40.00", reason = "No answer; the collection agency closed the file" }
|
|
||||||
|
|
||||||
# The liquidation notice makes the rest worthless, and it is written off.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-04-20"
|
|
||||||
write_off = { invoice = 1, amount = "60.00", reason = "Liquidation notice received" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "0.00"
|
|
||||||
"1200" = "0.00"
|
|
||||||
"4000" = "100.00"
|
|
||||||
"6990" = "100.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "0.00"
|
|
||||||
"1200" = "0.00"
|
|
||||||
"4000" = "-100.00"
|
|
||||||
"6990" = "100.00"
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
id = "cash-flow.debt-to-equity"
|
|
||||||
title = "An expense a member paid personally, then the member's advance converted to capital"
|
|
||||||
rule = "An expense a member pays for the company on credit to the member MUST be shown as cash borrowed from the member and spent, on its date; converting that debt to equity MUST be disclosed as a noncash financing activity, not reported as a cash flow, though both its accounts are financing."
|
|
||||||
# CHOICE: entry 1. ASC 230 doesn't address a payment a third party makes
|
|
||||||
# on the entity's behalf; the firm guides present it by substance as a
|
|
||||||
# constructive receipt and disbursement (a financing inflow and an
|
|
||||||
# operating outflow), and the owner chose that (wanderbooks D-42, revising
|
|
||||||
# D-35's noncash presentation). Entry 2's disclosure is 50-4's own example.
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"ASC 230-10-45-14(b)",
|
|
||||||
"ASC 230-10-45-17(b)",
|
|
||||||
"ASC 230-10-50-3",
|
|
||||||
"ASC 230-10-50-4",
|
|
||||||
"PwC Viewpoint, Financial statement presentation 6.9.3.1 (ASC 230 does not address cash a third party receives or pays on the entity's behalf; include it as though through the entity's accounts)",
|
|
||||||
"Deloitte Roadmap, Statement of cash flows 7.2 (constructive receipt and disbursement)",
|
|
||||||
"BDO Blueprint, Statement of cash flows under ASC 230 (December 2025) 3.4",
|
|
||||||
"wanderbooks D-42 (owner, 2026-10-20: a member or lender paying for the company is cash borrowed and spent)",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "cash-flow", "noncash", "constructive", "partnership"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital — Member A"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Supplies"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Member A"
|
|
||||||
capital_account = "3000"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-03"
|
|
||||||
description = "Supplies, paid by Member A personally"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "500.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-10"
|
|
||||||
description = "Member A's advance converted to capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "2100", side = "debit", amount = "500.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: no company cash moved. Entry 1 is borrowed from the member
|
|
||||||
# (financing +500.00, 45-14(b)) and spent (operating -500.00, class (d),
|
|
||||||
# 45-17(b)) on 2026-02-03, all of it on the company's behalf; net income
|
|
||||||
# -500.00 needs no reconciling item. Entry 2 converts debt to equity, 50-4's
|
|
||||||
# first example: disclosed 500.00, though both its accounts are financing.
|
|
||||||
[[expect]]
|
|
||||||
query = "cash_flow"
|
|
||||||
from = "2026-02-01"
|
|
||||||
to = "2026-02-28"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
operating = "-500.00"
|
|
||||||
investing = "0.00"
|
|
||||||
financing = "500.00"
|
|
||||||
net_change = "0.00"
|
|
||||||
difference = "0.00"
|
|
||||||
net_income = "-500.00"
|
|
||||||
"operating.1" = "-500.00"
|
|
||||||
"operating.1.on_behalf" = "-500.00"
|
|
||||||
"operating.count" = "1"
|
|
||||||
"financing.1" = "500.00"
|
|
||||||
"financing.1.on_behalf" = "500.00"
|
|
||||||
"financing.count" = "1"
|
|
||||||
"noncash.2" = "500.00"
|
|
||||||
"noncash.count" = "1"
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
# A payment to a government is not a supplier's, whatever account it is
|
|
||||||
# booked to: the payee decides (and so does a taxes-and-licenses account).
|
|
||||||
#
|
|
||||||
# wanderbooks: the P3 cutover review's open point 2 (a state's LLC filing
|
|
||||||
# fee, booked to an organization-costs account, read as cash paid to
|
|
||||||
# suppliers); rules.md B-14 rule 5; regression tests
|
|
||||||
# `a_payment_to_a_government_is_other_operating`,
|
|
||||||
# `a_government_payee_moves_a_fee_on_any_account`,
|
|
||||||
# `a_shared_government_payment_splits_to_the_cent`,
|
|
||||||
# `operating_classes_follow_asc_230_10_45_25` (ledger-core
|
|
||||||
# tests/spec_cash_flow.rs).
|
|
||||||
|
|
||||||
id = "cash-flow.government-fees"
|
|
||||||
title = "Fees paid to a government are other operating payments, not cash paid to suppliers"
|
|
||||||
rule = "In a statement of cash flows by the direct method, an operating cash payment to a government for taxes, duties, fines or other fees or penalties MUST NOT be reported as cash paid to suppliers and employees: it MUST be reported among other operating cash payments (income taxes apart), whether the payee is a government or the account is one for taxes and licenses. A payment to a supplier on the same account MUST stay cash paid to suppliers, and a payment shared between a government and a supplier MUST be split by their shares."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"ASC 230-10-45-17(b): \"Cash payments to other suppliers and employees for other goods or services\"; 45-17(c): \"Cash payments to governments for taxes, duties, fines, and other fees or penalties\" (EY Financial reporting developments, Statement of cash flows, July 2026, p. 43; docs/spec/sources/fasb-asc.md)",
|
|
||||||
"ASC 230-10-45-22: \"a reporting entity shall determine each separately identifiable source or each separately identifiable use within the cash receipts and cash payments on the basis of the nature of the underlying cash flows\" (EY p. 44)",
|
|
||||||
"ASC 230-10-45-25: the classes reported at a minimum, (d) cash paid to employees and other suppliers, (f) income taxes paid, (g) other operating cash payments (EY p. 8)",
|
|
||||||
]
|
|
||||||
tags = ["cash", "book", "us", "cash-flow", "operating-classes"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "cash"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
# Formation costs: a state's filing fee and a lawyer's bill both land here.
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Organization costs"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "organization_startup"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6400"
|
|
||||||
name = "Office supplies"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "office_expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6700"
|
|
||||||
name = "Taxes and licenses"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "taxes_licenses"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Department of Commerce, Corporations Division"
|
|
||||||
roles = ["government_agency"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Department of Revenue"
|
|
||||||
roles = ["tax_agency"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Ashgrove Legal LLP"
|
|
||||||
roles = ["vendor"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Paper Goods Co."
|
|
||||||
roles = ["vendor"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Northgate Registered Agents"
|
|
||||||
roles = ["vendor"]
|
|
||||||
|
|
||||||
# Event 1.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-05"
|
|
||||||
description = "Opening contribution"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "2000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "2000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Event 2: the state's fee to file the company, on the formation account.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-02"
|
|
||||||
description = "Certificate of organization filing fee"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "75.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "75.00" },
|
|
||||||
]
|
|
||||||
parties = [{ party = "Department of Commerce, Corporations Division" }]
|
|
||||||
|
|
||||||
# Event 3: the lawyer's bill for the operating agreement, the same account.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-03"
|
|
||||||
description = "Operating agreement"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "350.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "350.00" },
|
|
||||||
]
|
|
||||||
parties = [{ party = "Ashgrove Legal LLP" }]
|
|
||||||
|
|
||||||
# Event 4: a business license, on the taxes-and-licenses account, no
|
|
||||||
# party named.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-10"
|
|
||||||
description = "City business license"
|
|
||||||
lines = [
|
|
||||||
{ account = "6700", side = "debit", amount = "40.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "40.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Event 5: a late-filing penalty paid to the tax agency, on supplies by
|
|
||||||
# mistake: still the government's.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-12"
|
|
||||||
description = "Late filing penalty"
|
|
||||||
lines = [
|
|
||||||
{ account = "6400", side = "debit", amount = "15.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "15.00" },
|
|
||||||
]
|
|
||||||
parties = [{ party = "Department of Revenue" }]
|
|
||||||
|
|
||||||
# Event 6: paper and toner.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-20"
|
|
||||||
description = "Paper and toner"
|
|
||||||
lines = [
|
|
||||||
{ account = "6400", side = "debit", amount = "62.50" },
|
|
||||||
{ account = "1000", side = "credit", amount = "62.50" },
|
|
||||||
]
|
|
||||||
parties = [{ party = "Paper Goods Co." }]
|
|
||||||
|
|
||||||
# Event 7: an annual report filed through a registered agent, one
|
|
||||||
# payment: 37.00 of it the state's fee, 100.00 the agent's service.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-25"
|
|
||||||
description = "Annual report through the registered agent"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "137.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "137.00" },
|
|
||||||
]
|
|
||||||
parties = [
|
|
||||||
{ party = "Department of Commerce, Corporations Division", amount = "37.00" },
|
|
||||||
{ party = "Northgate Registered Agents", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: February's payments are 75.00 + 350.00 + 40.00 + 15.00 + 62.50
|
|
||||||
# + 137.00 = 679.50, all operating. To governments (45-17(c)), other
|
|
||||||
# operating cash payments (g): the filing fee (a government agency is its
|
|
||||||
# payee), the license (its account is taxes and licenses), the penalty (a
|
|
||||||
# tax agency is its payee) and the state's part of the annual report
|
|
||||||
# (separately identifiable by its share, 45-22) = 75.00 + 40.00 + 15.00 +
|
|
||||||
# 37.00 = 167.00. To suppliers (45-17(b)), (d): the lawyer, the paper and
|
|
||||||
# the agent's part = 350.00 + 62.50 + 100.00 = 512.50. Cash 2,000.00 to
|
|
||||||
# 1,320.50.
|
|
||||||
[[expect]]
|
|
||||||
query = "cash_flow"
|
|
||||||
from = "2026-02-01"
|
|
||||||
to = "2026-02-28"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
operating = "-679.50"
|
|
||||||
investing = "0.00"
|
|
||||||
financing = "0.00"
|
|
||||||
net_change = "-679.50"
|
|
||||||
cash_beginning = "2000.00"
|
|
||||||
cash_ending = "1320.50"
|
|
||||||
difference = "0.00"
|
|
||||||
"operating.2" = "-75.00"
|
|
||||||
"operating.3" = "-350.00"
|
|
||||||
"operating.4" = "-40.00"
|
|
||||||
"operating.5" = "-15.00"
|
|
||||||
"operating.6" = "-62.50"
|
|
||||||
"operating.7" = "-137.00"
|
|
||||||
"operating.count" = "6"
|
|
||||||
"class.d" = "-512.50"
|
|
||||||
"class.g" = "-167.00"
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
id = "cash-flow.paid-on-the-companys-behalf"
|
|
||||||
title = "A member and a lender pay for the company directly; the member is repaid"
|
|
||||||
rule = "When a member or a lender pays a company expense or purchase directly, with no cash account moving, the statement of cash flows MUST show a financing inflow (borrowed from the member or lender, or contributed when the member's capital is credited for an expense) and the operating or investing outflow, both on that payment's date; repaying the member or lender MUST be a financing outflow; such a payment and its reversal MUST net out together."
|
|
||||||
# CHOICE: ASC 230 does not address cash a third party pays on the entity's
|
|
||||||
# behalf. The firm guides present it by substance as a constructive
|
|
||||||
# receipt and disbursement (PwC 6.9.3.1; Deloitte 7.2, Example 7-2, a
|
|
||||||
# lender paying an insurer; BDO 3.4, Example 3-5, a bank paying the
|
|
||||||
# supplier of equipment, and a parent paying a subsidiary's expenses),
|
|
||||||
# and the owner chose that (wanderbooks D-42). KPMG also accepts a noncash
|
|
||||||
# presentation for equipment a lender pays for.
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"ASC 230-10-45-13(c)",
|
|
||||||
"ASC 230-10-45-14(a)",
|
|
||||||
"ASC 230-10-45-14(b)",
|
|
||||||
"ASC 230-10-45-15(b)",
|
|
||||||
"ASC 230-10-45-17(b)",
|
|
||||||
"PwC Viewpoint, Financial statement presentation 6.9.3.1 (cash a third party receives or pays on the entity's behalf is included as though through the entity's accounts)",
|
|
||||||
"Deloitte Roadmap, Statement of cash flows 7.2 (constructive receipt and disbursement; Example 7-2)",
|
|
||||||
"BDO Blueprint, Statement of cash flows under ASC 230 (December 2025) 3.4 and Example 3-5",
|
|
||||||
"EY Financial reporting developments, Statement of cash flows (July 2026) 4.6.2 and 5.3.1",
|
|
||||||
"wanderbooks D-42 (owner, 2026-10-20: a member or lender paying for the company is cash borrowed and spent)",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "cash-flow", "constructive", "partnership"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100", "2500"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2500"
|
|
||||||
name = "Bank loan"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital — Member A"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Supplies"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6100"
|
|
||||||
name = "Depreciation"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.fixed_assets]]
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6100"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Member A"
|
|
||||||
capital_account = "3000"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-02"
|
|
||||||
description = "Member A contributes cash"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-05"
|
|
||||||
description = "Supplies, paid by Member A personally (owed back)"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "120.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "120.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-09"
|
|
||||||
description = "Equipment, paid to the seller by the bank (a loan)"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "2400.00" },
|
|
||||||
{ account = "2500", side = "credit", amount = "2400.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-12"
|
|
||||||
description = "Supplies, paid by Member A personally (contributed)"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "45.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "45.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-15"
|
|
||||||
description = "Supplies, paid by Member A personally (posted in error)"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "30.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "30.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-16"
|
|
||||||
description = "Reverse entry 5"
|
|
||||||
reverses = 5
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-20"
|
|
||||||
description = "Reimburse Member A"
|
|
||||||
lines = [
|
|
||||||
{ account = "2100", side = "debit", amount = "120.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "120.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-28"
|
|
||||||
description = "Loan payment"
|
|
||||||
lines = [
|
|
||||||
{ account = "2500", side = "debit", amount = "200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand, March. Operating: entry 2, -120.00, and entry 4, -45.00 (cash
|
|
||||||
# paid to suppliers, 45-17(b), by the member for the company): -165.00;
|
|
||||||
# entries 5 and 6 net out. Investing: entry 3, -2,400.00 (equipment,
|
|
||||||
# 45-13(c), paid by the bank). Financing: entry 1 +1,000.00 and entry 4
|
|
||||||
# +45.00 (contributions, 45-14(a)), entry 2 +120.00 and entry 3 +2,400.00
|
|
||||||
# (borrowing, 45-14(b)), entry 7 -120.00 and entry 8 -200.00 (repayments,
|
|
||||||
# 45-15(b)): +3,245.00. Sum -165.00 - 2,400.00 + 3,245.00 = +680.00, the
|
|
||||||
# change in cash (1,000.00 - 120.00 - 200.00). Net income -165.00 equals
|
|
||||||
# operating: no reconciling item. Nothing is disclosed: no part of any
|
|
||||||
# entry went unsettled.
|
|
||||||
[[expect]]
|
|
||||||
query = "cash_flow"
|
|
||||||
from = "2026-03-01"
|
|
||||||
to = "2026-03-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
operating = "-165.00"
|
|
||||||
investing = "-2400.00"
|
|
||||||
financing = "3245.00"
|
|
||||||
net_change = "680.00"
|
|
||||||
cash_beginning = "0.00"
|
|
||||||
cash_ending = "680.00"
|
|
||||||
difference = "0.00"
|
|
||||||
net_income = "-165.00"
|
|
||||||
"operating.2" = "-120.00"
|
|
||||||
"operating.2.on_behalf" = "-120.00"
|
|
||||||
"operating.4" = "-45.00"
|
|
||||||
"operating.count" = "2"
|
|
||||||
"investing.3" = "-2400.00"
|
|
||||||
"investing.3.on_behalf" = "-2400.00"
|
|
||||||
"investing.count" = "1"
|
|
||||||
"financing.1" = "1000.00"
|
|
||||||
"financing.1.on_behalf" = "0.00"
|
|
||||||
"financing.2" = "120.00"
|
|
||||||
"financing.2.on_behalf" = "120.00"
|
|
||||||
"financing.3" = "2400.00"
|
|
||||||
"financing.3.on_behalf" = "2400.00"
|
|
||||||
"financing.4" = "45.00"
|
|
||||||
"financing.4.on_behalf" = "45.00"
|
|
||||||
"financing.7" = "-120.00"
|
|
||||||
"financing.7.on_behalf" = "0.00"
|
|
||||||
"financing.8" = "-200.00"
|
|
||||||
"financing.count" = "6"
|
|
||||||
"noncash.count" = "0"
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
id = "cash-flow.part-cash-purchase"
|
|
||||||
title = "Equipment bought part for cash, part on the seller's note"
|
|
||||||
rule = "Of a transaction that is part cash and part noncash, only the cash portion MUST be reported in the statement of cash flows; the rest MUST be disclosed as a noncash investing and financing activity."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = ["ASC 230-10-50-5", "ASC 230-10-50-3", "ASC 230-10-50-4", "ASC 230-10-45-13(c)", "EY Financial reporting developments, Statement of cash flows (July 2026) 5.3.1 (seller-financed purchases are noncash to the extent of the seller's financing)"]
|
|
||||||
tags = ["accrual", "book", "us", "cash-flow", "noncash"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
# The note is the seller's own credit, not a lender's: it is not listed as
|
|
||||||
# financing (a lender that paid the seller would be: the purchase would
|
|
||||||
# then be borrowed and invested, cash-flow.paid-on-the-companys-behalf).
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1520"
|
|
||||||
name = "Vehicles"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1530"
|
|
||||||
name = "Accumulated depreciation, vehicles"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2500"
|
|
||||||
name = "Note payable to the seller"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6100"
|
|
||||||
name = "Depreciation"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.fixed_assets]]
|
|
||||||
cost_account = "1520"
|
|
||||||
accumulated_account = "1530"
|
|
||||||
expense_account = "6100"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-02"
|
|
||||||
description = "Opening contribution"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "10000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "10000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-08"
|
|
||||||
description = "Vehicle: 300 down, 700 on the seller's note"
|
|
||||||
lines = [
|
|
||||||
{ account = "1520", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "300.00" },
|
|
||||||
{ account = "2500", side = "credit", amount = "700.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: 300 left the bank for the vehicle (investing, 45-13(c)); the
|
|
||||||
# 700 the seller financed moved no cash, to anyone, and is disclosed (50-4:
|
|
||||||
# acquiring an asset by assuming a directly related liability). Cash
|
|
||||||
# 10,000.00 to 9,700.00 in February.
|
|
||||||
[[expect]]
|
|
||||||
query = "cash_flow"
|
|
||||||
from = "2026-02-01"
|
|
||||||
to = "2026-02-28"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
operating = "0.00"
|
|
||||||
investing = "-300.00"
|
|
||||||
financing = "0.00"
|
|
||||||
net_change = "-300.00"
|
|
||||||
cash_beginning = "10000.00"
|
|
||||||
cash_ending = "9700.00"
|
|
||||||
difference = "0.00"
|
|
||||||
"investing.2" = "-300.00"
|
|
||||||
"investing.count" = "1"
|
|
||||||
"noncash.2" = "700.00"
|
|
||||||
"noncash.count" = "1"
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
id = "cash-flow.reversal-of-a-reversal"
|
|
||||||
title = "A contribution, its reversal and the reversal's reversal"
|
|
||||||
rule = "Reversals MUST pair along the chain from its root: an entry and its reversal both in the period drop out together, and a reversal of a reversal whose partner is already paired MUST be reported, as its root would be."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"ASC 230-10-45-14(a)",
|
|
||||||
"ASC 230-10-45-24",
|
|
||||||
"Pacioli, Summa (1494), ch. 31 (the correcting entry)",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "cash-flow", "reversal"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital — Member B"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Member B"
|
|
||||||
capital_account = "3001"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-20"
|
|
||||||
description = "Opening contribution"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "1000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-02"
|
|
||||||
description = "Contribution — Member B"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "300.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-03"
|
|
||||||
description = "Reverse entry 2"
|
|
||||||
reverses = 2
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-04"
|
|
||||||
description = "Reverse entry 3: the contribution stands"
|
|
||||||
reverses = 3
|
|
||||||
|
|
||||||
# By hand, February: entries 2 and 3 pair (the root and its reversal);
|
|
||||||
# entry 4 reinstates the contribution and is reported: financing +300.00,
|
|
||||||
# the change in cash.
|
|
||||||
[[expect]]
|
|
||||||
query = "cash_flow"
|
|
||||||
from = "2026-02-01"
|
|
||||||
to = "2026-02-28"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
financing = "300.00"
|
|
||||||
net_change = "300.00"
|
|
||||||
cash_beginning = "1000.00"
|
|
||||||
cash_ending = "1300.00"
|
|
||||||
difference = "0.00"
|
|
||||||
"financing.4" = "300.00"
|
|
||||||
"financing.count" = "1"
|
|
||||||
"noncash.count" = "0"
|
|
||||||
|
|
||||||
# From February 3 the root is before the period: entries 3 and 4 pair, and
|
|
||||||
# nothing is reported (no cash moved net, 1,300.00 to 1,300.00).
|
|
||||||
[[expect]]
|
|
||||||
query = "cash_flow"
|
|
||||||
from = "2026-02-03"
|
|
||||||
to = "2026-02-28"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
financing = "0.00"
|
|
||||||
net_change = "0.00"
|
|
||||||
cash_beginning = "1300.00"
|
|
||||||
cash_ending = "1300.00"
|
|
||||||
difference = "0.00"
|
|
||||||
"financing.count" = "0"
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
id = "cash-flow.reversed-property-contribution"
|
|
||||||
title = "A property contribution reversed the same day leaves the statement of cash flows empty"
|
|
||||||
rule = "An entry and its reversal both in the period MUST contribute nothing to any section or disclosure, and the sections MUST sum to the change in cash."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"ASC 230-10-45-1",
|
|
||||||
"ASC 230-10-45-24",
|
|
||||||
"ASC 230-10-50-3",
|
|
||||||
"Pacioli, Summa (1494), ch. 31 (the correcting entry)",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "cash-flow", "reversal", "regression:p3-defect-1"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6100"
|
|
||||||
name = "Depreciation"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.fixed_assets]]
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6100"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "Property contributed"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "2340.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "2340.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "Reverse entry 1"
|
|
||||||
reverses = 1
|
|
||||||
|
|
||||||
# By hand: no cash moved (sections 0.00, change in cash 0.00), and the
|
|
||||||
# entry and its reversal cancel, so nothing is disclosed either. Before the
|
|
||||||
# fix (wanderbooks P3, defect 1) investing showed -2,340.00 against a
|
|
||||||
# change of 0.00: the reversal's credit to equipment fell in no section.
|
|
||||||
[[expect]]
|
|
||||||
query = "cash_flow"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
operating = "0.00"
|
|
||||||
investing = "0.00"
|
|
||||||
financing = "0.00"
|
|
||||||
net_change = "0.00"
|
|
||||||
cash_beginning = "0.00"
|
|
||||||
cash_ending = "0.00"
|
|
||||||
difference = "0.00"
|
|
||||||
net_income = "0.00"
|
|
||||||
"operating.count" = "0"
|
|
||||||
"investing.count" = "0"
|
|
||||||
"financing.count" = "0"
|
|
||||||
"noncash.count" = "0"
|
|
||||||
@@ -1,443 +0,0 @@
|
|||||||
id = "cash-flow.two-member-llc-first-year"
|
|
||||||
title = "A two-member LLC's first weeks: contributions, member-paid expenses, property contributed, corrections"
|
|
||||||
rule = "The statement of cash flows MUST report cash receipts and payments, by section, with each reversal netted against its original in the period; expenses members paid personally on credit to them MUST be shown as cash borrowed from the members and spent, on each payment's date; property the members contributed MUST be disclosed as a noncash activity; the sections MUST sum to the change in cash."
|
|
||||||
# CHOICE: the member-paid expenses. ASC 230 doesn't address a payment made
|
|
||||||
# on the entity's behalf; the firm guides present it by substance as a
|
|
||||||
# constructive receipt and disbursement (a financing inflow and an
|
|
||||||
# operating outflow), and the owner chose that (D-42, revising D-35's
|
|
||||||
# noncash presentation). The rest is the Codification's.
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"ASC 230-10-10-1",
|
|
||||||
"ASC 230-10-45-14(a)",
|
|
||||||
"ASC 230-10-45-14(b)",
|
|
||||||
"ASC 230-10-45-15(b)",
|
|
||||||
"ASC 230-10-45-17(b)",
|
|
||||||
"ASC 230-10-45-24",
|
|
||||||
"ASC 230-10-45-28",
|
|
||||||
"ASC 230-10-45-30",
|
|
||||||
"ASC 230-10-50-3",
|
|
||||||
"ASC 230-10-50-4",
|
|
||||||
"PwC Viewpoint, Financial statement presentation 6.9.3.1 (cash a third party receives or pays on the entity's behalf is included as though through the entity's accounts)",
|
|
||||||
"Deloitte Roadmap, Statement of cash flows 7.2 (constructive receipt and disbursement)",
|
|
||||||
"BDO Blueprint, Statement of cash flows under ASC 230 (December 2025) 3.4 (a parent paying a subsidiary's expenses)",
|
|
||||||
"wanderbooks D-42 (owner, 2026-10-20: a member or lender paying for the company is cash borrowed and spent)",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "cash-flow", "partnership", "constructive", "regression:p3-defect-1"]
|
|
||||||
|
|
||||||
# A two-member LLC's first weeks, in the shape of the books P3 defect 1
|
|
||||||
# was found in: the same 23 entries in the same order, on the same
|
|
||||||
# accounts and roles (two contributions in cash, expenses the members paid
|
|
||||||
# personally through Due to Members and two reimbursements, a domain
|
|
||||||
# expensed then corrected to a prepaid, two card charges, four devices the
|
|
||||||
# members owned contributed, two of them first entered wrongly and
|
|
||||||
# reversed), with every amount and date this example's own. Before the fix
|
|
||||||
# the statement didn't foot: the reversals' credits to equipment (#13,
|
|
||||||
# #18) belonged to no section, and the contributed devices read as
|
|
||||||
# purchases.
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1300"
|
|
||||||
name = "Prepaid Expenses"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2050"
|
|
||||||
name = "Business Card"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to Members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital — Member A"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital — Member B"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Organizational Costs"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6050"
|
|
||||||
name = "Domain & Hosting"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Developer Fees"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6300"
|
|
||||||
name = "Software & Subscriptions"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6400"
|
|
||||||
name = "Memberships & Licenses"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6500"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.fixed_assets]]
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6500"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Member A"
|
|
||||||
capital_account = "3000"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Member B"
|
|
||||||
capital_account = "3001"
|
|
||||||
|
|
||||||
# Entry 1.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-02"
|
|
||||||
description = "Formation filing fee, paid by the members"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "85.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "85.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 2.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-03"
|
|
||||||
description = "Domain, 1 year, paid by the members"
|
|
||||||
lines = [
|
|
||||||
{ account = "6050", side = "debit", amount = "12.97" },
|
|
||||||
{ account = "2100", side = "credit", amount = "12.97" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 3.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-05"
|
|
||||||
description = "Developer registration, paid by the members"
|
|
||||||
lines = [
|
|
||||||
{ account = "6200", side = "debit", amount = "99.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "99.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 4.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-09"
|
|
||||||
description = "Contribution — Member A"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1200.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 5.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-10"
|
|
||||||
description = "Contribution — Member B"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "800.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "800.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 6.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-11"
|
|
||||||
description = "Subscription top-up, paid by the members"
|
|
||||||
lines = [
|
|
||||||
{ account = "6300", side = "debit", amount = "7.49" },
|
|
||||||
{ account = "2100", side = "credit", amount = "7.49" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 7.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-16"
|
|
||||||
description = "Developer membership, paid by the members"
|
|
||||||
lines = [
|
|
||||||
{ account = "6400", side = "debit", amount = "129.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "129.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 8.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-23"
|
|
||||||
description = "Reimburse members"
|
|
||||||
lines = [
|
|
||||||
{ account = "2100", side = "debit", amount = "333.46" },
|
|
||||||
{ account = "1000", side = "credit", amount = "333.46" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 9.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-20"
|
|
||||||
description = "Domain, 2 years, paid by the members"
|
|
||||||
lines = [
|
|
||||||
{ account = "6050", side = "debit", amount = "46.38" },
|
|
||||||
{ account = "2100", side = "credit", amount = "46.38" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 10.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-25"
|
|
||||||
description = "API credits on the card"
|
|
||||||
lines = [
|
|
||||||
{ account = "6300", side = "debit", amount = "14.20" },
|
|
||||||
{ account = "2050", side = "credit", amount = "14.20" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 11.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-26"
|
|
||||||
description = "Reimburse members"
|
|
||||||
lines = [
|
|
||||||
{ account = "2100", side = "debit", amount = "46.38" },
|
|
||||||
{ account = "1000", side = "credit", amount = "46.38" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 12.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-27"
|
|
||||||
description = "Laptop contributed by Member A"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1480.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1480.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 13.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-27"
|
|
||||||
description = "Reverse entry 12: contributed jointly"
|
|
||||||
reverses = 12
|
|
||||||
|
|
||||||
# Entry 14.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-27"
|
|
||||||
description = "Laptop contributed jointly"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1480.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "740.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "740.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 15.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-27"
|
|
||||||
description = "Laptop contributed jointly"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1190.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "595.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "595.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 16.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-27"
|
|
||||||
description = "Desktop contributed jointly"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "2210.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1105.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "1105.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 17.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-27"
|
|
||||||
description = "Tablet contributed jointly"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "630.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "315.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "315.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 18.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-27"
|
|
||||||
description = "Reverse entry 15: the service plan is separate"
|
|
||||||
reverses = 15
|
|
||||||
|
|
||||||
# Entry 19.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-27"
|
|
||||||
description = "Laptop and its service plan contributed jointly"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1151.00" },
|
|
||||||
{ account = "1300", side = "debit", amount = "39.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "595.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "595.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 20.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-20"
|
|
||||||
description = "Domain, 2 years, paid by the members, prepaid"
|
|
||||||
lines = [
|
|
||||||
{ account = "1300", side = "debit", amount = "46.38" },
|
|
||||||
{ account = "2100", side = "credit", amount = "46.38" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 21.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-12"
|
|
||||||
description = "Subscription, paid by the members"
|
|
||||||
lines = [
|
|
||||||
{ account = "6300", side = "debit", amount = "216.40" },
|
|
||||||
{ account = "2100", side = "credit", amount = "216.40" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 22.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-20"
|
|
||||||
description = "Reverse entry 9: prepaid, not expensed"
|
|
||||||
reverses = 9
|
|
||||||
|
|
||||||
# Entry 23.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-30"
|
|
||||||
description = "API credits on the card"
|
|
||||||
lines = [
|
|
||||||
{ account = "6300", side = "debit", amount = "42.75" },
|
|
||||||
{ account = "2050", side = "credit", amount = "42.75" },
|
|
||||||
]
|
|
||||||
# By hand, the year to 2026-03-31 (rules.md B-14; D-42 for what the
|
|
||||||
# members paid). Cash is 1000; 2100 (Due to Members) is financing.
|
|
||||||
# - Operating: the members paid #1 85.00, #2 12.97, #3 99.00, #6 7.49,
|
|
||||||
# #21 216.40, #7 129.00 and #20 46.38 (the prepaid domain) for the
|
|
||||||
# company through 2100, each borrowed (financing, 45-14(b)) and spent
|
|
||||||
# (operating, 45-17(b)) on its date: 85.00 + 12.97 + 99.00 + 7.49 +
|
|
||||||
# 216.40 + 129.00 + 46.38 = 596.24, so operating -596.24, all cash paid
|
|
||||||
# to suppliers. #9 and its reversal #22 net out (rule 6). The card's #10
|
|
||||||
# and #23 moved no cash and 2050 is not financing: no line.
|
|
||||||
# - Investing 0.00: every device was property the members owned (50-4),
|
|
||||||
# disclosed below; #12/#13 and #15/#18 net out.
|
|
||||||
# - Financing: #4 +1,200.00 and #5 +800.00 (contributions, 45-14(a));
|
|
||||||
# +596.24 borrowed from the members (the seven above); the
|
|
||||||
# reimbursements #8 -333.46 (#1, #2, #3, #6 and #7: 85.00 + 12.97 +
|
|
||||||
# 99.00 + 7.49 + 129.00) and #11 -46.38 (#20), 45-15(b): 1,200.00 +
|
|
||||||
# 800.00 + 596.24 - 333.46 - 46.38 = +2,216.40.
|
|
||||||
# - Sum: -596.24 + 0.00 + 2,216.40 = +1,620.16, the change in cash (in
|
|
||||||
# 1,200.00 + 800.00, out 333.46 + 46.38; 0.00 to 1,620.16); difference
|
|
||||||
# 0.00.
|
|
||||||
# - Reconciliation (45-28 to 45-30): net income is minus the expenses,
|
|
||||||
# 85.00 + 12.97 + 99.00 + 7.49 + 129.00 + 46.38 - 46.38 + 14.20 + 216.40
|
|
||||||
# + 42.75 = 606.81, so -606.81; other noncash +39.00 (#19's service
|
|
||||||
# plan, contributed with the laptop); increase in 1300, 39.00 + 46.38,
|
|
||||||
# so -85.38; increase in 2050, 14.20 + 42.75, so +56.95. Total -606.81 +
|
|
||||||
# 39.00 - 85.38 + 56.95 = -596.24, the operating section.
|
|
||||||
# - Disclosed (50-3, 50-4), the property contributed: #14 1,480.00, #16
|
|
||||||
# 2,210.00, #17 630.00, #19 1,190.00 (1,151.00 equipment and 39.00 a
|
|
||||||
# prepaid service plan), 5,510.00 in all; #12/#13 and #15/#18 drop out.
|
|
||||||
[[expect]]
|
|
||||||
query = "cash_flow"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-03-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
operating = "-596.24"
|
|
||||||
investing = "0.00"
|
|
||||||
financing = "2216.40"
|
|
||||||
net_change = "1620.16"
|
|
||||||
cash_beginning = "0.00"
|
|
||||||
cash_ending = "1620.16"
|
|
||||||
difference = "0.00"
|
|
||||||
net_income = "-606.81"
|
|
||||||
"operating.1" = "-85.00"
|
|
||||||
"operating.2" = "-12.97"
|
|
||||||
"operating.3" = "-99.00"
|
|
||||||
"operating.6" = "-7.49"
|
|
||||||
"operating.7" = "-129.00"
|
|
||||||
"operating.20" = "-46.38"
|
|
||||||
"operating.21" = "-216.40"
|
|
||||||
"operating.count" = "7"
|
|
||||||
"investing.count" = "0"
|
|
||||||
"financing.1" = "85.00"
|
|
||||||
"financing.1.on_behalf" = "85.00"
|
|
||||||
"financing.2" = "12.97"
|
|
||||||
"financing.3" = "99.00"
|
|
||||||
"financing.4" = "1200.00"
|
|
||||||
"financing.4.on_behalf" = "0.00"
|
|
||||||
"financing.5" = "800.00"
|
|
||||||
"financing.6" = "7.49"
|
|
||||||
"financing.7" = "129.00"
|
|
||||||
"financing.8" = "-333.46"
|
|
||||||
"financing.11" = "-46.38"
|
|
||||||
"financing.20" = "46.38"
|
|
||||||
"financing.20.on_behalf" = "46.38"
|
|
||||||
"financing.21" = "216.40"
|
|
||||||
"financing.count" = "11"
|
|
||||||
"reconciliation.other_noncash" = "39.00"
|
|
||||||
"reconciliation.balance_change.1300" = "-85.38"
|
|
||||||
"reconciliation.balance_change.2050" = "56.95"
|
|
||||||
"noncash.14" = "1480.00"
|
|
||||||
"noncash.16" = "2210.00"
|
|
||||||
"noncash.17" = "630.00"
|
|
||||||
"noncash.19" = "1190.00"
|
|
||||||
"noncash.count" = "4"
|
|
||||||
|
|
||||||
# The same books to 2026-03-18: #1 to #7 and #21 are dated by then (#9,
|
|
||||||
# #20 and #22 on 03-20, the rest later). The members paid #1, #2, #3, #6,
|
|
||||||
# #7 and #21 for the company, 85.00 + 12.97 + 99.00 + 7.49 + 129.00 +
|
|
||||||
# 216.40 = 549.86: operating -549.86; financing #4 +1,200.00, #5 +800.00
|
|
||||||
# and the 549.86 borrowed, +2,549.86; the change in cash 2,000.00
|
|
||||||
# (-549.86 + 2,549.86); net income -549.86 with no reconciling item (no
|
|
||||||
# operating balance has moved yet); nothing disclosed.
|
|
||||||
[[expect]]
|
|
||||||
query = "cash_flow"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-03-18"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
operating = "-549.86"
|
|
||||||
investing = "0.00"
|
|
||||||
financing = "2549.86"
|
|
||||||
net_change = "2000.00"
|
|
||||||
difference = "0.00"
|
|
||||||
net_income = "-549.86"
|
|
||||||
"operating.1" = "-85.00"
|
|
||||||
"operating.2" = "-12.97"
|
|
||||||
"operating.3" = "-99.00"
|
|
||||||
"operating.6" = "-7.49"
|
|
||||||
"operating.7" = "-129.00"
|
|
||||||
"operating.21" = "-216.40"
|
|
||||||
"operating.count" = "6"
|
|
||||||
"financing.1" = "85.00"
|
|
||||||
"financing.2" = "12.97"
|
|
||||||
"financing.3" = "99.00"
|
|
||||||
"financing.4" = "1200.00"
|
|
||||||
"financing.5" = "800.00"
|
|
||||||
"financing.6" = "7.49"
|
|
||||||
"financing.7" = "129.00"
|
|
||||||
"financing.21" = "216.40"
|
|
||||||
"financing.count" = "8"
|
|
||||||
"noncash.count" = "0"
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# A report asked for without dates covers this fiscal year so far.
|
|
||||||
#
|
|
||||||
# wanderbooks: P3 defect 16 (the report tools, books.show and the viewer
|
|
||||||
# each read omitted dates their own way); rules.md B-30; regression tests
|
|
||||||
# `prop_period_defaults_resolve` (ledger-core tests/properties.rs),
|
|
||||||
# `one_default_period_everywhere_defect_16` (ledger-cli
|
|
||||||
# tests/mcp_journeys.rs), `show_sends_the_dates_it_names_defect_16` and
|
|
||||||
# `show_refuses_start_after_end_defect_15` (ledger-cli tests/oracle_mcp.rs).
|
|
||||||
#
|
|
||||||
# Expected values (bookspec/README.md): `period` gives the dates a period
|
|
||||||
# report asked with these dates covers, `day` the day a report as of a day
|
|
||||||
# is made as of, `calendar_year` the year a calendar-year count counts,
|
|
||||||
# each by the one rule on `today`; a query that omits a date resolves it
|
|
||||||
# by the same rule. The amounts fall one to a fiscal year (July to June),
|
|
||||||
# so each period's figures show which period it was.
|
|
||||||
|
|
||||||
id = "default-period-fiscal-year-to-date"
|
|
||||||
title = "A report asked for without dates covers this fiscal year so far"
|
|
||||||
rule = "A period report whose end is omitted MUST end today, in the company's time zone, and one whose start is omitted MUST start on the first day of the fiscal year that holds its end; a start after the end, given or derived, MUST be refused, never swapped. A report as of a day whose day is omitted MUST be as of today, and a count by calendar year whose year is omitted MUST count this calendar year."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"IRS Publication 538 (Rev. January 2022), Tax year: \"A tax year is an annual accounting period for keeping records and reporting income and expenses.\" (the period: AUTHORITY; docs/spec/sources/irs-periods-and-methods.md)",
|
|
||||||
"26 U.S.C. §6041(a), \"in any calendar year\"; §6041A(a)(2), \"during such calendar year\" (the calendar-year counts: AUTHORITY; docs/spec/sources/irs-1099-thresholds.md)",
|
|
||||||
"DECISION, the owner, 2026-10-20 (wanderbooks plan/served.md P3, \"Settled in design\"): the fiscal year to date as the default, everywhere; no authority says what omitted dates mean (wanderbooks docs/spec/rules.md B-30)",
|
|
||||||
]
|
|
||||||
tags = ["periods", "reports", "defaults", "fiscal-year"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "07-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
today = "2026-10-20"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Software"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# Entry 1: in the fiscal year July 2024 to June 2025.
|
|
||||||
[[events]]
|
|
||||||
date = "2025-03-15"
|
|
||||||
description = "Capital contributed"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "500.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entries 2 and 3: in the fiscal year July 2025 to June 2026.
|
|
||||||
[[events]]
|
|
||||||
date = "2025-09-10"
|
|
||||||
description = "Sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "120.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "120.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-01"
|
|
||||||
description = "Software"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "20.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "20.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 4: in the fiscal year that holds today, July 2026 to June 2027.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-08-03"
|
|
||||||
description = "Sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "7.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "7.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Both dates omitted: the fiscal year that holds today, through today.
|
|
||||||
[[expect]]
|
|
||||||
query = "period"
|
|
||||||
values = { from = "2026-07-01", to = "2026-10-20" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "profit_and_loss"
|
|
||||||
values = { "4000" = "7.00", "6000" = "0.00", net_income = "7.00" }
|
|
||||||
|
|
||||||
# Only an end: the fiscal year that holds the end, through it (never the
|
|
||||||
# first entry's day, never the fiscal year of today).
|
|
||||||
[[expect]]
|
|
||||||
query = "period"
|
|
||||||
to = "2026-03-31"
|
|
||||||
values = { from = "2025-07-01", to = "2026-03-31" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "profit_and_loss"
|
|
||||||
to = "2026-03-31"
|
|
||||||
values = { "4000" = "120.00", "6000" = "20.00", net_income = "100.00" }
|
|
||||||
|
|
||||||
# Only a start: through today.
|
|
||||||
[[expect]]
|
|
||||||
query = "period"
|
|
||||||
from = "2025-01-01"
|
|
||||||
values = { from = "2025-01-01", to = "2026-10-20" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "profit_and_loss"
|
|
||||||
from = "2025-01-01"
|
|
||||||
values = { "4000" = "127.00", "6000" = "20.00", net_income = "107.00" }
|
|
||||||
|
|
||||||
# A one-day period is a period.
|
|
||||||
[[expect]]
|
|
||||||
query = "period"
|
|
||||||
from = "2025-09-10"
|
|
||||||
to = "2025-09-10"
|
|
||||||
values = { from = "2025-09-10", to = "2025-09-10" }
|
|
||||||
|
|
||||||
# A start after the end is refused, never swapped.
|
|
||||||
[[expect]]
|
|
||||||
query = "period"
|
|
||||||
from = "2026-12-31"
|
|
||||||
to = "2026-01-01"
|
|
||||||
refused = "end_before_start"
|
|
||||||
|
|
||||||
# Only a start, after today: the end an omitted end means is today, so the
|
|
||||||
# period would end before it starts. Refused, never swapped.
|
|
||||||
[[expect]]
|
|
||||||
query = "period"
|
|
||||||
from = "2027-01-01"
|
|
||||||
refused = "start_after_today"
|
|
||||||
|
|
||||||
# A report as of a day: today.
|
|
||||||
[[expect]]
|
|
||||||
query = "day"
|
|
||||||
values = { as_of = "2026-10-20" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
values = { "1000" = "607.00", "3000" = "-500.00", "4000" = "-127.00", "6000" = "20.00" }
|
|
||||||
|
|
||||||
# A count by calendar year: this calendar year, not the fiscal one.
|
|
||||||
[[expect]]
|
|
||||||
query = "calendar_year"
|
|
||||||
values = { year = "2026" }
|
|
||||||
|
|
||||||
# The calendar's first fiscal year: a July year holding a day early in
|
|
||||||
# year 1 began in year 0, outside the calendar; it starts on the
|
|
||||||
# calendar's first day.
|
|
||||||
[[expect]]
|
|
||||||
query = "period"
|
|
||||||
to = "0001-03-01"
|
|
||||||
values = { from = "0001-01-01", to = "0001-03-01" }
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
id = "depreciation-catch-up-month"
|
|
||||||
title = "A month of depreciation skipped is caught up in the month the run is made"
|
|
||||||
rule = "A depreciation run for a month posts everything the schedule says is due through that month's end that the books don't carry, so a skipped month is caught up in it, and a later run for the skipped month posts nothing."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = ["ASC 360-10-35-4", "DECISION: plan/served.md P3, Settled in design (2026-10-20)"]
|
|
||||||
tags = ["depreciation", "catch-up", "skipped-month", "p3"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "7100"
|
|
||||||
name = "Gain or Loss on Disposal"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
# Asset X: 12,000.00, no salvage, 12 months from 2026-01-01 (1,000.00 a
|
|
||||||
# month; December the final month).
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "X"
|
|
||||||
description = "Asset X"
|
|
||||||
cost = "12000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Bought asset X"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-01" }
|
|
||||||
|
|
||||||
# February is skipped; March is run.
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-03" }
|
|
||||||
|
|
||||||
# February run afterwards: nothing is due (March caught it up).
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-02" }
|
|
||||||
|
|
||||||
# Hand check (worked without the code): the schedule through Mar 31 is
|
|
||||||
# 3 x 1,000.00 = 3,000.00; January's run carried 1,000.00, so March's run
|
|
||||||
# posts 2,000.00 (its own 1,000.00 and February's, caught up). February's
|
|
||||||
# run then posts 0.00: the books already carry the schedule through
|
|
||||||
# February's end and beyond.
|
|
||||||
[[expect]]
|
|
||||||
query = "run"
|
|
||||||
event = 4
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
amount = "2000.00"
|
|
||||||
caught_up = "1000.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "run"
|
|
||||||
event = 5
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
amount = "0.00"
|
|
||||||
caught_up = "0.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-03-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "-3000.00"
|
|
||||||
"6200" = "3000.00"
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
id = "depreciation-disposal-after-a-late-change-undone"
|
|
||||||
title = "A disposal is taken once a late change to an earlier run is undone, on any later day"
|
|
||||||
rule = "A disposal clears what the asset's runs carry now, so it waits until each run carries at the end of the disposal's month what it carries now. A reversal of an earlier run dated after that month is undone by reversing it, on any open day: the disposal month and the end are then right, and a later month end keeps the change and its correction as they were made."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = ["ASC 360-10-35-4", "Pacioli 1494, ch. 31"]
|
|
||||||
tags = ["depreciation", "disposal", "refusal", "reversal", "p3-defect-22"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "7100"
|
|
||||||
name = "Gain or Loss on Disposal"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
# Asset X: 12,000.00, no salvage, 12 months from 2026-01-01 (1,000.00 a
|
|
||||||
# month; December the final month).
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "X"
|
|
||||||
description = "Asset X"
|
|
||||||
cost = "12000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Bought asset X"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-01" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-02" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-03" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-04" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-05" }
|
|
||||||
|
|
||||||
# May's run reversed on Aug 1 (doubted). Its month, and July's, still
|
|
||||||
# carried it at their ends.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-08-01"
|
|
||||||
description = "Reverse event 7"
|
|
||||||
reverses = 7
|
|
||||||
|
|
||||||
# Disposed of 2026-06-15 for 7,000.00: refused. The disposal clears what
|
|
||||||
# the runs carry now (January to April, 4,000.00) and would charge May
|
|
||||||
# again at Jun 15, so June would end carrying 6,500.00 against a
|
|
||||||
# schedule of 5,500.00. The refusal names May's run and the reversal.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-15"
|
|
||||||
dispose_asset = { asset = "X", proceeds = "7000.00", proceeds_account = "1000", gain_loss_account = "7100" }
|
|
||||||
refused = "depreciation_past_disposal"
|
|
||||||
|
|
||||||
# May was right: its reversal reversed on Sep 2 (August may be closed by
|
|
||||||
# then; any open day undoes it).
|
|
||||||
[[events]]
|
|
||||||
date = "2026-09-02"
|
|
||||||
description = "Reverse event 8"
|
|
||||||
reverses = 8
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-15"
|
|
||||||
dispose_asset = { asset = "X", proceeds = "7000.00", proceeds_account = "1000", gain_loss_account = "7100" }
|
|
||||||
|
|
||||||
# Hand check (worked without the code): the runs carry 5,000.00 at Jun 30
|
|
||||||
# and now. Held through Jun 15: 5,000.00 + 1,000.00 x 15/30 = 5,500.00, so
|
|
||||||
# the final depreciation is 500.00 and the disposal clears 5,500.00. Book
|
|
||||||
# value 6,500.00; gain 7,000.00 - 6,500.00 = 500.00. August's end carries
|
|
||||||
# May's reversal (accumulated depreciation a 1,000.00 debit, the expense
|
|
||||||
# 4,500.00) and September's its correction, as they would without the
|
|
||||||
# disposal.
|
|
||||||
[[expect]]
|
|
||||||
query = "disposal"
|
|
||||||
asset = "X"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
final_depreciation = "500.00"
|
|
||||||
catch_up_depreciation = "0.00"
|
|
||||||
excess_depreciation = "0.00"
|
|
||||||
accumulated = "5500.00"
|
|
||||||
book_value = "6500.00"
|
|
||||||
gain_loss = "500.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-06-30"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "0.00"
|
|
||||||
"6200" = "5500.00"
|
|
||||||
"7100" = "500.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-08-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "1000.00"
|
|
||||||
"6200" = "4500.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-09-30"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "0.00"
|
|
||||||
"6200" = "5500.00"
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
id = "depreciation-disposal-before-a-later-run"
|
|
||||||
title = "A disposal dated before a later month's run is refused until that run is reversed"
|
|
||||||
rule = "A disposal is refused while a run of a month after the disposal's stands: crediting it back at the disposal day would leave the disposal month's accumulated depreciation a debit and its expense short at the month's end. Once that run is reversed, the disposal takes the depreciation of the days held."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = ["ASC 360-10-35-4"]
|
|
||||||
tags = ["depreciation", "disposal", "refusal", "later-run", "p3-defect-22"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "7100"
|
|
||||||
name = "Gain or Loss on Disposal"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
# Asset X: 12,000.00, no salvage, 12 months from 2026-01-01 (1,000.00 a
|
|
||||||
# month; December the final month).
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "X"
|
|
||||||
description = "Asset X"
|
|
||||||
cost = "12000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Bought asset X"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-01" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-02" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-03" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-04" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-05" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-06" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-07" }
|
|
||||||
|
|
||||||
# Disposed of 2026-06-15 for 7,000.00, after July's run: refused, naming
|
|
||||||
# July's run.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-15"
|
|
||||||
dispose_asset = { asset = "X", proceeds = "7000.00", proceeds_account = "1000", gain_loss_account = "7100" }
|
|
||||||
refused = "depreciation_past_disposal"
|
|
||||||
|
|
||||||
# July's run reversed, dated in July.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-07-31"
|
|
||||||
description = "Reverse event 9"
|
|
||||||
reverses = 9
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-15"
|
|
||||||
dispose_asset = { asset = "X", proceeds = "7000.00", proceeds_account = "1000", gain_loss_account = "7100" }
|
|
||||||
|
|
||||||
# Hand check (worked without the code): January through June carry
|
|
||||||
# 6,000.00 once July's run is reversed. Held through Jun 15: 5,000.00 +
|
|
||||||
# 1,000.00 x 15/30 = 5,500.00, so June's run posted 500.00 past the
|
|
||||||
# disposal day. Book value 6,500.00; gain 7,000.00 - 6,500.00 = 500.00.
|
|
||||||
# Had the disposal taken July's run back at Jun 15 (1,500.00 credited),
|
|
||||||
# June would end with accumulated depreciation a 1,000.00 debit and
|
|
||||||
# 4,500.00 of expense.
|
|
||||||
[[expect]]
|
|
||||||
query = "disposal"
|
|
||||||
asset = "X"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
final_depreciation = "0.00"
|
|
||||||
catch_up_depreciation = "0.00"
|
|
||||||
excess_depreciation = "500.00"
|
|
||||||
accumulated = "6000.00"
|
|
||||||
book_value = "6500.00"
|
|
||||||
gain_loss = "500.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-06-30"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "0.00"
|
|
||||||
"6200" = "5500.00"
|
|
||||||
"7100" = "500.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-07-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "0.00"
|
|
||||||
"6200" = "5500.00"
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
id = "depreciation-disposal-in-final-month"
|
|
||||||
title = "A disposal in the life's final month depreciates that month by its days held"
|
|
||||||
rule = "A disposal dated inside an asset's final month takes depreciation for the days held in that month, not the month's whole charge."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = ["ASC 360-10-35-4"]
|
|
||||||
tags = ["depreciation", "disposal", "final-month", "p3-defect-22"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "7100"
|
|
||||||
name = "Gain or Loss on Disposal"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
# Asset X: 12,000.00, no salvage, 12 months from 2026-01-01 (1,000.00 a
|
|
||||||
# month; December the final month).
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "X"
|
|
||||||
description = "Asset X"
|
|
||||||
cost = "12000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Bought asset X"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-01" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-02" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-03" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-04" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-05" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-06" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-07" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-08" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-09" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-10" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-11" }
|
|
||||||
|
|
||||||
# Disposed of 2026-12-10 for 1,000.00.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-12-10"
|
|
||||||
dispose_asset = { asset = "X", proceeds = "1000.00", proceeds_account = "1000", gain_loss_account = "7100" }
|
|
||||||
|
|
||||||
# Hand check (worked without the code): January..November run, 11,000.00.
|
|
||||||
# December held through the 10th: 1,000.00 x 10/31 = 322.580... -> 322.58.
|
|
||||||
# Accumulated 11,322.58; book value 677.42; gain 1,000.00 - 677.42 =
|
|
||||||
# 322.58. (Before P3: 1,000.00 of depreciation and a 1,000.00 gain.)
|
|
||||||
[[expect]]
|
|
||||||
query = "disposal"
|
|
||||||
asset = "X"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
final_depreciation = "322.58"
|
|
||||||
catch_up_depreciation = "0.00"
|
|
||||||
excess_depreciation = "0.00"
|
|
||||||
accumulated = "11322.58"
|
|
||||||
book_value = "677.42"
|
|
||||||
gain_loss = "322.58"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "1000.00"
|
|
||||||
"1510" = "0.00"
|
|
||||||
"6200" = "11322.58"
|
|
||||||
"7100" = "322.58"
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
id = "depreciation-disposal-inside-a-posted-month"
|
|
||||||
title = "A disposal inside a month already run takes back the days after it"
|
|
||||||
rule = "When a month's depreciation was posted through its end and the asset is disposed of inside that month, the disposal credits back the depreciation for the days after the disposal day, so book value and gain are of the days held."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = ["ASC 360-10-35-4"]
|
|
||||||
tags = ["depreciation", "disposal", "mid-month", "excess-depreciation", "p3-defect-22"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "7100"
|
|
||||||
name = "Gain or Loss on Disposal"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
# Asset X: 12,000.00, no salvage, 12 months from 2026-01-01 (1,000.00 a
|
|
||||||
# month; December the final month).
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "X"
|
|
||||||
description = "Asset X"
|
|
||||||
cost = "12000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Bought asset X"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-01" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-02" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-03" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-04" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-05" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-06" }
|
|
||||||
|
|
||||||
# Disposed of 2026-06-15 for 7,000.00, after June's run (dated Jun 30).
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-15"
|
|
||||||
dispose_asset = { asset = "X", proceeds = "7000.00", proceeds_account = "1000", gain_loss_account = "7100" }
|
|
||||||
|
|
||||||
# Hand check (worked without the code): the runs carried 6,000.00. Held
|
|
||||||
# through Jun 15: 5,000.00 + 1,000.00 x 15/30 = 5,500.00, so 500.00 was
|
|
||||||
# posted past the disposal day. The disposal entry: Dr accumulated
|
|
||||||
# depreciation 6,000.00, Dr bank 7,000.00 / Cr equipment 12,000.00, Cr
|
|
||||||
# depreciation expense 500.00, Cr gain 500.00 (13,000.00 a side). Book
|
|
||||||
# value 6,500.00; gain 7,000.00 - 6,500.00 = 500.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "disposal"
|
|
||||||
asset = "X"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
final_depreciation = "0.00"
|
|
||||||
catch_up_depreciation = "0.00"
|
|
||||||
excess_depreciation = "500.00"
|
|
||||||
accumulated = "6000.00"
|
|
||||||
book_value = "6500.00"
|
|
||||||
gain_loss = "500.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-06-30"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "0.00"
|
|
||||||
"6200" = "5500.00"
|
|
||||||
"7100" = "500.00"
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
id = "depreciation-disposal-mid-month"
|
|
||||||
title = "A disposed asset depreciates through its disposal day, and its schedule stops there"
|
|
||||||
rule = "An asset disposed of mid-month is depreciated through the disposal day, held, and its schedule, accumulated depreciation and book value afterwards are what the disposal took."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = ["ASC 360-10-35-4"]
|
|
||||||
tags = ["depreciation", "disposal", "mid-month", "p3-defect-22"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "7100"
|
|
||||||
name = "Gain or Loss on Disposal"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
# Asset X: 12,000.00, no salvage, 12 months from 2026-01-01 (1,000.00 a
|
|
||||||
# month; December the final month).
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "X"
|
|
||||||
description = "Asset X"
|
|
||||||
cost = "12000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Bought asset X"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-01" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-02" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-03" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-04" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-05" }
|
|
||||||
|
|
||||||
# Disposed of 2026-06-15 for 7,000.00.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-15"
|
|
||||||
dispose_asset = { asset = "X", proceeds = "7000.00", proceeds_account = "1000", gain_loss_account = "7100" }
|
|
||||||
|
|
||||||
# Hand check (worked without the code): January..May run, 5 x 1,000.00 =
|
|
||||||
# 5,000.00. June held through the 15th: 1,000.00 x 15/30 = 500.00, the
|
|
||||||
# final depreciation. Accumulated 5,500.00; book value 12,000.00 -
|
|
||||||
# 5,500.00 = 6,500.00; gain 7,000.00 - 6,500.00 = 500.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "disposal"
|
|
||||||
asset = "X"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
final_depreciation = "500.00"
|
|
||||||
catch_up_depreciation = "0.00"
|
|
||||||
excess_depreciation = "0.00"
|
|
||||||
accumulated = "5500.00"
|
|
||||||
book_value = "6500.00"
|
|
||||||
gain_loss = "500.00"
|
|
||||||
|
|
||||||
# The disposed asset's schedule ends on the disposal day, whatever date it
|
|
||||||
# is asked through.
|
|
||||||
[[expect]]
|
|
||||||
query = "depreciation_schedule"
|
|
||||||
asset = "X"
|
|
||||||
as_of = "2030-12-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"2026-06" = "500.00"
|
|
||||||
accumulated = "5500.00"
|
|
||||||
book_value = "6500.00"
|
|
||||||
|
|
||||||
# The books: depreciation expense 5,500.00 (debit), the gain 500.00
|
|
||||||
# (credit), accumulated depreciation and equipment cleared, the bank
|
|
||||||
# 7,000.00 (debit).
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-06-30"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "7000.00"
|
|
||||||
"1500" = "0.00"
|
|
||||||
"1510" = "0.00"
|
|
||||||
"6200" = "5500.00"
|
|
||||||
"7100" = "500.00"
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
id = "depreciation-final-month-by-days"
|
|
||||||
title = "Straight-line depreciation by the day through the life's final month"
|
|
||||||
rule = "Depreciation through a day of an asset's final month is the depreciation before that month plus the final month's share for the days held, never the whole remaining cost."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = ["ASC 360-10-35-4"]
|
|
||||||
tags = ["depreciation", "straight-line", "final-month", "p3-defect-22"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# Asset X: 12,000.00, no salvage, 12 months from 2026-01-01. Its life is
|
|
||||||
# 2026-01-01 through 2026-12-31; December is its final month.
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "X"
|
|
||||||
description = "Asset X"
|
|
||||||
cost = "12000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Bought asset X"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Hand check (worked without the code): 12,000.00 / 12 = 1,000.00 a month.
|
|
||||||
# Through Dec 15: January..November, 11 x 1,000.00 = 11,000.00, plus
|
|
||||||
# December's 1,000.00 x 15/31 = 483.870... -> 483.87 (half away from zero).
|
|
||||||
# Accumulated 11,483.87; book value 12,000.00 - 11,483.87 = 516.13.
|
|
||||||
[[expect]]
|
|
||||||
query = "depreciation_schedule"
|
|
||||||
asset = "X"
|
|
||||||
as_of = "2026-12-15"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"2026-11" = "1000.00"
|
|
||||||
"2026-12" = "483.87"
|
|
||||||
accumulated = "11483.87"
|
|
||||||
book_value = "516.13"
|
|
||||||
|
|
||||||
# Hand check: through the life's last day, the whole of it.
|
|
||||||
[[expect]]
|
|
||||||
query = "depreciation_schedule"
|
|
||||||
asset = "X"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"2026-12" = "1000.00"
|
|
||||||
accumulated = "12000.00"
|
|
||||||
book_value = "0.00"
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
id = "depreciation-final-month-drift"
|
|
||||||
title = "The final month of a mid-month life takes what the months before it leave, evenly over its days"
|
|
||||||
rule = "When a life does not start on the 1st, its final month is charged the depreciable amount less what the months before it took, spread evenly over the final month's days."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = ["ASC 360-10-35-4"]
|
|
||||||
tags = ["depreciation", "straight-line", "final-month", "month-length-drift", "p3-defect-22"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# Cost 1,030.00, salvage 100.00 (930.00 to depreciate), 3 months from
|
|
||||||
# 2026-01-15: the life is 2026-01-15 through 2026-04-14.
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "D"
|
|
||||||
description = "Asset D"
|
|
||||||
cost = "1030.00"
|
|
||||||
salvage = "100.00"
|
|
||||||
life_months = 3
|
|
||||||
in_service = "2026-01-15"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-15"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1030.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1030.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-15"
|
|
||||||
description = "Bought asset D"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1030.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1030.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Hand check (worked without the code): 930.00 / 3 = 310.00 a month.
|
|
||||||
# January holds 17 of its 31 days: 310.00 x 17/31 = 170.00. February and
|
|
||||||
# March are whole: 310.00 each. Before April: 790.00. April holds 14 days
|
|
||||||
# (1-14) and is charged what is left, 930.00 - 790.00 = 140.00, evenly:
|
|
||||||
# 10.00 a day (January's own daily rate, 310.00/31). Through Apr 13:
|
|
||||||
# 130.00; accumulated 920.00; book value 1,030.00 - 920.00 = 110.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "depreciation_schedule"
|
|
||||||
asset = "D"
|
|
||||||
as_of = "2026-04-13"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"2026-01" = "170.00"
|
|
||||||
"2026-02" = "310.00"
|
|
||||||
"2026-03" = "310.00"
|
|
||||||
"2026-04" = "130.00"
|
|
||||||
accumulated = "920.00"
|
|
||||||
book_value = "110.00"
|
|
||||||
|
|
||||||
# Through Apr 14, the life's last day: April 140.00, accumulated 930.00,
|
|
||||||
# book value 100.00 (the salvage, never depreciated).
|
|
||||||
[[expect]]
|
|
||||||
query = "depreciation_schedule"
|
|
||||||
asset = "D"
|
|
||||||
as_of = "2026-04-14"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"2026-04" = "140.00"
|
|
||||||
accumulated = "930.00"
|
|
||||||
book_value = "100.00"
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
id = "depreciation-reversal-before-its-run-is-refused"
|
|
||||||
title = "A depreciation run can't be reversed on a day before its own"
|
|
||||||
rule = "A correction comes after what it corrects: a depreciation run, or a reversal of one, can't be reversed on a day before its own. A run of March reversed on Jan 31 would leave January and February with negative depreciation."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = [
|
|
||||||
"Pacioli 1494, ch. 31",
|
|
||||||
"wanderbooks docs/spec/rules.md B-15 rule 6, B-9: a correction comes after what it corrects (CONVENTION)",
|
|
||||||
]
|
|
||||||
tags = ["depreciation", "reversal", "refusal", "p3-defect-22"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "7100"
|
|
||||||
name = "Gain or Loss on Disposal"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
# Asset X: 12,000.00, no salvage, 12 months from 2026-01-01 (1,000.00 a
|
|
||||||
# month; December the final month).
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "X"
|
|
||||||
description = "Asset X"
|
|
||||||
cost = "12000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Bought asset X"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-01" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-02" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-03" }
|
|
||||||
|
|
||||||
# March's run (dated Mar 31) reversed on Feb 10: refused, naming its date.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-10"
|
|
||||||
description = "Reverse event 5"
|
|
||||||
reverses = 5
|
|
||||||
refused = "entry_irreversible"
|
|
||||||
|
|
||||||
# On its own day it is taken.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-31"
|
|
||||||
description = "Reverse event 5"
|
|
||||||
reverses = 5
|
|
||||||
|
|
||||||
# Hand check (worked without the code): January and February carry
|
|
||||||
# 2,000.00 at February's end, never 1,000.00 (with March's 1,000.00 taken
|
|
||||||
# away on Feb 10); March's end carries 2,000.00 (its run and its reversal
|
|
||||||
# both dated Mar 31).
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-02-28"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "-2000.00"
|
|
||||||
"6200" = "2000.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-03-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "-2000.00"
|
|
||||||
"6200" = "2000.00"
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
id = "depreciation-run-reversed-after-its-month"
|
|
||||||
title = "A run reversed after its month ended is caught up in the reversal's month, never posted twice"
|
|
||||||
rule = "What the books carry is read entry by entry, each in its own month: a run reversed after its month ended still carried that month at its end, so running the month again posts nothing, and the run of the reversal's month catches the month up."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = ["ASC 360-10-35-4", "DECISION: plan/served.md P3, Settled in design (2026-10-20)", "docs/spec/rules.md B-9"]
|
|
||||||
tags = ["depreciation", "catch-up", "reversal", "p3"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# Asset X: 12,000.00, no salvage, 12 months from 2026-01-01 (1,000.00 a
|
|
||||||
# month; December the final month).
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "X"
|
|
||||||
description = "Asset X"
|
|
||||||
cost = "12000.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 12
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Bought asset X"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "12000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "12000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-01" }
|
|
||||||
|
|
||||||
# January's run reversed on 2026-02-02, after January ended.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-02"
|
|
||||||
description = "Reverse event 3"
|
|
||||||
reverses = 3
|
|
||||||
|
|
||||||
# January run again: nothing is due (Jan 31 carried January).
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-01" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "X", period = "2026-02" }
|
|
||||||
|
|
||||||
# Hand check (worked without the code): January's run (1,000.00, dated Jan
|
|
||||||
# 31) stood on Jan 31, so the books carried the schedule there, 1,000.00;
|
|
||||||
# running January again posts 0.00 (were it to post, Jan 31 would carry
|
|
||||||
# 2,000.00). The reversal took 1,000.00 away on Feb 2, so February's run
|
|
||||||
# posts the schedule through Feb 28, 2,000.00, less the 0.00 then carried:
|
|
||||||
# 2,000.00, of which 1,000.00 is January caught up.
|
|
||||||
[[expect]]
|
|
||||||
query = "run"
|
|
||||||
event = 5
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
amount = "0.00"
|
|
||||||
caught_up = "0.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "run"
|
|
||||||
event = 6
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
amount = "2000.00"
|
|
||||||
caught_up = "1000.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-01-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "-1000.00"
|
|
||||||
"6200" = "1000.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-02-28"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1510" = "-2000.00"
|
|
||||||
"6200" = "2000.00"
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
id = "entry-a-line-amount-is-positive"
|
|
||||||
title = "A negative or zero line amount is refused; the side gives the direction"
|
|
||||||
rule = "A line's amount MUST be greater than zero, and its side, not its sign, MUST give the direction money moves; an entry with a negative or zero line MUST be refused."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = [
|
|
||||||
"Pacioli, Summa (1494), ch. 36: debtors at the left, creditors at the right (docs/spec/sources/pacioli-1494.md)",
|
|
||||||
"CONVENTION, the owner, 2026-10-13 (docs/spec/rules.md B-2): no authority forbids a negative amount outright; one reading of every line is chosen",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "entry"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
tax_category = "gross_receipts"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Rent"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "rent_property"
|
|
||||||
|
|
||||||
# A credit of minus 100.00 would be a debit of 100.00 written the other
|
|
||||||
# way; the books keep one way to write it.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-05"
|
|
||||||
description = "A negative credit"
|
|
||||||
refused = "negative_amount"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "-100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-06"
|
|
||||||
description = "A zero line"
|
|
||||||
refused = "zero_amount"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# The same 100.00 written with the sides doing the work is accepted.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-07"
|
|
||||||
description = "A sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "100.00"
|
|
||||||
"3000" = "0.00"
|
|
||||||
"4000" = "-100.00"
|
|
||||||
"6000" = "0.00"
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
id = "entry-an-unbalanced-entry-is-refused"
|
|
||||||
title = "An entry whose debits and credits differ is refused and posts nothing"
|
|
||||||
rule = "An entry MUST have debits equal to credits, exactly, and an entry that does not MUST be refused whole, leaving the books as they were."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"Pacioli, Summa (1494), ch. 36: \"All entries made in the ledger have to be double entries\" (docs/spec/sources/pacioli-1494.md)",
|
|
||||||
"IRS Publication 583: \"the total debits must equal the total credits\" (docs/spec/sources/irs-records.md)",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "entry", "double-entry"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
tax_category = "gross_receipts"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Rent"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "rent_property"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-02"
|
|
||||||
description = "Opening contribution"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Debits 100.00, credits 90.00: ten dollars short on the credit side.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-05"
|
|
||||||
description = "A sale that does not balance"
|
|
||||||
refused = "unbalanced"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "90.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# One cent over is just as wrong: there is no epsilon.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-06"
|
|
||||||
description = "A sale a cent over"
|
|
||||||
refused = "unbalanced"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "100.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: only event 1 posted, so cash holds 1,000.00 (a debit), capital
|
|
||||||
# 1,000.00 (a credit, shown negative), and sales nothing.
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "1000.00"
|
|
||||||
"3000" = "-1000.00"
|
|
||||||
"4000" = "0.00"
|
|
||||||
"6000" = "0.00"
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
id = "entry-cents-add-exactly"
|
|
||||||
title = "Amounts are kept exactly: ten cents and twenty cents make thirty"
|
|
||||||
rule = "The books MUST keep every amount exactly and add exactly, so that no sum of cents differs from its true total by any amount, however small; rounding MUST NOT occur inside the books."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"IRS Form 1065 instructions: \"include cents when adding the amounts and round off only the total\" (docs/spec/sources/irs-partnerships.md)",
|
|
||||||
"Pacioli, Summa (1494), ch. 36: the credits equal the debits, which no float sum guarantees (docs/spec/sources/pacioli-1494.md)",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "entry", "exact"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2000"
|
|
||||||
name = "Loan"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
tax_category = "gross_receipts"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Rent"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "rent_property"
|
|
||||||
|
|
||||||
# In binary floating point 0.1 + 0.2 is 0.30000000000000004. Two debits of
|
|
||||||
# 0.10 and 0.20 against one credit of 0.30 balance exactly.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-05"
|
|
||||||
description = "Three small sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.10" },
|
|
||||||
{ account = "1000", side = "debit", amount = "0.20" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.30" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-06"
|
|
||||||
description = "A sale of a trillion and a cent"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1000000000000.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "1000000000000.01" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "1000000000000.31"
|
|
||||||
"4000" = "1000000000000.31"
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
# The journal file QuickBooks Desktop imports (IIF), as of a day. A format,
|
|
||||||
# not accounting: P13's appendix of interchange formats.
|
|
||||||
#
|
|
||||||
# wanderbooks: P3 defects 7 and 21, decision D-36 and the P3 review (the
|
|
||||||
# file numbered transactions by their place in it, held entries dated after
|
|
||||||
# the package's as_of, wrote '?' for every character past ASCII, and knew
|
|
||||||
# nothing of QuickBooks' length limits, its double quote, or its rule that
|
|
||||||
# a subaccount has its parent's type, and could declare a subaccount
|
|
||||||
# before its parent; and the cutover review: it left CLASS empty though
|
|
||||||
# lines carry projects); rules.md B-34; regression tests
|
|
||||||
# `iif_docnum_is_the_entry_id`,
|
|
||||||
# `iif_writes_plain_ascii_and_lists_every_change`,
|
|
||||||
# `iif_fits_quickbooks_desktop_limits`,
|
|
||||||
# `iif_a_subaccount_of_another_type_stands_at_the_top_level`,
|
|
||||||
# `iif_declares_each_parent_before_its_subaccounts`,
|
|
||||||
# `iif_writes_each_lines_class_and_declares_the_classes_first`,
|
|
||||||
# `iif_class_names_fit_quickbooks_and_every_change_is_listed` (ledger-import
|
|
||||||
# tests/spec_import.rs) and `as_of_package_iif_stops_at_as_of`
|
|
||||||
# (ledger-package tests/package.rs).
|
|
||||||
#
|
|
||||||
# Setup beyond the accounts: `cash_accounts` are the bank accounts (BANK);
|
|
||||||
# an account's QuickBooks type otherwise follows its type (asset OCASSET,
|
|
||||||
# liability OCLIAB, equity EQUITY, revenue INC, expense EXP); an account's
|
|
||||||
# `parent` is its parent's code; project N in `setup.projects` is class N
|
|
||||||
# (the books number classes from 1). Event N is entry N (bookspec/README.md).
|
|
||||||
# Expected values: iif_lines gives the file's lines in order, each a
|
|
||||||
# string of fields joined by tabs ("\t"), every line ending CR LF in the
|
|
||||||
# file; iif_changes gives each text written differently from the books,
|
|
||||||
# in file order (the accounts as declared: by code, each after its
|
|
||||||
# parent; then the classes; then the entries), as [what, books' text,
|
|
||||||
# file's text], where what is "name <code>", "number" (left out of ACCNUM
|
|
||||||
# when the file's text is empty), "top level <code>" (written apart from
|
|
||||||
# its parent, the books' text: the parent's code), "class <id>" (a
|
|
||||||
# project's name) or "memo <entry id>".
|
|
||||||
|
|
||||||
id = "iif-quickbooks-journal-file"
|
|
||||||
appendix = "formats"
|
|
||||||
title = "A QuickBooks Desktop journal file as of a day"
|
|
||||||
rule = "An IIF journal file as of a day D MUST declare every account first (NAME, ACCNTTYPE, ACCNUM), then hold one GENERAL JOURNAL transaction per posted entry dated on or before D, period closes excepted, in the order posted: a TRNS line and SPL lines, debits positive and credits negative, dated M/D/YYYY, each numbered (DOCNUM) with the entry's id; then ENDTRNS. It MUST be ASCII text, every line ending CR LF, no field holding a tab, a line break or a double quote; an account name level MUST be at most 31 characters and no two NAMEs alike, an account number at most 7 characters and no two alike (else left out), a memo at most 4,095 characters; each parent MUST be declared before its subaccounts, and a subaccount whose QuickBooks type differs from its parent's MUST be written at the top level. Every project MUST be declared as a class (NAME, one level of at most 31 characters, no two alike) after the accounts and before the transactions, and each line's project MUST be its CLASS. Each text written differently from the books MUST be reported."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Intuit, IIF Overview: import kit, sample files, and headers: IIF files are ASCII text, tab-separated; an account not already in the company file is created as a new bank account (docs/spec/sources/intuit-iif.md)",
|
|
||||||
"Intuit, IIF Import Kit, IIF Header Help !TRNS, !SPL, !ACCNT: TRNS and SPL lines then ENDTRNS; \"Debit amounts are always positive, credit amounts are always negative\"; DOCNUM is the transaction's number; a subaccount's NAME its parents' names first, joined by colons (docs/spec/sources/intuit-iif.md)",
|
|
||||||
"Intuit, IIF Import Kit, 2007 General Journal Entry example file: GENERAL JOURNAL, M/D/YYYY, every line ending CR LF (docs/spec/sources/intuit-iif.md)",
|
|
||||||
"Intuit, Character limitations for fields in QuickBooks (US, updated 8/3/2026): Account Name 31, Account Numbers 7, Memo 4,095 (docs/spec/sources/intuit-iif.md)",
|
|
||||||
"Intuit, IIF Import Kit, IIF Header Help !CLASS: \"Only the NAME column is required\"; a subclass's NAME its parents' names first, colons between; the kit's list declarations example declares !CLASS before any transaction (docs/spec/sources/intuit-iif.md)",
|
|
||||||
"Conductor, QuickBooks Desktop API reference, Create a class: a class's name, \"Maximum length: 31 characters\", case-insensitive (docs/spec/sources/intuit-iif.md; not Intuit: Intuit states no class limit)",
|
|
||||||
"Intuit, Add, edit, or delete accounts in QuickBooks Desktop (updated 8/4/2026): \"The parent and subaccount must be the same account type.\" (docs/spec/sources/intuit-iif.md)",
|
|
||||||
"Reckon, IIF import changes a double quote to a single one by design; J. Clark (2003): QuickBooks uses double quotes as text delimiters (docs/spec/sources/intuit-iif.md; not Intuit)",
|
|
||||||
"DECISION D-36 (owner, 2026-10-20): plain ASCII, what isn't ASCII transliterated and listed; CR LF; DOCNUM the entry's id; closes left out (wanderbooks plan/served.md; rules.md B-34)",
|
|
||||||
]
|
|
||||||
tags = ["iif", "quickbooks", "as-of", "format", "transliteration", "projects"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "cash"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1090"
|
|
||||||
name = "Bank accounts"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
# A bank account (BANK) under a header that is no bank (OCASSET): written
|
|
||||||
# at the top level.
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
parent = "1090"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "12345678"
|
|
||||||
name = "Suspense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Owner capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Café meals"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# Coded before its parent: declared after it.
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "5900"
|
|
||||||
name = "Snacks"
|
|
||||||
type = "expense"
|
|
||||||
parent = "6000"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6100"
|
|
||||||
name = "Professional services and consulting fees"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# Class 1: a colon and an em dash, written "-" and "--".
|
|
||||||
[[setup.projects]]
|
|
||||||
name = "Café: lunch — rebrand"
|
|
||||||
|
|
||||||
# Class 2: declared though no line carries it, as every account is.
|
|
||||||
[[setup.projects]]
|
|
||||||
name = "Glaze tests"
|
|
||||||
|
|
||||||
# Entry 1.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-02"
|
|
||||||
description = "Owner contribution"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "5000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "5000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 2. An em dash and double quotes; the meal is project 1's.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-15"
|
|
||||||
description = "Lunch — \"team\""
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "42.50", project = "Café: lunch — rebrand" },
|
|
||||||
{ account = "1000", side = "credit", amount = "42.50" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 3. An account named past 31 characters.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-20"
|
|
||||||
description = "Consulting"
|
|
||||||
lines = [
|
|
||||||
{ account = "6100", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "300.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 4, dated the day itself: in.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-01"
|
|
||||||
description = "Sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "1000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 5, the day after: out.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-02"
|
|
||||||
description = "Second sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "250.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "250.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "iif_lines"
|
|
||||||
as_of = "2026-02-01"
|
|
||||||
values = [
|
|
||||||
"!ACCNT\tNAME\tACCNTTYPE\tACCNUM",
|
|
||||||
"ACCNT\tChecking\tBANK\t1000",
|
|
||||||
"ACCNT\tBank accounts\tOCASSET\t1090",
|
|
||||||
"ACCNT\tSuspense\tEXP\t",
|
|
||||||
"ACCNT\tOwner capital\tEQUITY\t3000",
|
|
||||||
"ACCNT\tSales\tINC\t4000",
|
|
||||||
"ACCNT\tCafe meals\tEXP\t6000",
|
|
||||||
"ACCNT\tCafe meals:Snacks\tEXP\t5900",
|
|
||||||
"ACCNT\tProfessional services and consu\tEXP\t6100",
|
|
||||||
"!CLASS\tNAME",
|
|
||||||
"CLASS\tCafe- lunch -- rebrand",
|
|
||||||
"CLASS\tGlaze tests",
|
|
||||||
"!TRNS\tTRNSID\tTRNSTYPE\tDATE\tACCNT\tCLASS\tAMOUNT\tDOCNUM\tMEMO",
|
|
||||||
"!SPL\tSPLID\tTRNSTYPE\tDATE\tACCNT\tCLASS\tAMOUNT\tDOCNUM\tMEMO",
|
|
||||||
"!ENDTRNS",
|
|
||||||
"TRNS\t\tGENERAL JOURNAL\t1/2/2026\tChecking\t\t5000.00\t1\tOwner contribution",
|
|
||||||
"SPL\t\tGENERAL JOURNAL\t1/2/2026\tOwner capital\t\t-5000.00\t1\tOwner contribution",
|
|
||||||
"ENDTRNS",
|
|
||||||
"TRNS\t\tGENERAL JOURNAL\t1/15/2026\tCafe meals\tCafe- lunch -- rebrand\t42.50\t2\tLunch -- 'team'",
|
|
||||||
"SPL\t\tGENERAL JOURNAL\t1/15/2026\tChecking\t\t-42.50\t2\tLunch -- 'team'",
|
|
||||||
"ENDTRNS",
|
|
||||||
"TRNS\t\tGENERAL JOURNAL\t1/20/2026\tProfessional services and consu\t\t300.00\t3\tConsulting",
|
|
||||||
"SPL\t\tGENERAL JOURNAL\t1/20/2026\tChecking\t\t-300.00\t3\tConsulting",
|
|
||||||
"ENDTRNS",
|
|
||||||
"TRNS\t\tGENERAL JOURNAL\t2/1/2026\tChecking\t\t1000.00\t4\tSale",
|
|
||||||
"SPL\t\tGENERAL JOURNAL\t2/1/2026\tSales\t\t-1000.00\t4\tSale",
|
|
||||||
"ENDTRNS",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "iif_changes"
|
|
||||||
as_of = "2026-02-01"
|
|
||||||
values = [
|
|
||||||
["top level 1000", "1090", ""],
|
|
||||||
["number", "12345678", ""],
|
|
||||||
["name 6000", "Café meals", "Cafe meals"],
|
|
||||||
["name 6100", "Professional services and consulting fees", "Professional services and consu"],
|
|
||||||
["class 1", "Café: lunch — rebrand", "Cafe- lunch -- rebrand"],
|
|
||||||
["memo 2", "Lunch — \"team\"", "Lunch -- 'team'"],
|
|
||||||
]
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
events = []
|
|
||||||
|
|
||||||
id = "ledger-a-child-account-has-its-parents-type"
|
|
||||||
title = "An account under a parent has the parent's type"
|
|
||||||
rule = "An account MUST be one of the five elements (asset, liability, equity, revenue, expense), and an account under a parent MUST have its parent's type, so a rollup never mixes elements."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"FASB Concepts Statement 8, Ch. 4, E16, E37, E61, E80, E81: the five elements (docs/spec/sources/fasb-asc.md)",
|
|
||||||
"CHOICE, the owner, 2026-10-13 (docs/spec/rules.md B-4): a child takes its parent's type",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "ledger", "chart"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Operating"
|
|
||||||
type = "expense"
|
|
||||||
header = true
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6150"
|
|
||||||
name = "Software"
|
|
||||||
type = "expense"
|
|
||||||
parent = "6000"
|
|
||||||
tax_category = "office_expense"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
# A revenue account under an expense header would mix two elements.
|
|
||||||
[[expect]]
|
|
||||||
query = "add_account"
|
|
||||||
account = { code = "6200", name = "Refunds", type = "revenue", parent = "6000", tax_category = "gross_receipts" }
|
|
||||||
refused = "parent_type_mismatch"
|
|
||||||
|
|
||||||
# An expense under the expense header is a child of the same element.
|
|
||||||
[[expect]]
|
|
||||||
query = "add_account"
|
|
||||||
account = { code = "6200", name = "Hosting", type = "expense", parent = "6000", tax_category = "office_expense" }
|
|
||||||
accepted = true
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"6000" = "0.00"
|
|
||||||
"6150" = "0.00"
|
|
||||||
"1000" = "0.00"
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
id = "ledger-a-fraction-of-a-cent-is-refused"
|
|
||||||
title = "A line finer than the currency's smallest unit is refused"
|
|
||||||
rule = "A ledger kept in one currency MUST hold amounts in whole units of that currency's smallest unit, so a line finer than it MUST be refused, never rounded silently."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Pacioli, Summa (1494), ch. 36: \"the money that you reckon by at the beginning should be the same all through the Ledger\" (docs/spec/sources/pacioli-1494.md)",
|
|
||||||
"ISO 4217: the US dollar has two minor-unit digits (docs/spec/sources/iso-4217.md)",
|
|
||||||
"CHOICE, the owner, 2026-10-13 (docs/spec/rules.md B-3): books in the smallest unit, so cash can match a bank statement",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "ledger", "currency"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
tax_category = "gross_receipts"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Rent"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "rent_property"
|
|
||||||
|
|
||||||
# 10.005 dollars is no amount anyone can pay.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-05"
|
|
||||||
description = "Half a cent too fine"
|
|
||||||
refused = "finer_than_minor_unit"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "10.005" },
|
|
||||||
{ account = "4000", side = "credit", amount = "10.005" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-06"
|
|
||||||
description = "The same sale in cents"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "10.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "10.01" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "10.01"
|
|
||||||
"3000" = "0.00"
|
|
||||||
"4000" = "-10.01"
|
|
||||||
"6000" = "0.00"
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
id = "ledger-a-mistake-is-reversed-never-erased"
|
|
||||||
title = "A reversal cancels an entry and leaves the original standing, and an entry is reversed only once"
|
|
||||||
rule = "A posted entry MUST NOT be changed or erased; a mistake MUST be corrected by an entry opposite to it, which MUST leave the original's lines as written, and an entry MUST be reversed at most once."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"Pacioli, Summa (1494), ch. 31: \"make another entry opposite this one\"; ch. 36: \"You should not write backward, but always forward\" (docs/spec/sources/pacioli-1494.md)",
|
|
||||||
"Rev. Proc. 98-25 section 5.01: the audit trail (docs/spec/sources/irs-records.md)",
|
|
||||||
"CHOICE, the owner, 2026-10-13 (docs/spec/rules.md B-10): a correction corrects once",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "ledger", "reversal"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2000"
|
|
||||||
name = "Loan"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
tax_category = "gross_receipts"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Rent"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "rent_property"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "Rent paid twice by mistake"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "800.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "800.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-12"
|
|
||||||
description = "Reverse entry 1"
|
|
||||||
reverses = 1
|
|
||||||
|
|
||||||
# A second reversal of the same entry would take the 800.00 out twice.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-13"
|
|
||||||
description = "Reverse entry 1 again"
|
|
||||||
refused = "already_reversed"
|
|
||||||
reverses = 1
|
|
||||||
|
|
||||||
# The original still reads as it was written: posted is forever.
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 1
|
|
||||||
[expect.values]
|
|
||||||
"6000.debit" = "800.00"
|
|
||||||
"1000.credit" = "800.00"
|
|
||||||
|
|
||||||
# The reversal is the original with its sides flipped.
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 2
|
|
||||||
[expect.values]
|
|
||||||
"6000.credit" = "800.00"
|
|
||||||
"1000.debit" = "800.00"
|
|
||||||
|
|
||||||
# Net of the two, nothing: as if nothing had been written (ch. 31).
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "0.00"
|
|
||||||
"2000" = "0.00"
|
|
||||||
"3000" = "0.00"
|
|
||||||
"4000" = "0.00"
|
|
||||||
"6000" = "0.00"
|
|
||||||
|
|
||||||
# Before the reversal's date the original stands alone.
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-01-11"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "-800.00"
|
|
||||||
"2000" = "0.00"
|
|
||||||
"3000" = "0.00"
|
|
||||||
"4000" = "0.00"
|
|
||||||
"6000" = "800.00"
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
id = "members-close.odd-cent-across-closes"
|
|
||||||
title = "An odd cent split cumulatively across closes, a close to retained earnings between"
|
|
||||||
rule = "Each close to the members MUST give each member their largest-remainder split of everything closed to the same members and shares so far, to the minor unit, less what earlier closes gave them; a close to retained earnings allocates nothing to the members and MUST NOT restart the count."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"26 U.S.C. §704(a)",
|
|
||||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)",
|
|
||||||
"DECISION (owner, 2026-10-13: largest remainder; D-10, 2026-10-16: counted from the first close) (wanderbooks rules.md B-13)",
|
|
||||||
]
|
|
||||||
tags = ["partnership", "close", "rounding", "book", "us"]
|
|
||||||
|
|
||||||
# The regression this example comes from (wanderbooks P3 defect 17, audit
|
|
||||||
# F52, anonymized): a net loss of 287.53 at 50/50, closed in January and
|
|
||||||
# again in March, with February closed to retained earnings. Restarting the
|
|
||||||
# count at March gave A the tied cent twice (A −287.54, B −287.52).
|
|
||||||
#
|
|
||||||
# By hand, from the rule:
|
|
||||||
# January: −287.53 × 0.5 = −143.765 each; rounded toward zero, −143.76
|
|
||||||
# each, one cent short; the remainders tie (0.005), so the cent goes to
|
|
||||||
# the member listed first: A −143.77, B −143.76.
|
|
||||||
# February: 10.00 to retained earnings; the members' count skips it.
|
|
||||||
# March: the count's running total is −575.06, −287.53 each exactly;
|
|
||||||
# less January's −143.77 / −143.76, March gives A −143.76, B −143.77.
|
|
||||||
# Each member ends at −287.53, their exact share.
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "largest-remainder" # odd cents to the largest remainders, ties
|
|
||||||
# to the member listed first, cumulatively
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital, A"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital, B"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3999"
|
|
||||||
name = "Retained earnings"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Fees"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "A"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "B"
|
|
||||||
capital_account = "3001"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[events]] # event 1
|
|
||||||
date = "2026-01-15"
|
|
||||||
description = "January's fees"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "287.53" },
|
|
||||||
{ account = "1000", side = "credit", amount = "287.53" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 2: the closing entry
|
|
||||||
date = "2026-01-31"
|
|
||||||
description = "Close January to the members"
|
|
||||||
close = { start = "2026-01-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 3
|
|
||||||
date = "2026-02-15"
|
|
||||||
description = "February's fees"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "10.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "10.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 4
|
|
||||||
date = "2026-02-28"
|
|
||||||
description = "Close February to retained earnings"
|
|
||||||
close = { start = "2026-02-01", to = "retained_earnings" }
|
|
||||||
|
|
||||||
[[events]] # event 5
|
|
||||||
date = "2026-03-15"
|
|
||||||
description = "March's fees"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "287.53" },
|
|
||||||
{ account = "1000", side = "credit", amount = "287.53" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 6
|
|
||||||
date = "2026-03-31"
|
|
||||||
description = "Close March to the members"
|
|
||||||
close = { start = "2026-03-01", to = "members" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 2
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"6000.credit" = "287.53"
|
|
||||||
"3000.debit" = "143.77"
|
|
||||||
"3001.debit" = "143.76"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 6
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"6000.credit" = "287.53"
|
|
||||||
"3000.debit" = "143.76"
|
|
||||||
"3001.debit" = "143.77"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-03-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"3000" = "-287.53"
|
|
||||||
"3001" = "-287.53"
|
|
||||||
"3999" = "-10.00"
|
|
||||||
"1000" = "-585.06"
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
id = "members-close.statement-before-and-after-a-close"
|
|
||||||
title = "A member's capital on a day is the same before and after its month closes"
|
|
||||||
rule = "The statement of members' equity MUST split earnings not yet closed the way the next close to the members will (each member's split of everything closed plus those earnings, less their split of what was closed), so a member's capital as of a day MUST NOT change when a period ending on or before it is closed."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)",
|
|
||||||
"DECISION D-38 (owner, 2026-10-20): the statement splits unclosed earnings the same running way as the closes (wanderbooks rules.md B-13)",
|
|
||||||
]
|
|
||||||
tags = ["partnership", "close", "rounding", "statement", "book", "us"]
|
|
||||||
|
|
||||||
# Two members at 50/50; January's cent is closed to them, February's is
|
|
||||||
# earned but not yet closed (event 3), then closed (event 4).
|
|
||||||
#
|
|
||||||
# By hand, from the rule:
|
|
||||||
# January's close: 0.01 is 0.005 each; the tied cent to A (listed first):
|
|
||||||
# A 0.01, B 0.00.
|
|
||||||
# As of Feb 28, February not yet closed: closed 0.01, unclosed 0.01. The
|
|
||||||
# split of everything, 0.02, is 0.01 / 0.01; of the closed part, 0.01 /
|
|
||||||
# 0.00; so the unclosed cent is B's: A 0.01 + 0.00, B 0.00 + 0.01.
|
|
||||||
# (Splitting February's cent on its own would give it to A: A 0.02,
|
|
||||||
# B 0.00, and the close would then move it to B.)
|
|
||||||
# February's close gives the running 0.02, 0.01 each, less 0.01 / 0.00:
|
|
||||||
# B 0.01. As of Feb 28: A 0.01, B 0.01, as before the close.
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "largest-remainder"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital, A"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital, B"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "A"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "B"
|
|
||||||
capital_account = "3001"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[events]] # event 1
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "January's sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.01" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 2
|
|
||||||
date = "2026-01-31"
|
|
||||||
description = "Close January to the members"
|
|
||||||
close = { start = "2026-01-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 3
|
|
||||||
date = "2026-02-10"
|
|
||||||
description = "February's sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.01" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 4
|
|
||||||
date = "2026-02-28"
|
|
||||||
description = "Close February to the members"
|
|
||||||
close = { start = "2026-02-01", to = "members" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "members_equity"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-02-28"
|
|
||||||
after = 3 # the books before February's close
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"A.ending" = "0.01"
|
|
||||||
"B.ending" = "0.01"
|
|
||||||
"A.net_income" = "0.01"
|
|
||||||
"B.net_income" = "0.01"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "members_equity"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-02-28" # after every event: February closed
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"A.ending" = "0.01"
|
|
||||||
"B.ending" = "0.01"
|
|
||||||
"A.net_income" = "0.01"
|
|
||||||
"B.net_income" = "0.01"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 4
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"4000.debit" = "0.01"
|
|
||||||
"3001.credit" = "0.01"
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
id = "members-close.thirds-over-twelve-months"
|
|
||||||
title = "Three members at a third each, twelve monthly closes"
|
|
||||||
rule = "Each monthly close to the members MUST give each member their largest-remainder split of everything closed so far, to the minor unit, less what earlier closes gave them, so after every close each member's total is less than one minor unit from their exact share."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"26 U.S.C. §704(a)",
|
|
||||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)",
|
|
||||||
"DECISION (owner, 2026-10-13: largest remainder; D-10, 2026-10-16: counted from the first close) (wanderbooks rules.md B-13)",
|
|
||||||
]
|
|
||||||
tags = ["partnership", "close", "rounding", "book", "us"]
|
|
||||||
|
|
||||||
# A third each can't be written as decimal shares summing to one, so the
|
|
||||||
# members hold 33.33%, 33.33% and 33.34%. Twelve months, each closed to
|
|
||||||
# them: 100.00, 0.01, 0.02, 33.33, -0.05, 0.10, 1.00, 0.07, 0.01, 12.34,
|
|
||||||
# -5.00, 0.02 (141.85 in all).
|
|
||||||
#
|
|
||||||
# By hand, from the rule (the running total, its exact shares, the floors
|
|
||||||
# to the cent, then the left-over cents to the largest remainders):
|
|
||||||
# January, 100.00: 33.33 / 33.33 / 33.34 exactly.
|
|
||||||
# February, running 100.01: 33.333333 / 33.333333 / 33.343334; floors
|
|
||||||
# 33.33 / 33.33 / 33.34 leave a cent for the largest remainder, C's
|
|
||||||
# .003334: 33.33 / 33.33 / 33.35, so February gives C 0.01.
|
|
||||||
# March, running 100.03: 33.339999 / 33.339999 / 33.350002; floors 33.33
|
|
||||||
# / 33.33 / 33.35 leave two cents for A's and B's .009999: 33.34 /
|
|
||||||
# 33.34 / 33.35, so March gives A 0.01 and B 0.01.
|
|
||||||
# November, running 141.83: 47.271939 / 47.271939 / 47.286122; floors
|
|
||||||
# 47.27 / 47.27 / 47.28 leave a cent for C's .006122: 47.27 / 47.27 /
|
|
||||||
# 47.29.
|
|
||||||
# December, running 141.85: 47.278605 / 47.278605 / 47.29279; floors
|
|
||||||
# 47.27 / 47.27 / 47.29 leave two cents for A's and B's .008605: 47.28
|
|
||||||
# / 47.28 / 47.29, so December gives A 0.01 and B 0.01.
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "largest-remainder"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital, A"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital, B"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3002"
|
|
||||||
name = "Capital, C"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Fees"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "A"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "0.3333"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "B"
|
|
||||||
capital_account = "3001"
|
|
||||||
share = "0.3333"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "C"
|
|
||||||
capital_account = "3002"
|
|
||||||
share = "0.3334"
|
|
||||||
|
|
||||||
[[events]] # event 1
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "January's sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 2
|
|
||||||
date = "2026-01-31"
|
|
||||||
description = "Close January to the members"
|
|
||||||
close = { start = "2026-01-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 3
|
|
||||||
date = "2026-02-10"
|
|
||||||
description = "February's sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.01" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 4
|
|
||||||
date = "2026-02-28"
|
|
||||||
description = "Close February to the members"
|
|
||||||
close = { start = "2026-02-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 5
|
|
||||||
date = "2026-03-10"
|
|
||||||
description = "March's sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.02" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.02" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 6
|
|
||||||
date = "2026-03-31"
|
|
||||||
description = "Close March to the members"
|
|
||||||
close = { start = "2026-03-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 7
|
|
||||||
date = "2026-04-10"
|
|
||||||
description = "April's sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "33.33" },
|
|
||||||
{ account = "4000", side = "credit", amount = "33.33" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 8
|
|
||||||
date = "2026-04-30"
|
|
||||||
description = "Close April to the members"
|
|
||||||
close = { start = "2026-04-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 9
|
|
||||||
date = "2026-05-10"
|
|
||||||
description = "May's fees"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "0.05" },
|
|
||||||
{ account = "1000", side = "credit", amount = "0.05" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 10
|
|
||||||
date = "2026-05-31"
|
|
||||||
description = "Close May to the members"
|
|
||||||
close = { start = "2026-05-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 11
|
|
||||||
date = "2026-06-10"
|
|
||||||
description = "June's sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.10" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.10" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 12
|
|
||||||
date = "2026-06-30"
|
|
||||||
description = "Close June to the members"
|
|
||||||
close = { start = "2026-06-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 13
|
|
||||||
date = "2026-07-10"
|
|
||||||
description = "July's sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "1.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 14
|
|
||||||
date = "2026-07-31"
|
|
||||||
description = "Close July to the members"
|
|
||||||
close = { start = "2026-07-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 15
|
|
||||||
date = "2026-08-10"
|
|
||||||
description = "August's sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.07" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.07" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 16
|
|
||||||
date = "2026-08-31"
|
|
||||||
description = "Close August to the members"
|
|
||||||
close = { start = "2026-08-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 17
|
|
||||||
date = "2026-09-10"
|
|
||||||
description = "September's sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.01" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 18
|
|
||||||
date = "2026-09-30"
|
|
||||||
description = "Close September to the members"
|
|
||||||
close = { start = "2026-09-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 19
|
|
||||||
date = "2026-10-10"
|
|
||||||
description = "October's sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "12.34" },
|
|
||||||
{ account = "4000", side = "credit", amount = "12.34" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 20
|
|
||||||
date = "2026-10-31"
|
|
||||||
description = "Close October to the members"
|
|
||||||
close = { start = "2026-10-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 21
|
|
||||||
date = "2026-11-10"
|
|
||||||
description = "November's fees"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "5.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "5.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 22
|
|
||||||
date = "2026-11-30"
|
|
||||||
description = "Close November to the members"
|
|
||||||
close = { start = "2026-11-01", to = "members" }
|
|
||||||
|
|
||||||
[[events]] # event 23
|
|
||||||
date = "2026-12-10"
|
|
||||||
description = "December's sales"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.02" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.02" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 24
|
|
||||||
date = "2026-12-31"
|
|
||||||
description = "Close December to the members"
|
|
||||||
close = { start = "2026-12-01", to = "members" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 2
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"4000.debit" = "100.00"
|
|
||||||
"3000.credit" = "33.33"
|
|
||||||
"3001.credit" = "33.33"
|
|
||||||
"3002.credit" = "33.34"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 4
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"4000.debit" = "0.01"
|
|
||||||
"3002.credit" = "0.01"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 6
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"4000.debit" = "0.02"
|
|
||||||
"3000.credit" = "0.01"
|
|
||||||
"3001.credit" = "0.01"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 24
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"4000.debit" = "0.02"
|
|
||||||
"3000.credit" = "0.01"
|
|
||||||
"3001.credit" = "0.01"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-11-30"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"3000" = "47.27"
|
|
||||||
"3001" = "47.27"
|
|
||||||
"3002" = "47.29"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"3000" = "47.28"
|
|
||||||
"3001" = "47.28"
|
|
||||||
"3002" = "47.29"
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
id = "members-close.true-up-of-a-per-close-split"
|
|
||||||
title = "Closes rounded one at a time, trued up by the first cumulative close"
|
|
||||||
rule = "When earlier closes split each close's net on its own, the first cumulative close that posts an entry MUST give each member their split of everything closed to the members, less what the earlier closes gave them, so it trues up the rounding they left; until it posts, the statement of members' equity MUST show the capital those closes posted, not the pending true-up."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)",
|
|
||||||
"DECISION D-10 (owner, 2026-10-16): members closes counted from the first close, so a later close carries any earlier rounding as a true-up (wanderbooks rules.md B-13)",
|
|
||||||
]
|
|
||||||
tags = ["partnership", "close", "rounding", "statement", "book", "us"]
|
|
||||||
|
|
||||||
# Books migrated from an engine that rounded each close on its own: three
|
|
||||||
# months of one cent at 50/50, each tie to A. April's cent is closed by the
|
|
||||||
# cumulative rule, counting from the first close.
|
|
||||||
#
|
|
||||||
# By hand, from the rule:
|
|
||||||
# January to March, each close on its own: 0.005 each, the tie to A, so
|
|
||||||
# A 0.03, B 0.00 (exact: 0.015 each).
|
|
||||||
# As of Apr 30, April's cent not yet closed: the split of everything,
|
|
||||||
# 0.04, is 0.02 / 0.02; of the closed 0.03, 0.02 / 0.01 (the tie to
|
|
||||||
# A); the unclosed cent is B's (0.02 − 0.01): A 0.03, B 0.01. The
|
|
||||||
# rounding the earlier closes left (A 0.01 ahead) shows only once
|
|
||||||
# posted.
|
|
||||||
# April's close: the running 0.04 is 0.02 / 0.02, less what was given
|
|
||||||
# (0.03 / 0.00): A −0.01, B +0.02. The entry: Dr 4000 0.01, Dr 3000
|
|
||||||
# 0.01, Cr 3001 0.02. As of Apr 30: A 0.02, B 0.02, each exact; as of
|
|
||||||
# Mar 31, still A 0.03, B 0.00.
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "largest-remainder"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital, A"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital, B"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "A"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "B"
|
|
||||||
capital_account = "3001"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[events]] # event 1
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "January's sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.01" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 2
|
|
||||||
date = "2026-01-31"
|
|
||||||
description = "Close January, rounded on its own"
|
|
||||||
close = { start = "2026-01-01", to = "members", rounding = "per-close" }
|
|
||||||
|
|
||||||
[[events]] # event 3
|
|
||||||
date = "2026-02-10"
|
|
||||||
description = "February's sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.01" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 4
|
|
||||||
date = "2026-02-28"
|
|
||||||
description = "Close February, rounded on its own"
|
|
||||||
close = { start = "2026-02-01", to = "members", rounding = "per-close" }
|
|
||||||
|
|
||||||
[[events]] # event 5
|
|
||||||
date = "2026-03-10"
|
|
||||||
description = "March's sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.01" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 6
|
|
||||||
date = "2026-03-31"
|
|
||||||
description = "Close March, rounded on its own"
|
|
||||||
close = { start = "2026-03-01", to = "members", rounding = "per-close" }
|
|
||||||
|
|
||||||
[[events]] # event 7
|
|
||||||
date = "2026-04-10"
|
|
||||||
description = "April's sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.01" },
|
|
||||||
{ account = "4000", side = "credit", amount = "0.01" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]] # event 8
|
|
||||||
date = "2026-04-30"
|
|
||||||
description = "Close April, cumulatively"
|
|
||||||
close = { start = "2026-04-01", to = "members" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-03-31"
|
|
||||||
after = 6
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"3000" = "0.03"
|
|
||||||
"3001" = "0.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "members_equity"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-04-30"
|
|
||||||
after = 7
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"A.ending" = "0.03"
|
|
||||||
"B.ending" = "0.01"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 8
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"4000.debit" = "0.01"
|
|
||||||
"3000.debit" = "0.01"
|
|
||||||
"3001.credit" = "0.02"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "members_equity"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-04-30"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"A.ending" = "0.02"
|
|
||||||
"B.ending" = "0.02"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "members_equity"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-03-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"A.ending" = "0.03"
|
|
||||||
"B.ending" = "0.00"
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
# P3 defect 24 (found in design; wanderbooks plan/served.md P3 "Settled in
|
|
||||||
# design"): party totals treated every liability but payables and payroll
|
|
||||||
# as money, so an accrual through another liability counted when it was
|
|
||||||
# booked and again when it was settled. Regression test: ledger-core
|
|
||||||
# tests/spec_party_totals.rs
|
|
||||||
# defect24_an_accrual_through_another_liability_counts_once. The shape:
|
|
||||||
# bookspec/README.md.
|
|
||||||
|
|
||||||
id = "p3-party-accrual-counts-once"
|
|
||||||
title = "An accrual through a liability that isn't money counts once, when it is settled"
|
|
||||||
rule = """
|
|
||||||
Money is cash, the company's cards, the members' financing and loans.
|
|
||||||
Every other liability (accrued expenses, deferred revenue) is a promise:
|
|
||||||
an entry through it moves nothing with the party until it is settled in
|
|
||||||
money.
|
|
||||||
"""
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Helvering v. Price, 309 U.S. 409, 412-413 (1940): a promise to pay is not payment (wanderbooks docs/spec/sources/helvering-v-price-1940.md)",
|
|
||||||
"DECISION (owner, 2026-10-20): money is cash, cards, the members' financing and loans (wanderbooks rules.md B-35 item 5)",
|
|
||||||
]
|
|
||||||
tags = ["parties", "accruals", "deferred-revenue", "money"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to Members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2200"
|
|
||||||
name = "Accrued Expenses"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2400"
|
|
||||||
name = "Deferred Revenue"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Revenue"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6050"
|
|
||||||
name = "Domain & Hosting"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Northwind Filings"
|
|
||||||
roles = ["vendor"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Riley Chen"
|
|
||||||
roles = ["customer"]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-31"
|
|
||||||
description = "Hosting accrued"
|
|
||||||
parties = [{ party = "Northwind Filings" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "6050", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "2200", side = "credit", amount = "300.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-04-15"
|
|
||||||
description = "Hosting paid"
|
|
||||||
parties = [{ party = "Northwind Filings" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2200", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "300.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-02"
|
|
||||||
description = "A year paid ahead"
|
|
||||||
parties = [{ party = "Riley Chen" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1200.00" },
|
|
||||||
{ account = "2400", side = "credit", amount = "1200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-31"
|
|
||||||
description = "January earned"
|
|
||||||
parties = [{ party = "Riley Chen" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2400", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 1
|
|
||||||
party = "Northwind Filings"
|
|
||||||
values = { resolved = "300.00", direction = "neither", cash = "0.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
# Before P3: 600.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Northwind Filings"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "300.00", received = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 4
|
|
||||||
party = "Riley Chen"
|
|
||||||
values = { resolved = "100.00", direction = "neither", cash = "0.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
# Before P3: 1,300.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Riley Chen"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "0.00", received = "1200.00" }
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
# P3 defect 24 in a member's entry (found in review, 2026-10-21): a
|
|
||||||
# distribution declared to a liability that isn't money counted as paid
|
|
||||||
# when declared and again when paid. A distribution's cash is the money it
|
|
||||||
# paid, its in kind the property it gave; the rest is still owed. Regression
|
|
||||||
# tests: ledger-core tests/spec_party_totals.rs
|
|
||||||
# defect24_a_distribution_declared_to_a_payable_counts_once_when_paid and
|
|
||||||
# a_distribution_of_money_and_depreciated_property_splits_by_what_moved.
|
|
||||||
# The shape: bookspec/README.md.
|
|
||||||
|
|
||||||
id = "p3-party-distribution-still-owed"
|
|
||||||
title = "A distribution counts the money it paid and the property it gave; what is still owed counts when paid"
|
|
||||||
rule = """
|
|
||||||
An entry naming only members that moves their capital counts what moved
|
|
||||||
it. Of a distribution, the cash is the money it paid (through the cash
|
|
||||||
accounts, a card, the members' financing or a loan), the in kind the
|
|
||||||
property it gave up (at what the books carried it), and the rest, credited
|
|
||||||
to a liability that isn't money for the entry (distributions payable, or
|
|
||||||
what the members are owed when they are named its creditors), is a
|
|
||||||
promise: it counts nothing until it is paid, then once, as paid. The
|
|
||||||
statement of members' equity counts a distribution when it is declared,
|
|
||||||
so party totals catch up with it when it is paid.
|
|
||||||
"""
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Helvering v. Price, 309 U.S. 409, 412-413 (1940): a promise to pay is not payment (wanderbooks docs/spec/sources/helvering-v-price-1940.md)",
|
|
||||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)(4)-(5): a partner's capital account falls by the money and the property distributed (docs/spec/sources/irs-capital-accounts.md)",
|
|
||||||
"DECISION (owner, 2026-10-20): a member's entry counts what moved their capital; money is cash, cards, the members' financing and loans (rules.md B-35)",
|
|
||||||
"DECISION (owner, 2026-10-20): property distributed counts at the books' carrying amount, not its fair market value (wanderbooks rules.md B-35 item 3)",
|
|
||||||
]
|
|
||||||
tags = ["parties", "members", "distributions", "promises", "in-kind", "money"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation — Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2150"
|
|
||||||
name = "Distributions Payable"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital — Tobin Harlow"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital — Morgan Ellis"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3100"
|
|
||||||
name = "Distributions — Tobin Harlow"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
roles = ["member"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
roles = ["member"]
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
share = "0.5"
|
|
||||||
capital_account = "3000"
|
|
||||||
distributions_account = "3100"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
share = "0.5"
|
|
||||||
capital_account = "3001"
|
|
||||||
|
|
||||||
# 1. A distribution declared to Tobin: still owed, counts nothing.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-30"
|
|
||||||
description = "Distribution declared"
|
|
||||||
parties = [{ party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "3100", side = "debit", amount = "200.00" },
|
|
||||||
{ account = "2150", side = "credit", amount = "200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 2. It is paid: paid, money, once.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-07-15"
|
|
||||||
description = "Distribution paid"
|
|
||||||
parties = [{ party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2150", side = "debit", amount = "200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 3. Declared and partly paid at once: the money now, the rest when paid.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-08-31"
|
|
||||||
description = "Distribution, part paid"
|
|
||||||
parties = [{ party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "3100", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "100.00" },
|
|
||||||
{ account = "2150", side = "credit", amount = "200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 4. Equipment carried at 1,000.00 less 300.00 depreciated, with 100.00 in
|
|
||||||
# cash, distributed to Morgan: 100.00 cash, 700.00 in kind.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-09-01"
|
|
||||||
description = "Equipment and cash distributed"
|
|
||||||
parties = [{ party = "Morgan Ellis" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "3001", side = "debit", amount = "800.00" },
|
|
||||||
{ account = "1510", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "1500", side = "credit", amount = "1000.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 1
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { resolved = "200.00", direction = "neither", cash = "0.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 3
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { resolved = "300.00", direction = "paid", cash = "100.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 4
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
values = { resolved = "800.00", direction = "paid", cash = "100.00", in_kind = "700.00" }
|
|
||||||
|
|
||||||
# Before the review: 700.00 (event 1's 200.00 and the 200.00 still owed
|
|
||||||
# on event 3 counted as paid, and event 2 paid the first again).
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
basis = "cash"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "300.00", received = "0.00" }
|
|
||||||
|
|
||||||
# Through June: declared, not yet paid. The statement counts it already.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-06-30"
|
|
||||||
values = { paid = "0.00", received = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "members_equity"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-06-30"
|
|
||||||
values = { "Tobin Harlow.contributions" = "0.00", "Tobin Harlow.distributions" = "200.00" }
|
|
||||||
|
|
||||||
# Before the review: 0.00 cash, 800.00 in kind.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
basis = "cash"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "100.00", received = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "800.00", received = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "members_equity"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Morgan Ellis.contributions" = "0.00", "Morgan Ellis.distributions" = "800.00" }
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
# P3 defect 24 and the card rule (found in review, 2026-10-21): with no
|
|
||||||
# business card recorded, the company's cards are the liabilities whose
|
|
||||||
# names say "card", so a gift card the company sold (deferred revenue)
|
|
||||||
# read as money: received when sold and again when redeemed. Regression
|
|
||||||
# test: ledger-core tests/spec_party_totals.rs
|
|
||||||
# defect24_a_gift_card_sold_is_received_once.
|
|
||||||
# The shape: bookspec/README.md.
|
|
||||||
|
|
||||||
id = "p3-party-gift-card-is-a-promise"
|
|
||||||
title = "A gift card the company sold is a promise, not a card it pays with"
|
|
||||||
rule = """
|
|
||||||
The company's cards are the business cards its books record as payment
|
|
||||||
methods; while they record none, the liability accounts whose names say
|
|
||||||
"card", but not "gift card": gift cards the company sold are revenue it
|
|
||||||
owes its customers in service (deferred revenue), a promise. Selling one
|
|
||||||
is money received from the buyer; redeeming it moves no money and counts
|
|
||||||
nothing.
|
|
||||||
"""
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"DECISION (owner, 2026-10-20): money is cash, cards, the members' financing and loans; every other liability is a promise, counted when settled (wanderbooks rules.md B-35 item 5)",
|
|
||||||
"Helvering v. Price, 309 U.S. 409, 412-413 (1940): a promise to pay is not payment (docs/spec/sources/helvering-v-price-1940.md)",
|
|
||||||
]
|
|
||||||
tags = ["parties", "cards", "deferred-revenue", "money"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2050"
|
|
||||||
name = "Business Card"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2410"
|
|
||||||
name = "Gift Card Liability"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Revenue"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Riley Chen"
|
|
||||||
roles = ["customer"]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-01"
|
|
||||||
description = "Gift card sold"
|
|
||||||
parties = [{ party = "Riley Chen" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "2410", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-05"
|
|
||||||
description = "Gift card redeemed"
|
|
||||||
parties = [{ party = "Riley Chen" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2410", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 1
|
|
||||||
party = "Riley Chen"
|
|
||||||
values = { resolved = "-100.00", direction = "received", cash = "-100.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 2
|
|
||||||
party = "Riley Chen"
|
|
||||||
values = { resolved = "100.00", direction = "neither", cash = "0.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
# Before the review: 200.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Riley Chen"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "0.00", received = "100.00" }
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
# P3 defect 14, settled in design (wanderbooks plan/served.md P3): party
|
|
||||||
# totals count a member's entry by what moved their capital, so basis all
|
|
||||||
# is the statement of members' equity and basis cash means what it means
|
|
||||||
# on the K-1 worksheet. Regression tests: ledger-core
|
|
||||||
# tests/spec_party_totals.rs a_members_mixed_entry_splits_as_the_k1_does,
|
|
||||||
# basis_all_is_the_members_equity_statement_and_cash_the_k1s, and the
|
|
||||||
# property member_totals_are_the_members_equity_statement. The shape:
|
|
||||||
# bookspec/README.md.
|
|
||||||
|
|
||||||
id = "p3-party-members-equity-and-k1"
|
|
||||||
title = "A member's totals are their capital account: basis all the statement of members' equity, basis cash the K-1's cash"
|
|
||||||
rule = """
|
|
||||||
An entry naming only members that moves their equity counts what moved
|
|
||||||
their capital: a contribution received, a distribution paid. The
|
|
||||||
property in it (asset accounts that are neither cash nor receivables) is
|
|
||||||
in kind, shared among the members in proportion to their parts, to the
|
|
||||||
cent, leftover cents by largest remainder, ties to the member listed
|
|
||||||
first; the rest is cash, as the K-1 worksheet counts it: money, or what a
|
|
||||||
member paid for the company (an expense booked straight to capital, a
|
|
||||||
debt to them converted to capital).
|
|
||||||
"""
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)(1)-(2), (c)(1): money and property contributed; a partnership liability a partner pays is money contributed (wanderbooks docs/spec/sources/irs-capital-accounts.md)",
|
|
||||||
"Partner's Instructions for Schedule K-1 (Form 1065), Item L: the capital contributed during the year (docs/spec/sources/irs-capital-accounts.md)",
|
|
||||||
"DECISION (owner, 2026-10-15): what a member paid for the company is cash on the K-1 (wanderbooks ledger-core k1 INVARIANT 3); DECISION (owner, 2026-10-20): a member's entry counts what moved their capital (rules.md B-35)",
|
|
||||||
]
|
|
||||||
tags = ["parties", "members", "k1", "members-equity", "in-kind", "rounding"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "largest-remainder"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to Members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital — Tobin Harlow"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital — Morgan Ellis"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6050"
|
|
||||||
name = "Domain & Hosting"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
roles = ["member"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
roles = ["member"]
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
share = "0.5"
|
|
||||||
capital_account = "3000"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
share = "0.5"
|
|
||||||
capital_account = "3001"
|
|
||||||
|
|
||||||
# 1. A joint contribution of cash and property (the K-1 worksheet's
|
|
||||||
# SCENARIO 3): 500.00 each, of it 300.00 cash and 200.00 in kind.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-01"
|
|
||||||
description = "Joint contribution, cash and equipment"
|
|
||||||
parties = [{ party = "Morgan Ellis" }, { party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "600.00" },
|
|
||||||
{ account = "1500", side = "debit", amount = "400.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "500.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 2. One cent of property shared by two equal parts: the cent goes to the
|
|
||||||
# member listed first in the company's members (Tobin), whatever the
|
|
||||||
# order the entry names them in.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-02"
|
|
||||||
description = "A cent of property"
|
|
||||||
parties = [{ party = "Morgan Ellis" }, { party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "0.99" },
|
|
||||||
{ account = "1500", side = "debit", amount = "0.01" },
|
|
||||||
{ account = "3000", side = "credit", amount = "0.50" },
|
|
||||||
{ account = "3001", side = "credit", amount = "0.50" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 3. An expense Morgan paid, booked straight to capital: cash.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-03"
|
|
||||||
description = "Hosting Morgan paid"
|
|
||||||
parties = [{ party = "Morgan Ellis" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "6050", side = "debit", amount = "50.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "50.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 4. What the company owed Tobin, converted to capital: cash.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-04"
|
|
||||||
description = "Advance converted"
|
|
||||||
parties = [{ party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2100", side = "debit", amount = "70.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "70.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 1
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { resolved = "-500.00", direction = "received", cash = "-300.00", in_kind = "-200.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 2
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { resolved = "-0.50", direction = "received", cash = "-0.49", in_kind = "-0.01" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 2
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
values = { resolved = "-0.50", direction = "received", cash = "-0.50", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 3
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
values = { resolved = "-50.00", direction = "received", cash = "-50.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 4
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { resolved = "-70.00", direction = "received", cash = "-70.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
# Tobin: cash 300.00 + 0.49 + 70.00; in kind 200.00 + 0.01.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
basis = "cash"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "0.00", received = "370.49" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "0.00", received = "570.50" }
|
|
||||||
|
|
||||||
# Morgan: cash 300.00 + 0.50 + 50.00; in kind 200.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
basis = "cash"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "0.00", received = "350.50" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "0.00", received = "550.50" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "members_equity"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Tobin Harlow.contributions" = "570.50", "Tobin Harlow.distributions" = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "members_equity"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Morgan Ellis.contributions" = "550.50", "Morgan Ellis.distributions" = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "k1"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Tobin Harlow.contributions_cash" = "370.49", "Tobin Harlow.contributions_property" = "200.01" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "k1"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Morgan Ellis.contributions_cash" = "350.50", "Morgan Ellis.contributions_property" = "200.00" }
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
# P3 defect 24 and wanderbooks rules.md B-35 item 6: a credit to a
|
|
||||||
# liability the company owes a party itself never counts as paying that
|
|
||||||
# party. Regression test: ledger-core tests/spec_party_totals.rs
|
|
||||||
# defect24_a_credit_to_a_partys_own_liability_never_pays_it. The shape:
|
|
||||||
# bookspec/README.md.
|
|
||||||
|
|
||||||
id = "p3-party-own-liability-never-pays"
|
|
||||||
title = "A credit to what the company owes a party itself never pays it"
|
|
||||||
rule = """
|
|
||||||
A party may be named the creditor of the liabilities the company owes it
|
|
||||||
itself: a card's issuer, a loan's lender, each member for what the
|
|
||||||
members advanced. For an entry naming it, a credit to one is a promise,
|
|
||||||
not a payment: a fee on the issuer's own card, equipment bought on the
|
|
||||||
seller's own note, a distribution credited to what the members are owed.
|
|
||||||
It counts when it is paid. Property a member contributes subject to a
|
|
||||||
loan counts at its value net of the loan, in kind.
|
|
||||||
"""
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Helvering v. Price, 309 U.S. 409, 412-413 (1940): a promise to pay, even a secured one, is not payment (wanderbooks docs/spec/sources/helvering-v-price-1940.md)",
|
|
||||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)(2): property contributed, net of liabilities the partnership takes it subject to (docs/spec/sources/irs-capital-accounts.md)",
|
|
||||||
"DECISION (owner, 2026-10-20): a liability credit never counts as paying that liability's own creditor (rules.md B-35)",
|
|
||||||
]
|
|
||||||
tags = ["parties", "creditors", "cards", "notes", "distributions", "members"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100", "2500"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2050"
|
|
||||||
name = "Business Card"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to Members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2500"
|
|
||||||
name = "Note Payable"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital — Tobin Harlow"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital — Morgan Ellis"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6400"
|
|
||||||
name = "Bank Fees"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Harbor Bank"
|
|
||||||
roles = ["financial_institution"]
|
|
||||||
creditor_of = ["2050"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Lakeside Supply"
|
|
||||||
roles = []
|
|
||||||
creditor_of = ["2500"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
roles = ["member"]
|
|
||||||
creditor_of = ["2100"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
roles = ["member"]
|
|
||||||
creditor_of = ["2100"]
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
share = "0.5"
|
|
||||||
capital_account = "3000"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
share = "0.5"
|
|
||||||
capital_account = "3001"
|
|
||||||
|
|
||||||
# 1. The issuer's fee on its own card: neither.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-05-01"
|
|
||||||
description = "Card fee"
|
|
||||||
parties = [{ party = "Harbor Bank" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "6400", side = "debit", amount = "25.00" },
|
|
||||||
{ account = "2050", side = "credit", amount = "25.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 2. The card paid off: paid to the issuer.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-05-20"
|
|
||||||
description = "Card payment"
|
|
||||||
parties = [{ party = "Harbor Bank" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2050", side = "debit", amount = "125.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "125.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 3. A distribution to Tobin credited to what the members are owed:
|
|
||||||
# still owed, counts as nothing.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-30"
|
|
||||||
description = "Distribution declared"
|
|
||||||
parties = [{ party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "3000", side = "debit", amount = "200.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 4. It is paid: paid, money.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-07-15"
|
|
||||||
description = "Distribution paid"
|
|
||||||
parties = [{ party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2100", side = "debit", amount = "200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 5. Equipment on the seller's own note: neither.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-08-01"
|
|
||||||
description = "Equipment on a note"
|
|
||||||
parties = [{ party = "Lakeside Supply" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "900.00" },
|
|
||||||
{ account = "2500", side = "credit", amount = "900.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 6. A payment on the note: paid.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-09-01"
|
|
||||||
description = "Note payment"
|
|
||||||
parties = [{ party = "Lakeside Supply" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2500", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "300.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 7. Equipment worth 1,000.00 Morgan contributes subject to 200.00 owed
|
|
||||||
# on the note: 800.00 received, in kind (before P3: "paid 200.00").
|
|
||||||
[[events]]
|
|
||||||
date = "2026-09-15"
|
|
||||||
description = "Equipment contributed with its loan"
|
|
||||||
parties = [{ party = "Morgan Ellis" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "2500", side = "credit", amount = "200.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "800.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 1
|
|
||||||
party = "Harbor Bank"
|
|
||||||
values = { resolved = "-25.00", direction = "neither", cash = "0.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
# Before naming the issuer the card's creditor: 150.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Harbor Bank"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "125.00", received = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 3
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { resolved = "200.00", direction = "neither", cash = "0.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "200.00", received = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 5
|
|
||||||
party = "Lakeside Supply"
|
|
||||||
values = { resolved = "-900.00", direction = "neither", cash = "0.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Lakeside Supply"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "300.00", received = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 7
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
values = { resolved = "-800.00", direction = "received", cash = "0.00", in_kind = "-800.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
basis = "cash"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "0.00", received = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "0.00", received = "800.00" }
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
# P3 defect 14, the second bug found in design (wanderbooks P3 brief
|
|
||||||
# §1.4): a member pays the card from their own funds; reversing it read
|
|
||||||
# as a second payment. Regression test: ledger-core
|
|
||||||
# tests/spec_party_totals.rs
|
|
||||||
# defect14_a_reversed_member_card_payment_nets_to_zero. The shape:
|
|
||||||
# bookspec/README.md (`after = N` asks a query after event N).
|
|
||||||
|
|
||||||
id = "p3-party-reversed-card-payment"
|
|
||||||
title = "A reversed payment of one liability by another nets to zero"
|
|
||||||
rule = """
|
|
||||||
A reversal's part for each party is its original's negated, never read
|
|
||||||
from the reversal's own lines. A member paying the company's card from
|
|
||||||
their own funds (Dr card / Cr due to members), tagged with the card's
|
|
||||||
issuer, is paid to the issuer; its reversal takes that back, so the pair
|
|
||||||
nets to zero; reversing the reversal pays it again.
|
|
||||||
"""
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Pacioli, Summa (1494), Geijsbeek tr.: a mistake is corrected by the opposite entry (wanderbooks docs/spec/sources/pacioli-1494.md, rules.md B-9)",
|
|
||||||
"DECISION D-42 (owner, 2026-10-20): what a member pays for the company through the members' financing is money: cash borrowed and spent (wanderbooks plan/served.md; rules.md B-14 rule 11, B-35 items 5 and 8)",
|
|
||||||
]
|
|
||||||
tags = ["parties", "reversals", "liabilities"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2050"
|
|
||||||
name = "Business Card"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to Members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Harbor Bank"
|
|
||||||
roles = ["financial_institution"]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-01"
|
|
||||||
description = "A member pays the card from their own funds"
|
|
||||||
parties = [{ party = "Harbor Bank" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2050", side = "debit", amount = "100.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "100.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-02"
|
|
||||||
description = "Reversed"
|
|
||||||
reverses = 1
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-03"
|
|
||||||
description = "The reversal reversed"
|
|
||||||
reverses = 2
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 1
|
|
||||||
party = "Harbor Bank"
|
|
||||||
values = { resolved = "100.00", direction = "paid", cash = "100.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 2
|
|
||||||
party = "Harbor Bank"
|
|
||||||
values = { resolved = "-100.00", direction = "received", cash = "-100.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
# After the reversal: nothing paid (before P3: 200.00, the reversal read
|
|
||||||
# as a second payment).
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
after = 2
|
|
||||||
party = "Harbor Bank"
|
|
||||||
basis = "cash"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "0.00", received = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
after = 3
|
|
||||||
party = "Harbor Bank"
|
|
||||||
basis = "cash"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "100.00", received = "0.00" }
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
# P3 defect 14 (wanderbooks plan/served.md P3; audit findings F36, F37):
|
|
||||||
# a party's part of an entry is signed as a line on the party's own
|
|
||||||
# account, with its direction and what of it was money or property; a
|
|
||||||
# reversal is the exact opposite of what it reverses; party totals take a
|
|
||||||
# basis. Regression test: ledger-core tests/spec_party_totals.rs
|
|
||||||
# defect14_shares_take_the_partys_side_and_reversals_flip. The shape:
|
|
||||||
# bookspec/README.md.
|
|
||||||
|
|
||||||
id = "p3-party-signs-and-reversals"
|
|
||||||
title = "A party's part of an entry is signed as its own account would show it, and a reversal is the exact opposite"
|
|
||||||
rule = """
|
|
||||||
A party's part of an entry is debit-positive, as a line on the party's own
|
|
||||||
account would be: positive when the company paid it or gave it value,
|
|
||||||
negative when the company received from it. Each part has a direction
|
|
||||||
(paid, received, neither) and a cash and an in-kind amount. An entry
|
|
||||||
naming only members that moves equity counts what moved their capital,
|
|
||||||
the property in it in kind. A reversal's part is exactly its original's
|
|
||||||
negated. A bill or invoice moves nothing with the party yet (neither).
|
|
||||||
Party totals on basis cash count the cash parts, on basis all the cash
|
|
||||||
and in-kind parts, each on its root original's side.
|
|
||||||
"""
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Pacioli, Summa (1494), Geijsbeek tr., ch. 36: debit is what owes, credit what is owed (wanderbooks docs/spec/sources/pacioli-1494.md)",
|
|
||||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)(1)-(2), (4)-(5): a partner's capital account rises by money and property contributed, falls by money and property distributed (docs/spec/sources/irs-capital-accounts.md)",
|
|
||||||
"Helvering v. Price, 309 U.S. 409, 412-413 (1940): a promise to pay is not payment (docs/spec/sources/helvering-v-price-1940.md)",
|
|
||||||
"DECISION (owner, 2026-10-20): a member's entry counts what moved their capital (wanderbooks rules.md B-35)",
|
|
||||||
"DECISION D-42 (owner, 2026-10-20): what a member pays for the company through the members' financing is money: cash borrowed and spent (wanderbooks plan/served.md; rules.md B-14 rule 11, B-35 items 5 and 8)",
|
|
||||||
]
|
|
||||||
tags = ["parties", "signs", "reversals", "members", "in-kind", "basis"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
financing_accounts = ["2100"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1200"
|
|
||||||
name = "Accounts Receivable"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2000"
|
|
||||||
name = "Accounts Payable"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2100"
|
|
||||||
name = "Due to Members"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital — Tobin Harlow"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital — Morgan Ellis"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3999"
|
|
||||||
name = "Retained Earnings"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Revenue"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Organizational Costs"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
roles = ["member"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
roles = ["member"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Northwind Filings"
|
|
||||||
roles = ["vendor"]
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Riley Chen"
|
|
||||||
roles = ["customer"]
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
share = "0.5"
|
|
||||||
capital_account = "3000"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
share = "0.5"
|
|
||||||
capital_account = "3001"
|
|
||||||
|
|
||||||
# 1. Tobin pays the filing fee for the company (through the members'
|
|
||||||
# financing account), tagged with the vendor: paid to the vendor, money.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-10-04"
|
|
||||||
description = "Filing fee, paid by Tobin"
|
|
||||||
parties = [{ party = "Northwind Filings" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "64.00" },
|
|
||||||
{ account = "2100", side = "credit", amount = "64.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 2. Tobin's cash contribution: received, money.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-10-05"
|
|
||||||
description = "Tobin's contribution"
|
|
||||||
parties = [{ party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "400.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "400.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 3. Reimbursing Tobin for the fee: paid, money (cash paying down what
|
|
||||||
# the company owed him).
|
|
||||||
[[events]]
|
|
||||||
date = "2026-10-08"
|
|
||||||
description = "Reimbursement"
|
|
||||||
parties = [{ party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "2100", side = "debit", amount = "64.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "64.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 4. A laptop Tobin contributes: received, in kind.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-10-12"
|
|
||||||
description = "Laptop contributed"
|
|
||||||
parties = [{ party = "Tobin Harlow" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1480.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1480.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 5. Its reversal: the exact opposite, paid, in kind.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-10-13"
|
|
||||||
description = "Laptop contribution reversed"
|
|
||||||
reverses = 4
|
|
||||||
|
|
||||||
# 6. The laptop again, half each, by amount.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-10-14"
|
|
||||||
description = "Laptop contributed jointly"
|
|
||||||
parties = [{ party = "Tobin Harlow", amount = "740.00" }, { party = "Morgan Ellis", amount = "740.00" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1480.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "740.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "740.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 7. A bill from the vendor (credit to payables, its own account): neither.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-10-20"
|
|
||||||
description = "Bill"
|
|
||||||
older = true
|
|
||||||
parties = [{ party = "Northwind Filings" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "80.00" },
|
|
||||||
{ account = "2000", side = "credit", amount = "80.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# 8. The bill reversed: the exact opposite, still neither.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-10-21"
|
|
||||||
description = "Bill reversed"
|
|
||||||
reverses = 7
|
|
||||||
|
|
||||||
# 9. An invoice to the customer (a debit to receivables): neither.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-10-20"
|
|
||||||
description = "Invoice"
|
|
||||||
older = true
|
|
||||||
parties = [{ party = "Riley Chen" }]
|
|
||||||
lines = [
|
|
||||||
{ account = "1200", side = "debit", amount = "120.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "120.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 1
|
|
||||||
party = "Northwind Filings"
|
|
||||||
values = { resolved = "64.00", direction = "paid", cash = "64.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 2
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { resolved = "-400.00", direction = "received", cash = "-400.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 3
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { resolved = "64.00", direction = "paid", cash = "64.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 4
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { resolved = "-1480.00", direction = "received", cash = "0.00", in_kind = "-1480.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 5
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { resolved = "1480.00", direction = "paid", cash = "0.00", in_kind = "1480.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 6
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
values = { resolved = "-740.00", direction = "received", cash = "0.00", in_kind = "-740.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 7
|
|
||||||
party = "Northwind Filings"
|
|
||||||
values = { resolved = "-80.00", direction = "neither", cash = "0.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 8
|
|
||||||
party = "Northwind Filings"
|
|
||||||
values = { resolved = "80.00", direction = "neither", cash = "0.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_part"
|
|
||||||
entry = 9
|
|
||||||
party = "Riley Chen"
|
|
||||||
values = { resolved = "120.00", direction = "neither", cash = "0.00", in_kind = "0.00" }
|
|
||||||
|
|
||||||
# Tobin: cash in 400.00 (event 2), paid back 64.00 (event 3); in kind
|
|
||||||
# 1,480.00 − 1,480.00 (events 4 and 5 net) + 740.00 (event 6).
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
basis = "cash"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "64.00", received = "400.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "64.00", received = "1140.00" }
|
|
||||||
|
|
||||||
# The vendor: the fee a member paid for the company; the bill and its
|
|
||||||
# reversal count nothing.
|
|
||||||
[[expect]]
|
|
||||||
query = "party_totals"
|
|
||||||
party = "Northwind Filings"
|
|
||||||
basis = "all"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { paid = "64.00", received = "0.00" }
|
|
||||||
|
|
||||||
# Basis all over Tobin's equity entries is the statement of members'
|
|
||||||
# equity: contributions 400.00 + 740.00 (events 4 and 5 cancel).
|
|
||||||
[[expect]]
|
|
||||||
query = "members_equity"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Tobin Harlow.contributions" = "1140.00", "Tobin Harlow.distributions" = "0.00" }
|
|
||||||
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# P3 defect 11 (audit finding F28): the directory showed a member's TIN as
|
|
||||||
# missing because it read W-9 facts only. Regression tests: ledger-core
|
|
||||||
# tests/spec_party_totals.rs
|
|
||||||
# defect11_a_partys_tin_last4_is_its_tin_facts_else_its_w9s; ledger-cli
|
|
||||||
# tests/spec_vault.rs defect11_the_directory_shows_a_members_tin_last4.
|
|
||||||
# The shape: bookspec/README.md.
|
|
||||||
|
|
||||||
id = "p3-party-tin-last4"
|
|
||||||
title = "A party's TIN last four is its TIN facts', else its W-9's"
|
|
||||||
rule = """
|
|
||||||
The books keep a taxpayer identification number truncated to its last
|
|
||||||
four digits. A party's last four is its TIN facts' (a member's SSN for
|
|
||||||
the K-1), else its W-9 facts'; the two never disagree. A TIN purged from
|
|
||||||
the vault keeps its last four in the books. A party with neither has
|
|
||||||
none.
|
|
||||||
"""
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Treas. Reg. §301.6109-4(a): a truncated TIN shows the last four digits (wanderbooks docs/spec/sources/irs-truncated-tin.md)",
|
|
||||||
"DECISION (owner, 2026-10-15): a party's TIN facts and W-9 facts never disagree (wanderbooks ledger-core parties INVARIANT 2a)",
|
|
||||||
]
|
|
||||||
tags = ["parties", "tin", "w9", "k1"]
|
|
||||||
# Nothing is posted: the facts are the directory's.
|
|
||||||
events = []
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Checking"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
roles = ["member"]
|
|
||||||
tin_last4 = "4321"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Riley Chen"
|
|
||||||
roles = ["contractor"]
|
|
||||||
w9_tin_last4 = "8765"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
roles = ["member", "contractor"]
|
|
||||||
tin_last4 = "1111"
|
|
||||||
w9_tin_last4 = "1111"
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Lakeside Supply"
|
|
||||||
roles = []
|
|
||||||
|
|
||||||
[[setup.parties]]
|
|
||||||
name = "Avery Quinn"
|
|
||||||
roles = ["member"]
|
|
||||||
tin_last4 = "2468"
|
|
||||||
tin_purged = true
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_tin_last4"
|
|
||||||
party = "Tobin Harlow"
|
|
||||||
values = { tin_last4 = "4321" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_tin_last4"
|
|
||||||
party = "Riley Chen"
|
|
||||||
values = { tin_last4 = "8765" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_tin_last4"
|
|
||||||
party = "Morgan Ellis"
|
|
||||||
values = { tin_last4 = "1111" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_tin_last4"
|
|
||||||
party = "Lakeside Supply"
|
|
||||||
values = { tin_last4 = "" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "party_tin_last4"
|
|
||||||
party = "Avery Quinn"
|
|
||||||
values = { tin_last4 = "2468" }
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
# P3 (D-41): the K-1 worksheet reports tax-basis capital beside book capital.
|
|
||||||
id = "partnership-k1-tax-basis-capital"
|
|
||||||
title = "Schedule K-1 Item L on the tax basis: property contributed at its adjusted basis"
|
|
||||||
rule = "Each partner's capital account is reported on the tax basis: capital contributed is cash plus the adjusted tax basis of property contributed; the share of income is as figured for tax; distributions are cash plus the adjusted tax basis of property distributed. Book capital (property at fair market value) differs by the partner's share of the built-in gain or loss at contribution, and by every book-tax difference after; a figure the books cannot know (tax depreciation) is left to the preparer, never guessed."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"Instructions for Form 1065 (2025), Item L, Tax-basis method: Capital contributed during the year; Current year net income (loss); Withdrawals and distributions",
|
|
||||||
"Instructions for Form 1065 (2020), Item L: 'you must use the tax basis method this year'",
|
|
||||||
"26 U.S.C. §723",
|
|
||||||
"Treas. Reg. §1.704-3(a)(3)(ii)",
|
|
||||||
"DECISION D-41 (owner, 2026-10-20): the K-1 worksheet reports tax-basis capital beside the book figures (wanderbooks plan/served.md; rules.md B-36)",
|
|
||||||
]
|
|
||||||
tags = ["partnerships", "k1", "tax-basis", "capital"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "largest-remainder"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital, Tobin Harlow"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital, Morgan Ellis"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
capital_account = "3001"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
# Two devices the members contribute jointly from personal use, 24-month
|
|
||||||
# lives: a laptop worth 1,440.00 (adjusted basis 1,688.40) and a camera
|
|
||||||
# worth 1,300.00 (adjusted basis 1,080.00).
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "laptop"
|
|
||||||
description = "Laptop"
|
|
||||||
cost = "1440.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 24
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 2
|
|
||||||
basis = { rule = "personal_use_conversion", contributor_adjusted_basis = "1688.40", fmv_at_contribution = "1440.00", sec704c_method = "traditional" }
|
|
||||||
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "camera"
|
|
||||||
description = "Camera"
|
|
||||||
cost = "1300.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 24
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 2
|
|
||||||
basis = { rule = "personal_use_conversion", contributor_adjusted_basis = "1080.00", fmv_at_contribution = "1300.00", sec704c_method = "traditional" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Cash contributed"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "2000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1000.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "1000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Devices contributed jointly, at their values"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "1440.00" },
|
|
||||||
{ account = "1500", side = "debit", amount = "1300.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1370.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "1370.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-01"
|
|
||||||
description = "Sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "600.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "600.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# December's runs catch the year up (rules.md B-15): 1,440.00 x 12/24 =
|
|
||||||
# 720.00 and 1,300.00 x 12/24 = 650.00, dated Dec 31.
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "laptop", period = "2026-12" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_depreciation = { asset = "camera", period = "2026-12" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-12-15"
|
|
||||||
description = "Distributions"
|
|
||||||
lines = [
|
|
||||||
{ account = "3000", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "3001", side = "debit", amount = "300.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "600.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Hand check, book, each: 1,000.00 cash + 1,370.00 property + (600.00 -
|
|
||||||
# 720.00 - 650.00) / 2 = -385.00 - 300.00 = 1,685.00.
|
|
||||||
[[expect]]
|
|
||||||
query = "k1"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Tobin Harlow.beginning" = "0.00", "Tobin Harlow.contributions_cash" = "1000.00", "Tobin Harlow.contributions_property" = "1370.00", "Tobin Harlow.net_income" = "-385.00", "Tobin Harlow.distributions" = "300.00", "Tobin Harlow.ending" = "1685.00" }
|
|
||||||
|
|
||||||
# Tax, each: property at its §723 basis, (1,688.40 + 1,080.00) / 2 =
|
|
||||||
# 1,384.20; income open (tax depreciation is the preparer's), known so far
|
|
||||||
# -385.00; ending known so far 1,000.00 + 1,384.20 - 385.00 - 300.00 =
|
|
||||||
# 1,699.20.
|
|
||||||
[[expect]]
|
|
||||||
query = "k1_tax_basis"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Tobin Harlow.beginning" = "0.00", "Tobin Harlow.contributions_cash" = "1000.00", "Tobin Harlow.contributions_property" = "1384.20", "Tobin Harlow.net_income" = "open", "Tobin Harlow.known_net_income" = "-385.00", "Tobin Harlow.distributions" = "300.00", "Tobin Harlow.ending" = "open", "Tobin Harlow.known_ending" = "1699.20" }
|
|
||||||
|
|
||||||
# Book less tax: the built-in gain share, ((1,440.00 - 1,688.40) +
|
|
||||||
# (1,300.00 - 1,080.00)) / 2 = (-248.40 + 220.00) / 2 = -14.20 =
|
|
||||||
# 1,685.00 - 1,699.20.
|
|
||||||
[[expect]]
|
|
||||||
query = "k1_reconciliation"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Tobin Harlow.beginning" = "0.00", "Tobin Harlow.built_in_gain_704c" = "-14.20", "Tobin Harlow.property_without_basis" = "0.00", "Tobin Harlow.property_distributed" = "0.00", "Tobin Harlow.total" = "-14.20" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "k1_tax_basis"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Morgan Ellis.contributions_property" = "1384.20", "Morgan Ellis.known_ending" = "1699.20" }
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
# B-36 with D-38: the tax-basis K-1's odd cents run cumulatively.
|
|
||||||
id = "partnership-k1-tax-basis-odd-cents"
|
|
||||||
title = "Tax-basis capital: odd cents of contributed property's basis split cumulatively"
|
|
||||||
rule = "Within each contribution, the property's built-in gain or loss (value less adjusted tax basis) is shared among the members as its book value is; to the cent, each member's running share from the books' first entry MUST be the largest-remainder rounding of their exact running share (ties to the member listed first), less what earlier entries gave them, so no member's share is ever a cent from exact and tied odd cents alternate instead of piling up on one member."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Instructions for Form 1065 (2025), Item L, Tax-basis method: Capital contributed during the year",
|
|
||||||
"26 U.S.C. §723",
|
|
||||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)",
|
|
||||||
"DECISION D-38 (owner, 2026-10-20): odd cents everywhere alike, cumulative largest remainder; applied to the tax-basis K-1 by the owner, 2026-10-22 (wanderbooks rules.md B-36, B-13)",
|
|
||||||
]
|
|
||||||
tags = ["partnerships", "k1", "tax-basis", "rounding"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "largest-remainder"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1510"
|
|
||||||
name = "Accumulated Depreciation"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital, Tobin Harlow"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3001"
|
|
||||||
name = "Capital, Morgan Ellis"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Depreciation Expense"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Tobin Harlow"
|
|
||||||
capital_account = "3000"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
[[setup.members]]
|
|
||||||
name = "Morgan Ellis"
|
|
||||||
capital_account = "3001"
|
|
||||||
share = "0.5"
|
|
||||||
|
|
||||||
# Three items the members contribute jointly, each from personal use and
|
|
||||||
# each worth less than its adjusted basis by an odd number of cents.
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "monitor"
|
|
||||||
description = "Monitor"
|
|
||||||
cost = "400.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 24
|
|
||||||
in_service = "2026-01-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 1
|
|
||||||
basis = { rule = "personal_use_conversion", contributor_adjusted_basis = "412.37", fmv_at_contribution = "400.00", sec704c_method = "traditional" }
|
|
||||||
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "chair"
|
|
||||||
description = "Chair"
|
|
||||||
cost = "250.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 24
|
|
||||||
in_service = "2026-06-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 2
|
|
||||||
basis = { rule = "personal_use_conversion", contributor_adjusted_basis = "263.41", fmv_at_contribution = "250.00", sec704c_method = "traditional" }
|
|
||||||
|
|
||||||
[[setup.assets]]
|
|
||||||
id = "desk"
|
|
||||||
description = "Desk"
|
|
||||||
cost = "600.00"
|
|
||||||
salvage = "0.00"
|
|
||||||
life_months = 24
|
|
||||||
in_service = "2026-11-01"
|
|
||||||
method = "straight_line"
|
|
||||||
cost_account = "1500"
|
|
||||||
accumulated_account = "1510"
|
|
||||||
expense_account = "6200"
|
|
||||||
acquired_by = 3
|
|
||||||
basis = { rule = "personal_use_conversion", contributor_adjusted_basis = "655.91", fmv_at_contribution = "600.00", sec704c_method = "traditional" }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-01"
|
|
||||||
description = "Monitor contributed jointly, at its value"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "400.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "200.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-01"
|
|
||||||
description = "Chair contributed jointly, at its value"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "250.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "125.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "125.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-11-01"
|
|
||||||
description = "Desk contributed jointly, at its value"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "600.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "300.00" },
|
|
||||||
{ account = "3001", side = "credit", amount = "300.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand, the built-in loss shared as it runs (exact shares in brackets):
|
|
||||||
# Monitor, -12.37: running -12.37 (-6.185 each); rounded toward zero
|
|
||||||
# -6.18 each, a cent short, the remainders tie, the cent to Tobin:
|
|
||||||
# Tobin -6.19, Morgan -6.18.
|
|
||||||
# Chair, -13.41: running -25.78 (-12.89 each, exact): Tobin -12.89 less
|
|
||||||
# -6.19 given = -6.70; Morgan -12.89 less -6.18 = -6.71.
|
|
||||||
# Desk, -55.91: running -81.69 (-40.845 each): tied again, to Tobin:
|
|
||||||
# Tobin -40.85 less -12.89 = -27.96; Morgan -40.84 less -12.89 =
|
|
||||||
# -27.95.
|
|
||||||
# Year: built-in loss Tobin -40.85, Morgan -40.84; book property 625.00
|
|
||||||
# each; tax property 625.00 + 40.85 = 665.85 and 625.00 + 40.84 = 665.84,
|
|
||||||
# a cent apart. Shared entry by entry (each entry's tied cent to Tobin)
|
|
||||||
# it was -40.86 / -40.83: 665.86 against 665.83, three cents apart.
|
|
||||||
[[expect]]
|
|
||||||
query = "k1_tax_basis"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Tobin Harlow.contributions_property" = "665.85", "Tobin Harlow.known_ending" = "665.85", "Morgan Ellis.contributions_property" = "665.84", "Morgan Ellis.known_ending" = "665.84" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "k1_reconciliation"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Tobin Harlow.beginning" = "0.00", "Tobin Harlow.built_in_gain_704c" = "-40.85", "Tobin Harlow.property_without_basis" = "0.00", "Tobin Harlow.property_distributed" = "0.00", "Tobin Harlow.total" = "-40.85", "Morgan Ellis.beginning" = "0.00", "Morgan Ellis.built_in_gain_704c" = "-40.84", "Morgan Ellis.property_without_basis" = "0.00", "Morgan Ellis.property_distributed" = "0.00", "Morgan Ellis.total" = "-40.84" }
|
|
||||||
|
|
||||||
# From June the count runs on from January: the beginning is January to
|
|
||||||
# May's ending, open (the monitor's tax depreciation is the preparer's),
|
|
||||||
# known so far Tobin 200.00 + 6.19 = 206.19, Morgan 206.18; the span
|
|
||||||
# takes the chair's and the desk's parts, -6.70 - 27.96 = -34.66 and
|
|
||||||
# -6.71 - 27.95 = -34.66: tax property 425.00 + 34.66 = 459.66 each, the
|
|
||||||
# ending the year's.
|
|
||||||
[[expect]]
|
|
||||||
query = "k1_tax_basis"
|
|
||||||
from = "2026-06-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
values = { "Tobin Harlow.beginning" = "open", "Tobin Harlow.known_beginning" = "206.19", "Tobin Harlow.contributions_property" = "459.66", "Tobin Harlow.known_ending" = "665.85", "Morgan Ellis.beginning" = "open", "Morgan Ellis.known_beginning" = "206.18", "Morgan Ellis.contributions_property" = "459.66", "Morgan Ellis.known_ending" = "665.84" }
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
id = "period-a-close-sweeps-the-year-into-capital"
|
|
||||||
title = "A close zeroes revenue and expenses into equity, and the closed period takes no new postings"
|
|
||||||
rule = "A close MUST zero every revenue and expense account through its last day and credit equity with exactly the net income; a period once closed MUST take no new postings."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"Pacioli, Summa (1494), ch. 34: \"you shall close this account into the capital account\" (docs/spec/sources/pacioli-1494.md)",
|
|
||||||
"OpenStax, Principles of Accounting, Vol. 1, 5.1: temporary accounts are closed at the end of each period (docs/spec/rules.md B-11)",
|
|
||||||
"CONVENTION (docs/spec/rules.md B-12): a closed period is locked, which follows from posted-is-forever",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "period", "close"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2000"
|
|
||||||
name = "Loan"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
tax_category = "gross_receipts"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Rent"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "rent_property"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-02"
|
|
||||||
description = "Members put in cash"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "5000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "5000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-10"
|
|
||||||
description = "A sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "900.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "900.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-10"
|
|
||||||
description = "Rent"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "350.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "350.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Close January 1 through March 31 into capital.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-31"
|
|
||||||
description = "Close the quarter"
|
|
||||||
close = { start = "2026-01-01", to = "retained_earnings" }
|
|
||||||
|
|
||||||
# March 31 is closed: an entry dated inside the quarter is refused.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-15"
|
|
||||||
description = "A late sale"
|
|
||||||
refused = "period_closed"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "50.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "50.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand: net income = 900.00 - 350.00 = 550.00. Revenue and expense are
|
|
||||||
# zero; the 550.00 lands in Retained Earnings (3999, the account the books
|
|
||||||
# create for the close), so equity is 5000.00 + 550.00 = 5550.00; cash is
|
|
||||||
# 5000 + 900 - 350 = 5550.00, so assets equal equity.
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "5550.00"
|
|
||||||
"3000" = "5000.00"
|
|
||||||
"3999" = "550.00"
|
|
||||||
"4000" = "0.00"
|
|
||||||
"6000" = "0.00"
|
|
||||||
|
|
||||||
# The statement for the quarter excludes the close: the year's income is
|
|
||||||
# still reported.
|
|
||||||
[[expect]]
|
|
||||||
query = "profit_and_loss"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-03-31"
|
|
||||||
[expect.values]
|
|
||||||
"4000" = "900.00"
|
|
||||||
"6000" = "350.00"
|
|
||||||
net_income = "550.00"
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
# An invoice's or bill's lines count toward the projects they name.
|
|
||||||
#
|
|
||||||
# wanderbooks: rules.md B-37 item 8 (journal feature
|
|
||||||
# `document_line_projects`); regression tests in ledger-core
|
|
||||||
# `tests/spec_document_projects.rs`, ledger-store
|
|
||||||
# `tests/document_projects.rs`, ledger-cli `tests/spec_projects.rs`
|
|
||||||
# (`invoice_and_bill_lines_count_toward_their_projects`).
|
|
||||||
#
|
|
||||||
# Expected values worked by hand. Invoice 1 bills 3,000.00 + 500.00 of
|
|
||||||
# Zeta's design (3,500.00), 200.00 of Harbor's hosting and an untagged
|
|
||||||
# 100.00 rush fee: 3,800.00, all Revenue (4000), the receivable untagged.
|
|
||||||
# Bill 1 is 1,200.00 of Zeta's illustration and 50.00 of untagged
|
|
||||||
# postage: 1,250.00 of General Expenses (6999). Bill 2, 300.00, names no
|
|
||||||
# project. The customer's payment and the bill's payment move cash and
|
|
||||||
# the receivable or payable only: no project figure. So Zeta nets
|
|
||||||
# 3,500.00 - 1,200.00 = 2,300.00; Harbor 200.00 (both are client work,
|
|
||||||
# so neither has an unrecovered cost: on spec, a hoped-for payment is no
|
|
||||||
# receivable, B-37 item 4); the untagged lines 100.00 - 50.00 - 300.00
|
|
||||||
# - 1,500.00 (rent) = -1,750.00; together 750.00, the P&L's net
|
|
||||||
# (3,800.00 - 1,550.00 - 1,500.00). Dana was paid 1,250.00 from the bank,
|
|
||||||
# whatever the bill's projects; the second bill is still owed.
|
|
||||||
|
|
||||||
id = "projects-invoice-and-bill-lines-count-by-project"
|
|
||||||
title = "An invoice's or bill's lines count toward the projects they name"
|
|
||||||
rule = "Each line of an invoice or bill MAY name a project; its revenue (an invoice's) or cost (a bill's) MUST then count toward that project's figures exactly as a tagged line of a hand-built entry would, the receivable, the payable and their payments MUST count toward none, and every figure that is not a project's (the P&L, a payee's 1099 payments) MUST be what it would be with no project named."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = [
|
|
||||||
"Job-order costing: a job cost sheet accumulates the revenue and the direct costs identified with one job, a supplier's invoice and the job's billing among them (management-accounting convention; wanderbooks docs/spec/rules.md B-37 items 3 and 8)",
|
|
||||||
"wanderbooks docs/spec/rules.md B-11 and B-14: a period's net income is its revenue less its expenses",
|
|
||||||
"Treas. Reg. §1.6041-1(a)(1)(iv) (docs/spec/sources/irs-1099-payment-method.md): a 1099 counts payments made, not how the books classify them",
|
|
||||||
]
|
|
||||||
tags = ["projects", "classes", "job-costing", "accrual", "invoices", "bills"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
cash_accounts = ["1000"]
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1200"
|
|
||||||
name = "Accounts Receivable"
|
|
||||||
type = "asset"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2000"
|
|
||||||
name = "Accounts Payable"
|
|
||||||
type = "liability"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Revenue"
|
|
||||||
type = "revenue"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Rent"
|
|
||||||
type = "expense"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6999"
|
|
||||||
name = "General Expenses"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.projects]]
|
|
||||||
name = "Zeta"
|
|
||||||
kind = "client"
|
|
||||||
[[setup.projects]]
|
|
||||||
name = "Harbor"
|
|
||||||
kind = "client"
|
|
||||||
|
|
||||||
[[setup.payees]]
|
|
||||||
name = "Dana Illustration"
|
|
||||||
form = "1099-NEC"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-02"
|
|
||||||
description = "Invoice to Cedar"
|
|
||||||
invoice = { customer = "Cedar Client", lines = [
|
|
||||||
{ description = "Design", amount = "3000.00", project = "Zeta" },
|
|
||||||
{ description = "Hosting", amount = "200.00", project = "Harbor" },
|
|
||||||
{ description = "Rush fee", amount = "100.00" },
|
|
||||||
{ description = "Revisions", amount = "500.00", project = "Zeta" },
|
|
||||||
] }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-09"
|
|
||||||
description = "Dana's bill"
|
|
||||||
bill = { vendor = "Dana Illustration", lines = [
|
|
||||||
{ description = "Illustration", amount = "1200.00", project = "Zeta" },
|
|
||||||
{ description = "Postage", amount = "50.00" },
|
|
||||||
] }
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-20"
|
|
||||||
description = "Cedar pays invoice 1"
|
|
||||||
older = true
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "3800.00" },
|
|
||||||
{ account = "1200", side = "credit", amount = "3800.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-30"
|
|
||||||
description = "Pay Dana's bill"
|
|
||||||
older = true
|
|
||||||
payee = "Dana Illustration"
|
|
||||||
lines = [
|
|
||||||
{ account = "2000", side = "debit", amount = "1250.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1250.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-31"
|
|
||||||
description = "Studio rent"
|
|
||||||
lines = [
|
|
||||||
{ account = "6200", side = "debit", amount = "1500.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1500.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-04-02"
|
|
||||||
description = "Dana's second bill"
|
|
||||||
bill = { vendor = "Dana Illustration", amount = "300.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 1
|
|
||||||
[expect.values]
|
|
||||||
"1200.debit" = "3800.00"
|
|
||||||
"4000.credit" = "3800.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "entry"
|
|
||||||
entry = 2
|
|
||||||
[expect.values]
|
|
||||||
"6999.debit" = "1250.00"
|
|
||||||
"2000.credit" = "1250.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "project"
|
|
||||||
project = "Zeta"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
revenue = "3500.00"
|
|
||||||
expenses = "1200.00"
|
|
||||||
net = "2300.00"
|
|
||||||
"to_date.revenue" = "3500.00"
|
|
||||||
"to_date.expenses" = "1200.00"
|
|
||||||
"to_date.net" = "2300.00"
|
|
||||||
unrecovered_cost = "none"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "project"
|
|
||||||
project = "Harbor"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
revenue = "200.00"
|
|
||||||
expenses = "0.00"
|
|
||||||
net = "200.00"
|
|
||||||
"to_date.revenue" = "200.00"
|
|
||||||
"to_date.expenses" = "0.00"
|
|
||||||
"to_date.net" = "200.00"
|
|
||||||
unrecovered_cost = "none"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "profit_and_loss"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"4000" = "3800.00"
|
|
||||||
"6200" = "1500.00"
|
|
||||||
"6999" = "1550.00"
|
|
||||||
net_income = "750.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "vendor_1099_paid"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"Dana Illustration" = "1250.00"
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
# A project's figures are the revenue and expense lines tagged to it.
|
|
||||||
#
|
|
||||||
# wanderbooks: rules.md B-37 (projects); regression tests
|
|
||||||
# `project_figures_are_its_tagged_lines` (ledger-core tests/spec_projects.rs),
|
|
||||||
# `project_figures_match_profit_and_loss_by_class` (ledger-cli
|
|
||||||
# tests/spec_projects.rs).
|
|
||||||
#
|
|
||||||
# Expected values worked by hand: Alpha earns 5,000.00 and costs 1,800.00
|
|
||||||
# (contractor) + 120.00 (its part of the tool) = 1,920.00; the contractor
|
|
||||||
# billed twice in March is reversed, so March nets to zero. Beta costs
|
|
||||||
# 80.00; its test devices (an asset) count nothing. Rent is no project's.
|
|
||||||
# The projects and the untagged rent add up to the books' net income:
|
|
||||||
# 3,080.00 - 80.00 - 1,500.00 = 1,500.00.
|
|
||||||
|
|
||||||
id = "projects-net-is-its-tagged-lines"
|
|
||||||
title = "A project's net is its tagged revenue less its tagged expenses"
|
|
||||||
rule = "A project's revenue and expenses for a period MUST be the revenue and expense lines tagged to it, dated in the period, closing entries left out; a tagged balance-sheet line MUST count nothing; a shared cost counts only as split across lines; and the projects with the untagged lines MUST add up to the period's net income."
|
|
||||||
status = "CONVENTION"
|
|
||||||
sources = [
|
|
||||||
"Job-order costing: a job cost sheet accumulates the revenue and the direct costs identified with one job (management-accounting convention; no overhead is allocated here: wanderbooks docs/spec/rules.md B-37)",
|
|
||||||
"wanderbooks docs/spec/rules.md B-11 and B-14: a period's net income is its revenue less its expenses, closing entries left out",
|
|
||||||
]
|
|
||||||
tags = ["projects", "classes", "job-costing", "accrual"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1500"
|
|
||||||
name = "Equipment"
|
|
||||||
type = "asset"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Revenue"
|
|
||||||
type = "revenue"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Contractors"
|
|
||||||
type = "expense"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6100"
|
|
||||||
name = "Software"
|
|
||||||
type = "expense"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6200"
|
|
||||||
name = "Rent"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.projects]]
|
|
||||||
name = "Alpha"
|
|
||||||
kind = "client"
|
|
||||||
[[setup.projects]]
|
|
||||||
name = "Beta"
|
|
||||||
kind = "internal"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-03"
|
|
||||||
description = "Alpha milestone paid"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "5000.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "5000.00", project = "Alpha" },
|
|
||||||
]
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-10"
|
|
||||||
description = "Contractor for Alpha"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1800.00", project = "Alpha" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1800.00" },
|
|
||||||
]
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-15"
|
|
||||||
description = "Design tool, split by use"
|
|
||||||
lines = [
|
|
||||||
{ account = "6100", side = "debit", amount = "120.00", project = "Alpha" },
|
|
||||||
{ account = "6100", side = "debit", amount = "80.00", project = "Beta" },
|
|
||||||
{ account = "1000", side = "credit", amount = "200.00" },
|
|
||||||
]
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-28"
|
|
||||||
description = "Studio rent"
|
|
||||||
lines = [
|
|
||||||
{ account = "6200", side = "debit", amount = "1500.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1500.00" },
|
|
||||||
]
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-05"
|
|
||||||
description = "Test devices for Beta"
|
|
||||||
lines = [
|
|
||||||
{ account = "1500", side = "debit", amount = "900.00", project = "Beta" },
|
|
||||||
{ account = "1000", side = "credit", amount = "900.00" },
|
|
||||||
]
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-12"
|
|
||||||
description = "Contractor for Alpha, billed twice"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1800.00", project = "Alpha" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1800.00" },
|
|
||||||
]
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-13"
|
|
||||||
description = "Reverse entry 6"
|
|
||||||
reverses = 6
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "project"
|
|
||||||
project = "Alpha"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
revenue = "5000.00"
|
|
||||||
expenses = "1920.00"
|
|
||||||
net = "3080.00"
|
|
||||||
"to_date.revenue" = "5000.00"
|
|
||||||
"to_date.expenses" = "1920.00"
|
|
||||||
"to_date.net" = "3080.00"
|
|
||||||
unrecovered_cost = "none"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "project"
|
|
||||||
project = "Alpha"
|
|
||||||
from = "2026-03-01"
|
|
||||||
to = "2026-03-31"
|
|
||||||
[expect.values]
|
|
||||||
revenue = "0.00"
|
|
||||||
expenses = "0.00"
|
|
||||||
net = "0.00"
|
|
||||||
"to_date.revenue" = "5000.00"
|
|
||||||
"to_date.expenses" = "1920.00"
|
|
||||||
"to_date.net" = "3080.00"
|
|
||||||
unrecovered_cost = "none"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "project"
|
|
||||||
project = "Beta"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
revenue = "0.00"
|
|
||||||
expenses = "80.00"
|
|
||||||
net = "-80.00"
|
|
||||||
"to_date.revenue" = "0.00"
|
|
||||||
"to_date.expenses" = "80.00"
|
|
||||||
"to_date.net" = "-80.00"
|
|
||||||
unrecovered_cost = "none"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "profit_and_loss"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"4000" = "5000.00"
|
|
||||||
"6000" = "1800.00"
|
|
||||||
"6100" = "200.00"
|
|
||||||
"6200" = "1500.00"
|
|
||||||
net_income = "1500.00"
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
# Work on spec: its costs are expenses when incurred, and the hoped-for
|
|
||||||
# payment is income only when received; until then it is no receivable.
|
|
||||||
#
|
|
||||||
# wanderbooks: rules.md B-37 item 4; regression tests
|
|
||||||
# `unrecovered_cost_only_for_on_spec_and_never_negative` (ledger-core),
|
|
||||||
# `on_spec_project_shows_unrecovered_cost_and_others_null` (ledger-mcp).
|
|
||||||
#
|
|
||||||
# Expected values worked by hand: 1,400.00 + 60.00 = 1,460.00 of costs,
|
|
||||||
# nothing receivable (1100 stays 0.00). Repaid 1,000.00: 460.00 still
|
|
||||||
# unrecovered. Repaid 700.00 more: 1,700.00 in, 240.00 net, nothing
|
|
||||||
# unrecovered (never below zero).
|
|
||||||
#
|
|
||||||
# Both sources are quoted through Deloitte's DART roadmaps and not yet
|
|
||||||
# re-read in the ASC basic view (wanderbooks docs/spec/sources/fasb-asc.md):
|
|
||||||
# an open item.
|
|
||||||
|
|
||||||
id = "projects-on-spec-costs-are-expenses"
|
|
||||||
title = "On-spec costs are expenses; a repayment is income when received"
|
|
||||||
rule = "The costs of work done on spec MUST be expensed as incurred, and a payment hoped for MUST NOT be recorded, as a receivable or as income, until it is received; a project's unrecovered cost MUST be its tagged expenses less its tagged revenue to date, never below zero."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"ASC 450-30-25-1: a contingency that might result in a gain usually is not reflected in the financial statements, since that might recognize revenue before its realization (originally FAS 5 ¶17(a))",
|
|
||||||
"ASC 340-40-25-5: costs to fulfill an anticipated contract are an asset only if, among other criteria, they are expected to be recovered (here recovery is uncertain: expensed; wanderbooks docs/spec/rules.md B-22)",
|
|
||||||
]
|
|
||||||
tags = ["projects", "on-spec", "gain-contingency", "accrual"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1100"
|
|
||||||
name = "Accounts Receivable"
|
|
||||||
type = "asset"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Revenue"
|
|
||||||
type = "revenue"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Contractors"
|
|
||||||
type = "expense"
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6100"
|
|
||||||
name = "Hosting"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
[[setup.projects]]
|
|
||||||
name = "Harbor app"
|
|
||||||
kind = "on_spec"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-04-02"
|
|
||||||
description = "Prototype build"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1400.00", project = "Harbor app" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1400.00" },
|
|
||||||
]
|
|
||||||
[[events]]
|
|
||||||
date = "2026-04-20"
|
|
||||||
description = "Hosting for the prototype"
|
|
||||||
lines = [
|
|
||||||
{ account = "6100", side = "debit", amount = "60.00", project = "Harbor app" },
|
|
||||||
{ account = "1000", side = "credit", amount = "60.00" },
|
|
||||||
]
|
|
||||||
[[events]]
|
|
||||||
date = "2026-06-15"
|
|
||||||
description = "Harbor repays part of the prototype"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "1000.00", project = "Harbor app" },
|
|
||||||
]
|
|
||||||
[[events]]
|
|
||||||
date = "2026-07-01"
|
|
||||||
description = "Harbor pays the rest, and more"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "700.00" },
|
|
||||||
{ account = "4000", side = "credit", amount = "700.00", project = "Harbor app" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "project"
|
|
||||||
after = 2
|
|
||||||
project = "Harbor app"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-04-30"
|
|
||||||
[expect.values]
|
|
||||||
revenue = "0.00"
|
|
||||||
expenses = "1460.00"
|
|
||||||
net = "-1460.00"
|
|
||||||
"to_date.revenue" = "0.00"
|
|
||||||
"to_date.expenses" = "1460.00"
|
|
||||||
"to_date.net" = "-1460.00"
|
|
||||||
unrecovered_cost = "1460.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
after = 2
|
|
||||||
as_of = "2026-04-30"
|
|
||||||
[expect.values]
|
|
||||||
"1100" = "0.00"
|
|
||||||
"6000" = "1400.00"
|
|
||||||
"6100" = "60.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "project"
|
|
||||||
after = 3
|
|
||||||
project = "Harbor app"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-06-30"
|
|
||||||
[expect.values]
|
|
||||||
revenue = "1000.00"
|
|
||||||
expenses = "1460.00"
|
|
||||||
net = "-460.00"
|
|
||||||
"to_date.revenue" = "1000.00"
|
|
||||||
"to_date.expenses" = "1460.00"
|
|
||||||
"to_date.net" = "-460.00"
|
|
||||||
unrecovered_cost = "460.00"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "project"
|
|
||||||
project = "Harbor app"
|
|
||||||
from = "2026-01-01"
|
|
||||||
to = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
revenue = "1700.00"
|
|
||||||
expenses = "1460.00"
|
|
||||||
net = "240.00"
|
|
||||||
"to_date.revenue" = "1700.00"
|
|
||||||
"to_date.expenses" = "1460.00"
|
|
||||||
"to_date.net" = "240.00"
|
|
||||||
unrecovered_cost = "0.00"
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
# Renaming an account changes its label, never an amount.
|
|
||||||
#
|
|
||||||
# wanderbooks: P3 defect 15 (an account could not be renamed); rules.md
|
|
||||||
# B-8, B-31; regression tests `rename_account_changes_only_the_name_defect_15`
|
|
||||||
# and `prop_rename_changes_no_figure` (ledger-store tests/store.rs),
|
|
||||||
# `rename_account_end_to_end_defect_15` (ledger-cli tests/mcp_journeys.rs).
|
|
||||||
# The journal's record of the old name beside the new (`account.renamed`)
|
|
||||||
# is the store's, checked there.
|
|
||||||
#
|
|
||||||
# Expected values (bookspec/README.md): the same trial balance before the
|
|
||||||
# rename (`after = 2`) and after it; `account_names` gives each account's
|
|
||||||
# name as every report prints it, past periods included; each
|
|
||||||
# `rename_account` is tried on a copy of the books, after the rename.
|
|
||||||
|
|
||||||
id = "rename-changes-no-amount"
|
|
||||||
title = "Renaming an account changes its label, never an amount"
|
|
||||||
rule = "Entries name accounts by identity, not by name, so renaming an account MUST change no posted entry, line, balance or statement amount, and every report, past periods included, MUST print the new name. A new name MUST be refused when it is blank, the name the account has, one another account under the same parent has (ignoring capitals and surrounding space), one that holds a run of nine digits, or not one line of at most 200 characters."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"Pacioli, Summa de arithmetica (1494), ch. 36: \"You should not write backward, but always forward\" (posted is forever: AUTHORITY; docs/spec/sources/pacioli-1494.md)",
|
|
||||||
"Rev. Proc. 98-25 §5.01: \"the relationship (i.e., audit trail): (a) between the total of the amounts in the taxpayer's machine-sensible records by account and the account totals in the taxpayer's books\" (an amount never moves with a name: AUTHORITY; docs/spec/sources/irs-records.md)",
|
|
||||||
"Intuit IIF Import Kit, !ACCNT: accounts are identified by NAME, so two siblings of one name would be one account in QuickBooks (CONVENTION; docs/spec/sources/intuit-iif.md)",
|
|
||||||
"DECISION, the owner, 2026-10-20 (wanderbooks plan/served.md P3, \"Settled in design\"): the name rules (wanderbooks docs/spec/rules.md B-31)",
|
|
||||||
]
|
|
||||||
tags = ["chart", "rename", "audit-trail", "posted-is-forever"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Operating"
|
|
||||||
type = "expense"
|
|
||||||
header = true
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6150"
|
|
||||||
name = "Software"
|
|
||||||
type = "expense"
|
|
||||||
parent = "6000"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6160"
|
|
||||||
name = "Hosting"
|
|
||||||
type = "expense"
|
|
||||||
parent = "6000"
|
|
||||||
|
|
||||||
# Entry 1.
|
|
||||||
[[events]]
|
|
||||||
date = "2025-11-04"
|
|
||||||
description = "Capital contributed"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "800.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "800.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 2: posted to 6150 under its old name.
|
|
||||||
[[events]]
|
|
||||||
date = "2025-11-20"
|
|
||||||
description = "Software"
|
|
||||||
lines = [
|
|
||||||
{ account = "6150", side = "debit", amount = "29.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "29.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# After entry 2, the owner renames 6150.
|
|
||||||
[[changes]]
|
|
||||||
after = 2
|
|
||||||
rename = { code = "6150", name = "Software subscriptions" }
|
|
||||||
|
|
||||||
# Before the rename.
|
|
||||||
[[expect]]
|
|
||||||
query = "account_names"
|
|
||||||
after = 2
|
|
||||||
values = { "1000" = "Cash", "3000" = "Capital", "6000" = "Operating", "6150" = "Software", "6160" = "Hosting" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2025-11-30"
|
|
||||||
after = 2
|
|
||||||
values = { "1000" = "771.00", "3000" = "-800.00", "6000" = "0.00", "6150" = "29.00", "6160" = "0.00" }
|
|
||||||
|
|
||||||
# After it: every amount as before; the new name, past periods included.
|
|
||||||
[[expect]]
|
|
||||||
query = "account_names"
|
|
||||||
values = { "1000" = "Cash", "3000" = "Capital", "6000" = "Operating", "6150" = "Software subscriptions", "6160" = "Hosting" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2025-11-30"
|
|
||||||
values = { "1000" = "771.00", "3000" = "-800.00", "6000" = "0.00", "6150" = "29.00", "6160" = "0.00" }
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "profit_and_loss"
|
|
||||||
from = "2025-11-01"
|
|
||||||
to = "2025-11-30"
|
|
||||||
values = { "6000" = "0.00", "6150" = "29.00", "6160" = "0.00", net_income = "-29.00" }
|
|
||||||
|
|
||||||
# Refused, changing nothing.
|
|
||||||
[[expect]]
|
|
||||||
query = "rename_account"
|
|
||||||
code = "6150"
|
|
||||||
name = " "
|
|
||||||
refused = "blank"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "rename_account"
|
|
||||||
code = "6150"
|
|
||||||
name = " Software subscriptions "
|
|
||||||
refused = "unchanged"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "rename_account"
|
|
||||||
code = "6150"
|
|
||||||
name = "HOSTING"
|
|
||||||
refused = "sibling_has_it"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "rename_account"
|
|
||||||
code = "6150"
|
|
||||||
name = "Card 123456789"
|
|
||||||
refused = "holds_a_tin"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "rename_account"
|
|
||||||
code = "6150"
|
|
||||||
name = "Software\nsubscriptions"
|
|
||||||
refused = "not_one_line"
|
|
||||||
|
|
||||||
# A name an account under another parent has is no sibling's: accepted,
|
|
||||||
# moving no amount.
|
|
||||||
[[expect]]
|
|
||||||
query = "rename_account"
|
|
||||||
code = "6150"
|
|
||||||
name = "Cash"
|
|
||||||
accepted = true
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
id = "schedule-daily-deferred-revenue"
|
|
||||||
title = "Deferred revenue is earned by the day, to the cent"
|
|
||||||
rule = "Revenue collected in advance for a service over time is recognized as each day of the service passes, one posting per calendar month, the last month taking what the others leave."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = ["ASC 606-10-25-23", "DECISION: plan/served.md D-11, D-37 (owner, 2026-10-20)"]
|
|
||||||
tags = ["schedules", "deferred-revenue", "daily", "p3-defect-9"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2400"
|
|
||||||
name = "Deferred Revenue"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Subscription Revenue"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
# 1,000.00 collected for service 2026-01-05 through 2026-04-04.
|
|
||||||
[[setup.schedules]]
|
|
||||||
id = "SUB"
|
|
||||||
kind = "deferred_revenue"
|
|
||||||
description = "Quarterly plan, a customer"
|
|
||||||
total = "1000.00"
|
|
||||||
start = "2026-01-05"
|
|
||||||
months = 3
|
|
||||||
convention = "daily"
|
|
||||||
balance_account = "2400"
|
|
||||||
pl_account = "4000"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-05"
|
|
||||||
description = "Collected a quarter up front"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1000.00" },
|
|
||||||
{ account = "2400", side = "credit", amount = "1000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_schedules = { through = "2026-04-30" }
|
|
||||||
|
|
||||||
# Hand check (worked without the code): January holds 27 of its 31 days
|
|
||||||
# (5-31); February and March are whole; April holds days 1-4 and takes the
|
|
||||||
# rest. Cumulative at each month end: 1,000 x (27/31)/3 = 290.3226 ->
|
|
||||||
# 290.32; 1,000 x (27/31 + 1)/3 = 623.6559 -> 623.66; 1,000 x (27/31 +
|
|
||||||
# 2)/3 = 956.9892 -> 956.99; then 1,000.00. Months: 290.32, 333.34,
|
|
||||||
# 333.33, 43.01.
|
|
||||||
[[expect]]
|
|
||||||
query = "schedule_plan"
|
|
||||||
schedule = "SUB"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"2026-01-31" = "290.32"
|
|
||||||
"2026-02-28" = "333.34"
|
|
||||||
"2026-03-31" = "333.33"
|
|
||||||
"2026-04-30" = "43.01"
|
|
||||||
|
|
||||||
# Earned through March: 956.99; still owed for April 1-4: 43.01.
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-03-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"2400" = "43.01"
|
|
||||||
"4000" = "956.99"
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
id = "schedule-daily-small-first-and-last-month"
|
|
||||||
title = "A prepaid expense spread by the day: a small first and last month around full ones"
|
|
||||||
rule = "A prepaid expense covering whole months from a mid-month start is expensed by the day: each full month an equal share, the first month its days' share, and the last month what the others leave, one posting per calendar month touched."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = ["ASC 340-10-05-4", "ASC 210-10-45-1(g)", "DECISION: plan/served.md D-11, D-37 (owner, 2026-10-20)"]
|
|
||||||
tags = ["schedules", "prepaid-expense", "daily", "p3-defect-9"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-away-from-zero"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Bank"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1300"
|
|
||||||
name = "Prepaid Expenses"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6300"
|
|
||||||
name = "Insurance"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# 1,200.00 of insurance covering 2026-03-15 through 2027-03-14.
|
|
||||||
[[setup.schedules]]
|
|
||||||
id = "INS"
|
|
||||||
kind = "prepaid_expense"
|
|
||||||
description = "Liability insurance"
|
|
||||||
total = "1200.00"
|
|
||||||
start = "2026-03-15"
|
|
||||||
months = 12
|
|
||||||
convention = "daily"
|
|
||||||
balance_account = "1300"
|
|
||||||
pl_account = "6300"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-15"
|
|
||||||
description = "Capital"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "1200.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "1200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-03-15"
|
|
||||||
description = "Paid a year of insurance"
|
|
||||||
lines = [
|
|
||||||
{ account = "1300", side = "debit", amount = "1200.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1200.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
run_schedules = { through = "2027-03-31" }
|
|
||||||
|
|
||||||
# Hand check (worked without the code): 1,200.00 / 12 = 100.00 a month.
|
|
||||||
# March 2026 holds 17 of its 31 days (15-31): 100.00 x 17/31 = 54.838...
|
|
||||||
# -> 54.84. April 2026 through February 2027 are whole: 100.00 each, 11 of
|
|
||||||
# them (cumulatives 154.84 ... 1,154.84). March 2027 holds days 1-14 and
|
|
||||||
# takes what is left: 1,200.00 - 1,154.84 = 45.16 (= 100.00 x 14/31 to the
|
|
||||||
# cent). Thirteen month ends for a twelve-month policy.
|
|
||||||
[[expect]]
|
|
||||||
query = "schedule_plan"
|
|
||||||
schedule = "INS"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"2026-03-31" = "54.84"
|
|
||||||
"2026-04-30" = "100.00"
|
|
||||||
"2026-05-31" = "100.00"
|
|
||||||
"2026-06-30" = "100.00"
|
|
||||||
"2026-07-31" = "100.00"
|
|
||||||
"2026-08-31" = "100.00"
|
|
||||||
"2026-09-30" = "100.00"
|
|
||||||
"2026-10-31" = "100.00"
|
|
||||||
"2026-11-30" = "100.00"
|
|
||||||
"2026-12-31" = "100.00"
|
|
||||||
"2027-01-31" = "100.00"
|
|
||||||
"2027-02-28" = "100.00"
|
|
||||||
"2027-03-31" = "45.16"
|
|
||||||
|
|
||||||
# Through June 2026 the books have expensed 54.84 + 3 x 100.00 = 354.84,
|
|
||||||
# and 845.16 is still prepaid.
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-06-30"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1300" = "845.16"
|
|
||||||
"6300" = "354.84"
|
|
||||||
|
|
||||||
# Through March 2027, all of it.
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2027-03-31"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"1300" = "0.00"
|
|
||||||
"6300" = "1200.00"
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
id = "schedule-equal-months-start-on-the-first"
|
|
||||||
title = "An equal-months schedule starts on the 1st of a month"
|
|
||||||
rule = "A schedule that spreads its total in equal months must start on the 1st of a month, for prepaid expenses and deferred revenue alike, so it never recognizes days it does not cover."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = ["ASC 606-10-25-23", "DECISION: plan/served.md D-37 (owner, 2026-10-20)"]
|
|
||||||
tags = ["schedules", "monthly-even", "refusal", "p3-defect-9"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
# Equal months keep the rounding schedules had before P3 (each month the
|
|
||||||
# total over the months to the cent, ties to even; the last month takes
|
|
||||||
# what the others leave): a posted month's replay re-derives its plan.
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1300"
|
|
||||||
name = "Prepaid Expenses"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2400"
|
|
||||||
name = "Deferred Revenue"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Subscription Revenue"
|
|
||||||
type = "revenue"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6300"
|
|
||||||
name = "Insurance"
|
|
||||||
type = "expense"
|
|
||||||
|
|
||||||
# Equal months from March 15: refused (it would recognize all of March's
|
|
||||||
# 100.00, 45.16 of it, March 1 to 14 at 100.00 × 14/31, before the service
|
|
||||||
# began). The refusal names the 1st.
|
|
||||||
[[events]]
|
|
||||||
register_schedule = { id = "MID", kind = "deferred_revenue", description = "Annual plan", total = "1200.00", start = "2026-03-15", months = 12, convention = "monthly_even", balance_account = "2400", pl_account = "4000" }
|
|
||||||
refused = "invalid_schedule"
|
|
||||||
|
|
||||||
# The same for a prepaid expense: one rule for both.
|
|
||||||
[[events]]
|
|
||||||
register_schedule = { id = "MIDP", kind = "prepaid_expense", description = "Insurance", total = "1200.00", start = "2026-03-15", months = 12, convention = "monthly_even", balance_account = "1300", pl_account = "6300" }
|
|
||||||
refused = "invalid_schedule"
|
|
||||||
|
|
||||||
# From the 1st it is taken: twelve equal months.
|
|
||||||
[[events]]
|
|
||||||
register_schedule = { id = "FIRST", kind = "deferred_revenue", description = "Annual plan", total = "1200.00", start = "2026-03-01", months = 12, convention = "monthly_even", balance_account = "2400", pl_account = "4000" }
|
|
||||||
|
|
||||||
# Hand check (worked without the code): 1,200.00 / 12 = 100.00, every
|
|
||||||
# month, the last absorbing no rounding.
|
|
||||||
[[expect]]
|
|
||||||
query = "schedule_plan"
|
|
||||||
schedule = "FIRST"
|
|
||||||
|
|
||||||
[expect.values]
|
|
||||||
"2026-03-31" = "100.00"
|
|
||||||
"2026-09-30" = "100.00"
|
|
||||||
"2027-02-28" = "100.00"
|
|
||||||
total = "1200.00"
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
id = "statement-the-books-balance-and-the-equation-holds"
|
|
||||||
title = "Normal balances, the trial balance and the accounting equation on one small book"
|
|
||||||
rule = "An account MUST show its balance on its normal side (assets and expenses debit, liabilities, equity and revenue credit); the trial balance's debits MUST equal its credits; and assets MUST equal liabilities plus equity plus revenue less expenses."
|
|
||||||
status = "AUTHORITY"
|
|
||||||
sources = [
|
|
||||||
"Pacioli, Summa (1494), ch. 36: the total of the credits equals the total of the debits (docs/spec/sources/pacioli-1494.md)",
|
|
||||||
"FASB Concepts Statement 8, Ch. 4, E62: equity \"is the difference between the entity's assets and its liabilities\" (docs/spec/sources/fasb-asc.md)",
|
|
||||||
"OpenStax, Principles of Accounting, Vol. 1, 3.1: normal balances (CONVENTION; docs/spec/rules.md B-5)",
|
|
||||||
]
|
|
||||||
tags = ["accrual", "book", "us", "statement", "equation"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "2000"
|
|
||||||
name = "Loan"
|
|
||||||
type = "liability"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "3000"
|
|
||||||
name = "Capital"
|
|
||||||
type = "equity"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
tax_category = "gross_receipts"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Rent"
|
|
||||||
type = "expense"
|
|
||||||
tax_category = "rent_property"
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-02"
|
|
||||||
description = "Members put in cash"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "5000.00" },
|
|
||||||
{ account = "3000", side = "credit", amount = "5000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-03"
|
|
||||||
description = "A loan"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "2000.00" },
|
|
||||||
{ account = "2000", side = "credit", amount = "2000.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-10"
|
|
||||||
description = "A sale"
|
|
||||||
lines = [
|
|
||||||
{ account = "1000", side = "debit", amount = "750.50" },
|
|
||||||
{ account = "4000", side = "credit", amount = "750.50" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[events]]
|
|
||||||
date = "2026-01-15"
|
|
||||||
description = "Rent"
|
|
||||||
lines = [
|
|
||||||
{ account = "6000", side = "debit", amount = "1200.25" },
|
|
||||||
{ account = "1000", side = "credit", amount = "1200.25" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# By hand. Cash: 5000.00 + 2000.00 + 750.50 - 1200.25 = 6550.25.
|
|
||||||
# Debit side: cash 6550.25 + rent 1200.25 = 7750.50.
|
|
||||||
# Credit side: loan 2000.00 + capital 5000.00 + sales 750.50 = 7750.50.
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "6550.25"
|
|
||||||
"2000" = "-2000.00"
|
|
||||||
"3000" = "-5000.00"
|
|
||||||
"4000" = "-750.50"
|
|
||||||
"6000" = "1200.25"
|
|
||||||
|
|
||||||
# Each on its normal side, all positive here; the equation:
|
|
||||||
# assets 6550.25 = liabilities 2000.00 + equity 5000.00
|
|
||||||
# + (revenue 750.50 - expenses 1200.25) = 6550.25.
|
|
||||||
[[expect]]
|
|
||||||
query = "balances"
|
|
||||||
as_of = "2026-12-31"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "6550.25"
|
|
||||||
"2000" = "2000.00"
|
|
||||||
"3000" = "5000.00"
|
|
||||||
"4000" = "750.50"
|
|
||||||
"6000" = "1200.25"
|
|
||||||
|
|
||||||
# The trial balance as of an earlier day balances too: on January 9 there
|
|
||||||
# is no sale and no rent.
|
|
||||||
[[expect]]
|
|
||||||
query = "trial_balance"
|
|
||||||
as_of = "2026-01-09"
|
|
||||||
[expect.values]
|
|
||||||
"1000" = "7000.00"
|
|
||||||
"2000" = "-2000.00"
|
|
||||||
"3000" = "-5000.00"
|
|
||||||
"4000" = "0.00"
|
|
||||||
"6000" = "0.00"
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# Every revenue and expense account maps to a line of the return.
|
|
||||||
#
|
|
||||||
# wanderbooks: P3 defect 8 (an account made without a tax category went
|
|
||||||
# unnoticed); rules.md B-32; regression tests
|
|
||||||
# `add_account_warns_without_tax_category_defect_8` and
|
|
||||||
# `require_tax_category_refuses_defect_8` (ledger-cli
|
|
||||||
# tests/spec_tax_policy.rs), `prop_tax_policy_refuses_exactly` (ledger-store
|
|
||||||
# tests/store.rs), `needs_attention_flags_every_uncategorized_pl_account_defect_8`
|
|
||||||
# (ledger-gui tests/view_model.rs).
|
|
||||||
#
|
|
||||||
# Expected values (bookspec/README.md): `uncategorized_accounts` lists
|
|
||||||
# every account that needs a category and has none, by code; each command
|
|
||||||
# is tried on a copy of the books under the policy in force when it is
|
|
||||||
# asked: `warn` (the default) after entry 1, `refuse` from then on.
|
|
||||||
|
|
||||||
id = "tax-category-required"
|
|
||||||
title = "Every revenue and expense account maps to a line of the return"
|
|
||||||
rule = "A revenue or expense account that is not a header MUST have a tax category, its link to a line of the income tax return, and every such account without one MUST be listed for the human, with activity or not. By default an account created without one is created (and listed); a company MAY refuse to create such an account without one, or to clear one it has. The accounts the books create for a bill, an invoice or a payroll run MUST NOT be refused: they are listed instead."
|
|
||||||
status = "CHOICE"
|
|
||||||
sources = [
|
|
||||||
"IRS Publication 583 (Rev. December 2024): \"Your books must show your gross income, as well as your deductions and credits.\" (why it matters: AUTHORITY; docs/spec/sources/irs-records.md)",
|
|
||||||
"Rev. Proc. 98-25 §5.01: the relationship \"between the total of the amounts in the taxpayer's machine-sensible records by account and the taxpayer's return\" (AUTHORITY; docs/spec/sources/irs-records.md)",
|
|
||||||
"IRS Publication 583: \"Except in a few cases, the law does not require any specific kind of records.\" (so warn or refuse is the company's choice; docs/spec/sources/irs-records.md)",
|
|
||||||
"DECISION, the owner, 2026-10-20 (wanderbooks plan/served.md P3, \"Settled in design\"): warn by default, header accounts exempt (wanderbooks docs/spec/rules.md B-32)",
|
|
||||||
]
|
|
||||||
tags = ["tax", "chart", "policy"]
|
|
||||||
|
|
||||||
[setup]
|
|
||||||
currency = "USD"
|
|
||||||
fiscal_year_start = "01-01"
|
|
||||||
basis = "accrual"
|
|
||||||
rounding = "half-even"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "1000"
|
|
||||||
name = "Cash"
|
|
||||||
type = "asset"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "4000"
|
|
||||||
name = "Sales"
|
|
||||||
type = "revenue"
|
|
||||||
tax_category = "gross_receipts"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6000"
|
|
||||||
name = "Operating"
|
|
||||||
type = "expense"
|
|
||||||
header = true
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6150"
|
|
||||||
name = "Software"
|
|
||||||
type = "expense"
|
|
||||||
parent = "6000"
|
|
||||||
|
|
||||||
[[setup.accounts]]
|
|
||||||
code = "6160"
|
|
||||||
name = "Hosting"
|
|
||||||
type = "expense"
|
|
||||||
parent = "6000"
|
|
||||||
|
|
||||||
# Entry 1: Hosting used in the year before; Software never.
|
|
||||||
[[events]]
|
|
||||||
date = "2025-06-01"
|
|
||||||
description = "Hosting"
|
|
||||||
lines = [
|
|
||||||
{ account = "6160", side = "debit", amount = "12.00" },
|
|
||||||
{ account = "1000", side = "credit", amount = "12.00" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Entry 2, under refuse: a bill, whose expense account (General Expenses,
|
|
||||||
# 6999) the books create without a category, never refused.
|
|
||||||
[[events]]
|
|
||||||
date = "2026-02-01"
|
|
||||||
description = "Hosting, billed"
|
|
||||||
bill = { vendor = "Example Hosting Co", amount = "42.00" }
|
|
||||||
|
|
||||||
# After entry 1 (and its expectations), the company turns to refuse.
|
|
||||||
[[changes]]
|
|
||||||
after = 1
|
|
||||||
require_tax_category = "refuse"
|
|
||||||
|
|
||||||
# Under warn: every account without a category listed, active or not
|
|
||||||
# (the header needs none); one created without one is created.
|
|
||||||
[[expect]]
|
|
||||||
query = "uncategorized_accounts"
|
|
||||||
after = 1
|
|
||||||
values = ["6150 Software", "6160 Hosting"]
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "add_account"
|
|
||||||
after = 1
|
|
||||||
account = { code = "6170", name = "Domains", type = "expense" }
|
|
||||||
accepted = true
|
|
||||||
|
|
||||||
# Under refuse: refused without one, created with one; a header needs
|
|
||||||
# none; a needed category can't be cleared, but can be set; the accounts
|
|
||||||
# made earlier stay as they are, and the bill's is listed.
|
|
||||||
[[expect]]
|
|
||||||
query = "add_account"
|
|
||||||
account = { code = "6180", name = "Postage", type = "expense" }
|
|
||||||
refused = "tax_category_required"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "add_account"
|
|
||||||
account = { code = "6180", name = "Postage", type = "expense", tax_category = "office_expense" }
|
|
||||||
accepted = true
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "add_account"
|
|
||||||
account = { code = "6900", name = "Other", type = "expense", header = true }
|
|
||||||
accepted = true
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "set_tax_category"
|
|
||||||
code = "4000"
|
|
||||||
refused = "tax_category_required"
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "set_tax_category"
|
|
||||||
code = "6150"
|
|
||||||
category = "other_deductions"
|
|
||||||
accepted = true
|
|
||||||
|
|
||||||
[[expect]]
|
|
||||||
query = "uncategorized_accounts"
|
|
||||||
values = ["6150 Software", "6160 Hosting", "6999 General Expenses"]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "bookspec-gnucash"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
description = "A second engine for bookspec: GnuCash, driven through the oracle container's files."
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bookspec-checker = { path = "../checker" }
|
|
||||||
serde_json = "1"
|
|
||||||
rust_decimal = "1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
bookspec-runner = { path = "../runner" }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
//! GnuCash as a second bookspec engine (`plan/served.md` P13 step 5).
|
|
||||||
//!
|
|
||||||
//! The adapter keeps an example's chart and entries as the oracle's JSON
|
|
||||||
//! books (`oracle/gnucash/books.py`: accounts, entries, as-of days) and
|
|
||||||
//! asks GnuCash 5.10, in its pinned container, for each balance. GnuCash
|
|
||||||
//! is a cross-check, never the authority (`docs/oracle.md`, "Who is
|
|
||||||
//! right"): where it differs from an example, the difference is
|
|
||||||
//! adjudicated from the sources and recorded, not conformed to.
|
|
||||||
//!
|
|
||||||
//! Only what GnuCash itself answers is claimed: the balance queries
|
|
||||||
//! (`trial_balance`, `balances`), by the rules whose examples ask nothing
|
|
||||||
//! else. GnuCash never refuses an entry (it books the difference to an
|
|
||||||
//! `Imbalance` account), so a refused event is *accepted* here, as it
|
|
||||||
//! would be by a GnuCash user, and the runner reports the difference.
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use bookspec_checker::{
|
|
||||||
Area, ChangeSpec, Claims, Engine, Event, Query, Refusal, RuleId, Setup, fallback_refusal,
|
|
||||||
};
|
|
||||||
use rust_decimal::Decimal;
|
|
||||||
use serde_json::{Value, json};
|
|
||||||
|
|
||||||
/// The oracle container (`just oracle` builds it from `oracle/gnucash`).
|
|
||||||
pub const IMAGE: &str = "wanderbooks-gnucash-oracle";
|
|
||||||
|
|
||||||
/// The rules this adapter claims: those whose examples ask only for
|
|
||||||
/// balances, which GnuCash answers itself.
|
|
||||||
pub fn claimed() -> Vec<RuleId> {
|
|
||||||
vec![
|
|
||||||
RuleId::new(Area::Entry, 1),
|
|
||||||
RuleId::new(Area::Entry, 2),
|
|
||||||
RuleId::new(Area::Entry, 3),
|
|
||||||
RuleId::new(Area::Ledger, 1),
|
|
||||||
RuleId::new(Area::Ledger, 3),
|
|
||||||
RuleId::new(Area::Statement, 1),
|
|
||||||
RuleId::new(Area::Statement, 2),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One account of the book handed to GnuCash.
|
|
||||||
struct Account {
|
|
||||||
code: String,
|
|
||||||
kind: &'static str,
|
|
||||||
normal_debit: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GnuCash, as bookspec's `Engine`.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct GnuCash {
|
|
||||||
accounts: Vec<Account>,
|
|
||||||
entries: Vec<Value>,
|
|
||||||
/// Each event's entry, by event number (1-based), for reversals.
|
|
||||||
event_entries: BTreeMap<usize, usize>,
|
|
||||||
events: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GnuCash {
|
|
||||||
/// An empty engine.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self, code: &str) -> Option<usize> {
|
|
||||||
self.accounts
|
|
||||||
.iter()
|
|
||||||
.position(|a| a.code == code)
|
|
||||||
.map(|i| i + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn book(&self, as_of: &str) -> Value {
|
|
||||||
let accounts: Vec<Value> = self
|
|
||||||
.accounts
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, a)| json!({"id": i + 1, "code": a.code, "name": a.code, "type": a.kind}))
|
|
||||||
.collect();
|
|
||||||
json!({"accounts": accounts, "entries": self.entries, "as_of": [as_of]})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GnuCash's own balance of every account as of a day: debits
|
|
||||||
/// positive, exact decimal strings, by account code.
|
|
||||||
fn balances(&self, as_of: &str) -> Result<BTreeMap<String, Decimal>, Refusal> {
|
|
||||||
let mut child = Command::new("docker")
|
|
||||||
.args(["run", "--rm", "-i", IMAGE])
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| unavailable(format!("docker: {e}")))?;
|
|
||||||
let books = json!([self.book(as_of)]);
|
|
||||||
child
|
|
||||||
.stdin
|
|
||||||
.take()
|
|
||||||
.unwrap()
|
|
||||||
.write_all(books.to_string().as_bytes())
|
|
||||||
.map_err(|e| unavailable(format!("docker stdin: {e}")))?;
|
|
||||||
let out = child
|
|
||||||
.wait_with_output()
|
|
||||||
.map_err(|e| unavailable(format!("docker: {e}")))?;
|
|
||||||
if !out.status.success() {
|
|
||||||
return Err(unavailable(
|
|
||||||
"the GnuCash oracle failed; build it with `just oracle`".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let results: Value = serde_json::from_slice(&out.stdout)
|
|
||||||
.map_err(|e| unavailable(format!("oracle output: {e}")))?;
|
|
||||||
let at = &results[0]["balances"][as_of];
|
|
||||||
let mut map = BTreeMap::new();
|
|
||||||
for (i, a) in self.accounts.iter().enumerate() {
|
|
||||||
let v = at[(i + 1).to_string()]
|
|
||||||
.as_str()
|
|
||||||
.ok_or_else(|| unavailable(format!("no balance for {}", a.code)))?;
|
|
||||||
map.insert(
|
|
||||||
a.code.clone(),
|
|
||||||
Decimal::from_str(v).map_err(|e| unavailable(e.to_string()))?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unavailable(why: String) -> Refusal {
|
|
||||||
fallback_refusal(
|
|
||||||
RuleId::new(Area::Ledger, 1),
|
|
||||||
format!("the GnuCash oracle: {why}"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn not_supported(what: &str) -> Refusal {
|
|
||||||
fallback_refusal(
|
|
||||||
RuleId::new(Area::Ledger, 1),
|
|
||||||
format!("the GnuCash adapter does not take {what}"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Engine for GnuCash {
|
|
||||||
fn load(&mut self, setup: &Setup) -> Result<(), Refusal> {
|
|
||||||
for a in &setup.accounts {
|
|
||||||
let (kind, normal_debit) = match a.kind.as_str() {
|
|
||||||
"asset" => ("Asset", true),
|
|
||||||
"liability" => ("Liability", false),
|
|
||||||
"equity" => ("Equity", false),
|
|
||||||
"revenue" => ("Revenue", false),
|
|
||||||
"expense" => ("Expense", true),
|
|
||||||
other => return Err(not_supported(&format!("account type {other}"))),
|
|
||||||
};
|
|
||||||
self.accounts.push(Account {
|
|
||||||
code: a.code.clone(),
|
|
||||||
kind,
|
|
||||||
normal_debit,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if setup.currency != "USD" {
|
|
||||||
return Err(not_supported("a currency other than USD"));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply(&mut self, event: &Event) -> Result<(), Refusal> {
|
|
||||||
self.events += 1;
|
|
||||||
let n = self.events;
|
|
||||||
let date = event
|
|
||||||
.date
|
|
||||||
.clone()
|
|
||||||
.ok_or_else(|| not_supported("an undated event"))?;
|
|
||||||
let description = event.description.clone().unwrap_or_default();
|
|
||||||
if !event.lines.is_empty() {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
for l in &event.lines {
|
|
||||||
let account = self
|
|
||||||
.id(&l.account)
|
|
||||||
.ok_or_else(|| not_supported("an unknown account"))?;
|
|
||||||
lines.push(json!({"account": account, "side": l.side, "amount": l.amount}));
|
|
||||||
}
|
|
||||||
self.entries
|
|
||||||
.push(json!({"date": date, "description": description, "lines": lines}));
|
|
||||||
self.event_entries.insert(n, self.entries.len() - 1);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if let Some(of) = event.reverses {
|
|
||||||
// GnuCash has no reversal link; a user reverses by writing the
|
|
||||||
// opposite transaction, which is what is done here.
|
|
||||||
let idx = *self
|
|
||||||
.event_entries
|
|
||||||
.get(&of)
|
|
||||||
.ok_or_else(|| not_supported("a reversal of an event that posted nothing"))?;
|
|
||||||
let original = self.entries[idx].clone();
|
|
||||||
let flipped: Vec<Value> = original["lines"]
|
|
||||||
.as_array()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.map(|l| {
|
|
||||||
let side = if l["side"] == "debit" {
|
|
||||||
"credit"
|
|
||||||
} else {
|
|
||||||
"debit"
|
|
||||||
};
|
|
||||||
json!({"account": l["account"], "side": side, "amount": l["amount"]})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
self.entries
|
|
||||||
.push(json!({"date": date, "description": description, "lines": flipped}));
|
|
||||||
self.event_entries.insert(n, self.entries.len() - 1);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err(not_supported("this kind of event"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn query(&self, q: &Query) -> Result<Value, Refusal> {
|
|
||||||
let as_of = q
|
|
||||||
.as_of
|
|
||||||
.clone()
|
|
||||||
.ok_or_else(|| not_supported("a query without as_of"))?;
|
|
||||||
match q.name.as_str() {
|
|
||||||
"trial_balance" | "balances" => {
|
|
||||||
let balances = self.balances(&as_of)?;
|
|
||||||
let mut out = serde_json::Map::new();
|
|
||||||
for a in &self.accounts {
|
|
||||||
let b = balances[&a.code];
|
|
||||||
let mut shown = if q.name == "balances" && !a.normal_debit {
|
|
||||||
-b
|
|
||||||
} else {
|
|
||||||
b
|
|
||||||
};
|
|
||||||
if shown.is_zero() {
|
|
||||||
shown = Decimal::ZERO;
|
|
||||||
}
|
|
||||||
if shown.scale() < 2 {
|
|
||||||
shown.rescale(2);
|
|
||||||
}
|
|
||||||
out.insert(a.code.clone(), Value::String(shown.to_string()));
|
|
||||||
}
|
|
||||||
Ok(Value::Object(out))
|
|
||||||
}
|
|
||||||
other => Err(not_supported(&format!("the {other} query"))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn claims(&self) -> Claims {
|
|
||||||
Claims::new(claimed())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change(&mut self, _change: &ChangeSpec) -> Result<(), Refusal> {
|
|
||||||
Err(not_supported("a change"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
//! GnuCash, run through bookspec's examples (`just oracle` builds the
|
|
||||||
//! container). Needs Docker, so the test is ignored by default:
|
|
||||||
//! `cargo test -p bookspec-gnucash -- --ignored --nocapture`.
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use bookspec_checker::{Example, rules};
|
|
||||||
use bookspec_gnucash::{GnuCash, claimed};
|
|
||||||
|
|
||||||
fn examples() -> Vec<Example> {
|
|
||||||
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples");
|
|
||||||
let mut files: Vec<PathBuf> = std::fs::read_dir(dir)
|
|
||||||
.unwrap()
|
|
||||||
.map(|e| e.unwrap().path())
|
|
||||||
.filter(|p| p.extension().is_some_and(|x| x == "toml"))
|
|
||||||
.collect();
|
|
||||||
files.sort();
|
|
||||||
files
|
|
||||||
.iter()
|
|
||||||
.map(|f| Example::from_toml(&std::fs::read_to_string(f).unwrap()).unwrap())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The differences GnuCash shows, each settled from the sources
|
|
||||||
/// (`bookspec/README.md`, "GnuCash, the second engine"). A new difference,
|
|
||||||
/// or one that goes away, fails this test until it is adjudicated again.
|
|
||||||
const ADJUDICATED: [(&str, &str); 3] = [
|
|
||||||
(
|
|
||||||
"entry-an-unbalanced-entry-is-refused",
|
|
||||||
"event 2: accepted, expected refused: unbalanced",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"entry-a-line-amount-is-positive",
|
|
||||||
"event 1: accepted, expected refused: negative_amount",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"ledger-a-fraction-of-a-cent-is-refused",
|
|
||||||
"event 1: accepted, expected refused: finer_than_minor_unit",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
fn run_claimed() -> bookspec_runner::Report {
|
|
||||||
let claimed = claimed();
|
|
||||||
let mut wanted = std::collections::BTreeSet::new();
|
|
||||||
for r in rules().iter().filter(|r| claimed.contains(&r.id)) {
|
|
||||||
wanted.extend(r.examples.iter().copied());
|
|
||||||
}
|
|
||||||
let examples: Vec<Example> = examples()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|e| wanted.contains(e.id.as_str()))
|
|
||||||
.collect();
|
|
||||||
bookspec_runner::run(&examples, rules(), |_| GnuCash::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "needs Docker and the GnuCash image: `just oracle`"]
|
|
||||||
fn gnucash_differs_only_where_it_was_adjudicated() {
|
|
||||||
let report = run_claimed();
|
|
||||||
let found: Vec<(&str, &str)> = report
|
|
||||||
.examples
|
|
||||||
.iter()
|
|
||||||
.filter_map(|e| e.failure.as_deref().map(|f| (e.id.as_str(), f)))
|
|
||||||
.collect();
|
|
||||||
let mut want: Vec<(&str, &str)> = ADJUDICATED.to_vec();
|
|
||||||
want.sort();
|
|
||||||
let mut got = found.clone();
|
|
||||||
got.sort();
|
|
||||||
assert_eq!(got, want, "GnuCash's differences changed: adjudicate again");
|
|
||||||
// The rules that every example passes are passed.
|
|
||||||
for rule in [
|
|
||||||
"BS-entry-3",
|
|
||||||
"BS-ledger-3",
|
|
||||||
"BS-statement-1",
|
|
||||||
"BS-statement-2",
|
|
||||||
] {
|
|
||||||
let id = bookspec_checker::RuleId::parse(rule).unwrap();
|
|
||||||
assert_eq!(report.rules[&id], bookspec_runner::Outcome::Pass, "{rule}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The evidence the adjudications rest on, from GnuCash's own answers:
|
|
||||||
/// what it did with the entry it accepted.
|
|
||||||
#[test]
|
|
||||||
#[ignore = "needs Docker and the GnuCash image: `just oracle`"]
|
|
||||||
fn what_gnucash_did_with_the_entries_the_specification_refuses() {
|
|
||||||
use bookspec_checker::Engine;
|
|
||||||
use std::str::FromStr;
|
|
||||||
let by_id = |id: &str| examples().into_iter().find(|e| e.id == id).unwrap();
|
|
||||||
let net = |id: &str, event: usize| -> rust_decimal::Decimal {
|
|
||||||
let ex = by_id(id);
|
|
||||||
let mut g = GnuCash::new();
|
|
||||||
g.load(&ex.setup).unwrap();
|
|
||||||
for e in ex.events.iter().take(event) {
|
|
||||||
g.apply(e).expect("GnuCash accepts it");
|
|
||||||
}
|
|
||||||
let q = ex
|
|
||||||
.expect
|
|
||||||
.iter()
|
|
||||||
.find(|x| x.query == "trial_balance")
|
|
||||||
.unwrap()
|
|
||||||
.query();
|
|
||||||
let answer = g.query(&q).unwrap();
|
|
||||||
answer
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.values()
|
|
||||||
.map(|v| rust_decimal::Decimal::from_str(v.as_str().unwrap()).unwrap())
|
|
||||||
.sum()
|
|
||||||
};
|
|
||||||
// Debits 100.00 against credits 90.00: the accounts GnuCash lists no
|
|
||||||
// longer net to zero; ten dollars sit where GnuCash parked them, in an
|
|
||||||
// Imbalance account of its own making.
|
|
||||||
assert_eq!(
|
|
||||||
net("entry-an-unbalanced-entry-is-refused", 2).to_string(),
|
|
||||||
"10.00"
|
|
||||||
);
|
|
||||||
// A credit of minus 100.00 is booked as a debit of 100.00: both lines
|
|
||||||
// are debits, and 200.00 sits in the Imbalance account.
|
|
||||||
assert_eq!(
|
|
||||||
net("entry-a-line-amount-is-positive", 1).to_string(),
|
|
||||||
"200.00"
|
|
||||||
);
|
|
||||||
// 10.005 on each side is rounded to the cent, silently: GnuCash's USD
|
|
||||||
// balance reads 10.01 (the choice rules.md B-3 records the owner took
|
|
||||||
// the other way, after the first cross-check found it).
|
|
||||||
let ex = by_id("ledger-a-fraction-of-a-cent-is-refused");
|
|
||||||
let mut g = GnuCash::new();
|
|
||||||
g.load(&ex.setup).unwrap();
|
|
||||||
g.apply(&ex.events[0]).unwrap();
|
|
||||||
let q = ex
|
|
||||||
.expect
|
|
||||||
.iter()
|
|
||||||
.find(|x| x.query == "trial_balance")
|
|
||||||
.unwrap()
|
|
||||||
.query();
|
|
||||||
assert_eq!(g.query(&q).unwrap()["1000"], "10.01");
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.lake/
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import Bookspec
|
|
||||||
|
|
||||||
/-! Each of the five laws' axioms are printed, and must be only Lean's
|
|
||||||
standard three (`propext`, `Quot.sound`, `Classical.choice`) or fewer. No
|
|
||||||
`sorryAx`, nothing assumed. `bookspec/checker/tests/lean.rs` reads this
|
|
||||||
file's output shape; run it with `lake env lean Audit.lean`. -/
|
|
||||||
|
|
||||||
open Bookspec
|
|
||||||
|
|
||||||
#print axioms law_debits_equal_credits
|
|
||||||
#print axioms law_accounting_equation
|
|
||||||
#print axioms law_trial_balance
|
|
||||||
#print axioms law_net_income_articulates
|
|
||||||
#print axioms law_cash_flow_is_change_in_cash
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import Bookspec.Laws
|
|
||||||
import Bookspec.Ledger
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
/-!
|
|
||||||
# Lines, sides, and nets
|
|
||||||
|
|
||||||
The vocabulary of double entry. Money is `Int`: an exact count of the
|
|
||||||
smallest unit the books use. Any finite set of exact decimals (what the
|
|
||||||
engine stores) scales to integers without loss, and every law here is a
|
|
||||||
law of addition, so `Int` is a faithful model.
|
|
||||||
|
|
||||||
Mirrors `crates/ledger-core/src/kernel.rs` (`flip`, `signed`, `net`,
|
|
||||||
`totals`) and `account.rs` (`Side`, `AccountType`, `normal_side`).
|
|
||||||
-/
|
|
||||||
|
|
||||||
namespace Bookspec
|
|
||||||
|
|
||||||
/-- Debit or credit. -/
|
|
||||||
inductive Side where
|
|
||||||
| debit
|
|
||||||
| credit
|
|
||||||
deriving DecidableEq, Repr
|
|
||||||
|
|
||||||
/-- The other side (`kernel::flip`). -/
|
|
||||||
def Side.flip : Side → Side
|
|
||||||
| .debit => .credit
|
|
||||||
| .credit => .debit
|
|
||||||
|
|
||||||
@[simp] theorem Side.flip_flip (s : Side) : s.flip.flip = s := by
|
|
||||||
cases s <;> rfl
|
|
||||||
|
|
||||||
theorem Side.flip_ne (s : Side) : s.flip ≠ s := by
|
|
||||||
cases s <;> simp [Side.flip]
|
|
||||||
|
|
||||||
/-- The five account types. -/
|
|
||||||
inductive AccountType where
|
|
||||||
| asset
|
|
||||||
| liability
|
|
||||||
| equity
|
|
||||||
| revenue
|
|
||||||
| expense
|
|
||||||
deriving DecidableEq, Repr
|
|
||||||
|
|
||||||
/-- The side that increases an account (`AccountType::normal_side`). -/
|
|
||||||
def AccountType.normal : AccountType → Side
|
|
||||||
| .asset | .expense => .debit
|
|
||||||
| .liability | .equity | .revenue => .credit
|
|
||||||
|
|
||||||
/-- A line's signed amount: `+a` for a debit, `-a` for a credit
|
|
||||||
(`kernel::signed`). -/
|
|
||||||
def signed : Side → Int → Int
|
|
||||||
| .debit, a => a
|
|
||||||
| .credit, a => -a
|
|
||||||
|
|
||||||
@[simp] theorem signed_debit (a : Int) : signed .debit a = a := rfl
|
|
||||||
@[simp] theorem signed_credit (a : Int) : signed .credit a = -a := rfl
|
|
||||||
|
|
||||||
theorem signed_flip (s : Side) (a : Int) : signed s.flip a = -signed s a := by
|
|
||||||
cases s <;> simp [Side.flip]
|
|
||||||
|
|
||||||
/-- One journal line. -/
|
|
||||||
structure Line where
|
|
||||||
account : Nat
|
|
||||||
side : Side
|
|
||||||
amount : Int
|
|
||||||
deriving DecidableEq, Repr
|
|
||||||
|
|
||||||
/-- A line's signed amount. -/
|
|
||||||
def Line.signed (l : Line) : Int := Bookspec.signed l.side l.amount
|
|
||||||
|
|
||||||
/-- The net of some lines: debits minus credits (`kernel::net`). -/
|
|
||||||
def net : List Line → Int
|
|
||||||
| [] => 0
|
|
||||||
| l :: ls => l.signed + net ls
|
|
||||||
|
|
||||||
/-- Total debits. -/
|
|
||||||
def debits : List Line → Int
|
|
||||||
| [] => 0
|
|
||||||
| l :: ls => (if l.side = .debit then l.amount else 0) + debits ls
|
|
||||||
|
|
||||||
/-- Total credits. -/
|
|
||||||
def credits : List Line → Int
|
|
||||||
| [] => 0
|
|
||||||
| l :: ls => (if l.side = .credit then l.amount else 0) + credits ls
|
|
||||||
|
|
||||||
@[simp] theorem net_nil : net [] = 0 := rfl
|
|
||||||
@[simp] theorem net_cons (l : Line) (ls : List Line) : net (l :: ls) = l.signed + net ls := rfl
|
|
||||||
|
|
||||||
theorem net_append (xs ys : List Line) : net (xs ++ ys) = net xs + net ys := by
|
|
||||||
induction xs with
|
|
||||||
| nil => simp
|
|
||||||
| cons x xs ih => simp only [List.cons_append, net_cons, ih]; omega
|
|
||||||
|
|
||||||
/-- The net is debits minus credits. -/
|
|
||||||
theorem net_eq_debits_sub_credits (ls : List Line) : net ls = debits ls - credits ls := by
|
|
||||||
induction ls with
|
|
||||||
| nil => rfl
|
|
||||||
| cons l ls ih =>
|
|
||||||
obtain ⟨a, s, m⟩ := l
|
|
||||||
cases s <;> simp [net, debits, credits, Line.signed, signed, ih] <;> omega
|
|
||||||
|
|
||||||
/-- A sum over a list of account codes. -/
|
|
||||||
def sumOver (f : Nat → Int) : List Nat → Int
|
|
||||||
| [] => 0
|
|
||||||
| a :: as => f a + sumOver f as
|
|
||||||
|
|
||||||
@[simp] theorem sumOver_nil (f : Nat → Int) : sumOver f [] = 0 := rfl
|
|
||||||
@[simp] theorem sumOver_cons (f : Nat → Int) (a : Nat) (as : List Nat) :
|
|
||||||
sumOver f (a :: as) = f a + sumOver f as := rfl
|
|
||||||
|
|
||||||
theorem sumOver_add (f g : Nat → Int) (as : List Nat) :
|
|
||||||
sumOver (fun a => f a + g a) as = sumOver f as + sumOver g as := by
|
|
||||||
induction as with
|
|
||||||
| nil => rfl
|
|
||||||
| cons a as ih => simp only [sumOver_cons, ih]; omega
|
|
||||||
|
|
||||||
theorem sumOver_neg (f : Nat → Int) (as : List Nat) :
|
|
||||||
sumOver (fun a => -f a) as = -sumOver f as := by
|
|
||||||
induction as with
|
|
||||||
| nil => rfl
|
|
||||||
| cons a as ih => simp only [sumOver_cons, ih]; omega
|
|
||||||
|
|
||||||
theorem sumOver_zero (as : List Nat) : sumOver (fun _ => 0) as = 0 := by
|
|
||||||
induction as with
|
|
||||||
| nil => rfl
|
|
||||||
| cons a as ih => simp [ih]
|
|
||||||
|
|
||||||
theorem sumOver_congr (f g : Nat → Int) (as : List Nat) (h : ∀ a ∈ as, f a = g a) :
|
|
||||||
sumOver f as = sumOver g as := by
|
|
||||||
induction as with
|
|
||||||
| nil => rfl
|
|
||||||
| cons a as ih =>
|
|
||||||
simp only [sumOver_cons]
|
|
||||||
rw [h a (by simp), ih (fun b hb => h b (by simp [hb]))]
|
|
||||||
|
|
||||||
/-- No code repeats (a chart's codes are unique). -/
|
|
||||||
def Distinct : List Nat → Prop
|
|
||||||
| [] => True
|
|
||||||
| a :: as => a ∉ as ∧ Distinct as
|
|
||||||
|
|
||||||
/-- Summing an indicator over codes that do not include `k` gives zero. -/
|
|
||||||
theorem sumOver_indicator_absent (k : Nat) (x : Int) (as : List Nat) (h : k ∉ as) :
|
|
||||||
sumOver (fun a => if k = a then x else 0) as = 0 := by
|
|
||||||
induction as with
|
|
||||||
| nil => rfl
|
|
||||||
| cons a as ih =>
|
|
||||||
have hka : k ≠ a := fun e => h (e ▸ List.mem_cons_self ..)
|
|
||||||
have hk : k ∉ as := fun hm => h (List.mem_cons_of_mem _ hm)
|
|
||||||
simp [hka, ih hk]
|
|
||||||
|
|
||||||
/-- Summing an indicator over distinct codes that include `k` picks out
|
|
||||||
exactly one term. -/
|
|
||||||
theorem sumOver_indicator (k : Nat) (x : Int) (as : List Nat) (hd : Distinct as)
|
|
||||||
(hk : k ∈ as) : sumOver (fun a => if k = a then x else 0) as = x := by
|
|
||||||
induction as with
|
|
||||||
| nil => simp at hk
|
|
||||||
| cons a as ih =>
|
|
||||||
obtain ⟨ha, hd⟩ := hd
|
|
||||||
by_cases e : k = a
|
|
||||||
· subst e
|
|
||||||
simp [sumOver_indicator_absent k x as ha]
|
|
||||||
· have : k ∈ as := by
|
|
||||||
cases hk with
|
|
||||||
| head => exact absurd rfl e
|
|
||||||
| tail _ h => exact h
|
|
||||||
simp [e, ih hd this]
|
|
||||||
|
|
||||||
end Bookspec
|
|
||||||
@@ -1,451 +0,0 @@
|
|||||||
import Bookspec.Statements
|
|
||||||
|
|
||||||
/-!
|
|
||||||
# Closing a period, and splitting the net
|
|
||||||
|
|
||||||
Mirrors `kernel::closing_line`, `kernel::credit_line`, `kernel::plug_last`,
|
|
||||||
`periods::allocate`, and `Ledger::close_period_to` (income closing, net to
|
|
||||||
one equity account or split among members).
|
|
||||||
-/
|
|
||||||
|
|
||||||
namespace Bookspec
|
|
||||||
|
|
||||||
/-- The line that zeroes an account with movement `m`: the opposite side,
|
|
||||||
same amount; nothing for zero (`kernel::closing_line`). It is also the
|
|
||||||
line that credits `m` to an account (`kernel::credit_line`). -/
|
|
||||||
def closingLine (a : Nat) (m : Int) : List Line :=
|
|
||||||
if 0 < m then [⟨a, .credit, m⟩] else if m < 0 then [⟨a, .debit, -m⟩] else []
|
|
||||||
|
|
||||||
theorem net_closingLine (a : Nat) (m : Int) : net (closingLine a m) = -m := by
|
|
||||||
unfold closingLine
|
|
||||||
split
|
|
||||||
· simp [Line.signed]
|
|
||||||
· split <;> simp [Line.signed] <;> omega
|
|
||||||
|
|
||||||
theorem movement_closingLine (b a : Nat) (m : Int) :
|
|
||||||
movement b (closingLine a m) = if b = a then -m else 0 := by
|
|
||||||
by_cases e : b = a
|
|
||||||
· subst e
|
|
||||||
rcases (by omega : 0 < m ∨ m < 0 ∨ m = 0) with h | h | h
|
|
||||||
· simp [closingLine, h, Line.signed]
|
|
||||||
· have : ¬ 0 < m := by omega
|
|
||||||
simp [closingLine, h, this, Line.signed]
|
|
||||||
· simp [closingLine, h]
|
|
||||||
· have e' : ¬ a = b := fun x => e x.symm
|
|
||||||
unfold closingLine
|
|
||||||
split <;> (try split) <;> simp [e']
|
|
||||||
|
|
||||||
theorem closingLine_pos (a : Nat) (m : Int) : ∀ l ∈ closingLine a m, 0 < l.amount := by
|
|
||||||
unfold closingLine
|
|
||||||
split
|
|
||||||
· simp; omega
|
|
||||||
· split
|
|
||||||
· simp; omega
|
|
||||||
· simp
|
|
||||||
|
|
||||||
theorem closingLine_account (a : Nat) (m : Int) : ∀ l ∈ closingLine a m, l.account = a := by
|
|
||||||
unfold closingLine
|
|
||||||
split
|
|
||||||
· simp
|
|
||||||
· split <;> simp
|
|
||||||
|
|
||||||
theorem closingLine_eq_nil (a : Nat) (m : Int) : closingLine a m = [] ↔ m = 0 := by
|
|
||||||
unfold closingLine
|
|
||||||
split
|
|
||||||
· simp; omega
|
|
||||||
· split
|
|
||||||
· simp; omega
|
|
||||||
· simp; omega
|
|
||||||
|
|
||||||
/-- The closing lines for some accounts: one per account with a nonzero
|
|
||||||
movement. -/
|
|
||||||
def closings (as : List Nat) (ls : List Line) : List Line :=
|
|
||||||
match as with
|
|
||||||
| [] => []
|
|
||||||
| a :: as => closingLine a (movement a ls) ++ closings as ls
|
|
||||||
|
|
||||||
theorem net_closings (as : List Nat) (ls : List Line) :
|
|
||||||
net (closings as ls) = -sumOver (fun a => movement a ls) as := by
|
|
||||||
induction as with
|
|
||||||
| nil => rfl
|
|
||||||
| cons a as ih => simp only [closings, net_append, net_closingLine, ih, sumOver_cons]; omega
|
|
||||||
|
|
||||||
theorem movement_closings (b : Nat) (as : List Nat) (ls : List Line) (hd : Distinct as) :
|
|
||||||
movement b (closings as ls) = if b ∈ as then -movement b ls else 0 := by
|
|
||||||
induction as with
|
|
||||||
| nil => simp [closings]
|
|
||||||
| cons a as ih =>
|
|
||||||
obtain ⟨ha, hd⟩ := hd
|
|
||||||
simp only [closings, movement_append, movement_closingLine, ih hd]
|
|
||||||
by_cases e : b = a
|
|
||||||
· subst e; simp [ha]
|
|
||||||
· simp [e]
|
|
||||||
|
|
||||||
theorem closings_pos (as : List Nat) (ls : List Line) : ∀ l ∈ closings as ls, 0 < l.amount := by
|
|
||||||
induction as with
|
|
||||||
| nil => simp [closings]
|
|
||||||
| cons a as ih =>
|
|
||||||
intro l hl
|
|
||||||
simp only [closings, List.mem_append] at hl
|
|
||||||
cases hl with
|
|
||||||
| inl h => exact closingLine_pos _ _ l h
|
|
||||||
| inr h => exact ih l h
|
|
||||||
|
|
||||||
theorem closings_account (as : List Nat) (ls : List Line) : ∀ l ∈ closings as ls, l.account ∈ as := by
|
|
||||||
induction as with
|
|
||||||
| nil => simp [closings]
|
|
||||||
| cons a as ih =>
|
|
||||||
intro l hl
|
|
||||||
simp only [closings, List.mem_append] at hl
|
|
||||||
cases hl with
|
|
||||||
| inl h => rw [closingLine_account _ _ l h]; simp
|
|
||||||
| inr h => exact List.mem_cons_of_mem _ (ih l h)
|
|
||||||
|
|
||||||
/-- Nothing to close means every closed account was already at zero. -/
|
|
||||||
theorem closings_eq_nil (as : List Nat) (ls : List Line) (h : closings as ls = []) :
|
|
||||||
∀ a ∈ as, movement a ls = 0 := by
|
|
||||||
induction as with
|
|
||||||
| nil => simp
|
|
||||||
| cons a as ih =>
|
|
||||||
simp only [closings, List.append_eq_nil_iff] at h
|
|
||||||
intro b hb
|
|
||||||
cases hb with
|
|
||||||
| head => exact (closingLine_eq_nil _ _).1 h.1
|
|
||||||
| tail _ hb => exact ih h.2 b hb
|
|
||||||
|
|
||||||
/-! ## The net to equity -/
|
|
||||||
|
|
||||||
/-- The sum of some amounts. -/
|
|
||||||
def total : List Int → Int
|
|
||||||
| [] => 0
|
|
||||||
| x :: xs => x + total xs
|
|
||||||
|
|
||||||
/-- Lines crediting each target its part (`kernel::credit_line` per
|
|
||||||
member). -/
|
|
||||||
def equityLines : List (Nat × Int) → List Line
|
|
||||||
| [] => []
|
|
||||||
| (t, c) :: ts => closingLine t c ++ equityLines ts
|
|
||||||
|
|
||||||
theorem net_equityLines (ts : List (Nat × Int)) :
|
|
||||||
net (equityLines ts) = -total (ts.map Prod.snd) := by
|
|
||||||
induction ts with
|
|
||||||
| nil => rfl
|
|
||||||
| cons x ts ih =>
|
|
||||||
obtain ⟨t, c⟩ := x
|
|
||||||
simp only [equityLines, net_append, net_closingLine, ih, List.map_cons, total]; omega
|
|
||||||
|
|
||||||
theorem movement_equityLines_absent (b : Nat) (ts : List (Nat × Int))
|
|
||||||
(h : ∀ x ∈ ts, x.1 ≠ b) : movement b (equityLines ts) = 0 := by
|
|
||||||
induction ts with
|
|
||||||
| nil => rfl
|
|
||||||
| cons x ts ih =>
|
|
||||||
obtain ⟨t, c⟩ := x
|
|
||||||
have ht : b ≠ t := fun e => h (t, c) (by simp) e.symm
|
|
||||||
simp only [equityLines, movement_append, movement_closingLine, ht, ite_false,
|
|
||||||
ih (fun y hy => h y (List.mem_cons_of_mem _ hy))]
|
|
||||||
rfl
|
|
||||||
|
|
||||||
theorem equityLines_pos (ts : List (Nat × Int)) : ∀ l ∈ equityLines ts, 0 < l.amount := by
|
|
||||||
induction ts with
|
|
||||||
| nil => simp [equityLines]
|
|
||||||
| cons x ts ih =>
|
|
||||||
obtain ⟨t, c⟩ := x
|
|
||||||
intro l hl
|
|
||||||
simp only [equityLines, List.mem_append] at hl
|
|
||||||
cases hl with
|
|
||||||
| inl h => exact closingLine_pos _ _ l h
|
|
||||||
| inr h => exact ih l h
|
|
||||||
|
|
||||||
theorem equityLines_account (ts : List (Nat × Int)) :
|
|
||||||
∀ l ∈ equityLines ts, ∃ x ∈ ts, l.account = x.1 := by
|
|
||||||
induction ts with
|
|
||||||
| nil => simp [equityLines]
|
|
||||||
| cons x ts ih =>
|
|
||||||
obtain ⟨t, c⟩ := x
|
|
||||||
intro l hl
|
|
||||||
simp only [equityLines, List.mem_append] at hl
|
|
||||||
cases hl with
|
|
||||||
| inl h => exact ⟨(t, c), by simp, closingLine_account _ _ l h⟩
|
|
||||||
| inr h =>
|
|
||||||
obtain ⟨y, hy, e⟩ := ih l h
|
|
||||||
exact ⟨y, List.mem_cons_of_mem _ hy, e⟩
|
|
||||||
|
|
||||||
theorem equityLines_eq_nil (ts : List (Nat × Int)) (h : equityLines ts = []) :
|
|
||||||
total (ts.map Prod.snd) = 0 := by
|
|
||||||
induction ts with
|
|
||||||
| nil => rfl
|
|
||||||
| cons x ts ih =>
|
|
||||||
obtain ⟨t, c⟩ := x
|
|
||||||
simp only [equityLines, List.append_eq_nil_iff] at h
|
|
||||||
simp only [List.map_cons, total, (closingLine_eq_nil _ _).1 h.1, ih h.2]; rfl
|
|
||||||
|
|
||||||
/-- The closing entry: zero the closed accounts, credit the net to the
|
|
||||||
targets. -/
|
|
||||||
def closingEntry (as : List Nat) (ts : List (Nat × Int)) (ls : List Line) : List Line :=
|
|
||||||
closings as ls ++ equityLines ts
|
|
||||||
|
|
||||||
/-- **A closing entry balances** when the targets' parts sum to the net
|
|
||||||
of the closing lines (the net income), however that net was split. -/
|
|
||||||
theorem closingEntry_net (as : List Nat) (ts : List (Nat × Int)) (ls : List Line)
|
|
||||||
(hsum : total (ts.map Prod.snd) = net (closings as ls)) :
|
|
||||||
net (closingEntry as ts ls) = 0 := by
|
|
||||||
rw [closingEntry, net_append, net_equityLines, hsum]; omega
|
|
||||||
|
|
||||||
/-- **A closing entry zeroes every account it closes.** -/
|
|
||||||
theorem closingEntry_zeroes (as : List Nat) (ts : List (Nat × Int)) (ls : List Line)
|
|
||||||
(hd : Distinct as) (b : Nat) (hb : b ∈ as) (ht : ∀ x ∈ ts, x.1 ≠ b) :
|
|
||||||
movement b (ls ++ closingEntry as ts ls) = 0 := by
|
|
||||||
rw [movement_append, closingEntry, movement_append, movement_closings b as ls hd,
|
|
||||||
movement_equityLines_absent b ts ht]
|
|
||||||
simp only [hb, ite_true]; omega
|
|
||||||
|
|
||||||
/-- **A non-empty closing entry is acceptable**: two or more lines, every
|
|
||||||
amount positive, debits equal to credits. -/
|
|
||||||
theorem closingEntry_accepted (as : List Nat) (ts : List (Nat × Int)) (ls : List Line)
|
|
||||||
(hsum : total (ts.map Prod.snd) = net (closings as ls)) (hne : closings as ls ≠ []) :
|
|
||||||
Accepted (closingEntry as ts ls) := by
|
|
||||||
refine ⟨?_, ?_, ?_⟩
|
|
||||||
· rw [closingEntry, List.length_append]
|
|
||||||
match hc : closings as ls with
|
|
||||||
| [] => exact absurd hc hne
|
|
||||||
| [l] =>
|
|
||||||
have hl : 0 < l.amount := closings_pos as ls l (by rw [hc]; simp)
|
|
||||||
have hnet : net (closings as ls) ≠ 0 := by
|
|
||||||
rw [hc]; obtain ⟨a, s, m⟩ := l
|
|
||||||
cases s <;> simp [Line.signed] at hl ⊢ <;> omega
|
|
||||||
have : equityLines ts ≠ [] := fun h => hnet (hsum ▸ equityLines_eq_nil ts h)
|
|
||||||
have : 1 ≤ (equityLines ts).length := by
|
|
||||||
cases h : equityLines ts with
|
|
||||||
| nil => exact absurd h this
|
|
||||||
| cons _ _ => simp
|
|
||||||
simp; omega
|
|
||||||
| _ :: _ :: _ => simp; omega
|
|
||||||
· intro l hl
|
|
||||||
simp only [closingEntry, List.mem_append] at hl
|
|
||||||
cases hl with
|
|
||||||
| inl h => exact closings_pos as ls l h
|
|
||||||
| inr h => exact equityLines_pos ts l h
|
|
||||||
· have := closingEntry_net as ts ls hsum
|
|
||||||
rw [net_eq_debits_sub_credits] at this; omega
|
|
||||||
|
|
||||||
/-! ## Net income -/
|
|
||||||
|
|
||||||
/-- The chart's revenue and expense codes: what a close zeroes. -/
|
|
||||||
def nominalCodes : Chart → List Nat
|
|
||||||
| [] => []
|
|
||||||
| (a, t) :: c => if t = .revenue ∨ t = .expense then a :: nominalCodes c else nominalCodes c
|
|
||||||
|
|
||||||
theorem nominalCodes_sub (c : Chart) : ∀ a ∈ nominalCodes c, a ∈ codes c := by
|
|
||||||
induction c with
|
|
||||||
| nil => simp [nominalCodes]
|
|
||||||
| cons x c ih =>
|
|
||||||
obtain ⟨a, t⟩ := x
|
|
||||||
intro b hb
|
|
||||||
simp only [nominalCodes] at hb
|
|
||||||
split at hb
|
|
||||||
· cases hb with
|
|
||||||
| head => simp
|
|
||||||
| tail _ h => exact List.mem_cons_of_mem _ (ih b h)
|
|
||||||
· exact List.mem_cons_of_mem _ (ih b hb)
|
|
||||||
|
|
||||||
theorem nominalCodes_distinct (c : Chart) (hd : Distinct (codes c)) : Distinct (nominalCodes c) := by
|
|
||||||
induction c with
|
|
||||||
| nil => trivial
|
|
||||||
| cons x c ih =>
|
|
||||||
obtain ⟨a, t⟩ := x
|
|
||||||
obtain ⟨ha, hd⟩ := hd
|
|
||||||
simp only [nominalCodes]
|
|
||||||
split
|
|
||||||
· exact ⟨fun h => ha (nominalCodes_sub c a h), ih hd⟩
|
|
||||||
· exact ih hd
|
|
||||||
|
|
||||||
/-- **The net a close credits to equity is the period's net income**:
|
|
||||||
revenue balances minus expense balances. -/
|
|
||||||
theorem net_closings_is_net_income (c : Chart) (ls : List Line) :
|
|
||||||
net (closings (nominalCodes c) ls) = typeTotal .revenue ls c - typeTotal .expense ls c := by
|
|
||||||
rw [net_closings]
|
|
||||||
induction c with
|
|
||||||
| nil => rfl
|
|
||||||
| cons x c ih =>
|
|
||||||
obtain ⟨a, t⟩ := x
|
|
||||||
cases t <;> simp [nominalCodes, typeTotal, balance, AccountType.normal, signed] at ih ⊢ <;> omega
|
|
||||||
|
|
||||||
/-! ## Splitting among members -/
|
|
||||||
|
|
||||||
/-- Make rounded parts sum to the whole: the last becomes the whole minus
|
|
||||||
the others (`kernel::plug_last`). -/
|
|
||||||
def plugLast (whole : Int) : List Int → List Int
|
|
||||||
| [] => []
|
|
||||||
| [_] => [whole]
|
|
||||||
| p :: q :: rest => p :: plugLast (whole - p) (q :: rest)
|
|
||||||
|
|
||||||
/-- **An allocation's parts sum to the whole, exactly**, however the
|
|
||||||
other parts were rounded. -/
|
|
||||||
theorem plugLast_total (whole : Int) (ps : List Int) (h : ps ≠ []) :
|
|
||||||
total (plugLast whole ps) = whole := by
|
|
||||||
induction ps generalizing whole with
|
|
||||||
| nil => exact absurd rfl h
|
|
||||||
| cons p rest ih =>
|
|
||||||
match rest, ih with
|
|
||||||
| [], _ => show total [whole] = whole; simp [total]
|
|
||||||
| q :: rest, ih =>
|
|
||||||
simp only [plugLast, total]
|
|
||||||
have := ih (whole - p) (by simp)
|
|
||||||
omega
|
|
||||||
|
|
||||||
theorem plugLast_length (whole : Int) (ps : List Int) : (plugLast whole ps).length = ps.length := by
|
|
||||||
induction ps generalizing whole with
|
|
||||||
| nil => rfl
|
|
||||||
| cons p rest ih =>
|
|
||||||
match rest, ih with
|
|
||||||
| [], _ => rfl
|
|
||||||
| q :: rest, ih => simp only [plugLast, List.length_cons]; rw [ih]; rfl
|
|
||||||
|
|
||||||
/-- Only the last part changes. -/
|
|
||||||
theorem plugLast_keeps (whole : Int) (ps : List Int) :
|
|
||||||
(plugLast whole ps).dropLast = ps.dropLast := by
|
|
||||||
induction ps generalizing whole with
|
|
||||||
| nil => rfl
|
|
||||||
| cons p rest ih =>
|
|
||||||
match rest, ih with
|
|
||||||
| [], _ => rfl
|
|
||||||
| q :: rest, ih =>
|
|
||||||
have hne : plugLast (whole - p) (q :: rest) ≠ [] := by
|
|
||||||
intro h
|
|
||||||
have := plugLast_length (whole - p) (q :: rest)
|
|
||||||
rw [h] at this; simp at this
|
|
||||||
rw [show plugLast whole (p :: q :: rest) = p :: plugLast (whole - p) (q :: rest) from rfl,
|
|
||||||
List.dropLast_cons_of_ne_nil hne, ih, List.dropLast_cons_cons]
|
|
||||||
|
|
||||||
/-! ## Cumulative member closes
|
|
||||||
|
|
||||||
A run of members closes under the run rule of 2026-10-13 to P3
|
|
||||||
(`CloseTo::MembersRun`, journaled `"cumulative"`; rules.md B-13) gives each
|
|
||||||
member, at each close, their allocation of the run's total so far minus
|
|
||||||
their allocation of the total before it. `alloc` is any allocation of an
|
|
||||||
amount to members (member `i` gets `alloc x i`), such as
|
|
||||||
`periods::allocate`. -/
|
|
||||||
|
|
||||||
/-- What one close gives member `i`: `before` is the run's total before
|
|
||||||
it, `net` what it closes. -/
|
|
||||||
def closePart (alloc : Int → Nat → Int) (before net : Int) (i : Nat) : Int :=
|
|
||||||
alloc (before + net) i - alloc before i
|
|
||||||
|
|
||||||
/-- What a run of closes with these nets, starting after `before`, gives
|
|
||||||
member `i` in all. -/
|
|
||||||
def runPart (alloc : Int → Nat → Int) : Int → List Int → Nat → Int
|
|
||||||
| _, [], _ => 0
|
|
||||||
| before, net :: rest, i => closePart alloc before net i + runPart alloc (before + net) rest i
|
|
||||||
|
|
||||||
/-- **A run telescopes**: member `i`'s total from a run is their allocation
|
|
||||||
of everything the run closed, less their allocation of where it started.
|
|
||||||
From an empty start (`alloc 0 = 0`), it is exactly their allocation of
|
|
||||||
the run's total, so its rounding is one allocation's rounding, never a
|
|
||||||
sum of them. -/
|
|
||||||
theorem runPart_telescopes (alloc : Int → Nat → Int) (before : Int) (nets : List Int) (i : Nat) :
|
|
||||||
runPart alloc before nets i = alloc (before + nets.sum) i - alloc before i := by
|
|
||||||
induction nets generalizing before with
|
|
||||||
| nil => simp [runPart]
|
|
||||||
| cons n rest ih =>
|
|
||||||
simp only [runPart, closePart, ih, List.sum_cons]
|
|
||||||
rw [show before + n + rest.sum = before + (n + rest.sum) by omega]
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- A sum of differences is the difference of the sums. -/
|
|
||||||
theorem sum_map_sub (f g : Nat → Int) (ms : List Nat) :
|
|
||||||
(ms.map (fun i => f i - g i)).sum = (ms.map f).sum - (ms.map g).sum := by
|
|
||||||
induction ms with
|
|
||||||
| nil => simp
|
|
||||||
| cons m ms ih => simp only [List.map_cons, List.sum_cons, ih]; omega
|
|
||||||
|
|
||||||
/-- **Each close hands out exactly its net** when every allocation sums
|
|
||||||
to its amount (`periods::allocate` does: `kernel::apportion`). -/
|
|
||||||
theorem closePart_total (alloc : Int → Nat → Int) (members : List Nat)
|
|
||||||
(sums : ∀ x, (members.map (alloc x)).sum = x) (before net : Int) :
|
|
||||||
(members.map (closePart alloc before net)).sum = net := by
|
|
||||||
have h := sum_map_sub (alloc (before + net)) (alloc before) members
|
|
||||||
unfold closePart
|
|
||||||
rw [h, sums, sums]
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-! ## Members closes counted from a count's start (P3)
|
|
||||||
|
|
||||||
The rule since P3 (`CloseTo::Members`, rules.md B-13; D-10): a close gives
|
|
||||||
member `i` their allocation of everything the count has closed (`closed`
|
|
||||||
before it, plus its `net`), less what the count's earlier closes gave them
|
|
||||||
(`given i`), whatever rule those closes were made under
|
|
||||||
(`periods::allocate_cumulative`, `kernel::apportion_cumulative`). The run
|
|
||||||
rule of 2026-10-13 (`CloseTo::MembersRun`, `closePart`) is the special case
|
|
||||||
where the earlier closes gave exactly the allocation. -/
|
|
||||||
|
|
||||||
/-- What a cumulative close gives member `i`. -/
|
|
||||||
def trueUp (alloc : Int → Nat → Int) (closed net : Int) (given : Nat → Int) (i : Nat) : Int :=
|
|
||||||
alloc (closed + net) i - given i
|
|
||||||
|
|
||||||
/-- **A cumulative close hands out exactly its net**, whatever rule the
|
|
||||||
earlier closes used, as long as they handed out what they closed. -/
|
|
||||||
theorem trueUp_total (alloc : Int → Nat → Int) (members : List Nat)
|
|
||||||
(sums : ∀ x, (members.map (alloc x)).sum = x) (closed net : Int) (given : Nat → Int)
|
|
||||||
(hg : (members.map given).sum = closed) :
|
|
||||||
(members.map (trueUp alloc closed net given)).sum = net := by
|
|
||||||
have h := sum_map_sub (alloc (closed + net)) given members
|
|
||||||
unfold trueUp
|
|
||||||
rw [h, sums, hg]
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- **A running total is one allocation**: after the close, what the count
|
|
||||||
has given member `i` is their allocation of everything it closed, so its
|
|
||||||
rounding is one allocation's, never a sum of them. -/
|
|
||||||
theorem trueUp_running (alloc : Int → Nat → Int) (closed net : Int) (given : Nat → Int)
|
|
||||||
(i : Nat) : given i + trueUp alloc closed net given i = alloc (closed + net) i := by
|
|
||||||
unfold trueUp
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- The run rule is the special case `given = alloc closed`. -/
|
|
||||||
theorem closePart_eq_trueUp (alloc : Int → Nat → Int) (before net : Int) (i : Nat) :
|
|
||||||
closePart alloc before net i = trueUp alloc before net (alloc before) i := rfl
|
|
||||||
|
|
||||||
/-- **Bounded drift, for any history**: when every allocation is under one
|
|
||||||
unit from exact (scaled by `W`, the shares being `w i / W`), so is every
|
|
||||||
member's running total after a cumulative close, whatever the earlier
|
|
||||||
closes gave (`Apportion.allocW_drift` supplies the hypothesis). -/
|
|
||||||
theorem cumulative_drift (alloc : Int → Nat → Int) (w : Nat → Int) (W : Int)
|
|
||||||
(hd : ∀ x i, -W < alloc x i * W - x * w i ∧ alloc x i * W - x * w i < W)
|
|
||||||
(closed net : Int) (given : Nat → Int) (i : Nat) :
|
|
||||||
-W < (given i + trueUp alloc closed net given i) * W - (closed + net) * w i ∧
|
|
||||||
(given i + trueUp alloc closed net given i) * W - (closed + net) * w i < W := by
|
|
||||||
rw [trueUp_running]
|
|
||||||
exact hd _ _
|
|
||||||
|
|
||||||
/-- The statement of members' equity's share of the earnings `U` not yet
|
|
||||||
closed, as of a day: what the count's next close would give of them, its
|
|
||||||
allocation of `closed + U` less its allocation of `closed` (D-38; P3 review
|
|
||||||
R-1: never the true-up a close hasn't posted). -/
|
|
||||||
def unclosedShare (alloc : Int → Nat → Int) (closed U : Int) (i : Nat) : Int :=
|
|
||||||
alloc (closed + U) i - alloc closed i
|
|
||||||
|
|
||||||
/-- **Closing a period moves a member's capital on a day by exactly the
|
|
||||||
true-up the close posts** (D-38, R-1): before, their capital `cap` plus
|
|
||||||
their share of the unclosed `U`; after closing `net` of it, `cap` plus the
|
|
||||||
close's part plus their share of the rest. The difference is
|
|
||||||
`alloc closed i - given i`. -/
|
|
||||||
theorem statement_after_close (alloc : Int → Nat → Int) (cap closed net U : Int)
|
|
||||||
(given : Nat → Int) (i : Nat) :
|
|
||||||
cap + trueUp alloc closed net given i + unclosedShare alloc (closed + net) (U - net) i
|
|
||||||
= cap + unclosedShare alloc closed U i + (alloc closed i - given i) := by
|
|
||||||
unfold trueUp unclosedShare
|
|
||||||
rw [show closed + net + (U - net) = closed + U by omega]
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- **A member's capital on a day never moves when its period is closed**
|
|
||||||
(D-38), once the count's closes gave what the cumulative rule gives
|
|
||||||
(`given = alloc closed`: every count the rule began, and every count after
|
|
||||||
its first close that posts). -/
|
|
||||||
theorem statement_unmoved_by_close (alloc : Int → Nat → Int) (cap closed net U : Int)
|
|
||||||
(i : Nat) :
|
|
||||||
cap + trueUp alloc closed net (alloc closed) i + unclosedShare alloc (closed + net) (U - net) i
|
|
||||||
= cap + unclosedShare alloc closed U i := by
|
|
||||||
rw [statement_after_close]
|
|
||||||
omega
|
|
||||||
|
|
||||||
end Bookspec
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import Bookspec.Basic
|
|
||||||
|
|
||||||
/-!
|
|
||||||
# Entries: acceptance, movements, reversals
|
|
||||||
|
|
||||||
Mirrors `kernel::check_lines`, `kernel::movements`, `kernel::reversal`,
|
|
||||||
and `JournalEntry::validate`.
|
|
||||||
-/
|
|
||||||
|
|
||||||
namespace Bookspec
|
|
||||||
|
|
||||||
/-- What the engine accepts (the arithmetic of `JournalEntry::validate`,
|
|
||||||
i.e. `kernel::check_lines`): two or more lines, every amount positive,
|
|
||||||
debits equal to credits. (Range, account, and currency checks are not
|
|
||||||
arithmetic laws and are left out of the model.) -/
|
|
||||||
def Accepted (ls : List Line) : Prop :=
|
|
||||||
2 ≤ ls.length ∧ (∀ l ∈ ls, 0 < l.amount) ∧ debits ls = credits ls
|
|
||||||
|
|
||||||
/-- **Every accepted entry nets to zero.** -/
|
|
||||||
theorem Accepted.net_zero {ls : List Line} (h : Accepted ls) : net ls = 0 := by
|
|
||||||
rw [net_eq_debits_sub_credits]; have := h.2.2; omega
|
|
||||||
|
|
||||||
/-- Net movement of account `a` over some lines. -/
|
|
||||||
def movement (a : Nat) : List Line → Int
|
|
||||||
| [] => 0
|
|
||||||
| l :: ls => (if l.account = a then l.signed else 0) + movement a ls
|
|
||||||
|
|
||||||
@[simp] theorem movement_nil (a : Nat) : movement a [] = 0 := rfl
|
|
||||||
@[simp] theorem movement_cons (a : Nat) (l : Line) (ls : List Line) :
|
|
||||||
movement a (l :: ls) = (if l.account = a then l.signed else 0) + movement a ls := rfl
|
|
||||||
|
|
||||||
theorem movement_append (a : Nat) (xs ys : List Line) :
|
|
||||||
movement a (xs ++ ys) = movement a xs + movement a ys := by
|
|
||||||
induction xs with
|
|
||||||
| nil => simp
|
|
||||||
| cons x xs ih => simp only [List.cons_append, movement_cons, ih]; omega
|
|
||||||
|
|
||||||
/-- **Movements partition the net.** Summed over a chart's distinct codes
|
|
||||||
that cover every line's account, the per-account movements add up to the
|
|
||||||
net of the lines: grouping by account loses and invents nothing. -/
|
|
||||||
theorem sum_movements (as : List Nat) (hd : Distinct as) :
|
|
||||||
∀ ls : List Line, (∀ l ∈ ls, l.account ∈ as) →
|
|
||||||
sumOver (fun a => movement a ls) as = net ls := by
|
|
||||||
intro ls
|
|
||||||
induction ls with
|
|
||||||
| nil => intro _; simpa using sumOver_zero as
|
|
||||||
| cons l ls ih =>
|
|
||||||
intro hcov
|
|
||||||
simp only [movement_cons, net_cons]
|
|
||||||
rw [sumOver_add, ih (fun m hm => hcov m (List.mem_cons_of_mem _ hm)),
|
|
||||||
sumOver_indicator l.account l.signed as hd (hcov l (List.mem_cons_self ..))]
|
|
||||||
|
|
||||||
/-- So the movements of an accepted entry sum to zero. -/
|
|
||||||
theorem Accepted.movements_sum_zero {ls : List Line} (h : Accepted ls) (as : List Nat)
|
|
||||||
(hd : Distinct as) (hcov : ∀ l ∈ ls, l.account ∈ as) :
|
|
||||||
sumOver (fun a => movement a ls) as = 0 := by
|
|
||||||
rw [sum_movements as hd ls hcov, h.net_zero]
|
|
||||||
|
|
||||||
/-! ## Reversals -/
|
|
||||||
|
|
||||||
/-- A line with its side flipped. -/
|
|
||||||
def Line.reverse (l : Line) : Line := { l with side := l.side.flip }
|
|
||||||
|
|
||||||
/-- A reversing entry's lines (`kernel::reversal`). -/
|
|
||||||
def reversal (ls : List Line) : List Line := ls.map Line.reverse
|
|
||||||
|
|
||||||
theorem Line.reverse_signed (l : Line) : l.reverse.signed = -l.signed := by
|
|
||||||
simp [Line.reverse, Line.signed, signed_flip]
|
|
||||||
|
|
||||||
theorem net_reversal (ls : List Line) : net (reversal ls) = -net ls := by
|
|
||||||
induction ls with
|
|
||||||
| nil => rfl
|
|
||||||
| cons l ls ih =>
|
|
||||||
simp only [reversal, List.map_cons] at ih ⊢
|
|
||||||
simp only [net_cons, ih, Line.reverse_signed]; omega
|
|
||||||
|
|
||||||
theorem movement_reversal (a : Nat) (ls : List Line) :
|
|
||||||
movement a (reversal ls) = -movement a ls := by
|
|
||||||
induction ls with
|
|
||||||
| nil => rfl
|
|
||||||
| cons l ls ih =>
|
|
||||||
simp only [reversal, List.map_cons] at ih ⊢
|
|
||||||
simp only [movement_cons, ih, Line.reverse_signed]
|
|
||||||
by_cases h : l.account = a <;> simp [Line.reverse, h] <;> omega
|
|
||||||
|
|
||||||
/-- **An entry and its reversal net every account to zero.** -/
|
|
||||||
theorem movement_with_reversal (a : Nat) (ls : List Line) :
|
|
||||||
movement a (ls ++ reversal ls) = 0 := by
|
|
||||||
rw [movement_append, movement_reversal]; omega
|
|
||||||
|
|
||||||
theorem debits_reversal (ls : List Line) : debits (reversal ls) = credits ls := by
|
|
||||||
induction ls with
|
|
||||||
| nil => rfl
|
|
||||||
| cons l ls ih =>
|
|
||||||
simp only [reversal, List.map_cons] at ih ⊢
|
|
||||||
obtain ⟨a, s, m⟩ := l
|
|
||||||
cases s <;> simp [debits, credits, Line.reverse, Side.flip, ih]
|
|
||||||
|
|
||||||
theorem credits_reversal (ls : List Line) : credits (reversal ls) = debits ls := by
|
|
||||||
induction ls with
|
|
||||||
| nil => rfl
|
|
||||||
| cons l ls ih =>
|
|
||||||
simp only [reversal, List.map_cons] at ih ⊢
|
|
||||||
obtain ⟨a, s, m⟩ := l
|
|
||||||
cases s <;> simp [debits, credits, Line.reverse, Side.flip, ih]
|
|
||||||
|
|
||||||
/-- **A reversal is accepted exactly when the original is.** -/
|
|
||||||
theorem accepted_reversal_iff (ls : List Line) : Accepted (reversal ls) ↔ Accepted ls := by
|
|
||||||
unfold Accepted
|
|
||||||
rw [debits_reversal, credits_reversal]
|
|
||||||
have hlen : (reversal ls).length = ls.length := List.length_map ..
|
|
||||||
have hpos : (∀ l ∈ reversal ls, 0 < l.amount) ↔ (∀ l ∈ ls, 0 < l.amount) := by
|
|
||||||
simp [reversal, Line.reverse]
|
|
||||||
rw [hlen, hpos]
|
|
||||||
constructor <;> rintro ⟨a, b, c⟩ <;> exact ⟨a, b, c.symm⟩
|
|
||||||
|
|
||||||
end Bookspec
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import Bookspec.Ledger
|
|
||||||
|
|
||||||
/-!
|
|
||||||
# bookspec's five laws
|
|
||||||
|
|
||||||
The five laws of the first edition (`bookspec/spec/00-model.md`, "The five
|
|
||||||
laws"), each stated once, by name, as a theorem about *every* reachable
|
|
||||||
state of the books, ported from `verify/lean` (P13 step 4). Money is `Int`,
|
|
||||||
an exact count of the smallest unit; any finite set of exact decimals
|
|
||||||
scales to integers without loss, and every law is a law of addition.
|
|
||||||
|
|
||||||
1. Debits equal credits (BS-entry-1).
|
|
||||||
2. Assets = Liabilities + Equity + (Revenue - Expenses) (BS-statement-2).
|
|
||||||
3. The trial balance balances (BS-statement-1).
|
|
||||||
4. Net income articulates to equity: a close credits equity with exactly
|
|
||||||
revenue less expenses (BS-period-1).
|
|
||||||
5. The cash-flow sections sum to the change in cash (BS-statement-3).
|
|
||||||
-/
|
|
||||||
|
|
||||||
namespace Bookspec
|
|
||||||
|
|
||||||
/-- Law 1. In every reachable state the posted lines' debits equal their
|
|
||||||
credits (`net` is debits less credits). -/
|
|
||||||
theorem law_debits_equal_credits {L : Ledger} (r : Reachable L) :
|
|
||||||
net (allLines L.posted) = 0 :=
|
|
||||||
r.books_net_zero
|
|
||||||
|
|
||||||
/-- Law 2. The accounting equation, in every reachable state. -/
|
|
||||||
theorem law_accounting_equation {L : Ledger} (r : Reachable L) :
|
|
||||||
let ls := allLines L.posted
|
|
||||||
typeTotal .asset ls L.chart
|
|
||||||
= typeTotal .liability ls L.chart + typeTotal .equity ls L.chart
|
|
||||||
+ (typeTotal .revenue ls L.chart - typeTotal .expense ls L.chart) :=
|
|
||||||
r.accounting_equation
|
|
||||||
|
|
||||||
/-- Law 3. The trial balance balances, in every reachable state. -/
|
|
||||||
theorem law_trial_balance {L : Ledger} (r : Reachable L) :
|
|
||||||
tbDebits (allLines L.posted) (codes L.chart)
|
|
||||||
= tbCredits (allLines L.posted) (codes L.chart) :=
|
|
||||||
r.trial_balance
|
|
||||||
|
|
||||||
/-- Law 4. A close credits equity with exactly the period's net income. -/
|
|
||||||
theorem law_net_income_articulates (L : Ledger) (p : Period) (ts : List (Nat × Int))
|
|
||||||
(hsum : total (ts.map Prod.snd)
|
|
||||||
= net (closings (nominalCodes L.chart) (allLines (through p.stop L.posted)))) :
|
|
||||||
let ls := allLines (through p.stop L.posted)
|
|
||||||
total (ts.map Prod.snd) = typeTotal .revenue ls L.chart - typeTotal .expense ls L.chart :=
|
|
||||||
close_credits_net_income L p ts hsum
|
|
||||||
|
|
||||||
/-- Law 5. The three sections of the statement of cash flows sum to the
|
|
||||||
change in the cash accounts. -/
|
|
||||||
theorem law_cash_flow_is_change_in_cash {L : Ledger} (r : Reachable L) (b c : Nat)
|
|
||||||
(hbc : b ≤ c) (F : List Line → Line → Flow) (C : List Line → List Int)
|
|
||||||
(hC : ∀ e, sum3 (C e) = 0) (cash : List Nat) (hd : Distinct cash)
|
|
||||||
(hF : ∀ e l, F e l = .cash ↔ l.account ∈ cash) :
|
|
||||||
let es := (between b c L.posted).map Entry.lines
|
|
||||||
sectionTotal F C 0 es + sectionTotal F C 1 es + sectionTotal F C 2 es
|
|
||||||
= sumOver (fun a => movement a (allLines (through c L.posted))) cash
|
|
||||||
- sumOver (fun a => movement a (allLines (through b L.posted))) cash :=
|
|
||||||
r.cashflow_is_change_in_cash b c hbc F C hC cash hd hF
|
|
||||||
|
|
||||||
end Bookspec
|
|
||||||
@@ -1,562 +0,0 @@
|
|||||||
import Bookspec.Closing
|
|
||||||
|
|
||||||
/-!
|
|
||||||
# The ledger as a state machine
|
|
||||||
|
|
||||||
The engine's command surface (`Ledger`), modelled as the steps it allows:
|
|
||||||
add an account, start a draft, post, reverse, open a period, close one.
|
|
||||||
Each step carries the checks the engine makes before it mutates. The
|
|
||||||
theorems below hold in *every* state reachable from empty books, by any
|
|
||||||
sequence of commands, of any length.
|
|
||||||
|
|
||||||
Mirrors `Ledger::add_account`, `try_create_entry`, `post_entry`,
|
|
||||||
`reverse_entry`, `create_period`, and `close_period_to`; and, as one step
|
|
||||||
`record`, everything the books keep about how they are kept: settings,
|
|
||||||
conventions, decisions, open items and approvals (`apply_settings`,
|
|
||||||
`apply_convention`, `apply_decision`, `apply_open_item`,
|
|
||||||
`apply_approval`, and their retirements and resolutions), each a record
|
|
||||||
the step adds and nothing else; and an account's rename
|
|
||||||
(`rename_account`, `account.renamed`, rules.md B-31), since the model's
|
|
||||||
chart is codes and types and a name is no part of it.
|
|
||||||
-/
|
|
||||||
|
|
||||||
namespace Bookspec
|
|
||||||
|
|
||||||
/-- A journal entry: its id, its date (a day number), its lines. -/
|
|
||||||
structure Entry where
|
|
||||||
id : Nat
|
|
||||||
date : Nat
|
|
||||||
lines : List Line
|
|
||||||
deriving DecidableEq
|
|
||||||
|
|
||||||
/-- Every line of some entries, in order. -/
|
|
||||||
def allLines : List Entry → List Line
|
|
||||||
| [] => []
|
|
||||||
| e :: es => e.lines ++ allLines es
|
|
||||||
|
|
||||||
theorem allLines_append (xs ys : List Entry) : allLines (xs ++ ys) = allLines xs ++ allLines ys := by
|
|
||||||
induction xs with
|
|
||||||
| nil => rfl
|
|
||||||
| cons x xs ih => simp [allLines, ih]
|
|
||||||
|
|
||||||
theorem mem_allLines {l : Line} {es : List Entry} : l ∈ allLines es ↔ ∃ e ∈ es, l ∈ e.lines := by
|
|
||||||
induction es with
|
|
||||||
| nil => simp [allLines]
|
|
||||||
| cons e es ih => simp [allLines, ih]
|
|
||||||
|
|
||||||
/-- **Any collection of balanced entries nets to zero**: all of them, those
|
|
||||||
dated through some day, those in some period, any filter at all. -/
|
|
||||||
theorem net_allLines (es : List Entry) (h : ∀ e ∈ es, net e.lines = 0) : net (allLines es) = 0 := by
|
|
||||||
induction es with
|
|
||||||
| nil => rfl
|
|
||||||
| cons e es ih =>
|
|
||||||
simp only [allLines, net_append, h e (by simp), ih (fun x hx => h x (by simp [hx]))]
|
|
||||||
rfl
|
|
||||||
|
|
||||||
/-- An accounting period: inclusive day range, open or closed. -/
|
|
||||||
structure Period where
|
|
||||||
id : Nat
|
|
||||||
start : Nat
|
|
||||||
stop : Nat
|
|
||||||
closed : Bool
|
|
||||||
deriving DecidableEq
|
|
||||||
|
|
||||||
/-- Whether a day falls in a period. -/
|
|
||||||
def Period.contains (p : Period) (d : Nat) : Bool := decide (p.start ≤ d ∧ d ≤ p.stop)
|
|
||||||
|
|
||||||
/-- The books: chart, posted entries in posting order, drafts, periods,
|
|
||||||
and the record of how they are kept (settings, conventions, decisions,
|
|
||||||
open items, approvals), each record abstracted to a number. -/
|
|
||||||
structure Ledger where
|
|
||||||
chart : Chart
|
|
||||||
posted : List Entry
|
|
||||||
drafts : List Entry
|
|
||||||
periods : List Period
|
|
||||||
records : List Nat
|
|
||||||
|
|
||||||
/-- Empty books. -/
|
|
||||||
def Ledger.empty : Ledger := ⟨[], [], [], [], []⟩
|
|
||||||
|
|
||||||
/-- Whether a closed period contains a day. -/
|
|
||||||
def Ledger.closedOn (L : Ledger) (d : Nat) : Prop :=
|
|
||||||
∃ p ∈ L.periods, p.closed = true ∧ p.contains d = true
|
|
||||||
|
|
||||||
/-- Entries dated on or before a day. -/
|
|
||||||
def through (d : Nat) (es : List Entry) : List Entry := es.filter (fun e => decide (e.date ≤ d))
|
|
||||||
|
|
||||||
/-- Mark one period closed. -/
|
|
||||||
def closeIn (p : Period) (ps : List Period) : List Period :=
|
|
||||||
ps.map (fun q => if q = p then { q with closed := true } else q)
|
|
||||||
|
|
||||||
/-- The closing entry for period `p` crediting targets `ts`, if there is
|
|
||||||
anything to close. -/
|
|
||||||
def closingFor (L : Ledger) (p : Period) (ts : List (Nat × Int)) (id : Nat) : List Entry :=
|
|
||||||
if closings (nominalCodes L.chart) (allLines (through p.stop L.posted)) = [] then []
|
|
||||||
else [⟨id, p.stop, closingEntry (nominalCodes L.chart) ts (allLines (through p.stop L.posted))⟩]
|
|
||||||
|
|
||||||
/-- What the engine allows. -/
|
|
||||||
inductive Step : Ledger → Ledger → Prop
|
|
||||||
/-- `add_account`: codes are unique. -/
|
|
||||||
| addAccount (L : Ledger) (a : Nat) (t : AccountType) (h : a ∉ codes L.chart) :
|
|
||||||
Step L { L with chart := (a, t) :: L.chart }
|
|
||||||
/-- `try_create_entry` (and lines added to it): not in a closed period. -/
|
|
||||||
| draft (L : Ledger) (e : Entry) (h : ¬ L.closedOn e.date) :
|
|
||||||
Step L { L with drafts := e :: L.drafts }
|
|
||||||
/-- `post_entry`: a draft, acceptable, on known accounts, not in a closed
|
|
||||||
period. Appends to the log. -/
|
|
||||||
| post (L : Ledger) (e : Entry) (hd : e ∈ L.drafts) (ha : Accepted e.lines)
|
|
||||||
(hk : ∀ l ∈ e.lines, l.account ∈ codes L.chart) (hc : ¬ L.closedOn e.date) :
|
|
||||||
Step L { L with posted := L.posted ++ [e], drafts := L.drafts.filter (· ≠ e) }
|
|
||||||
/-- `reverse_entry`: a posted entry's lines, sides flipped, posted on a
|
|
||||||
day not in a closed period. -/
|
|
||||||
| reverse (L : Ledger) (e : Entry) (he : e ∈ L.posted) (r : Entry)
|
|
||||||
(hr : r.lines = reversal e.lines) (hc : ¬ L.closedOn r.date) :
|
|
||||||
Step L { L with posted := L.posted ++ [r] }
|
|
||||||
/-- `create_period`: periods open. -/
|
|
||||||
| openPeriod (L : Ledger) (p : Period) (h : p.closed = false) :
|
|
||||||
Step L { L with periods := p :: L.periods }
|
|
||||||
/-- `close_period_to`: an open period with no drafts in it, whose last day
|
|
||||||
is in no other closed period, with no later period closed, crediting the
|
|
||||||
net income to equity targets (not themselves revenue or expense) in
|
|
||||||
parts that sum to it. -/
|
|
||||||
| close (L : Ledger) (p : Period) (hp : p ∈ L.periods) (hopen : p.closed = false)
|
|
||||||
(hdrafts : ∀ d ∈ L.drafts, p.contains d.date = false)
|
|
||||||
(hother : ∀ q ∈ L.periods, q.closed = true → q.contains p.stop = false)
|
|
||||||
(hlater : ∀ q ∈ L.periods, q.closed = true → q.stop ≤ p.stop)
|
|
||||||
(ts : List (Nat × Int))
|
|
||||||
(hts : ∀ x ∈ ts, x.1 ∈ codes L.chart ∧ x.1 ∉ nominalCodes L.chart)
|
|
||||||
(hsum : total (ts.map Prod.snd)
|
|
||||||
= net (closings (nominalCodes L.chart) (allLines (through p.stop L.posted))))
|
|
||||||
(id : Nat) :
|
|
||||||
Step L { L with posted := L.posted ++ closingFor L p ts id, periods := closeIn p L.periods }
|
|
||||||
/-- `apply_settings`, `apply_convention`, `apply_decision`,
|
|
||||||
`apply_open_item`, `apply_approval` and their retirements and
|
|
||||||
resolutions, and the 1099 records `set_vendor_1099`,
|
|
||||||
`set_vendor_w9_flag`, `add_instrument` and `set_entry_instrument`: a
|
|
||||||
record of how the books are kept, and nothing else. `rename_account`
|
|
||||||
too: a name is outside the chart the model keeps. -/
|
|
||||||
| record (L : Ledger) (r : Nat) : Step L { L with records := r :: L.records }
|
|
||||||
|
|
||||||
/-- States reachable from empty books. -/
|
|
||||||
inductive Reachable : Ledger → Prop
|
|
||||||
| empty : Reachable Ledger.empty
|
|
||||||
| step {L L' : Ledger} : Reachable L → Step L L' → Reachable L'
|
|
||||||
|
|
||||||
/-- What holds in every reachable state. -/
|
|
||||||
structure Inv (L : Ledger) : Prop where
|
|
||||||
distinct : Distinct (codes L.chart)
|
|
||||||
accepted : ∀ e ∈ L.posted, Accepted e.lines
|
|
||||||
known : ∀ e ∈ L.posted, ∀ l ∈ e.lines, l.account ∈ codes L.chart
|
|
||||||
draftsOpen : ∀ d ∈ L.drafts, ¬ L.closedOn d.date
|
|
||||||
|
|
||||||
theorem closedOn_closeIn (L : Ledger) (p : Period) (d : Nat) :
|
|
||||||
({ L with periods := closeIn p L.periods } : Ledger).closedOn d →
|
|
||||||
L.closedOn d ∨ (p ∈ L.periods ∧ p.contains d = true) := by
|
|
||||||
rintro ⟨q, hq, hc, hd⟩
|
|
||||||
simp only [closeIn, List.mem_map] at hq
|
|
||||||
obtain ⟨q', hq', rfl⟩ := hq
|
|
||||||
by_cases e : q' = p
|
|
||||||
· subst e; right; simp at hd; exact ⟨hq', by simpa [Period.contains] using hd⟩
|
|
||||||
· left; simp [e] at hc hd; exact ⟨q', hq', hc, hd⟩
|
|
||||||
|
|
||||||
theorem closingFor_lines (L : Ledger) (p : Period) (ts : List (Nat × Int)) (id : Nat) :
|
|
||||||
∀ e ∈ closingFor L p ts id,
|
|
||||||
e.date = p.stop ∧ e.lines = closingEntry (nominalCodes L.chart) ts
|
|
||||||
(allLines (through p.stop L.posted))
|
|
||||||
∧ closings (nominalCodes L.chart) (allLines (through p.stop L.posted)) ≠ [] := by
|
|
||||||
intro e he
|
|
||||||
unfold closingFor at he
|
|
||||||
split at he
|
|
||||||
· simp at he
|
|
||||||
· simp at he; subst he; exact ⟨rfl, rfl, by assumption⟩
|
|
||||||
|
|
||||||
/-- **Every step preserves the invariant.** -/
|
|
||||||
theorem Step.inv {L L' : Ledger} (s : Step L L') (h : Inv L) : Inv L' := by
|
|
||||||
cases s with
|
|
||||||
| addAccount a t ha =>
|
|
||||||
exact ⟨⟨ha, h.distinct⟩, h.accepted,
|
|
||||||
fun e he l hl => List.mem_cons_of_mem _ (h.known e he l hl), h.draftsOpen⟩
|
|
||||||
| draft e hc =>
|
|
||||||
refine ⟨h.distinct, h.accepted, h.known, ?_⟩
|
|
||||||
intro d hd
|
|
||||||
cases hd with
|
|
||||||
| head => exact hc
|
|
||||||
| tail _ hd => exact h.draftsOpen d hd
|
|
||||||
| post e hd ha hk hc =>
|
|
||||||
refine ⟨h.distinct, ?_, ?_, ?_⟩
|
|
||||||
· intro x hx
|
|
||||||
simp only [List.mem_append, List.mem_singleton] at hx
|
|
||||||
rcases hx with hx | rfl
|
|
||||||
· exact h.accepted x hx
|
|
||||||
· exact ha
|
|
||||||
· intro x hx
|
|
||||||
simp only [List.mem_append, List.mem_singleton] at hx
|
|
||||||
rcases hx with hx | rfl
|
|
||||||
· exact h.known x hx
|
|
||||||
· exact hk
|
|
||||||
· intro d hd
|
|
||||||
exact h.draftsOpen d ((List.mem_filter.1 hd).1)
|
|
||||||
| reverse e he r hr hc =>
|
|
||||||
refine ⟨h.distinct, ?_, ?_, h.draftsOpen⟩
|
|
||||||
· intro x hx
|
|
||||||
simp only [List.mem_append, List.mem_singleton] at hx
|
|
||||||
rcases hx with hx | rfl
|
|
||||||
· exact h.accepted x hx
|
|
||||||
· rw [hr, accepted_reversal_iff]; exact h.accepted e he
|
|
||||||
· intro x hx
|
|
||||||
simp only [List.mem_append, List.mem_singleton] at hx
|
|
||||||
rcases hx with hx | rfl
|
|
||||||
· exact h.known x hx
|
|
||||||
· intro l hl
|
|
||||||
rw [hr] at hl
|
|
||||||
simp only [reversal, List.mem_map] at hl
|
|
||||||
obtain ⟨l', hl', rfl⟩ := hl
|
|
||||||
exact h.known e he l' hl'
|
|
||||||
| openPeriod p hp =>
|
|
||||||
refine ⟨h.distinct, h.accepted, h.known, ?_⟩
|
|
||||||
rintro d hd ⟨q, hq, hc, hdq⟩
|
|
||||||
cases hq with
|
|
||||||
| head => simp [hp] at hc
|
|
||||||
| tail _ hq => exact h.draftsOpen d hd ⟨q, hq, hc, hdq⟩
|
|
||||||
| close p hp hopen hdrafts hother hlater ts hts hsum id =>
|
|
||||||
refine ⟨h.distinct, ?_, ?_, ?_⟩
|
|
||||||
· intro x hx
|
|
||||||
simp only [List.mem_append] at hx
|
|
||||||
rcases hx with hx | hx
|
|
||||||
· exact h.accepted x hx
|
|
||||||
· obtain ⟨-, hl, hne⟩ := closingFor_lines L p ts id x hx
|
|
||||||
rw [hl]; exact closingEntry_accepted _ _ _ hsum hne
|
|
||||||
· intro x hx
|
|
||||||
simp only [List.mem_append] at hx
|
|
||||||
rcases hx with hx | hx
|
|
||||||
· exact h.known x hx
|
|
||||||
· obtain ⟨-, hl, -⟩ := closingFor_lines L p ts id x hx
|
|
||||||
intro l hl'
|
|
||||||
rw [hl] at hl'
|
|
||||||
simp only [closingEntry, List.mem_append] at hl'
|
|
||||||
rcases hl' with hl' | hl'
|
|
||||||
· exact nominalCodes_sub _ _ (closings_account _ _ l hl')
|
|
||||||
· obtain ⟨y, hy, e⟩ := equityLines_account ts l hl'
|
|
||||||
rw [e]; exact (hts y hy).1
|
|
||||||
· intro d hd hcl
|
|
||||||
rcases closedOn_closeIn L p d.date hcl with hcl | ⟨-, hc⟩
|
|
||||||
· exact h.draftsOpen d hd hcl
|
|
||||||
· simp [hdrafts d hd] at hc
|
|
||||||
| record r => exact ⟨h.distinct, h.accepted, h.known, h.draftsOpen⟩
|
|
||||||
|
|
||||||
/-- **The invariant holds in every reachable state.** -/
|
|
||||||
theorem Reachable.inv {L : Ledger} (r : Reachable L) : Inv L := by
|
|
||||||
induction r with
|
|
||||||
| empty => exact ⟨trivial, by simp [Ledger.empty], by simp [Ledger.empty], by simp [Ledger.empty]⟩
|
|
||||||
| step _ s ih => exact s.inv ih
|
|
||||||
|
|
||||||
/-! ## The books always balance -/
|
|
||||||
|
|
||||||
/-- **The books net to zero**: in every reachable state, the posted lines'
|
|
||||||
debits equal their credits. -/
|
|
||||||
theorem Reachable.books_net_zero {L : Ledger} (r : Reachable L) : net (allLines L.posted) = 0 :=
|
|
||||||
net_allLines _ (fun e he => (r.inv.accepted e he).net_zero)
|
|
||||||
|
|
||||||
/-- The same through any day: the books as of any date balance too. -/
|
|
||||||
theorem Reachable.net_zero_through {L : Ledger} (r : Reachable L) (d : Nat) :
|
|
||||||
net (allLines (through d L.posted)) = 0 :=
|
|
||||||
net_allLines _ (fun e he => (r.inv.accepted e ((List.mem_filter.1 he).1)).net_zero)
|
|
||||||
|
|
||||||
theorem Reachable.covers {L : Ledger} (r : Reachable L) (es : List Entry)
|
|
||||||
(hs : ∀ e ∈ es, e ∈ L.posted) : ∀ l ∈ allLines es, l.account ∈ codes L.chart := by
|
|
||||||
intro l hl
|
|
||||||
obtain ⟨e, he, hl⟩ := mem_allLines.1 hl
|
|
||||||
exact r.inv.known e (hs e he) l hl
|
|
||||||
|
|
||||||
/-- **The trial balance balances**, in every reachable state. -/
|
|
||||||
theorem Reachable.trial_balance {L : Ledger} (r : Reachable L) :
|
|
||||||
tbDebits (allLines L.posted) (codes L.chart) = tbCredits (allLines L.posted) (codes L.chart) :=
|
|
||||||
trial_balance_balances _ _ r.inv.distinct (r.covers _ (fun _ h => h)) r.books_net_zero
|
|
||||||
|
|
||||||
/-- **The trial balance as of any date balances.** -/
|
|
||||||
theorem Reachable.trial_balance_through {L : Ledger} (r : Reachable L) (d : Nat) :
|
|
||||||
tbDebits (allLines (through d L.posted)) (codes L.chart)
|
|
||||||
= tbCredits (allLines (through d L.posted)) (codes L.chart) :=
|
|
||||||
trial_balance_balances _ _ r.inv.distinct
|
|
||||||
(r.covers _ (fun _ h => (List.mem_filter.1 h).1)) (r.net_zero_through d)
|
|
||||||
|
|
||||||
/-- **The accounting equation holds**, in every reachable state:
|
|
||||||
Assets = Liabilities + Equity + (Revenue - Expenses). -/
|
|
||||||
theorem Reachable.accounting_equation {L : Ledger} (r : Reachable L) :
|
|
||||||
let ls := allLines L.posted
|
|
||||||
typeTotal .asset ls L.chart
|
|
||||||
= typeTotal .liability ls L.chart + typeTotal .equity ls L.chart
|
|
||||||
+ (typeTotal .revenue ls L.chart - typeTotal .expense ls L.chart) :=
|
|
||||||
Bookspec.accounting_equation _ _ r.inv.distinct (r.covers _ (fun _ h => h)) r.books_net_zero
|
|
||||||
|
|
||||||
/-- **The balance sheet as of any date balances.** -/
|
|
||||||
theorem Reachable.accounting_equation_through {L : Ledger} (r : Reachable L) (d : Nat) :
|
|
||||||
let ls := allLines (through d L.posted)
|
|
||||||
typeTotal .asset ls L.chart
|
|
||||||
= typeTotal .liability ls L.chart + typeTotal .equity ls L.chart
|
|
||||||
+ (typeTotal .revenue ls L.chart - typeTotal .expense ls L.chart) :=
|
|
||||||
Bookspec.accounting_equation _ _ r.inv.distinct
|
|
||||||
(r.covers _ (fun _ h => (List.mem_filter.1 h).1)) (r.net_zero_through d)
|
|
||||||
|
|
||||||
/-! ## Posted is forever -/
|
|
||||||
|
|
||||||
/-- **Posting only appends.** No step edits, reorders, or removes a posted
|
|
||||||
entry. -/
|
|
||||||
theorem Step.extends {L L' : Ledger} (s : Step L L') : ∃ t, L'.posted = L.posted ++ t := by
|
|
||||||
cases s with
|
|
||||||
| addAccount => exact ⟨[], by simp⟩
|
|
||||||
| draft => exact ⟨[], by simp⟩
|
|
||||||
| post e => exact ⟨[e], rfl⟩
|
|
||||||
| reverse e _ r => exact ⟨[r], rfl⟩
|
|
||||||
| openPeriod => exact ⟨[], by simp⟩
|
|
||||||
| close p _ _ _ _ _ ts _ _ id => exact ⟨closingFor L p ts id, rfl⟩
|
|
||||||
| record => exact ⟨[], by simp⟩
|
|
||||||
|
|
||||||
/-- Any number of steps. -/
|
|
||||||
inductive Steps : Ledger → Ledger → Prop
|
|
||||||
| refl (L : Ledger) : Steps L L
|
|
||||||
| tail {L M N : Ledger} : Steps L M → Step M N → Steps L N
|
|
||||||
|
|
||||||
/-- **Posted is forever**: after any sequence of commands, the old log is
|
|
||||||
a prefix of the new one. Every posted entry keeps its place and its
|
|
||||||
lines. -/
|
|
||||||
theorem Steps.extends {L L' : Ledger} (s : Steps L L') : ∃ t, L'.posted = L.posted ++ t := by
|
|
||||||
induction s with
|
|
||||||
| refl => exact ⟨[], by simp⟩
|
|
||||||
| tail _ s ih =>
|
|
||||||
obtain ⟨t, ht⟩ := ih
|
|
||||||
obtain ⟨u, hu⟩ := s.extends
|
|
||||||
exact ⟨t ++ u, by rw [hu, ht, List.append_assoc]⟩
|
|
||||||
|
|
||||||
/-! ## Closed periods are frozen -/
|
|
||||||
|
|
||||||
/-- Entries dated in a period. -/
|
|
||||||
def within (p : Period) (es : List Entry) : List Entry := es.filter (fun e => p.contains e.date)
|
|
||||||
|
|
||||||
theorem within_append (p : Period) (xs ys : List Entry) :
|
|
||||||
within p (xs ++ ys) = within p xs ++ within p ys := List.filter_append ..
|
|
||||||
|
|
||||||
/-- A closed period stays closed. -/
|
|
||||||
theorem Step.keeps_closed {L L' : Ledger} (s : Step L L') (q : Period) (hq : q ∈ L.periods)
|
|
||||||
(hc : q.closed = true) : q ∈ L'.periods := by
|
|
||||||
cases s with
|
|
||||||
| openPeriod p _ => exact List.mem_cons_of_mem _ hq
|
|
||||||
| close p _ hopen =>
|
|
||||||
have : q ≠ p := fun e => by subst e; simp [hopen] at hc
|
|
||||||
exact List.mem_map.2 ⟨q, hq, by simp [this]⟩
|
|
||||||
| _ => exact hq
|
|
||||||
|
|
||||||
/-- **A step never changes what a closed period contains.** -/
|
|
||||||
theorem Step.frozen {L L' : Ledger} (s : Step L L') (q : Period) (hq : q ∈ L.periods)
|
|
||||||
(hc : q.closed = true) : within q L'.posted = within q L.posted := by
|
|
||||||
have nothing : ∀ e : Entry, ¬ L.closedOn e.date → within q [e] = [] := by
|
|
||||||
intro e he
|
|
||||||
simp only [within, List.filter_cons, List.filter_nil]
|
|
||||||
split
|
|
||||||
· exact absurd ⟨q, hq, hc, by assumption⟩ he
|
|
||||||
· rfl
|
|
||||||
cases s with
|
|
||||||
| post e _ _ _ hcl => rw [within_append, nothing e hcl, List.append_nil]
|
|
||||||
| reverse e _ r _ hcl => rw [within_append, nothing r hcl, List.append_nil]
|
|
||||||
| close p hp hopen hdrafts hother hlater ts hts hsum id =>
|
|
||||||
rw [within_append]
|
|
||||||
have : within q (closingFor L p ts id) = [] := by
|
|
||||||
unfold closingFor
|
|
||||||
split
|
|
||||||
· rfl
|
|
||||||
· simp only [within, List.filter_cons, List.filter_nil]
|
|
||||||
rw [hother q hq hc]; rfl
|
|
||||||
rw [this, List.append_nil]
|
|
||||||
| _ => rfl
|
|
||||||
|
|
||||||
/-- **Closed periods are frozen**: once a period is closed, no sequence of
|
|
||||||
commands adds, removes, or changes an entry dated in it. -/
|
|
||||||
theorem Steps.frozen {L L' : Ledger} (s : Steps L L') (q : Period) (hq : q ∈ L.periods)
|
|
||||||
(hc : q.closed = true) : within q L'.posted = within q L.posted ∧ q ∈ L'.periods := by
|
|
||||||
induction s with
|
|
||||||
| refl => exact ⟨rfl, hq⟩
|
|
||||||
| tail _ s ih =>
|
|
||||||
obtain ⟨h1, h2⟩ := ih
|
|
||||||
exact ⟨(s.frozen q h2 hc).trans h1, s.keeps_closed q h2 hc⟩
|
|
||||||
|
|
||||||
/-! ## Closing -/
|
|
||||||
|
|
||||||
/-- **After a close, every revenue and expense account is zero through the
|
|
||||||
period's last day.** (The trial balance still balances: the close is a
|
|
||||||
step, and `Reachable.trial_balance` covers every step.) -/
|
|
||||||
theorem close_zeroes {L : Ledger} (r : Reachable L) (p : Period)
|
|
||||||
(ts : List (Nat × Int)) (hts : ∀ x ∈ ts, x.1 ∈ codes L.chart ∧ x.1 ∉ nominalCodes L.chart)
|
|
||||||
(id : Nat) (a : Nat) (ha : a ∈ nominalCodes L.chart) :
|
|
||||||
movement a (allLines (through p.stop (L.posted ++ closingFor L p ts id))) = 0 := by
|
|
||||||
have hd := nominalCodes_distinct _ r.inv.distinct
|
|
||||||
have hta : ∀ x ∈ ts, x.1 ≠ a := fun x hx e => (hts x hx).2 (e ▸ ha)
|
|
||||||
rw [through, List.filter_append, ← through, allLines_append]
|
|
||||||
unfold closingFor
|
|
||||||
split
|
|
||||||
· rename_i hnil
|
|
||||||
simp only [List.filter_nil, allLines, List.append_nil]
|
|
||||||
exact closings_eq_nil _ _ hnil a ha
|
|
||||||
· simp only [List.filter_cons, Nat.le_refl, decide_true, ite_true, List.filter_nil, allLines,
|
|
||||||
List.append_nil]
|
|
||||||
exact closingEntry_zeroes _ ts _ hd a ha hta
|
|
||||||
|
|
||||||
/-- **A close credits equity with exactly the period's net income**:
|
|
||||||
revenue minus expenses through its last day, split among the targets. -/
|
|
||||||
theorem close_credits_net_income (L : Ledger) (p : Period) (ts : List (Nat × Int))
|
|
||||||
(hsum : total (ts.map Prod.snd)
|
|
||||||
= net (closings (nominalCodes L.chart) (allLines (through p.stop L.posted)))) :
|
|
||||||
let ls := allLines (through p.stop L.posted)
|
|
||||||
total (ts.map Prod.snd) = typeTotal .revenue ls L.chart - typeTotal .expense ls L.chart := by
|
|
||||||
intro ls
|
|
||||||
rw [hsum, net_closings_is_net_income]
|
|
||||||
|
|
||||||
/-! ## Period statements add up -/
|
|
||||||
|
|
||||||
/-- Entries dated after day `b`, through day `c`. -/
|
|
||||||
def between (b c : Nat) (es : List Entry) : List Entry :=
|
|
||||||
es.filter (fun e => decide (b < e.date ∧ e.date ≤ c))
|
|
||||||
|
|
||||||
theorem movement_split (a b c : Nat) (h : b ≤ c) (es : List Entry) :
|
|
||||||
movement a (allLines (through c es))
|
|
||||||
= movement a (allLines (through b es)) + movement a (allLines (between b c es)) := by
|
|
||||||
induction es with
|
|
||||||
| nil => rfl
|
|
||||||
| cons e es ih =>
|
|
||||||
simp only [through, between] at ih ⊢
|
|
||||||
by_cases h1 : e.date ≤ b
|
|
||||||
· have h2 : e.date ≤ c := by omega
|
|
||||||
have h3 : ¬ (b < e.date ∧ e.date ≤ c) := by omega
|
|
||||||
rw [List.filter_cons_of_pos (by simpa using h2), List.filter_cons_of_pos (by simpa using h1),
|
|
||||||
List.filter_cons_of_neg (by simpa using h3)]
|
|
||||||
simp only [allLines, movement_append, ih]; omega
|
|
||||||
· by_cases h2 : e.date ≤ c
|
|
||||||
· have h3 : b < e.date ∧ e.date ≤ c := by omega
|
|
||||||
rw [List.filter_cons_of_pos (by simpa using h2), List.filter_cons_of_neg (by simpa using h1),
|
|
||||||
List.filter_cons_of_pos (by simpa using h3)]
|
|
||||||
simp only [allLines, movement_append, ih]; omega
|
|
||||||
· have h3 : ¬ (b < e.date ∧ e.date ≤ c) := by omega
|
|
||||||
rw [List.filter_cons_of_neg (by simpa using h2), List.filter_cons_of_neg (by simpa using h1),
|
|
||||||
List.filter_cons_of_neg (by simpa using h3)]
|
|
||||||
exact ih
|
|
||||||
|
|
||||||
theorem typeTotal_split (t : AccountType) (x y z : List Line)
|
|
||||||
(h : ∀ a, movement a x = movement a y + movement a z) (c : Chart) :
|
|
||||||
typeTotal t x c = typeTotal t y c + typeTotal t z c := by
|
|
||||||
induction c with
|
|
||||||
| nil => rfl
|
|
||||||
| cons p c ih =>
|
|
||||||
obtain ⟨a, t'⟩ := p
|
|
||||||
simp only [typeTotal, h a, ih]
|
|
||||||
split
|
|
||||||
· cases t' <;> simp [balance, AccountType.normal, signed] <;> omega
|
|
||||||
· omega
|
|
||||||
|
|
||||||
/-- **Period statements add up**: every account type's total (so revenue,
|
|
||||||
expenses, and net income) through day `c` is its total through day `b`
|
|
||||||
plus its total for the days after `b` through `c`. A year's P&L is the sum
|
|
||||||
of its months'. -/
|
|
||||||
theorem statements_add_up (t : AccountType) (b c : Nat) (h : b ≤ c) (es : List Entry)
|
|
||||||
(chart : Chart) :
|
|
||||||
typeTotal t (allLines (through c es)) chart
|
|
||||||
= typeTotal t (allLines (through b es)) chart
|
|
||||||
+ typeTotal t (allLines (between b c es)) chart :=
|
|
||||||
typeTotal_split t _ _ _ (fun a => movement_split a b c h es) chart
|
|
||||||
|
|
||||||
/-! ## The statement of cash flows foots (rules.md B-14 rule 9) -/
|
|
||||||
|
|
||||||
/-- Some entries' cash, summed, is the movement of the cash accounts over
|
|
||||||
their lines, when the cash tag follows the account. -/
|
|
||||||
theorem cashTotal_is_movement (F : List Line → Line → Flow) (cash : List Nat)
|
|
||||||
(hd : Distinct cash) (hF : ∀ e l, F e l = .cash ↔ l.account ∈ cash) :
|
|
||||||
∀ xs : List Entry,
|
|
||||||
cashTotal F (xs.map Entry.lines) = sumOver (fun a => movement a (allLines xs)) cash := by
|
|
||||||
intro xs
|
|
||||||
induction xs with
|
|
||||||
| nil => simpa [cashTotal, allLines] using (sumOver_zero cash).symm
|
|
||||||
| cons x xs ih =>
|
|
||||||
simp only [List.map_cons, cashTotal, allLines, movement_append]
|
|
||||||
rw [sumOver_add, ← ih, cashOf, secNet_cash_is_movement (F x.lines) cash hd (hF x.lines)]
|
|
||||||
|
|
||||||
/-- **The statement of cash flows foots**, in every reachable state: over
|
|
||||||
the posted entries dated after day `b` through day `c` (any selection of
|
|
||||||
them, `p`: the engine leaves out closing entries), operating + investing +
|
|
||||||
financing is the movement of the cash accounts over the same entries, for
|
|
||||||
any tagging whose cash tag follows the account, and whatever a member or
|
|
||||||
lender handled for each entry (`C`, rule 11) as long as it nets to zero
|
|
||||||
(`constructive_nets`). Every posted entry is accepted (`Reachable.inv`),
|
|
||||||
so each nets to zero. -/
|
|
||||||
theorem Reachable.cashflow_foots {L : Ledger} (r : Reachable L) (b c : Nat) (p : Entry → Bool)
|
|
||||||
(F : List Line → Line → Flow) (C : List Line → List Int) (hC : ∀ e, sum3 (C e) = 0)
|
|
||||||
(cash : List Nat) (hd : Distinct cash)
|
|
||||||
(hF : ∀ e l, F e l = .cash ↔ l.account ∈ cash) :
|
|
||||||
let xs := (between b c L.posted).filter p
|
|
||||||
sectionTotal F C 0 (xs.map Entry.lines) + sectionTotal F C 1 (xs.map Entry.lines)
|
|
||||||
+ sectionTotal F C 2 (xs.map Entry.lines)
|
|
||||||
= sumOver (fun a => movement a (allLines xs)) cash := by
|
|
||||||
intro xs
|
|
||||||
rw [cashflow_sections_sum_to_cash_change, cashTotal_is_movement F cash hd hF]
|
|
||||||
· intro e he
|
|
||||||
obtain ⟨x, hx, rfl⟩ := List.mem_map.1 he
|
|
||||||
have hb := (List.mem_filter.1 hx).1
|
|
||||||
exact (r.inv.accepted x (List.mem_filter.1 hb).1).net_zero
|
|
||||||
· exact fun e _ => hC e
|
|
||||||
|
|
||||||
/-- **The sections are the change in the cash accounts' balances**: with
|
|
||||||
every entry between `b` and `c` counted, the three sections sum to the
|
|
||||||
cash accounts' movement through `c` less their movement through `b`
|
|
||||||
(their ending balance less their beginning balance). -/
|
|
||||||
theorem Reachable.cashflow_is_change_in_cash {L : Ledger} (r : Reachable L) (b c : Nat)
|
|
||||||
(hbc : b ≤ c) (F : List Line → Line → Flow) (C : List Line → List Int)
|
|
||||||
(hC : ∀ e, sum3 (C e) = 0) (cash : List Nat) (hd : Distinct cash)
|
|
||||||
(hF : ∀ e l, F e l = .cash ↔ l.account ∈ cash) :
|
|
||||||
let es := (between b c L.posted).map Entry.lines
|
|
||||||
sectionTotal F C 0 es + sectionTotal F C 1 es + sectionTotal F C 2 es
|
|
||||||
= sumOver (fun a => movement a (allLines (through c L.posted))) cash
|
|
||||||
- sumOver (fun a => movement a (allLines (through b L.posted))) cash := by
|
|
||||||
intro es
|
|
||||||
have h := r.cashflow_foots b c (fun _ => true) F C hC cash hd hF
|
|
||||||
have hall : (between b c L.posted).filter (fun _ => true) = between b c L.posted :=
|
|
||||||
List.filter_eq_self.2 (fun _ _ => rfl)
|
|
||||||
simp only [hall] at h
|
|
||||||
rw [h]
|
|
||||||
have hs : ∀ a, movement a (allLines (through c L.posted))
|
|
||||||
= movement a (allLines (through b L.posted)) + movement a (allLines (between b c L.posted)) :=
|
|
||||||
fun a => movement_split a b c hbc L.posted
|
|
||||||
rw [sumOver_congr _ _ cash (fun a _ => hs a), sumOver_add]
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- **A reversal and its original in one period drop out together**: for
|
|
||||||
an entry and its reversal (lines flipped, tagged as the original's, and
|
|
||||||
what was handled for it mirrored, `constructive_neg`), the sum of their
|
|
||||||
flows in each section is zero. -/
|
|
||||||
theorem cashflow_reversal_nets (F : List Line → Line → Flow) (C : List Line → List Int)
|
|
||||||
(e : List Line) (hF : ∀ l ∈ e, F (reversal e) l.reverse = F e l)
|
|
||||||
(hC : C (reversal e) = (C e).map (fun x => -x)) (k : Nat) :
|
|
||||||
sectionTotal F C k [e, reversal e] = 0 := by
|
|
||||||
simp only [sectionTotal, stmt_flows_reversal F C e hF hC, getD_neg]
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-! ## How the books are kept changes no figure -/
|
|
||||||
|
|
||||||
/-- **A record changes nothing the books hold**: a settings change, a
|
|
||||||
convention, a decision, an open item or an approval leaves the chart, every
|
|
||||||
posted entry, every draft and every period exactly as they were. (The
|
|
||||||
`record` step touches only `records` by definition: this pins the model.
|
|
||||||
The engine's side is checked in Rust by `settings_replay_is_pure`.
|
|
||||||
Settings that change how the same entries are presented, such as the
|
|
||||||
fiscal year, are outside the model.) -/
|
|
||||||
theorem Step.record_changes_no_figure (L : Ledger) (r : Nat) :
|
|
||||||
let L' : Ledger := { L with records := r :: L.records }
|
|
||||||
L'.chart = L.chart ∧ L'.posted = L.posted ∧ L'.drafts = L.drafts ∧ L'.periods = L.periods :=
|
|
||||||
⟨rfl, rfl, rfl, rfl⟩
|
|
||||||
|
|
||||||
/-- **Any number of records changes no figure**, and they keep the
|
|
||||||
invariant: a reachable state plus records is reachable, with the same
|
|
||||||
books. -/
|
|
||||||
theorem Reachable.records {L : Ledger} (h : Reachable L) (rs : List Nat) :
|
|
||||||
Reachable { L with records := rs ++ L.records } := by
|
|
||||||
induction rs with
|
|
||||||
| nil => simpa using h
|
|
||||||
| cons r rs ih => exact Reachable.step ih (Step.record _ r)
|
|
||||||
|
|
||||||
end Bookspec
|
|
||||||
@@ -1,683 +0,0 @@
|
|||||||
import Bookspec.Entry
|
|
||||||
|
|
||||||
/-!
|
|
||||||
# Balances and statements
|
|
||||||
|
|
||||||
Mirrors `kernel::normal_balance`, `kernel::columns`,
|
|
||||||
`Ledger::account_balance`, and `reports::trial_balance`; and the statement
|
|
||||||
of cash flows (`kernel::take`, `kernel::apply_cash`, `kernel::entry_flows`,
|
|
||||||
`kernel::constructive`, `cashflow::cash_flow`'s footing and reconciliation
|
|
||||||
checks).
|
|
||||||
-/
|
|
||||||
|
|
||||||
namespace Bookspec
|
|
||||||
|
|
||||||
/-- A chart of accounts: each account's code and type. -/
|
|
||||||
abbrev Chart := List (Nat × AccountType)
|
|
||||||
|
|
||||||
/-- The chart's codes. -/
|
|
||||||
def codes (c : Chart) : List Nat := c.map Prod.fst
|
|
||||||
|
|
||||||
@[simp] theorem codes_nil : codes [] = [] := rfl
|
|
||||||
@[simp] theorem codes_cons (a : Nat) (t : AccountType) (c : Chart) :
|
|
||||||
codes ((a, t) :: c) = a :: codes c := rfl
|
|
||||||
|
|
||||||
/-- An account's balance on its normal side (`kernel::normal_balance`). -/
|
|
||||||
def balance (t : AccountType) (m : Int) : Int := signed t.normal m
|
|
||||||
|
|
||||||
/-- **A normal-side line raises a balance by its amount; an opposite-side
|
|
||||||
line lowers it.** -/
|
|
||||||
theorem balance_after_line (t : AccountType) (m : Int) (s : Side) (a : Int) :
|
|
||||||
balance t (m + signed s a) = balance t m + (if s = t.normal then a else -a) := by
|
|
||||||
cases t <;> cases s <;> simp [balance, AccountType.normal, signed] <;> omega
|
|
||||||
|
|
||||||
/-- The total balance of every account of type `t`. -/
|
|
||||||
def typeTotal (t : AccountType) (ls : List Line) : Chart → Int
|
|
||||||
| [] => 0
|
|
||||||
| (a, t') :: rest => (if t' = t then balance t' (movement a ls) else 0) + typeTotal t ls rest
|
|
||||||
|
|
||||||
/-- The accounting equation's two sides differ by the net movement of the
|
|
||||||
chart's accounts. -/
|
|
||||||
theorem equation_gap (ls : List Line) (c : Chart) :
|
|
||||||
typeTotal .asset ls c + typeTotal .expense ls c
|
|
||||||
- typeTotal .liability ls c - typeTotal .equity ls c - typeTotal .revenue ls c
|
|
||||||
= sumOver (fun a => movement a ls) (codes c) := by
|
|
||||||
induction c with
|
|
||||||
| nil => rfl
|
|
||||||
| cons x c ih =>
|
|
||||||
obtain ⟨a, t⟩ := x
|
|
||||||
cases t <;> simp [typeTotal, balance, AccountType.normal, signed] <;> omega
|
|
||||||
|
|
||||||
/-- **The accounting equation.** For lines that net to zero, over a chart
|
|
||||||
with distinct codes covering every line's account:
|
|
||||||
Assets = Liabilities + Equity + (Revenue - Expenses). -/
|
|
||||||
theorem accounting_equation (ls : List Line) (c : Chart) (hd : Distinct (codes c))
|
|
||||||
(hcov : ∀ l ∈ ls, l.account ∈ codes c) (hnet : net ls = 0) :
|
|
||||||
typeTotal .asset ls c
|
|
||||||
= typeTotal .liability ls c + typeTotal .equity ls c
|
|
||||||
+ (typeTotal .revenue ls c - typeTotal .expense ls c) := by
|
|
||||||
have := equation_gap ls c
|
|
||||||
rw [sum_movements _ hd ls hcov, hnet] at this
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-! ## Trial balance -/
|
|
||||||
|
|
||||||
/-- The debit column for a movement (`kernel::columns`). -/
|
|
||||||
def colDebit (m : Int) : Int := if 0 < m then m else 0
|
|
||||||
/-- The credit column for a movement (`kernel::columns`). -/
|
|
||||||
def colCredit (m : Int) : Int := if 0 < m then 0 else -m
|
|
||||||
|
|
||||||
theorem columns_spec (m : Int) :
|
|
||||||
0 ≤ colDebit m ∧ 0 ≤ colCredit m ∧ (colDebit m = 0 ∨ colCredit m = 0)
|
|
||||||
∧ colDebit m - colCredit m = m := by
|
|
||||||
unfold colDebit colCredit; split <;> omega
|
|
||||||
|
|
||||||
/-- The trial balance's debit column total. -/
|
|
||||||
def tbDebits (ls : List Line) (as : List Nat) : Int := sumOver (fun a => colDebit (movement a ls)) as
|
|
||||||
/-- The trial balance's credit column total. -/
|
|
||||||
def tbCredits (ls : List Line) (as : List Nat) : Int :=
|
|
||||||
sumOver (fun a => colCredit (movement a ls)) as
|
|
||||||
|
|
||||||
theorem tb_gap (ls : List Line) (as : List Nat) :
|
|
||||||
tbDebits ls as - tbCredits ls as = sumOver (fun a => movement a ls) as := by
|
|
||||||
induction as with
|
|
||||||
| nil => rfl
|
|
||||||
| cons a as ih =>
|
|
||||||
simp only [tbDebits, tbCredits, sumOver_cons] at ih ⊢
|
|
||||||
have := (columns_spec (movement a ls)).2.2.2
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- **The trial balance balances** for lines that net to zero. -/
|
|
||||||
theorem trial_balance_balances (ls : List Line) (as : List Nat) (hd : Distinct as)
|
|
||||||
(hcov : ∀ l ∈ ls, l.account ∈ as) (hnet : net ls = 0) :
|
|
||||||
tbDebits ls as = tbCredits ls as := by
|
|
||||||
have := tb_gap ls as
|
|
||||||
rw [sum_movements as hd ls hcov, hnet] at this
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- And conversely: a balanced trial balance means the lines net to zero.
|
|
||||||
The trial balance is a complete check of the books' arithmetic. -/
|
|
||||||
theorem trial_balance_iff (ls : List Line) (as : List Nat) (hd : Distinct as)
|
|
||||||
(hcov : ∀ l ∈ ls, l.account ∈ as) :
|
|
||||||
tbDebits ls as = tbCredits ls as ↔ net ls = 0 := by
|
|
||||||
have := tb_gap ls as
|
|
||||||
rw [sum_movements as hd ls hcov] at this
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-! ## The statement of cash flows (rules.md B-14)
|
|
||||||
|
|
||||||
Mirrors `kernel::Flow`, `kernel::take`, `kernel::apply_cash` and
|
|
||||||
`kernel::entry_flows`, and the identities `cashflow::cash_flow` checks at
|
|
||||||
runtime: the sections sum to the cash accounts' change, a reversal's flows
|
|
||||||
are its original's negated, and net income reconciles to operating cash.
|
|
||||||
|
|
||||||
Lines are tagged per entry (`F : List Line → Line → Flow`), as the engine
|
|
||||||
tags them: a revenue or expense line is operating in one entry and
|
|
||||||
investing in another (rule 4), and a reversal is tagged by its chain's
|
|
||||||
root. The theorems hold for every such tagging; their hypotheses say what
|
|
||||||
the engine guarantees (the cash tag follows the account; revenue and
|
|
||||||
expense lines are operating or investing; a reversal's lines take its
|
|
||||||
original's tags). A line the engine splits between two sections (the bill
|
|
||||||
split, rule 12) is two lines here; a split keeps the entry's net. -/
|
|
||||||
|
|
||||||
/-- Where a line falls on the statement of cash flows (`kernel::Flow`). -/
|
|
||||||
inductive Flow where
|
|
||||||
| cash
|
|
||||||
| operating
|
|
||||||
| investing
|
|
||||||
| financing
|
|
||||||
deriving DecidableEq, Repr
|
|
||||||
|
|
||||||
/-- The net movement of the lines `f` tags `k`. -/
|
|
||||||
def secNet (f : Line → Flow) (k : Flow) : List Line → Int
|
|
||||||
| [] => 0
|
|
||||||
| l :: ls => (if f l = k then l.signed else 0) + secNet f k ls
|
|
||||||
|
|
||||||
@[simp] theorem secNet_nil (f : Line → Flow) (k : Flow) : secNet f k [] = 0 := rfl
|
|
||||||
@[simp] theorem secNet_cons (f : Line → Flow) (k : Flow) (l : Line) (ls : List Line) :
|
|
||||||
secNet f k (l :: ls) = (if f l = k then l.signed else 0) + secNet f k ls := rfl
|
|
||||||
|
|
||||||
/-- **Every line is in exactly one place**: an entry's net is its cash plus
|
|
||||||
its three sections' movements. -/
|
|
||||||
theorem net_splits_by_flow (f : Line → Flow) (ls : List Line) :
|
|
||||||
net ls = secNet f .cash ls + secNet f .operating ls + secNet f .investing ls
|
|
||||||
+ secNet f .financing ls := by
|
|
||||||
induction ls with
|
|
||||||
| nil => rfl
|
|
||||||
| cons l ls ih =>
|
|
||||||
simp only [net_cons, secNet_cons, ih]
|
|
||||||
cases f l <;> simp <;> omega
|
|
||||||
|
|
||||||
/-- The cash one section takes from what is left (`kernel::take`): only a
|
|
||||||
section moving against the cash, never more than its own movement. -/
|
|
||||||
def take (rem n : Int) : Int :=
|
|
||||||
if 0 < rem ∧ n < 0 then min rem (-n) else if rem < 0 ∧ 0 < n then max rem (-n) else 0
|
|
||||||
|
|
||||||
/-- `kernel::apply_cash`: the cash applied to each movement in order, and
|
|
||||||
what is left. -/
|
|
||||||
def applyCash : Int → List Int → List Int × Int
|
|
||||||
| rem, [] => ([], rem)
|
|
||||||
| rem, n :: ns => (take rem n :: (applyCash (rem - take rem n) ns).1,
|
|
||||||
(applyCash (rem - take rem n) ns).2)
|
|
||||||
|
|
||||||
/-- A sum of integers. -/
|
|
||||||
def isum : List Int → Int
|
|
||||||
| [] => 0
|
|
||||||
| x :: xs => x + isum xs
|
|
||||||
|
|
||||||
@[simp] theorem isum_nil : isum [] = 0 := rfl
|
|
||||||
@[simp] theorem isum_cons (x : Int) (xs : List Int) : isum (x :: xs) = x + isum xs := rfl
|
|
||||||
|
|
||||||
theorem isum_append (xs ys : List Int) : isum (xs ++ ys) = isum xs + isum ys := by
|
|
||||||
induction xs with
|
|
||||||
| nil => simp
|
|
||||||
| cons x xs ih => simp only [List.cons_append, isum_cons, ih]; omega
|
|
||||||
|
|
||||||
theorem isum_neg (xs : List Int) : isum (xs.map (fun x => -x)) = -isum xs := by
|
|
||||||
induction xs with
|
|
||||||
| nil => rfl
|
|
||||||
| cons x xs ih => simp only [List.map_cons, isum_cons, ih]; omega
|
|
||||||
|
|
||||||
/-- Applying cash invents and loses nothing: the parts and what is left
|
|
||||||
sum to the cash. -/
|
|
||||||
theorem applyCash_conserves (rem : Int) (ns : List Int) :
|
|
||||||
isum (applyCash rem ns).1 + (applyCash rem ns).2 = rem := by
|
|
||||||
induction ns generalizing rem with
|
|
||||||
| nil => simp [applyCash]
|
|
||||||
| cons n ns ih =>
|
|
||||||
simp only [applyCash, isum_cons]
|
|
||||||
have := ih (rem - take rem n)
|
|
||||||
omega
|
|
||||||
|
|
||||||
theorem applyCash_length (rem : Int) (ns : List Int) :
|
|
||||||
(applyCash rem ns).1.length = ns.length := by
|
|
||||||
induction ns generalizing rem with
|
|
||||||
| nil => rfl
|
|
||||||
| cons n ns ih => simp [applyCash, ih]
|
|
||||||
|
|
||||||
theorem take_zero (n : Int) : take 0 n = 0 := by
|
|
||||||
simp [take]
|
|
||||||
|
|
||||||
/-- With no cash, nothing is applied and nothing is left. -/
|
|
||||||
theorem applyCash_zero (ns : List Int) : (applyCash 0 ns).2 = 0 := by
|
|
||||||
induction ns with
|
|
||||||
| nil => rfl
|
|
||||||
| cons n ns ih => simp [applyCash, take_zero, ih]
|
|
||||||
|
|
||||||
/-- Cash in is used up when the movements against it cover it. -/
|
|
||||||
theorem applyCash_exhausts_pos (rem : Int) (ns : List Int) (h0 : 0 ≤ rem)
|
|
||||||
(h : rem + isum ns ≤ 0) : (applyCash rem ns).2 = 0 := by
|
|
||||||
induction ns generalizing rem with
|
|
||||||
| nil => simp [applyCash] at h ⊢; omega
|
|
||||||
| cons n ns ih =>
|
|
||||||
simp only [applyCash]
|
|
||||||
simp only [isum_cons] at h
|
|
||||||
by_cases hr : rem = 0
|
|
||||||
· subst hr; simp [take_zero, applyCash_zero]
|
|
||||||
· by_cases hn : n < 0
|
|
||||||
· by_cases hle : -n ≤ rem
|
|
||||||
· have ht : take rem n = -n := by
|
|
||||||
simp only [take]; rw [ite_eq_left (by omega)]; omega
|
|
||||||
rw [ht]; exact ih _ (by omega) (by omega)
|
|
||||||
· have ht : take rem n = rem := by
|
|
||||||
simp only [take]; rw [ite_eq_left (by omega)]; omega
|
|
||||||
rw [ht, Int.sub_self]; exact applyCash_zero ns
|
|
||||||
· have ht : take rem n = 0 := by
|
|
||||||
simp only [take]; rw [ite_eq_right (by omega), ite_eq_right (by omega)]
|
|
||||||
rw [ht, Int.sub_zero]; exact ih _ h0 (by omega)
|
|
||||||
|
|
||||||
/-- Cash out is used up when the movements against it cover it. -/
|
|
||||||
theorem applyCash_exhausts_neg (rem : Int) (ns : List Int) (h0 : rem ≤ 0)
|
|
||||||
(h : 0 ≤ rem + isum ns) : (applyCash rem ns).2 = 0 := by
|
|
||||||
induction ns generalizing rem with
|
|
||||||
| nil => simp [applyCash] at h ⊢; omega
|
|
||||||
| cons n ns ih =>
|
|
||||||
simp only [applyCash]
|
|
||||||
simp only [isum_cons] at h
|
|
||||||
by_cases hr : rem = 0
|
|
||||||
· subst hr; simp [take_zero, applyCash_zero]
|
|
||||||
· by_cases hn : 0 < n
|
|
||||||
· by_cases hle : rem ≤ -n
|
|
||||||
· have ht : take rem n = -n := by
|
|
||||||
simp only [take]; rw [ite_eq_right (by omega), ite_eq_left (by omega)]; omega
|
|
||||||
rw [ht]; exact ih _ (by omega) (by omega)
|
|
||||||
· have ht : take rem n = rem := by
|
|
||||||
simp only [take]; rw [ite_eq_right (by omega), ite_eq_left (by omega)]; omega
|
|
||||||
rw [ht, Int.sub_self]; exact applyCash_zero ns
|
|
||||||
· have ht : take rem n = 0 := by
|
|
||||||
simp only [take]; rw [ite_eq_right (by omega), ite_eq_right (by omega)]
|
|
||||||
rw [ht, Int.sub_zero]; exact ih _ h0 (by omega)
|
|
||||||
|
|
||||||
/-- **Balanced, the cash is used up**: when the cash and the movements sum
|
|
||||||
to zero, nothing is left over. -/
|
|
||||||
theorem applyCash_exhausts (rem : Int) (ns : List Int) (h : rem + isum ns = 0) :
|
|
||||||
(applyCash rem ns).2 = 0 := by
|
|
||||||
rcases Int.le_total 0 rem with h0 | h0
|
|
||||||
· exact applyCash_exhausts_pos rem ns h0 (by omega)
|
|
||||||
· exact applyCash_exhausts_neg rem ns h0 (by omega)
|
|
||||||
|
|
||||||
theorem take_neg (r n : Int) : take (-r) (-n) = -take r n := by
|
|
||||||
simp only [take]
|
|
||||||
by_cases h1 : 0 < r ∧ n < 0
|
|
||||||
· rw [ite_eq_right (by omega), ite_eq_left (by omega), ite_eq_left h1]; omega
|
|
||||||
· by_cases h2 : r < 0 ∧ 0 < n
|
|
||||||
· rw [ite_eq_left (by omega), ite_eq_right h1, ite_eq_left h2]; omega
|
|
||||||
· rw [ite_eq_right (by omega), ite_eq_right (by omega), ite_eq_right h1, ite_eq_right h2]; omega
|
|
||||||
|
|
||||||
/-- Applying minus the cash to minus the movements gives minus the parts:
|
|
||||||
what lets a reversal mirror its original. -/
|
|
||||||
theorem applyCash_neg (rem : Int) (ns : List Int) :
|
|
||||||
applyCash (-rem) (ns.map (fun x => -x))
|
|
||||||
= ((applyCash rem ns).1.map (fun x => -x), -(applyCash rem ns).2) := by
|
|
||||||
induction ns generalizing rem with
|
|
||||||
| nil => rfl
|
|
||||||
| cons n ns ih =>
|
|
||||||
simp only [List.map_cons, applyCash, take_neg]
|
|
||||||
have : -rem - -take rem n = -(rem - take rem n) := by omega
|
|
||||||
rw [this, ih]
|
|
||||||
|
|
||||||
/-- An entry's cash: the net of the lines tagged cash. -/
|
|
||||||
def cashOf (F : List Line → Line → Flow) (e : List Line) : Int := secNet (F e) .cash e
|
|
||||||
|
|
||||||
/-- An entry's section movements, `[operating, investing, financing]`. -/
|
|
||||||
def moves (F : List Line → Line → Flow) (e : List Line) : List Int :=
|
|
||||||
[secNet (F e) .operating e, secNet (F e) .investing e, secNet (F e) .financing e]
|
|
||||||
|
|
||||||
/-- An entry's cash flows per section (`kernel::entry_flows`). -/
|
|
||||||
def flows (F : List Line → Line → Flow) (e : List Line) : List Int :=
|
|
||||||
(applyCash (cashOf F e) (moves F e)).1
|
|
||||||
|
|
||||||
/-- **An entry's cash flows sum to its cash** (`entry_flows_sum`): for a
|
|
||||||
balanced entry, the sections' cash is exactly the cash that moved. -/
|
|
||||||
theorem entry_flows_sum (F : List Line → Line → Flow) (e : List Line) (h : net e = 0) :
|
|
||||||
isum (flows F e) = cashOf F e := by
|
|
||||||
have hb := net_splits_by_flow (F e) e
|
|
||||||
have hx : (applyCash (cashOf F e) (moves F e)).2 = 0 := by
|
|
||||||
apply applyCash_exhausts
|
|
||||||
simp only [moves, cashOf, isum_cons, isum_nil]
|
|
||||||
omega
|
|
||||||
have := applyCash_conserves (cashOf F e) (moves F e)
|
|
||||||
simp only [flows]
|
|
||||||
omega
|
|
||||||
|
|
||||||
theorem flows_length (F : List Line → Line → Flow) (e : List Line) : (flows F e).length = 3 := by
|
|
||||||
simp [flows, applyCash_length, moves]
|
|
||||||
|
|
||||||
theorem secNet_reversal (f g : Line → Flow) (k : Flow) (ls : List Line)
|
|
||||||
(h : ∀ l ∈ ls, g l.reverse = f l) : secNet g k (reversal ls) = -secNet f k ls := by
|
|
||||||
induction ls with
|
|
||||||
| nil => rfl
|
|
||||||
| cons l ls ih =>
|
|
||||||
simp only [reversal, List.map_cons] at ih ⊢
|
|
||||||
simp only [secNet_cons, h l (by simp), Line.reverse_signed,
|
|
||||||
ih (fun m hm => h m (by simp [hm]))]
|
|
||||||
split <;> omega
|
|
||||||
|
|
||||||
/-- **A reversal's flows are its original's, negated** (`flows_reversal`),
|
|
||||||
when its lines take its original's tags: what lets an entry and its
|
|
||||||
reversal drop out of the statement together (rules.md B-14 rule 6). -/
|
|
||||||
theorem flows_reversal (F : List Line → Line → Flow) (e : List Line)
|
|
||||||
(hF : ∀ l ∈ e, F (reversal e) l.reverse = F e l) :
|
|
||||||
flows F (reversal e) = (flows F e).map (fun x => -x) := by
|
|
||||||
simp only [flows, cashOf, moves]
|
|
||||||
rw [secNet_reversal (F e) (F (reversal e)) .cash e hF,
|
|
||||||
secNet_reversal (F e) (F (reversal e)) .operating e hF,
|
|
||||||
secNet_reversal (F e) (F (reversal e)) .investing e hF,
|
|
||||||
secNet_reversal (F e) (F (reversal e)) .financing e hF]
|
|
||||||
have := applyCash_neg (secNet (F e) .cash e)
|
|
||||||
[secNet (F e) .operating e, secNet (F e) .investing e, secNet (F e) .financing e]
|
|
||||||
simp only [List.map_cons, List.map_nil] at this
|
|
||||||
rw [this]
|
|
||||||
|
|
||||||
theorem isum_three (xs : List Int) (h : xs.length = 3) :
|
|
||||||
isum xs = xs.getD 0 0 + xs.getD 1 0 + xs.getD 2 0 := by
|
|
||||||
match xs, h with
|
|
||||||
| [a, b, c], _ => simp; omega
|
|
||||||
|
|
||||||
/-! ### What a member or lender pays or receives for the company (rule 11)
|
|
||||||
|
|
||||||
`kernel::constructive` (D-42): what is left unsettled of an entry's
|
|
||||||
kinds that can pay or receive for the company (`s`, a financing liability
|
|
||||||
or a member's capital) settles what is left of the kinds they pay or
|
|
||||||
receive for (`u`), as if through the cash accounts. The engine sums the
|
|
||||||
parts by section into an entry's constructive triple; the entry theorems
|
|
||||||
below take any triple that nets to zero (and, for reversals, mirrors),
|
|
||||||
which `constructive_nets` and `constructive_neg` give the kernel's. -/
|
|
||||||
|
|
||||||
/-- The credits among some movements, `Σ max(-m, 0)`: what cash in can
|
|
||||||
settle. -/
|
|
||||||
def creditTotal : List Int → Int
|
|
||||||
| [] => 0
|
|
||||||
| m :: ms => max (-m) 0 + creditTotal ms
|
|
||||||
|
|
||||||
/-- The debits among some movements, `Σ max(m, 0)`: what cash out can
|
|
||||||
settle. -/
|
|
||||||
def debitTotal : List Int → Int
|
|
||||||
| [] => 0
|
|
||||||
| m :: ms => max m 0 + debitTotal ms
|
|
||||||
|
|
||||||
theorem creditTotal_nonneg (ms : List Int) : 0 ≤ creditTotal ms := by
|
|
||||||
induction ms with
|
|
||||||
| nil => simp [creditTotal]
|
|
||||||
| cons m ms ih => simp only [creditTotal]; omega
|
|
||||||
|
|
||||||
theorem debitTotal_nonneg (ms : List Int) : 0 ≤ debitTotal ms := by
|
|
||||||
induction ms with
|
|
||||||
| nil => simp [debitTotal]
|
|
||||||
| cons m ms ih => simp only [debitTotal]; omega
|
|
||||||
|
|
||||||
theorem creditTotal_neg (ms : List Int) :
|
|
||||||
creditTotal (ms.map (fun x => -x)) = debitTotal ms := by
|
|
||||||
induction ms with
|
|
||||||
| nil => rfl
|
|
||||||
| cons m ms ih => simp only [List.map_cons, creditTotal, debitTotal, ih]; omega
|
|
||||||
|
|
||||||
theorem debitTotal_neg (ms : List Int) :
|
|
||||||
debitTotal (ms.map (fun x => -x)) = creditTotal ms := by
|
|
||||||
induction ms with
|
|
||||||
| nil => rfl
|
|
||||||
| cons m ms ih =>
|
|
||||||
simp only [List.map_cons, creditTotal, debitTotal, ih]
|
|
||||||
|
|
||||||
/-- Cash in is used up when the credits cover it (whatever the debits). -/
|
|
||||||
theorem applyCash_exhausts_in (rem : Int) (ns : List Int) (h0 : 0 ≤ rem)
|
|
||||||
(h : rem ≤ creditTotal ns) : (applyCash rem ns).2 = 0 := by
|
|
||||||
induction ns generalizing rem with
|
|
||||||
| nil => simp [applyCash, creditTotal] at h ⊢; omega
|
|
||||||
| cons n ns ih =>
|
|
||||||
simp only [applyCash]
|
|
||||||
simp only [creditTotal] at h
|
|
||||||
by_cases hr : rem = 0
|
|
||||||
· subst hr; simp [take_zero, applyCash_zero]
|
|
||||||
· by_cases hn : n < 0
|
|
||||||
· by_cases hle : -n ≤ rem
|
|
||||||
· have ht : take rem n = -n := by
|
|
||||||
simp only [take]; rw [ite_eq_left (by omega)]; omega
|
|
||||||
rw [ht]; exact ih _ (by omega) (by omega)
|
|
||||||
· have ht : take rem n = rem := by
|
|
||||||
simp only [take]; rw [ite_eq_left (by omega)]; omega
|
|
||||||
rw [ht, Int.sub_self]; exact applyCash_zero ns
|
|
||||||
· have ht : take rem n = 0 := by
|
|
||||||
simp only [take]; rw [ite_eq_right (by omega), ite_eq_right (by omega)]
|
|
||||||
rw [ht, Int.sub_zero]; exact ih _ h0 (by omega)
|
|
||||||
|
|
||||||
/-- Cash out is used up when the debits cover it (whatever the credits). -/
|
|
||||||
theorem applyCash_exhausts_out (rem : Int) (ns : List Int) (h0 : rem ≤ 0)
|
|
||||||
(h : -rem ≤ debitTotal ns) : (applyCash rem ns).2 = 0 := by
|
|
||||||
induction ns generalizing rem with
|
|
||||||
| nil => simp [applyCash, debitTotal] at h ⊢; omega
|
|
||||||
| cons n ns ih =>
|
|
||||||
simp only [applyCash]
|
|
||||||
simp only [debitTotal] at h
|
|
||||||
by_cases hr : rem = 0
|
|
||||||
· subst hr; simp [take_zero, applyCash_zero]
|
|
||||||
· by_cases hn : 0 < n
|
|
||||||
· by_cases hle : rem ≤ -n
|
|
||||||
· have ht : take rem n = -n := by
|
|
||||||
simp only [take]; rw [ite_eq_right (by omega), ite_eq_left (by omega)]; omega
|
|
||||||
rw [ht]; exact ih _ (by omega) (by omega)
|
|
||||||
· have ht : take rem n = rem := by
|
|
||||||
simp only [take]; rw [ite_eq_right (by omega), ite_eq_left (by omega)]; omega
|
|
||||||
rw [ht, Int.sub_self]; exact applyCash_zero ns
|
|
||||||
· have ht : take rem n = 0 := by
|
|
||||||
simp only [take]; rw [ite_eq_right (by omega), ite_eq_right (by omega)]
|
|
||||||
rw [ht, Int.sub_zero]; exact ih _ h0 (by omega)
|
|
||||||
|
|
||||||
/-- Which way the money went (`kernel::OnBehalf`). -/
|
|
||||||
inductive OnBehalf where
|
|
||||||
| paid
|
|
||||||
| received
|
|
||||||
deriving DecidableEq, Repr
|
|
||||||
|
|
||||||
/-- The other way: what a reversal does. -/
|
|
||||||
def OnBehalf.mirror : OnBehalf → OnBehalf
|
|
||||||
| .paid => .received
|
|
||||||
| .received => .paid
|
|
||||||
|
|
||||||
/-- The cash the sources take, + in (`kernel::constructive`): paid, the
|
|
||||||
smaller of the sources' credits and the uses' debits; received, minus the
|
|
||||||
smaller of the sources' debits and the uses' credits. -/
|
|
||||||
def onBehalfCash : OnBehalf → List Int → List Int → Int
|
|
||||||
| .paid, s, u => min (creditTotal s) (debitTotal u)
|
|
||||||
| .received, s, u => -min (debitTotal s) (creditTotal u)
|
|
||||||
|
|
||||||
/-- `kernel::constructive`: the sources' parts and the uses' parts. -/
|
|
||||||
def constructive (w : OnBehalf) (s u : List Int) : List Int × List Int :=
|
|
||||||
((applyCash (onBehalfCash w s u) s).1, (applyCash (-onBehalfCash w s u) u).1)
|
|
||||||
|
|
||||||
/-- **What a member or lender handled for the company nets to zero**
|
|
||||||
(`constructive_nets`; Kani `constructive_nets_to_zero`): the sources take
|
|
||||||
exactly what the uses give, so an entry's flows still sum to its cash. -/
|
|
||||||
theorem constructive_nets (w : OnBehalf) (s u : List Int) :
|
|
||||||
isum (constructive w s u).1 + isum (constructive w s u).2 = 0 := by
|
|
||||||
have hs := applyCash_conserves (onBehalfCash w s u) s
|
|
||||||
have hu := applyCash_conserves (-onBehalfCash w s u) u
|
|
||||||
have cs := creditTotal_nonneg s
|
|
||||||
have ds := debitTotal_nonneg s
|
|
||||||
have cu := creditTotal_nonneg u
|
|
||||||
have du := debitTotal_nonneg u
|
|
||||||
have ⟨xs, xu⟩ : (applyCash (onBehalfCash w s u) s).2 = 0 ∧
|
|
||||||
(applyCash (-onBehalfCash w s u) u).2 = 0 := by
|
|
||||||
cases w with
|
|
||||||
| paid =>
|
|
||||||
simp only [onBehalfCash]
|
|
||||||
exact ⟨applyCash_exhausts_in _ _ (by omega) (by omega),
|
|
||||||
applyCash_exhausts_out _ _ (by omega) (by omega)⟩
|
|
||||||
| received =>
|
|
||||||
simp only [onBehalfCash]
|
|
||||||
exact ⟨applyCash_exhausts_out _ _ (by omega) (by omega),
|
|
||||||
applyCash_exhausts_in _ _ (by omega) (by omega)⟩
|
|
||||||
simp only [constructive]
|
|
||||||
omega
|
|
||||||
|
|
||||||
theorem onBehalfCash_mirror (w : OnBehalf) (s u : List Int) :
|
|
||||||
onBehalfCash w.mirror (s.map (fun x => -x)) (u.map (fun x => -x))
|
|
||||||
= -onBehalfCash w s u := by
|
|
||||||
cases w <;> simp only [OnBehalf.mirror, onBehalfCash, creditTotal_neg, debitTotal_neg] <;> omega
|
|
||||||
|
|
||||||
/-- **The other way, minus the sources and uses give minus the parts**
|
|
||||||
(`constructive_neg`; Kani `constructive_mirrors`): a payment made for the
|
|
||||||
company and its reversal drop out together. -/
|
|
||||||
theorem constructive_neg (w : OnBehalf) (s u : List Int) :
|
|
||||||
constructive w.mirror (s.map (fun x => -x)) (u.map (fun x => -x))
|
|
||||||
= ((constructive w s u).1.map (fun x => -x), (constructive w s u).2.map (fun x => -x)) := by
|
|
||||||
simp only [constructive, onBehalfCash_mirror]
|
|
||||||
rw [applyCash_neg (onBehalfCash w s u) s, applyCash_neg (-onBehalfCash w s u) u]
|
|
||||||
|
|
||||||
/-- Three sections' figures, summed. -/
|
|
||||||
def sum3 (xs : List Int) : Int := xs.getD 0 0 + xs.getD 1 0 + xs.getD 2 0
|
|
||||||
|
|
||||||
@[simp] theorem sum3_cons (a b c : Int) : sum3 [a, b, c] = a + b + c := by
|
|
||||||
simp [sum3]
|
|
||||||
|
|
||||||
/-- **The kernel's parts, by section, net to zero**: split the uses into
|
|
||||||
operating ones then investing ones (`kernel::constructive` takes them in
|
|
||||||
that order), and the triple (operating, investing, financing = the
|
|
||||||
sources) the engine adds to an entry's flows sums to zero. -/
|
|
||||||
theorem constructive_sections_net (w : OnBehalf) (s uo ui : List Int) :
|
|
||||||
sum3 [isum ((constructive w s (uo ++ ui)).2.take uo.length),
|
|
||||||
isum ((constructive w s (uo ++ ui)).2.drop uo.length),
|
|
||||||
isum (constructive w s (uo ++ ui)).1] = 0 := by
|
|
||||||
have h := constructive_nets w s (uo ++ ui)
|
|
||||||
have hs := isum_append ((constructive w s (uo ++ ui)).2.take uo.length)
|
|
||||||
((constructive w s (uo ++ ui)).2.drop uo.length)
|
|
||||||
rw [List.take_append_drop] at hs
|
|
||||||
rw [sum3_cons]
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- Passes add: two triples that net to zero add to one that does. -/
|
|
||||||
theorem sum3_add (xs ys : List Int) (hx : sum3 xs = 0) (hy : sum3 ys = 0) :
|
|
||||||
sum3 [xs.getD 0 0 + ys.getD 0 0, xs.getD 1 0 + ys.getD 1 0, xs.getD 2 0 + ys.getD 2 0] = 0 := by
|
|
||||||
rw [sum3_cons]
|
|
||||||
simp only [sum3] at hx hy
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- Two triples, added section by section. -/
|
|
||||||
def add3 (xs ys : List Int) : List Int :=
|
|
||||||
[xs.getD 0 0 + ys.getD 0 0, xs.getD 1 0 + ys.getD 1 0, xs.getD 2 0 + ys.getD 2 0]
|
|
||||||
|
|
||||||
/-- An entry's flows on the statement: its cash flows, plus `C e`, what a
|
|
||||||
member or lender paid or received for it, by section (rule 11). -/
|
|
||||||
def stmtFlows (F : List Line → Line → Flow) (C : List Line → List Int) (e : List Line) :
|
|
||||||
List Int :=
|
|
||||||
add3 (flows F e) (C e)
|
|
||||||
|
|
||||||
/-- **An entry's statement flows sum to its cash** when what was handled
|
|
||||||
for it nets to zero: rule 11 moves no cash (`stmt_flows_sum`). -/
|
|
||||||
theorem stmt_flows_sum (F : List Line → Line → Flow) (C : List Line → List Int)
|
|
||||||
(e : List Line) (h : net e = 0) (hC : sum3 (C e) = 0) :
|
|
||||||
sum3 (stmtFlows F C e) = cashOf F e := by
|
|
||||||
have he := entry_flows_sum F e h
|
|
||||||
rw [isum_three _ (flows_length F e)] at he
|
|
||||||
simp only [stmtFlows, add3, sum3_cons]
|
|
||||||
simp only [sum3] at hC
|
|
||||||
omega
|
|
||||||
|
|
||||||
theorem getD_neg (xs : List Int) (k : Nat) :
|
|
||||||
(xs.map (fun x => -x)).getD k 0 = -xs.getD k 0 := by
|
|
||||||
induction xs generalizing k with
|
|
||||||
| nil => simp
|
|
||||||
| cons x xs ih => cases k with
|
|
||||||
| zero => simp
|
|
||||||
| succ k => simp only [List.map_cons, List.getD_cons_succ, ih]
|
|
||||||
|
|
||||||
/-- **A reversal's statement flows are its original's, negated**, when its
|
|
||||||
lines take the original's tags and what was handled for it mirrors
|
|
||||||
(`constructive_neg`). -/
|
|
||||||
theorem stmt_flows_reversal (F : List Line → Line → Flow) (C : List Line → List Int)
|
|
||||||
(e : List Line) (hF : ∀ l ∈ e, F (reversal e) l.reverse = F e l)
|
|
||||||
(hC : C (reversal e) = (C e).map (fun x => -x)) :
|
|
||||||
stmtFlows F C (reversal e) = (stmtFlows F C e).map (fun x => -x) := by
|
|
||||||
simp only [stmtFlows, add3, flows_reversal F e hF, hC, getD_neg, List.map_cons,
|
|
||||||
List.map_nil, List.cons.injEq, and_true]
|
|
||||||
refine ⟨?_, ?_, ?_⟩ <;> omega
|
|
||||||
|
|
||||||
/-- A section's total over some entries: the sum of each entry's cash in
|
|
||||||
it (`k` = 0 operating, 1 investing, 2 financing), what a member or lender
|
|
||||||
handled for it included. -/
|
|
||||||
def sectionTotal (F : List Line → Line → Flow) (C : List Line → List Int) (k : Nat) :
|
|
||||||
List (List Line) → Int
|
|
||||||
| [] => 0
|
|
||||||
| e :: es => (stmtFlows F C e).getD k 0 + sectionTotal F C k es
|
|
||||||
|
|
||||||
/-- The entries' cash, summed. -/
|
|
||||||
def cashTotal (F : List Line → Line → Flow) : List (List Line) → Int
|
|
||||||
| [] => 0
|
|
||||||
| e :: es => cashOf F e + cashTotal F es
|
|
||||||
|
|
||||||
/-- **The sections sum to the change in cash** (plan P3): over any balanced
|
|
||||||
entries, operating + investing + financing is the entries' cash, exactly,
|
|
||||||
whatever a member or lender handled for them as long as it nets to zero
|
|
||||||
within each entry (rule 11, `constructive_nets`). The statement's footing
|
|
||||||
(rules.md B-14 rule 9) can't fail on posted entries. -/
|
|
||||||
theorem cashflow_sections_sum_to_cash_change (F : List Line → Line → Flow)
|
|
||||||
(C : List Line → List Int) (es : List (List Line)) (h : ∀ e ∈ es, net e = 0)
|
|
||||||
(hC : ∀ e ∈ es, sum3 (C e) = 0) :
|
|
||||||
sectionTotal F C 0 es + sectionTotal F C 1 es + sectionTotal F C 2 es = cashTotal F es := by
|
|
||||||
induction es with
|
|
||||||
| nil => rfl
|
|
||||||
| cons e es ih =>
|
|
||||||
simp only [sectionTotal, cashTotal]
|
|
||||||
have he := stmt_flows_sum F C e (h e (by simp)) (hC e (by simp))
|
|
||||||
simp only [sum3] at he
|
|
||||||
have := ih (fun x hx => h x (by simp [hx])) (fun x hx => hC x (by simp [hx]))
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- When the cash tag follows the account (a line is cash exactly when
|
|
||||||
its account is a cash account), an entry's cash is the movement of the
|
|
||||||
cash accounts. -/
|
|
||||||
theorem secNet_cash_is_movement (f : Line → Flow) (cash : List Nat) (hd : Distinct cash)
|
|
||||||
(hf : ∀ l, f l = .cash ↔ l.account ∈ cash) (ls : List Line) :
|
|
||||||
secNet f .cash ls = sumOver (fun a => movement a ls) cash := by
|
|
||||||
induction ls with
|
|
||||||
| nil => simpa using (sumOver_zero cash).symm
|
|
||||||
| cons l ls ih =>
|
|
||||||
simp only [secNet_cons, movement_cons]
|
|
||||||
rw [sumOver_add, ← ih]
|
|
||||||
by_cases hc : l.account ∈ cash
|
|
||||||
· rw [ite_eq_left ((hf l).2 hc), sumOver_indicator l.account l.signed cash hd hc]
|
|
||||||
· rw [ite_eq_right (fun e => hc ((hf l).1 e)), sumOver_indicator_absent l.account l.signed cash hc]
|
|
||||||
|
|
||||||
/-! ### Net income reconciled to operating cash (rules.md B-14 rule 8) -/
|
|
||||||
|
|
||||||
/-- Net movement of the lines on revenue and expense accounts (`pl`)
|
|
||||||
tagged `k` (`sel = true`), or on balance-sheet accounts (`sel = false`). -/
|
|
||||||
def plNet (pl : Nat → Bool) (sel : Bool) (f : Line → Flow) (k : Flow) : List Line → Int
|
|
||||||
| [] => 0
|
|
||||||
| l :: ls => (if pl l.account = sel ∧ f l = k then l.signed else 0) + plNet pl sel f k ls
|
|
||||||
|
|
||||||
/-- Net movement of every revenue and expense line. -/
|
|
||||||
def plAll (pl : Nat → Bool) : List Line → Int
|
|
||||||
| [] => 0
|
|
||||||
| l :: ls => (if pl l.account = true then l.signed else 0) + plAll pl ls
|
|
||||||
|
|
||||||
/-- Net income: revenue less expenses, `-(net of the P&L lines)`. -/
|
|
||||||
def netIncome (pl : Nat → Bool) (e : List Line) : Int := -plAll pl e
|
|
||||||
|
|
||||||
/-- G: the P&L lines rule 4 puts in investing (gains and losses). -/
|
|
||||||
def gainLoss (pl : Nat → Bool) (F : List Line → Line → Flow) (e : List Line) : Int :=
|
|
||||||
plNet pl true (F e) .investing e
|
|
||||||
|
|
||||||
/-- W: minus the movement of the operating balance-sheet lines. -/
|
|
||||||
def balanceChange (pl : Nat → Bool) (F : List Line → Line → Flow) (e : List Line) : Int :=
|
|
||||||
-plNet pl false (F e) .operating e
|
|
||||||
|
|
||||||
/-- X: what no cash, real or handled for the company, settled of the
|
|
||||||
operating movement. -/
|
|
||||||
def noncashOp (F : List Line → Line → Flow) (C : List Line → List Int) (e : List Line) : Int :=
|
|
||||||
secNet (F e) .operating e + (stmtFlows F C e).getD 0 0
|
|
||||||
|
|
||||||
theorem secNet_split (pl : Nat → Bool) (f : Line → Flow) (k : Flow) (ls : List Line) :
|
|
||||||
secNet f k ls = plNet pl true f k ls + plNet pl false f k ls := by
|
|
||||||
induction ls with
|
|
||||||
| nil => rfl
|
|
||||||
| cons l ls ih =>
|
|
||||||
simp only [secNet_cons, plNet, ih]
|
|
||||||
cases pl l.account <;> by_cases h : f l = k <;> simp [h] <;> omega
|
|
||||||
|
|
||||||
theorem plAll_split (pl : Nat → Bool) (f : Line → Flow) (ls : List Line)
|
|
||||||
(h : ∀ l ∈ ls, pl l.account = true → f l = .operating ∨ f l = .investing) :
|
|
||||||
plAll pl ls = plNet pl true f .operating ls + plNet pl true f .investing ls := by
|
|
||||||
induction ls with
|
|
||||||
| nil => rfl
|
|
||||||
| cons l ls ih =>
|
|
||||||
simp only [plAll, plNet, ih (fun m hm => h m (by simp [hm]))]
|
|
||||||
cases hp : pl l.account
|
|
||||||
· simp
|
|
||||||
· rcases h l (by simp) hp with hk | hk <;> simp [hk] <;> omega
|
|
||||||
|
|
||||||
/-- **Net income reconciles to operating cash**, entry by entry:
|
|
||||||
operating cash = NI + G + X + W, when revenue and expense lines are only
|
|
||||||
ever operating or investing (rules.md B-14 rule 8; ASC 230-10-45-28 to
|
|
||||||
45-30), whatever a member or lender handled for it (rule 11). -/
|
|
||||||
theorem operating_reconciliation_ties (pl : Nat → Bool) (F : List Line → Line → Flow)
|
|
||||||
(C : List Line → List Int) (e : List Line)
|
|
||||||
(h : ∀ l ∈ e, pl l.account = true → F e l = .operating ∨ F e l = .investing) :
|
|
||||||
(stmtFlows F C e).getD 0 0
|
|
||||||
= netIncome pl e + gainLoss pl F e + noncashOp F C e + balanceChange pl F e := by
|
|
||||||
have hs := secNet_split pl (F e) .operating e
|
|
||||||
have hp := plAll_split pl (F e) e h
|
|
||||||
simp only [netIncome, gainLoss, noncashOp, balanceChange]
|
|
||||||
omega
|
|
||||||
|
|
||||||
/-- The operating section's total over some entries. -/
|
|
||||||
def opTotal (pl : Nat → Bool) (F : List Line → Line → Flow) (C : List Line → List Int) :
|
|
||||||
List (List Line) → Int
|
|
||||||
| [] => 0
|
|
||||||
| e :: es => netIncome pl e + gainLoss pl F e + noncashOp F C e + balanceChange pl F e
|
|
||||||
+ opTotal pl F C es
|
|
||||||
|
|
||||||
/-- **The reconciliation schedule ties to the operating section** over any
|
|
||||||
entries: the section's total is NI + G + X + W summed. -/
|
|
||||||
theorem operating_reconciliation_ties_all (pl : Nat → Bool) (F : List Line → Line → Flow)
|
|
||||||
(C : List Line → List Int) (es : List (List Line))
|
|
||||||
(h : ∀ e ∈ es, ∀ l ∈ e, pl l.account = true → F e l = .operating ∨ F e l = .investing) :
|
|
||||||
sectionTotal F C 0 es = opTotal pl F C es := by
|
|
||||||
induction es with
|
|
||||||
| nil => rfl
|
|
||||||
| cons e es ih =>
|
|
||||||
simp only [sectionTotal, opTotal]
|
|
||||||
rw [operating_reconciliation_ties pl F C e (h e (by simp)),
|
|
||||||
ih (fun x hx => h x (by simp [hx]))]
|
|
||||||
|
|
||||||
end Bookspec
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{"version": "1.3.0",
|
|
||||||
"packagesDir": ".lake/packages",
|
|
||||||
"packages": [],
|
|
||||||
"name": "bookspec",
|
|
||||||
"lakeDir": ".lake",
|
|
||||||
"fixedToolchain": false}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
name = "bookspec"
|
|
||||||
defaultTargets = ["Bookspec"]
|
|
||||||
|
|
||||||
[[lean_lib]]
|
|
||||||
name = "Bookspec"
|
|
||||||
Loaded 100 of 234 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user