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,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}}"
@@ -0,0 +1,6 @@
1
+ from rest_framework.permissions import BasePermission
2
+
3
+
4
+ class IsInvoiceAdmin(BasePermission):
5
+ def has_permission(self, request, view):
6
+ return request.user.has_perm("wbaccounting.administrate_invoice")
File without changes
@@ -0,0 +1,5 @@
1
+ from wbaccounting.models import Invoice
2
+
3
+
4
+ def processor(invoice: Invoice):
5
+ print(f"I am a dummy processor that processes the Invoice: {invoice}") # noqa
@@ -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")