wbaccounting 1.60.1__tar.gz → 1.61.1__tar.gz

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 (103) hide show
  1. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/PKG-INFO +1 -1
  2. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/admin/__init__.py +1 -1
  3. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/admin/entry_accounting_information.py +15 -1
  4. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/factories/entry_accounting_information.py +21 -3
  5. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/generators/base.py +14 -5
  6. wbaccounting-1.61.1/wbaccounting/migrations/0013_vat_alter_entryaccountinginformation_vat_vatrate.py +84 -0
  7. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/models/__init__.py +1 -1
  8. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/models/entry_accounting_information.py +72 -4
  9. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/models/model_tasks.py +4 -2
  10. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/serializers/__init__.py +1 -0
  11. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/serializers/entry_accounting_information.py +9 -5
  12. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/conftest.py +2 -0
  13. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_models/test_entry_accounting_information.py +24 -0
  14. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_serializers/test_entry_accounting_information.py +4 -2
  15. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/urls.py +2 -0
  16. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/entry_accounting_information.py +10 -3
  17. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/.gitignore +0 -0
  18. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/pyproject.toml +0 -0
  19. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/__init__.py +0 -0
  20. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/admin/booking_entry.py +0 -0
  21. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/admin/invoice.py +0 -0
  22. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/admin/invoice_type.py +0 -0
  23. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/admin/transactions.py +0 -0
  24. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/apps.py +0 -0
  25. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/dynamic_preferences_registry.py +0 -0
  26. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/factories/__init__.py +0 -0
  27. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/factories/booking_entry.py +0 -0
  28. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/factories/invoice.py +0 -0
  29. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/factories/transactions.py +0 -0
  30. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/files/__init__.py +0 -0
  31. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/files/invoice_document_file.py +0 -0
  32. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/files/utils.py +0 -0
  33. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/generators/__init__.py +0 -0
  34. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/io/handlers/__init__.py +0 -0
  35. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/io/handlers/transactions.py +0 -0
  36. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/io/parsers/__init__.py +0 -0
  37. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/io/parsers/societe_generale_lux.py +0 -0
  38. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/io/parsers/societe_generale_lux_prenotification.py +0 -0
  39. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/migrations/0001_initial_squashed_squashed_0005_alter_bookingentry_counterparty_and_more.py +0 -0
  40. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/migrations/0006_alter_invoice_status.py +0 -0
  41. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/migrations/0007_alter_invoice_options.py +0 -0
  42. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/migrations/0008_alter_invoice_options.py +0 -0
  43. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/migrations/0009_invoicetype_alter_bookingentry_options_and_more.py +0 -0
  44. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/migrations/0010_alter_bookingentry_options.py +0 -0
  45. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/migrations/0011_transaction.py +0 -0
  46. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/migrations/0012_entryaccountinginformation_external_invoice_users.py +0 -0
  47. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/migrations/__init__.py +0 -0
  48. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/models/booking_entry.py +0 -0
  49. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/models/invoice.py +0 -0
  50. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/models/invoice_type.py +0 -0
  51. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/models/transactions.py +0 -0
  52. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/permissions.py +0 -0
  53. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/processors/__init__.py +0 -0
  54. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/processors/dummy_processor.py +0 -0
  55. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/serializers/booking_entry.py +0 -0
  56. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/serializers/consolidated_invoice.py +0 -0
  57. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/serializers/invoice.py +0 -0
  58. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/serializers/invoice_type.py +0 -0
  59. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/serializers/transactions.py +0 -0
  60. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/__init__.py +0 -0
  61. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_displays/__init__.py +0 -0
  62. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_displays/test_booking_entries.py +0 -0
  63. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_models/__init__.py +0 -0
  64. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_models/test_booking_entries.py +0 -0
  65. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_models/test_invoice_types.py +0 -0
  66. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_models/test_invoices.py +0 -0
  67. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_models/test_transactions.py +0 -0
  68. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_processors.py +0 -0
  69. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_serializers/__init__.py +0 -0
  70. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_serializers/test_booking_entries.py +0 -0
  71. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_serializers/test_invoice_types.py +0 -0
  72. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/tests/test_serializers/test_transactions.py +0 -0
  73. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/__init__.py +0 -0
  74. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/booking_entry.py +0 -0
  75. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/buttons/__init__.py +0 -0
  76. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/buttons/booking_entry.py +0 -0
  77. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/buttons/entry_accounting_information.py +0 -0
  78. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/buttons/invoice.py +0 -0
  79. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/cashflows.py +0 -0
  80. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/display/__init__.py +0 -0
  81. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/display/booking_entry.py +0 -0
  82. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/display/cashflows.py +0 -0
  83. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/display/entry_accounting_information.py +0 -0
  84. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/display/invoice.py +0 -0
  85. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/display/invoice_type.py +0 -0
  86. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/display/transactions.py +0 -0
  87. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/endpoints/__init__.py +0 -0
  88. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/endpoints/invoice.py +0 -0
  89. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/invoice.py +0 -0
  90. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/invoice_type.py +0 -0
  91. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/menu/__init__.py +0 -0
  92. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/menu/booking_entry.py +0 -0
  93. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/menu/cashflows.py +0 -0
  94. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/menu/entry_accounting_information.py +0 -0
  95. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/menu/invoice.py +0 -0
  96. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/menu/invoice_type.py +0 -0
  97. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/menu/transactions.py +0 -0
  98. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/titles/__init__.py +0 -0
  99. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/titles/booking_entry.py +0 -0
  100. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/titles/entry_accounting_information.py +0 -0
  101. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/titles/invoice.py +0 -0
  102. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/titles/invoice_type.py +0 -0
  103. {wbaccounting-1.60.1 → wbaccounting-1.61.1}/wbaccounting/viewsets/transactions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbaccounting
3
- Version: 1.60.1
3
+ Version: 1.61.1
4
4
  Summary: A workbench module for managing invoicing and simple accounting.
5
5
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
6
6
  Requires-Dist: wbcore
@@ -1,5 +1,5 @@
1
1
  from .booking_entry import BookingEntryInline, BookingEntryModelAdmin
2
- from .entry_accounting_information import EntryAccountingInformationModelAdmin
2
+ from .entry_accounting_information import EntryAccountingInformationModelAdmin, VatModelAdmin
3
3
  from .invoice_type import InvoiceTypeModelAdmin
4
4
  from .invoice import InvoiceModelAdmin
5
5
  from .transactions import TransactionModelAdmin
@@ -1,6 +1,20 @@
1
1
  from django.contrib import admin
2
2
 
3
- from wbaccounting.models import EntryAccountingInformation
3
+ from wbaccounting.models import EntryAccountingInformation, Vat, VatRate
4
+
5
+
6
+ class VarRateInline(admin.TabularInline):
7
+ model = VatRate
8
+ fk_name = "vat"
9
+ fields = ("rate", "timespan")
10
+
11
+
12
+ @admin.register(Vat)
13
+ class VatModelAdmin(admin.ModelAdmin):
14
+ list_display = ("name",)
15
+ search_fields = ["name"]
16
+
17
+ inlines = [VarRateInline]
4
18
 
5
19
 
6
20
  @admin.register(EntryAccountingInformation)
@@ -1,8 +1,26 @@
1
+ from datetime import date
2
+
1
3
  import factory
2
- from factory.fuzzy import FuzzyDecimal
4
+ from psycopg.types.range import DateRange
3
5
  from wbcore.contrib.directory.factories import CompanyFactory, EmailContactFactory
4
6
 
5
- from wbaccounting.models import EntryAccountingInformation
7
+ from wbaccounting.models import EntryAccountingInformation, Vat, VatRate
8
+
9
+
10
+ class VatFactory(factory.django.DjangoModelFactory):
11
+ name = "Default VAT"
12
+ primary = True
13
+
14
+ class Meta:
15
+ model = Vat
16
+ django_get_or_create = ("primary",)
17
+
18
+ @factory.post_generation
19
+ def initiate_rate(self, create, extracted, **kwargs):
20
+ if not create:
21
+ return
22
+ if not self.rates.exists():
23
+ VatRate.objects.create(vat=self, timespan=DateRange(date(2010, 1, 1), None), rate=0.08)
6
24
 
7
25
 
8
26
  class EntryAccountingInformationFactory(factory.django.DjangoModelFactory):
@@ -12,7 +30,7 @@ class EntryAccountingInformationFactory(factory.django.DjangoModelFactory):
12
30
 
13
31
  entry = factory.SubFactory("wbcore.contrib.directory.factories.EntryFactory")
14
32
  tax_id = factory.Faker("text", max_nb_chars=64)
15
- vat = FuzzyDecimal(0, 0.9, 4)
33
+ vat = factory.SubFactory(VatFactory)
16
34
  send_mail = factory.Faker("pybool")
17
35
  counterparty_is_private = False
18
36
 
@@ -51,7 +51,9 @@ class AbstractBookingEntryGenerator(abc.ABC):
51
51
 
52
52
  @staticmethod
53
53
  @abc.abstractmethod
54
- def generate_booking_entries(from_date: date, to_date: date, counterparty: Entry) -> Iterable[BookingEntry]:
54
+ def generate_booking_entries(
55
+ from_date: date, to_date: date, counterparty: Entry, booking_date: date
56
+ ) -> Iterable[BookingEntry]:
55
57
  """
56
58
  Generates a sequence of booking entries for a specified date range and counterparty.
57
59
 
@@ -62,6 +64,7 @@ class AbstractBookingEntryGenerator(abc.ABC):
62
64
  from_date (date): The start date of the period for which booking entries are to be generated.
63
65
  to_date (date): The end date of the period for which booking entries are to be generated.
64
66
  counterparty (Entry): The counterparty associated with the booking entries to be generated.
67
+ booking_date (date): The booking entry date
65
68
 
66
69
  Returns:
67
70
  Iterable[BookingEntry]: A sequence of BookingEntry instances generated for the specified
@@ -97,9 +100,13 @@ class AbstractBookingEntryGenerator(abc.ABC):
97
100
 
98
101
 
99
102
  def generate_booking_entries(
100
- _class: type[AbstractBookingEntryGenerator], from_date: date, to_date: date, counterparty: Entry
103
+ _class: type[AbstractBookingEntryGenerator],
104
+ from_date: date,
105
+ to_date: date,
106
+ counterparty: Entry,
107
+ booking_date: date,
101
108
  ):
102
- booking_entries = _class.generate_booking_entries(from_date, to_date, counterparty)
109
+ booking_entries = _class.generate_booking_entries(from_date, to_date, counterparty, booking_date)
103
110
  for booking_entry in booking_entries:
104
111
  booking_entry.save()
105
112
 
@@ -108,11 +115,13 @@ class TestGenerator(AbstractBookingEntryGenerator):
108
115
  TITLE = "Test Generator"
109
116
 
110
117
  @staticmethod
111
- def generate_booking_entries(from_date: date, to_date: date, counterparty: Entry) -> Iterable[BookingEntry]:
118
+ def generate_booking_entries(
119
+ from_date: date, to_date: date, counterparty: Entry, booking_date: date
120
+ ) -> Iterable[BookingEntry]:
112
121
  yield BookingEntry(
113
122
  title="Test Booking Entry",
114
123
  counterparty=counterparty,
115
- booking_date=date.today(),
124
+ booking_date=booking_date,
116
125
  reference_date=date.today(),
117
126
  net_value=100,
118
127
  currency=Currency.objects.first(),
@@ -0,0 +1,84 @@
1
+ # Generated by Django 5.2.9 on 2026-01-21 12:55
2
+ from datetime import date
3
+
4
+ import django.contrib.postgres.constraints
5
+ import django.contrib.postgres.fields.ranges
6
+ import django.db.models.deletion
7
+ from django.db import migrations, models
8
+ from psycopg.types.range import DateRange
9
+
10
+ def migrate_vat(apps, schema_editor):
11
+ EntryAccountingInformation = apps.get_model('wbaccounting', 'EntryAccountingInformation')
12
+ Vat = apps.get_model('wbaccounting', 'Vat')
13
+ VatRate = apps.get_model('wbaccounting', 'VatRate')
14
+ objs = []
15
+ default_vat = Vat.objects.create(name=str(0.08), primary=True)
16
+ VatRate.objects.create(rate=0.08, vat=default_vat, timespan=DateRange(date(2010, 1, 1), None))
17
+ for entry in EntryAccountingInformation.objects.all():
18
+ if entry.old_vat:
19
+ try:
20
+ rate = VatRate.objects.get(rate=entry.old_vat)
21
+ entry.vat = rate.vat
22
+ except VatRate.DoesNotExist:
23
+ vat = Vat.objects.create(name=str(entry.old_vat))
24
+ rate = VatRate.objects.create(rate=entry.old_vat, vat=vat, timespan=DateRange(date(2010,1,1), None))
25
+ entry.vat = rate.vat
26
+ else:
27
+ entry.vat = default_vat
28
+ objs.append(entry)
29
+ EntryAccountingInformation.objects.bulk_update(objs, ["vat"])
30
+
31
+ class Migration(migrations.Migration):
32
+
33
+ dependencies = [
34
+ ('wbaccounting', '0012_entryaccountinginformation_external_invoice_users'),
35
+ ]
36
+
37
+ operations = [
38
+ migrations.CreateModel(
39
+ name='Vat',
40
+ fields=[
41
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
42
+ ('name', models.CharField(max_length=255)),
43
+ ("primary", models.BooleanField(default=False, verbose_name='Primary'))
44
+ ],
45
+ ),
46
+ migrations.RenameField(model_name='entryaccountinginformation', old_name="vat", new_name="old_vat",),
47
+ migrations.AddField(
48
+ model_name='entryaccountinginformation',
49
+ name='vat',
50
+ field=models.ForeignKey(default=None, blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vat_recipients', to='wbaccounting.vat', verbose_name='VAT'),
51
+ preserve_default=False,
52
+ ),
53
+ migrations.CreateModel(
54
+ name='VatRate',
55
+ fields=[
56
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
57
+ ('timespan', django.contrib.postgres.fields.ranges.DateRangeField(verbose_name='Timespan')),
58
+ ('rate', models.DecimalField(decimal_places=4, max_digits=5)),
59
+ ('vat', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rates', to='wbaccounting.vat')),
60
+ ],
61
+ options={
62
+ 'verbose_name': 'Vat Rate',
63
+ 'verbose_name_plural': 'Vat Rates',
64
+ 'constraints': [django.contrib.postgres.constraints.ExclusionConstraint(expressions=[('timespan', '&&'), ('vat', '=')], name='exclude_overlapping_vat_rates')],
65
+ },
66
+ ),
67
+ migrations.RunPython(migrate_vat),
68
+ migrations.RemoveField(model_name='entryaccountinginformation', name="old_vat"),
69
+ migrations.AlterField(
70
+ model_name='entryaccountinginformation',
71
+ name='vat',
72
+ field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT,
73
+ related_name='vat_recipients', to='wbaccounting.vat', verbose_name='VAT'),
74
+ preserve_default=False,
75
+ ),
76
+ migrations.AlterModelOptions(
77
+ name='vat',
78
+ options={'verbose_name': 'Vat Group', 'verbose_name_plural': 'Vat Groups'},
79
+ ),
80
+ migrations.AddConstraint(
81
+ model_name='vat',
82
+ constraint=models.UniqueConstraint(fields=('primary',), name='unique_primary_vat_group'),
83
+ ),
84
+ ]
@@ -1,6 +1,6 @@
1
1
  from .booking_entry import BookingEntry
2
2
  from .invoice import Invoice
3
3
  from .invoice_type import InvoiceType
4
- from .entry_accounting_information import EntryAccountingInformation
4
+ from .entry_accounting_information import EntryAccountingInformation, Vat, VatRate
5
5
  from .transactions import Transaction
6
6
  from .model_tasks import submit_invoices_as_task
@@ -1,8 +1,11 @@
1
1
  from contextlib import suppress
2
2
  from datetime import date
3
+ from decimal import Decimal
3
4
 
5
+ from django.contrib.postgres.constraints import ExclusionConstraint
6
+ from django.contrib.postgres.fields import DateRangeField, RangeOperators
4
7
  from django.db import models
5
- from django.db.models import Q, QuerySet
8
+ from django.db.models import Q, QuerySet, UniqueConstraint
6
9
  from django.db.models.signals import post_save
7
10
  from django.dispatch import receiver
8
11
  from django.utils.module_loading import import_string
@@ -10,6 +13,7 @@ from dynamic_preferences.registries import global_preferences_registry as gpr
10
13
  from wbcore.contrib.authentication.models import User
11
14
  from wbcore.contrib.currency.models import Currency
12
15
  from wbcore.signals import pre_merge
16
+ from wbcore.utils.models import PrimaryMixin
13
17
 
14
18
  from wbaccounting.models.model_tasks import generate_booking_entries_as_task
15
19
 
@@ -47,6 +51,63 @@ class EntryAccountingInformationDefaultQuerySet(QuerySet):
47
51
  return self.filter(Q(counterparty_is_private=False) | Q(exempt_users=user))
48
52
 
49
53
 
54
+ class Vat(PrimaryMixin, models.Model):
55
+ name = models.CharField(max_length=255)
56
+
57
+ def get_rate(self, date: date) -> Decimal:
58
+ try:
59
+ return self.rates.get(timespan__contains=date).rate
60
+ except VatRate.DoesNotExist:
61
+ return Decimal("0.0")
62
+
63
+ def __str__(self) -> str:
64
+ return self.name
65
+
66
+ class Meta:
67
+ verbose_name = "Vat Group"
68
+ verbose_name_plural = "Vat Groups"
69
+ constraints = [
70
+ UniqueConstraint(
71
+ name="unique_primary_vat_group",
72
+ fields=["primary"],
73
+ )
74
+ ]
75
+
76
+ @classmethod
77
+ def get_representation_endpoint(cls):
78
+ return "wbaccounting:vatrepresentation-list"
79
+
80
+ @classmethod
81
+ def get_representation_value_key(cls):
82
+ return "id"
83
+
84
+ @classmethod
85
+ def get_representation_label_key(cls):
86
+ return "{{name}}"
87
+
88
+
89
+ class VatRate(models.Model):
90
+ vat = models.ForeignKey("wbaccounting.Vat", related_name="rates", on_delete=models.CASCADE)
91
+ timespan = DateRangeField(verbose_name="Timespan")
92
+ rate = models.DecimalField(decimal_places=4, max_digits=5)
93
+
94
+ class Meta:
95
+ verbose_name = "Vat Rate"
96
+ verbose_name_plural = "Vat Rates"
97
+ constraints = [
98
+ ExclusionConstraint(
99
+ name="exclude_overlapping_vat_rates",
100
+ expressions=[
101
+ ("timespan", RangeOperators.OVERLAPS),
102
+ ("vat", RangeOperators.EQUAL),
103
+ ],
104
+ ),
105
+ ]
106
+
107
+ def __str__(self) -> str:
108
+ return f"{self.vat}: {self.rate}"
109
+
110
+
50
111
  class EntryAccountingInformation(models.Model):
51
112
  # Link to Entry
52
113
  entry = models.OneToOneField(
@@ -58,7 +119,9 @@ class EntryAccountingInformation(models.Model):
58
119
 
59
120
  # Tax Information
60
121
  tax_id = models.CharField(max_length=512, blank=True, null=True, verbose_name="Tax ID")
61
- vat = models.FloatField(blank=True, null=True, verbose_name="VAT")
122
+ vat = models.ForeignKey(
123
+ "wbaccounting.Vat", related_name="vat_recipients", on_delete=models.PROTECT, verbose_name="VAT"
124
+ )
62
125
 
63
126
  # Invoice Information
64
127
  default_currency = models.ForeignKey(
@@ -123,6 +186,11 @@ class EntryAccountingInformation(models.Model):
123
186
 
124
187
  objects = EntryAccountingInformationDefaultQuerySet.as_manager()
125
188
 
189
+ def save(self, *args, **kwargs):
190
+ if not self.vat:
191
+ self.vat = Vat.objects.get_or_create(primary=True, defaults={"name": "Default VAT group"})[0]
192
+ super().save(*args, **kwargs)
193
+
126
194
  class Meta:
127
195
  verbose_name = "Counterparty"
128
196
  verbose_name_plural = "Counterparties"
@@ -146,9 +214,9 @@ class EntryAccountingInformation(models.Model):
146
214
  def get_representation_label_key(cls):
147
215
  return "{{entry_repr}}"
148
216
 
149
- def generate_booking_entries(self, from_date: date, to_date: date):
217
+ def generate_booking_entries(self, from_date: date, to_date: date, booking_date: date):
150
218
  generate_booking_entries_as_task.delay( # type: ignore
151
- self.booking_entry_generator or "", from_date, to_date, self.entry.id
219
+ self.booking_entry_generator or "", from_date, to_date, self.entry.id, booking_date
152
220
  )
153
221
 
154
222
 
@@ -66,8 +66,10 @@ def refresh_invoice_document_as_task(invoice_id):
66
66
 
67
67
 
68
68
  @shared_task(queue=Queue.DEFAULT.value)
69
- def generate_booking_entries_as_task(func: str, from_date: date, to_date: date, counterparty_id: int):
69
+ def generate_booking_entries_as_task(
70
+ func: str, from_date: date, to_date: date, counterparty_id: int, booking_date: date
71
+ ):
70
72
  with suppress(ImportError):
71
73
  generator = import_string(func)
72
74
  counterparty = Entry.objects.get(id=counterparty_id)
73
- generate_booking_entries(generator, from_date, to_date, counterparty)
75
+ generate_booking_entries(generator, from_date, to_date, counterparty, booking_date)
@@ -2,6 +2,7 @@ from .invoice_type import InvoiceTypeModelSerializer, InvoiceTypeRepresentationS
2
2
  from .entry_accounting_information import (
3
3
  EntryAccountingInformationModelSerializer,
4
4
  EntryAccountingInformationRepresentationSerializer,
5
+ VatRepresentationSerializer,
5
6
  )
6
7
  from .invoice import InvoiceRepresentationSerializer, InvoiceModelSerializer
7
8
  from .booking_entry import (
@@ -1,5 +1,3 @@
1
- from decimal import Decimal
2
-
3
1
  from django.db.models import Q
4
2
  from django.dispatch import receiver
5
3
  from rest_framework.exceptions import ValidationError
@@ -18,10 +16,16 @@ from wbcore.contrib.directory.serializers import (
18
16
  from wbcore.signals import add_instance_additional_resource
19
17
 
20
18
  from wbaccounting.generators.base import get_all_booking_entry_choices
21
- from wbaccounting.models import EntryAccountingInformation
19
+ from wbaccounting.models import EntryAccountingInformation, Vat
22
20
  from wbaccounting.serializers import InvoiceTypeRepresentationSerializer
23
21
 
24
22
 
23
+ class VatRepresentationSerializer(serializers.RepresentationSerializer):
24
+ class Meta:
25
+ model = Vat
26
+ fields = ("id", "name", "primary")
27
+
28
+
25
29
  class EntryAccountingInformationRepresentationSerializer(serializers.RepresentationSerializer):
26
30
  entry_repr = serializers.CharField(source="entry.computed_str", read_only=True)
27
31
 
@@ -37,7 +41,7 @@ class EntryAccountingInformationModelSerializer(serializers.ModelSerializer):
37
41
  _entry = EntryRepresentationSerializer(source="entry")
38
42
  _default_currency = CurrencyRepresentationSerializer(source="default_currency")
39
43
  _default_invoice_type = InvoiceTypeRepresentationSerializer(source="default_invoice_type")
40
- vat = serializers.DecimalField(percent=True, required=False, max_digits=6, decimal_places=4, default=Decimal(0))
44
+ _vat = VatRepresentationSerializer(source="vat")
41
45
  _exempt_users = UserRepresentationSerializer(source="exempt_users", many=True)
42
46
 
43
47
  _email_to = EmailContactRepresentationSerializer(source="email_to", many=True, ignore_filter=True)
@@ -90,13 +94,13 @@ class EntryAccountingInformationModelSerializer(serializers.ModelSerializer):
90
94
  class Meta:
91
95
  model = EntryAccountingInformation
92
96
 
93
- percent_fields = ["vat"]
94
97
  fields = (
95
98
  "id",
96
99
  "entry",
97
100
  "_entry",
98
101
  "tax_id",
99
102
  "vat",
103
+ "_vat",
100
104
  "default_currency",
101
105
  "_default_currency",
102
106
  "default_invoice_type",
@@ -13,6 +13,7 @@ from wbaccounting.factories import (
13
13
  LocalCurrencyTransactionFactory,
14
14
  TransactionFactory,
15
15
  )
16
+ from wbaccounting.factories.entry_accounting_information import VatFactory
16
17
  from wbcore.contrib.authentication.factories import (
17
18
  SuperUserFactory,
18
19
  UserActivityFactory,
@@ -27,6 +28,7 @@ from wbcore.contrib.directory.factories import (
27
28
  from wbcore.contrib.geography.tests.signals import app_pre_migration
28
29
  from wbcore.tests.conftest import *
29
30
 
31
+ register(VatFactory)
30
32
  register(BookingEntryFactory)
31
33
  register(InvoiceFactory)
32
34
  register(InvoiceTypeFactory)
@@ -1,15 +1,39 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+
1
4
  import pytest
2
5
  from dynamic_preferences.registries import global_preferences_registry
6
+ from psycopg.types.range import DateRange
3
7
  from wbcore.contrib.authentication.models import User
4
8
  from wbcore.contrib.currency.models import Currency
5
9
 
6
10
  from wbaccounting.models import BookingEntry, EntryAccountingInformation
7
11
  from wbaccounting.models.entry_accounting_information import (
12
+ VatRate,
8
13
  default_currency,
9
14
  default_email_body,
10
15
  )
11
16
 
12
17
 
18
+ @pytest.mark.django_db
19
+ class TestVat:
20
+ def test_get_rate(self, vat):
21
+ current_rate = vat.rates.first()
22
+ assert vat.get_rate(date(2009, 12, 31)) == Decimal("0.0") # test exclusion
23
+ assert vat.get_rate(date(2010, 1, 1)) == Decimal("0.08") # test default factory data
24
+ assert vat.get_rate(date(2050, 1, 1)) == Decimal("0.08") # test default unbounded
25
+
26
+ # change the end date of the existing rate and create a new rate
27
+ new_pivot = date(2011, 1, 1)
28
+ current_rate.timespan = DateRange(current_rate.timespan.lower, new_pivot)
29
+ current_rate.save()
30
+ VatRate.objects.create(vat=vat, rate=0.1, timespan=DateRange(new_pivot, date(2012, 1, 1)))
31
+ assert vat.get_rate(date(2011, 1, 1)) == Decimal("0.1")
32
+ assert vat.get_rate(date(2012, 1, 1)) == Decimal("0") # excluded so default to the previous rate
33
+
34
+ assert vat.get_rate(date(2050, 1, 1)) == Decimal("0")
35
+
36
+
13
37
  @pytest.mark.django_db
14
38
  class TestEntryAccountingInformation:
15
39
  def test_str(self, entry_accounting_information: EntryAccountingInformation):
@@ -23,6 +23,7 @@ class TestEntryAccountingInformationModelSerializer:
23
23
  "_entry",
24
24
  "tax_id",
25
25
  "vat",
26
+ "_vat",
26
27
  "default_currency",
27
28
  "_default_currency",
28
29
  "default_invoice_type",
@@ -45,12 +46,13 @@ class TestEntryAccountingInformationModelSerializer:
45
46
  "_additional_resources",
46
47
  ) == tuple(serializer.data.keys()) # type: ignore
47
48
 
48
- def test_deserialize(self, entry_accounting_information_factory, entry, currency):
49
+ def test_deserialize(self, entry_accounting_information_factory, entry, currency, vat):
49
50
  data = factory.build(dict, FACTORY_CLASS=entry_accounting_information_factory)
50
51
  data["entry"] = entry.pk
52
+ data["vat"] = vat.id
51
53
  data["default_currency"] = currency.pk
52
54
  serializer = EntryAccountingInformationModelSerializer(data=data)
53
- assert serializer.is_valid()
55
+ assert serializer.is_valid(raise_exception=True)
54
56
 
55
57
 
56
58
  @pytest.mark.django_db
@@ -16,6 +16,7 @@ from wbaccounting.viewsets import (
16
16
  TransactionModelViewSet,
17
17
  TransactionRepresentationViewSet,
18
18
  )
19
+ from wbaccounting.viewsets.entry_accounting_information import VatRepresentationViewSet
19
20
 
20
21
  router = WBCoreRouter()
21
22
  router.register(r"bookingentry", BookingEntryModelViewSet, basename="bookingentry")
@@ -41,6 +42,7 @@ router.register(r"futurecashflow", FutureCashFlowPandasAPIViewSet, basename="fut
41
42
  router.register(
42
43
  r"futurecashflowtransaction", FutureCashFlowTransactionsPandasAPIViewSet, basename="futurecashflowtransaction"
43
44
  )
45
+ router.register(r"vatrepresentation", VatRepresentationViewSet, basename="vatrepresentation")
44
46
 
45
47
  entry_router = WBCoreRouter()
46
48
 
@@ -8,11 +8,12 @@ from rest_framework.response import Response
8
8
  from wbcore import viewsets
9
9
  from wbcore.utils.date import get_date_interval_from_request
10
10
 
11
- from wbaccounting.models import EntryAccountingInformation, Invoice
11
+ from wbaccounting.models import EntryAccountingInformation, Invoice, Vat
12
12
  from wbaccounting.models.booking_entry import BookingEntry
13
13
  from wbaccounting.serializers import (
14
14
  EntryAccountingInformationModelSerializer,
15
15
  EntryAccountingInformationRepresentationSerializer,
16
+ VatRepresentationSerializer,
16
17
  )
17
18
  from wbaccounting.viewsets.buttons import EntryAccountingInformationButtonConfig
18
19
  from wbaccounting.viewsets.display import EntryAccountingInformationDisplayConfig
@@ -21,6 +22,12 @@ from wbaccounting.viewsets.titles import EntryAccountingInformationTitleConfig
21
22
  from ..permissions import CanGenerateBookingEntry, CanGenerateInvoice
22
23
 
23
24
 
25
+ class VatRepresentationViewSet(viewsets.RepresentationViewSet):
26
+ search_fields = ("name",)
27
+ queryset = Vat.objects.all()
28
+ serializer_class = VatRepresentationSerializer
29
+
30
+
24
31
  class EntryAccountingInformationRepresentationViewSet(viewsets.RepresentationViewSet):
25
32
  search_fields = ("entry__computed_str",)
26
33
  queryset = EntryAccountingInformation.objects.all()
@@ -74,7 +81,7 @@ class EntryAccountingInformationModelViewSet(viewsets.ModelViewSet):
74
81
  eai = get_object_or_404(EntryAccountingInformation, id=pk)
75
82
  start, end = get_date_interval_from_request(request, request_type="POST")
76
83
  if start and end:
77
- eai.generate_booking_entries(start, end) # type: ignore
84
+ eai.generate_booking_entries(start, end, date.today()) # type: ignore
78
85
  return Response(
79
86
  {"__notification": {"title": "Booking Entries are being generated in the background."}},
80
87
  status=status.HTTP_200_OK,
@@ -90,7 +97,7 @@ class EntryAccountingInformationModelViewSet(viewsets.ModelViewSet):
90
97
  if counterparties := request.POST.get("counterparties", ""):
91
98
  eais = EntryAccountingInformation.objects.filter(entry__id__in=counterparties.split(","))
92
99
  for eai in eais:
93
- eai.generate_booking_entries(start, end) # type: ignore
100
+ eai.generate_booking_entries(start, end, date.today()) # type: ignore
94
101
  return Response(
95
102
  {"__notification": {"title": "Booking Entries are being generated in the background."}},
96
103
  status=status.HTTP_200_OK,
File without changes