threeway 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
threeway-1.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bhavya Dhoot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,289 @@
1
+ Metadata-Version: 2.4
2
+ Name: threeway
3
+ Version: 1.1.0
4
+ Summary: Three-way reconciliation of purchase orders, goods received, and supplier invoices
5
+ Author: Bhavya Dhoot
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Bhavya-Dhoot/threeway
8
+ Project-URL: Issues, https://github.com/Bhavya-Dhoot/threeway/issues
9
+ Keywords: reconciliation,three-way-match,accounts-payable,procurement,invoice,erp
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Financial and Insurance Industry
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # threeway
23
+
24
+ **Reconcile a purchase order against goods received against the supplier invoice, and find out which lines disagree and why.**
25
+
26
+ [![tests](https://github.com/Bhavya-Dhoot/threeway/actions/workflows/tests.yml/badge.svg)](https://github.com/Bhavya-Dhoot/threeway/actions/workflows/tests.yml)
27
+ [![License: MIT](https://img.shields.io/badge/license-MIT-0F766E?style=flat-square)](LICENSE)
28
+ [![Python](https://img.shields.io/badge/python-%3E%3D3.9-8A8F98?style=flat-square)](pyproject.toml)
29
+ [![Tests](https://img.shields.io/badge/tests-106%20passing-0F766E?style=flat-square)](tests)
30
+ [![Dependencies](https://img.shields.io/badge/dependencies-none-0F766E?style=flat-square)](pyproject.toml)
31
+
32
+ Three systems will give you three different numbers for the same delivery. This
33
+ tells you which line is wrong and names the reason, in one command.
34
+
35
+ **No dependencies.** Standard library only — no pandas, no click. `Decimal`
36
+ throughout, so money never drifts.
37
+
38
+ ```console
39
+ $ threeway --po po.csv --grn grn.csv --invoice invoices.csv --only-exceptions
40
+
41
+ PO ITEM STATUS ORDERED RECEIVED INVOICED QTYVAR VALVAR REASON
42
+ -----------------------------------------------------------------------------------------------
43
+ PO-1003 WGT-300 exception 20 0 5 5 125.00 Invoiced but nothing received
44
+ PO-1004 WGT-400 exception 100 90 95 5 50.00 Billed for more than was received
45
+ PO-1005 WGT-500 exception 50 60 60 0 30.00 Received more than was ordered
46
+ PO-1006 WGT-600 exception 100 70 70 0 35.00 Short delivery against the order
47
+ PO-1007 WGT-700 exception 40 40 40 0 20.00 Invoice price differs from the PO price
48
+ PO-1008 WGT-800 exception 1000 1000 1000 0 20.00 Value gap with matching quantities
49
+ PO-9999 WGT-9999 exception 0 0 10 10 50.00 Receipt or invoice with no matching purchase order line
50
+
51
+ 13 lines | not_started=2 matched=3 check=1 exception=7 | exception value variance = 330.00
52
+ ```
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install git+https://github.com/Bhavya-Dhoot/threeway
58
+ ```
59
+
60
+ Or run it straight from a clone, with nothing installed at all:
61
+
62
+ ```bash
63
+ git clone https://github.com/Bhavya-Dhoot/threeway && cd threeway
64
+ PYTHONPATH=src python -m threeway.cli --po po.csv --grn grn.csv --invoice invoices.csv
65
+ ```
66
+
67
+ Python 3.9 or newer. Not on PyPI yet.
68
+
69
+ ## Usage
70
+
71
+ ```
72
+ threeway --po PO.csv --grn GRN.csv --invoice INV.csv
73
+ [--qty-tolerance N] [--value-tolerance N] [--pct-tolerance N]
74
+ [--format table|json|csv] [--only-exceptions] [--output FILE] [--quiet]
75
+ [--uom-map FILE] [--as-of DATE] [--cutoff-days N] [--config FILE]
76
+ ```
77
+
78
+ Column names are matched case-insensitively with the obvious aliases — `po_number`,
79
+ `po number`, `po`, `purchase_order` all work, as do `item`/`sku`, `qty`/`quantity`,
80
+ `price`/`unit_price`. If a required column is missing, it exits with the file name
81
+ and the columns it actually found, rather than guessing.
82
+
83
+ ### Exit codes
84
+
85
+ | Code | Meaning |
86
+ |---|---|
87
+ | `0` | No exceptions. Everything reconciled. |
88
+ | `1` | One or more exceptions found. |
89
+ | `2` | Usage or input error — bad file, missing column, unparseable number. |
90
+
91
+ Exit `1` is deliberate: it lets you gate a pipeline on a clean reconciliation.
92
+
93
+ ```bash
94
+ threeway --po po.csv --grn grn.csv --invoice inv.csv --quiet || echo "someone needs to look at this"
95
+ ```
96
+
97
+ ## How a line is judged
98
+
99
+ Every line is keyed on **PO number + item code**. If two of your systems disagree
100
+ about either, the line will not join and is reported as an exception — that is the
101
+ correct answer, not a bug, and it is usually the real problem worth finding.
102
+
103
+ Then, in order:
104
+
105
+ | Status | When |
106
+ |---|---|
107
+ | `not_started` | Nothing received and nothing invoiced yet. |
108
+ | `matched` | Quantity and value variance both inside tolerance. |
109
+ | `check` | Outside tolerance but within a small multiple of it. Worth a look, not an alarm. |
110
+ | `exception` | Everything else. |
111
+
112
+ An exception carries exactly one reason, chosen first-match-wins:
113
+
114
+ 1. Invoiced but nothing received
115
+ 2. No receipt and no invoice yet
116
+ 3. Billed for more than was received
117
+ 4. Received more than was ordered
118
+ 5. Short delivery against the order
119
+ 6. Invoice price differs from the PO price
120
+ 7. Value gap with matching quantities
121
+
122
+ Plus **Receipt or invoice with no matching purchase order line** for orphans.
123
+
124
+ ### Tolerances
125
+
126
+ Defaults are `--qty-tolerance 2`, `--value-tolerance 1.00`, `--pct-tolerance 0.5`.
127
+ They exist to forgive rounding. They must not forgive real gaps — if you find
128
+ yourself widening them until the exception list is empty, you have turned the tool
129
+ off rather than fixed the data.
130
+
131
+ ## Units of measure
132
+
133
+ A purchase order in cases and an invoice in eaches is not a discrepancy, it is
134
+ a conversion. If your files carry an optional `uom` column (aliases: `uom`,
135
+ `unit`, `unit_of_measure`, `units`), point `--uom-map` at a JSON or TOML file
136
+ that says how to convert:
137
+
138
+ ```json
139
+ {
140
+ "global": {
141
+ "CASE": { "unit": "EA", "factor": 12 }
142
+ },
143
+ "items": {
144
+ "WGT-1300": {
145
+ "BOX": { "unit": "EA", "factor": 24 }
146
+ }
147
+ }
148
+ }
149
+ ```
150
+
151
+ `global` rules apply to any item; `items` rules override `global` for a
152
+ specific item code. Which of the three files' units is treated as "the base"
153
+ is decided by majority: if two files agree on a unit and one doesn't, the
154
+ majority wins (a PO in cases against a GRN and invoice both in eaches means
155
+ eaches is the base, and the PO line gets converted). A factor that produces a
156
+ fractional quantity is kept exact as a `Decimal` - it is never rounded.
157
+
158
+ ```bash
159
+ threeway --po po.csv --grn grn.csv --invoice inv.csv --uom-map examples/uom_map.json
160
+ ```
161
+
162
+ If a line's units differ across files and no rule covers the conversion, that
163
+ is an exception with reason **"Units differ and no conversion rule applies"**.
164
+ This is checked after the orphan check but before any of the generic quantity
165
+ reasons - a unit mismatch is always the more useful thing to report.
166
+
167
+ See `examples/po_units.csv`, `examples/grn_units.csv`,
168
+ `examples/invoices_units.csv`, and `examples/uom_map.json` for a worked
169
+ example, including one line that converts cleanly and one that doesn't.
170
+
171
+ ## Timing and cut-off
172
+
173
+ If your files carry an optional date column (aliases: `date`, `posted_date`,
174
+ `document_date`, `received_date`, `invoice_date`, ISO `YYYY-MM-DD`), two flags
175
+ become available:
176
+
177
+ - `--as-of DATE` ignores any row dated after `DATE`. This is how you
178
+ reconcile a closed period as it stood at close, rather than as it stands
179
+ today.
180
+ - `--cutoff-days N` (default `0`) downgrades an exception to `check` - with
181
+ reason **"Timing difference within the cut-off window"** - when it is
182
+ explained by a gap between the receipt date and the invoice date of `N`
183
+ days or less. A gap of exactly `N` days downgrades; `N + 1` days does not.
184
+
185
+ ```bash
186
+ threeway --po examples/po_cutoff.csv --grn examples/grn_cutoff.csv \
187
+ --invoice examples/invoices_cutoff.csv --cutoff-days 3
188
+ ```
189
+
190
+ In `examples/grn_cutoff.csv` the goods are received on `2026-01-30`; in
191
+ `examples/invoices_cutoff.csv` the invoice is dated `2026-02-02` - a 3-day
192
+ gap. Run the example without `--cutoff-days` and it is an exception ("Billed
193
+ for more than was received"); with `--cutoff-days 3` or higher it downgrades
194
+ to `check`. The cut-off check never applies to an orphan line or a units
195
+ exception - those aren't timing problems.
196
+
197
+ ## Config file
198
+
199
+ Settings can live in a file instead of being retyped on every run:
200
+
201
+ ```bash
202
+ threeway --po po.csv --grn grn.csv --invoice inv.csv --config threeway.toml
203
+ ```
204
+
205
+ `--config` accepts JSON always, and TOML when running on Python 3.11+
206
+ (`tomllib` is part of the standard library from 3.11 onward; on 3.9/3.10, use
207
+ JSON). If `--config` isn't given, `threeway.toml` then `threeway.json` in the
208
+ current directory are tried automatically. A missing or malformed file exits
209
+ `2` and names the file and the problem.
210
+
211
+ ```toml
212
+ # threeway.toml
213
+ qty_tolerance = 5
214
+ value_tolerance = 2.00
215
+ pct_tolerance = 1.0
216
+ cutoff_days = 3
217
+
218
+ [uom_map.global.CASE]
219
+ unit = "EA"
220
+ factor = 12
221
+ ```
222
+
223
+ A config file can also override column-name aliases, for source systems that
224
+ use their own vocabulary:
225
+
226
+ ```json
227
+ {
228
+ "columns": {
229
+ "po_number": ["order_ref"],
230
+ "quantity": { "grn": ["delivered"] }
231
+ }
232
+ }
233
+ ```
234
+
235
+ `columns` overrides *add* aliases; the built-in ones (`po_number`, `po`,
236
+ `purchase_order`, and so on) still work alongside them.
237
+
238
+ ### Precedence
239
+
240
+ For every tolerance, `cutoff_days`, and the uom map, highest wins first:
241
+
242
+ | Priority | Source |
243
+ |---|---|
244
+ | 1 (highest) | An explicit command-line flag (`--qty-tolerance`, `--cutoff-days`, `--uom-map`, ...) |
245
+ | 2 | The config file (`--config`, or an auto-discovered `threeway.toml`/`threeway.json`) |
246
+ | 3 (lowest) | The built-in default |
247
+
248
+ See `examples/threeway.toml` for a complete example.
249
+
250
+ ## Why the reason matters more than the variance
251
+
252
+ A single 4% variance is not one problem. It is usually a unit mismatch on some
253
+ lines, a cut-off effect on others, and a genuine shortage on the rest — and until
254
+ you separate them you are negotiating with a supplier, or writing off stock, on a
255
+ number that is three different problems wearing one disguise.
256
+
257
+ That is why every line gets a reason rather than only a number.
258
+
259
+ ## Development
260
+
261
+ ```bash
262
+ git clone https://github.com/Bhavya-Dhoot/threeway
263
+ cd threeway
264
+ python -m pytest # 106 tests, no install needed
265
+ ```
266
+
267
+ Tests run straight from a clone — `pythonpath` is set in `pyproject.toml`.
268
+
269
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the zero-dependency rule and what
270
+ a bug-fix change is expected to include. See [`CHANGELOG.md`](CHANGELOG.md)
271
+ for release history.
272
+
273
+ ## If you would rather not run Python
274
+
275
+ The same logic exists as a spreadsheet, for the finance and ops people who need it
276
+ but do not want a terminal: [**The Three-Way
277
+ Match**](https://bhavyadhoot.gumroad.com/l/three-way-match) — an Excel workbook
278
+ with the same statuses and reasons, live formulas, nothing locked.
279
+
280
+ There is also a free written guide on why operational data disagrees in the first
281
+ place, and how to build a reconciliation that surfaces exceptions instead of
282
+ burying them: [**Why Your Numbers
283
+ Disagree**](https://bhavyadhoot.gumroad.com/l/numbers-disagree).
284
+
285
+ Neither is required. This tool is complete on its own and always will be.
286
+
287
+ ## License
288
+
289
+ [MIT](LICENSE) — Bhavya Dhoot
@@ -0,0 +1,268 @@
1
+ # threeway
2
+
3
+ **Reconcile a purchase order against goods received against the supplier invoice, and find out which lines disagree and why.**
4
+
5
+ [![tests](https://github.com/Bhavya-Dhoot/threeway/actions/workflows/tests.yml/badge.svg)](https://github.com/Bhavya-Dhoot/threeway/actions/workflows/tests.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-0F766E?style=flat-square)](LICENSE)
7
+ [![Python](https://img.shields.io/badge/python-%3E%3D3.9-8A8F98?style=flat-square)](pyproject.toml)
8
+ [![Tests](https://img.shields.io/badge/tests-106%20passing-0F766E?style=flat-square)](tests)
9
+ [![Dependencies](https://img.shields.io/badge/dependencies-none-0F766E?style=flat-square)](pyproject.toml)
10
+
11
+ Three systems will give you three different numbers for the same delivery. This
12
+ tells you which line is wrong and names the reason, in one command.
13
+
14
+ **No dependencies.** Standard library only — no pandas, no click. `Decimal`
15
+ throughout, so money never drifts.
16
+
17
+ ```console
18
+ $ threeway --po po.csv --grn grn.csv --invoice invoices.csv --only-exceptions
19
+
20
+ PO ITEM STATUS ORDERED RECEIVED INVOICED QTYVAR VALVAR REASON
21
+ -----------------------------------------------------------------------------------------------
22
+ PO-1003 WGT-300 exception 20 0 5 5 125.00 Invoiced but nothing received
23
+ PO-1004 WGT-400 exception 100 90 95 5 50.00 Billed for more than was received
24
+ PO-1005 WGT-500 exception 50 60 60 0 30.00 Received more than was ordered
25
+ PO-1006 WGT-600 exception 100 70 70 0 35.00 Short delivery against the order
26
+ PO-1007 WGT-700 exception 40 40 40 0 20.00 Invoice price differs from the PO price
27
+ PO-1008 WGT-800 exception 1000 1000 1000 0 20.00 Value gap with matching quantities
28
+ PO-9999 WGT-9999 exception 0 0 10 10 50.00 Receipt or invoice with no matching purchase order line
29
+
30
+ 13 lines | not_started=2 matched=3 check=1 exception=7 | exception value variance = 330.00
31
+ ```
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install git+https://github.com/Bhavya-Dhoot/threeway
37
+ ```
38
+
39
+ Or run it straight from a clone, with nothing installed at all:
40
+
41
+ ```bash
42
+ git clone https://github.com/Bhavya-Dhoot/threeway && cd threeway
43
+ PYTHONPATH=src python -m threeway.cli --po po.csv --grn grn.csv --invoice invoices.csv
44
+ ```
45
+
46
+ Python 3.9 or newer. Not on PyPI yet.
47
+
48
+ ## Usage
49
+
50
+ ```
51
+ threeway --po PO.csv --grn GRN.csv --invoice INV.csv
52
+ [--qty-tolerance N] [--value-tolerance N] [--pct-tolerance N]
53
+ [--format table|json|csv] [--only-exceptions] [--output FILE] [--quiet]
54
+ [--uom-map FILE] [--as-of DATE] [--cutoff-days N] [--config FILE]
55
+ ```
56
+
57
+ Column names are matched case-insensitively with the obvious aliases — `po_number`,
58
+ `po number`, `po`, `purchase_order` all work, as do `item`/`sku`, `qty`/`quantity`,
59
+ `price`/`unit_price`. If a required column is missing, it exits with the file name
60
+ and the columns it actually found, rather than guessing.
61
+
62
+ ### Exit codes
63
+
64
+ | Code | Meaning |
65
+ |---|---|
66
+ | `0` | No exceptions. Everything reconciled. |
67
+ | `1` | One or more exceptions found. |
68
+ | `2` | Usage or input error — bad file, missing column, unparseable number. |
69
+
70
+ Exit `1` is deliberate: it lets you gate a pipeline on a clean reconciliation.
71
+
72
+ ```bash
73
+ threeway --po po.csv --grn grn.csv --invoice inv.csv --quiet || echo "someone needs to look at this"
74
+ ```
75
+
76
+ ## How a line is judged
77
+
78
+ Every line is keyed on **PO number + item code**. If two of your systems disagree
79
+ about either, the line will not join and is reported as an exception — that is the
80
+ correct answer, not a bug, and it is usually the real problem worth finding.
81
+
82
+ Then, in order:
83
+
84
+ | Status | When |
85
+ |---|---|
86
+ | `not_started` | Nothing received and nothing invoiced yet. |
87
+ | `matched` | Quantity and value variance both inside tolerance. |
88
+ | `check` | Outside tolerance but within a small multiple of it. Worth a look, not an alarm. |
89
+ | `exception` | Everything else. |
90
+
91
+ An exception carries exactly one reason, chosen first-match-wins:
92
+
93
+ 1. Invoiced but nothing received
94
+ 2. No receipt and no invoice yet
95
+ 3. Billed for more than was received
96
+ 4. Received more than was ordered
97
+ 5. Short delivery against the order
98
+ 6. Invoice price differs from the PO price
99
+ 7. Value gap with matching quantities
100
+
101
+ Plus **Receipt or invoice with no matching purchase order line** for orphans.
102
+
103
+ ### Tolerances
104
+
105
+ Defaults are `--qty-tolerance 2`, `--value-tolerance 1.00`, `--pct-tolerance 0.5`.
106
+ They exist to forgive rounding. They must not forgive real gaps — if you find
107
+ yourself widening them until the exception list is empty, you have turned the tool
108
+ off rather than fixed the data.
109
+
110
+ ## Units of measure
111
+
112
+ A purchase order in cases and an invoice in eaches is not a discrepancy, it is
113
+ a conversion. If your files carry an optional `uom` column (aliases: `uom`,
114
+ `unit`, `unit_of_measure`, `units`), point `--uom-map` at a JSON or TOML file
115
+ that says how to convert:
116
+
117
+ ```json
118
+ {
119
+ "global": {
120
+ "CASE": { "unit": "EA", "factor": 12 }
121
+ },
122
+ "items": {
123
+ "WGT-1300": {
124
+ "BOX": { "unit": "EA", "factor": 24 }
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ `global` rules apply to any item; `items` rules override `global` for a
131
+ specific item code. Which of the three files' units is treated as "the base"
132
+ is decided by majority: if two files agree on a unit and one doesn't, the
133
+ majority wins (a PO in cases against a GRN and invoice both in eaches means
134
+ eaches is the base, and the PO line gets converted). A factor that produces a
135
+ fractional quantity is kept exact as a `Decimal` - it is never rounded.
136
+
137
+ ```bash
138
+ threeway --po po.csv --grn grn.csv --invoice inv.csv --uom-map examples/uom_map.json
139
+ ```
140
+
141
+ If a line's units differ across files and no rule covers the conversion, that
142
+ is an exception with reason **"Units differ and no conversion rule applies"**.
143
+ This is checked after the orphan check but before any of the generic quantity
144
+ reasons - a unit mismatch is always the more useful thing to report.
145
+
146
+ See `examples/po_units.csv`, `examples/grn_units.csv`,
147
+ `examples/invoices_units.csv`, and `examples/uom_map.json` for a worked
148
+ example, including one line that converts cleanly and one that doesn't.
149
+
150
+ ## Timing and cut-off
151
+
152
+ If your files carry an optional date column (aliases: `date`, `posted_date`,
153
+ `document_date`, `received_date`, `invoice_date`, ISO `YYYY-MM-DD`), two flags
154
+ become available:
155
+
156
+ - `--as-of DATE` ignores any row dated after `DATE`. This is how you
157
+ reconcile a closed period as it stood at close, rather than as it stands
158
+ today.
159
+ - `--cutoff-days N` (default `0`) downgrades an exception to `check` - with
160
+ reason **"Timing difference within the cut-off window"** - when it is
161
+ explained by a gap between the receipt date and the invoice date of `N`
162
+ days or less. A gap of exactly `N` days downgrades; `N + 1` days does not.
163
+
164
+ ```bash
165
+ threeway --po examples/po_cutoff.csv --grn examples/grn_cutoff.csv \
166
+ --invoice examples/invoices_cutoff.csv --cutoff-days 3
167
+ ```
168
+
169
+ In `examples/grn_cutoff.csv` the goods are received on `2026-01-30`; in
170
+ `examples/invoices_cutoff.csv` the invoice is dated `2026-02-02` - a 3-day
171
+ gap. Run the example without `--cutoff-days` and it is an exception ("Billed
172
+ for more than was received"); with `--cutoff-days 3` or higher it downgrades
173
+ to `check`. The cut-off check never applies to an orphan line or a units
174
+ exception - those aren't timing problems.
175
+
176
+ ## Config file
177
+
178
+ Settings can live in a file instead of being retyped on every run:
179
+
180
+ ```bash
181
+ threeway --po po.csv --grn grn.csv --invoice inv.csv --config threeway.toml
182
+ ```
183
+
184
+ `--config` accepts JSON always, and TOML when running on Python 3.11+
185
+ (`tomllib` is part of the standard library from 3.11 onward; on 3.9/3.10, use
186
+ JSON). If `--config` isn't given, `threeway.toml` then `threeway.json` in the
187
+ current directory are tried automatically. A missing or malformed file exits
188
+ `2` and names the file and the problem.
189
+
190
+ ```toml
191
+ # threeway.toml
192
+ qty_tolerance = 5
193
+ value_tolerance = 2.00
194
+ pct_tolerance = 1.0
195
+ cutoff_days = 3
196
+
197
+ [uom_map.global.CASE]
198
+ unit = "EA"
199
+ factor = 12
200
+ ```
201
+
202
+ A config file can also override column-name aliases, for source systems that
203
+ use their own vocabulary:
204
+
205
+ ```json
206
+ {
207
+ "columns": {
208
+ "po_number": ["order_ref"],
209
+ "quantity": { "grn": ["delivered"] }
210
+ }
211
+ }
212
+ ```
213
+
214
+ `columns` overrides *add* aliases; the built-in ones (`po_number`, `po`,
215
+ `purchase_order`, and so on) still work alongside them.
216
+
217
+ ### Precedence
218
+
219
+ For every tolerance, `cutoff_days`, and the uom map, highest wins first:
220
+
221
+ | Priority | Source |
222
+ |---|---|
223
+ | 1 (highest) | An explicit command-line flag (`--qty-tolerance`, `--cutoff-days`, `--uom-map`, ...) |
224
+ | 2 | The config file (`--config`, or an auto-discovered `threeway.toml`/`threeway.json`) |
225
+ | 3 (lowest) | The built-in default |
226
+
227
+ See `examples/threeway.toml` for a complete example.
228
+
229
+ ## Why the reason matters more than the variance
230
+
231
+ A single 4% variance is not one problem. It is usually a unit mismatch on some
232
+ lines, a cut-off effect on others, and a genuine shortage on the rest — and until
233
+ you separate them you are negotiating with a supplier, or writing off stock, on a
234
+ number that is three different problems wearing one disguise.
235
+
236
+ That is why every line gets a reason rather than only a number.
237
+
238
+ ## Development
239
+
240
+ ```bash
241
+ git clone https://github.com/Bhavya-Dhoot/threeway
242
+ cd threeway
243
+ python -m pytest # 106 tests, no install needed
244
+ ```
245
+
246
+ Tests run straight from a clone — `pythonpath` is set in `pyproject.toml`.
247
+
248
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the zero-dependency rule and what
249
+ a bug-fix change is expected to include. See [`CHANGELOG.md`](CHANGELOG.md)
250
+ for release history.
251
+
252
+ ## If you would rather not run Python
253
+
254
+ The same logic exists as a spreadsheet, for the finance and ops people who need it
255
+ but do not want a terminal: [**The Three-Way
256
+ Match**](https://bhavyadhoot.gumroad.com/l/three-way-match) — an Excel workbook
257
+ with the same statuses and reasons, live formulas, nothing locked.
258
+
259
+ There is also a free written guide on why operational data disagrees in the first
260
+ place, and how to build a reconciliation that surfaces exceptions instead of
261
+ burying them: [**Why Your Numbers
262
+ Disagree**](https://bhavyadhoot.gumroad.com/l/numbers-disagree).
263
+
264
+ Neither is required. This tool is complete on its own and always will be.
265
+
266
+ ## License
267
+
268
+ [MIT](LICENSE) — Bhavya Dhoot
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "threeway"
7
+ version = "1.1.0"
8
+ description = "Three-way reconciliation of purchase orders, goods received, and supplier invoices"
9
+ requires-python = ">=3.9"
10
+ readme = "README.md"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Bhavya Dhoot" }]
13
+ keywords = ["reconciliation", "three-way-match", "accounts-payable", "procurement", "invoice", "erp"]
14
+ dependencies = []
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Office/Business :: Financial :: Accounting",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ dev = ["pytest"]
25
+
26
+ [project.scripts]
27
+ threeway = "threeway.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/Bhavya-Dhoot/threeway"
31
+ Issues = "https://github.com/Bhavya-Dhoot/threeway/issues"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.pytest.ini_options]
37
+ pythonpath = ["src"]
38
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Package marker; version lives here so cli.py has a single source of truth for --version."""
2
+
3
+ __version__ = "1.1.0"