Skip to content

Commit ba4f599

Browse files
authored
✨ Add Invoice V4 (clearer field names, line items) (#107)
1 parent 89674ca commit ba4f599

File tree

13 files changed

+840
-226
lines changed

13 files changed

+840
-226
lines changed

mindee/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class CommandConfig(Generic[TypeDoc]):
2424
),
2525
"invoice": CommandConfig(
2626
help="Invoice",
27-
doc_class=documents.TypeInvoiceV3,
27+
doc_class=documents.TypeInvoiceV4,
2828
),
2929
"receipt": CommandConfig(
3030
help="Expense Receipt",

mindee/client.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
CustomV1,
77
FinancialV1,
88
InvoiceV3,
9+
InvoiceV4,
910
PassportV1,
1011
ReceiptV3,
1112
ReceiptV4,
@@ -199,6 +200,15 @@ def _init_default_endpoints(self) -> None:
199200
)
200201
],
201202
),
203+
(OTS_OWNER, InvoiceV4.__name__): DocumentConfig(
204+
document_type="invoice_v4",
205+
document_class=InvoiceV4,
206+
endpoints=[
207+
StandardEndpoint(
208+
url_name="invoices_beta", version="4", api_key=self.api_key
209+
)
210+
],
211+
),
202212
(OTS_OWNER, ReceiptV3.__name__): DocumentConfig(
203213
document_type="receipt_v3",
204214
document_class=ReceiptV3,
@@ -209,7 +219,7 @@ def _init_default_endpoints(self) -> None:
209219
],
210220
),
211221
(OTS_OWNER, ReceiptV4.__name__): DocumentConfig(
212-
document_type="receipt_v3",
222+
document_type="receipt_v4",
213223
document_class=ReceiptV4,
214224
endpoints=[
215225
StandardEndpoint(

mindee/documents/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
from mindee.documents.cropper import CropperV1, TypeCropperV1
33
from mindee.documents.custom import CustomV1, TypeCustomV1
44
from mindee.documents.financial import FinancialV1, TypeFinancialV1
5-
from mindee.documents.invoice import InvoiceV3, TypeInvoiceV3
5+
from mindee.documents.invoice import InvoiceV3, InvoiceV4, TypeInvoiceV3, TypeInvoiceV4
66
from mindee.documents.passport import PassportV1, TypePassportV1
77
from mindee.documents.receipt import ReceiptV3, ReceiptV4, TypeReceiptV3, TypeReceiptV4
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .invoice_v3 import InvoiceV3, TypeInvoiceV3
2+
from .invoice_v4 import InvoiceV4, TypeInvoiceV4

mindee/documents/invoice/checks.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
def taxes_match_total_incl(doc) -> bool:
2+
"""
3+
Check invoice matching rule between taxes and total_incl.
4+
5+
:return: True if rule matches, False otherwise
6+
"""
7+
# Ensure taxes and total_incl exist
8+
if not doc.taxes or not doc.total_amount.value:
9+
return False
10+
11+
# Reconstruct total_incl from taxes
12+
total_vat = 0.0
13+
reconstructed_total = 0.0
14+
for tax in doc.taxes:
15+
if tax.value is None or tax.rate is None or tax.rate == 0:
16+
return False
17+
total_vat += tax.value
18+
reconstructed_total += tax.value + 100 * tax.value / tax.rate
19+
20+
# Sanity check
21+
if total_vat <= 0:
22+
return False
23+
24+
# Crate epsilon
25+
eps = 1 / (100 * total_vat)
26+
if (
27+
doc.total_amount.value * (1 - eps) - 0.02
28+
<= reconstructed_total
29+
<= doc.total_amount.value * (1 + eps) + 0.02
30+
):
31+
for tax in doc.taxes:
32+
tax.confidence = 1
33+
doc.total_tax.confidence = 1.0
34+
doc.total_amount.confidence = 1.0
35+
return True
36+
return False
37+
38+
39+
def taxes_match_total_excl(doc) -> bool:
40+
"""
41+
Check invoice matching rule between taxes and total_excl.
42+
43+
:return: True if rule matches, False otherwise
44+
"""
45+
# Check taxes and total excl exist
46+
if len(doc.taxes) == 0 or doc.total_net.value is None:
47+
return False
48+
49+
# Reconstruct total excl from taxes
50+
total_vat = 0.0
51+
reconstructed_total = 0.0
52+
for tax in doc.taxes:
53+
if tax.value is None or tax.rate is None or tax.rate == 0:
54+
return False
55+
total_vat += tax.value
56+
reconstructed_total += 100 * tax.value / tax.rate
57+
58+
# Sanity check
59+
if total_vat <= 0:
60+
return False
61+
62+
# Crate epsilon
63+
eps = 1 / (100 * total_vat)
64+
# Check that reconstructed total excl matches total excl
65+
if (
66+
doc.total_net.value * (1 - eps) - 0.02
67+
<= reconstructed_total
68+
<= doc.total_net.value * (1 + eps) + 0.02
69+
):
70+
for tax in doc.taxes:
71+
tax.confidence = 1
72+
doc.total_tax.confidence = 1.0
73+
doc.total_net.confidence = 1.0
74+
return True
75+
return False
76+
77+
78+
def taxes_plus_total_excl_match_total_incl(doc) -> bool:
79+
"""
80+
Check invoice matching rule.
81+
82+
Rule is: sum(taxes) + total_excluding_taxes = total_including_taxes
83+
:return: True if rule matches, False otherwise
84+
"""
85+
# Check total_tax, total excl and total incl exist
86+
if (
87+
doc.total_net.value is None
88+
or len(doc.taxes) == 0
89+
or doc.total_amount.value is None
90+
):
91+
return False
92+
93+
# Reconstruct total_incl
94+
total_vat = 0.0
95+
for tax in doc.taxes:
96+
if tax.value is not None:
97+
total_vat += tax.value
98+
reconstructed_total = total_vat + doc.total_net.value
99+
100+
# Sanity check
101+
if total_vat <= 0:
102+
return False
103+
104+
# Check that reconstructed total incl matches total excl + taxes sum
105+
if (
106+
doc.total_amount.value - 0.01
107+
<= reconstructed_total
108+
<= doc.total_amount.value + 0.01
109+
):
110+
for tax in doc.taxes:
111+
tax.confidence = 1
112+
doc.total_tax.confidence = 1.0
113+
doc.total_net.confidence = 1.0
114+
doc.total_amount.confidence = 1.0
115+
return True
116+
return False

0 commit comments

Comments
 (0)