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,71 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from datetime import date
|
|
3
|
+
|
|
4
|
+
from celery import shared_task
|
|
5
|
+
from django.db.models.signals import post_save
|
|
6
|
+
from django.utils.module_loading import import_string
|
|
7
|
+
from wbaccounting.generators.base import generate_booking_entries
|
|
8
|
+
from wbcore.contrib.directory.models import Entry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@shared_task
|
|
12
|
+
def submit_invoices_as_task(ids: list[int]):
|
|
13
|
+
from wbaccounting.models import Invoice
|
|
14
|
+
|
|
15
|
+
invoices = Invoice.objects.filter(id__in=ids)
|
|
16
|
+
for invoice in invoices:
|
|
17
|
+
invoice.submit()
|
|
18
|
+
invoice.save()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@shared_task
|
|
22
|
+
def approve_invoices_as_task(ids: list[int]):
|
|
23
|
+
from wbaccounting.models import Invoice
|
|
24
|
+
|
|
25
|
+
invoices = Invoice.objects.filter(id__in=ids)
|
|
26
|
+
for invoice in invoices:
|
|
27
|
+
invoice.approve()
|
|
28
|
+
invoice.save()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@shared_task
|
|
32
|
+
def pay_invoices_as_task(ids: list[int]):
|
|
33
|
+
from wbaccounting.models import Invoice
|
|
34
|
+
|
|
35
|
+
invoices = Invoice.objects.filter(id__in=ids)
|
|
36
|
+
for invoice in invoices:
|
|
37
|
+
invoice.pay()
|
|
38
|
+
invoice.save()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@shared_task
|
|
42
|
+
def refresh_complete_invoice_as_task(invoice_id: int):
|
|
43
|
+
from wbaccounting.models import Invoice
|
|
44
|
+
from wbaccounting.models.booking_entry import BookingEntry, booking_entry_changed
|
|
45
|
+
|
|
46
|
+
# We temmporarily disconnect the post_save hook in order to not regenerate the invoice over and over again
|
|
47
|
+
post_save.disconnect(booking_entry_changed, sender=BookingEntry)
|
|
48
|
+
|
|
49
|
+
for booking_entry in BookingEntry.objects.filter(invoice_id=invoice_id):
|
|
50
|
+
booking_entry.save()
|
|
51
|
+
|
|
52
|
+
Invoice.objects.get(id=invoice_id).save()
|
|
53
|
+
|
|
54
|
+
# We reattach the post_save hook
|
|
55
|
+
post_save.connect(booking_entry_changed, sender=BookingEntry)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@shared_task
|
|
59
|
+
def refresh_invoice_document_as_task(invoice_id):
|
|
60
|
+
from wbaccounting.models import Invoice
|
|
61
|
+
|
|
62
|
+
invoice = Invoice.objects.get(id=invoice_id)
|
|
63
|
+
invoice.refresh_invoice_document()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@shared_task
|
|
67
|
+
def generate_booking_entries_as_task(func: str, from_date: date, to_date: date, counterparty_id: int):
|
|
68
|
+
with suppress(ImportError):
|
|
69
|
+
generator = import_string(func)
|
|
70
|
+
counterparty = Entry.objects.get(id=counterparty_id)
|
|
71
|
+
generate_booking_entries(generator, from_date, to_date, counterparty)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.utils.translation import gettext as _
|
|
6
|
+
from wbaccounting.io.handlers.transactions import TransactionImportHandler
|
|
7
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
8
|
+
from wbcore.models import WBModel
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from wbcore.contrib.authentication.models import User
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TransactionQuerySet(models.QuerySet):
|
|
15
|
+
def filter_for_user(self, user: "User") -> models.QuerySet["Transaction"]:
|
|
16
|
+
if user.is_superuser:
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
return self.filter(bank_account__access__user_account__in=[user])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Transaction(ImportMixin, WBModel):
|
|
23
|
+
"""A transaction represents a bank transfer of some funds in some currency from one Bank Account to another one."""
|
|
24
|
+
|
|
25
|
+
import_export_handler_class = TransactionImportHandler
|
|
26
|
+
|
|
27
|
+
booking_date = models.DateField(verbose_name=_("Booking Date"))
|
|
28
|
+
value_date = models.DateField(verbose_name=_("Value Date"))
|
|
29
|
+
|
|
30
|
+
bank_account = models.ForeignKey(
|
|
31
|
+
to="directory.BankingContact",
|
|
32
|
+
related_name="wbaccounting_transactions",
|
|
33
|
+
on_delete=models.PROTECT,
|
|
34
|
+
verbose_name=_("Linked Bank Account"),
|
|
35
|
+
)
|
|
36
|
+
from_bank_account = models.ForeignKey(
|
|
37
|
+
to="directory.BankingContact",
|
|
38
|
+
related_name="+",
|
|
39
|
+
null=True,
|
|
40
|
+
blank=True,
|
|
41
|
+
on_delete=models.PROTECT,
|
|
42
|
+
verbose_name=_("Source Bank Account"),
|
|
43
|
+
)
|
|
44
|
+
to_bank_account = models.ForeignKey(
|
|
45
|
+
to="directory.BankingContact",
|
|
46
|
+
related_name="+",
|
|
47
|
+
null=True,
|
|
48
|
+
blank=True,
|
|
49
|
+
on_delete=models.PROTECT,
|
|
50
|
+
verbose_name=_("Target Bank Account"),
|
|
51
|
+
)
|
|
52
|
+
currency = models.ForeignKey(
|
|
53
|
+
to="currency.Currency",
|
|
54
|
+
related_name="+",
|
|
55
|
+
null=True,
|
|
56
|
+
blank=True,
|
|
57
|
+
on_delete=models.PROTECT,
|
|
58
|
+
verbose_name=_("Currency"),
|
|
59
|
+
)
|
|
60
|
+
fx_rate = models.DecimalField(default=Decimal(1), max_digits=10, decimal_places=4, verbose_name=_("FX Rate"))
|
|
61
|
+
value_local_ccy = models.DecimalField(
|
|
62
|
+
max_digits=19, decimal_places=2, null=True, blank=True, verbose_name=_("Value (Local Currency)")
|
|
63
|
+
)
|
|
64
|
+
value = models.DecimalField(max_digits=19, decimal_places=2, null=True, blank=True, verbose_name=_("Value"))
|
|
65
|
+
description = models.TextField(default="")
|
|
66
|
+
prenotification = models.BooleanField(
|
|
67
|
+
default=False,
|
|
68
|
+
verbose_name=_("Prenotification"),
|
|
69
|
+
help_text=_("This field indicates that this transaction will happen sometime in the future."),
|
|
70
|
+
)
|
|
71
|
+
_hash = models.CharField(max_length=64, null=True, blank=True)
|
|
72
|
+
|
|
73
|
+
objects = TransactionQuerySet.as_manager()
|
|
74
|
+
|
|
75
|
+
def save(self, *args, **kwargs):
|
|
76
|
+
# We make sure that the relationship between value and
|
|
77
|
+
# value_local_ccy stays consistant while prefering value
|
|
78
|
+
if self.value is not None:
|
|
79
|
+
self.value_local_ccy = self.value / self.fx_rate
|
|
80
|
+
|
|
81
|
+
elif self.value_local_ccy is not None:
|
|
82
|
+
self.value = self.value_local_ccy * self.fx_rate
|
|
83
|
+
|
|
84
|
+
# If value date is not set we set it to the booking date
|
|
85
|
+
if self.value_date is None:
|
|
86
|
+
self.value_date = self.booking_date
|
|
87
|
+
|
|
88
|
+
super().save(*args, **kwargs)
|
|
89
|
+
|
|
90
|
+
def __str__(self) -> str:
|
|
91
|
+
return f"{self.booking_date:%d.%m.%Y}: {self.value:.2f}"
|
|
92
|
+
|
|
93
|
+
class Meta:
|
|
94
|
+
default_related_name = "transactions"
|
|
95
|
+
verbose_name = _("Transaction")
|
|
96
|
+
verbose_name_plural = _("Transactions")
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def get_endpoint_basename(cls) -> str:
|
|
100
|
+
return "wbaccounting:transaction"
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def get_representation_value_key(cls) -> str:
|
|
104
|
+
return "id"
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def get_representation_endpoint(cls) -> str:
|
|
108
|
+
return "wbaccounting:transactionrepresentation-list"
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def get_representation_label_key(cls) -> str:
|
|
112
|
+
return "{{booking_date}}: {{value}}"
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .invoice_type import InvoiceTypeModelSerializer, InvoiceTypeRepresentationSerializer
|
|
2
|
+
from .entry_accounting_information import (
|
|
3
|
+
EntryAccountingInformationModelSerializer,
|
|
4
|
+
EntryAccountingInformationRepresentationSerializer,
|
|
5
|
+
)
|
|
6
|
+
from .invoice import InvoiceRepresentationSerializer, InvoiceModelSerializer
|
|
7
|
+
from .booking_entry import (
|
|
8
|
+
BookingEntryModelSerializer,
|
|
9
|
+
BookingEntryRepresentationSerializer,
|
|
10
|
+
)
|
|
11
|
+
from .transactions import TransactionModelSerializer, TransactionRepresentationSerializer
|
|
12
|
+
from .consolidated_invoice import ConsolidatedInvoiceSerializer
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from django.utils.http import urlencode
|
|
2
|
+
from rest_framework.reverse import reverse
|
|
3
|
+
from wbaccounting.models import BookingEntry
|
|
4
|
+
from wbaccounting.serializers import InvoiceRepresentationSerializer
|
|
5
|
+
from wbcore import serializers
|
|
6
|
+
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
7
|
+
from wbcore.contrib.directory.serializers import EntryRepresentationSerializer
|
|
8
|
+
from wbcore.metadata.configs.buttons import WidgetButton
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BookingEntryRepresentationSerializer(serializers.RepresentationSerializer):
|
|
12
|
+
_detail = serializers.HyperlinkField(reverse_name="wbaccounting:bookingentry-detail")
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
model = BookingEntry
|
|
16
|
+
fields = (
|
|
17
|
+
"id",
|
|
18
|
+
"title",
|
|
19
|
+
"_detail",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BookingEntryModelSerializer(serializers.ModelSerializer):
|
|
24
|
+
_currency = CurrencyRepresentationSerializer(source="currency")
|
|
25
|
+
_counterparty = EntryRepresentationSerializer(source="counterparty")
|
|
26
|
+
_invoice = InvoiceRepresentationSerializer(source="invoice")
|
|
27
|
+
|
|
28
|
+
invoice_currency = serializers.StringRelatedField(
|
|
29
|
+
source="invoice.invoice_currency", read_only=True, label="Inv. Currency"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@serializers.register_dynamic_button()
|
|
33
|
+
def dynamic_buttons(self, instance, request, user):
|
|
34
|
+
buttons = []
|
|
35
|
+
|
|
36
|
+
if backlinks := instance.backlinks:
|
|
37
|
+
for _, backlink in backlinks.items():
|
|
38
|
+
buttons.append(
|
|
39
|
+
WidgetButton(
|
|
40
|
+
label=backlink["title"],
|
|
41
|
+
endpoint=f'{reverse(backlink["reverse"], request=request)}?{urlencode(backlink["parameters"])}',
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return buttons
|
|
46
|
+
|
|
47
|
+
class Meta:
|
|
48
|
+
model = BookingEntry
|
|
49
|
+
decorators = {
|
|
50
|
+
"gross_value": serializers.decorator(decorator_type="text", position="left", value="{{_currency.symbol}}"),
|
|
51
|
+
"net_value": serializers.decorator(decorator_type="text", position="left", value="{{_currency.symbol}}"),
|
|
52
|
+
}
|
|
53
|
+
percent_fields = ["vat"]
|
|
54
|
+
read_only_fields = ["invoice_net_value", "invoice_gross_value", "invoice_fx_rate"]
|
|
55
|
+
|
|
56
|
+
fields = (
|
|
57
|
+
"id",
|
|
58
|
+
"title",
|
|
59
|
+
"booking_date",
|
|
60
|
+
"due_date",
|
|
61
|
+
"payment_date",
|
|
62
|
+
"reference_date",
|
|
63
|
+
"net_value",
|
|
64
|
+
"gross_value",
|
|
65
|
+
"vat",
|
|
66
|
+
"invoice_net_value",
|
|
67
|
+
"invoice_gross_value",
|
|
68
|
+
"invoice_fx_rate",
|
|
69
|
+
"currency",
|
|
70
|
+
"_currency",
|
|
71
|
+
"counterparty",
|
|
72
|
+
"_counterparty",
|
|
73
|
+
"invoice",
|
|
74
|
+
"_invoice",
|
|
75
|
+
"invoice_currency",
|
|
76
|
+
"_additional_resources",
|
|
77
|
+
"_buttons",
|
|
78
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from django.utils.http import urlencode
|
|
2
|
+
from rest_framework.reverse import reverse
|
|
3
|
+
from wbaccounting.models import BookingEntry, Invoice
|
|
4
|
+
from wbaccounting.serializers import (
|
|
5
|
+
BookingEntryRepresentationSerializer,
|
|
6
|
+
InvoiceRepresentationSerializer,
|
|
7
|
+
InvoiceTypeRepresentationSerializer,
|
|
8
|
+
)
|
|
9
|
+
from wbcore import serializers
|
|
10
|
+
from wbcore.contrib.authentication.models import User
|
|
11
|
+
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
12
|
+
from wbcore.contrib.directory.serializers import EntryRepresentationSerializer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConsolidatedInvoiceSerializer(serializers.Serializer):
|
|
16
|
+
id = serializers.PrimaryKeyCharField()
|
|
17
|
+
reference_date = serializers.DateField()
|
|
18
|
+
invoice = serializers.PrimaryKeyRelatedField(allow_null=True)
|
|
19
|
+
_invoice = InvoiceRepresentationSerializer(source="invoice")
|
|
20
|
+
invoice_currency = serializers.PrimaryKeyRelatedField(allow_null=True)
|
|
21
|
+
_invoice_currency = CurrencyRepresentationSerializer(source="invoice_currency")
|
|
22
|
+
counterparty = serializers.PrimaryKeyRelatedField(allow_null=True)
|
|
23
|
+
_counterparty = EntryRepresentationSerializer(source="counterparty")
|
|
24
|
+
_group_key = serializers.CharField()
|
|
25
|
+
type = serializers.PrimaryKeyRelatedField(allow_null=True)
|
|
26
|
+
_type = InvoiceTypeRepresentationSerializer(source="type")
|
|
27
|
+
booking_entries = serializers.PrimaryKeyRelatedField(allow_null=True)
|
|
28
|
+
_booking_entries = BookingEntryRepresentationSerializer(source="booking_entries")
|
|
29
|
+
group = serializers.CharField()
|
|
30
|
+
currency_symbol = serializers.CharField()
|
|
31
|
+
value = serializers.DecimalField(
|
|
32
|
+
max_digits=15,
|
|
33
|
+
decimal_places=2,
|
|
34
|
+
decorators=[serializers.decorator(decorator_type="text", position="left", value="{{currency_symbol}}")],
|
|
35
|
+
)
|
|
36
|
+
casted_endpoint = serializers.SerializerMethodField()
|
|
37
|
+
depth = serializers.IntegerField()
|
|
38
|
+
num_draft = serializers.IntegerField()
|
|
39
|
+
num_submitted = serializers.IntegerField()
|
|
40
|
+
num_sent = serializers.IntegerField()
|
|
41
|
+
num_paid = serializers.IntegerField()
|
|
42
|
+
|
|
43
|
+
@serializers.register_resource()
|
|
44
|
+
def register_buttons(self, instance: dict, request, user: User) -> dict[str, str]:
|
|
45
|
+
button_dict = {}
|
|
46
|
+
|
|
47
|
+
if instance["depth"] == 5:
|
|
48
|
+
return button_dict
|
|
49
|
+
|
|
50
|
+
if instance.get("num_draft", 0) > 0:
|
|
51
|
+
button_dict["submit"] = reverse(
|
|
52
|
+
"wbaccounting:consolidated-invoice-submit", args=[instance["id"]], request=request
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if instance.get("num_submitted", 0) > 0 and user.has_perm("wbaccounting.administrate_invoice"):
|
|
56
|
+
button_dict["approve"] = reverse(
|
|
57
|
+
"wbaccounting:consolidated-invoice-approve", args=[instance["id"]], request=request
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if instance.get("num_sent", 0) > 0 and user.has_perm("wbaccounting.administrate_invoice"):
|
|
61
|
+
button_dict["pay"] = reverse(
|
|
62
|
+
"wbaccounting:consolidated-invoice-pay", args=[instance["id"]], request=request
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
for key in button_dict.keys():
|
|
66
|
+
button_dict[key] = button_dict[key] + "?" + urlencode(dict(request.GET.items()))
|
|
67
|
+
return button_dict
|
|
68
|
+
|
|
69
|
+
def get_casted_endpoint(self, obj: dict) -> str | None:
|
|
70
|
+
if obj.get("depth", 0) == 4:
|
|
71
|
+
return reverse(
|
|
72
|
+
f"{Invoice.get_endpoint_basename()}-detail",
|
|
73
|
+
args=[obj["id"]],
|
|
74
|
+
request=self.context.get("request", None),
|
|
75
|
+
)
|
|
76
|
+
elif obj.get("depth", 0) == 5:
|
|
77
|
+
return reverse(
|
|
78
|
+
f"{BookingEntry.get_endpoint_basename()}-detail",
|
|
79
|
+
args=[obj["id"]],
|
|
80
|
+
request=self.context.get("request", None),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
class Meta:
|
|
84
|
+
fields = read_only_fields = (
|
|
85
|
+
"id",
|
|
86
|
+
"reference_date",
|
|
87
|
+
"invoice",
|
|
88
|
+
"_invoice",
|
|
89
|
+
"invoice_currency",
|
|
90
|
+
"_invoice_currency",
|
|
91
|
+
"counterparty",
|
|
92
|
+
"_counterparty",
|
|
93
|
+
"booking_entries",
|
|
94
|
+
"_booking_entries",
|
|
95
|
+
"group",
|
|
96
|
+
"value",
|
|
97
|
+
"_additional_resources",
|
|
98
|
+
"type",
|
|
99
|
+
"_type",
|
|
100
|
+
"casted_endpoint",
|
|
101
|
+
"currency_symbol",
|
|
102
|
+
"_buttons",
|
|
103
|
+
"_group_key",
|
|
104
|
+
"depth",
|
|
105
|
+
"num_draft",
|
|
106
|
+
"num_submitted",
|
|
107
|
+
"num_sent",
|
|
108
|
+
"num_paid",
|
|
109
|
+
)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from django.db.models import Q
|
|
4
|
+
from django.dispatch import receiver
|
|
5
|
+
from rest_framework.exceptions import ValidationError
|
|
6
|
+
from rest_framework.reverse import reverse
|
|
7
|
+
from wbaccounting.generators.base import get_all_booking_entry_choices
|
|
8
|
+
from wbaccounting.models import EntryAccountingInformation
|
|
9
|
+
from wbaccounting.serializers import InvoiceTypeRepresentationSerializer
|
|
10
|
+
from wbcore import serializers
|
|
11
|
+
from wbcore.contrib.authentication.models import User
|
|
12
|
+
from wbcore.contrib.authentication.serializers import UserRepresentationSerializer
|
|
13
|
+
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
14
|
+
from wbcore.contrib.directory.serializers import (
|
|
15
|
+
CompanyModelSerializer,
|
|
16
|
+
EmailContactRepresentationSerializer,
|
|
17
|
+
EntryModelSerializer,
|
|
18
|
+
EntryRepresentationSerializer,
|
|
19
|
+
PersonModelSerializer,
|
|
20
|
+
)
|
|
21
|
+
from wbcore.signals import add_instance_additional_resource
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EntryAccountingInformationRepresentationSerializer(serializers.RepresentationSerializer):
|
|
25
|
+
entry_repr = serializers.CharField(source="entry.computed_str", read_only=True)
|
|
26
|
+
|
|
27
|
+
class Meta:
|
|
28
|
+
model = EntryAccountingInformation
|
|
29
|
+
fields = (
|
|
30
|
+
"id",
|
|
31
|
+
"entry_repr",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EntryAccountingInformationModelSerializer(serializers.ModelSerializer):
|
|
36
|
+
_entry = EntryRepresentationSerializer(source="entry")
|
|
37
|
+
_default_currency = CurrencyRepresentationSerializer(source="default_currency")
|
|
38
|
+
_default_invoice_type = InvoiceTypeRepresentationSerializer(source="default_invoice_type")
|
|
39
|
+
vat = serializers.DecimalField(percent=True, required=False, max_digits=6, decimal_places=4, default=Decimal(0))
|
|
40
|
+
_exempt_users = UserRepresentationSerializer(source="exempt_users", many=True)
|
|
41
|
+
|
|
42
|
+
_email_to = EmailContactRepresentationSerializer(source="email_to", many=True, ignore_filter=True)
|
|
43
|
+
_email_cc = EmailContactRepresentationSerializer(source="email_cc", many=True, ignore_filter=True)
|
|
44
|
+
_email_bcc = EmailContactRepresentationSerializer(source="email_bcc", many=True, ignore_filter=True)
|
|
45
|
+
booking_entry_generator = serializers.ChoiceField(
|
|
46
|
+
choices=list(get_all_booking_entry_choices()), required=False, allow_null=True
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_external_invoice_users = UserRepresentationSerializer(source="external_invoice_users", many=True)
|
|
50
|
+
|
|
51
|
+
@serializers.register_only_instance_resource()
|
|
52
|
+
def generate(self, instance, request, user, **kwargs):
|
|
53
|
+
generators = {}
|
|
54
|
+
|
|
55
|
+
if instance.booking_entry_generator and (
|
|
56
|
+
user.is_superuser or user.has_perm("wbaccounting.can_generate_booking_entries")
|
|
57
|
+
):
|
|
58
|
+
generators["generate_booking_entries"] = reverse(
|
|
59
|
+
"wbaccounting:entryaccountinginformation-generate-booking-entries",
|
|
60
|
+
args=[instance.id],
|
|
61
|
+
request=request,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if user.is_superuser or user.has_perm("wbaccounting.can_generate_invoice"):
|
|
65
|
+
if instance.entry.booking_entries.filter(Q(invoice__isnull=True) & Q(payment_date__isnull=True)).exists():
|
|
66
|
+
generators["invoice_booking_entries"] = reverse(
|
|
67
|
+
"wbaccounting:entryaccountinginformation-invoice-booking-entries",
|
|
68
|
+
args=[instance.id],
|
|
69
|
+
request=request,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return generators
|
|
73
|
+
|
|
74
|
+
@serializers.register_only_instance_resource()
|
|
75
|
+
def inline_lists(self, instance, request, user, **kwargs):
|
|
76
|
+
return {
|
|
77
|
+
"invoices": reverse(
|
|
78
|
+
"wbaccounting:entryaccountinginformation-invoice-list",
|
|
79
|
+
args=[instance.id],
|
|
80
|
+
request=request,
|
|
81
|
+
),
|
|
82
|
+
"bookingentries": reverse(
|
|
83
|
+
"wbaccounting:entryaccountinginformation-bookingentry-list",
|
|
84
|
+
args=[instance.id],
|
|
85
|
+
request=request,
|
|
86
|
+
),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class Meta:
|
|
90
|
+
model = EntryAccountingInformation
|
|
91
|
+
|
|
92
|
+
percent_fields = ["vat"]
|
|
93
|
+
fields = (
|
|
94
|
+
"id",
|
|
95
|
+
"entry",
|
|
96
|
+
"_entry",
|
|
97
|
+
"tax_id",
|
|
98
|
+
"vat",
|
|
99
|
+
"default_currency",
|
|
100
|
+
"_default_currency",
|
|
101
|
+
"default_invoice_type",
|
|
102
|
+
"_default_invoice_type",
|
|
103
|
+
"email_to",
|
|
104
|
+
"email_cc",
|
|
105
|
+
"email_bcc",
|
|
106
|
+
"_email_to",
|
|
107
|
+
"_email_cc",
|
|
108
|
+
"_email_bcc",
|
|
109
|
+
"email_subject",
|
|
110
|
+
"email_body",
|
|
111
|
+
"send_mail",
|
|
112
|
+
"counterparty_is_private",
|
|
113
|
+
"exempt_users",
|
|
114
|
+
"_exempt_users",
|
|
115
|
+
"booking_entry_generator",
|
|
116
|
+
"external_invoice_users",
|
|
117
|
+
"_external_invoice_users",
|
|
118
|
+
"_additional_resources",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def validate(self, data: dict) -> dict:
|
|
122
|
+
counterparty_is_private: bool | None = data.get(
|
|
123
|
+
"counterparty_is_private", self.instance.counterparty_is_private if self.instance else None
|
|
124
|
+
)
|
|
125
|
+
exempt_users: list[User] | None = data.get(
|
|
126
|
+
"exempt_users", list(self.instance.exempt_users.all()) if self.instance else None
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if exempt_users and not counterparty_is_private:
|
|
130
|
+
raise ValidationError({"exempt_users": "You can only select exempt users for private counterparties."})
|
|
131
|
+
|
|
132
|
+
return super().validate(data)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@receiver(add_instance_additional_resource, sender=CompanyModelSerializer)
|
|
136
|
+
@receiver(add_instance_additional_resource, sender=PersonModelSerializer)
|
|
137
|
+
@receiver(add_instance_additional_resource, sender=EntryModelSerializer)
|
|
138
|
+
def entry_adding_additional_resource(sender, serializer, instance, request, user, **kwargs):
|
|
139
|
+
if hasattr(instance, "entry_accounting_information"):
|
|
140
|
+
entry_accounting_information = instance.entry_accounting_information
|
|
141
|
+
if entry_accounting_information:
|
|
142
|
+
return {
|
|
143
|
+
"accounting-information": reverse(
|
|
144
|
+
"wbaccounting:entryaccountinginformation-detail",
|
|
145
|
+
args=[entry_accounting_information.id],
|
|
146
|
+
request=request,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
return {}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from django.contrib.contenttypes.models import ContentType
|
|
2
|
+
from django.utils.http import urlencode
|
|
3
|
+
from rest_framework.reverse import reverse
|
|
4
|
+
from wbaccounting.models import Invoice, InvoiceType
|
|
5
|
+
from wbaccounting.serializers import InvoiceTypeRepresentationSerializer
|
|
6
|
+
from wbcore import serializers
|
|
7
|
+
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
8
|
+
from wbcore.contrib.directory.serializers import EntryRepresentationSerializer
|
|
9
|
+
from wbcore.metadata.configs.buttons import WidgetButton
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvoiceRepresentationSerializer(serializers.RepresentationSerializer):
|
|
13
|
+
_detail = serializers.HyperlinkField(reverse_name="wbaccounting:invoice-detail")
|
|
14
|
+
|
|
15
|
+
class Meta:
|
|
16
|
+
model = Invoice
|
|
17
|
+
fields = (
|
|
18
|
+
"id",
|
|
19
|
+
"title",
|
|
20
|
+
"invoice_date",
|
|
21
|
+
"_detail",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InvoiceModelSerializer(serializers.ModelSerializer):
|
|
26
|
+
_counterparty = EntryRepresentationSerializer(source="counterparty")
|
|
27
|
+
_invoice_currency = CurrencyRepresentationSerializer(source="invoice_currency")
|
|
28
|
+
_invoice_type = InvoiceTypeRepresentationSerializer(source="invoice_type")
|
|
29
|
+
invoice_type = serializers.PrimaryKeyRelatedField(label="Type", required=True, queryset=InvoiceType.objects.all())
|
|
30
|
+
|
|
31
|
+
net_value = serializers.DecimalField(max_digits=16, decimal_places=2, label="Net Value", read_only=True)
|
|
32
|
+
gross_value = serializers.DecimalField(max_digits=16, decimal_places=2, label="Gross Value", read_only=True)
|
|
33
|
+
|
|
34
|
+
@serializers.register_resource()
|
|
35
|
+
def bookingentries(self, instance, request, user):
|
|
36
|
+
# Do some something (checks, etc.)
|
|
37
|
+
return {
|
|
38
|
+
"bookingentries": reverse("wbaccounting:invoice-bookingentry-list", args=[instance.id], request=request)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@serializers.register_resource()
|
|
42
|
+
def invoice_file(self, instance, request, user):
|
|
43
|
+
return {
|
|
44
|
+
"invoice_file": f'{reverse("wbcore:documents:document-urlredirect", args=[], request=request)}?content_type={ContentType.objects.get_for_model(Invoice).id}&object_id={instance.id}'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@serializers.register_dynamic_button()
|
|
48
|
+
def dynamic_buttons(self, instance, request, user):
|
|
49
|
+
buttons = []
|
|
50
|
+
|
|
51
|
+
if backlinks := instance.backlinks:
|
|
52
|
+
for _, backlink in backlinks.items():
|
|
53
|
+
buttons.append(
|
|
54
|
+
WidgetButton(
|
|
55
|
+
label=backlink["title"],
|
|
56
|
+
endpoint=f'{reverse(backlink["reverse"], request=request)}?{urlencode(backlink["parameters"])}',
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return buttons
|
|
61
|
+
|
|
62
|
+
class Meta:
|
|
63
|
+
model = Invoice
|
|
64
|
+
decorators = {
|
|
65
|
+
"net_value": serializers.decorator(
|
|
66
|
+
decorator_type="text", position="left", value="{{_invoice_currency.symbol}}"
|
|
67
|
+
),
|
|
68
|
+
"gross_value": serializers.decorator(
|
|
69
|
+
decorator_type="text", position="left", value="{{_invoice_currency.symbol}}"
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
fields = (
|
|
73
|
+
"id",
|
|
74
|
+
"status",
|
|
75
|
+
"reference_date",
|
|
76
|
+
"resolved",
|
|
77
|
+
"title",
|
|
78
|
+
"invoice_date",
|
|
79
|
+
"invoice_currency",
|
|
80
|
+
"_invoice_currency",
|
|
81
|
+
"counterparty",
|
|
82
|
+
"_counterparty",
|
|
83
|
+
"text_above",
|
|
84
|
+
"text_below",
|
|
85
|
+
"_additional_resources",
|
|
86
|
+
"gross_value",
|
|
87
|
+
"net_value",
|
|
88
|
+
"invoice_type",
|
|
89
|
+
"_invoice_type",
|
|
90
|
+
"_buttons",
|
|
91
|
+
)
|
|
92
|
+
read_only_fields = (
|
|
93
|
+
"gross_value",
|
|
94
|
+
"net_value",
|
|
95
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from wbaccounting.models import InvoiceType
|
|
2
|
+
from wbcore import serializers
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InvoiceTypeModelSerializer(serializers.ModelSerializer):
|
|
6
|
+
class Meta:
|
|
7
|
+
model = InvoiceType
|
|
8
|
+
fields = ("id", "name", "processor")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InvoiceTypeRepresentationSerializer(serializers.RepresentationSerializer):
|
|
12
|
+
_detail = serializers.HyperlinkField(reverse_name="wbaccounting:invoicetype-detail")
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
model = InvoiceType
|
|
16
|
+
fields = ("id", "name", "_detail")
|