wbaccounting 2.2.1__py2.py3-none-any.whl
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.
- wbaccounting/__init__.py +1 -0
- wbaccounting/admin/__init__.py +5 -0
- wbaccounting/admin/booking_entry.py +53 -0
- wbaccounting/admin/entry_accounting_information.py +10 -0
- wbaccounting/admin/invoice.py +26 -0
- wbaccounting/admin/invoice_type.py +8 -0
- wbaccounting/admin/transactions.py +16 -0
- wbaccounting/apps.py +5 -0
- wbaccounting/dynamic_preferences_registry.py +107 -0
- wbaccounting/factories/__init__.py +10 -0
- wbaccounting/factories/booking_entry.py +21 -0
- wbaccounting/factories/entry_accounting_information.py +46 -0
- wbaccounting/factories/invoice.py +43 -0
- wbaccounting/factories/transactions.py +32 -0
- wbaccounting/files/__init__.py +0 -0
- wbaccounting/files/invoice_document_file.py +134 -0
- wbaccounting/files/utils.py +331 -0
- wbaccounting/generators/__init__.py +6 -0
- wbaccounting/generators/base.py +120 -0
- wbaccounting/io/handlers/__init__.py +0 -0
- wbaccounting/io/handlers/transactions.py +32 -0
- wbaccounting/io/parsers/__init__.py +0 -0
- wbaccounting/io/parsers/societe_generale_lux.py +49 -0
- wbaccounting/io/parsers/societe_generale_lux_prenotification.py +60 -0
- wbaccounting/migrations/0001_initial_squashed_squashed_0005_alter_bookingentry_counterparty_and_more.py +284 -0
- wbaccounting/migrations/0006_alter_invoice_status.py +30 -0
- wbaccounting/migrations/0007_alter_invoice_options.py +23 -0
- wbaccounting/migrations/0008_alter_invoice_options.py +20 -0
- wbaccounting/migrations/0009_invoicetype_alter_bookingentry_options_and_more.py +366 -0
- wbaccounting/migrations/0010_alter_bookingentry_options.py +20 -0
- wbaccounting/migrations/0011_transaction.py +103 -0
- wbaccounting/migrations/0012_entryaccountinginformation_external_invoice_users.py +25 -0
- wbaccounting/migrations/__init__.py +0 -0
- wbaccounting/models/__init__.py +6 -0
- wbaccounting/models/booking_entry.py +167 -0
- wbaccounting/models/entry_accounting_information.py +157 -0
- wbaccounting/models/invoice.py +467 -0
- wbaccounting/models/invoice_type.py +30 -0
- wbaccounting/models/model_tasks.py +71 -0
- wbaccounting/models/transactions.py +112 -0
- wbaccounting/permissions.py +6 -0
- wbaccounting/processors/__init__.py +0 -0
- wbaccounting/processors/dummy_processor.py +5 -0
- wbaccounting/serializers/__init__.py +12 -0
- wbaccounting/serializers/booking_entry.py +78 -0
- wbaccounting/serializers/consolidated_invoice.py +109 -0
- wbaccounting/serializers/entry_accounting_information.py +149 -0
- wbaccounting/serializers/invoice.py +95 -0
- wbaccounting/serializers/invoice_type.py +16 -0
- wbaccounting/serializers/transactions.py +50 -0
- wbaccounting/tests/__init__.py +0 -0
- wbaccounting/tests/conftest.py +65 -0
- wbaccounting/tests/test_displays/__init__.py +0 -0
- wbaccounting/tests/test_displays/test_booking_entries.py +1 -0
- wbaccounting/tests/test_models/__init__.py +0 -0
- wbaccounting/tests/test_models/test_booking_entries.py +119 -0
- wbaccounting/tests/test_models/test_entry_accounting_information.py +81 -0
- wbaccounting/tests/test_models/test_invoice_types.py +21 -0
- wbaccounting/tests/test_models/test_invoices.py +73 -0
- wbaccounting/tests/test_models/test_transactions.py +40 -0
- wbaccounting/tests/test_processors.py +28 -0
- wbaccounting/tests/test_serializers/__init__.py +0 -0
- wbaccounting/tests/test_serializers/test_booking_entries.py +69 -0
- wbaccounting/tests/test_serializers/test_entry_accounting_information.py +64 -0
- wbaccounting/tests/test_serializers/test_invoice_types.py +35 -0
- wbaccounting/tests/test_serializers/test_transactions.py +72 -0
- wbaccounting/urls.py +68 -0
- wbaccounting/viewsets/__init__.py +12 -0
- wbaccounting/viewsets/booking_entry.py +61 -0
- wbaccounting/viewsets/buttons/__init__.py +3 -0
- wbaccounting/viewsets/buttons/booking_entry.py +15 -0
- wbaccounting/viewsets/buttons/entry_accounting_information.py +100 -0
- wbaccounting/viewsets/buttons/invoice.py +65 -0
- wbaccounting/viewsets/cashflows.py +124 -0
- wbaccounting/viewsets/display/__init__.py +8 -0
- wbaccounting/viewsets/display/booking_entry.py +58 -0
- wbaccounting/viewsets/display/cashflows.py +58 -0
- wbaccounting/viewsets/display/entry_accounting_information.py +91 -0
- wbaccounting/viewsets/display/invoice.py +218 -0
- wbaccounting/viewsets/display/invoice_type.py +19 -0
- wbaccounting/viewsets/display/transactions.py +35 -0
- wbaccounting/viewsets/endpoints/__init__.py +1 -0
- wbaccounting/viewsets/endpoints/invoice.py +6 -0
- wbaccounting/viewsets/entry_accounting_information.py +143 -0
- wbaccounting/viewsets/invoice.py +277 -0
- wbaccounting/viewsets/invoice_type.py +25 -0
- wbaccounting/viewsets/menu/__init__.py +6 -0
- wbaccounting/viewsets/menu/booking_entry.py +15 -0
- wbaccounting/viewsets/menu/cashflows.py +10 -0
- wbaccounting/viewsets/menu/entry_accounting_information.py +11 -0
- wbaccounting/viewsets/menu/invoice.py +15 -0
- wbaccounting/viewsets/menu/invoice_type.py +15 -0
- wbaccounting/viewsets/menu/transactions.py +15 -0
- wbaccounting/viewsets/titles/__init__.py +4 -0
- wbaccounting/viewsets/titles/booking_entry.py +12 -0
- wbaccounting/viewsets/titles/entry_accounting_information.py +12 -0
- wbaccounting/viewsets/titles/invoice.py +23 -0
- wbaccounting/viewsets/titles/invoice_type.py +12 -0
- wbaccounting/viewsets/transactions.py +34 -0
- wbaccounting-2.2.1.dist-info/METADATA +8 -0
- wbaccounting-2.2.1.dist-info/RECORD +102 -0
- wbaccounting-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
from django.template import Context, Template
|
|
2
|
+
from reportlab.lib import colors, utils
|
|
3
|
+
from reportlab.lib.enums import TA_CENTER, TA_RIGHT
|
|
4
|
+
from reportlab.lib.pagesizes import A4
|
|
5
|
+
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
|
6
|
+
from reportlab.lib.units import cm
|
|
7
|
+
from reportlab.platypus import Image, Paragraph, Table, TableStyle
|
|
8
|
+
from wbaccounting.dynamic_preferences_registry import format_invoice_number
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_styles():
|
|
12
|
+
styles = getSampleStyleSheet()
|
|
13
|
+
styles.add(ParagraphStyle(name="Headline", fontName="Helvetica", fontSize=24, leading=34))
|
|
14
|
+
styles.add(ParagraphStyle(name="Default", fontName="Helvetica", fontSize=10, leading=12))
|
|
15
|
+
styles.add(ParagraphStyle(name="Default-Table-Header", fontName="Helvetica", fontSize=14, leading=16))
|
|
16
|
+
styles.add(
|
|
17
|
+
ParagraphStyle(
|
|
18
|
+
name="Default-Table-Header-Right", fontName="Helvetica", fontSize=14, leading=16, alignment=TA_RIGHT
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
styles.add(ParagraphStyle(name="Default-Right", fontName="Helvetica", fontSize=10, leading=12, alignment=TA_RIGHT))
|
|
22
|
+
styles.add(
|
|
23
|
+
ParagraphStyle(
|
|
24
|
+
name="Default-Right-Linebreak",
|
|
25
|
+
fontName="Helvetica",
|
|
26
|
+
fontSize=10,
|
|
27
|
+
leading=12,
|
|
28
|
+
alignment=TA_RIGHT,
|
|
29
|
+
spaceAfter=12,
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
styles.add(
|
|
33
|
+
ParagraphStyle(name="Default-Center", fontName="Helvetica", fontSize=10, leading=12, alignment=TA_CENTER)
|
|
34
|
+
)
|
|
35
|
+
styles.add(ParagraphStyle(name="Default-Linebreak", fontName="Helvetica", fontSize=10, leading=12, spaceAfter=12))
|
|
36
|
+
styles.add(
|
|
37
|
+
ParagraphStyle(name="Default-Linebreak-Before", fontName="Helvetica", fontSize=10, leading=12, spaceBefore=12)
|
|
38
|
+
)
|
|
39
|
+
styles.add(
|
|
40
|
+
ParagraphStyle(name="Default-Double-Linebreak", fontName="Helvetica", fontSize=10, leading=12, spaceAfter=24)
|
|
41
|
+
)
|
|
42
|
+
styles.add(
|
|
43
|
+
ParagraphStyle(
|
|
44
|
+
name="Default-Double-Linebreak-With-Before",
|
|
45
|
+
fontName="Helvetica",
|
|
46
|
+
fontSize=10,
|
|
47
|
+
leading=12,
|
|
48
|
+
spaceAfter=24,
|
|
49
|
+
spaceBefore=24,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
styles.add(ParagraphStyle(name="Normal-Right", alignment=TA_RIGHT)),
|
|
53
|
+
styles.add(ParagraphStyle(name="Annotation", fontName="Helvetica", fontSize=8, leading=10)),
|
|
54
|
+
styles.add(
|
|
55
|
+
ParagraphStyle(name="Annotation-Right", fontName="Helvetica", fontSize=8, leading=10, alignment=TA_RIGHT)
|
|
56
|
+
),
|
|
57
|
+
return styles
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# TODO to be removed
|
|
61
|
+
# def get_taxes(invoice, party, counterparty):
|
|
62
|
+
# vat = 0
|
|
63
|
+
# social_charges = 0
|
|
64
|
+
# if invoice.favour == 'COUNTERPARTY':
|
|
65
|
+
# vat = counterparty.accounting_information.vat if counterparty.accounting_information.vat else 0
|
|
66
|
+
# social_charges = counterparty.accounting_information.social_charges if counterparty.accounting_information.social_charges else 0
|
|
67
|
+
# else:
|
|
68
|
+
# vat = party.vat if party is not None and party.vat else 0
|
|
69
|
+
# social_charges = party.accounting_information.social_charges if party is not None and party.accounting_information.social_charges else 0
|
|
70
|
+
# if invoice.vat or invoice.vat == 0:
|
|
71
|
+
# vat = invoice.vat
|
|
72
|
+
# if invoice.social_charges or invoice.social_charges == 0:
|
|
73
|
+
# social_charges = invoice.social_charges
|
|
74
|
+
# return vat, social_charges
|
|
75
|
+
|
|
76
|
+
# def banking_block(entry, invoice, styles, banking=None):
|
|
77
|
+
# elements = list()
|
|
78
|
+
# if banking is None:
|
|
79
|
+
# bankings = entry.banking.all() if entry else None
|
|
80
|
+
# if bankings:
|
|
81
|
+
# if bankings.filter(currency=invoice.base_currency).exists():
|
|
82
|
+
# banking = bankings.filter(currency=invoice.base_currency).first()
|
|
83
|
+
# elif bankings.filter(primary=True).exists():
|
|
84
|
+
# banking = bankings.filter(primary=True).first()
|
|
85
|
+
# else:
|
|
86
|
+
# return elements
|
|
87
|
+
# elements.append(Paragraph('{}'.format(banking.institute), styles['Default-Linebreak-Before']))
|
|
88
|
+
# if banking.institute_additional:
|
|
89
|
+
# elements.append(Paragraph('{}'.format(banking.institute_additional), styles['Default']))
|
|
90
|
+
# elements.append(Paragraph('IBAN: {}'.format(banking.iban), styles['Default']))
|
|
91
|
+
# if banking.swift_bic:
|
|
92
|
+
# elements.append(Paragraph('SWIFT: {}'.format(banking.swift_bic), styles['Default']))
|
|
93
|
+
# return elements
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def address_block(entry, invoice, styles, right=False, address=None, tax_id=None):
|
|
97
|
+
elements = list()
|
|
98
|
+
if address is None:
|
|
99
|
+
addresses = entry.addresses.all() if entry is not None else None
|
|
100
|
+
if addresses:
|
|
101
|
+
if addresses.filter(primary=True).exists():
|
|
102
|
+
address = addresses.filter(primary=True).first()
|
|
103
|
+
elif addresses.count() > 0:
|
|
104
|
+
address = addresses.first()
|
|
105
|
+
else:
|
|
106
|
+
return elements
|
|
107
|
+
|
|
108
|
+
if right:
|
|
109
|
+
default_style = styles["Default-Right"]
|
|
110
|
+
default_linebreak_style = styles["Default-Right-Linebreak"]
|
|
111
|
+
else:
|
|
112
|
+
default_style = styles["Default"]
|
|
113
|
+
default_linebreak_style = styles["Default-Linebreak"]
|
|
114
|
+
|
|
115
|
+
casted_entry = entry.get_casted_entry()
|
|
116
|
+
if entry.is_company:
|
|
117
|
+
elements.append(Paragraph("<b>{0.name}</b>".format(casted_entry), default_style))
|
|
118
|
+
else:
|
|
119
|
+
elements.append(Paragraph("<b>{0.first_name} {0.last_name}</b>".format(casted_entry), default_style))
|
|
120
|
+
|
|
121
|
+
elements.append(Paragraph(address.street, default_style))
|
|
122
|
+
if address.street_additional is not None and address.street_additional != "":
|
|
123
|
+
elements.append(Paragraph(address.street_additional, default_style))
|
|
124
|
+
elements.append(Paragraph("{} {}".format(address.zip, address.geography_city), default_style))
|
|
125
|
+
elements.append(Paragraph(address.geography_city.parent.parent.name, default_linebreak_style))
|
|
126
|
+
|
|
127
|
+
if tax_id and entry.entry_accounting_information.tax_id:
|
|
128
|
+
elements.append(Paragraph(f"VAT: {entry.entry_accounting_information.tax_id}", default_linebreak_style))
|
|
129
|
+
|
|
130
|
+
return elements
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def city_and_date_block(invoice, styles, entry=None, city=None, date=None):
|
|
134
|
+
elements = list()
|
|
135
|
+
if date is None:
|
|
136
|
+
date = invoice.invoice_date
|
|
137
|
+
if city is None:
|
|
138
|
+
primary_address = entry.primary_address_contact() if entry else None
|
|
139
|
+
if primary_address:
|
|
140
|
+
city = primary_address.geography_city
|
|
141
|
+
if city:
|
|
142
|
+
elements.append(Paragraph("{}, {:%d.%m.%Y}".format(city, date), styles["Default-Right-Linebreak"]))
|
|
143
|
+
else:
|
|
144
|
+
elements.append(Paragraph("{:%d.%m.%Y}".format(date), styles["Default-Right-Linebreak"]))
|
|
145
|
+
return elements
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def logo_block(image):
|
|
149
|
+
# print(image)
|
|
150
|
+
reportlab_image = utils.ImageReader(image)
|
|
151
|
+
width, height = reportlab_image.getSize()
|
|
152
|
+
ratio = float(width) / float(height)
|
|
153
|
+
image = Image(image, width=30 * ratio, height=30)
|
|
154
|
+
return image, ratio
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def signature_block(signees, styles):
|
|
158
|
+
elements = list()
|
|
159
|
+
if signees:
|
|
160
|
+
elements.append(Paragraph("Best Regards,", styles["Default-Double-Linebreak-With-Before"]))
|
|
161
|
+
for signee in signees:
|
|
162
|
+
if signee is not None and signee.signature.name is not None and signee.signature.name != "":
|
|
163
|
+
reportlab_image = utils.ImageReader(signee.signature)
|
|
164
|
+
width, height = reportlab_image.getSize()
|
|
165
|
+
ratio = float(width) / float(height)
|
|
166
|
+
|
|
167
|
+
image = Image(signee.signature, width=50 * ratio, height=50)
|
|
168
|
+
image.hAlign = "LEFT"
|
|
169
|
+
elements.append(image)
|
|
170
|
+
elements.append(Paragraph("{}".format(signee.computed_str), styles["Default"]))
|
|
171
|
+
return elements
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def add_text_block_with_context(text, invoice, styles):
|
|
175
|
+
elements = list()
|
|
176
|
+
if text:
|
|
177
|
+
elements.append(Paragraph("", styles["Default-Linebreak"]))
|
|
178
|
+
elements.append(Paragraph(Template(text).render(Context(invoice.get_context())), styles["Default-Linebreak"]))
|
|
179
|
+
return elements
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def add_transfer_block(invoice, receiver, styles, total_amount_gross):
|
|
183
|
+
elements = list()
|
|
184
|
+
|
|
185
|
+
if invoice.is_counterparty_invoice:
|
|
186
|
+
text = f"The amount of <b>{invoice.invoice_currency} {format_invoice_number(total_amount_gross)}</b> will be transfered to the following bank account:"
|
|
187
|
+
else:
|
|
188
|
+
text = f"Please transfer <b>{invoice.invoice_currency} {format_invoice_number(total_amount_gross)}</b> to the following bank account:"
|
|
189
|
+
|
|
190
|
+
elements.append(Paragraph(text, styles["Default-Linebreak-Before"]))
|
|
191
|
+
|
|
192
|
+
bank_accounts = receiver.banking.all()
|
|
193
|
+
if bank_accounts.exists():
|
|
194
|
+
try:
|
|
195
|
+
bank_account = bank_accounts.get(currency=invoice.invoice_currency)
|
|
196
|
+
except Exception:
|
|
197
|
+
bank_account = bank_accounts.first()
|
|
198
|
+
|
|
199
|
+
elements.append(Paragraph(bank_account.institute, styles["Default"]))
|
|
200
|
+
|
|
201
|
+
if bank_account.institute_additional:
|
|
202
|
+
elements.append(Paragraph(bank_account.institute_additional, styles["Default"]))
|
|
203
|
+
|
|
204
|
+
if bank_account.iban and bank_account.iban != "":
|
|
205
|
+
elements.append(Paragraph(f"IBAN: {bank_account.iban}", styles["Default"]))
|
|
206
|
+
if bank_account.swift_bic:
|
|
207
|
+
elements.append(Paragraph(f"SWIFT: {bank_account.swift_bic}", styles["Default"]))
|
|
208
|
+
if bank_account.additional_information and bank_account.additional_information != "":
|
|
209
|
+
elements.append(Paragraph(bank_account.additional_information, styles["Default"]))
|
|
210
|
+
|
|
211
|
+
return elements
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_gross_and_net_value(invoice):
|
|
215
|
+
multiplier = 1 if not invoice.is_counterparty_invoice else -1
|
|
216
|
+
|
|
217
|
+
total_gross = 0
|
|
218
|
+
total_net = 0
|
|
219
|
+
for booking_entry in invoice.booking_entries.all():
|
|
220
|
+
if booking_entry.currency != invoice.invoice_currency:
|
|
221
|
+
conversion_rate = booking_entry.currency.convert(booking_entry.booking_date, invoice.invoice_currency)
|
|
222
|
+
total_net += multiplier * booking_entry.net_value * conversion_rate
|
|
223
|
+
total_gross += multiplier * booking_entry.gross_value * conversion_rate
|
|
224
|
+
else:
|
|
225
|
+
total_net += multiplier * booking_entry.net_value
|
|
226
|
+
total_gross += multiplier * booking_entry.gross_value
|
|
227
|
+
|
|
228
|
+
return total_gross, total_net
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def add_booking_entries(invoice, styles, inverse=False):
|
|
232
|
+
multiplier = 1 if not invoice.is_counterparty_invoice else -1
|
|
233
|
+
|
|
234
|
+
tables = list()
|
|
235
|
+
for vat in invoice.booking_entries.all().distinct("vat").values_list("vat", flat=True):
|
|
236
|
+
table_data = list()
|
|
237
|
+
|
|
238
|
+
table_data.append([Paragraph("", styles["Default"])] * 2)
|
|
239
|
+
table_data.append(
|
|
240
|
+
[
|
|
241
|
+
Paragraph("Title", styles["Default-Table-Header"]),
|
|
242
|
+
Paragraph("Amount", styles["Default-Table-Header-Right"]),
|
|
243
|
+
]
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
for booking_entry in invoice.booking_entries.filter(vat=vat).order_by("title"):
|
|
247
|
+
title_col = [Paragraph(f"{booking_entry.title}", styles["Default"])]
|
|
248
|
+
amount_col = list()
|
|
249
|
+
|
|
250
|
+
if booking_entry.currency != invoice.invoice_currency:
|
|
251
|
+
conversion_rate = booking_entry.currency.convert(booking_entry.booking_date, invoice.invoice_currency)
|
|
252
|
+
|
|
253
|
+
title_col.append(
|
|
254
|
+
Paragraph(
|
|
255
|
+
f"<i>1.00 {booking_entry.currency.key} = {conversion_rate:.4f} {invoice.invoice_currency.key}</i>",
|
|
256
|
+
styles["Annotation"],
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
amount_col.append(
|
|
261
|
+
Paragraph(
|
|
262
|
+
f"<i>{format_invoice_number(multiplier * conversion_rate * booking_entry.gross_value)} {invoice.invoice_currency.key}</i>",
|
|
263
|
+
styles["Default-Right"],
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
amount_col.append(
|
|
267
|
+
Paragraph(
|
|
268
|
+
f"{format_invoice_number(multiplier * booking_entry.gross_value)} {booking_entry.currency}",
|
|
269
|
+
styles["Annotation-Right"],
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
amount_col.append(
|
|
274
|
+
Paragraph(
|
|
275
|
+
f"{format_invoice_number(multiplier * booking_entry.gross_value)} {booking_entry.currency}",
|
|
276
|
+
styles["Default-Right"],
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if (
|
|
281
|
+
(params := booking_entry.parameters)
|
|
282
|
+
and (from_date := params.get("from_date"))
|
|
283
|
+
and (to_date := params.get("to_date"))
|
|
284
|
+
):
|
|
285
|
+
title_col.append(
|
|
286
|
+
Paragraph(
|
|
287
|
+
f"<i>{from_date} - {to_date}</i>",
|
|
288
|
+
styles["Annotation"],
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
table_data.append([title_col, amount_col])
|
|
293
|
+
|
|
294
|
+
total_gross, total_net = get_gross_and_net_value(invoice)
|
|
295
|
+
table_data.append([Paragraph("", styles["Default"])] * 2)
|
|
296
|
+
table_data.append(
|
|
297
|
+
[
|
|
298
|
+
[
|
|
299
|
+
Paragraph("<strong>Total Amount</strong>", styles["Default"]),
|
|
300
|
+
Paragraph(f"<i>Vat: {vat*100:.2f}%</i>", styles["Annotation"]),
|
|
301
|
+
Paragraph(f"Total Amount (excl. {vat*100:.2f}% VAT)", styles["Annotation"]),
|
|
302
|
+
],
|
|
303
|
+
[
|
|
304
|
+
Paragraph(
|
|
305
|
+
f"<strong>{format_invoice_number(total_gross)} {invoice.invoice_currency}</strong>",
|
|
306
|
+
styles["Default-Right"],
|
|
307
|
+
),
|
|
308
|
+
Paragraph(
|
|
309
|
+
f"<i>{format_invoice_number(total_gross - total_net)} {invoice.invoice_currency}</i>",
|
|
310
|
+
styles["Annotation-Right"],
|
|
311
|
+
),
|
|
312
|
+
Paragraph(
|
|
313
|
+
f"{format_invoice_number(total_net)} {invoice.invoice_currency}",
|
|
314
|
+
styles["Annotation-Right"],
|
|
315
|
+
),
|
|
316
|
+
],
|
|
317
|
+
]
|
|
318
|
+
)
|
|
319
|
+
table = Table(table_data, colWidths=[A4[0] - 6.4 * cm, 3.5 * cm])
|
|
320
|
+
table.setStyle(
|
|
321
|
+
TableStyle(
|
|
322
|
+
[
|
|
323
|
+
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#f37200")),
|
|
324
|
+
("BACKGROUND", (0, 1), (-1, 1), colors.HexColor("#f38a32")),
|
|
325
|
+
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
326
|
+
]
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
tables.append(table)
|
|
330
|
+
|
|
331
|
+
return tables
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from contextlib import suppress
|
|
3
|
+
from datetime import date
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
from typing import Callable, Iterable
|
|
6
|
+
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.db.models import QuerySet
|
|
9
|
+
from wbaccounting.models.booking_entry import BookingEntry
|
|
10
|
+
from wbcore.contrib.currency.models import Currency
|
|
11
|
+
from wbcore.contrib.directory.models import Entry
|
|
12
|
+
|
|
13
|
+
GENERATE_BOOKING_ENTRIES = Callable[[date, date, Entry], list[BookingEntry]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_generator(name: str) -> Callable:
|
|
17
|
+
def decorator(func: Callable) -> Callable:
|
|
18
|
+
setattr(func, "_is_booking_entry_generator", True)
|
|
19
|
+
setattr(func, "_booking_entry_generator_name", name)
|
|
20
|
+
return func
|
|
21
|
+
|
|
22
|
+
return decorator
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_all_booking_entry_choices() -> Iterable[tuple[str, str]]:
|
|
26
|
+
for app in settings.INSTALLED_APPS:
|
|
27
|
+
with suppress(ModuleNotFoundError):
|
|
28
|
+
import_module(f"{app}.generators")
|
|
29
|
+
|
|
30
|
+
for subclass in AbstractBookingEntryGenerator.__subclasses__():
|
|
31
|
+
yield f"{subclass.__module__}.{subclass.__name__}", subclass.TITLE
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AbstractBookingEntryGenerator(abc.ABC):
|
|
35
|
+
"""
|
|
36
|
+
An abstract base class designed to define a template for generating and managing
|
|
37
|
+
booking entries within a financial or accounting system.
|
|
38
|
+
|
|
39
|
+
This class outlines the necessary operations for generating a series of booking
|
|
40
|
+
entries based on a given date range and counterparty, as well as merging backlinks
|
|
41
|
+
for the generated booking entries.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
TITLE (str): A class-level attribute that provides a title or a descriptive name for the
|
|
45
|
+
generator implementation. This should be overridden in concrete subclasses
|
|
46
|
+
to provide a specific title.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
TITLE = ""
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
@abc.abstractmethod
|
|
53
|
+
def generate_booking_entries(from_date: date, to_date: date, counterparty: Entry) -> Iterable[BookingEntry]:
|
|
54
|
+
"""
|
|
55
|
+
Generates a sequence of booking entries for a specified date range and counterparty.
|
|
56
|
+
|
|
57
|
+
This abstract method must be implemented by subclasses to provide the logic for
|
|
58
|
+
generating booking entries based on the provided criteria.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
from_date (date): The start date of the period for which booking entries are to be generated.
|
|
62
|
+
to_date (date): The end date of the period for which booking entries are to be generated.
|
|
63
|
+
counterparty (Entry): The counterparty associated with the booking entries to be generated.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Iterable[BookingEntry]: A sequence of BookingEntry instances generated for the specified
|
|
67
|
+
criteria.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
NotImplementedError: If the method is not implemented in a subclass.
|
|
71
|
+
"""
|
|
72
|
+
raise NotImplementedError()
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
@abc.abstractmethod
|
|
76
|
+
def merge_backlinks(booking_entries: QuerySet[BookingEntry]) -> dict:
|
|
77
|
+
"""
|
|
78
|
+
Merges backlinks for a collection of booking entries, to consolidate
|
|
79
|
+
related entries to insert a single backling in an invoice.
|
|
80
|
+
|
|
81
|
+
This abstract method should be implemented by subclasses to provide specific
|
|
82
|
+
logic for merging or updating backlinks of booking entries.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
booking_entries (Iterable[BookingEntry]): A sequence of BookingEntry instances
|
|
86
|
+
to be merged or updated.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
dict: A dictionary or other structured data indicating the results of the
|
|
90
|
+
merge operation, specific to the implementation.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
NotImplementedError: If the method is not implemented in a subclass.
|
|
94
|
+
"""
|
|
95
|
+
raise NotImplementedError()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def generate_booking_entries(_class: type[AbstractBookingEntryGenerator], from_date: date, to_date: date, counterparty: Entry):
|
|
99
|
+
booking_entries = _class.generate_booking_entries(from_date, to_date, counterparty)
|
|
100
|
+
for booking_entry in booking_entries:
|
|
101
|
+
booking_entry.save()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestGenerator(AbstractBookingEntryGenerator):
|
|
105
|
+
TITLE = "Test Generator"
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def generate_booking_entries(from_date: date, to_date: date, counterparty: Entry) -> Iterable[BookingEntry]:
|
|
109
|
+
yield BookingEntry(
|
|
110
|
+
title="Test Booking Entry",
|
|
111
|
+
counterparty=counterparty,
|
|
112
|
+
booking_date=date.today(),
|
|
113
|
+
reference_date=date.today(),
|
|
114
|
+
net_value=100,
|
|
115
|
+
currency=Currency.objects.first(),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def merge_backlinks(booking_entries: Iterable[BookingEntry]) -> dict:
|
|
120
|
+
return {}
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.db.models import Value
|
|
6
|
+
from django.db.models.functions import Replace
|
|
7
|
+
from wbcore.contrib.currency.import_export.handlers import CurrencyImportHandler
|
|
8
|
+
from wbcore.contrib.directory.models import BankingContact
|
|
9
|
+
from wbcore.contrib.io.imports import ImportExportHandler
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TransactionImportHandler(ImportExportHandler):
|
|
13
|
+
MODEL_APP_LABEL: str = "wbaccounting.Transaction"
|
|
14
|
+
|
|
15
|
+
def __init__(self, *args, **kwargs):
|
|
16
|
+
super().__init__(*args, **kwargs)
|
|
17
|
+
self.currency_handler = CurrencyImportHandler(self.import_source)
|
|
18
|
+
|
|
19
|
+
def _deserialize(self, data: dict[str, Any]):
|
|
20
|
+
data["currency"] = self.currency_handler.process_object({"key": data["currency"]}, read_only=True)[0]
|
|
21
|
+
data["bank_account"] = BankingContact.objects.annotate(
|
|
22
|
+
stripped_iban=Replace("iban", Value(" "), Value(""))
|
|
23
|
+
).get(stripped_iban=data["bank_account"].replace(" ", ""))
|
|
24
|
+
data["value"] = Decimal(data["value"])
|
|
25
|
+
return super()._deserialize(data)
|
|
26
|
+
|
|
27
|
+
def _get_instance(self, data: dict[str, Any], history: models.QuerySet | None = None, **kwargs) -> Any | None:
|
|
28
|
+
if _hash := data.get("_hash", None):
|
|
29
|
+
qs = self.model.objects.filter(_hash=_hash)
|
|
30
|
+
if qs.exists():
|
|
31
|
+
return qs.first()
|
|
32
|
+
return super()._get_instance(data, history, **kwargs)
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import datetime
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
from schwifty import IBAN
|
|
6
|
+
|
|
7
|
+
# Bank specific Information for Societe Generale Luxembourg
|
|
8
|
+
BANK_COUNTRY_CODE = "LU"
|
|
9
|
+
BANK_CODE = "060"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse(import_source):
|
|
13
|
+
# Load file into a CSV DictReader
|
|
14
|
+
csv_file = import_source.file.open(mode="rt")
|
|
15
|
+
|
|
16
|
+
# Read file into a CSV Dict Reader
|
|
17
|
+
csv_reader = csv.DictReader(csv_file, delimiter=";")
|
|
18
|
+
|
|
19
|
+
# Iterate through the CSV File and parse the data into a list
|
|
20
|
+
data = list()
|
|
21
|
+
for transaction in csv_reader:
|
|
22
|
+
if amount := transaction.get("Amount", None):
|
|
23
|
+
booking_date = datetime.datetime.strptime(transaction["Accounting date"], "%Y/%m/%d").date()
|
|
24
|
+
value_date = datetime.datetime.strptime(transaction["Value date"], "%Y/%m/%d").date()
|
|
25
|
+
currency = transaction["Account currency"]
|
|
26
|
+
value = float(amount.replace(",", "."))
|
|
27
|
+
description = transaction["Transaction main description"]
|
|
28
|
+
bank_account = str(
|
|
29
|
+
IBAN.generate(BANK_COUNTRY_CODE, bank_code=BANK_CODE, account_code=transaction["Account number"])
|
|
30
|
+
)
|
|
31
|
+
item = {
|
|
32
|
+
"booking_date": booking_date.strftime("%Y-%m-%d"),
|
|
33
|
+
"value_date": value_date.strftime("%Y-%m-%d"),
|
|
34
|
+
"currency": currency,
|
|
35
|
+
"value": value,
|
|
36
|
+
"description": description,
|
|
37
|
+
"bank_account": bank_account,
|
|
38
|
+
}
|
|
39
|
+
_hash = hashlib.sha256()
|
|
40
|
+
for field in item.values():
|
|
41
|
+
_hash.update(str(field).encode())
|
|
42
|
+
|
|
43
|
+
item["_hash"] = _hash.hexdigest()
|
|
44
|
+
data.append(item)
|
|
45
|
+
|
|
46
|
+
csv_file.close()
|
|
47
|
+
return {
|
|
48
|
+
"data": data,
|
|
49
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from contextlib import suppress
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from wbportfolio.models import Product
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse(import_source):
|
|
10
|
+
csv_file = import_source.file.open()
|
|
11
|
+
df = pd.read_csv(csv_file)
|
|
12
|
+
df = df[~df["Isin"].isnull()]
|
|
13
|
+
df["Trade date"] = df["Trade date"].ffill()
|
|
14
|
+
|
|
15
|
+
df_sub = df[
|
|
16
|
+
["Trade date", "Isin", "Quantity Sub", "Amount Sub class ccy", "Value date Sub", "Nav Class ccy", "Ccy Class"]
|
|
17
|
+
]
|
|
18
|
+
df_sub = df_sub.rename(
|
|
19
|
+
columns={"Quantity Sub": "quantity", "Amount Sub class ccy": "amount", "Value date Sub": "value_date"}
|
|
20
|
+
)
|
|
21
|
+
df_red = df[
|
|
22
|
+
["Trade date", "Isin", "Quantity Red", "Amount Red Class ccy", "Value date Red", "Nav Class ccy", "Ccy Class"]
|
|
23
|
+
]
|
|
24
|
+
df_red = df_red.rename(
|
|
25
|
+
columns={"Quantity Red": "quantity", "Amount Red Class ccy": "amount", "Value date Red": "value_date"}
|
|
26
|
+
)
|
|
27
|
+
df_red["quantity"] = df_red["quantity"] * -1
|
|
28
|
+
df_red["amount"] = df_red["amount"] * -1
|
|
29
|
+
df = pd.concat([df_sub, df_red])
|
|
30
|
+
df["value"] = df["amount"] + (df["quantity"] * df["Nav Class ccy"])
|
|
31
|
+
|
|
32
|
+
df = df[df["value"] != 0]
|
|
33
|
+
|
|
34
|
+
# Iterate through the CSV File and parse the data into a list
|
|
35
|
+
data = list()
|
|
36
|
+
for transaction in df.to_dict(orient="records"):
|
|
37
|
+
_hash = hashlib.sha256()
|
|
38
|
+
for field in transaction.values():
|
|
39
|
+
_hash.update(str(field).encode())
|
|
40
|
+
|
|
41
|
+
description = f'Trade ({transaction["Isin"]}): {transaction["quantity"]} shares and {transaction["amount"]} {transaction["Ccy Class"]}.'
|
|
42
|
+
|
|
43
|
+
with suppress(Product.DoesNotExist):
|
|
44
|
+
bank_account = Product.objects.get(isin=transaction["Isin"]).bank_account.iban
|
|
45
|
+
data.append(
|
|
46
|
+
{
|
|
47
|
+
"booking_date": datetime.strptime(transaction["Trade date"], "%d/%m/%Y").strftime("%Y-%m-%d"),
|
|
48
|
+
"value_date": datetime.strptime(transaction["value_date"], "%d/%m/%Y").strftime("%Y-%m-%d"),
|
|
49
|
+
"currency": transaction.get("Ccy Class"),
|
|
50
|
+
"value": float(transaction["value"]),
|
|
51
|
+
"description": description,
|
|
52
|
+
"bank_account": bank_account,
|
|
53
|
+
"prenotification": True,
|
|
54
|
+
"_hash": _hash.hexdigest(),
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"data": data,
|
|
60
|
+
}
|