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,72 @@
1
+ import factory
2
+ import pytest
3
+ from rest_framework.reverse import reverse
4
+ from wbaccounting.models import Transaction
5
+ from wbaccounting.serializers import (
6
+ TransactionModelSerializer,
7
+ TransactionRepresentationSerializer,
8
+ )
9
+ from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
10
+ from wbcore.contrib.directory.serializers import BankingContactRepresentationSerializer
11
+
12
+
13
+ @pytest.mark.django_db
14
+ class TestTransactionModelSerializer:
15
+ def test_serialize(self, transaction: Transaction):
16
+ # We need to add the currency symbol, which is usually done through the queryset in the viewset
17
+ transaction.bank_account_currency_symbol = transaction.bank_account.currency.symbol # type: ignore
18
+
19
+ serializer = TransactionModelSerializer(transaction)
20
+ assert transaction.currency is not None
21
+ assert transaction.fx_rate is not None
22
+ assert transaction.value_local_ccy is not None
23
+ assert transaction.value is not None
24
+ assert serializer.data == {
25
+ "id": transaction.pk,
26
+ "description": transaction.description,
27
+ "booking_date": transaction.booking_date.strftime("%Y-%m-%d"),
28
+ "value_date": transaction.value_date.strftime("%Y-%m-%d"),
29
+ "bank_account": transaction.bank_account.id,
30
+ "_bank_account": BankingContactRepresentationSerializer(transaction.bank_account).data,
31
+ "from_bank_account": transaction.bank_account.id,
32
+ "_from_bank_account": BankingContactRepresentationSerializer(transaction.bank_account).data,
33
+ "to_bank_account": transaction.bank_account.id,
34
+ "_to_bank_account": BankingContactRepresentationSerializer(transaction.bank_account).data,
35
+ "currency": transaction.currency.pk,
36
+ "_currency": CurrencyRepresentationSerializer(transaction.currency).data,
37
+ "fx_rate": str(round(transaction.fx_rate, 4)),
38
+ "value_local_ccy": str(round(transaction.value_local_ccy, 2)),
39
+ "value": str(round(transaction.value, 2)),
40
+ "bank_account_currency_symbol": transaction.bank_account.currency.symbol,
41
+ }
42
+
43
+ def test_deserialize(self, transaction_factory, banking_contact):
44
+ data = factory.build(
45
+ dict,
46
+ FACTORY_CLASS=transaction_factory,
47
+ bank_account=banking_contact,
48
+ )
49
+
50
+ data["bank_account"] = data["bank_account"].id
51
+ data["from_bank_account"] = data["from_bank_account"].id
52
+ data["to_bank_account"] = data["to_bank_account"].id
53
+ data["currency"] = data["currency"].id
54
+ data["value"] = round(data["value"], 2)
55
+
56
+ serializer = TransactionModelSerializer(data=data)
57
+ assert serializer.is_valid()
58
+
59
+
60
+ @pytest.mark.django_db
61
+ class TestTransactionRepresentationSerializer:
62
+ def test_serialize(self, transaction: Transaction):
63
+ serializer = TransactionRepresentationSerializer(transaction)
64
+ assert transaction.value is not None
65
+ assert serializer.data == {
66
+ "id": transaction.pk,
67
+ "booking_date": transaction.booking_date.strftime("%Y-%m-%d"),
68
+ "bank_account": transaction.bank_account.id,
69
+ "_bank_account": BankingContactRepresentationSerializer(transaction.bank_account).data,
70
+ "value": str(round(transaction.value, 2)),
71
+ "_detail": reverse("wbaccounting:transaction-detail", args=[transaction.pk]),
72
+ }
wbaccounting/urls.py ADDED
@@ -0,0 +1,68 @@
1
+ from django.urls import include, path
2
+ from wbaccounting.viewsets import (
3
+ BookingEntryModelViewSet,
4
+ BookingEntryRepresentationViewSet,
5
+ ConsolidatedInvoiceViewSet,
6
+ EntryAccountingInformationModelViewSet,
7
+ EntryAccountingInformationRepresentationViewSet,
8
+ FutureCashFlowPandasAPIViewSet,
9
+ FutureCashFlowTransactionsPandasAPIViewSet,
10
+ InvoiceModelViewSet,
11
+ InvoiceRepresentationViewSet,
12
+ InvoiceTypeModelViewSet,
13
+ InvoiceTypeRepresentationViewSet,
14
+ TransactionModelViewSet,
15
+ TransactionRepresentationViewSet,
16
+ )
17
+ from wbcore.routers import WBCoreRouter
18
+
19
+ router = WBCoreRouter()
20
+ router.register(r"bookingentry", BookingEntryModelViewSet, basename="bookingentry")
21
+ router.register(
22
+ r"bookingentryrepresentation", BookingEntryRepresentationViewSet, basename="bookingentryrepresentation"
23
+ )
24
+ router.register(r"invoice", InvoiceModelViewSet, basename="invoice")
25
+ router.register(r"invoicerepresentation", InvoiceRepresentationViewSet, basename="invoicerepresentation")
26
+ router.register(r"transaction", TransactionModelViewSet, basename="transaction")
27
+ router.register(r"transactionrepresentation", TransactionRepresentationViewSet, basename="transactionrepresentation")
28
+ router.register(r"consolidated-invoice", ConsolidatedInvoiceViewSet, basename="consolidated-invoice")
29
+ router.register(r"invoicetype", InvoiceTypeModelViewSet, basename="invoicetype")
30
+ router.register(r"invoicetyperepresentation", InvoiceTypeRepresentationViewSet, basename="invoicetyperepresentation")
31
+ router.register(
32
+ r"entryaccountinginformationrepresentation",
33
+ EntryAccountingInformationRepresentationViewSet,
34
+ basename="entryaccountinginformationrepresentation",
35
+ )
36
+ router.register(
37
+ r"entryaccountinginformation", EntryAccountingInformationModelViewSet, basename="entryaccountinginformation"
38
+ )
39
+ router.register(r"futurecashflow", FutureCashFlowPandasAPIViewSet, basename="futurecashflow")
40
+ router.register(
41
+ r"futurecashflowtransaction", FutureCashFlowTransactionsPandasAPIViewSet, basename="futurecashflowtransaction"
42
+ )
43
+
44
+ entry_router = WBCoreRouter()
45
+
46
+ invoice_router = WBCoreRouter()
47
+ invoice_router.register(r"bookingentry", BookingEntryModelViewSet, basename="invoice-bookingentry")
48
+
49
+ entry_accounting_information_router = WBCoreRouter()
50
+ entry_accounting_information_router.register(
51
+ r"invoice", InvoiceModelViewSet, basename="entryaccountinginformation-invoice"
52
+ )
53
+ entry_accounting_information_router.register(
54
+ r"bookingentry", BookingEntryModelViewSet, basename="entryaccountinginformation-bookingentry"
55
+ )
56
+
57
+ booking_entry_router = WBCoreRouter()
58
+
59
+ urlpatterns = [
60
+ path("", include(router.urls)),
61
+ path("entry/<int:entry_id>/", include(entry_router.urls)),
62
+ path("invoice/<int:invoice_id>/", include(invoice_router.urls)),
63
+ path("bookingentry/<int:booking_entry_id>/", include(booking_entry_router.urls)),
64
+ path(
65
+ "entryaccountinginformation/<int:entry_accounting_information_id>/",
66
+ include(entry_accounting_information_router.urls),
67
+ ),
68
+ ]
@@ -0,0 +1,12 @@
1
+ from .booking_entry import (
2
+ BookingEntryModelViewSet,
3
+ BookingEntryRepresentationViewSet,
4
+ )
5
+ from .entry_accounting_information import (
6
+ EntryAccountingInformationModelViewSet,
7
+ EntryAccountingInformationRepresentationViewSet,
8
+ )
9
+ from .invoice_type import InvoiceTypeModelViewSet, InvoiceTypeRepresentationViewSet
10
+ from .invoice import InvoiceModelViewSet, ConsolidatedInvoiceViewSet, InvoiceRepresentationViewSet
11
+ from .transactions import TransactionModelViewSet, TransactionRepresentationViewSet
12
+ from .cashflows import FutureCashFlowPandasAPIViewSet, FutureCashFlowTransactionsPandasAPIViewSet
@@ -0,0 +1,61 @@
1
+ from django.db.models import QuerySet
2
+ from wbaccounting.models import BookingEntry
3
+ from wbaccounting.serializers import (
4
+ BookingEntryModelSerializer,
5
+ BookingEntryRepresentationSerializer,
6
+ )
7
+ from wbaccounting.viewsets.buttons import BookingEntryButtonConfig
8
+ from wbaccounting.viewsets.display import BookingEntryDisplayConfig
9
+ from wbaccounting.viewsets.titles import BookingEntryTitleConfig
10
+ from wbcore import viewsets
11
+
12
+
13
+ class BookingEntryRepresentationViewSet(viewsets.RepresentationViewSet):
14
+ search_fields = ["counterparty__computed_str", "title", "currency__key"]
15
+ ordering_fields = ["title"]
16
+ ordering = ["title"]
17
+ serializer_class = BookingEntryRepresentationSerializer
18
+ queryset = BookingEntry.objects.all()
19
+
20
+ def get_queryset(self):
21
+ return BookingEntry.objects.filter_for_user(self.request.user)
22
+
23
+
24
+ class BookingEntryModelViewSet(viewsets.ModelViewSet):
25
+ filterset_fields = {
26
+ "counterparty": ["exact"],
27
+ "booking_date": ["gte", "exact", "lte"],
28
+ "reference_date": ["gte", "exact", "lte"],
29
+ }
30
+ search_fields = ["counterparty__computed_str", "title", "currency__key"]
31
+ ordering_fields = (
32
+ "counterparty__computed_str",
33
+ "booking_date",
34
+ "gross_value",
35
+ "net_value",
36
+ "reference_date",
37
+ )
38
+ ordering = ["-booking_date"]
39
+
40
+ serializer_class = BookingEntryModelSerializer
41
+ queryset = BookingEntry.objects.all()
42
+
43
+ button_config_class = BookingEntryButtonConfig
44
+ display_config_class = BookingEntryDisplayConfig
45
+ title_config_class = BookingEntryTitleConfig
46
+
47
+ def get_queryset(self) -> QuerySet[BookingEntry]:
48
+ booking_entries = BookingEntry.objects.filter_for_user(self.request.user)
49
+
50
+ if eai_id := self.kwargs.get("entry_accounting_information_id", None):
51
+ booking_entries = booking_entries.filter(counterparty__entry_accounting_information__id=eai_id)
52
+
53
+ if invoice_id := self.kwargs.get("invoice_id", None):
54
+ booking_entries = booking_entries.filter(invoice_id=invoice_id)
55
+
56
+ return booking_entries.select_related(
57
+ "counterparty",
58
+ "invoice",
59
+ "currency",
60
+ "invoice__invoice_currency",
61
+ )
@@ -0,0 +1,3 @@
1
+ from .entry_accounting_information import EntryAccountingInformationButtonConfig
2
+ from .booking_entry import BookingEntryButtonConfig
3
+ from .invoice import InvoiceButtonConfig, ConsolidatedInvoiceButtonConfig
@@ -0,0 +1,15 @@
1
+ from wbcore.contrib.icons import WBIcon
2
+ from wbcore.metadata.configs import buttons
3
+ from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
4
+
5
+
6
+ class BookingEntryButtonConfig(ButtonViewConfig):
7
+ def get_custom_instance_buttons(self):
8
+ return {
9
+ buttons.WidgetButton(
10
+ key="appended_booking_entries", label="Appended Booking Entries", icon=WBIcon.NOTEBOOK.icon
11
+ )
12
+ }
13
+
14
+ def get_custom_list_instance_buttons(self):
15
+ return self.get_custom_instance_buttons()
@@ -0,0 +1,100 @@
1
+ from django.dispatch import receiver
2
+ from rest_framework.reverse import reverse
3
+ from wbcore import serializers
4
+ from wbcore.contrib.directory.serializers import EntryRepresentationSerializer
5
+ from wbcore.contrib.directory.viewsets import (
6
+ CompanyModelViewSet,
7
+ EntryModelViewSet,
8
+ PersonModelViewSet,
9
+ )
10
+ from wbcore.contrib.icons import WBIcon
11
+ from wbcore.enums import RequestType
12
+ from wbcore.metadata.configs import buttons
13
+ from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
14
+ from wbcore.metadata.configs.display.instance_display.shortcuts import (
15
+ create_simple_display,
16
+ )
17
+ from wbcore.signals.instance_buttons import add_instance_button
18
+ from wbcore.utils.date import current_quarter_date_end, current_quarter_date_start
19
+
20
+
21
+ class StartEndParametersSerializer(serializers.Serializer):
22
+ start = serializers.DateField(default=current_quarter_date_start(), label="Start")
23
+ end = serializers.DateField(default=current_quarter_date_end(), label="End")
24
+
25
+
26
+ class CounterpartiesSerializer(serializers.Serializer):
27
+ counterparties = serializers.PrimaryKeyRelatedField(many=True)
28
+ _counterparties = EntryRepresentationSerializer(many=True, source="counterparties")
29
+
30
+
31
+ class StartEndParametersWithCounterpartiesSerializer(CounterpartiesSerializer, StartEndParametersSerializer):
32
+ ...
33
+
34
+
35
+ class EntryAccountingInformationButtonConfig(ButtonViewConfig):
36
+ def get_custom_buttons(self) -> set:
37
+ if not self.instance:
38
+ return {
39
+ buttons.ActionButton(
40
+ method=RequestType.POST,
41
+ identifiers=("wbaccounting:bookingentry",),
42
+ endpoint=reverse(
43
+ "wbaccounting:entryaccountinginformation-generate-booking-entries-for-counterparties",
44
+ request=self.request,
45
+ ),
46
+ label="Generate Booking Entries",
47
+ description_fields="""<p>Generate Booking Entries between {{start}} and {{end}}?</p>""",
48
+ action_label="Generate Booking Entries",
49
+ title="Generate Booking Entries",
50
+ serializer=StartEndParametersWithCounterpartiesSerializer,
51
+ instance_display=create_simple_display([["start"], ["end"], ["counterparties"]]),
52
+ ),
53
+ buttons.ActionButton(
54
+ method=RequestType.POST,
55
+ identifiers=("wbaccounting:invoice",),
56
+ endpoint=reverse(
57
+ "wbaccounting:entryaccountinginformation-generate-invoices-for-counterparties",
58
+ request=self.request,
59
+ ),
60
+ label="Invoice outstanding Bookings",
61
+ description_fields="""<p>Invoice outstanding bookings? If you don't supply counterparties, all counterparties will be considered</p>""",
62
+ action_label="Invoice outstanding Bookings",
63
+ title="Invoice outstanding Bookings",
64
+ serializer=CounterpartiesSerializer,
65
+ instance_display=create_simple_display([["counterparties"]]),
66
+ ),
67
+ }
68
+ return set()
69
+
70
+ def get_custom_instance_buttons(self) -> set:
71
+ return {
72
+ buttons.ActionButton(
73
+ method=RequestType.POST,
74
+ identifiers=("wbaccounting:bookingentry",),
75
+ key="generate_booking_entries",
76
+ label="Generate Booking Entries",
77
+ description_fields="""<p>Generate Booking Entries between {{start}} and {{end}}?</p>""",
78
+ action_label="Generate Booking Entries",
79
+ title="Generate Booking Entries",
80
+ serializer=StartEndParametersSerializer,
81
+ instance_display=create_simple_display([["start"], ["end"]]),
82
+ ),
83
+ buttons.ActionButton(
84
+ method=RequestType.POST,
85
+ identifiers=("wbaccounting:invoice",),
86
+ key="invoice_booking_entries",
87
+ label="Invoice Booking Entries",
88
+ description_fields="<p>Do you want to invoice all outstanding booking entries?</p>",
89
+ action_label="Invoice Booking Entries",
90
+ title="Invoice Booking Entries",
91
+ ),
92
+ }
93
+
94
+
95
+ @receiver(add_instance_button, sender=PersonModelViewSet)
96
+ @receiver(add_instance_button, sender=EntryModelViewSet)
97
+ @receiver(add_instance_button, sender=CompanyModelViewSet)
98
+ def entry_adding_instance_buttons(sender, many, *args, **kwargs):
99
+ if not many:
100
+ return buttons.WidgetButton(key="accounting-information", label="Accounting", icon=WBIcon.MAIL_OPEN.icon)
@@ -0,0 +1,65 @@
1
+ from rest_framework.reverse import reverse
2
+ from wbcore.contrib.icons import WBIcon
3
+ from wbcore.enums import RequestType
4
+ from wbcore.metadata.configs import buttons as bt
5
+ from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
6
+
7
+
8
+ class InvoiceBaseButtonConfig(ButtonViewConfig):
9
+ def get_custom_instance_buttons(self):
10
+ return {bt.HyperlinkButton(key="invoice_file", label="Invoice", icon=WBIcon.DOCUMENT.icon)}
11
+
12
+ def get_custom_list_instance_buttons(self):
13
+ return self.get_custom_instance_buttons()
14
+
15
+
16
+ class InvoiceButtonConfig(InvoiceBaseButtonConfig):
17
+ def get_custom_buttons(self) -> set:
18
+ buttons = super().get_custom_buttons()
19
+ if not (self.view.kwargs.get("pk") or self.new_mode):
20
+ buttons.add(
21
+ bt.WidgetButton(
22
+ label="Consolidated Invoices",
23
+ endpoint=reverse("wbaccounting:consolidated-invoice-list", args=[], request=self.request),
24
+ )
25
+ )
26
+ return buttons
27
+
28
+
29
+ class ConsolidatedInvoiceButtonConfig(InvoiceBaseButtonConfig):
30
+ def get_custom_list_instance_buttons(self) -> set:
31
+ return {
32
+ bt.ActionButton(
33
+ method=RequestType.PATCH,
34
+ identifiers=("wbaccounting:invoice",),
35
+ label="Approve ({{num_submitted}})",
36
+ description_fields="""
37
+ <p>Do you want to approve all {{num_submitted}} submitted invoices?</p>
38
+ """,
39
+ action_label="Approving",
40
+ title="Approve",
41
+ key="approve",
42
+ ),
43
+ bt.ActionButton(
44
+ method=RequestType.PATCH,
45
+ identifiers=("wbaccounting:invoice",),
46
+ label="Submit ({{num_draft}})",
47
+ description_fields="""
48
+ <p>Do you want to submit {{num_draft}} invoices?</p>
49
+ """,
50
+ action_label="Submitting",
51
+ title="Submit",
52
+ key="submit",
53
+ ),
54
+ bt.ActionButton(
55
+ method=RequestType.PATCH,
56
+ identifiers=("wbaccounting:invoice",),
57
+ label="Pay ({{num_sent}})",
58
+ description_fields="""
59
+ <p>Do you want to pay all {{num_sent}} invoices?</p>
60
+ """,
61
+ action_label="Payment",
62
+ title="Pay",
63
+ key="pay",
64
+ ),
65
+ }
@@ -0,0 +1,124 @@
1
+ from datetime import date, datetime
2
+ from typing import TYPE_CHECKING, Any
3
+
4
+ import pandas as pd
5
+ from django.db.models import DateField, F, Sum, Value
6
+ from wbaccounting.models import Transaction
7
+ from wbaccounting.viewsets.display import FutureCashFlowDisplayConfig
8
+ from wbcore.pandas import fields as pf
9
+ from wbcore.pandas.views import PandasAPIViewSet
10
+
11
+ if TYPE_CHECKING:
12
+ from django.db.models import QuerySet
13
+ from django.db.models.query import ValuesQuerySet
14
+ from rest_framework.request import Request
15
+
16
+
17
+ class FutureCashFlowPandasAPIViewSetMixin(PandasAPIViewSet):
18
+ DATE_COL_START = 0
19
+ BOLD = False
20
+
21
+ queryset = Transaction.objects.none()
22
+ display_config_class = FutureCashFlowDisplayConfig
23
+
24
+ def get_pandas_fields(self, request) -> pf.PandasFields:
25
+ fields = self.get_dataframe(request, self.get_queryset()).columns[self.DATE_COL_START :]
26
+ return pf.PandasFields(
27
+ fields=[
28
+ pf.PKField(key="bank_account__id", label="ID"),
29
+ pf.CharField(key="_group_key", label="GROUPKEY"),
30
+ pf.CharField(key="bank_account__iban", label="IBAN"),
31
+ pf.CharField(key="bank_account__currency__symbol", label="Currency"),
32
+ *[
33
+ pf.FloatField(
34
+ key=field,
35
+ label=datetime.strptime(field, "%Y-%m-%d").strftime("%d.%m.%Y"),
36
+ display_mode=pf.DisplayMode.SHORTENED,
37
+ )
38
+ for field in fields
39
+ ],
40
+ ]
41
+ )
42
+
43
+
44
+ class FutureCashFlowPandasAPIViewSet(FutureCashFlowPandasAPIViewSetMixin):
45
+ DATE_COL_START = 4
46
+ BOLD = True
47
+
48
+ def get_queryset(self) -> "ValuesQuerySet[Transaction, Any]":
49
+ base_queryset = Transaction.objects.filter_for_user(self.request.user)
50
+ if banking_contact_ids := self.request.GET.get("banking_contact"):
51
+ base_queryset = base_queryset.filter(bank_account__id__in=banking_contact_ids.split(","))
52
+
53
+ past_transactions = (
54
+ base_queryset.filter(prenotification=False)
55
+ .values("bank_account__id")
56
+ .annotate(
57
+ value_date=Value(date.today(), output_field=DateField()),
58
+ balance=Sum("value"),
59
+ _group_key=F("bank_account__id"),
60
+ )
61
+ .values(
62
+ "bank_account__id",
63
+ "bank_account__iban",
64
+ "bank_account__currency__symbol",
65
+ "value_date",
66
+ "balance",
67
+ "_group_key",
68
+ )
69
+ )
70
+
71
+ future_transactions = (
72
+ base_queryset.filter(prenotification=True, value_date__gt=date.today())
73
+ .values("bank_account__id", "value_date")
74
+ .annotate(balance=Sum("value"), _group_key=F("bank_account__id"))
75
+ .values(
76
+ "bank_account__id",
77
+ "bank_account__iban",
78
+ "bank_account__currency__symbol",
79
+ "value_date",
80
+ "balance",
81
+ "_group_key",
82
+ )
83
+ )
84
+
85
+ return past_transactions.union(future_transactions)
86
+
87
+ def get_dataframe(
88
+ self, request: "Request", queryset: "ValuesQuerySet[Transaction, Any]", **kwargs
89
+ ) -> pd.DataFrame:
90
+ if not queryset.exists():
91
+ return pd.DataFrame()
92
+ df = (
93
+ pd.DataFrame(queryset)
94
+ .pivot_table(
95
+ index=["bank_account__id", "bank_account__iban", "bank_account__currency__symbol", "_group_key"],
96
+ columns=["value_date"],
97
+ values="balance",
98
+ )
99
+ .astype(float)
100
+ .fillna(0)
101
+ .cumsum(axis=1)
102
+ )
103
+ df.columns = df.columns.astype(str)
104
+ return df.reset_index()
105
+
106
+
107
+ class FutureCashFlowTransactionsPandasAPIViewSet(FutureCashFlowPandasAPIViewSetMixin):
108
+ DATE_COL_START = 2
109
+
110
+ def get_queryset(self):
111
+ queryset = Transaction.objects.filter(prenotification=True, value_date__gt=date.today())
112
+ if bank_account := self.request.GET.get("bank_account", None):
113
+ return queryset.filter(bank_account_id=bank_account)
114
+ return queryset
115
+
116
+ def get_dataframe(self, request: "Request", queryset: "QuerySet[Transaction]", **kwargs) -> pd.DataFrame:
117
+ df = pd.DataFrame(
118
+ queryset.values_list("description", "value", "value_date"), columns=["description", "value", "value_date"]
119
+ ).pivot_table(index=["description"], columns=["value_date"], values="value")
120
+ df.columns = df.columns.astype(str)
121
+ df = df.reset_index()
122
+ df = df.rename(columns={"description": "bank_account__iban"})
123
+ df.insert(0, "bank_account__id", df["bank_account__iban"])
124
+ return df
@@ -0,0 +1,8 @@
1
+ from .booking_entry import BookingEntryDisplayConfig
2
+ from .invoice import InvoiceDisplayConfig, ConsolidatedInvoiceDisplayConfig
3
+ from .invoice_type import InvoiceTypeDisplayConfig
4
+ from .entry_accounting_information import (
5
+ EntryAccountingInformationDisplayConfig,
6
+ )
7
+ from .transactions import TransactionDisplayConfig
8
+ from .cashflows import FutureCashFlowDisplayConfig
@@ -0,0 +1,58 @@
1
+ from wbcore.metadata.configs import display as dp
2
+ from wbcore.metadata.configs.display.view_config import DisplayViewConfig
3
+
4
+
5
+ class BookingEntryDisplayConfig(DisplayViewConfig):
6
+ def get_list_display(self) -> dp.ListDisplay:
7
+ _counterparty_fields = [dp.Field(key="counterparty", label="Counterparty")]
8
+ _common_fields = [
9
+ dp.Field(key="booking_date", label="Book. Date", width=100),
10
+ dp.Field(key="reference_date", label="Ref. Date", width=100),
11
+ dp.Field(key="payment_date", label="Payment. Date", width=100),
12
+ dp.Field(key="title", label="Title", width=325),
13
+ dp.Field(key="vat", label="VAT", width=100),
14
+ dp.Field(key="gross_value", label="Gross", width=100),
15
+ dp.Field(key="net_value", label="Net", width=100),
16
+ ]
17
+ _invoice_fields = [dp.Field(key="invoice", label="Invoice", width=325)]
18
+
19
+ if "invoice_id" in self.view.kwargs:
20
+ return dp.ListDisplay(fields=_common_fields)
21
+
22
+ if "entry_accounting_information_id" in self.view.kwargs:
23
+ return dp.ListDisplay(fields=[*_common_fields, *_invoice_fields])
24
+
25
+ return dp.ListDisplay(fields=[*_counterparty_fields, *_common_fields, *_invoice_fields])
26
+
27
+ def get_instance_display(self) -> dp.Display:
28
+ return dp.Display(
29
+ pages=[
30
+ dp.Page(
31
+ layouts={
32
+ dp.default(): dp.Layout(
33
+ grid_template_areas=[
34
+ ["title", "title", "title", "counterparty", ".", "booking_date"],
35
+ ["currency", "vat", "gross_value", "net_value", ".", "reference_date"],
36
+ [
37
+ "invoice_currency",
38
+ "invoice_fx_rate",
39
+ "invoice_gross_value",
40
+ "invoice_net_value",
41
+ ".",
42
+ "due_date",
43
+ ],
44
+ ["invoice", "invoice", "invoice", "invoice", ".", "payment_date"],
45
+ ],
46
+ grid_template_columns=[
47
+ "100px",
48
+ "100px",
49
+ "200px",
50
+ "200px",
51
+ "25px",
52
+ "150px",
53
+ ],
54
+ )
55
+ }
56
+ )
57
+ ]
58
+ )
@@ -0,0 +1,58 @@
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING
3
+
4
+ from rest_framework.reverse import reverse
5
+ from wbcore.metadata.configs import display as dp
6
+ from wbcore.metadata.configs.display.view_config import DisplayViewConfig
7
+ from wbcore.utils.models import WBColor
8
+
9
+ if TYPE_CHECKING:
10
+ from wbaccounting.viewsets.cashflows import FutureCashFlowPandasAPIViewSetMixin
11
+
12
+
13
+ class FutureCashFlowDisplayConfig(DisplayViewConfig):
14
+ def get_list_display(self) -> dp.ListDisplay:
15
+ view: "FutureCashFlowPandasAPIViewSetMixin" = self.view # type: ignore
16
+ fields = view.get_dataframe(self.request, view.get_queryset()).columns[view.DATE_COL_START :]
17
+ return dp.ListDisplay(
18
+ fields=[
19
+ dp.Field(key="bank_account__currency__symbol", label="Currency"),
20
+ *[
21
+ dp.Field(
22
+ key=field,
23
+ label=datetime.strptime(field, "%Y-%m-%d").strftime("%d.%m.%Y"),
24
+ formatting_rules=[
25
+ dp.FormattingRule(
26
+ style={"color": WBColor.GREEN_DARK.value},
27
+ condition=(">=", 0),
28
+ ),
29
+ dp.FormattingRule(
30
+ style={"color": WBColor.RED_DARK.value},
31
+ condition=("<", 0),
32
+ ),
33
+ *[
34
+ dp.FormattingRule(style={"fontWeight": "bold"})
35
+ if view.BOLD
36
+ else dp.FormattingRule(style={"fontWeight": "normal"})
37
+ ],
38
+ ],
39
+ )
40
+ for field in fields
41
+ ],
42
+ ],
43
+ tree=True,
44
+ tree_group_field="bank_account__iban",
45
+ tree_group_label="IBAN",
46
+ tree_group_level_options=[
47
+ dp.TreeGroupLevelOption(
48
+ filter_depth=1,
49
+ lookup="bank_account__id",
50
+ filter_key="bank_account",
51
+ list_endpoint=reverse(
52
+ "wbaccounting:futurecashflowtransaction-list",
53
+ args=[],
54
+ request=self.request,
55
+ ),
56
+ )
57
+ ],
58
+ )