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,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,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
|
+
)
|