Skip to content

Commit 25df23d

Browse files
committed
Initial commit!
0 parents  commit 25df23d

File tree

5 files changed

+456
-0
lines changed

5 files changed

+456
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
data.yaml
2+
__pycache__/

README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Tax document generator for small businesses in Poland
2+
This is a set of scripts for automated accounting and **JPK_VAT** and **VAT-7** generation for small businesses in **Poland**.
3+
4+
## Usage
5+
1. Check out the repo.
6+
2. Create a file named `data.yaml`, with content similar to this:
7+
```
8+
subject:
9+
vat_id: 5252462530
10+
name: LEW21 Linus Lewandowski
11+
first_name: Linus
12+
last_name: Lewandowski
13+
birthdate: 1992-11-06
14+
15+
phone: +48123456789
16+
auth_amount: 1234
17+
tax_office_code: 1435 # Pierwszy Urząd Skarbowy - Warszawa Śródmieście
18+
uses_cash_method: false # or true
19+
20+
21+
22+
contractors:
23+
netguru: &netguru
24+
vat_id: 7781454968
25+
name: NETGURU SPÓŁKA AKCYJNA
26+
address: ul. Wojskowa 6, 60-792 Poznań
27+
28+
ovh: &ovh
29+
vat_id: 8992520556
30+
name: OVH SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ
31+
address: ul. Swobodna 1, 50-088 Wrocław
32+
33+
google: &google
34+
vat_id: IE6388047V
35+
name: Google Ireland Limited
36+
address: Gordon House, Barrow Street, Dublin 4
37+
38+
heroku: &heroku
39+
name: Heroku, Inc.
40+
address: 1 Market St. Suite 300, 94105 San Francisco, CA
41+
42+
43+
44+
invoices:
45+
46+
- date: 2019-03-30
47+
paid_on: 2019-04-01 # You need paid_on only if you're using cash method.
48+
issuer: *ovh
49+
id: PL2436776/F
50+
description: aiakos.co, aiakos.me, aiakos.net
51+
taxable_amount: 203.92
52+
vat_amount: 46.90
53+
54+
- date: 2019-03-31
55+
paid_on: 2019-04-02
56+
billed_to: *netguru
57+
id: A1/3/2019
58+
description: Usługi programistyczne
59+
taxable_amount: 2000
60+
vat_amount: 460
61+
62+
- date: 2019-03-31
63+
paid_on: 2019-04-02
64+
issuer: *google
65+
id: 3571748692
66+
description: Serwer
67+
taxable_amount: 0.12
68+
69+
- date: 2019-03-31
70+
paid_on: 2019-04-09
71+
issuer: *heroku
72+
id: 24963482
73+
description: Serwer
74+
taxable_amount: 187.99
75+
```
76+
3. Run `python jpk.py 2019-03` to generate a JPK file
77+
4. Run `python vat7.py 2019-03` to generate a VAT-7 file
78+
79+
## Future plans
80+
* PIT calculations
81+
* Support for declaration submission
82+
* Using Google Sheets as the data source

data.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from __future__ import annotations
2+
3+
import yaml
4+
from datetime import date
5+
from dataclasses import dataclass
6+
from itertools import chain
7+
8+
9+
@dataclass
10+
class Hacks:
11+
reverse_charges_paid_on_invoice_date: bool = False
12+
13+
14+
@dataclass
15+
class Entity:
16+
name: str
17+
vat_id: str = None
18+
address: str = None
19+
20+
21+
@dataclass
22+
class Subject(Entity):
23+
first_name: str = None
24+
last_name: str = None
25+
birthdate: date = None
26+
email: str = None
27+
phone: str = None
28+
tax_office_code: str = None
29+
auth_amount: float = None
30+
uses_cash_method: bool = False
31+
hacks: Hacks = None
32+
33+
34+
@dataclass
35+
class Invoice:
36+
description: str
37+
id: str
38+
date: date
39+
taxable_amount: str
40+
paid_on: date = None
41+
vat_amount_applied: float = 0
42+
vat_amount_required: float = None
43+
billed_to: Entity = None
44+
issuer: Entity = None
45+
reverse_charge: bool = False
46+
47+
@property
48+
def contractor(self) -> Entity:
49+
return self.issuer or self.billed_to
50+
51+
@property
52+
def tax_date(self):
53+
return self.paid_on if subject.uses_cash_method else self.date
54+
55+
56+
def load_invoice(data):
57+
taxable_amount = data.pop('taxable_amount')
58+
59+
try:
60+
vat_amount = data.pop('vat_amount')
61+
except KeyError:
62+
vat_amount = None
63+
64+
try:
65+
vat_amount_applied = data.pop('vat_amount_applied')
66+
except KeyError:
67+
vat_amount_applied = vat_amount or 0
68+
69+
try:
70+
vat_amount_required = data.pop('vat_amount_required')
71+
except KeyError:
72+
vat_amount_required = vat_amount
73+
74+
calculated_vat_amount_required = round(taxable_amount * 0.23, 2)
75+
if not vat_amount_required:
76+
vat_amount_required = calculated_vat_amount_required
77+
#if vat_amount_required != calculated_vat_amount_required:
78+
# print(vat_amount_required, calculated_vat_amount_required, invoice.contractor.name)
79+
80+
try:
81+
issuer = Entity(**data.pop('issuer'))
82+
except KeyError:
83+
issuer = None
84+
85+
try:
86+
billed_to = Entity(**data.pop('billed_to'))
87+
except KeyError:
88+
billed_to = None
89+
90+
return Invoice(
91+
taxable_amount = taxable_amount,
92+
vat_amount_applied = vat_amount_applied,
93+
vat_amount_required = vat_amount_required,
94+
issuer = issuer,
95+
billed_to = billed_to,
96+
**data
97+
)
98+
99+
100+
def gen_reverse_charge_invoices(subject, invoices):
101+
reverse_charge_series = {}
102+
103+
def gen_reverse_charge_invoices(invoice):
104+
month = str(invoice.date)[:7]
105+
num = reverse_charge_series.get(month, 0)
106+
num += 1
107+
reverse_charge_series[month] = num
108+
number = f'B{num}/{invoice.date.month}/{invoice.date.year}'
109+
110+
return (Invoice(
111+
description = f'Faktura wewnętrzna dot. faktury {invoice.id}',
112+
id = number,
113+
date = invoice.date,
114+
taxable_amount = invoice.taxable_amount,
115+
vat_amount_applied = invoice.vat_amount_required,
116+
vat_amount_required = invoice.vat_amount_required,
117+
paid_on = invoice.date if subject.hacks.reverse_charges_paid_on_invoice_date else invoice.paid_on,
118+
issuer = invoice.issuer,
119+
reverse_charge = True,
120+
), Invoice(
121+
description = f'Faktura wewnętrzna dot. faktury {invoice.id}',
122+
id = number,
123+
date = invoice.date,
124+
taxable_amount = invoice.taxable_amount,
125+
vat_amount_applied = invoice.vat_amount_required,
126+
vat_amount_required = invoice.vat_amount_required,
127+
paid_on = invoice.date if subject.hacks.reverse_charges_paid_on_invoice_date else invoice.paid_on,
128+
billed_to = invoice.issuer,
129+
reverse_charge = True,
130+
))
131+
132+
return list(chain(*(gen_reverse_charge_invoices(invoice) for invoice in invoices if invoice.vat_amount_applied != invoice.vat_amount_required)))
133+
134+
135+
with open('data.yaml') as f:
136+
data = yaml.safe_load(f.read())
137+
138+
try:
139+
hacks = Hacks(**data['subject'].pop('hacks'))
140+
except KeyError:
141+
hacks = Hacks()
142+
143+
subject = Subject(hacks = hacks, **data['subject'])
144+
145+
invoices = [load_invoice(invoice) for invoice in data['invoices']]
146+
invoices += gen_reverse_charge_invoices(subject, invoices)
147+
148+
invoices = sorted(invoices, key = lambda i: i.tax_date)

jpk.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import sys
2+
from datetime import datetime, timezone, timedelta
3+
4+
from data import subject, invoices
5+
6+
7+
def to_jpk(subject, invoices, month):
8+
now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
9+
10+
since_dt = datetime.fromisoformat(month + '-01T00:00:00+00:00')
11+
month_num = since_dt.month
12+
tmp = since_dt
13+
while tmp.month == month_num:
14+
until_dt = tmp
15+
tmp += timedelta(days=1)
16+
17+
since = since_dt.date().isoformat()
18+
until = until_dt.date().isoformat()
19+
20+
invoices = [i for i in invoices if str(i.tax_date).startswith(month)]
21+
22+
issued = [i for i in invoices if i.billed_to]
23+
received = [i for i in invoices if i.issuer]
24+
25+
sprzedaz_xml = [f"""
26+
<SprzedazWiersz>
27+
<LpSprzedazy>{i}</LpSprzedazy>
28+
<NrKontrahenta>{invoice.contractor.vat_id or 'brak'}</NrKontrahenta>
29+
<NazwaKontrahenta>{invoice.contractor.name}</NazwaKontrahenta>
30+
<AdresKontrahenta>{invoice.contractor.address}</AdresKontrahenta>
31+
<DowodSprzedazy>{invoice.id}</DowodSprzedazy>
32+
<DataWystawienia>{invoice.date}</DataWystawienia>
33+
<DataSprzedazy>{invoice.date}</DataSprzedazy>
34+
<K_10>0</K_10>
35+
<K_11>0</K_11>
36+
<K_12>0</K_12>
37+
<K_13>0</K_13>
38+
<K_14>0</K_14>
39+
<K_15>0</K_15>
40+
<K_16>0</K_16>
41+
<K_17>0</K_17>
42+
<K_18>0</K_18>
43+
<K_19>{invoice.taxable_amount if not invoice.reverse_charge else 0:1.2f}</K_19>
44+
<K_20>{invoice.vat_amount_applied if not invoice.reverse_charge else 0:1.2f}</K_20>
45+
<K_21>0</K_21>
46+
<K_22>0</K_22>
47+
<K_23>0</K_23>
48+
<K_24>0</K_24>
49+
<K_25>0</K_25>
50+
<K_26>0</K_26>
51+
<K_27>{invoice.taxable_amount if invoice.reverse_charge else 0:1.2f}</K_27>
52+
<K_28>{invoice.vat_amount_applied if invoice.reverse_charge else 0:1.2f}</K_28>
53+
<K_29>0</K_29>
54+
<K_30>0</K_30>
55+
<K_31>0</K_31>
56+
<K_32>0</K_32>
57+
<K_33>0</K_33>
58+
<K_34>0</K_34>
59+
<K_35>0</K_35>
60+
<K_36>0</K_36>
61+
<K_37>0</K_37>
62+
<K_38>0</K_38>
63+
<K_39>0</K_39>
64+
</SprzedazWiersz>
65+
""" for i, invoice in enumerate(issued, 1)]
66+
zakup_xml = [f"""
67+
<ZakupWiersz>
68+
<LpZakupu>{i}</LpZakupu>
69+
<NrDostawcy>{invoice.contractor.vat_id or 'brak'}</NrDostawcy>
70+
<NazwaDostawcy>{invoice.contractor.name}</NazwaDostawcy>
71+
<AdresDostawcy>{invoice.contractor.address}</AdresDostawcy>
72+
<DowodZakupu>{invoice.id}</DowodZakupu>
73+
<DataZakupu>{invoice.date}</DataZakupu>
74+
<DataWplywu>{invoice.date}</DataWplywu>
75+
<K_43>0</K_43>
76+
<K_44>0</K_44>
77+
<K_45>{invoice.taxable_amount:1.2f}</K_45>
78+
<K_46>{invoice.vat_amount_applied:1.2f}</K_46>
79+
<K_47>0</K_47>
80+
<K_48>0</K_48>
81+
<K_49>0</K_49>
82+
<K_50>0</K_50>
83+
</ZakupWiersz>
84+
""" for i, invoice in enumerate(received, 1)]
85+
86+
return f"""
87+
<JPK xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://jpk.mf.gov.pl/wzor/2017/11/13/1113/">
88+
<Naglowek>
89+
<KodFormularza kodSystemowy="JPK_VAT (3)" wersjaSchemy="1-1">JPK_VAT</KodFormularza>
90+
<WariantFormularza>3</WariantFormularza>
91+
<CelZlozenia>0</CelZlozenia>
92+
<DataWytworzeniaJPK>{now}</DataWytworzeniaJPK>
93+
<DataOd>{since}</DataOd>
94+
<DataDo>{until}</DataDo>
95+
<NazwaSystemu>pyjpk</NazwaSystemu>
96+
</Naglowek>
97+
<Podmiot1>
98+
<NIP>{subject.vat_id}</NIP>
99+
<PelnaNazwa>{subject.name}</PelnaNazwa>
100+
</Podmiot1>
101+
{"".join(sprzedaz_xml)}
102+
<SprzedazCtrl>
103+
<LiczbaWierszySprzedazy>{len(issued)}</LiczbaWierszySprzedazy>
104+
<PodatekNalezny>{sum(i.vat_amount_applied for i in issued):1.2f}</PodatekNalezny>
105+
</SprzedazCtrl>
106+
{"".join(zakup_xml)}
107+
<ZakupCtrl>
108+
<LiczbaWierszyZakupow>{len(received)}</LiczbaWierszyZakupow>
109+
<PodatekNaliczony>{sum(i.vat_amount_applied for i in received):1.2f}</PodatekNaliczony>
110+
</ZakupCtrl>
111+
</JPK>
112+
""".replace('>0.00<', '>0<')
113+
114+
print(to_jpk(subject, invoices, sys.argv[1]))

0 commit comments

Comments
 (0)