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.
Files changed (102) hide show
  1. wbaccounting/__init__.py +1 -0
  2. wbaccounting/admin/__init__.py +5 -0
  3. wbaccounting/admin/booking_entry.py +53 -0
  4. wbaccounting/admin/entry_accounting_information.py +10 -0
  5. wbaccounting/admin/invoice.py +26 -0
  6. wbaccounting/admin/invoice_type.py +8 -0
  7. wbaccounting/admin/transactions.py +16 -0
  8. wbaccounting/apps.py +5 -0
  9. wbaccounting/dynamic_preferences_registry.py +107 -0
  10. wbaccounting/factories/__init__.py +10 -0
  11. wbaccounting/factories/booking_entry.py +21 -0
  12. wbaccounting/factories/entry_accounting_information.py +46 -0
  13. wbaccounting/factories/invoice.py +43 -0
  14. wbaccounting/factories/transactions.py +32 -0
  15. wbaccounting/files/__init__.py +0 -0
  16. wbaccounting/files/invoice_document_file.py +134 -0
  17. wbaccounting/files/utils.py +331 -0
  18. wbaccounting/generators/__init__.py +6 -0
  19. wbaccounting/generators/base.py +120 -0
  20. wbaccounting/io/handlers/__init__.py +0 -0
  21. wbaccounting/io/handlers/transactions.py +32 -0
  22. wbaccounting/io/parsers/__init__.py +0 -0
  23. wbaccounting/io/parsers/societe_generale_lux.py +49 -0
  24. wbaccounting/io/parsers/societe_generale_lux_prenotification.py +60 -0
  25. wbaccounting/migrations/0001_initial_squashed_squashed_0005_alter_bookingentry_counterparty_and_more.py +284 -0
  26. wbaccounting/migrations/0006_alter_invoice_status.py +30 -0
  27. wbaccounting/migrations/0007_alter_invoice_options.py +23 -0
  28. wbaccounting/migrations/0008_alter_invoice_options.py +20 -0
  29. wbaccounting/migrations/0009_invoicetype_alter_bookingentry_options_and_more.py +366 -0
  30. wbaccounting/migrations/0010_alter_bookingentry_options.py +20 -0
  31. wbaccounting/migrations/0011_transaction.py +103 -0
  32. wbaccounting/migrations/0012_entryaccountinginformation_external_invoice_users.py +25 -0
  33. wbaccounting/migrations/__init__.py +0 -0
  34. wbaccounting/models/__init__.py +6 -0
  35. wbaccounting/models/booking_entry.py +167 -0
  36. wbaccounting/models/entry_accounting_information.py +157 -0
  37. wbaccounting/models/invoice.py +467 -0
  38. wbaccounting/models/invoice_type.py +30 -0
  39. wbaccounting/models/model_tasks.py +71 -0
  40. wbaccounting/models/transactions.py +112 -0
  41. wbaccounting/permissions.py +6 -0
  42. wbaccounting/processors/__init__.py +0 -0
  43. wbaccounting/processors/dummy_processor.py +5 -0
  44. wbaccounting/serializers/__init__.py +12 -0
  45. wbaccounting/serializers/booking_entry.py +78 -0
  46. wbaccounting/serializers/consolidated_invoice.py +109 -0
  47. wbaccounting/serializers/entry_accounting_information.py +149 -0
  48. wbaccounting/serializers/invoice.py +95 -0
  49. wbaccounting/serializers/invoice_type.py +16 -0
  50. wbaccounting/serializers/transactions.py +50 -0
  51. wbaccounting/tests/__init__.py +0 -0
  52. wbaccounting/tests/conftest.py +65 -0
  53. wbaccounting/tests/test_displays/__init__.py +0 -0
  54. wbaccounting/tests/test_displays/test_booking_entries.py +1 -0
  55. wbaccounting/tests/test_models/__init__.py +0 -0
  56. wbaccounting/tests/test_models/test_booking_entries.py +119 -0
  57. wbaccounting/tests/test_models/test_entry_accounting_information.py +81 -0
  58. wbaccounting/tests/test_models/test_invoice_types.py +21 -0
  59. wbaccounting/tests/test_models/test_invoices.py +73 -0
  60. wbaccounting/tests/test_models/test_transactions.py +40 -0
  61. wbaccounting/tests/test_processors.py +28 -0
  62. wbaccounting/tests/test_serializers/__init__.py +0 -0
  63. wbaccounting/tests/test_serializers/test_booking_entries.py +69 -0
  64. wbaccounting/tests/test_serializers/test_entry_accounting_information.py +64 -0
  65. wbaccounting/tests/test_serializers/test_invoice_types.py +35 -0
  66. wbaccounting/tests/test_serializers/test_transactions.py +72 -0
  67. wbaccounting/urls.py +68 -0
  68. wbaccounting/viewsets/__init__.py +12 -0
  69. wbaccounting/viewsets/booking_entry.py +61 -0
  70. wbaccounting/viewsets/buttons/__init__.py +3 -0
  71. wbaccounting/viewsets/buttons/booking_entry.py +15 -0
  72. wbaccounting/viewsets/buttons/entry_accounting_information.py +100 -0
  73. wbaccounting/viewsets/buttons/invoice.py +65 -0
  74. wbaccounting/viewsets/cashflows.py +124 -0
  75. wbaccounting/viewsets/display/__init__.py +8 -0
  76. wbaccounting/viewsets/display/booking_entry.py +58 -0
  77. wbaccounting/viewsets/display/cashflows.py +58 -0
  78. wbaccounting/viewsets/display/entry_accounting_information.py +91 -0
  79. wbaccounting/viewsets/display/invoice.py +218 -0
  80. wbaccounting/viewsets/display/invoice_type.py +19 -0
  81. wbaccounting/viewsets/display/transactions.py +35 -0
  82. wbaccounting/viewsets/endpoints/__init__.py +1 -0
  83. wbaccounting/viewsets/endpoints/invoice.py +6 -0
  84. wbaccounting/viewsets/entry_accounting_information.py +143 -0
  85. wbaccounting/viewsets/invoice.py +277 -0
  86. wbaccounting/viewsets/invoice_type.py +25 -0
  87. wbaccounting/viewsets/menu/__init__.py +6 -0
  88. wbaccounting/viewsets/menu/booking_entry.py +15 -0
  89. wbaccounting/viewsets/menu/cashflows.py +10 -0
  90. wbaccounting/viewsets/menu/entry_accounting_information.py +11 -0
  91. wbaccounting/viewsets/menu/invoice.py +15 -0
  92. wbaccounting/viewsets/menu/invoice_type.py +15 -0
  93. wbaccounting/viewsets/menu/transactions.py +15 -0
  94. wbaccounting/viewsets/titles/__init__.py +4 -0
  95. wbaccounting/viewsets/titles/booking_entry.py +12 -0
  96. wbaccounting/viewsets/titles/entry_accounting_information.py +12 -0
  97. wbaccounting/viewsets/titles/invoice.py +23 -0
  98. wbaccounting/viewsets/titles/invoice_type.py +12 -0
  99. wbaccounting/viewsets/transactions.py +34 -0
  100. wbaccounting-2.2.1.dist-info/METADATA +8 -0
  101. wbaccounting-2.2.1.dist-info/RECORD +102 -0
  102. 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,6 @@
1
+ from django.conf import settings
2
+
3
+ from .base import AbstractBookingEntryGenerator
4
+
5
+ if settings.DEBUG:
6
+ from .base import TestGenerator
@@ -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
+ }